shopify_api-graphql-tiny 1.0.1 → 1.1.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: 5e48648295daa6495ff73f0415253f062439c876620351d00b90da3ec98ba700
4
- data.tar.gz: 6789cbc730bc6f98f8ac89d63f1a9b57b043a990c685250f3992e8e4756f8a26
3
+ metadata.gz: 28d098f2ad608cf375916eb9efdf9c3278b514fc632058a835c1566989a4538b
4
+ data.tar.gz: 0a1b2d891a11116279b0169d2d093589148538f3c8abecaa35a5bd0d3bb2363a
5
5
  SHA512:
6
- metadata.gz: e22a8f947de401a3e8ca218486d5ac9e6c396b091d848d189b3564790b4f1b2dcca3e3b32f651698cee5030908ec1aa3926df31c013153b407cb0b466a8624ba
7
- data.tar.gz: 313dc8dcb7058042be6b39240f5177123bd4498d8ddae770801324875e187b5b625246f49b34e1b62ca2b87c0d94d51156a5ecf43b8829e0fcc7ab5378d5c735
6
+ metadata.gz: c0a66340735b1fc42126eda00b52598cbb4f1668da9e2680d99eab359e840865a6082568e53a71550f7e2edf936c7b78e1b15998391b6485d09958c7ae9cbfd2
7
+ data.tar.gz: b1ad6ca96b39d5c5a1e06c4d4a6b5e893a3236034b10426bbd9822f25e11bd9f03deddbc0125577abf3f126c5b2d78c4176f8913aa03f68f38362529e9ac0e2b
data/.env.template CHANGED
@@ -7,7 +7,8 @@
7
7
 
8
8
  # Must be full domain as it's used in test assertions
9
9
  SHOPIFY_DOMAIN=
10
- SHOPIFY_TOKEN=
10
+ SHOPIFY_ADMIN_TOKEN=
11
+ SHOPIFY_STOREFRONT_TOKEN=
11
12
 
12
13
  # Will result in the creation of a metadafield on the customer. write_customers permission required
13
14
  SHOPIFY_CUSTOMER_ID=
data/Changes CHANGED
@@ -1,3 +1,11 @@
1
+ 2026-06-25 v1.1.0
2
+ --------------------
3
+ * Add support for requests against the storefront API
4
+
5
+ 2026-06-23 v1.0.2
6
+ --------------------
7
+ * Add :raise_on_warnings option to raise a WarningError when a response contains warnings
8
+
1
9
  2026-01-24 v1.0.1
2
10
  --------------------
3
11
  * Add support for calling #paginate without a block
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ShopifyAPI::GraphQL::Tiny
2
2
 
3
- Lightweight, no-nonsense, Shopify GraphQL Admin API client with built-in pagination and retry
3
+ Lightweight, no-nonsense, Shopify Admin & Storefront GraphQL API client with built-in pagination and retry
4
4
 
5
5
  [![CI](https://github.com/ScreenStaring/shopify_api-graphql-tiny/actions/workflows/ci.yml/badge.svg)](https://github.com/ScreenStaring/shopify_api-graphql-tiny/actions)
6
6
 
@@ -74,6 +74,24 @@ GQL
74
74
  p result.dig("data", "customerUpdate", "userErrors")
75
75
  ```
76
76
 
77
+ ### Storefront API
78
+
79
+ By default requests are made against the Admin API.
80
+ To make requests against the Storefront API use the `:storefront => true` option:
81
+
82
+ ```rb
83
+ gql = ShopifyAPI::GraphQL::Tiny.new("my-shop", token, :storefront => true)
84
+ result = gql.execute(<<-GQL, :id => your_gid)
85
+ query($id: ID!) {
86
+ product(id: $id) {
87
+ selectedOrFirstAvailableVariant {
88
+ id
89
+ }
90
+ }
91
+ }
92
+ GQL
93
+ ```
94
+
77
95
  ### Automatically Retrying Failed Requests
78
96
 
79
97
  There are 2 types of retries: 1) request is rate-limited by Shopify 2) request fails due to an exception or non-200 HTTP response.
@@ -266,6 +284,37 @@ pager.execute(query) { |page| }
266
284
 
267
285
  The `"data"` and `"pageInfo"` keys are automatically added if not provided.
268
286
 
287
+ ### Raising on Warnings
288
+
289
+ A successful GraphQL response can still contain warnings (for example an invalid search field reported under
290
+ `extensions.search`). By default these are ignored. Set `:raise_on_warnings` to `true` to raise a `WarningError`
291
+ (a subclass of `GraphQLError`) whenever the response contains warnings:
292
+
293
+ ```rb
294
+ gql = ShopifyAPI::GraphQL::Tiny.new(shop, token, :raise_on_warnings => true)
295
+
296
+ begin
297
+ gql.execute(query)
298
+ rescue ShopifyAPI::GraphQL::Tiny::WarningError => e
299
+ warn e.message # The warnings, formatted
300
+ e.response # The full GraphQL response Hash
301
+ end
302
+ ```
303
+
304
+ ## Why Use This Instead of Shopify's API Client?
305
+
306
+ - Easy-to-use
307
+ - Built-in retry
308
+ - Built-in pagination
309
+ - Lightweight
310
+
311
+ Overall, Shopify's API client is bloated trash that will give you development headaches and long-term maintenance nightmares.
312
+
313
+ We used to use it, staring way back in 2015, but eventually had to pivot away from their Ruby libraries due to developer
314
+ frustration and high maintenance cost (and don't get us started on the ShopifyApp gem!@#).
315
+
316
+ For more information see: https://github.com/Shopify/shopify-api-ruby/issues/1181
317
+
269
318
  ## Testing
270
319
 
271
320
  `cp env.template .env` and fill-in `.env` with the missing values. This requires a Shopify store.
@@ -278,8 +327,8 @@ bundle exec rake rate_limit SHOPIFY_DOMAIN=your-domain SHOPIFY_TOKEN=your-token
278
327
 
279
328
  ## See Also
280
329
 
330
+ - [`ShopifyAPI::GraphQL::Request`](https://github.com/ScreenStaring/shopify_api-graphql-request) - A higher-level wrapper around this class with improved exception handling and `:snake_case` hash key conversion
281
331
  - [Shopify Dev Tools](https://github.com/ScreenStaring/shopify-dev-tools) - Command-line program to assist with the development and/or maintenance of Shopify apps and stores
282
- - [Shopify ID Export](https://github.com/ScreenStaring/shopify_id_export/) - Dump Shopify product and variant IDs —along with other identifiers— to a CSV or JSON file
283
332
  - [`TinyGID`](https://github.com/sshaw/tiny_gid/) - Build Global ID (gid://) URI strings from scalar values
284
333
 
285
334
  ## License
@@ -288,4 +337,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
288
337
 
289
338
  ---
290
339
 
291
- Made by [ScreenStaring](http://screenstaring.com)
340
+ Made by [ScreenStaring](https://screenstaring.com)
@@ -3,7 +3,7 @@
3
3
  module ShopifyAPI
4
4
  module GraphQL
5
5
  class Tiny
6
- VERSION = "1.0.1"
6
+ VERSION = "1.1.0"
7
7
  end
8
8
  end
9
9
  end
@@ -8,7 +8,7 @@ require "shopify_api/graphql/tiny/version"
8
8
  module ShopifyAPI
9
9
  module GraphQL
10
10
  ##
11
- # Lightweight, no-nonsense, Shopify GraphQL Admin API client with built-in pagination and retry
11
+ # Lightweight, no-nonsense, Shopify GraphQL Admin/Storefront API client with built-in pagination and retry
12
12
  #
13
13
  class Tiny
14
14
  Error = Class.new(StandardError)
@@ -29,6 +29,7 @@ module ShopifyAPI
29
29
  end
30
30
 
31
31
  RateLimitError = Class.new(GraphQLError)
32
+ WarningError = Class.new(GraphQLError)
32
33
 
33
34
  class HTTPError < Error
34
35
  attr_reader :code
@@ -44,6 +45,8 @@ module ShopifyAPI
44
45
  SHOPIFY_DOMAIN = ".myshopify.com"
45
46
 
46
47
  ACCESS_TOKEN_HEADER = "X-Shopify-Access-Token"
48
+ STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token"
49
+ STOREFRONT_BUYER_IP_HEADER = "X-Shopify-Storefront-Buyer-IP"
47
50
  QUERY_COST_HEADER = "X-GraphQL-Cost-Include-Fields"
48
51
 
49
52
  DEFAULT_HEADERS = { "Content-Type" => "application/json" }.freeze
@@ -63,7 +66,7 @@ module ShopifyAPI
63
66
  *NetHttpTimeoutErrors.all
64
67
  ]
65
68
 
66
- ENDPOINT = "https://%s/admin/api%s/graphql.json" # Note that we omit the "/" after API for the case where there's no version.
69
+ ENDPOINT = "https://%s%s/api%s/graphql.json" # We omit the "/" after API for the case where there's no version
67
70
 
68
71
  ##
69
72
  #
@@ -72,18 +75,21 @@ module ShopifyAPI
72
75
  # === Arguments
73
76
  #
74
77
  # [shop (String)] Shopify domain to make requests against
75
- # [token (String)] Shopify Admin API GraphQL token
78
+ # [token (String)] Shopify Admin API or Storefront Access Token, depending on options
76
79
  # [options (Hash)] Client options. Optional.
77
80
  #
78
81
  # === Options
79
82
  #
80
83
  # [:retry (Boolean|Array)] If +false+ disable retries or an +Array+ of errors to retry. Can be HTTP status codes, GraphQL errors, or exception classes.
81
84
  # [:version (String)] Shopify API version to use. Defaults to the latest version.
85
+ # [:storefront (Boolean)] If +true+ use the Storefront API instead of Admin API. Defaults to +false+.
86
+ # [:ip (String)] Optional buyer IP address for Storefront API (sets X-Shopify-Storefront-Buyer-IP header). Only used when :storefront is +true+.
82
87
  # [:max_attempts (Integer)] Maximum number of retry attempts across all errors. Defaults to +10+
83
88
  # [:base_delay (Float)] Exponential backoff base delay. Defaults to +0.5+
84
89
  # [:jitter (Boolean)] Exponential backoff jitter (random delay added to backoff). Defaults to +true+
85
90
  # [:multiplier (Float)] Exponential backoff multiplier. Defaults to +2.0+
86
91
  # [:debug (Boolean|IO)] Output the HTTP request/response to +STDERR+ or to its value if it's an +IO+. Defaults to +false+.
92
+ # [:raise_on_warnings (Boolean)] If +true+ raise a WarningError when the GraphQL response contains warnings. Defaults to +false+.
87
93
  #
88
94
  # === Errors
89
95
  #
@@ -97,11 +103,22 @@ module ShopifyAPI
97
103
  @domain = shopify_domain(shop)
98
104
  @options = options || {}
99
105
 
106
+ @raise_on_warnings = @options[:raise_on_warnings]
107
+ @storefront = !!@options[:storefront]
108
+
100
109
  @headers = DEFAULT_HEADERS.dup
101
- @headers[ACCESS_TOKEN_HEADER] = token
110
+
111
+ if @storefront
112
+ @headers[STOREFRONT_ACCESS_TOKEN_HEADER] = token
113
+ @headers[STOREFRONT_BUYER_IP_HEADER] = @options[:ip] if @options[:ip]
114
+ else
115
+ @headers[ACCESS_TOKEN_HEADER] = token
116
+ end
117
+
102
118
  @headers[QUERY_COST_HEADER] = "true" unless @options[:retry] == false
103
119
 
104
- @endpoint = URI(sprintf(ENDPOINT, @domain, !@options[:version].to_s.strip.empty? ? "/#{@options[:version]}" : ""))
120
+ admin_path = @storefront ? "" : "/admin"
121
+ @endpoint = URI(sprintf(ENDPOINT, @domain, admin_path, !@options[:version].to_s.strip.empty? ? "/#{@options[:version]}" : ""))
105
122
  @backoff_options = DEFAULT_BACKOFF_OPTIONS.merge(@options.slice(*DEFAULT_BACKOFF_OPTIONS.keys))
106
123
 
107
124
  if @options[:debug]
@@ -137,6 +154,8 @@ module ShopifyAPI
137
154
  # rate-limited after the configured number of retry attempts
138
155
  # * A ShopifyAPI::GraphQL::Tiny::GraphQLError is raised if the response contains an +errors+ property that is
139
156
  # not a rate-limit error
157
+ # * A ShopifyAPI::GraphQL::Tiny::WarningError is raised if the +:raise_on_warnings+ option is +true+ and the
158
+ # response contains warnings
140
159
  #
141
160
  # === Returns
142
161
  #
@@ -158,7 +177,8 @@ module ShopifyAPI
158
177
  # page.dig("data", "product", "title")
159
178
  # end
160
179
  #
161
- # The block is called for each page.
180
+ # The block is called for each page. If a block is not provided returns an instance of Enumerator::Lazy that will fetch the next
181
+ # page on each iteration.
162
182
  #
163
183
  # Using pagination requires you to include the
164
184
  # {PageInfo}[https://shopify.dev/api/admin-graphql/2022-10/objects/PageInfo]
@@ -188,9 +208,13 @@ module ShopifyAPI
188
208
  # Defaults to <code>"before"</code> or <code>"after"</code>, depending on the pagination
189
209
  # direction.
190
210
  #
211
+ # === Returns
212
+ #
213
+ # +nil+ or `Enumerator::Lazy` if no block was provided
214
+ #
191
215
  # === Errors
192
216
  #
193
- # ArgumentError
217
+ # See #execute
194
218
 
195
219
  def paginate(*options)
196
220
  Pager.new(self, options)
@@ -216,12 +240,20 @@ module ShopifyAPI
216
240
  end
217
241
 
218
242
  json = parse_json(response.body)
219
- return json unless json.include?("errors")
220
243
 
221
- return make_request(query, variables) if handle_graphql_error(json)
244
+ if json.include?("errors")
245
+ return make_request(query, variables) if handle_graphql_error(json)
246
+
247
+ message = error_message(json["errors"])
248
+ raise GraphQLError.new("failed to execute query for #@domain: #{message}", json)
249
+ end
250
+
251
+ if @raise_on_warnings
252
+ warnings = find_warnings(json)
253
+ raise WarningError.new("query for #@domain returned warnings: #{warning_message(warnings)}", json) unless warnings.empty?
254
+ end
222
255
 
223
- message = error_message(json["errors"])
224
- raise GraphQLError.new("failed to execute query for #@domain: #{message}", json)
256
+ json
225
257
  end
226
258
 
227
259
  def post(query, variables = nil)
@@ -318,6 +350,34 @@ module ShopifyAPI
318
350
  end.join(", ")
319
351
  end
320
352
 
353
+ def find_warnings(data, prop = nil)
354
+ case data
355
+ when Hash
356
+ data.flat_map do |key, value|
357
+ if key == "warnings" && value.is_a?(Array)
358
+ value.map { |w| [prop, w] }
359
+ else
360
+ find_warnings(value, key)
361
+ end
362
+ end
363
+ when Array
364
+ data.flat_map { |value| find_warnings(value, prop) }
365
+ else
366
+ []
367
+ end
368
+ end
369
+
370
+ def warning_message(warnings)
371
+ warnings.map do |prop, warning|
372
+ next warning.to_s unless warning.is_a?(Hash)
373
+
374
+ field = Array(warning["field"]).join(".")
375
+ parts = [prop]
376
+ parts << field unless field.empty?
377
+ "#{parts.join(" ")}: #{warning["message"]}"
378
+ end.join("\n")
379
+ end
380
+
321
381
  def request_attempts_remain?
322
382
  @request_attempts < @backoff_options[:max_attempts]
323
383
  end
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Skye Shaw"]
10
10
  spec.email = ["skye.shaw@gmail.com"]
11
11
 
12
- spec.summary = %q{Lightweight, no-nonsense, Shopify GraphQL Admin API client with built-in pagination and retry}
12
+ spec.summary = %q{Lightweight, no-nonsense, Shopify Admin & Storefront GraphQL API client with built-in pagination and retry}
13
13
  spec.homepage = "https://github.com/ScreenStaring/shopify_api-graphql-tiny"
14
14
  spec.license = "MIT"
15
15
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shopify_api-graphql-tiny
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Skye Shaw
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-01-24 00:00:00.000000000 Z
10
+ date: 2026-06-25 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: net_http_timeout_errors
@@ -123,6 +123,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
123
  requirements: []
124
124
  rubygems_version: 3.6.2
125
125
  specification_version: 4
126
- summary: Lightweight, no-nonsense, Shopify GraphQL Admin API client with built-in
127
- pagination and retry
126
+ summary: Lightweight, no-nonsense, Shopify Admin & Storefront GraphQL API client with
127
+ built-in pagination and retry
128
128
  test_files: []