kirillplatonov / shopify_graphql

Less painful way to work with Shopify Graphql API in Ruby.
MIT License
59 stars 9 forks source link
graphql rails ruby shopify

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

Dependencies

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

Installation

Add shopify_graphql to your Gemfile:

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:

shop.with_shopify_session do
  # your calls to graphql
end

Conventions

To better organize your Graphql code use the following conventions:

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 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

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:

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

Usage example:

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

Graphql webhooks

Since version 10 shopify_api gem includes built-in support for Graphql webhooks. If you are using shopify_api version 10 or higher you don't need to use this gem to handle Graphql 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:

ShopifyGraphql.configure do |config|
  # Webhooks
  webhooks_prefix = "https://#{Rails.configuration.app_host}/graphql_webhooks"
  config.webhook_jobs_namespace = 'shopify/webhooks'
  config.webhook_enabled_environments = ['development', 'staging', 'production']
  config.webhooks = [
    { topic: 'SHOP_UPDATE', address: "#{webhooks_prefix}/shop_update" },
    { topic: 'APP_SUBSCRIPTIONS_UPDATE', address: "#{webhooks_prefix}/app_subscriptions_update" },
    { topic: 'APP_UNINSTALLED', address: "#{webhooks_prefix}/app_uninstalled" },
  ]
end

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

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:

# config/initializers/shopify_app.rb
ShopifyApp.configure do |config|
  # ...
  config.after_authenticate_job = {job: "AfterAuthenticateJob", inline: true}
end
# app/jobs/after_install_job.rb
class AfterInstallJob < ApplicationJob
  def perform(shop)
    # ...
    update_webhooks(shop)
  end

  def update_webhooks(shop)
    ShopifyGraphql::UpdateWebhooksJob.perform_later(
      shop_domain: shop.shopify_domain,
      shop_token: shop.shopify_token
    )
  end
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:

class Webhooks::AppUninstalledJob < ApplicationJob
  queue_as :default

  def perform(shop_domain:, webhook:)
    shop = Shop.find_by!(shopify_domain: shop_domain)
    # handle shop uninstall
  end
end

License

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