shopify_api-graphql-tiny 0.2.0 → 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: be69f222bb991d00b37c44d8aea2ad9caee713527ce23c60121a401d8d2a8b39
4
- data.tar.gz: 5a35aaada946e0f0ef1dc50ea7962da2252a7f70a111a911f660b1c8eab3a79a
3
+ metadata.gz: c827a0b468aeb34cfb05760a7a1a25e641c61f21f6d2d15c4ed8a05b29bd383b
4
+ data.tar.gz: 23b357593431fb8beda6a29689b6b90e4101fde3cac2eaaad38c7a2cb48f1437
5
5
  SHA512:
6
- metadata.gz: be94db6e155cc11d0136a229e6ee728ac808c655896f5489a657fc4ba82b7d5ab8403989bc356cfaaa617766077220e8234c60ff6d9aa9a651d484490cb161c3
7
- data.tar.gz: 166c19ddde4a76d8d216bffac16d3844c9add39f9884eb70adac3477ce5b222e19cba820c1bae1204f58424239be38a1ec063d8e75a977f337f0d5df5afc6452
6
+ metadata.gz: 7fe6d5f27b0936703ce4f7f45875406ab11069d41f8e83e9d57a873ce5dd63d1093e88219cd8b5b69eed2bdd3b39bb233292360aa8bb4534f5a799690cceab5f
7
+ data.tar.gz: d7231b2be188d9bedbfa92f6de69a0ee4c082b8801633db73c570c7cef6c6a349afc81e942e2f36b685edf26a02d9e74817595d7b4988298f42092182a49a718
data/.env.template CHANGED
@@ -4,11 +4,13 @@
4
4
  # 2. Fill-in the below variables with valid values.
5
5
  # These values will be used by the tests. Data will be written to SHOPIFY_DOMAIN
6
6
  #
7
+
8
+ # Must be full domain as it's used in test assertions
7
9
  SHOPIFY_DOMAIN=
8
10
  SHOPIFY_TOKEN=
9
11
 
10
- # Will result in the creation of a private metadafield
12
+ # Will result in the creation of a metadafield on the customer. write_customers permission required
11
13
  SHOPIFY_CUSTOMER_ID=
12
14
 
13
- # Must have more than 1 variant
15
+ # Must have more than 1 variant values
14
16
  SHOPIFY_PRODUCT_ID=
@@ -15,10 +15,10 @@ jobs:
15
15
 
16
16
  strategy:
17
17
  matrix:
18
- ruby: ["3.2", "3.1", "3.0", "2.7.2", "2.6.6", "2.5.8", "2.4.10"]
18
+ ruby: ['3.4', '3.3', '3.2', '3.1', '3.0', '2.7']
19
19
 
20
20
  steps:
21
- - uses: actions/checkout@v2
21
+ - uses: actions/checkout@v4
22
22
  - uses: ruby/setup-ruby@v1
23
23
  with:
24
24
  bundler-cache: true
data/Changes CHANGED
@@ -1,3 +1,12 @@
1
+ 2026-01-20 v1.0.0
2
+ --------------------
3
+ * Add support for exponential backoff and remove ShopifyAPIRetry
4
+ * Support HTTP status and Shopify error codes as retry options
5
+
6
+ 2026-01-20 v0.2.0
7
+ --------------------
8
+ * Deprecate :retry with a Hash value
9
+
1
10
  2023-02-12 v0.1.1
2
11
  --------------------
3
12
  * Add pagination support
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 GraphQL Admin 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,69 @@ GQL
74
74
  p result.dig("data", "customerUpdate", "userErrors")
75
75
  ```
76
76
 
77
+ ### Automatically Retrying Failed Requests
78
+
79
+ 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.
80
+
81
+ When a request is rate-limited by Shopify retry occurs according to [Shopify's `throttleStatus`](https://shopify.dev/docs/api/admin-graphql/unstable#rate-limits)
82
+
83
+ When a request fails due to an exception or non-200 HTTP status a retry will be attempted after an exponential backoff waiting period.
84
+ This is controlled by `ShopifyAPI::GraphQL::Tiny::DEFAULT_BACKOFF_OPTIONS`. It contains:
85
+
86
+ * `:base_delay` - `0.5`
87
+ * `:jitter` - `true`
88
+ * `:max_attempts` - `10`
89
+ * `:max_delay` - `60`
90
+ * `:multiplier` - `2.0`
91
+
92
+ `:max_attempts` dictates how many retry attempts will be made **for all** types of retries.
93
+
94
+ These can be overridden globally (by assigning to the constant) or per instance:
95
+
96
+ ```rb
97
+ gql = ShopifyAPI::GraphQL::Tiny.new(shop, token, :max_attempts => 20, :max_delay => 90)
98
+ ```
99
+
100
+ `ShopifyAPI::GraphQL::Tiny::DEFAULT_RETRY_ERRORS` determines what is retried. It contains and HTTP statuses codes, Shopify GraphQL errors codes, and exceptions.
101
+ By default it contains:
102
+
103
+ * `"5XX"` - Any HTTP 5XX status
104
+ * `"INTERNAL_SERVER_ERROR"` - Shopify GraphQL error code
105
+ * `"TIMEOUT"` - Shopify GraphQL error code
106
+ * `EOFError`
107
+ * `Errno::ECONNABORTED`
108
+ * `Errno::ECONNREFUSED`
109
+ * `Errno::ECONNRESET`
110
+ * `Errno::EHOSTUNREACH`
111
+ * `Errno::EINVAL`
112
+ * `Errno::ENETUNREACH`
113
+ * `Errno::ENOPROTOOPT`
114
+ * `Errno::ENOTSOCK`
115
+ * `Errno::EPIPE`
116
+ * `Errno::ETIMEDOUT`
117
+ * `Net::HTTPBadResponse`
118
+ * `Net::HTTPHeaderSyntaxError`
119
+ * `Net::ProtocolError`
120
+ * `Net::ReadTimeout`
121
+ * `OpenSSL::SSL::SSLError`
122
+ * `SocketError`
123
+ * `Timeout::Error`
124
+
125
+ These can be overridden globally (by assigning to the constant) or per instance:
126
+
127
+ ```rb
128
+ # Only retry on 2 errors
129
+ gql = ShopifyAPI::GraphQL::Tiny.new(shop, token, :retry => [SystemCallError, "500"])
130
+ ```
131
+
132
+ #### Disabling Automatic Retry
133
+
134
+ To disable retries set the `:retry` option to `false`:
135
+
136
+ ```rb
137
+ gql = ShopifyAPI::GraphQL::Tiny.new(shop, token, :retry => false)
138
+ ```
139
+
77
140
  ### Pagination
78
141
 
79
142
  In addition to built-in request retry `ShopifyAPI::GraphQL::Tiny` also builds in support for pagination.
@@ -196,19 +259,21 @@ pager.execute(query) { |page| }
196
259
 
197
260
  The `"data"` and `"pageInfo"` keys are automatically added if not provided.
198
261
 
199
- ### Automatically Retrying Failed Requests
200
-
201
- See [the docs](https://rubydoc.info/gems/shopify_api-graphql-tiny) for more information.
202
-
203
262
  ## Testing
204
263
 
205
264
  `cp env.template .env` and fill-in `.env` with the missing values. This requires a Shopify store.
206
265
 
266
+ To elicit a request that will be rate-limited by Shopify run following Rake task:
267
+
268
+ ```sh
269
+ bundle exec rake rate_limit SHOPIFY_DOMAIN=your-domain SHOPIFY_TOKEN=your-token
270
+ ```
271
+
207
272
  ## See Also
208
273
 
209
274
  - [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
210
- - [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
211
- - [ShopifyAPIRetry](https://github.com/ScreenStaring/shopify_api_retry) - Retry a ShopifyAPI request if rate-limited or other errors occur (REST and GraphQL APIs)
275
+ - [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
276
+ - [`TinyGID`](https://github.com/sshaw/tiny_gid/) - Build Global ID (gid://) URI strings from scalar values
212
277
 
213
278
  ## License
214
279
 
data/Rakefile CHANGED
@@ -4,3 +4,161 @@ require "rspec/core/rake_task"
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
6
  task :default => :spec
7
+
8
+ desc "Elicit a Shopify rate limit"
9
+ task :rate_limit do
10
+ require "shopify_api/graphql/tiny"
11
+
12
+ query =<<-GQL
13
+ query {
14
+ products(first: 50, sortKey: UPDATED_AT, reverse: true) {
15
+ pageInfo {
16
+ hasNextPage
17
+ endCursor
18
+ }
19
+ edges {
20
+ node {
21
+ id
22
+ title
23
+ handle
24
+ status
25
+ createdAt
26
+ updatedAt
27
+ publishedAt
28
+ vendor
29
+ productType
30
+ tags
31
+ descriptionHtml
32
+ description
33
+ onlineStoreUrl
34
+ options(first: 3) {
35
+ id
36
+ name
37
+ position
38
+ values
39
+ }
40
+ variants(first: 80) {
41
+ edges {
42
+ node {
43
+ id
44
+ title
45
+ price
46
+ compareAtPrice
47
+ sku
48
+ barcode
49
+ inventoryItem {
50
+ id
51
+ unitCost {
52
+ amount
53
+ currencyCode
54
+ }
55
+ countryCodeOfOrigin
56
+ harmonizedSystemCode
57
+ }
58
+ inventoryPolicy
59
+ taxable
60
+ availableForSale
61
+ metafields(first: 20) {
62
+ edges {
63
+ node {
64
+ id
65
+ namespace
66
+ key
67
+ value
68
+ type
69
+ description
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ media(first: 20) {
77
+ edges {
78
+ node {
79
+ __typename
80
+ alt
81
+ status
82
+ ... on MediaImage {
83
+ id
84
+ preview {
85
+ image {
86
+ url
87
+ }
88
+ }
89
+ image {
90
+ id
91
+ url
92
+ width
93
+ height
94
+ altText
95
+ }
96
+ }
97
+ ... on Video {
98
+ id
99
+ sources {
100
+ url
101
+ format
102
+ height
103
+ width
104
+ }
105
+ }
106
+ ... on ExternalVideo {
107
+ id
108
+ originUrl
109
+ embedUrl
110
+ }
111
+ }
112
+ }
113
+ }
114
+ images(first: 20) {
115
+ edges {
116
+ node {
117
+ id
118
+ url
119
+ altText
120
+ width
121
+ height
122
+ }
123
+ }
124
+ }
125
+ seo {
126
+ title
127
+ description
128
+ }
129
+ metafields(first: 30) {
130
+ edges {
131
+ node {
132
+ id
133
+ namespace
134
+ key
135
+ value
136
+ type
137
+ ownerType
138
+ }
139
+ }
140
+ }
141
+ collections(first: 10) {
142
+ edges {
143
+ node {
144
+ id
145
+ title
146
+ handle
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ GQL
155
+
156
+ threads = 5.times.map do
157
+ Thread.new do
158
+ gql = ShopifyAPI::GQL::Tiny.new(ENV.fetch("SHOPIFY_DOMAIN"), ENV.fetch("SHOPIFY_TOKEN"), :debug => true)
159
+ pp gql.execute(query).dig("extensions", "cost", "throttleStatus")
160
+ end
161
+ end
162
+
163
+ threads.each(&:join)
164
+ end
@@ -3,7 +3,7 @@
3
3
  module ShopifyAPI
4
4
  module GraphQL
5
5
  class Tiny
6
- VERSION = "0.2.0"
6
+ VERSION = "1.0.0"
7
7
  end
8
8
  end
9
9
  end
@@ -2,18 +2,22 @@
2
2
  require "json"
3
3
  require "net/http"
4
4
 
5
- require "shopify_api_retry"
5
+ require "net_http_timeout_errors"
6
6
  require "shopify_api/graphql/tiny/version"
7
7
 
8
8
  module ShopifyAPI
9
9
  module GraphQL
10
10
  ##
11
- # Client to make Shopify GraphQL Admin API requests with built-in retries.
11
+ # Lightweight, no-nonsense, Shopify GraphQL Admin API client with built-in pagination and retry
12
12
  #
13
13
  class Tiny
14
14
  Error = Class.new(StandardError)
15
15
  ConnectionError = Class.new(Error)
16
16
 
17
+ ERROR_CODE_THROTTLED = "THROTTLED"
18
+ ERROR_CODE_TIMEOUT = "TIMEOUT"
19
+ ERROR_CODE_SERVER_ERROR = "INTERNAL_SERVER_ERROR"
20
+
17
21
  class GraphQLError < Error
18
22
  # Hash of failed GraphQL response
19
23
  attr_reader :response
@@ -44,18 +48,26 @@ module ShopifyAPI
44
48
 
45
49
  DEFAULT_HEADERS = { "Content-Type" => "application/json" }.freeze
46
50
 
47
- # Retry rules to be used for all instances if no rules are specified via the +:retry+ option when creating an instance
48
- DEFAULT_RETRY_OPTIONS = {
49
- ConnectionError => { :wait => 3, :tries => 20 },
50
- GraphQLError => { :wait => 3, :tries => 20 },
51
- HTTPError => { :wait => 3, :tries => 20 }
51
+ DEFAULT_BACKOFF_OPTIONS = {
52
+ :base_delay => 0.5,
53
+ :jitter => true,
54
+ :max_attempts => 10,
55
+ :max_delay => 60,
56
+ :multiplier => 2.0
52
57
  }
53
58
 
59
+ DEFAULT_RETRY_ERRORS = [
60
+ "5XX",
61
+ ERROR_CODE_SERVER_ERROR,
62
+ ERROR_CODE_TIMEOUT,
63
+ *NetHttpTimeoutErrors.all
64
+ ]
65
+
54
66
  ENDPOINT = "https://%s/admin/api%s/graphql.json" # Note that we omit the "/" after API for the case where there's no version.
55
67
 
56
68
  ##
57
69
  #
58
- # Create a new GraphQL client.
70
+ # Create a new GraphQL client
59
71
  #
60
72
  # === Arguments
61
73
  #
@@ -65,8 +77,12 @@ module ShopifyAPI
65
77
  #
66
78
  # === Options
67
79
  #
68
- # [:retry (Boolean|Hash)] +Hash+ can be retry config options. For the format see {ShopifyAPIRetry}[https://github.com/ScreenStaring/shopify_api_retry/#usage]. Defaults to +true+
80
+ # [:retry (Boolean|Array)] If +false+ disable retries or an +Array+ of errors to retry. Can be HTTP status codes, GraphQL errors, or exception classes.
69
81
  # [:version (String)] Shopify API version to use. Defaults to the latest version.
82
+ # [:max_attempts (Integer)] Maximum number of retry attempts across all errors. Defaults to +10+
83
+ # [:base_delay (Float)] Exponential backoff base delay. Defaults to +0.5+
84
+ # [:jitter (Boolean)] Exponential backoff jitter (random delay added to backoff). Defaults to +true+
85
+ # [:multiplier (Float)] Exponential backoff multiplier. Defaults to +2.0+
70
86
  # [:debug (Boolean|IO)] Output the HTTP request/response to +STDERR+ or to its value if it's an +IO+. Defaults to +false+.
71
87
  #
72
88
  # === Errors
@@ -83,17 +99,27 @@ module ShopifyAPI
83
99
 
84
100
  @headers = DEFAULT_HEADERS.dup
85
101
  @headers[ACCESS_TOKEN_HEADER] = token
86
- @headers[QUERY_COST_HEADER] = "true" if retry?
102
+ @headers[QUERY_COST_HEADER] = "true" unless @options[:retry] == false
87
103
 
88
104
  @endpoint = URI(sprintf(ENDPOINT, @domain, !@options[:version].to_s.strip.empty? ? "/#{@options[:version]}" : ""))
105
+ @backoff_options = DEFAULT_BACKOFF_OPTIONS.merge(@options.slice(*DEFAULT_BACKOFF_OPTIONS.keys))
106
+
107
+ if @options[:debug]
108
+ @debug = @options[:debug].is_a?(IO) ? @options[:debug] : $stderr
109
+ end
89
110
 
90
- if @options[:retry].is_a?(Hash)
91
- warn "DEPRECATION WARNING from #{self.class}: specifying retry options as a Hash via the :retry option is deprecated and will be removed in v1.0"
111
+ case @options[:retry]
112
+ when false
113
+ @retryable = []
114
+ when Array
115
+ @retryable = @options[:retry]
116
+ else
117
+ @retryable = DEFAULT_RETRY_ERRORS
92
118
  end
93
119
  end
94
120
 
95
121
  #
96
- # Execute a GraphQL query or mutation.
122
+ # Execute a GraphQL query or mutation
97
123
  #
98
124
  # === Arguments
99
125
  #
@@ -104,6 +130,8 @@ module ShopifyAPI
104
130
  #
105
131
  # ArgumentError, ConnectionError, HTTPError, RateLimitError, GraphQLError
106
132
  #
133
+ # Outside of ArgumentError these are raised after exhausing the configured retry.
134
+ #
107
135
  # * An ShopifyAPI::GraphQL::Tiny::HTTPError is raised of the response does not have 200 status code
108
136
  # * A ShopifyAPI::GraphQL::Tiny::RateLimitError is raised if rate-limited and retries are disabled or if still
109
137
  # rate-limited after the configured number of retry attempts
@@ -117,8 +145,9 @@ module ShopifyAPI
117
145
  def execute(q, variables = nil)
118
146
  raise ArgumentError, "query required" if q.nil? || q.to_s.strip.empty?
119
147
 
120
- config = retry? ? @options[:retry] || DEFAULT_RETRY_OPTIONS : {}
121
- ShopifyAPIRetry::GraphQL.retry(config) { post(q, variables) }
148
+ @request_attempts = 0
149
+
150
+ make_request(q, variables)
122
151
  end
123
152
 
124
153
  ##
@@ -169,57 +198,134 @@ module ShopifyAPI
169
198
 
170
199
  private
171
200
 
172
- def retry?
173
- @options[:retry] != false
174
- end
201
+ def make_request(query, variables = nil)
202
+ response = nil
203
+ exceptions = @retryable.select { |target| target.is_a?(Class) }
175
204
 
176
- def shopify_domain(host)
177
- domain = host.sub(%r{\Ahttps?://}i, "")
178
- domain << SHOPIFY_DOMAIN unless domain.end_with?(SHOPIFY_DOMAIN)
179
- domain
205
+ begin
206
+ @request_attempts += 1
207
+ response = post(query, variables)
208
+ rescue => e
209
+ retry if exceptions.include?(e.class) && wait_to_retry
210
+ raise ConnectionError.new("failed to execute query for #@domain: #{e.message}")
211
+ end
212
+
213
+ if response.code != "200"
214
+ return make_request(query, variables) if handle_http_error(response.code)
215
+ raise HTTPError.new("failed to execute query for #@domain: #{response.body}", response.code)
216
+ end
217
+
218
+ json = parse_json(response.body)
219
+ return json unless json.include?("errors")
220
+
221
+ return make_request(query, variables) if handle_graphql_error(json)
222
+
223
+ message = error_message(json["errors"])
224
+ raise GraphQLError.new("failed to execute query for #@domain: #{message}", json)
180
225
  end
181
226
 
182
227
  def post(query, variables = nil)
183
- begin
184
- # Newer versions of Ruby
185
- # response = Net::HTTP.post(@endpoint, query, @headers)
186
- params = { :query => query }
187
- params[:variables] = variables if variables
228
+ # Newer versions of Ruby:
229
+ # response = Net::HTTP.post(@endpoint, query, @headers)
230
+ params = { :query => query }
231
+ params[:variables] = variables if variables
188
232
 
189
- post = Net::HTTP::Post.new(@endpoint.path)
190
- post.body = params.to_json
191
- post["User-Agent"] = USER_AGENT
233
+ post = Net::HTTP::Post.new(@endpoint.path)
234
+ post.body = params.to_json
235
+ post["User-Agent"] = USER_AGENT
192
236
 
193
- @headers.each { |k,v| post[k] = v }
237
+ @headers.each { |k,v| post[k] = v }
194
238
 
195
- request = Net::HTTP.new(@endpoint.host, @endpoint.port)
196
- request.use_ssl = true
239
+ request = Net::HTTP.new(@endpoint.host, @endpoint.port)
240
+ request.use_ssl = true
241
+ request.set_debug_output(@debug) if @debug
197
242
 
198
- if @options[:debug]
199
- request.set_debug_output(
200
- @options[:debug].is_a?(IO) ? @options[:debug] : $stderr
201
- )
202
- end
243
+ request.start { |http| http.request(post) }
244
+ end
203
245
 
204
- response = request.start { |http| http.request(post) }
205
- rescue => e
206
- raise ConnectionError, "request to #@endpoint failed: #{e}"
246
+ def handle_graphql_error(json)
247
+ errors = json["errors"]
248
+ codes = errors.map { |error| error.dig("extensions", "code") }
249
+
250
+ if codes.include?(ERROR_CODE_THROTTLED)
251
+ return true if wait_for_shopify_retry(json.dig("extensions", "cost"))
252
+
253
+ raise RateLimitError.new(error_message(errors), json)
207
254
  end
208
255
 
209
- # TODO: Even if non-200 check if JSON. See: https://shopify.dev/api/admin-graphql
210
- prefix = "failed to execute query for #@domain: "
211
- raise HTTPError.new("#{prefix}#{response.body}", response.code) if response.code != "200"
256
+ return true if @retryable.any? { |error| codes.include?(error) } && wait_to_retry
212
257
 
213
- json = JSON.parse(response.body)
214
- return json unless json.include?("errors")
258
+ false
259
+ end
215
260
 
216
- errors = json["errors"].map { |e| e["message"] }.join(", ")
217
- if json.dig("errors", 0, "extensions", "code") == "THROTTLED"
218
- raise RateLimitError.new(errors, json) unless retry?
219
- return json
220
- end
261
+ def handle_http_error(status)
262
+ return false unless @retryable.include?(status) || @retryable.any? { |error| error.is_a?(String) && error.size == 3 && error.end_with?("XX") && error[0] == status[0] }
263
+
264
+ wait_to_retry
265
+ end
266
+
267
+ def wait_to_retry
268
+ return false unless request_attempts_remain?
269
+
270
+ backoff(@request_attempts)
271
+
272
+ true
273
+ end
274
+
275
+ def wait_for_shopify_retry(cost)
276
+ return false if cost.nil? || cost["actualQueryCost"] || !request_attempts_remain?
277
+
278
+ status = cost["throttleStatus"]
279
+ time = (cost["requestedQueryCost"] - status["currentlyAvailable"]) / status["restoreRate"]
280
+
281
+ debug("retrying rate-limited request (retry count: #@request_attempts, status: #{status}, sleep: #{time})")
282
+
283
+ sleep(time)
284
+
285
+ true
286
+ end
287
+
288
+ def backoff(attempts)
289
+ delay = @backoff_options[:base_delay] * (@backoff_options[:multiplier] ** (attempts - 1))
290
+ delay = [delay, @backoff_options[:max_delay]].min if @backoff_options[:max_delay]
291
+ delay = rand * delay if @backoff_options[:jitter]
292
+
293
+ debug("backoff sleeping for #{delay}")
294
+
295
+ sleep(delay)
296
+ end
297
+
298
+ def shopify_domain(host)
299
+ domain = host.sub(%r{\Ahttps?://}i, "")
300
+ domain << SHOPIFY_DOMAIN unless domain.end_with?(SHOPIFY_DOMAIN)
301
+ domain
302
+ end
303
+
304
+ def parse_json(json)
305
+ JSON.parse(json)
306
+ rescue JSON::ParserError => e
307
+ raise Error, "failed to parse JSON response: #{e.message}"
308
+ end
309
+
310
+ def error_message(errors)
311
+ errors.map do |e|
312
+ message = e["message"]
313
+
314
+ path = e["path"]
315
+ message << sprintf(" at %s", path.join(".")) if path
316
+
317
+ message
318
+ end.join(", ")
319
+ end
320
+
321
+ def request_attempts_remain?
322
+ @request_attempts < @backoff_options[:max_attempts]
323
+ end
324
+
325
+ def debug(message)
326
+ return unless @debug
221
327
 
222
- raise GraphQLError.new(prefix + errors, json)
328
+ @debug.puts "#{self.class}: #{message}"
223
329
  end
224
330
  end
225
331
 
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
28
28
  "source_code_uri" => "https://github.com/ScreenStaring/shopify_api-graphql-tiny",
29
29
  }
30
30
 
31
- spec.add_dependency "shopify_api_retry", "~> 0.2"
31
+ spec.add_dependency "net_http_timeout_errors"
32
32
  spec.add_development_dependency "webmock", "~> 3.0"
33
33
  spec.add_development_dependency "bundler"
34
34
  spec.add_development_dependency "rake", ">= 12.3.3"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shopify_api-graphql-tiny
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Skye Shaw
@@ -10,19 +10,19 @@ cert_chain: []
10
10
  date: 2026-01-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: shopify_api_retry
13
+ name: net_http_timeout_errors
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '0.2'
18
+ version: '0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - "~>"
23
+ - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '0.2'
25
+ version: '0'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: webmock
28
28
  requirement: !ruby/object:Gem::Requirement