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/graphqlfolder - Use
Fieldssuffix to name fields (egAppSubscriptionFields) - Use
Getprefix to name queries (egGetProductsorGetAppSubscription) - Use imperative to name mutations (eg
CreateUsageSubscriptionorBulkUpdateVariants)
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.