lucid-shopify 0.40.0 → 0.50.0

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: 98281c9fea5c19507b29e96c9ac8d5ccaf1c1f94b902cd3f736c28514913228a
4
- data.tar.gz: 5e7c2d62db39d4ca105666fccc329b255d3e20e4de7730d2d75f8dd031d3cd77
3
+ metadata.gz: 3bb84f7626f55c571effa2ee48a98c1c688da8eea47b4de4c5afae7c14a3497a
4
+ data.tar.gz: 79082a133ae357a08c07f18dfbaef1250e0edb3d59c513dd1479407214cf241a
5
5
  SHA512:
6
- metadata.gz: 9461458119fca0aafc7fe4ebe00366a544ca63aba55588beec3d86d749f26ae9b2e7f31da3460511ea471b4f0fdbfb04c05a35ff0906fbb4809d7b5c00cc84a7
7
- data.tar.gz: 6912e93c75a6c708b7da8e31b778fe266ce26e823473c3a6a5859b73e716feb73b307bab2b7dd0ddfe90ab88fde65bb7e219349705656ef9b7cda715db4719c6
6
+ metadata.gz: 60e51efc5e0718d4372a7d0544ffac51387536da2a89f31d8a6baba15b3e0bb04e83be03c37664f1e7cf8b75430ca8db9fa87b480729bcada2650ed9d1a22454
7
+ data.tar.gz: 6e30ffd8a298e1359831900b4476a512ecbb3fc2c9a57f5ff7173c80d0f360dda9f688489e177616a87a12cf70942e233a3f592eaccb69aa5a559d701d2ecd94
data/README.md CHANGED
@@ -1,6 +1,25 @@
1
1
  lucid-shopify
2
2
  =============
3
3
 
4
+ 1. [Installation](#installation)
5
+ 2. [Setup](#setup)
6
+ * [Configure the API client](#configure-the-api-client)
7
+ 3. [Calling the API](#calling-the-api)
8
+ * [Make API requests](#make-api-requests)
9
+ * [Make unthrottled API requests](#make-unthrottled-api-requests)
10
+ * [Make bulk API requests](#make-bulk-api-requests)
11
+ * [Pagination](#pagination)
12
+ 4. [Authorisation](#authorisation)
13
+ 5. [Billing](#billing)
14
+ 6. [Webhooks](#webhooks)
15
+ * [Configure webhooks](#configure-webhooks)
16
+ * [Register webhook handlers](#register-webhook-handlers)
17
+ * [Create and delete webhooks](#create-and-delete-webhooks)
18
+ 7. [Verification](#verification)
19
+ * [Verify callbacks](#verify-callbacks)
20
+ * [Verify webhooks](#verify-webhooks)
21
+
22
+
4
23
  Installation
5
24
  ------------
6
25
 
@@ -9,10 +28,10 @@ Add the gem to your ‘Gemfile’:
9
28
  gem 'lucid-shopify'
10
29
 
11
30
 
12
- Usage
31
+ Setup
13
32
  -----
14
33
 
15
- ### Configure the default API client
34
+ ### Configure the API client
16
35
 
17
36
  Lucid::Shopify.configure do |config|
18
37
  config.api_key = '...'
@@ -25,8 +44,8 @@ Usage
25
44
  config.webhook_uri = '...'
26
45
  end
27
46
 
28
- All settings are optional and in some private apps, you may not
29
- require any configuration at all.
47
+ All settings are optional and in some private apps, you may not require any
48
+ configuration at all.
30
49
 
31
50
  Additionally, each API request requires authorisation:
32
51
 
@@ -35,10 +54,111 @@ Additionally, each API request requires authorisation:
35
54
  '...', # access_token
36
55
  )
37
56
 
38
- If the access token is omitted, the request will be unauthorised.
39
- This is only useful during the OAuth2 process.
57
+ If the access token is omitted, the request will be unauthorised. This is only
58
+ useful during the OAuth2 process.
59
+
60
+
61
+ Calling the API
62
+ ---------------
63
+
64
+ ### Make API requests
65
+
66
+ client = Lucid::Shopify::Client.new
67
+
68
+ client.get(credentials, 'orders', since_id: since_id)['orders']
69
+ client.post_json(credentials, 'orders', new_order)
70
+ client.post_graphql(credentials, <<~QUERY)['data']['orders']
71
+ {
72
+ orders {
73
+ edges {
74
+ node {
75
+ id
76
+ tags
77
+ }
78
+ }
79
+ }
80
+ }
81
+ QUERY
82
+
83
+ Request logging is disabled by default. To enable it:
84
+
85
+ Lucid::Shopify.config.logger = Logger.new(STDOUT)
86
+
87
+ Request throttling is enabled by default. If you're using Redis, throttling will
88
+ automatically make use of it; otherwise, throttling will only be maintained
89
+ across a single thread.
90
+
91
+
92
+ ### Make unthrottled API requests
93
+
94
+ client.unthrottled.get(credentials, 'orders')
95
+ client.unthrottled.post_json(credentials, 'orders', new_order)
96
+
97
+
98
+ ### Make bulk API requests
99
+
100
+ Since API version 2019-10, Shopify has offered an API for bulk requests (using
101
+ the GraphQL API). The gem wraps this API, by writing the result to a temporary
102
+ file and yielding each line of the result to limit memory usage.
103
+
104
+ client.bulk(credentials, <<~QUERY) do |product|
105
+ {
106
+ products {
107
+ edges {
108
+ node {
109
+ id
110
+ title
111
+ }
112
+ }
113
+ }
114
+ }
115
+ QUERY
116
+ puts product['id']
117
+ puts product['title']
118
+ end
119
+
120
+
121
+ ### Pagination
122
+
123
+ Since API version 2019-07, Shopify has encouraged a new method for pagination
124
+ based on the Link header. When you make a GET request, you can request the next
125
+ or the previous page directly from the response object.
126
+
127
+ page_1 = client.get(credentials, 'orders')
128
+ page_2 = page_1.next
129
+ page_1 = page_2.previous
130
+
131
+ When no page is available, `nil` will be returned.
132
+
133
+
134
+ Authorisation
135
+ -------------
136
+
137
+ authorise = Lucid::Shopify::Authorise.new
138
+
139
+ access_token = authorise.(credentials, authorisation_code)
140
+
141
+
142
+ Billing
143
+ -------
144
+
145
+ Create a new charge:
146
+
147
+ create_charge = Lucid::Shopify::CreateCharge.new
148
+
149
+ charge = create_charge.(credentials, charge) # see Lucid::Shopify::Charge
150
+
151
+ Redirect the user to `charge['confirmation_url']`. When the user returns (see
152
+ `config.billing_callback_uri`), activate the accepted charge:
153
+
154
+ activate_charge = Lucid::Shopify::ActivateCharge.new
155
+
156
+ activate_charge.(credentials, accepted_charge)
40
157
 
41
158
 
159
+ Webhooks
160
+ --------
161
+
42
162
  ### Configure webhooks
43
163
 
44
164
  Configure each webhook the app will create (if any):
@@ -81,7 +201,10 @@ Create/delete webhooks manually:
81
201
  Lucid::Shopify::DeleteWebhook.new.(credentials, webhook_id)
82
202
 
83
203
 
84
- ### Verification
204
+ Verification
205
+ ------------
206
+
207
+ ### Verify callbacks
85
208
 
86
209
  Verify callback requests with the request params:
87
210
 
@@ -91,6 +214,9 @@ Verify callback requests with the request params:
91
214
  # ...
92
215
  end
93
216
 
217
+
218
+ ### Verify webhooks
219
+
94
220
  Verify webhook requests with the request data and the HMAC header:
95
221
 
96
222
  begin
@@ -98,63 +224,3 @@ Verify webhook requests with the request data and the HMAC header:
98
224
  rescue Lucid::Shopify::Error => e
99
225
  # ...
100
226
  end
101
-
102
-
103
- ### Authorisation
104
-
105
- authorise = Lucid::Shopify::Authorise.new
106
-
107
- access_token = authorise.(credentials, authorisation_code)
108
-
109
-
110
- ### Billing
111
-
112
- Create a new charge:
113
-
114
- create_charge = Lucid::Shopify::CreateCharge.new
115
-
116
- charge = create_charge.(credentials, charge) # see Lucid::Shopify::Charge
117
-
118
- Redirect the user to `charge['confirmation_url']`. When the user
119
- returns (see `config.billing_callback_uri`), activate the accepted
120
- charge:
121
-
122
- activate_charge = Lucid::Shopify::ActivateCharge.new
123
-
124
- activate_charge.(credentials, accepted_charge)
125
-
126
-
127
- ### Make API requests
128
-
129
- client = Lucid::Shopify::Client.new
130
-
131
- client.get(credentials, 'orders', since_id: since_id)['orders']
132
- client.post_json(credentials, 'orders', new_order)
133
-
134
- Request logging is disabled by default. To enable it:
135
-
136
- Lucid::Shopify.config.logger = Logger.new(STDOUT)
137
-
138
- Request throttling is enabled by default. If you're using Redis, throttling
139
- will automatically make use of it; otherwise, throttling will only be
140
- maintained across a single thread.
141
-
142
-
143
- ### Make unthrottled API requests
144
-
145
- client.unthrottled.get(credentials, 'orders')
146
- client.unthrottled.post_json(credentials, 'orders', new_order)
147
-
148
-
149
- ### Pagination
150
-
151
- Since API version 2019-07, Shopify has encouraged a new method for
152
- pagination based on the Link header. When you make a GET request,
153
- you can request the next or the previous page directly from the
154
- response object.
155
-
156
- page_1 = client.get(credentials, 'orders')
157
- page_2 = page_1.next
158
- page_1 = page_2.previous
159
-
160
- When no page is available, `nil` will be returned.
data/lib/lucid/shopify.rb CHANGED
@@ -25,6 +25,7 @@ module Lucid
25
25
  autoload :GetRequest, 'lucid/shopify/get_request'
26
26
  autoload :ParseLinkHeader, 'lucid/shopify/parse_link_header'
27
27
  autoload :PostRequest, 'lucid/shopify/post_request'
28
+ autoload :PostGraphQLRequest, 'lucid/shopify/post_graphql_request'
28
29
  autoload :PutRequest, 'lucid/shopify/put_request'
29
30
  autoload :RedisThrottledStrategy, 'lucid/shopify/redis_throttled_strategy'
30
31
  autoload :Request, 'lucid/shopify/request'
@@ -52,6 +53,13 @@ module Lucid
52
53
  setting :webhook_uri
53
54
 
54
55
  class << self
56
+ # @param version [String]
57
+ #
58
+ # @raise [RuntimeError]
59
+ def assert_api_version!(version)
60
+ raise "requires API version >= #{version}" if config.api_version < version
61
+ end
62
+
55
63
  # Webhooks created for each shop.
56
64
  #
57
65
  # @return [WebhookList]
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'lucid/shopify'
5
+
6
+ module Lucid
7
+ module Shopify
8
+ class BulkRequest
9
+ OperationError = Class.new(Error)
10
+
11
+ CanceledOperationError = Class.new(OperationError)
12
+ ExpiredOperationError = Class.new(OperationError)
13
+ FailedOperationError = Class.new(OperationError)
14
+ ObsoleteOperationError = Class.new(OperationError)
15
+
16
+ # @param http [HTTP::Client]
17
+ def initialize(http: Container[:http])
18
+ @http = http
19
+ end
20
+
21
+ # @param client [Client]
22
+ # @param credentials [Credentials]
23
+ # @param query [String]
24
+ #
25
+ # @yield [String] each parsed line of JSONL (streamed to limit memory usage)
26
+ #
27
+ # @example
28
+ # bulk_request.(client, credentials, <<~QUERY)
29
+ # {
30
+ # products {
31
+ # edges {
32
+ # node {
33
+ # id
34
+ # handle
35
+ # publishedAt
36
+ # }
37
+ # }
38
+ # }
39
+ # }
40
+ # QUERY
41
+ def call(client, credentials, query, &block)
42
+ Shopify.assert_api_version!('2019-10')
43
+
44
+ id = client.post_graphql(credentials, <<~QUERY)['data']['bulkOperationRunQuery']['bulkOperation']['id']
45
+ mutation {
46
+ bulkOperationRunQuery(
47
+ query: """
48
+ #{query}
49
+ """
50
+ ) {
51
+ bulkOperation {
52
+ id
53
+ }
54
+ userErrors {
55
+ field
56
+ message
57
+ }
58
+ }
59
+ }
60
+ QUERY
61
+
62
+ url = poll(client, credentials, id)
63
+
64
+ # TODO: Verify signature?
65
+
66
+ begin
67
+ file = Tempfile.new(mode: 0600)
68
+ body = @http.get(url).body
69
+ until (chunk = body.readpartial).nil?
70
+ file.write(chunk)
71
+ end
72
+ file.rewind
73
+ file.each_line do |line|
74
+ block.call(JSON.parse(line))
75
+ end
76
+ ensure
77
+ file.close
78
+ file.unlink
79
+ end
80
+ end
81
+
82
+ # @param client [Client]
83
+ # @param credentials [Credentials]
84
+ # @param id [Integer] of the bulk operation
85
+ #
86
+ # @return [String] the download URL
87
+ private def poll(client, credentials, id)
88
+ op = client.post_graphql(credentials, <<~QUERY)['data']['currentBulkOperation']
89
+ {
90
+ currentBulkOperation {
91
+ id
92
+ status
93
+ url
94
+ }
95
+ }
96
+ QUERY
97
+
98
+ raise ObsoleteOperationError if op['id'] != id
99
+
100
+ case op['status']
101
+ when 'CANCELED'
102
+ raise CanceledOperationError
103
+ when 'EXPIRED'
104
+ raise ExpiredOperationError
105
+ when 'FAILED'
106
+ raise FailedOperationError
107
+ when 'COMPLETED'
108
+ op['url']
109
+ else
110
+ poll(client, credentials, id)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -2,22 +2,26 @@
2
2
 
3
3
  require 'lucid/shopify/container'
4
4
 
5
- %w[delete get post put].each { |m| require "lucid/shopify/#{m}_request" }
5
+ %w[delete get post post_graphql put].each { |m| require "lucid/shopify/#{m}_request" }
6
6
 
7
7
  module Lucid
8
8
  module Shopify
9
9
  class Client
10
+ # @param bulk_request [#call]
10
11
  # @param send_request [#call]
11
12
  # @param send_throttled_request [#call]
12
13
  # @param throttling [Boolean]
13
- def initialize(send_request: Container[:send_request],
14
+ def initialize(bulk_request: Container[:bulk_request],
15
+ send_request: Container[:send_request],
14
16
  send_throttled_request: Container[:send_throttled_request],
15
17
  throttling: true)
18
+ @bulk_request = bulk_request
16
19
  @send_request = send_request
17
20
  @send_throttled_request = send_throttled_request
18
21
  @throttling = throttling
19
22
 
20
23
  @params = {
24
+ bulk_request: @bulk_request,
21
25
  send_request: @send_request,
22
26
  send_throttled_request: @send_throttled_request
23
27
  }
@@ -58,6 +62,11 @@ module Lucid
58
62
  AuthenticatedClient.new(self, credentials)
59
63
  end
60
64
 
65
+ # @see BulkRequest#call
66
+ def bulk(*args, &block)
67
+ @bulk_request.(self, *args, &block)
68
+ end
69
+
61
70
  # @see DeleteRequest#initialize
62
71
  def delete(*args)
63
72
  send_request.(DeleteRequest.new(*args))
@@ -68,6 +77,11 @@ module Lucid
68
77
  send_request.(GetRequest.new(*args))
69
78
  end
70
79
 
80
+ # @see PostGraphQLRequest#initialize
81
+ def post_graphql(*args)
82
+ send_request.(PostGraphQLRequest.new(*args))
83
+ end
84
+
71
85
  # @see PostRequest#initialize
72
86
  def post_json(*args)
73
87
  send_request.(PostRequest.new(*args))
@@ -12,6 +12,7 @@ module Lucid
12
12
  # Services only (dependencies); no value objects, entities.
13
13
  Container.register(:activate_charge) { ActivateCharge.new }
14
14
  Container.register(:authorise) { Authorise.new }
15
+ Container.register(:bulk_request) { BulkRequest.new }
15
16
  Container.register(:client) { Client.new }
16
17
  Container.register(:create_all_webhooks) { CreateAllWebhooks.new }
17
18
  Container.register(:create_charge) { CreateCharge.new }
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lucid/shopify'
4
+
5
+ module Lucid
6
+ module Shopify
7
+ class PostGraphQLRequest < Request
8
+ # @private
9
+ #
10
+ # @param credentials [Credentials]
11
+ # @param query [String] the GraphQL query
12
+ # @param variables [Hash] the GraphQL variables (if any)
13
+ #
14
+ # @see https://graphql.org/graphql-js/graphql-clients
15
+ # @see https://graphql.org/graphql-js/mutations-and-input-types
16
+ def initialize(credentials, query, variables: {})
17
+ super(credentials, :post, 'graphql', json: {
18
+ query: query,
19
+ variables: variables,
20
+ })
21
+ end
22
+ end
23
+ end
24
+ end
@@ -111,6 +111,11 @@ module Lucid
111
111
  raise ServerError.new(request, self)
112
112
  end
113
113
 
114
+ # GraphQL always has status 200.
115
+ if request.is_a?(PostGraphQLRequest) && errors?
116
+ raise ClientError.new(request, self)
117
+ end
118
+
114
119
  self
115
120
  end
116
121
 
@@ -126,18 +131,50 @@ module Lucid
126
131
 
127
132
  # @return [Boolean]
128
133
  def errors?
134
+ return user_errors? if user_errors?
135
+
129
136
  data_hash.has_key?('errors') # should be only on 422
130
137
  end
131
138
 
139
+ # GraphQL user errors.
140
+ #
141
+ # @return [Boolean]
142
+ private def user_errors?
143
+ errors = data_hash.dig('data', 'userErrors')
144
+
145
+ !errors.nil? && !errors.empty?
146
+ end
147
+
132
148
  # A string rather than an object is returned by Shopify in the case of,
133
149
  # e.g., 'Not found'. In this case, we return it under the 'resource' key.
134
150
  #
135
151
  # @return [Hash]
136
152
  def errors
137
153
  errors = data_hash['errors']
138
- return {} if errors.nil?
139
- return {'resource' => errors} if errors.is_a?(String)
140
- errors
154
+ errors = case
155
+ when errors.nil?
156
+ {}
157
+ when errors.is_a?(String)
158
+ {'resource' => errors}
159
+ else
160
+ errors
161
+ end
162
+
163
+ errors.merge(user_errors)
164
+ end
165
+
166
+ # GraphQL user errors.
167
+ #
168
+ # @return [Hash]
169
+ private def user_errors
170
+ errors = data_hash.dig('data', 'userErrors')
171
+ return {} if errors.nil? || errors.empty?
172
+ errors.map do |error|
173
+ [
174
+ error['field'],
175
+ error['message'],
176
+ ]
177
+ end.to_h
141
178
  end
142
179
 
143
180
  # @return [Array<String>]
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lucid
4
4
  module Shopify
5
- VERSION = '0.40.0'
5
+ VERSION = '0.50.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lucid-shopify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.40.0
4
+ version: 0.50.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kelsey Judson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-12 00:00:00.000000000 Z
11
+ date: 2020-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dotenv
@@ -147,6 +147,7 @@ files:
147
147
  - lib/lucid/shopify.rb
148
148
  - lib/lucid/shopify/activate_charge.rb
149
149
  - lib/lucid/shopify/authorise.rb
150
+ - lib/lucid/shopify/bulk_request.rb
150
151
  - lib/lucid/shopify/client.rb
151
152
  - lib/lucid/shopify/container.rb
152
153
  - lib/lucid/shopify/create_all_webhooks.rb
@@ -159,6 +160,7 @@ files:
159
160
  - lib/lucid/shopify/error.rb
160
161
  - lib/lucid/shopify/get_request.rb
161
162
  - lib/lucid/shopify/parse_link_header.rb
163
+ - lib/lucid/shopify/post_graphql_request.rb
162
164
  - lib/lucid/shopify/post_request.rb
163
165
  - lib/lucid/shopify/put_request.rb
164
166
  - lib/lucid/shopify/redis_throttled_strategy.rb