Lightweight, no-nonsense, Shopify GraphQL Admin API client with built-in pagination and retry
Add this line to your application's Gemfile:
gem "shopify_api-graphql-tiny"And then execute:
bundleOr install it yourself as:
gem install shopify_api-graphql-tinyrequire "shopify_api/graphql/tiny"
gql = ShopifyAPI::GraphQL::Tiny.new("my-shop", token)
# Automatically retried
result = gql.execute(<<-GQL, :id => "gid://shopify/Customer/1283599123")
query findCustomer($id: ID!) {
customer(id: $id) {
id
tags
metafields(first: 10 namespace: "foo") {
edges {
node {
id
key
value
}
}
}
}
}
GQL
customer = result["data"]["customer"]
p customer["tags"]
p customer.dig("metafields", "edges", 0, "node")["value"]
updates = { :id => customer["id"], :tags => customer["tags"] + %w[foo bar] }
# Automatically retried as well
result = gql.execute(<<-GQL, :input => updates)
mutation customerUpdate($input: CustomerInput!) {
customerUpdate(input: $input) {
customer {
id
}
userErrors {
field
message
}
}
}
GQL
p result.dig("data", "customerUpdate", "userErrors")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.
When a request is rate-limited by Shopify retry occurs according to Shopify's throttleStatus
When a request fails due to an exception or non-200 HTTP status a retry will be attempted after an exponential backoff waiting period.
This is controlled by ShopifyAPI::GraphQL::Tiny::DEFAULT_BACKOFF_OPTIONS. It contains:
:base_delay-0.5:jitter-true:max_attempts-10:max_delay-60:multiplier-2.0
:max_attempts dictates how many retry attempts will be made for all types of retries.
These can be overridden globally (by assigning to the constant) or per instance:
gql = ShopifyAPI::GraphQL::Tiny.new(shop, token, :max_attempts => 20, :max_delay => 90)ShopifyAPI::GraphQL::Tiny::DEFAULT_RETRY_ERRORS determines what is retried. It contains and HTTP statuses codes, Shopify GraphQL errors codes, and exceptions.
By default it contains:
"5XX"- Any HTTP 5XX status"INTERNAL_SERVER_ERROR"- Shopify GraphQL error code"TIMEOUT"- Shopify GraphQL error codeEOFErrorErrno::ECONNABORTEDErrno::ECONNREFUSEDErrno::ECONNRESETErrno::EHOSTUNREACHErrno::EINVALErrno::ENETUNREACHErrno::ENOPROTOOPTErrno::ENOTSOCKErrno::EPIPEErrno::ETIMEDOUTNet::HTTPBadResponseNet::HTTPHeaderSyntaxErrorNet::ProtocolErrorNet::ReadTimeoutOpenSSL::SSL::SSLErrorSocketErrorTimeout::Error
These can be overridden globally (by assigning to the constant) or per instance:
# Only retry on 2 errors
gql = ShopifyAPI::GraphQL::Tiny.new(shop, token, :retry => [SystemCallError, "500"])To disable retries set the :retry option to false:
gql = ShopifyAPI::GraphQL::Tiny.new(shop, token, :retry => false)In addition to built-in request retry ShopifyAPI::GraphQL::Tiny also builds in support for pagination.
Using pagination requires you to include the Shopify PageInfo object
in your queries and wrap them in a function that accepts a page/cursor argument.
The pager's #execute is like the non-paginated #execute method and accepts additional, non-pagination query arguments:
gql = ShopifyAPI::GraphQL::Tiny.new("my-shop", token)
pager = gql.paginate
pager.execute(query, :foo => 123)And it accepts a block which will be passed each page returned by the query:
pager.execute(query, :foo => 123) do |page|
# do something with each page
endTo use after pagination, i.e., to paginate forward, your query must:
- Make the page/cursor argument optional
- Include
PageInfo'shasNextPageandendCursorfields
For example:
FIND_ORDERS = <<-GQL
query findOrders($after: String) {
orders(first: 10 after: $after) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
email
}
}
}
}
GQL
pager = gql.paginate # This is the same as gql.paginate(:after)
pager.execute(FIND_ORDERS) do |page|
orders = page.dig("data", "orders", "edges")
orders.each do |order|
# ...
end
endBy default it is assumed your GraphQL query uses a variable named $after. You can specify a different name using the :variable
option:
pager = gql.paginate(:after, :variable => "yourVariable")To use before pagination, i.e. to paginate backward, your query must:
- Make the page/cursor argument required
- Include the
PageInfo'shasPreviousPageandstartCursorfields - Specify the
:beforeargument to#paginate
For example:
FIND_ORDERS = <<-GQL
query findOrders($before: String) {
orders(last: 10 before: $before) {
pageInfo {
hasPreviousPage
startCursor
}
edges {
node {
id
email
}
}
}
}
GQL
pager = gql.paginate(:before)
pager.execute(FIND_ORDERS) do |page|
# ...
endBy default it is assumed your GraphQL query uses a variable named $before. You can specify a different name using the :variable
option:
pager = gql.paginate(:before, :variable => "yourVariable")By default ShopifyAPI::GraphQL::Tiny will use the first pageInfo block with a next or previous page it finds
in the GraphQL response. If necessary you can specify an explicit location for the pageInfo block:
pager = gql.paginate(:after => %w[some path to it])
pager.execute(query) { |page| }
pager = gql.paginate(:after => ->(data) { data.dig("some", "path", "to", "it") })
pager.execute(query) { |page| }The "data" and "pageInfo" keys are automatically added if not provided.
cp env.template .env and fill-in .env with the missing values. This requires a Shopify store.
To elicit a request that will be rate-limited by Shopify run following Rake task:
bundle exec rake rate_limit SHOPIFY_DOMAIN=your-domain SHOPIFY_TOKEN=your-token- Shopify Dev Tools - Command-line program to assist with the development and/or maintenance of Shopify apps and stores
- Shopify ID Export - Dump Shopify product and variant IDs —along with other identifiers— to a CSV or JSON file
TinyGID- Build Global ID (gid://) URI strings from scalar values
The gem is available as open source under the terms of the MIT License.
Made by ScreenStaring