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 HTTP status codes, Shopify GraphQL errors codes, and exception classes.
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
endIf a block is not given an Enumerator::Lazy instance is returned that will fetch the next page upon each iteration:
results = pager.execute(query, :foo => 123)
results.each { |page| ... }To 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.
- Easy-to-use
- Built-in retry
- Built-in pagination
- Lightweight
Overall, Shopify's API client is bloated trash that will give you development headaches and long-term maintenance nightmares.
We used to use it, staring way back in 2015, but eventually had to pivot away from their Ruby libraries due to developer frustration and high maintenance cost (and don't get us started on the ShopifyApp gem!@#).
For more information see: Shopify/shopify-api-ruby#1181
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-tokenShopifyAPI::GraphQL::Request- A higher-level wrapper around this class with improved exception handling and:snake_casehash key conversion- 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