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 +4 -4
- data/.env.template +2 -1
- data/Changes +8 -0
- data/README.md +52 -3
- data/lib/shopify_api/graphql/tiny/version.rb +1 -1
- data/lib/shopify_api/graphql/tiny.rb +71 -11
- data/shopify_api-graphql-tiny.gemspec +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 28d098f2ad608cf375916eb9efdf9c3278b514fc632058a835c1566989a4538b
|
|
4
|
+
data.tar.gz: 0a1b2d891a11116279b0169d2d093589148538f3c8abecaa35a5bd0d3bb2363a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
3
|
+
Lightweight, no-nonsense, Shopify Admin & Storefront GraphQL API client with built-in pagination and retry
|
|
4
4
|
|
|
5
5
|
[](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](
|
|
340
|
+
Made by [ScreenStaring](https://screenstaring.com)
|
|
@@ -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/
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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: []
|