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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21b4a873e0cc66c1e23474a6a8ca3ea2494aa8e42e202efef805119191d11d3f
4
- data.tar.gz: e182f04e2d786bac83085e8f5117bf84af4f2e9373ee185d1263632b15ec6345
3
+ metadata.gz: a9c0cf0f17f7be4a5d2cca27309dfe08963507d9293342df944be33b8766d49e
4
+ data.tar.gz: 91849801ca320f1d173a20197094ca5fbfa102bef5bbc3c711cedaa8b332c259
5
5
  SHA512:
6
- metadata.gz: b167b5b9ac039c1c353228d3ee5d8abd412baebe908d8828fb93ff1c2d023bbd6690075e10a1dc01fdbaaa966b88f07ed164ca6686983a27557a63ef61fb3702
7
- data.tar.gz: be973dee48594d75a347ab63b0569e75395bcd6c20e4fb7871730cc4c01037498b0831fc500356648d8b3e03cf55e85a60e7287df9ac5a634efe57a013a48fa0
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/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
+
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
- You can also use `WEBHOOKS_ENABLED=true` env variable to enable webhooks (useful in development).
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
- 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
+ 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 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
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.status
23
+ case response.code
49
24
  when 200..400
50
- handle_graphql_errors(response.body)
25
+ handle_graphql_errors(parsed_body(response))
51
26
  when 400
52
- raise BadRequest.new(response.body, code: response.status)
27
+ raise BadRequest.new(parsed_body(response), code: response.code)
53
28
  when 401
54
- raise UnauthorizedAccess.new(response.body, code: response.status)
29
+ raise UnauthorizedAccess.new(parsed_body(response), code: response.code)
55
30
  when 402
56
- raise PaymentRequired.new(response.body, code: response.status)
31
+ raise PaymentRequired.new(parsed_body(response), code: response.code)
57
32
  when 403
58
- raise ForbiddenAccess.new(response.body, code: response.status)
33
+ raise ForbiddenAccess.new(parsed_body(response), code: response.code)
59
34
  when 404
60
- raise ResourceNotFound.new(response.body, code: response.status)
35
+ raise ResourceNotFound.new(parsed_body(response), code: response.code)
61
36
  when 405
62
- raise MethodNotAllowed.new(response.body, code: response.status)
37
+ raise MethodNotAllowed.new(parsed_body(response), code: response.code)
63
38
  when 409
64
- raise ResourceConflict.new(response.body, code: response.status)
39
+ raise ResourceConflict.new(parsed_body(response), code: response.code)
65
40
  when 410
66
- raise ResourceGone.new(response.body, code: response.status)
41
+ raise ResourceGone.new(parsed_body(response), code: response.code)
67
42
  when 412
68
- raise PreconditionFailed.new(response.body, code: response.status)
43
+ raise PreconditionFailed.new(parsed_body(response), code: response.code)
69
44
  when 422
70
- raise ResourceInvalid.new(response.body, code: response.status)
45
+ raise ResourceInvalid.new(parsed_body(response), code: response.code)
71
46
  when 429
72
- raise TooManyRequests.new(response.body, code: response.status)
47
+ raise TooManyRequests.new(parsed_body(response), code: response.code)
73
48
  when 401...500
74
- raise ClientError.new(response.body, code: response.status)
49
+ raise ClientError.new(parsed_body(response), code: response.code)
75
50
  when 500...600
76
- raise ServerError.new(response.body, code: response.status)
51
+ raise ServerError.new(parsed_body(response), code: response.code)
77
52
  else
78
- raise ConnectionError.new(response.body, "Unknown response code: #{response.status}")
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(api_version = ShopifyAPI::Base.api_version)
114
- Client.new(api_version)
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(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
@@ -2,8 +2,8 @@ module ShopifyGraphql::Mutation
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  class_methods do
5
- def call(**kwargs)
6
- new.call(**kwargs)
5
+ def call(**kwargs, &block)
6
+ new.call(**kwargs, &block)
7
7
  end
8
8
  end
9
9
 
@@ -2,8 +2,8 @@ module ShopifyGraphql::Query
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  class_methods do
5
- def call(**kwargs)
6
- new.call(**kwargs)
5
+ def call(**kwargs, &block)
6
+ new.call(**kwargs, &block)
7
7
  end
8
8
  end
9
9
 
@@ -1,3 +1,3 @@
1
1
  module ShopifyGraphql
2
- VERSION = "0.4.1"
2
+ VERSION = "1.0.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.1
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: 2022-06-16 00:00:00.000000000 Z
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: 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
@@ -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.2.33
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.