shopify_graphql 0.4.1 → 1.0.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 +4 -4
- data/README.md +708 -161
- data/lib/shopify_graphql/client.rb +29 -54
- data/lib/shopify_graphql/jobs/create_webhooks_job.rb +1 -2
- data/lib/shopify_graphql/jobs/destroy_webhooks_job.rb +1 -2
- data/lib/shopify_graphql/jobs/update_webhooks_job.rb +1 -2
- data/lib/shopify_graphql/mutation.rb +2 -2
- data/lib/shopify_graphql/query.rb +2 -2
- data/lib/shopify_graphql/version.rb +1 -1
- data/lib/shopify_graphql.rb +0 -2
- metadata +9 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9c0cf0f17f7be4a5d2cca27309dfe08963507d9293342df944be33b8766d49e
|
4
|
+
data.tar.gz: 91849801ca320f1d173a20197094ca5fbfa102bef5bbc3c711cedaa8b332c259
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '086eef695310cd96ebc3730d43df74ea56f9bcb80b1536c547a3c293f396f72483abf7be0e0ef549e09c892e78ed7ed8c74789fde518330125c350eed9f96e6b'
|
7
|
+
data.tar.gz: fe14262b4729dc21a3ec5515de1f9273a9a599d7faa47db48ab2eac359eb194730197ccf438dcb0383bf780640e839425bf4e531024ed724aaaf2635382645e1
|
data/README.md
CHANGED
@@ -1,254 +1,757 @@
|
|
1
1
|
# Shopify Graphql
|
2
2
|
|
3
|
-
Less painful way to work with [Shopify Graphql API](https://shopify.dev/api/admin
|
4
|
-
|
5
|
-
> **NOTE: The library only supports `shopify_api` < 10.0 at the moment.**
|
3
|
+
Less painful way to work with [Shopify Graphql API](https://shopify.dev/api/admin-graphql) in Ruby. This library is a tiny wrapper on top of [`shopify_api`](https://github.com/Shopify/shopify-api-ruby) gem. It provides a simple API for Graphql calls, better error handling, and Graphql webhooks integration.
|
6
4
|
|
7
5
|
## Features
|
8
6
|
|
9
|
-
- Simple API for Graphql
|
10
|
-
- Graphql
|
11
|
-
-
|
12
|
-
-
|
13
|
-
-
|
14
|
-
-
|
15
|
-
-
|
7
|
+
- Simple API for Graphql queries and mutations
|
8
|
+
- Conventions for organizing Graphql code
|
9
|
+
- ActiveResource-like error handling
|
10
|
+
- Graphql and user error handlers
|
11
|
+
- Auto-conversion of responses to OpenStruct
|
12
|
+
- Graphql webhooks integration for Rails
|
13
|
+
- Wrappers for Graphql rate limit extensions
|
14
|
+
- Built-in calls for common Graphql calls
|
16
15
|
|
17
|
-
##
|
16
|
+
## Dependencies
|
18
17
|
|
19
|
-
|
18
|
+
- [`shopify_api`](https://github.com/Shopify/shopify-api-ruby) v10+
|
19
|
+
- [`shopify_app`](https://github.com/Shopify/shopify_app) v19+
|
20
20
|
|
21
|
-
|
22
|
-
CREATE_WEBHOOK_MUTATION = <<~GRAPHQL
|
23
|
-
mutation($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
|
24
|
-
webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
|
25
|
-
webhookSubscription {
|
26
|
-
id
|
27
|
-
}
|
28
|
-
userErrors {
|
29
|
-
field
|
30
|
-
message
|
31
|
-
}
|
32
|
-
}
|
33
|
-
}
|
34
|
-
GRAPHQL
|
21
|
+
> For `shopify_api` < v10 use [`0-4-stable`](https://github.com/kirillplatonov/shopify_graphql/tree/0-4-stable) branch.
|
35
22
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
23
|
+
## Installation
|
24
|
+
|
25
|
+
Add `shopify_graphql` to your Gemfile:
|
26
|
+
|
27
|
+
```bash
|
28
|
+
bundle add shopify_graphql
|
29
|
+
```
|
30
|
+
|
31
|
+
This gem relies on `shopify_app` for authentication so no extra setup is required. But you still need to wrap your Graphql calls with `shop.with_shopify_session`:
|
32
|
+
|
33
|
+
```rb
|
34
|
+
shop.with_shopify_session do
|
35
|
+
# your calls to graphql
|
36
|
+
end
|
42
37
|
```
|
43
38
|
|
44
|
-
|
39
|
+
## Conventions
|
45
40
|
|
46
|
-
To
|
47
|
-
|
41
|
+
To better organize your Graphql code use the following conventions:
|
42
|
+
|
43
|
+
- Create wrappers for all of your queries and mutations to isolate them
|
44
|
+
- Put all Graphql-related code into `app/graphql` folder
|
48
45
|
- Use `Fields` suffix to name fields (eg `AppSubscriptionFields`)
|
49
46
|
- Use `Get` prefix to name queries (eg `GetProducts` or `GetAppSubscription`)
|
50
47
|
- Use imperative to name mutations (eg `CreateUsageSubscription` or `BulkUpdateVariants`)
|
51
48
|
|
52
|
-
|
49
|
+
## Usage examples
|
50
|
+
|
51
|
+
### Simple query
|
53
52
|
|
53
|
+
<details><summary>Click to expand</summary>
|
54
54
|
Definition:
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
55
|
+
|
56
|
+
```rb
|
57
|
+
# app/graphql/get_product.rb
|
58
|
+
|
59
|
+
class GetProduct
|
60
|
+
include ShopifyGraphql::Query
|
61
|
+
|
62
|
+
QUERY = <<~GRAPHQL
|
63
|
+
query($id: ID!) {
|
64
|
+
product(id: $id) {
|
65
|
+
handle
|
66
|
+
title
|
67
|
+
description
|
68
|
+
}
|
69
|
+
}
|
70
|
+
GRAPHQL
|
71
|
+
|
72
|
+
def call(id:)
|
73
|
+
response = execute(QUERY, id: id)
|
74
|
+
response.data = response.data.product
|
75
|
+
response
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
Usage:
|
81
|
+
|
82
|
+
```rb
|
83
|
+
product = GetProduct.call(id: "gid://shopify/Product/12345").data
|
84
|
+
puts product.handle
|
85
|
+
puts product.title
|
86
|
+
```
|
87
|
+
</details>
|
88
|
+
|
89
|
+
### Query with data parsing
|
90
|
+
|
91
|
+
<details><summary>Click to expand</summary>
|
92
|
+
Definition:
|
93
|
+
|
94
|
+
```rb
|
95
|
+
# app/graphql/get_product.rb
|
96
|
+
|
97
|
+
class GetProduct
|
98
|
+
include ShopifyGraphql::Query
|
99
|
+
|
100
|
+
QUERY = <<~GRAPHQL
|
101
|
+
query($id: ID!) {
|
102
|
+
product(id: $id) {
|
66
103
|
id
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
... on AppRecurringPricing {
|
71
|
-
price {
|
72
|
-
amount
|
73
|
-
}
|
74
|
-
interval
|
75
|
-
}
|
76
|
-
... on AppUsagePricing {
|
77
|
-
balanceUsed {
|
78
|
-
amount
|
79
|
-
}
|
80
|
-
cappedAmount {
|
81
|
-
amount
|
82
|
-
}
|
83
|
-
interval
|
84
|
-
terms
|
85
|
-
}
|
86
|
-
}
|
104
|
+
title
|
105
|
+
featuredImage {
|
106
|
+
source: url
|
87
107
|
}
|
88
108
|
}
|
89
109
|
}
|
90
110
|
GRAPHQL
|
91
111
|
|
92
|
-
def
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
112
|
+
def call(id:)
|
113
|
+
response = execute(QUERY, id: id)
|
114
|
+
response.data = parse_data(response.data.product)
|
115
|
+
response
|
116
|
+
end
|
97
117
|
|
118
|
+
private
|
119
|
+
|
120
|
+
def parse_data(data)
|
98
121
|
OpenStruct.new(
|
99
122
|
id: data.id,
|
100
|
-
|
101
|
-
|
102
|
-
trial_days: data.trialDays,
|
103
|
-
current_period_end: data.currentPeriodEnd && Time.parse(data.currentPeriodEnd),
|
104
|
-
test: data.test,
|
105
|
-
recurring_line_item_id: recurring_line_item&.id,
|
106
|
-
recurring_price: recurring_pricing&.price&.amount&.to_d,
|
107
|
-
recurring_interval: recurring_pricing&.interval,
|
108
|
-
usage_line_item_id: usage_line_item&.id,
|
109
|
-
usage_balance: usage_pricing&.balanceUsed&.amount&.to_d,
|
110
|
-
usage_capped_amount: usage_pricing&.cappedAmount&.amount&.to_d,
|
111
|
-
usage_interval: usage_pricing&.interval,
|
112
|
-
usage_terms: usage_pricing&.terms,
|
123
|
+
title: data.title,
|
124
|
+
featured_image: data.featuredImage&.source
|
113
125
|
)
|
114
126
|
end
|
115
127
|
end
|
116
128
|
```
|
117
129
|
|
118
|
-
|
130
|
+
Usage:
|
131
|
+
|
132
|
+
```rb
|
133
|
+
product = GetProduct.call(id: "gid://shopify/Product/12345").data
|
134
|
+
puts product.id
|
135
|
+
puts product.title
|
136
|
+
puts product.featured_image
|
137
|
+
```
|
138
|
+
</details>
|
119
139
|
|
120
|
-
|
140
|
+
### Query with fields
|
121
141
|
|
142
|
+
<details><summary>Click to expand</summary>
|
122
143
|
Definition:
|
123
|
-
|
124
|
-
|
144
|
+
|
145
|
+
```rb
|
146
|
+
# app/graphql/product_fields.rb
|
147
|
+
|
148
|
+
class ProductFields
|
149
|
+
FRAGMENT = <<~GRAPHQL
|
150
|
+
fragment ProductFields on Product {
|
151
|
+
id
|
152
|
+
title
|
153
|
+
featuredImage {
|
154
|
+
source: url
|
155
|
+
}
|
156
|
+
}
|
157
|
+
GRAPHQL
|
158
|
+
|
159
|
+
def self.parse(data)
|
160
|
+
OpenStruct.new(
|
161
|
+
id: data.id,
|
162
|
+
title: data.title,
|
163
|
+
featured_image: data.featuredImage&.source
|
164
|
+
)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
```rb
|
170
|
+
# app/graphql/get_product.rb
|
171
|
+
|
172
|
+
class GetProduct
|
125
173
|
include ShopifyGraphql::Query
|
126
174
|
|
127
175
|
QUERY = <<~GRAPHQL
|
128
|
-
#{
|
176
|
+
#{ProductFields::FRAGMENT}
|
177
|
+
|
129
178
|
query($id: ID!) {
|
130
|
-
|
131
|
-
...
|
179
|
+
product(id: $id) {
|
180
|
+
... ProductFields
|
132
181
|
}
|
133
182
|
}
|
134
183
|
GRAPHQL
|
135
184
|
|
136
185
|
def call(id:)
|
137
186
|
response = execute(QUERY, id: id)
|
138
|
-
response.data =
|
187
|
+
response.data = ProductFields.parse(response.data.product)
|
139
188
|
response
|
140
189
|
end
|
141
190
|
end
|
142
191
|
```
|
143
192
|
|
144
193
|
Usage:
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
194
|
+
|
195
|
+
```rb
|
196
|
+
product = GetProduct.call(id: "gid://shopify/Product/12345").data
|
197
|
+
puts product.id
|
198
|
+
puts product.title
|
199
|
+
puts product.featured_image
|
149
200
|
```
|
201
|
+
</details>
|
150
202
|
|
151
|
-
|
203
|
+
### Simple collection query
|
152
204
|
|
205
|
+
<details><summary>Click to expand</summary>
|
153
206
|
Definition:
|
154
|
-
```ruby
|
155
|
-
class CreateRecurringSubscription
|
156
|
-
include ShopifyGraphql::Mutation
|
157
207
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
... AppSubscriptionFields
|
208
|
+
```rb
|
209
|
+
# app/graphql/get_products.rb
|
210
|
+
|
211
|
+
class GetProducts
|
212
|
+
include ShopifyGraphql::Query
|
213
|
+
|
214
|
+
QUERY = <<~GRAPHQL
|
215
|
+
query {
|
216
|
+
products(first: 5) {
|
217
|
+
edges {
|
218
|
+
node {
|
219
|
+
id
|
220
|
+
title
|
221
|
+
featuredImage {
|
222
|
+
source: url
|
223
|
+
}
|
224
|
+
}
|
176
225
|
}
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
226
|
+
}
|
227
|
+
}
|
228
|
+
GRAPHQL
|
229
|
+
|
230
|
+
def call
|
231
|
+
response = execute(QUERY)
|
232
|
+
response.data = parse_data(response.data.products.edges)
|
233
|
+
response
|
234
|
+
end
|
235
|
+
|
236
|
+
private
|
237
|
+
|
238
|
+
def parse_data(data)
|
239
|
+
return [] if data.blank?
|
240
|
+
|
241
|
+
data.compact.map do |edge|
|
242
|
+
OpenStruct.new(
|
243
|
+
id: edge.node.id,
|
244
|
+
title: edge.node.title,
|
245
|
+
featured_image: edge.node.featuredImage&.source
|
246
|
+
)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
Usage:
|
253
|
+
|
254
|
+
```rb
|
255
|
+
products = GetProducts.call.data
|
256
|
+
products.each do |product|
|
257
|
+
puts product.id
|
258
|
+
puts product.title
|
259
|
+
puts product.featured_image
|
260
|
+
end
|
261
|
+
```
|
262
|
+
</details>
|
263
|
+
|
264
|
+
### Collection query with fields
|
265
|
+
|
266
|
+
<details><summary>Click to expand</summary>
|
267
|
+
Definition:
|
268
|
+
|
269
|
+
```rb
|
270
|
+
# app/graphql/product_fields.rb
|
271
|
+
|
272
|
+
class ProductFields
|
273
|
+
FRAGMENT = <<~GRAPHQL
|
274
|
+
fragment ProductFields on Product {
|
275
|
+
id
|
276
|
+
title
|
277
|
+
featuredImage {
|
278
|
+
source: url
|
279
|
+
}
|
280
|
+
}
|
281
|
+
GRAPHQL
|
282
|
+
|
283
|
+
def self.parse(data)
|
284
|
+
OpenStruct.new(
|
285
|
+
id: data.id,
|
286
|
+
title: data.title,
|
287
|
+
featured_image: data.featuredImage&.source
|
288
|
+
)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
```
|
292
|
+
|
293
|
+
```rb
|
294
|
+
# app/graphql/get_products.rb
|
295
|
+
|
296
|
+
class GetProducts
|
297
|
+
include ShopifyGraphql::Query
|
298
|
+
|
299
|
+
QUERY = <<~GRAPHQL
|
300
|
+
#{ProductFields::FRAGMENT}
|
301
|
+
|
302
|
+
query {
|
303
|
+
products(first: 5) {
|
304
|
+
edges {
|
305
|
+
cursor
|
306
|
+
node {
|
307
|
+
... ProductFields
|
308
|
+
}
|
181
309
|
}
|
182
310
|
}
|
183
311
|
}
|
184
312
|
GRAPHQL
|
185
313
|
|
186
|
-
def call
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
314
|
+
def call
|
315
|
+
response = execute(QUERY)
|
316
|
+
response.data = parse_data(response.data.products.edges)
|
317
|
+
response
|
318
|
+
end
|
319
|
+
|
320
|
+
private
|
321
|
+
|
322
|
+
def parse_data(data)
|
323
|
+
return [] if data.blank?
|
324
|
+
|
325
|
+
data.compact.map do |edge|
|
326
|
+
OpenStruct.new(
|
327
|
+
cursor: edge.cursor,
|
328
|
+
node: ProductFields.parse(edge.node)
|
329
|
+
)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
```
|
334
|
+
|
335
|
+
Usage:
|
336
|
+
|
337
|
+
```rb
|
338
|
+
products = GetProducts.call.data
|
339
|
+
products.each do |edge|
|
340
|
+
puts edge.cursor
|
341
|
+
puts edge.node.id
|
342
|
+
puts edge.node.title
|
343
|
+
puts edge.node.featured_image
|
344
|
+
end
|
345
|
+
```
|
346
|
+
</details>
|
347
|
+
|
348
|
+
### Collection query with pagination
|
349
|
+
|
350
|
+
<details><summary>Click to expand</summary>
|
351
|
+
Definition:
|
352
|
+
|
353
|
+
```rb
|
354
|
+
# app/graphql/product_fields.rb
|
355
|
+
|
356
|
+
class ProductFields
|
357
|
+
FRAGMENT = <<~GRAPHQL
|
358
|
+
fragment ProductFields on Product {
|
359
|
+
id
|
360
|
+
title
|
361
|
+
featuredImage {
|
362
|
+
source: url
|
363
|
+
}
|
364
|
+
}
|
365
|
+
GRAPHQL
|
366
|
+
|
367
|
+
def self.parse(data)
|
368
|
+
OpenStruct.new(
|
369
|
+
id: data.id,
|
370
|
+
title: data.title,
|
371
|
+
featured_image: data.featuredImage&.source
|
372
|
+
)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
```
|
376
|
+
|
377
|
+
```rb
|
378
|
+
# app/graphql/get_products.rb
|
379
|
+
|
380
|
+
class GetProducts
|
381
|
+
include ShopifyGraphql::Query
|
382
|
+
|
383
|
+
LIMIT = 5
|
384
|
+
QUERY = <<~GRAPHQL
|
385
|
+
#{ProductFields::FRAGMENT}
|
386
|
+
|
387
|
+
query {
|
388
|
+
products(first: #{LIMIT}) {
|
389
|
+
edges {
|
390
|
+
node {
|
391
|
+
... ProductFields
|
392
|
+
}
|
393
|
+
}
|
394
|
+
pageInfo {
|
395
|
+
hasNextPage
|
396
|
+
endCursor
|
397
|
+
}
|
398
|
+
}
|
399
|
+
}
|
400
|
+
GRAPHQL
|
401
|
+
QUERY_WITH_CURSOR = <<~GRAPHQL
|
402
|
+
#{ProductFields::FRAGMENT}
|
403
|
+
|
404
|
+
query($cursor: String!) {
|
405
|
+
products(first: #{LIMIT}, after: $cursor) {
|
406
|
+
edges {
|
407
|
+
node {
|
408
|
+
... ProductFields
|
409
|
+
}
|
410
|
+
}
|
411
|
+
pageInfo {
|
412
|
+
hasNextPage
|
413
|
+
endCursor
|
194
414
|
}
|
195
415
|
}
|
196
|
-
}
|
197
|
-
|
198
|
-
payload[:test] = test if test
|
416
|
+
}
|
417
|
+
GRAPHQL
|
199
418
|
|
200
|
-
|
201
|
-
response
|
202
|
-
|
203
|
-
|
419
|
+
def call
|
420
|
+
response = execute(QUERY)
|
421
|
+
data = parse_data(response.data.products.edges)
|
422
|
+
|
423
|
+
while response.data.products.pageInfo.hasNextPage
|
424
|
+
response = execute(QUERY_WITH_CURSOR, cursor: response.data.products.pageInfo.endCursor)
|
425
|
+
data += parse_data(response.data.products.edges)
|
426
|
+
end
|
427
|
+
|
428
|
+
response.data = data
|
204
429
|
response
|
205
430
|
end
|
206
431
|
|
207
432
|
private
|
208
433
|
|
209
434
|
def parse_data(data)
|
435
|
+
return [] if data.blank?
|
436
|
+
|
437
|
+
data.compact.map do |edge|
|
438
|
+
ProductFields.parse(edge.node)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
```
|
443
|
+
|
444
|
+
Usage:
|
445
|
+
|
446
|
+
```rb
|
447
|
+
products = GetProducts.call.data
|
448
|
+
products.each do |product|
|
449
|
+
puts product.id
|
450
|
+
puts product.title
|
451
|
+
puts product.featured_image
|
452
|
+
end
|
453
|
+
```
|
454
|
+
</details>
|
455
|
+
|
456
|
+
### Collection query with block
|
457
|
+
|
458
|
+
<details><summary>Click to expand</summary>
|
459
|
+
Definition:
|
460
|
+
|
461
|
+
```rb
|
462
|
+
# app/graphql/product_fields.rb
|
463
|
+
|
464
|
+
class ProductFields
|
465
|
+
FRAGMENT = <<~GRAPHQL
|
466
|
+
fragment ProductFields on Product {
|
467
|
+
id
|
468
|
+
title
|
469
|
+
featuredImage {
|
470
|
+
source: url
|
471
|
+
}
|
472
|
+
}
|
473
|
+
GRAPHQL
|
474
|
+
|
475
|
+
def self.parse(data)
|
210
476
|
OpenStruct.new(
|
211
|
-
|
212
|
-
|
477
|
+
id: data.id,
|
478
|
+
title: data.title,
|
479
|
+
featured_image: data.featuredImage&.source
|
213
480
|
)
|
214
481
|
end
|
215
482
|
end
|
216
483
|
```
|
217
484
|
|
485
|
+
```rb
|
486
|
+
# app/graphql/get_products.rb
|
487
|
+
|
488
|
+
class GetProducts
|
489
|
+
include ShopifyGraphql::Query
|
490
|
+
|
491
|
+
LIMIT = 5
|
492
|
+
QUERY = <<~GRAPHQL
|
493
|
+
#{ProductFields::FRAGMENT}
|
494
|
+
|
495
|
+
query {
|
496
|
+
products(first: #{LIMIT}) {
|
497
|
+
edges {
|
498
|
+
node {
|
499
|
+
... ProductFields
|
500
|
+
}
|
501
|
+
}
|
502
|
+
pageInfo {
|
503
|
+
hasNextPage
|
504
|
+
endCursor
|
505
|
+
}
|
506
|
+
}
|
507
|
+
}
|
508
|
+
GRAPHQL
|
509
|
+
QUERY_WITH_CURSOR = <<~GRAPHQL
|
510
|
+
#{ProductFields::FRAGMENT}
|
511
|
+
|
512
|
+
query($cursor: String!) {
|
513
|
+
products(first: #{LIMIT}, after: $cursor) {
|
514
|
+
edges {
|
515
|
+
node {
|
516
|
+
... ProductFields
|
517
|
+
}
|
518
|
+
}
|
519
|
+
pageInfo {
|
520
|
+
hasNextPage
|
521
|
+
endCursor
|
522
|
+
}
|
523
|
+
}
|
524
|
+
}
|
525
|
+
GRAPHQL
|
526
|
+
|
527
|
+
def call(&block)
|
528
|
+
response = execute(QUERY)
|
529
|
+
response.data.products.edges.each do |edge|
|
530
|
+
block.call ProductFields.parse(edge.node)
|
531
|
+
end
|
532
|
+
|
533
|
+
while response.data.products.pageInfo.hasNextPage
|
534
|
+
response = execute(QUERY_WITH_CURSOR, cursor: response.data.products.pageInfo.endCursor)
|
535
|
+
response.data.products.edges.each do |edge|
|
536
|
+
block.call ProductFields.parse(edge.node)
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
response
|
541
|
+
end
|
542
|
+
end
|
543
|
+
```
|
544
|
+
|
218
545
|
Usage:
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
).data
|
227
|
-
confirmation_url = response.confirmation_url
|
228
|
-
shopify_subscription = response.subscription
|
546
|
+
|
547
|
+
```rb
|
548
|
+
GetProducts.call do |product|
|
549
|
+
puts product.id
|
550
|
+
puts product.title
|
551
|
+
puts product.featured_image
|
552
|
+
end
|
229
553
|
```
|
554
|
+
</details>
|
230
555
|
|
231
|
-
|
556
|
+
### Collection query with nested pagination
|
557
|
+
|
558
|
+
<details><summary>Click to expand</summary>
|
559
|
+
Definition:
|
560
|
+
|
561
|
+
```rb
|
562
|
+
# app/graphql/get_collections_with_products.rb
|
232
563
|
|
233
|
-
|
564
|
+
class GetCollectionsWithProducts
|
565
|
+
include ShopifyGraphql::Query
|
566
|
+
|
567
|
+
COLLECTIONS_LIMIT = 1
|
568
|
+
PRODUCTS_LIMIT = 25
|
569
|
+
QUERY = <<~GRAPHQL
|
570
|
+
query {
|
571
|
+
collections(first: #{COLLECTIONS_LIMIT}) {
|
572
|
+
edges {
|
573
|
+
node {
|
574
|
+
id
|
575
|
+
title
|
576
|
+
products(first: #{PRODUCTS_LIMIT}) {
|
577
|
+
edges {
|
578
|
+
node {
|
579
|
+
id
|
580
|
+
}
|
581
|
+
}
|
582
|
+
}
|
583
|
+
}
|
584
|
+
}
|
585
|
+
pageInfo {
|
586
|
+
hasNextPage
|
587
|
+
endCursor
|
588
|
+
}
|
589
|
+
}
|
590
|
+
}
|
591
|
+
GRAPHQL
|
592
|
+
QUERY_WITH_CURSOR = <<~GRAPHQL
|
593
|
+
query ($cursor: String!) {
|
594
|
+
collections(first: #{COLLECTIONS_LIMIT}, after: $cursor) {
|
595
|
+
edges {
|
596
|
+
node {
|
597
|
+
id
|
598
|
+
title
|
599
|
+
products(first: #{PRODUCTS_LIMIT}) {
|
600
|
+
edges {
|
601
|
+
node {
|
602
|
+
id
|
603
|
+
}
|
604
|
+
}
|
605
|
+
}
|
606
|
+
}
|
607
|
+
}
|
608
|
+
pageInfo {
|
609
|
+
hasNextPage
|
610
|
+
endCursor
|
611
|
+
}
|
612
|
+
}
|
613
|
+
}
|
614
|
+
GRAPHQL
|
615
|
+
|
616
|
+
def call
|
617
|
+
response = execute(QUERY)
|
618
|
+
data = parse_data(response.data.collections.edges)
|
619
|
+
|
620
|
+
while response.data.collections.pageInfo.hasNextPage
|
621
|
+
response = execute(QUERY_WITH_CURSOR, cursor: response.data.collections.pageInfo.endCursor)
|
622
|
+
data += parse_data(response.data.collections.edges)
|
623
|
+
end
|
624
|
+
|
625
|
+
response.data = data
|
626
|
+
response
|
627
|
+
end
|
628
|
+
|
629
|
+
private
|
630
|
+
|
631
|
+
def parse_data(data)
|
632
|
+
return [] if data.blank?
|
633
|
+
|
634
|
+
data.compact.map do |edge|
|
635
|
+
OpenStruct.new(
|
636
|
+
id: edge.node.id,
|
637
|
+
title: edge.node.title,
|
638
|
+
products: edge.node.products.edges.map do |product_edge|
|
639
|
+
OpenStruct.new(id: product_edge.node.id)
|
640
|
+
end
|
641
|
+
)
|
642
|
+
end
|
643
|
+
end
|
644
|
+
end
|
234
645
|
```
|
235
|
-
|
646
|
+
|
647
|
+
Usage:
|
648
|
+
|
649
|
+
```rb
|
650
|
+
collections = GetCollectionsWithProducts.call.data
|
651
|
+
collections.each do |collection|
|
652
|
+
puts collection.id
|
653
|
+
puts collection.title
|
654
|
+
collection.products.each do |product|
|
655
|
+
puts product.id
|
656
|
+
end
|
657
|
+
end
|
236
658
|
```
|
659
|
+
</details>
|
237
660
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
661
|
+
### Mutation
|
662
|
+
|
663
|
+
<details><summary>Click to expand</summary>
|
664
|
+
|
665
|
+
Definition:
|
666
|
+
|
667
|
+
```rb
|
668
|
+
# app/graphql/update_product.rb
|
669
|
+
|
670
|
+
class UpdateProduct
|
671
|
+
include ShopifyGraphql::Mutation
|
672
|
+
|
673
|
+
MUTATION = <<~GRAPHQL
|
674
|
+
mutation($input: ProductInput!) {
|
675
|
+
productUpdate(input: $input) {
|
676
|
+
product {
|
677
|
+
id
|
678
|
+
title
|
679
|
+
}
|
680
|
+
userErrors {
|
681
|
+
field
|
682
|
+
message
|
683
|
+
}
|
684
|
+
}
|
685
|
+
}
|
686
|
+
GRAPHQL
|
687
|
+
|
688
|
+
def call(input:)
|
689
|
+
response = execute(MUTATION, input: input)
|
690
|
+
response.data = response.data.productUpdate
|
691
|
+
handle_user_errors(response.data)
|
692
|
+
response
|
693
|
+
end
|
242
694
|
end
|
243
695
|
```
|
244
696
|
|
245
|
-
|
246
|
-
|
697
|
+
Usage:
|
698
|
+
|
699
|
+
```rb
|
700
|
+
response = UpdateProduct.call(input: { id: "gid://shopify/Product/123", title: "New title" })
|
701
|
+
puts response.data.product.title
|
702
|
+
```
|
703
|
+
</details>
|
704
|
+
|
705
|
+
### Graphql call without wrapper
|
706
|
+
|
707
|
+
<details><summary>Click to expand</summary>
|
708
|
+
|
709
|
+
```rb
|
710
|
+
PRODUCT_UPDATE_MUTATION = <<~GRAPHQL
|
711
|
+
mutation($input: ProductInput!) {
|
712
|
+
productUpdate(input: $input) {
|
713
|
+
product {
|
714
|
+
id
|
715
|
+
title
|
716
|
+
}
|
717
|
+
userErrors {
|
718
|
+
field
|
719
|
+
message
|
720
|
+
}
|
721
|
+
}
|
722
|
+
}
|
723
|
+
GRAPHQL
|
724
|
+
|
725
|
+
response = ShopifyGraphql.execute(
|
726
|
+
PRODUCT_UPDATE_MUTATION,
|
727
|
+
input: { id: "gid://shopify/Product/12345", title: "New title" }
|
728
|
+
)
|
729
|
+
response = response.data.productUpdate
|
730
|
+
ShopifyGraphql.handle_user_errors(response)
|
731
|
+
```
|
732
|
+
</details>
|
733
|
+
|
734
|
+
## Built-in Graphql calls
|
735
|
+
|
736
|
+
- `ShopifyGraphql::CancelSubscription`
|
737
|
+
- `ShopifyGraphql::CreateRecurringSubscription`
|
738
|
+
- `ShopifyGraphql::CreateUsageSubscription`
|
739
|
+
- `ShopifyGraphql::GetAppSubscription`
|
740
|
+
|
741
|
+
Built-in wrappers are located in [`app/graphql/shopify_graphql`](/app/graphql/shopify_graphql/) folder. You can use them directly in your apps or as an example to create your own wrappers.
|
742
|
+
|
743
|
+
## Graphql webhooks
|
744
|
+
|
745
|
+
> Since version 10 `shopify_api` gem includes built-in support for Graphql webhooks. If you are using `shopify_api` version 10 or higher you don't need to use this gem to handle Graphql webhooks. See [`shopify_app` documentation](https://github.com/Shopify/shopify_app/blob/main/docs/shopify_app/webhooks.md) for more details.
|
746
|
+
|
747
|
+
The gem has built-in support for Graphql webhooks (similar to `shopify_app`). To enable it add the following config to `config/initializers/shopify_app.rb`:
|
748
|
+
|
749
|
+
```rb
|
247
750
|
ShopifyGraphql.configure do |config|
|
248
751
|
# Webhooks
|
249
752
|
webhooks_prefix = "https://#{Rails.configuration.app_host}/graphql_webhooks"
|
250
753
|
config.webhook_jobs_namespace = 'shopify/webhooks'
|
251
|
-
config.webhook_enabled_environments = ['production']
|
754
|
+
config.webhook_enabled_environments = ['development', 'staging', 'production']
|
252
755
|
config.webhooks = [
|
253
756
|
{ topic: 'SHOP_UPDATE', address: "#{webhooks_prefix}/shop_update" },
|
254
757
|
{ topic: 'APP_SUBSCRIPTIONS_UPDATE', address: "#{webhooks_prefix}/app_subscriptions_update" },
|
@@ -257,7 +760,51 @@ ShopifyGraphql.configure do |config|
|
|
257
760
|
end
|
258
761
|
```
|
259
762
|
|
260
|
-
|
763
|
+
And add the following routes to `config/routes.rb`:
|
764
|
+
|
765
|
+
```rb
|
766
|
+
mount ShopifyGraphql::Engine, at: '/'
|
767
|
+
```
|
768
|
+
|
769
|
+
To register defined webhooks you need to call `ShopifyGraphql::UpdateWebhooksJob`. You can call it manually or use `AfterAuthenticateJob` from `shopify_app`:
|
770
|
+
|
771
|
+
```rb
|
772
|
+
# config/initializers/shopify_app.rb
|
773
|
+
ShopifyApp.configure do |config|
|
774
|
+
# ...
|
775
|
+
config.after_authenticate_job = {job: "AfterAuthenticateJob", inline: true}
|
776
|
+
end
|
777
|
+
```
|
778
|
+
|
779
|
+
```rb
|
780
|
+
# app/jobs/after_install_job.rb
|
781
|
+
class AfterInstallJob < ApplicationJob
|
782
|
+
def perform(shop)
|
783
|
+
# ...
|
784
|
+
update_webhooks(shop)
|
785
|
+
end
|
786
|
+
|
787
|
+
def update_webhooks(shop)
|
788
|
+
ShopifyGraphql::UpdateWebhooksJob.perform_later(
|
789
|
+
shop_domain: shop.shopify_domain,
|
790
|
+
shop_token: shop.shopify_token
|
791
|
+
)
|
792
|
+
end
|
793
|
+
end
|
794
|
+
```
|
795
|
+
|
796
|
+
To handle webhooks create jobs in `app/jobs/webhooks` folder. The gem will automatically call them when new webhooks are received. The job name should match the webhook topic name. For example, to handle `APP_UNINSTALLED` webhook create `app/jobs/webhooks/app_uninstalled_job.rb`:
|
797
|
+
|
798
|
+
```rb
|
799
|
+
class Webhooks::AppUninstalledJob < ApplicationJob
|
800
|
+
queue_as :default
|
801
|
+
|
802
|
+
def perform(shop_domain:, webhook:)
|
803
|
+
shop = Shop.find_by!(shopify_domain: shop_domain)
|
804
|
+
# handle shop uninstall
|
805
|
+
end
|
806
|
+
end
|
807
|
+
```
|
261
808
|
|
262
809
|
## License
|
263
810
|
|
@@ -1,81 +1,56 @@
|
|
1
1
|
module ShopifyGraphql
|
2
2
|
class Client
|
3
|
-
|
4
|
-
|
5
|
-
Errno::ECONNREFUSED,
|
6
|
-
Errno::EHOSTUNREACH,
|
7
|
-
'Timeout::Error',
|
8
|
-
Faraday::TimeoutError,
|
9
|
-
Faraday::RetriableResponse,
|
10
|
-
Faraday::ParsingError,
|
11
|
-
Faraday::ConnectionFailed,
|
12
|
-
].freeze
|
13
|
-
|
14
|
-
def initialize(api_version = ShopifyAPI::Base.api_version)
|
15
|
-
@api_version = api_version
|
3
|
+
def client
|
4
|
+
@client ||= ShopifyAPI::Clients::Graphql::Admin.new(session: ShopifyAPI::Context.active_session)
|
16
5
|
end
|
17
6
|
|
18
7
|
def execute(query, **variables)
|
19
|
-
|
20
|
-
response
|
21
|
-
|
22
|
-
|
23
|
-
operationName: operation_name,
|
24
|
-
variables: variables,
|
25
|
-
}.to_json
|
26
|
-
end
|
27
|
-
response = handle_response(response)
|
28
|
-
ShopifyGraphql::Response.new(response)
|
29
|
-
end
|
30
|
-
|
31
|
-
def api_url
|
32
|
-
[ShopifyAPI::Base.site, @api_version.construct_graphql_path].join
|
8
|
+
response = client.query(query: query, variables: variables)
|
9
|
+
ShopifyGraphql::Response.new(handle_response(response))
|
10
|
+
rescue ShopifyAPI::Errors::HttpResponseError => e
|
11
|
+
ShopifyGraphql::Response.new(handle_response(e.response))
|
33
12
|
end
|
34
13
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
@connection ||= Faraday.new(url: api_url, headers: request_headers) do |conn|
|
41
|
-
conn.request :json
|
42
|
-
conn.response :json, parser_options: { object_class: OpenStruct }
|
43
|
-
conn.request :retry, max: 3, interval: 1, backoff_factor: 2, exceptions: RETRIABLE_EXCEPTIONS
|
14
|
+
def parsed_body(response)
|
15
|
+
if response.body.is_a?(Hash)
|
16
|
+
JSON.parse(response.body.to_json, object_class: OpenStruct)
|
17
|
+
else
|
18
|
+
response.body
|
44
19
|
end
|
45
20
|
end
|
46
21
|
|
47
22
|
def handle_response(response)
|
48
|
-
case response.
|
23
|
+
case response.code
|
49
24
|
when 200..400
|
50
|
-
handle_graphql_errors(response
|
25
|
+
handle_graphql_errors(parsed_body(response))
|
51
26
|
when 400
|
52
|
-
raise BadRequest.new(response
|
27
|
+
raise BadRequest.new(parsed_body(response), code: response.code)
|
53
28
|
when 401
|
54
|
-
raise UnauthorizedAccess.new(response
|
29
|
+
raise UnauthorizedAccess.new(parsed_body(response), code: response.code)
|
55
30
|
when 402
|
56
|
-
raise PaymentRequired.new(response
|
31
|
+
raise PaymentRequired.new(parsed_body(response), code: response.code)
|
57
32
|
when 403
|
58
|
-
raise ForbiddenAccess.new(response
|
33
|
+
raise ForbiddenAccess.new(parsed_body(response), code: response.code)
|
59
34
|
when 404
|
60
|
-
raise ResourceNotFound.new(response
|
35
|
+
raise ResourceNotFound.new(parsed_body(response), code: response.code)
|
61
36
|
when 405
|
62
|
-
raise MethodNotAllowed.new(response
|
37
|
+
raise MethodNotAllowed.new(parsed_body(response), code: response.code)
|
63
38
|
when 409
|
64
|
-
raise ResourceConflict.new(response
|
39
|
+
raise ResourceConflict.new(parsed_body(response), code: response.code)
|
65
40
|
when 410
|
66
|
-
raise ResourceGone.new(response
|
41
|
+
raise ResourceGone.new(parsed_body(response), code: response.code)
|
67
42
|
when 412
|
68
|
-
raise PreconditionFailed.new(response
|
43
|
+
raise PreconditionFailed.new(parsed_body(response), code: response.code)
|
69
44
|
when 422
|
70
|
-
raise ResourceInvalid.new(response
|
45
|
+
raise ResourceInvalid.new(parsed_body(response), code: response.code)
|
71
46
|
when 429
|
72
|
-
raise TooManyRequests.new(response
|
47
|
+
raise TooManyRequests.new(parsed_body(response), code: response.code)
|
73
48
|
when 401...500
|
74
|
-
raise ClientError.new(response
|
49
|
+
raise ClientError.new(parsed_body(response), code: response.code)
|
75
50
|
when 500...600
|
76
|
-
raise ServerError.new(response
|
51
|
+
raise ServerError.new(parsed_body(response), code: response.code)
|
77
52
|
else
|
78
|
-
raise ConnectionError.new(response
|
53
|
+
raise ConnectionError.new(parsed_body(response), "Unknown response code: #{response.code}")
|
79
54
|
end
|
80
55
|
end
|
81
56
|
|
@@ -110,8 +85,8 @@ module ShopifyGraphql
|
|
110
85
|
class << self
|
111
86
|
delegate :execute, :handle_user_errors, to: :client
|
112
87
|
|
113
|
-
def client
|
114
|
-
Client.new
|
88
|
+
def client
|
89
|
+
Client.new
|
115
90
|
end
|
116
91
|
end
|
117
92
|
end
|
@@ -5,10 +5,9 @@ module ShopifyGraphql
|
|
5
5
|
end
|
6
6
|
|
7
7
|
def perform(shop_domain:, shop_token:)
|
8
|
-
api_version = ShopifyApp.configuration.api_version
|
9
8
|
webhooks = ShopifyGraphql.configuration.webhooks
|
10
9
|
|
11
|
-
ShopifyAPI::Session.temp(
|
10
|
+
ShopifyAPI::Auth::Session.temp(shop: shop_domain, access_token: shop_token) do
|
12
11
|
manager = WebhooksManager.new(webhooks)
|
13
12
|
manager.create_webhooks
|
14
13
|
end
|
@@ -5,10 +5,9 @@ module ShopifyGraphql
|
|
5
5
|
end
|
6
6
|
|
7
7
|
def perform(shop_domain:, shop_token:)
|
8
|
-
api_version = ShopifyApp.configuration.api_version
|
9
8
|
webhooks = ShopifyGraphql.configuration.webhooks
|
10
9
|
|
11
|
-
ShopifyAPI::Session.temp(
|
10
|
+
ShopifyAPI::Auth::Session.temp(shop: shop_domain, access_token: shop_token) do
|
12
11
|
manager = WebhooksManager.new(webhooks)
|
13
12
|
manager.destroy_webhooks
|
14
13
|
end
|
@@ -5,10 +5,9 @@ module ShopifyGraphql
|
|
5
5
|
end
|
6
6
|
|
7
7
|
def perform(shop_domain:, shop_token:)
|
8
|
-
api_version = ShopifyApp.configuration.api_version
|
9
8
|
webhooks = ShopifyGraphql.configuration.webhooks
|
10
9
|
|
11
|
-
ShopifyAPI::Session.temp(
|
10
|
+
ShopifyAPI::Auth::Session.temp(shop: shop_domain, access_token: shop_token) do
|
12
11
|
manager = WebhooksManager.new(webhooks)
|
13
12
|
manager.recreate_webhooks!
|
14
13
|
end
|
data/lib/shopify_graphql.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shopify_graphql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kirill Platonov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-01-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -16,70 +16,42 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 6.0.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 6.0.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: shopify_api
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '10.0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '10.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: shopify_app
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '17.0'
|
48
|
-
type: :runtime
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '17.0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: faraday
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '1.0'
|
62
|
-
type: :runtime
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '1.0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: faraday_middleware
|
71
43
|
requirement: !ruby/object:Gem::Requirement
|
72
44
|
requirements:
|
73
45
|
- - ">="
|
74
46
|
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
47
|
+
version: '19.0'
|
76
48
|
type: :runtime
|
77
49
|
prerelease: false
|
78
50
|
version_requirements: !ruby/object:Gem::Requirement
|
79
51
|
requirements:
|
80
52
|
- - ">="
|
81
53
|
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
54
|
+
version: '19.0'
|
83
55
|
description:
|
84
56
|
email:
|
85
57
|
- mail@kirillplatonov.com
|
@@ -135,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
107
|
- !ruby/object:Gem::Version
|
136
108
|
version: '0'
|
137
109
|
requirements: []
|
138
|
-
rubygems_version: 3.
|
110
|
+
rubygems_version: 3.4.3
|
139
111
|
signing_key:
|
140
112
|
specification_version: 4
|
141
113
|
summary: Less painful way to work with Shopify Graphql API in Ruby.
|