shopify-client 0.0.1 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 996fd4c7f7e89ddeff22da53168828c5d8bd1a87ad3d965a512c10aee7c99850
4
- data.tar.gz: 0c2829baff3a2518d6a4ab44544a975a7f72dcb8614d75b703e0da46f3238b25
3
+ metadata.gz: 818841a24a7d51fd3a425b4a0c55f42ee67c00ce4c63266bb9ca3c1b1d5ba9e7
4
+ data.tar.gz: 18b286b836e472ea6842082012f57e3b6c762d32999cb9e4e3f3c03ea01225d0
5
5
  SHA512:
6
- metadata.gz: 93aa4de201357efbf4a51f5924bf1f547d85ee822f5fe35f34b2a1e7cc23645849026bb309de43f48644a94ff6772f63b967e98dad5ca1670d305c1877f411c2
7
- data.tar.gz: 2930f0d23bb145629ab392015142b213d4470d81c69675890ad9594983f8acf6fcac91ce1fabb68acb7207d779f9282faa6d261b9ee3249d021dad58dda101e9
6
+ metadata.gz: b71e062da5ded3d0689375261147a516eeb6b6135f9ca9cb94cc1a47fb4440aabc050e81b4cc64838f35f825d98af9cafec896354d4c80a6221a7f68cab75e21
7
+ data.tar.gz: 55adbc9bed92f981a939fb64a3bff3e743d4967164545bc9fe7d7820f7498c51a376594abd96f07274a124020f36ffb1c859df7cb44f5cdf46074f38af242ba6
data/README.md CHANGED
@@ -24,6 +24,8 @@ shopify-client
24
24
  * [Create a resource](#create-a-resource)
25
25
  * [Update a resource](#update-a-resource)
26
26
  * [Delete a resource](#delete-a-resource)
27
+ 9. [Testing](#testing)
28
+ * [Integration tests](#integration-tests)
27
29
 
28
30
 
29
31
  Installation
@@ -44,7 +46,7 @@ Setup
44
46
  config.api_version = '...' # e.g. '2021-04'
45
47
  config.cache_ttl = 3600
46
48
  config.redirect_uri = '...' # for OAuth
47
- config.logger = Logger.new(STDOUT) # defaults to a null logger
49
+ config.logger = Logger.new($stdout) # defaults to a null logger
48
50
  config.scope = '...'
49
51
  config.shared_secret = '...'
50
52
  config.webhook_uri = '...'
@@ -78,7 +80,7 @@ Calling the API
78
80
 
79
81
  Request logging is disabled by default. To enable it:
80
82
 
81
- ShopifyClient.config.logger = Logger.new(STDOUT)
83
+ ShopifyClient.config.logger = Logger.new($stdout)
82
84
 
83
85
  Request throttling is enabled by default. If you're using Redis, throttling will
84
86
  automatically make use of it; otherwise, throttling will only be maintained
@@ -91,7 +93,7 @@ The gem wraps Shopify's bulk query API by writing the result to a temporary file
91
93
  and yielding an enumerator which itself streams each line of the result to limit
92
94
  memory usage.
93
95
 
94
- client.grahql_bulk(%(
96
+ client.graphql_bulk(%(
95
97
  {
96
98
  products {
97
99
  edges {
@@ -298,7 +300,7 @@ Find a single result:
298
300
 
299
301
  Iterate over results (automatic pagination):
300
302
 
301
- order_repo.all.each do |order|
303
+ order_repo.all(client).each do |order|
302
304
  # ...
303
305
  end
304
306
 
@@ -340,3 +342,21 @@ Iterate over results (automatic pagination):
340
342
  order_repo = OrderRepository.new
341
343
 
342
344
  order_repo.delete(client, id)
345
+
346
+
347
+ Testing
348
+ -------
349
+
350
+ ### Integration tests
351
+
352
+ The integration tests require a private app with the scope `write_products`.
353
+ Create a .env file specifying the test shop, private app password, and a valid
354
+ webhook URI:
355
+
356
+ TEST_SHOP='test-shop.myshopify.com'
357
+ TEST_PASSWORD='shppa_...'
358
+ TEST_WEBHOOK_URI='https://.../webhooks'
359
+
360
+ Run the suite:
361
+
362
+ $ bundle exec rake test:integration
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'async/http/faraday'
3
4
  require 'faraday'
4
5
  require 'faraday_middleware'
5
6
 
@@ -18,6 +19,7 @@ module ShopifyClient
18
19
  },
19
20
  url: "https://#{myshopify_domain}/admin/api/#{ShopifyClient.config.api_version}",
20
21
  ) do |conn|
22
+ conn.adapter :async_http
21
23
  # Request throttling to avoid API rate limit.
22
24
  conn.use default_throttling_strategy
23
25
  # Retry for 429, too many requests.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyClient
4
+ ConfigError = Class.new(Error)
5
+ end
@@ -6,18 +6,52 @@ module ShopifyClient
6
6
  #
7
7
  # @param client [Client]
8
8
  #
9
- # @return [Array<Hash>] response data
9
+ # @return [Array<String>] GraphQL IDs
10
10
  def call(client)
11
- create_webhook = CreateWebhook.new
11
+ raise ConfigError, 'webhook_uri is not set' unless ShopifyClient.config.webhook_uri
12
12
 
13
- ShopifyClient.webhooks.map do |topic|
14
- Thread.new do
15
- create_webhook.(client, {
16
- topic: topic,
17
- fields: topic[:fields],
18
- })
13
+ webhooks_with_index = ShopifyClient.webhooks.each_with_index
14
+
15
+ return [] unless webhooks_with_index.any?
16
+
17
+ client.graphql(%(
18
+ mutation webhookSubscriptionCreate(
19
+ #{webhooks_with_index.map { |_, i| %(
20
+ $topic#{i}: WebhookSubscriptionTopic!
21
+ $webhookSubscription#{i}: WebhookSubscriptionInput!
22
+ )}.join("\n")}
23
+ ) {
24
+ #{webhooks_with_index.map { |_, i| %(
25
+ webhookSubscriptionCreate#{i}: webhookSubscriptionCreate(
26
+ topic: $topic#{i}
27
+ webhookSubscription: $webhookSubscription#{i}
28
+ ) {
29
+ userErrors {
30
+ field
31
+ message
32
+ }
33
+ webhookSubscription {
34
+ id
35
+ }
36
+ }
37
+ )}.join("\n")}
38
+ }
39
+ ), webhooks_with_index.each_with_object({}) do |((topic, options), i), variables|
40
+ variables["topic#{i}"] = topic_to_graphql(topic)
41
+ variables["webhookSubscription#{i}"] = {}.tap do |subscription|
42
+ subscription['callbackUrl'] = ShopifyClient.config.webhook_uri
43
+ subscription['includeFields'] = options[:fields] unless options[:fields].empty?
19
44
  end
20
- end.map(&:value)
45
+ end).data['data'].map do |_, mutation|
46
+ mutation['webhookSubscription']['id']
47
+ end
48
+ end
49
+
50
+ # @param topic [String]
51
+ #
52
+ # @return [String]
53
+ private def topic_to_graphql(topic)
54
+ topic.upcase.sub('/', '_')
21
55
  end
22
56
  end
23
57
  end
@@ -7,11 +7,13 @@ module ShopifyClient
7
7
  # @option webhook [String] :topic
8
8
  # @option webhook [Array<String>] :fields
9
9
  #
10
- # @return [Hash] response data
10
+ # @return [Integer] ID
11
11
  def call(client, webhook)
12
- client.post_json(credentials, 'webhooks', webhook: webhook.merge(
12
+ raise ConfigError, 'webhook_uri is not set' unless ShopifyClient.config.webhook_uri
13
+
14
+ client.post('webhooks', webhook: webhook.merge(
13
15
  address: ShopifyClient.config.webhook_uri,
14
- ))
16
+ )).data['webhook']['id']
15
17
  rescue Response::Error => e
16
18
  raise e unless e.response.errors.message?([
17
19
  /has already been taken/,
@@ -5,18 +5,40 @@ module ShopifyClient
5
5
  # Delete any existing webhooks.
6
6
  #
7
7
  # @param client [Client]
8
- #
9
- # @return [Array<Hash>] response data
10
- def call(client)
11
- webhooks = client.get(credentials, 'webhooks')['webhooks']
8
+ # @param ids [Array<Integer>, nil] GraphQL IDs
9
+ def call(client, ids: nil)
10
+ ids ||= client.graphql(%({
11
+ webhookSubscriptions(first: 100) {
12
+ edges {
13
+ node {
14
+ id
15
+ }
16
+ }
17
+ }
18
+ })).data['data']['webhookSubscriptions']['edges'].map do |edge|
19
+ edge['node']['id']
20
+ end
12
21
 
13
- delete_webhook = DeleteWebhook.new
22
+ return if ids.empty?
14
23
 
15
- webhooks.map do |webhook|
16
- Thread.new do
17
- delete_webhook.(client, webhook['id'])
18
- end
19
- end.map(&:value)
24
+ client.graphql(%(
25
+ mutation webhookSubscriptionDelete(
26
+ #{ids.each_with_index.map { |_, i| %(
27
+ $id#{i}: ID!
28
+ )}.join("\n")}
29
+ ) {
30
+ #{ids.each_with_index.map { |_, i| %(
31
+ webhookSubscriptionDelete#{i}: webhookSubscriptionDelete(id: $id#{i}) {
32
+ userErrors {
33
+ field
34
+ message
35
+ }
36
+ }
37
+ )}.join("\n")}
38
+ }
39
+ ), ids.each_with_index.each_with_object({}) do |(id, i), variables|
40
+ variables["id#{i}"] = id
41
+ end)
20
42
  end
21
43
  end
22
44
  end
@@ -4,10 +4,8 @@ module ShopifyClient
4
4
  class DeleteWebhook
5
5
  # @param client [Client]
6
6
  # @param id [Integer]
7
- #
8
- # @return [Hash] response data
9
7
  def call(client, id)
10
- client.delete(credentials, "webhooks/#{id}")
8
+ client.delete("webhooks/#{id}")
11
9
  end
12
10
  end
13
11
  end
@@ -72,6 +72,15 @@ module ShopifyClient
72
72
  end
73
73
  when 423
74
74
  raise ShopError.new(request, self), 'Shop is locked'
75
+ when 430
76
+ # NOTE: This is an unofficial code used by Shopify. See:
77
+ #
78
+ # https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#Unofficial_codes
79
+ #
80
+ # It's undocumented unfortunately, but seems to be like a 429 response,
81
+ # except where the app is making too many API calls (rather than hitting
82
+ # the per store rate limit).
83
+ raise TooManyRequestsError.new(request, self), 'Too many requests'
75
84
  when 400..499
76
85
  raise ClientError.new(request, self)
77
86
  when 500..599
@@ -183,6 +192,8 @@ module ShopifyClient
183
192
  InvalidAccessTokenError = Class.new(ClientError)
184
193
  # The shop is frozen/locked/unavailable.
185
194
  ShopError = Class.new(ClientError)
195
+ # The app is making too many requests to the API.
196
+ TooManyRequestsError = Class.new(ClientError)
186
197
 
187
198
  # The GraphQL API always responds with a status code of 200.
188
199
  GraphQLClientError = Class.new(ClientError) do
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ShopifyClient
4
- VERSION = '0.0.1'
4
+ VERSION = '0.0.6'
5
5
  end
@@ -12,7 +12,7 @@ module ShopifyClient
12
12
  # @param topic [String]
13
13
  # @param handler [#call]
14
14
  # @param fields [Array<String>] e.g. %w[id tags]
15
- def register(topic, handler = nil, fields: nil, &block)
15
+ def register(topic, handler = nil, fields: [], &block)
16
16
  raise ArgumentError unless nil ^ handler ^ block
17
17
 
18
18
  handler = block if block
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shopify-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kelsey Judson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-03 00:00:00.000000000 Z
11
+ date: 2021-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dotenv
@@ -66,6 +66,34 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '2.7'
69
+ - !ruby/object:Gem::Dependency
70
+ name: async
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.29'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.29'
83
+ - !ruby/object:Gem::Dependency
84
+ name: async-http-faraday
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.11'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.11'
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: dry-configurable
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -153,6 +181,7 @@ files:
153
181
  - lib/shopify-client/client.rb
154
182
  - lib/shopify-client/client/logging.rb
155
183
  - lib/shopify-client/client/normalise_path.rb
184
+ - lib/shopify-client/config_error.rb
156
185
  - lib/shopify-client/cookieless/check_header.rb
157
186
  - lib/shopify-client/cookieless/decode_session_token.rb
158
187
  - lib/shopify-client/cookieless/middleware.rb
@@ -180,7 +209,7 @@ files:
180
209
  - lib/shopify-client/version.rb
181
210
  - lib/shopify-client/webhook.rb
182
211
  - lib/shopify-client/webhook_list.rb
183
- homepage: https://gitea.judson.nz/shopify-apps/shopify-client
212
+ homepage: https://github.com/kj/shopify-client
184
213
  licenses:
185
214
  - ISC
186
215
  metadata: {}
@@ -199,7 +228,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
199
228
  - !ruby/object:Gem::Version
200
229
  version: '0'
201
230
  requirements: []
202
- rubygems_version: 3.2.15
231
+ rubygems_version: 3.2.22
203
232
  signing_key:
204
233
  specification_version: 4
205
234
  summary: Shopify client library