shopify_graphql 0.4.2 → 1.1.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: e5c044ffb73a80d80b528b5fa0db311615bce5b39b26fb6c84bbd2fba9a76678
4
- data.tar.gz: 8fc1cb0cf055ed4e847dd51fce8d4d52d29db89308e48e930817c8a881c6132c
3
+ metadata.gz: ae1e8d2f4d571b2b213c2a16b4119aa5067f4eaca441db0893b80cff71b0bde7
4
+ data.tar.gz: 7246d23c32915ef20166202cf920f811631dd9a47c922bc207eeba7706d85f44
5
5
  SHA512:
6
- metadata.gz: 1d8e8c1aff9b0517c5ebab5cbe938f87f0040fff6a26a31212b123fdafbe56717442d01d9104cff79fc003a90f762e69f2721f2f55fceb459d600f1a72dfbd22
7
- data.tar.gz: c33b10f631685e8031479f90778bef818cd5681767d9043e96ffe050730af3089066f713b78895f95968edc3562eaa049a2dd9c9ef760fb9731327408a63fa29
6
+ metadata.gz: 744a08c3749b3083d5a3f7214a639102a2fa6a6a817bc416e5ae7688064f797a182555ab0965abaca75353a82464c2e124274585216f3df75665a8511ea37fb6
7
+ data.tar.gz: aad5f84d8c809cb208d455d4cbf1d25024f254febb3582469b8a65886d357a247f23f94821fc93333767d72bff6dc5cecde57bb87173ae580d8a22bcdd496d58
data/README.md CHANGED
@@ -1,254 +1,759 @@
1
1
  # Shopify Graphql
2
2
 
3
- Less painful way to work with [Shopify Graphql API](https://shopify.dev/api/admin/graphql/reference) in Ruby.
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 calls
10
- - Graphql webhooks integration
11
- - Built-in error handling
12
- - No schema and no memory issues
13
- - Buil-in retry on error
14
- - (Planned) Testing helpers
15
- - (Planned) Pre-built calls for common Graphql operations
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
- ## Usage
16
+ ## Dependencies
18
17
 
19
- ### Making Graphql calls directly
18
+ - [`shopify_api`](https://github.com/Shopify/shopify-api-ruby) v10+
19
+ - [`shopify_app`](https://github.com/Shopify/shopify_app) v19+
20
20
 
21
- ```ruby
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
- response = ShopifyGraphql.execute(CREATE_WEBHOOK_MUTATION,
37
- topic: "TOPIC",
38
- webhookSubscription: { callbackUrl: "ADDRESS", format: "JSON" },
39
- )
40
- response = response.data.webhookSubscriptionCreate
41
- ShopifyGraphql.handle_user_errors(response)
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
- ### Creating wrappers for queries, mutations, and fields
39
+ ## Conventions
45
40
 
46
- To isolate Graphql boilerplate you can create wrappers. To keep them organized use the following conventions:
47
- - Put them all into `app/graphql` folder
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
- #### Example fields
49
+ ## Usage examples
50
+
51
+ ### Simple query
53
52
 
53
+ <details><summary>Click to expand</summary>
54
54
  Definition:
55
- ```ruby
56
- class AppSubscriptionFields
57
- FRAGMENT = <<~GRAPHQL
58
- fragment AppSubscriptionFields on AppSubscription {
59
- id
60
- name
61
- status
62
- trialDays
63
- currentPeriodEnd
64
- test
65
- lineItems {
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
- plan {
68
- pricingDetails {
69
- __typename
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 self.parse(data)
93
- recurring_line_item = data.lineItems.find { |i| i.plan.pricingDetails.__typename == "AppRecurringPricing" }
94
- recurring_pricing = recurring_line_item&.plan&.pricingDetails
95
- usage_line_item = data.lineItems.find { |i| i.plan.pricingDetails.__typename == "AppUsagePricing" }
96
- usage_pricing = usage_line_item&.plan&.pricingDetails
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
- name: data.name,
101
- status: data.status,
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
- For usage examples see query and mutation below.
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
- #### Example query
140
+ ### Query with fields
121
141
 
142
+ <details><summary>Click to expand</summary>
122
143
  Definition:
123
- ```ruby
124
- class GetAppSubscription
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
- #{AppSubscriptionFields::FRAGMENT}
176
+ #{ProductFields::FRAGMENT}
177
+
129
178
  query($id: ID!) {
130
- node(id: $id) {
131
- ... AppSubscriptionFields
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 = AppSubscriptionFields.parse(response.data.node)
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
- ```ruby
146
- shopify_subscription = GetAppSubscription.call(id: @id).data
147
- shopify_subscription.status
148
- shopify_subscription.current_period_end
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
- #### Example mutation
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
- MUTATION = <<~GRAPHQL
159
- #{AppSubscriptionFields::FRAGMENT}
160
- mutation appSubscriptionCreate(
161
- $name: String!,
162
- $lineItems: [AppSubscriptionLineItemInput!]!,
163
- $returnUrl: URL!,
164
- $trialDays: Int,
165
- $test: Boolean
166
- ) {
167
- appSubscriptionCreate(
168
- name: $name,
169
- lineItems: $lineItems,
170
- returnUrl: $returnUrl,
171
- trialDays: $trialDays,
172
- test: $test
173
- ) {
174
- appSubscription {
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
- confirmationUrl
178
- userErrors {
179
- field
180
- message
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(name:, price:, return_url:, trial_days: nil, test: nil, interval: :monthly)
187
- payload = { name: name, returnUrl: return_url }
188
- plan_interval = (interval == :monthly) ? 'EVERY_30_DAYS' : 'ANNUAL'
189
- payload[:lineItems] = [{
190
- plan: {
191
- appRecurringPricingDetails: {
192
- price: { amount: price, currencyCode: 'USD' },
193
- interval: plan_interval
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
- payload[:trialDays] = trial_days if trial_days
198
- payload[:test] = test if test
416
+ }
417
+ GRAPHQL
199
418
 
200
- response = execute(MUTATION, **payload)
201
- response.data = response.data.appSubscriptionCreate
202
- handle_user_errors(response.data)
203
- response.data = parse_data(response.data)
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
- subscription: AppSubscriptionFields.parse(data.appSubscription),
212
- confirmation_url: data.confirmationUrl,
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
- ```ruby
220
- response = CreateRecurringSubscription.call(
221
- name: "Plan Name",
222
- price: 10,
223
- return_url: "RETURN URL",
224
- trial_days: 3,
225
- test: true,
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
- ## Installation
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
- In Gemfile, add:
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
- gem 'shopify_graphql', github: 'kirillplatonov/shopify_graphql', branch: 'main'
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
- 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`:
239
- ```ruby
240
- shop.with_shopify_session do
241
- # your calls to graphql
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
- 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`:
246
- ```ruby
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
+ - `ShopifyGraphql::UpsertPrivateMetafield`
741
+ - `ShopifyGraphql::DeletePrivateMetafield`
742
+
743
+ 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.
744
+
745
+ ## Graphql webhooks
746
+
747
+ > 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.
748
+
749
+ 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`:
750
+
751
+ ```rb
247
752
  ShopifyGraphql.configure do |config|
248
753
  # Webhooks
249
754
  webhooks_prefix = "https://#{Rails.configuration.app_host}/graphql_webhooks"
250
755
  config.webhook_jobs_namespace = 'shopify/webhooks'
251
- config.webhook_enabled_environments = ['production']
756
+ config.webhook_enabled_environments = ['development', 'staging', 'production']
252
757
  config.webhooks = [
253
758
  { topic: 'SHOP_UPDATE', address: "#{webhooks_prefix}/shop_update" },
254
759
  { topic: 'APP_SUBSCRIPTIONS_UPDATE', address: "#{webhooks_prefix}/app_subscriptions_update" },
@@ -257,7 +762,51 @@ ShopifyGraphql.configure do |config|
257
762
  end
258
763
  ```
259
764
 
260
- You can also use `WEBHOOKS_ENABLED=true` env variable to enable webhooks (useful in development).
765
+ And add the following routes to `config/routes.rb`:
766
+
767
+ ```rb
768
+ mount ShopifyGraphql::Engine, at: '/'
769
+ ```
770
+
771
+ To register defined webhooks you need to call `ShopifyGraphql::UpdateWebhooksJob`. You can call it manually or use `AfterAuthenticateJob` from `shopify_app`:
772
+
773
+ ```rb
774
+ # config/initializers/shopify_app.rb
775
+ ShopifyApp.configure do |config|
776
+ # ...
777
+ config.after_authenticate_job = {job: "AfterAuthenticateJob", inline: true}
778
+ end
779
+ ```
780
+
781
+ ```rb
782
+ # app/jobs/after_install_job.rb
783
+ class AfterInstallJob < ApplicationJob
784
+ def perform(shop)
785
+ # ...
786
+ update_webhooks(shop)
787
+ end
788
+
789
+ def update_webhooks(shop)
790
+ ShopifyGraphql::UpdateWebhooksJob.perform_later(
791
+ shop_domain: shop.shopify_domain,
792
+ shop_token: shop.shopify_token
793
+ )
794
+ end
795
+ end
796
+ ```
797
+
798
+ 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`:
799
+
800
+ ```rb
801
+ class Webhooks::AppUninstalledJob < ApplicationJob
802
+ queue_as :default
803
+
804
+ def perform(shop_domain:, webhook:)
805
+ shop = Shop.find_by!(shopify_domain: shop_domain)
806
+ # handle shop uninstall
807
+ end
808
+ end
809
+ ```
261
810
 
262
811
  ## License
263
812
 
@@ -0,0 +1,36 @@
1
+ module ShopifyGraphql
2
+ class DeletePrivateMetafield
3
+ include ShopifyGraphql::Mutation
4
+
5
+ MUTATION = <<~GRAPHQL
6
+ mutation privateMetafieldDelete($input: PrivateMetafieldDeleteInput!) {
7
+ privateMetafieldDelete(input: $input) {
8
+ deletedPrivateMetafieldId
9
+ userErrors {
10
+ field
11
+ message
12
+ }
13
+ }
14
+ }
15
+ GRAPHQL
16
+
17
+ def call(namespace:, key:, owner: nil)
18
+ input = {namespace: namespace, key: key}
19
+ input[:owner] = owner if owner
20
+
21
+ response = execute(MUTATION, input: input)
22
+ handle_user_errors(response.data.privateMetafieldDelete)
23
+ response.data = parse_data(response.data)
24
+ response
25
+ end
26
+
27
+ private
28
+
29
+ def parse_data(data)
30
+ id = data.privateMetafieldDelete.deletedPrivateMetafieldId
31
+ OpenStruct.new(
32
+ deleted_private_metafield_id: id
33
+ )
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,58 @@
1
+ module ShopifyGraphql
2
+ class UpsertPrivateMetafield
3
+ include ShopifyGraphql::Mutation
4
+
5
+ MUTATION = <<~GRAPHQL
6
+ mutation privateMetafieldUpsert($input: PrivateMetafieldInput!) {
7
+ privateMetafieldUpsert(input: $input) {
8
+ privateMetafield {
9
+ id
10
+ namespace
11
+ key
12
+ value
13
+ valueType
14
+ }
15
+ userErrors {
16
+ field
17
+ message
18
+ }
19
+ }
20
+ }
21
+ GRAPHQL
22
+
23
+ def call(namespace:, key:, value:, owner: nil)
24
+ input = {namespace: namespace, key: key}
25
+ input[:owner] = owner if owner
26
+
27
+ case value
28
+ when Hash, Array
29
+ value = value.to_json
30
+ value_type = "JSON_STRING"
31
+ when Integer
32
+ value_type = "INTEGER"
33
+ else
34
+ value = value.to_s
35
+ value_type = "STRING"
36
+ end
37
+ input[:valueInput] = {value: value, valueType: value_type}
38
+
39
+ response = execute(MUTATION, input: input)
40
+ handle_user_errors(response.data.privateMetafieldUpsert)
41
+ response.data = parse_data(response.data)
42
+ response
43
+ end
44
+
45
+ private
46
+
47
+ def parse_data(data)
48
+ metafield = data.privateMetafieldUpsert.privateMetafield
49
+ OpenStruct.new(
50
+ id: metafield.id,
51
+ namespace: metafield.namespace,
52
+ key: metafield.key,
53
+ value: metafield.value,
54
+ value_type: metafield.valueType
55
+ )
56
+ end
57
+ end
58
+ end
@@ -1,81 +1,60 @@
1
1
  module ShopifyGraphql
2
2
  class Client
3
- RETRIABLE_EXCEPTIONS = [
4
- Errno::ETIMEDOUT,
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
- operation_name = variables.delete(:operation_name)
20
- response = connection.post do |req|
21
- req.body = {
22
- query: query,
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
+ Response.new(handle_response(response))
10
+ rescue ShopifyAPI::Errors::HttpResponseError => e
11
+ Response.new(handle_response(e.response))
12
+ rescue JSON::ParserError => e
13
+ raise ServerError.new(e, "Invalid JSON response")
14
+ rescue Errno::ECONNRESET, Errno::EPIPE, Net::ReadTimeout, Net::OpenTimeout, OpenSSL::SSL::SSLError => e
15
+ raise ConnectionError.new(e, "Network error")
33
16
  end
34
17
 
35
- def request_headers
36
- ShopifyAPI::Base.headers
37
- end
38
-
39
- def connection
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
18
+ def parsed_body(response)
19
+ if response.body.is_a?(Hash)
20
+ JSON.parse(response.body.to_json, object_class: OpenStruct)
21
+ else
22
+ response.body
44
23
  end
45
24
  end
46
25
 
47
26
  def handle_response(response)
48
- case response.status
27
+ case response.code
49
28
  when 200..400
50
- handle_graphql_errors(response.body)
29
+ handle_graphql_errors(parsed_body(response))
51
30
  when 400
52
- raise BadRequest.new(response.body, code: response.status)
31
+ raise BadRequest.new(parsed_body(response), code: response.code)
53
32
  when 401
54
- raise UnauthorizedAccess.new(response.body, code: response.status)
33
+ raise UnauthorizedAccess.new(parsed_body(response), code: response.code)
55
34
  when 402
56
- raise PaymentRequired.new(response.body, code: response.status)
35
+ raise PaymentRequired.new(parsed_body(response), code: response.code)
57
36
  when 403
58
- raise ForbiddenAccess.new(response.body, code: response.status)
37
+ raise ForbiddenAccess.new(parsed_body(response), code: response.code)
59
38
  when 404
60
- raise ResourceNotFound.new(response.body, code: response.status)
39
+ raise ResourceNotFound.new(parsed_body(response), code: response.code)
61
40
  when 405
62
- raise MethodNotAllowed.new(response.body, code: response.status)
41
+ raise MethodNotAllowed.new(parsed_body(response), code: response.code)
63
42
  when 409
64
- raise ResourceConflict.new(response.body, code: response.status)
43
+ raise ResourceConflict.new(parsed_body(response), code: response.code)
65
44
  when 410
66
- raise ResourceGone.new(response.body, code: response.status)
45
+ raise ResourceGone.new(parsed_body(response), code: response.code)
67
46
  when 412
68
- raise PreconditionFailed.new(response.body, code: response.status)
47
+ raise PreconditionFailed.new(parsed_body(response), code: response.code)
69
48
  when 422
70
- raise ResourceInvalid.new(response.body, code: response.status)
49
+ raise ResourceInvalid.new(parsed_body(response), code: response.code)
71
50
  when 429
72
- raise TooManyRequests.new(response.body, code: response.status)
51
+ raise TooManyRequests.new(parsed_body(response), code: response.code)
73
52
  when 401...500
74
- raise ClientError.new(response.body, code: response.status)
53
+ raise ClientError.new(parsed_body(response), code: response.code)
75
54
  when 500...600
76
- raise ServerError.new(response.body, code: response.status)
55
+ raise ServerError.new(parsed_body(response), code: response.code)
77
56
  else
78
- raise ConnectionError.new(response.body, "Unknown response code: #{response.status}")
57
+ raise ConnectionError.new(parsed_body(response), "Unknown response code: #{response.code}")
79
58
  end
80
59
  end
81
60
 
@@ -90,6 +69,8 @@ module ShopifyGraphql
90
69
  case error_code
91
70
  when "THROTTLED"
92
71
  raise TooManyRequests.new(response, error_message, code: error_code, doc: error_doc)
72
+ when "INTERNAL_SERVER_ERROR"
73
+ raise ServerError.new(response, error_message, code: error_code, doc: error_doc)
93
74
  else
94
75
  raise ConnectionError.new(response, error_message, code: error_code, doc: error_doc)
95
76
  end
@@ -110,8 +91,8 @@ module ShopifyGraphql
110
91
  class << self
111
92
  delegate :execute, :handle_user_errors, to: :client
112
93
 
113
- def client(api_version = ShopifyAPI::Base.api_version)
114
- Client.new(api_version)
94
+ def client
95
+ Client.new
115
96
  end
116
97
  end
117
98
  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(domain: shop_domain, token: shop_token, api_version: api_version) do
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(domain: shop_domain, token: shop_token, api_version: api_version) do
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(domain: shop_domain, token: shop_token, api_version: api_version) do
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
@@ -1,3 +1,3 @@
1
1
  module ShopifyGraphql
2
- VERSION = "0.4.2"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -1,6 +1,4 @@
1
1
  require 'shopify_api'
2
- require 'faraday'
3
- require 'faraday_middleware'
4
2
 
5
3
  require 'shopify_graphql/client'
6
4
  require 'shopify_graphql/configuration'
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.2
4
+ version: 1.1.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: 2022-07-13 00:00:00.000000000 Z
11
+ date: 2023-03-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: 5.2.0
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: 5.2.0
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
@@ -94,7 +66,9 @@ files:
94
66
  - app/graphql/shopify_graphql/cancel_subscription.rb
95
67
  - app/graphql/shopify_graphql/create_recurring_subscription.rb
96
68
  - app/graphql/shopify_graphql/create_usage_subscription.rb
69
+ - app/graphql/shopify_graphql/delete_private_metafield.rb
97
70
  - app/graphql/shopify_graphql/get_app_subscription.rb
71
+ - app/graphql/shopify_graphql/upsert_private_metafield.rb
98
72
  - config/routes.rb
99
73
  - lib/shopify_graphql.rb
100
74
  - lib/shopify_graphql/client.rb
@@ -135,7 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
109
  - !ruby/object:Gem::Version
136
110
  version: '0'
137
111
  requirements: []
138
- rubygems_version: 3.3.7
112
+ rubygems_version: 3.4.3
139
113
  signing_key:
140
114
  specification_version: 4
141
115
  summary: Less painful way to work with Shopify Graphql API in Ruby.