Shopify Graphql

Less painful way to work with Shopify Graphql API in Ruby.

NOTE: The library only supports shopify_api < 10.0 at the moment.

Features

  • Simple API for Graphql calls
  • Graphql webhooks integration
  • Built-in error handling
  • No schema and no memory issues
  • Buil-in retry on error
  • (Planned) Testing helpers
  • (Planned) Pre-built calls for common Graphql operations

Usage

Making Graphql calls directly

CREATE_WEBHOOK_MUTATION = "mutation($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {\n  webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {\n    webhookSubscription {\n      id\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n"

response = ShopifyGraphql.execute(CREATE_WEBHOOK_MUTATION,
  topic: "TOPIC",
  webhookSubscription: { callbackUrl: "ADDRESS", format: "JSON" },
)
response = response.data.webhookSubscriptionCreate
ShopifyGraphql.handle_user_errors(response)

Creating wrappers for queries, mutations, and fields

To isolate Graphql boilerplate you can create wrappers. To keep them organized use the following conventions:

  • Put them all 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)

Example fields

Definition:

class AppSubscriptionFields
  FRAGMENT = "    fragment AppSubscriptionFields on AppSubscription {\n      id\n      name\n      status\n      trialDays\n      currentPeriodEnd\n      test\n      lineItems {\n        id\n        plan {\n          pricingDetails {\n            __typename\n            ... on AppRecurringPricing {\n              price {\n                amount\n              }\n              interval\n            }\n            ... on AppUsagePricing {\n              balanceUsed {\n                amount\n              }\n              cappedAmount {\n                amount\n              }\n              interval\n              terms\n            }\n          }\n        }\n      }\n    }\n  GRAPHQL\n\n  def self.parse(data)\n    recurring_line_item = data.lineItems.find { |i| i.plan.pricingDetails.__typename == \"AppRecurringPricing\" }\n    recurring_pricing = recurring_line_item&.plan&.pricingDetails\n    usage_line_item = data.lineItems.find { |i| i.plan.pricingDetails.__typename == \"AppUsagePricing\" }\n    usage_pricing = usage_line_item&.plan&.pricingDetails\n\n    OpenStruct.new(\n      id: data.id,\n      name: data.name,\n      status: data.status,\n      trial_days: data.trialDays,\n      current_period_end: data.currentPeriodEnd && Time.parse(data.currentPeriodEnd),\n      test: data.test,\n      recurring_line_item_id: recurring_line_item&.id,\n      recurring_price: recurring_pricing&.price&.amount&.to_d,\n      recurring_interval: recurring_pricing&.interval,\n      usage_line_item_id: usage_line_item&.id,\n      usage_balance: usage_pricing&.balanceUsed&.amount&.to_d,\n      usage_capped_amount: usage_pricing&.cappedAmount&.amount&.to_d,\n      usage_interval: usage_pricing&.interval,\n      usage_terms: usage_pricing&.terms,\n    )\n  end\nend\n"

For usage examples see query and mutation below.

Example query

Definition:

class GetAppSubscription
  include ShopifyGraphql::Query

  QUERY = "    \#{AppSubscriptionFields::FRAGMENT}\n    query($id: ID!) {\n      node(id: $id) {\n        ... AppSubscriptionFields\n      }\n    }\n  GRAPHQL\n\n  def call(id:)\n    response = execute(QUERY, id: id)\n    response.data = AppSubscriptionFields.parse(response.data.node)\n    response\n  end\nend\n"

Usage:

shopify_subscription = GetAppSubscription.call(id: @id).data
shopify_subscription.status
shopify_subscription.current_period_end

Example mutation

Definition:

class CreateRecurringSubscription
  include ShopifyGraphql::Mutation

  MUTATION = "    \#{AppSubscriptionFields::FRAGMENT}\n    mutation appSubscriptionCreate(\n      $name: String!,\n      $lineItems: [AppSubscriptionLineItemInput!]!,\n      $returnUrl: URL!,\n      $trialDays: Int,\n      $test: Boolean\n    ) {\n      appSubscriptionCreate(\n        name: $name,\n        lineItems: $lineItems,\n        returnUrl: $returnUrl,\n        trialDays: $trialDays,\n        test: $test\n      ) {\n        appSubscription {\n          ... AppSubscriptionFields\n        }\n        confirmationUrl\n        userErrors {\n          field\n          message\n        }\n      }\n    }\n  GRAPHQL\n\n  def call(name:, price:, return_url:, trial_days: nil, test: nil, interval: :monthly)\n    payload = { name: name, returnUrl: return_url }\n    plan_interval = (interval == :monthly) ? 'EVERY_30_DAYS' : 'ANNUAL'\n    payload[:lineItems] = [{\n      plan: {\n        appRecurringPricingDetails: {\n          price: { amount: price, currencyCode: 'USD' },\n          interval: plan_interval\n        }\n      }\n    }]\n    payload[:trialDays] = trial_days if trial_days\n    payload[:test] = test if test\n\n    response = execute(MUTATION, **payload)\n    response.data = response.data.appSubscriptionCreate\n    handle_user_errors(response.data)\n    response.data = parse_data(response.data)\n    response\n  end\n\n  private\n\n  def parse_data(data)\n    OpenStruct.new(\n      subscription: AppSubscriptionFields.parse(data.appSubscription),\n      confirmation_url: data.confirmationUrl,\n    )\n  end\nend\n"

Usage:

response = CreateRecurringSubscription.call(
  name: "Plan Name",
  price: 10,
  return_url: "RETURN URL",
  trial_days: 3,
  test: true,
).data
confirmation_url = response.confirmation_url
shopify_subscription = response.subscription

Installation

In Gemfile, add:

gem 'shopify_graphql', github: 'kirillplatonov/shopify_graphql', branch: 'main'

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

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 = ['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

You can also use WEBHOOKS_ENABLED=true env variable to enable webhooks (useful in development).

License

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