From 8b149e85665c0d1cbe9874ca4e3aa21e222611be Mon Sep 17 00:00:00 2001 From: Baz Date: Wed, 19 Jan 2022 22:53:57 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20=20Source=20Shopify:=20implement?= =?UTF-8?q?=20Oauth2.0=20for=20Airbyte-Cloud=20(#9591)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../9da77001-af33-4bcd-be46-6252bf9342b9.json | 2 +- .../resources/seed/source_definitions.yaml | 2 +- .../src/main/resources/seed/source_specs.yaml | 76 ++++++++---- .../destination/bigquery/BigQueryUtils.java | 7 +- .../bigquery/BigQueryDestinationTest.java | 21 ++-- .../bigquery/BigQueryUtilsTest.java | 3 +- .../connectors/source-shopify/Dockerfile | 2 +- .../integration_tests/invalid_config.json | 2 +- .../invalid_oauth_config.json | 4 +- .../source-shopify/source_shopify/auth.py | 14 +-- .../source-shopify/source_shopify/spec.json | 78 +++++++++--- .../SnowflakeSource.java | 19 +-- .../oauth/OAuthImplementationFactory.java | 1 + .../airbyte/oauth/flows/ShopifyOAuthFlow.java | 116 ++++++++++++++++++ docs/integrations/sources/shopify.md | 1 + 15 files changed, 270 insertions(+), 78 deletions(-) create mode 100644 airbyte-oauth/src/main/java/io/airbyte/oauth/flows/ShopifyOAuthFlow.java diff --git a/airbyte-config/init/src/main/resources/config/STANDARD_SOURCE_DEFINITION/9da77001-af33-4bcd-be46-6252bf9342b9.json b/airbyte-config/init/src/main/resources/config/STANDARD_SOURCE_DEFINITION/9da77001-af33-4bcd-be46-6252bf9342b9.json index 1eabc052ada40..94a52b10f6171 100644 --- a/airbyte-config/init/src/main/resources/config/STANDARD_SOURCE_DEFINITION/9da77001-af33-4bcd-be46-6252bf9342b9.json +++ b/airbyte-config/init/src/main/resources/config/STANDARD_SOURCE_DEFINITION/9da77001-af33-4bcd-be46-6252bf9342b9.json @@ -2,7 +2,7 @@ "sourceDefinitionId": "9da77001-af33-4bcd-be46-6252bf9342b9", "name": "Shopify", "dockerRepository": "airbyte/source-shopify", - "dockerImageTag": "0.1.27", + "dockerImageTag": "0.1.28", "documentationUrl": "https://docs.airbyte.io/integrations/sources/shopify", "icon": "shopify.svg" } diff --git a/airbyte-config/init/src/main/resources/seed/source_definitions.yaml b/airbyte-config/init/src/main/resources/seed/source_definitions.yaml index 5658e1026587d..95fa5edb2d484 100644 --- a/airbyte-config/init/src/main/resources/seed/source_definitions.yaml +++ b/airbyte-config/init/src/main/resources/seed/source_definitions.yaml @@ -641,7 +641,7 @@ - name: Shopify sourceDefinitionId: 9da77001-af33-4bcd-be46-6252bf9342b9 dockerRepository: airbyte/source-shopify - dockerImageTag: 0.1.27 + dockerImageTag: 0.1.28 documentationUrl: https://docs.airbyte.io/integrations/sources/shopify icon: shopify.svg sourceType: api diff --git a/airbyte-config/init/src/main/resources/seed/source_specs.yaml b/airbyte-config/init/src/main/resources/seed/source_specs.yaml index 15a641b97ed67..dee1594ebbed3 100644 --- a/airbyte-config/init/src/main/resources/seed/source_specs.yaml +++ b/airbyte-config/init/src/main/resources/seed/source_specs.yaml @@ -6684,7 +6684,7 @@ supportsNormalization: false supportsDBT: false supported_destination_sync_modes: [] -- dockerImage: "airbyte/source-shopify:0.1.27" +- dockerImage: "airbyte/source-shopify:0.1.28" spec: documentationUrl: "https://docs.airbyte.io/integrations/sources/shopify" connectionSpecification: @@ -6694,8 +6694,8 @@ required: - "shop" - "start_date" - - "auth_method" - additionalProperties: false + - "credentials" + additionalProperties: true properties: shop: type: "string" @@ -6710,24 +6710,21 @@ examples: - "2021-01-01" pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" - auth_method: + credentials: title: "Shopify Authorization Method" type: "object" oneOf: - type: "object" title: "OAuth2.0" required: - - "client_id" - - "client_secret" - - "access_token" - "auth_method" properties: auth_method: type: "string" - const: "access_token" + const: "oauth2.0" enum: - - "access_token" - default: "access_token" + - "oauth2.0" + default: "oauth2.0" order: 0 client_id: type: "string" @@ -6747,8 +6744,8 @@ - title: "API Password" type: "object" required: - - "api_password" - "auth_method" + - "api_password" properties: auth_method: type: "string" @@ -6766,17 +6763,52 @@ supportsNormalization: false supportsDBT: false supported_destination_sync_modes: [] - authSpecification: - auth_type: "oauth2.0" - oauth2Specification: - rootObject: - - "auth_method" - - "0" - oauthFlowInitParameters: - - - "client_id" - - - "client_secret" - oauthFlowOutputParameters: - - - "access_token" + advanced_auth: + auth_flow_type: "oauth2.0" + predicate_key: + - "credentials" + - "auth_method" + predicate_value: "oauth2.0" + oauth_config_specification: + oauth_user_input_from_connector_config_specification: + type: "object" + additionalProperties: false + properties: + shop: + type: "string" + path_in_connector_config: + - "shop" + complete_oauth_output_specification: + type: "object" + additionalProperties: false + properties: + access_token: + type: "string" + path_in_connector_config: + - "credentials" + - "access_token" + complete_oauth_server_input_specification: + type: "object" + additionalProperties: false + properties: + client_id: + type: "string" + client_secret: + type: "string" + complete_oauth_server_output_specification: + type: "object" + additionalProperties: false + properties: + client_id: + type: "string" + path_in_connector_config: + - "credentials" + - "client_id" + client_secret: + type: "string" + path_in_connector_config: + - "credentials" + - "client_secret" - dockerImage: "airbyte/source-shortio:0.1.2" spec: documentationUrl: "https://developers.short.io/reference" diff --git a/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryUtils.java b/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryUtils.java index 545e8aff58969..2c7a3dddd5833 100644 --- a/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryUtils.java +++ b/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryUtils.java @@ -5,7 +5,6 @@ package io.airbyte.integrations.destination.bigquery; import static io.airbyte.integrations.destination.bigquery.helpers.LoggerHelper.getJobErrorMessage; -import static java.util.Objects.isNull; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -172,9 +171,9 @@ public static String getDatasetId(final JsonNode config) { String projectId = config.get(BigQueryConsts.CONFIG_PROJECT_ID).asText(); if (!(projectId.equals(projectIdPart))) { throw new IllegalArgumentException(String.format( - "Project ID included in Dataset ID must match Project ID field's value: Project ID is `%s`, but you specified `%s` in Dataset ID", - projectId, - projectIdPart)); + "Project ID included in Dataset ID must match Project ID field's value: Project ID is `%s`, but you specified `%s` in Dataset ID", + projectId, + projectIdPart)); } } // if colonIndex is -1, then this returns the entire string diff --git a/airbyte-integrations/connectors/destination-bigquery/src/test-integration/java/io/airbyte/integrations/destination/bigquery/BigQueryDestinationTest.java b/airbyte-integrations/connectors/destination-bigquery/src/test-integration/java/io/airbyte/integrations/destination/bigquery/BigQueryDestinationTest.java index 70c7e9dd1627e..9edfa8f2ba0bc 100644 --- a/airbyte-integrations/connectors/destination-bigquery/src/test-integration/java/io/airbyte/integrations/destination/bigquery/BigQueryDestinationTest.java +++ b/airbyte-integrations/connectors/destination-bigquery/src/test-integration/java/io/airbyte/integrations/destination/bigquery/BigQueryDestinationTest.java @@ -385,6 +385,7 @@ private boolean isTablePartitioned(final BigQuery bigquery, final Dataset datase } private static class DatasetIdResetter { + private Consumer consumer; DatasetIdResetter(Consumer consumer) { @@ -394,20 +395,20 @@ private static class DatasetIdResetter { public void accept(JsonNode config) { consumer.accept(config); } + } private static Stream datasetIdResetterProvider() { // parameterized test with two dataset-id patterns: `dataset_id` and `project-id:dataset_id` return Stream.of( - Arguments.arguments(new DatasetIdResetter(config -> {})), - Arguments.arguments(new DatasetIdResetter( - config -> { - String projectId = ((ObjectNode) config).get(BigQueryConsts.CONFIG_PROJECT_ID).asText(); - String datasetId = ((ObjectNode) config).get(BigQueryConsts.CONFIG_DATASET_ID).asText(); - ((ObjectNode) config).put(BigQueryConsts.CONFIG_DATASET_ID, - String.format("%s:%s", projectId, datasetId)); - } - )) - ); + Arguments.arguments(new DatasetIdResetter(config -> {})), + Arguments.arguments(new DatasetIdResetter( + config -> { + String projectId = ((ObjectNode) config).get(BigQueryConsts.CONFIG_PROJECT_ID).asText(); + String datasetId = ((ObjectNode) config).get(BigQueryConsts.CONFIG_DATASET_ID).asText(); + ((ObjectNode) config).put(BigQueryConsts.CONFIG_DATASET_ID, + String.format("%s:%s", projectId, datasetId)); + }))); } + } diff --git a/airbyte-integrations/connectors/destination-bigquery/src/test/java/io/airbyte/integrations/destination/bigquery/BigQueryUtilsTest.java b/airbyte-integrations/connectors/destination-bigquery/src/test/java/io/airbyte/integrations/destination/bigquery/BigQueryUtilsTest.java index 586e0cf7ce747..f0538e443a624 100644 --- a/airbyte-integrations/connectors/destination-bigquery/src/test/java/io/airbyte/integrations/destination/bigquery/BigQueryUtilsTest.java +++ b/airbyte-integrations/connectors/destination-bigquery/src/test/java/io/airbyte/integrations/destination/bigquery/BigQueryUtilsTest.java @@ -26,7 +26,7 @@ public void init() { .put(BigQueryConsts.CONFIG_CREDS, "test_secret") .put(BigQueryConsts.CONFIG_DATASET_LOCATION, "US"); } - + @ParameterizedTest @MethodSource("validBigQueryIdProvider") public void testGetDatasetIdSuccess(String projectId, String datasetId, String expected) throws Exception { @@ -66,4 +66,5 @@ private static Stream invalidBigQueryIdProvider() { Arguments.arguments("my-project", "your-project:my_dataset", "Project ID included in Dataset ID must match Project ID field's value: Project ID is `my-project`, but you specified `your-project` in Dataset ID")); } + } diff --git a/airbyte-integrations/connectors/source-shopify/Dockerfile b/airbyte-integrations/connectors/source-shopify/Dockerfile index a4c18b641f04d..27a2909ad9283 100644 --- a/airbyte-integrations/connectors/source-shopify/Dockerfile +++ b/airbyte-integrations/connectors/source-shopify/Dockerfile @@ -28,5 +28,5 @@ COPY source_shopify ./source_shopify ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] -LABEL io.airbyte.version=0.1.27 +LABEL io.airbyte.version=0.1.28 LABEL io.airbyte.name=airbyte/source-shopify diff --git a/airbyte-integrations/connectors/source-shopify/integration_tests/invalid_config.json b/airbyte-integrations/connectors/source-shopify/integration_tests/invalid_config.json index 78b1a4afac6ed..c3ff95944b38c 100644 --- a/airbyte-integrations/connectors/source-shopify/integration_tests/invalid_config.json +++ b/airbyte-integrations/connectors/source-shopify/integration_tests/invalid_config.json @@ -1,7 +1,7 @@ { "shop": "SHOP_NAME", "start_date": "2020-11-01", - "auth_method": { + "credentials": { "auth_method": "api_password", "api_password": "SOME_API_PASSWORD" } diff --git a/airbyte-integrations/connectors/source-shopify/integration_tests/invalid_oauth_config.json b/airbyte-integrations/connectors/source-shopify/integration_tests/invalid_oauth_config.json index 5770cfa2d2097..db9882d1eec4b 100644 --- a/airbyte-integrations/connectors/source-shopify/integration_tests/invalid_oauth_config.json +++ b/airbyte-integrations/connectors/source-shopify/integration_tests/invalid_oauth_config.json @@ -1,8 +1,8 @@ { "shop": "SHOP_NAME", "start_date": "2020-11-01", - "auth_method": { - "auth_method": "access_token", + "credentials": { + "auth_method": "oauth2.0", "client_id": "SOME_CLIENT_ID", "client_secret": "SOME_CLIENT_SECRET", "access_token": "SOME_ACCESS_TOKEN" diff --git a/airbyte-integrations/connectors/source-shopify/source_shopify/auth.py b/airbyte-integrations/connectors/source-shopify/source_shopify/auth.py index bc3ab9db3d97e..ea3d36dda1a03 100644 --- a/airbyte-integrations/connectors/source-shopify/source_shopify/auth.py +++ b/airbyte-integrations/connectors/source-shopify/source_shopify/auth.py @@ -30,12 +30,12 @@ def __init__(self, config: Mapping[str, Any]): def get_auth_header(self) -> Mapping[str, Any]: auth_header: str = "X-Shopify-Access-Token" - auth_method: Dict = self.config["auth_method"] - auth_option: str = auth_method.get("auth_method") + credentials: Dict = self.config["credentials"] + auth_method: str = credentials.get("auth_method") - if auth_option == "access_token": - return {auth_header: auth_method.get("access_token")} - elif auth_option == "api_password": - return {auth_header: auth_method.get("api_password")} + if auth_method == "oauth2.0": + return {auth_header: credentials.get("access_token")} + elif auth_method == "api_password": + return {auth_header: credentials.get("api_password")} else: - raise NotImplementedAuth(auth_option) + raise NotImplementedAuth(auth_method) diff --git a/airbyte-integrations/connectors/source-shopify/source_shopify/spec.json b/airbyte-integrations/connectors/source-shopify/source_shopify/spec.json index a342e64eb1f43..69186baf72eed 100644 --- a/airbyte-integrations/connectors/source-shopify/source_shopify/spec.json +++ b/airbyte-integrations/connectors/source-shopify/source_shopify/spec.json @@ -4,8 +4,8 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Shopify Source CDK Specifications", "type": "object", - "required": ["shop", "start_date", "auth_method"], - "additionalProperties": false, + "required": ["shop", "start_date", "credentials"], + "additionalProperties": true, "properties": { "shop": { "type": "string", @@ -19,25 +19,20 @@ "examples": ["2021-01-01"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, - "auth_method": { + "credentials": { "title": "Shopify Authorization Method", "type": "object", "oneOf": [ { "type": "object", "title": "OAuth2.0", - "required": [ - "client_id", - "client_secret", - "access_token", - "auth_method" - ], + "required": ["auth_method"], "properties": { "auth_method": { "type": "string", - "const": "access_token", - "enum": ["access_token"], - "default": "access_token", + "const": "oauth2.0", + "enum": ["oauth2.0"], + "default": "oauth2.0", "order": 0 }, "client_id": { @@ -63,7 +58,7 @@ { "title": "API Password", "type": "object", - "required": ["api_password", "auth_method"], + "required": ["auth_method", "api_password"], "properties": { "auth_method": { "type": "string", @@ -84,12 +79,57 @@ } } }, - "authSpecification": { - "auth_type": "oauth2.0", - "oauth2Specification": { - "rootObject": ["auth_method", 0], - "oauthFlowInitParameters": [["client_id"], ["client_secret"]], - "oauthFlowOutputParameters": [["access_token"]] + "advanced_auth": { + "auth_flow_type": "oauth2.0", + "predicate_key": ["credentials", "auth_method"], + "predicate_value": "oauth2.0", + "oauth_config_specification": { + "complete_oauth_output_specification": { + "type": "object", + "additionalProperties": false, + "properties": { + "access_token": { + "type": "string", + "path_in_connector_config": ["credentials", "access_token"] + } + } + }, + "complete_oauth_server_input_specification": { + "type": "object", + "additionalProperties": false, + "properties": { + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + } + } + }, + "complete_oauth_server_output_specification": { + "type": "object", + "additionalProperties": false, + "properties": { + "client_id": { + "type": "string", + "path_in_connector_config": ["credentials", "client_id"] + }, + "client_secret": { + "type": "string", + "path_in_connector_config": ["credentials", "client_secret"] + } + } + }, + "oauth_user_input_from_connector_config_specification": { + "type": "object", + "additionalProperties": false, + "properties": { + "shop": { + "type": "string", + "path_in_connector_config": ["shop"] + } + } + } } } } diff --git a/airbyte-integrations/connectors/source-snowflake/src/main/java/io.airbyte.integrations.source.snowflake/SnowflakeSource.java b/airbyte-integrations/connectors/source-snowflake/src/main/java/io.airbyte.integrations.source.snowflake/SnowflakeSource.java index 95e8f8836c9bf..cdffb1abac6f1 100644 --- a/airbyte-integrations/connectors/source-snowflake/src/main/java/io.airbyte.integrations.source.snowflake/SnowflakeSource.java +++ b/airbyte-integrations/connectors/source-snowflake/src/main/java/io.airbyte.integrations.source.snowflake/SnowflakeSource.java @@ -39,15 +39,16 @@ public JsonNode toDatabaseConfig(final JsonNode config) { .put("host", config.get("host").asText()) .put("username", config.get("username").asText()) .put("password", config.get("password").asText()) - .put("connection_properties", String.format("role=%s;warehouse=%s;database=%s;schema=%s;JDBC_QUERY_RESULT_FORMAT=%s;CLIENT_SESSION_KEEP_ALIVE=%s;", - config.get("role").asText(), - config.get("warehouse").asText(), - config.get("database").asText(), - config.get("schema").asText(), - // Needed for JDK17 - see - // https://stackoverflow.com/questions/67409650/snowflake-jdbc-driver-internal-error-fail-to-retrieve-row-count-for-first-arrow - "JSON", - true)) + .put("connection_properties", + String.format("role=%s;warehouse=%s;database=%s;schema=%s;JDBC_QUERY_RESULT_FORMAT=%s;CLIENT_SESSION_KEEP_ALIVE=%s;", + config.get("role").asText(), + config.get("warehouse").asText(), + config.get("database").asText(), + config.get("schema").asText(), + // Needed for JDK17 - see + // https://stackoverflow.com/questions/67409650/snowflake-jdbc-driver-internal-error-fail-to-retrieve-row-count-for-first-arrow + "JSON", + true)) .build()); } diff --git a/airbyte-oauth/src/main/java/io/airbyte/oauth/OAuthImplementationFactory.java b/airbyte-oauth/src/main/java/io/airbyte/oauth/OAuthImplementationFactory.java index 7a549d65a3650..2c2498c132c90 100644 --- a/airbyte-oauth/src/main/java/io/airbyte/oauth/OAuthImplementationFactory.java +++ b/airbyte-oauth/src/main/java/io/airbyte/oauth/OAuthImplementationFactory.java @@ -57,6 +57,7 @@ public OAuthImplementationFactory(final ConfigRepository configRepository, final .put("airbyte/source-monday", new MondayOAuthFlow(configRepository, httpClient)) .put("airbyte/source-zendesk-sunshine", new ZendeskSunshineOAuthFlow(configRepository, httpClient)) .put("airbyte/source-mailchimp", new MailchimpOAuthFlow(configRepository, httpClient)) + .put("airbyte/source-shopify", new ShopifyOAuthFlow(configRepository, httpClient)) .build(); } diff --git a/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/ShopifyOAuthFlow.java b/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/ShopifyOAuthFlow.java new file mode 100644 index 0000000000000..1e49cfcba18d2 --- /dev/null +++ b/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/ShopifyOAuthFlow.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2021 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.oauth.flows; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import io.airbyte.config.persistence.ConfigRepository; +import io.airbyte.oauth.BaseOAuth2Flow; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Supplier; +import org.apache.http.client.utils.URIBuilder; + +public class ShopifyOAuthFlow extends BaseOAuth2Flow { + + private static final List SCOPES = Arrays.asList( + "read_themes", + "read_orders", + "read_all_orders", + "read_assigned_fulfillment_orders", + "read_checkouts", + "read_content", + "read_customers", + "read_discounts", + "read_draft_orders", + "read_fulfillments", + "read_locales", + "read_locations", + "read_price_rules", + "read_products", + "read_product_listings", + "read_shopify_payments_payouts"); + + public String getScopes() { + return String.join(",", SCOPES); + } + + public ShopifyOAuthFlow(final ConfigRepository configRepository, final HttpClient httpClient) { + super(configRepository, httpClient); + } + + @VisibleForTesting + public ShopifyOAuthFlow(final ConfigRepository configRepository, final HttpClient httpClient, final Supplier stateSupplier) { + super(configRepository, httpClient, stateSupplier); + } + + @Override + protected String formatConsentUrl(final UUID definitionId, + final String clientId, + final String redirectUrl, + final JsonNode inputOAuthConfiguration) + throws IOException { + + // getting shop value from user's config + final String shop = getConfigValueUnsafe(inputOAuthConfiguration, "shop"); + // building consent url + final URIBuilder builder = new URIBuilder() + .setScheme("https") + .setHost(shop + ".myshopify.com") + .setPath("admin/oauth/authorize") + .addParameter("client_id", clientId) + .addParameter("redirect_uri", redirectUrl) + .addParameter("state", getState()) + .addParameter("grant_options[]", "value") + .addParameter("scope", getScopes()); + + try { + return builder.build().toString(); + } catch (URISyntaxException e) { + throw new IOException("Failed to format Consent URL for OAuth flow", e); + } + } + + @Override + protected Map getAccessTokenQueryParameters(String clientId, + String clientSecret, + String authCode, + String redirectUrl) { + return ImmutableMap.builder() + .put("client_id", clientId) + .put("client_secret", clientSecret) + .put("code", authCode) + .build(); + } + + @Override + protected String getAccessTokenUrl(final JsonNode inputOAuthConfiguration) { + // getting shop value from user's config + final String shop = getConfigValueUnsafe(inputOAuthConfiguration, "shop"); + // building the access_token_url + return "https://" + shop + ".myshopify.com/admin/oauth/access_token"; + } + + @Override + protected Map extractOAuthOutput(final JsonNode data, final String accessTokenUrl) throws IOException { + final Map result = new HashMap<>(); + // getting out access_token + if (data.has("access_token")) { + result.put("access_token", data.get("access_token").asText()); + } else { + throw new IOException(String.format("Missing 'access_token' in query params from %s", accessTokenUrl)); + } + + return result; + } + +} diff --git a/docs/integrations/sources/shopify.md b/docs/integrations/sources/shopify.md index 31528619ed025..79afd2388b4bc 100644 --- a/docs/integrations/sources/shopify.md +++ b/docs/integrations/sources/shopify.md @@ -101,6 +101,7 @@ This connector support both: `OAuth 2.0` and `API PASSWORD` (for private applica | Version | Date | Pull Request | Subject | | :--- | :--- | :--- | :--- | +| 0.1.28 | 2022-01-19 | [9591](https://github.com/airbytehq/airbyte/pull/9591) | Implemented `OAuth2.0` authentication method for Airbyte Cloud | | 0.1.27 | 2021-12-22 | [9049](https://github.com/airbytehq/airbyte/pull/9049) | Update connector fields title/description | | 0.1.26 | 2021-12-14 | [8597](https://github.com/airbytehq/airbyte/pull/8597) | Fix `mismatched number of tables` for base-normalization, increased performance of `order_refunds` stream | | 0.1.25 | 2021-12-02 | [8297](https://github.com/airbytehq/airbyte/pull/8297) | Added Shop stream |