项目作者: kirillplatonov

项目描述 :
Less painful way to work with Shopify Graphql API in Ruby.
高级语言: Ruby
项目地址: git://github.com/kirillplatonov/shopify_graphql.git
创建时间: 2021-07-09T08:03:53Z
项目社区:https://github.com/kirillplatonov/shopify_graphql

开源协议:MIT License

下载


Shopify Graphql

Less painful way to work with Shopify Graphql API in Ruby. This library is a tiny wrapper on top of shopify_api gem. It provides a simple API for Graphql calls, better error handling, and Graphql webhooks integration.

Features

  • Simple API for Graphql queries and mutations
  • Conventions for organizing Graphql code
  • ActiveResource-like error handling
  • Graphql and user error handlers
  • Auto-conversion of responses to OpenStruct
  • Graphql webhooks integration for Rails
  • Wrappers for Graphql rate limit extensions
  • Built-in calls for common Graphql calls

Dependencies

For shopify_api < v10 use 0-4-stable branch.

Installation

Add shopify_graphql to your Gemfile:

  1. bundle add shopify_graphql

This gem relies on shopify_app for authentication so no extra setup is required. But you still need to wrap your Graphql calls with shop.with_shopify_session:

  1. shop.with_shopify_session do
  2. # your calls to graphql
  3. end

Conventions

To better organize your Graphql code use the following conventions:

  • Create wrappers for all of your queries and mutations to isolate them
  • Put all Graphql-related code into app/graphql folder
  • Use Fields suffix to name fields (eg AppSubscriptionFields)
  • Use Get prefix to name queries (eg GetProducts or GetAppSubscription)
  • Use imperative to name mutations (eg CreateUsageSubscription or BulkUpdateVariants)

Usage examples

Simple query

Click to expand
Definition:

rb # app/graphql/get_product.rb class GetProduct include ShopifyGraphql::Query QUERY = <<~GRAPHQL query($id: ID!) { product(id: $id) { handle title description } } GRAPHQL def call(id:) response = execute(QUERY, id: id) response.data = response.data.product response end end

Usage:

rb product = GetProduct.call(id: "gid://shopify/Product/12345").data puts product.handle puts product.title

Query with custom headers

Click to expand
You can pass custom headers to any GraphQL query or mutation by using the headers parameter. A common use case is setting the Accept-Language header to retrieve content in specific languages:

rb # Pass custom headers to a direct GraphQL call to get French content response = ShopifyGraphql.execute(QUERY, headers: { "Accept-Language" => "fr" }) # Or create a language-aware query wrapper class GetProduct include ShopifyGraphql::Query QUERY = <<~GRAPHQL query($id: ID!) { product(id: $id) { id title description seo { title description } } } GRAPHQL def call(id:, language: nil) headers = language ? { "Accept-Language" => language } : nil response = execute(QUERY, headers: headers, id: id) response.data = response.data.product response end end # Then use it to get content in different languages french_product = GetProduct.call( id: "gid://shopify/Product/12345", language: "fr" ).data puts french_product.title # => "Le Produit" puts french_product.description # => "Description en français" # Get content in Japanese japanese_product = GetProduct.call( id: "gid://shopify/Product/12345", language: "ja" ).data puts japanese_product.title # => "商品名" puts japanese_product.description # => "商品の説明"

The Accept-Language header tells Shopify which language to return the content in. This is particularly useful for:
- Retrieving translated content for products, collections, and pages
- Building multi-language storefronts
- Showing localized SEO content

You can also use custom headers for other purposes like passing metadata or context with your GraphQL requests.

Query with data parsing

Click to expand
Definition:

rb # app/graphql/get_product.rb class GetProduct include ShopifyGraphql::Query QUERY = <<~GRAPHQL query($id: ID!) { product(id: $id) { id title featuredImage { source: url } } } GRAPHQL def call(id:) response = execute(QUERY, id: id) response.data = parse_data(response.data.product) response end private def parse_data(data) OpenStruct.new( id: data.id, title: data.title, featured_image: data.featuredImage&.source ) end end

Usage:

rb product = GetProduct.call(id: "gid://shopify/Product/12345").data puts product.id puts product.title puts product.featured_image

Query with fields

Click to expand
Definition:

rb # app/graphql/product_fields.rb class ProductFields FRAGMENT = <<~GRAPHQL fragment ProductFields on Product { id title featuredImage { source: url } } GRAPHQL def self.parse(data) OpenStruct.new( id: data.id, title: data.title, featured_image: data.featuredImage&.source ) end end

rb # app/graphql/get_product.rb class GetProduct include ShopifyGraphql::Query QUERY = <<~GRAPHQL #{ProductFields::FRAGMENT} query($id: ID!) { product(id: $id) { ... ProductFields } } GRAPHQL def call(id:) response = execute(QUERY, id: id) response.data = ProductFields.parse(response.data.product) response end end

Usage:

rb product = GetProduct.call(id: "gid://shopify/Product/12345").data puts product.id puts product.title puts product.featured_image

Simple collection query

Click to expand
Definition:

rb # app/graphql/get_products.rb class GetProducts include ShopifyGraphql::Query QUERY = <<~GRAPHQL query { products(first: 5) { edges { node { id title featuredImage { source: url } } } } } GRAPHQL def call response = execute(QUERY) response.data = parse_data(response.data.products.edges) response end private def parse_data(data) return [] if data.blank? data.compact.map do |edge| OpenStruct.new( id: edge.node.id, title: edge.node.title, featured_image: edge.node.featuredImage&.source ) end end end

Usage:

rb products = GetProducts.call.data products.each do |product| puts product.id puts product.title puts product.featured_image end

Collection query with fields

Click to expand
Definition:

rb # app/graphql/product_fields.rb class ProductFields FRAGMENT = <<~GRAPHQL fragment ProductFields on Product { id title featuredImage { source: url } } GRAPHQL def self.parse(data) OpenStruct.new( id: data.id, title: data.title, featured_image: data.featuredImage&.source ) end end

rb # app/graphql/get_products.rb class GetProducts include ShopifyGraphql::Query QUERY = <<~GRAPHQL #{ProductFields::FRAGMENT} query { products(first: 5) { edges { cursor node { ... ProductFields } } } } GRAPHQL def call response = execute(QUERY) response.data = parse_data(response.data.products.edges) response end private def parse_data(data) return [] if data.blank? data.compact.map do |edge| OpenStruct.new( cursor: edge.cursor, node: ProductFields.parse(edge.node) ) end end end

Usage:

rb products = GetProducts.call.data products.each do |edge| puts edge.cursor puts edge.node.id puts edge.node.title puts edge.node.featured_image end

Collection query with pagination

Click to expand
Definition:

rb # app/graphql/product_fields.rb class ProductFields FRAGMENT = <<~GRAPHQL fragment ProductFields on Product { id title featuredImage { source: url } } GRAPHQL def self.parse(data) OpenStruct.new( id: data.id, title: data.title, featured_image: data.featuredImage&.source ) end end

rb # app/graphql/get_products.rb class GetProducts include ShopifyGraphql::Query LIMIT = 5 QUERY = <<~GRAPHQL #{ProductFields::FRAGMENT} query($cursor: String) { products(first: #{LIMIT}, after: $cursor) { edges { node { ... ProductFields } } pageInfo { hasNextPage endCursor } } } GRAPHQL def call response = execute(QUERY) data = parse_data(response.data.products.edges) while response.data.products.pageInfo.hasNextPage response = execute(QUERY, cursor: response.data.products.pageInfo.endCursor) data += parse_data(response.data.products.edges) end response.data = data response end private def parse_data(data) return [] if data.blank? data.compact.map do |edge| ProductFields.parse(edge.node) end end end

Usage:

rb products = GetProducts.call.data products.each do |product| puts product.id puts product.title puts product.featured_image end

Collection query with block

Click to expand
Definition:

rb # app/graphql/product_fields.rb class ProductFields FRAGMENT = <<~GRAPHQL fragment ProductFields on Product { id title featuredImage { source: url } } GRAPHQL def self.parse(data) OpenStruct.new( id: data.id, title: data.title, featured_image: data.featuredImage&.source ) end end

rb # app/graphql/get_products.rb class GetProducts include ShopifyGraphql::Query LIMIT = 5 QUERY = <<~GRAPHQL #{ProductFields::FRAGMENT} query($cursor: String) { products(first: #{LIMIT}, after: $cursor) { edges { node { ... ProductFields } } pageInfo { hasNextPage endCursor } } } GRAPHQL def call(&block) response = execute(QUERY) response.data.products.edges.each do |edge| block.call ProductFields.parse(edge.node) end while response.data.products.pageInfo.hasNextPage response = execute(QUERY, cursor: response.data.products.pageInfo.endCursor) response.data.products.edges.each do |edge| block.call ProductFields.parse(edge.node) end end response end end

Usage:

rb GetProducts.call do |product| puts product.id puts product.title puts product.featured_image end

Collection query with nested pagination

Click to expand
Definition:

rb # app/graphql/get_collections_with_products.rb class GetCollectionsWithProducts include ShopifyGraphql::Query COLLECTIONS_LIMIT = 1 PRODUCTS_LIMIT = 25 QUERY = <<~GRAPHQL query ($cursor: String) { collections(first: #{COLLECTIONS_LIMIT}, after: $cursor) { edges { node { id title products(first: #{PRODUCTS_LIMIT}) { edges { node { id } } } } } pageInfo { hasNextPage endCursor } } } GRAPHQL def call response = execute(QUERY) data = parse_data(response.data.collections.edges) while response.data.collections.pageInfo.hasNextPage response = execute(QUERY, cursor: response.data.collections.pageInfo.endCursor) data += parse_data(response.data.collections.edges) end response.data = data response end private def parse_data(data) return [] if data.blank? data.compact.map do |edge| OpenStruct.new( id: edge.node.id, title: edge.node.title, products: edge.node.products.edges.map do |product_edge| OpenStruct.new(id: product_edge.node.id) end ) end end end

Usage:

rb collections = GetCollectionsWithProducts.call.data collections.each do |collection| puts collection.id puts collection.title collection.products.each do |product| puts product.id end end

Mutation

Click to expand

Definition:

rb # app/graphql/update_product.rb class UpdateProduct include ShopifyGraphql::Mutation MUTATION = <<~GRAPHQL mutation($input: ProductInput!) { productUpdate(input: $input) { product { id title } userErrors { field message } } } GRAPHQL def call(input:) response = execute(MUTATION, input: input) response.data = response.data.productUpdate handle_user_errors(response.data) response end end

Usage:

rb response = UpdateProduct.call(input: { id: "gid://shopify/Product/123", title: "New title" }) puts response.data.product.title

Graphql call without wrapper

Click to expand

rb PRODUCT_UPDATE_MUTATION = <<~GRAPHQL mutation($input: ProductInput!) { productUpdate(input: $input) { product { id title } userErrors { field message } } } GRAPHQL response = ShopifyGraphql.execute( PRODUCT_UPDATE_MUTATION, input: { id: "gid://shopify/Product/12345", title: "New title" } ) response = response.data.productUpdate ShopifyGraphql.handle_user_errors(response)

Built-in Graphql calls

  • ShopifyGraphql::CurrentShop:

    Equivalent to ShopifyAPI::Shop.current. Usage example:

    1. shop = ShopifyGraphql::CurrentShop.call
    2. puts shop.name

    Or with locales (requires read_locales scope):

    1. shop = ShopifyGraphql::CurrentShop.call(with_locales: true)
    2. puts shop.primary_locale
    3. puts shop.shop_locales
  • ShopifyGraphql::CancelSubscription

  • ShopifyGraphql::CreateRecurringSubscription
  • ShopifyGraphql::CreateUsageSubscription
  • ShopifyGraphql::GetAppSubscription
  • ShopifyGraphql::UpsertPrivateMetafield
  • ShopifyGraphql::DeletePrivateMetafield
  • ShopifyGraphql::CreateBulkMutation
  • ShopifyGraphql::CreateBulkQuery
  • ShopifyGraphql::CreateStagedUploads
  • ShopifyGraphql::GetBulkOperation

Built-in wrappers are located in app/graphql/shopify_graphql folder. You can use them directly in your apps or as an example to create your own wrappers.

Rate limits

The gem exposes Graphql rate limit extensions in response object:

  • points_left
  • points_limit
  • points_restore_rate
  • query_cost

And adds a helper to check if available points lower than threshold (useful for implementing API backoff):

  • points_maxed?(threshold: 100)

Usage example:

  1. response = GetProduct.call(id: "gid://shopify/Product/PRODUCT_GID")
  2. response.points_left # => 1999
  3. response.points_limit # => 2000.0
  4. response.points_restore_rate # => 100.0
  5. response.query_cost # => 1
  6. response.points_maxed?(threshold: 100) # => false

Custom apps

In custom apps, if you’re using shopify_app gem, then the setup is similar public apps. Except Shop model which must include class method to make queries to your store:

  1. # app/models/shop.rb
  2. class Shop < ActiveRecord::Base
  3. include ShopifyApp::ShopSessionStorageWithScopes
  4. def self.system
  5. new(
  6. shopify_domain: "MYSHOPIFY_DOMAIN",
  7. shopify_token: "API_ACCESS_TOKEN_FOR_CUSTOM_APP"
  8. )
  9. end
  10. end

Using this method, you should be able to make API calls like this:

  1. Shop.system.with_shopify_session do
  2. GetOrder.call(id: order.shopify_gid)
  3. end

If you’re not using shopify_app gem, then you need to setup ShopifyAPI::Context manually:

  1. # config/initializers/shopify_api.rb
  2. ShopifyAPI::Context.setup(
  3. api_key: "XXX",
  4. api_secret_key: "XXXX",
  5. scope: "read_orders,read_products",
  6. is_embedded: false,
  7. api_version: "2024-07",
  8. is_private: true,
  9. )

And create another method in Shop model to make queries to your store:

  1. # app/models/shop.rb
  2. def Shop
  3. def self.with_shopify_session(&block)
  4. ShopifyAPI::Auth::Session.temp(
  5. shop: "MYSHOPIFY_DOMAIN",
  6. access_token: "API_ACCESS_TOKEN_FOR_CUSTOM_APP",
  7. &block
  8. )
  9. end
  10. end

Using this method, you should be able to make API calls like this:

  1. Shop.with_shopify_session do
  2. GetOrder.call(id: order.shopify_gid)
  3. end

Graphql webhooks (deprecated)

[!WARNING]
ShopifyGraphql webhooks are deprecated and will be removed in v3.0. Please use shopify_app gem for handling webhooks. See shopify_app documentation for more details.

The gem has built-in support for Graphql webhooks (similar to shopify_app). To enable it add the following config to config/initializers/shopify_app.rb:

  1. ShopifyGraphql.configure do |config|
  2. # Webhooks
  3. webhooks_prefix = "https://#{Rails.configuration.app_host}/graphql_webhooks"
  4. config.webhook_jobs_namespace = 'shopify/webhooks'
  5. config.webhook_enabled_environments = ['development', 'staging', 'production']
  6. config.webhooks = [
  7. { topic: 'SHOP_UPDATE', address: "#{webhooks_prefix}/shop_update" },
  8. { topic: 'APP_SUBSCRIPTIONS_UPDATE', address: "#{webhooks_prefix}/app_subscriptions_update" },
  9. { topic: 'APP_UNINSTALLED', address: "#{webhooks_prefix}/app_uninstalled" },
  10. ]
  11. end

And add the following routes to config/routes.rb:

  1. mount ShopifyGraphql::Engine, at: '/'

To register defined webhooks you need to call ShopifyGraphql::UpdateWebhooksJob. You can call it manually or use AfterAuthenticateJob from shopify_app:

  1. # config/initializers/shopify_app.rb
  2. ShopifyApp.configure do |config|
  3. # ...
  4. config.after_authenticate_job = {job: "AfterAuthenticateJob", inline: true}
  5. end
  1. # app/jobs/after_install_job.rb
  2. class AfterInstallJob < ApplicationJob
  3. def perform(shop)
  4. # ...
  5. update_webhooks(shop)
  6. end
  7. def update_webhooks(shop)
  8. ShopifyGraphql::UpdateWebhooksJob.perform_later(
  9. shop_domain: shop.shopify_domain,
  10. shop_token: shop.shopify_token
  11. )
  12. end
  13. end

To handle webhooks create jobs in app/jobs/webhooks folder. The gem will automatically call them when new webhooks are received. The job name should match the webhook topic name. For example, to handle APP_UNINSTALLED webhook create app/jobs/webhooks/app_uninstalled_job.rb:

  1. class Webhooks::AppUninstalledJob < ApplicationJob
  2. queue_as :default
  3. def perform(shop_domain:, webhook:)
  4. shop = Shop.find_by!(shopify_domain: shop_domain)
  5. # handle shop uninstall
  6. end
  7. end

License

The gem is available as open source under the terms of the MIT License.