lucid-shopify 0.40.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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