Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/kirillplatonov/shopify_graphql
Less painful way to work with Shopify Graphql API in Ruby.
https://github.com/kirillplatonov/shopify_graphql
graphql rails ruby shopify
Last synced: 12 days ago
JSON representation
Less painful way to work with Shopify Graphql API in Ruby.
- Host: GitHub
- URL: https://github.com/kirillplatonov/shopify_graphql
- Owner: kirillplatonov
- License: mit
- Created: 2021-07-09T08:03:53.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2024-10-22T16:17:10.000Z (19 days ago)
- Last Synced: 2024-10-23T08:11:13.428Z (19 days ago)
- Topics: graphql, rails, ruby, shopify
- Language: Ruby
- Homepage:
- Size: 154 KB
- Stars: 64
- Watchers: 10
- Forks: 9
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE.txt
Awesome Lists containing this project
README
# Shopify Graphql
Less painful way to work with [Shopify Graphql API](https://shopify.dev/api/admin-graphql) in Ruby. This library is a tiny wrapper on top of [`shopify_api`](https://github.com/Shopify/shopify-api-ruby) 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
- [`shopify_api`](https://github.com/Shopify/shopify-api-ruby) v10+
- [`shopify_app`](https://github.com/Shopify/shopify_app) v19+> For `shopify_api` < v10 use [`0-4-stable`](https://github.com/kirillplatonov/shopify_graphql/tree/0-4-stable) branch.
## Installation
Add `shopify_graphql` to your Gemfile:
```bash
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`:
```rb
shop.with_shopify_session do
# your calls to graphql
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.rbclass GetProduct
include ShopifyGraphql::QueryQUERY = <<~GRAPHQL
query($id: ID!) {
product(id: $id) {
handle
title
description
}
}
GRAPHQLdef 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.rbclass GetProduct
include ShopifyGraphql::QueryQUERY = <<~GRAPHQL
query($id: ID!) {
product(id: $id) {
id
title
featuredImage {
source: url
}
}
}
GRAPHQLdef call(id:)
response = execute(QUERY, id: id)
response.data = parse_data(response.data.product)
response
endprivate
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.rbclass ProductFields
FRAGMENT = <<~GRAPHQL
fragment ProductFields on Product {
id
title
featuredImage {
source: url
}
}
GRAPHQLdef self.parse(data)
OpenStruct.new(
id: data.id,
title: data.title,
featured_image: data.featuredImage&.source
)
end
end
``````rb
# app/graphql/get_product.rbclass GetProduct
include ShopifyGraphql::QueryQUERY = <<~GRAPHQL
#{ProductFields::FRAGMENT}query($id: ID!) {
product(id: $id) {
... ProductFields
}
}
GRAPHQLdef 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.rbclass GetProducts
include ShopifyGraphql::QueryQUERY = <<~GRAPHQL
query {
products(first: 5) {
edges {
node {
id
title
featuredImage {
source: url
}
}
}
}
}
GRAPHQLdef call
response = execute(QUERY)
response.data = parse_data(response.data.products.edges)
response
endprivate
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.rbclass ProductFields
FRAGMENT = <<~GRAPHQL
fragment ProductFields on Product {
id
title
featuredImage {
source: url
}
}
GRAPHQLdef self.parse(data)
OpenStruct.new(
id: data.id,
title: data.title,
featured_image: data.featuredImage&.source
)
end
end
``````rb
# app/graphql/get_products.rbclass GetProducts
include ShopifyGraphql::QueryQUERY = <<~GRAPHQL
#{ProductFields::FRAGMENT}query {
products(first: 5) {
edges {
cursor
node {
... ProductFields
}
}
}
}
GRAPHQLdef call
response = execute(QUERY)
response.data = parse_data(response.data.products.edges)
response
endprivate
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.rbclass ProductFields
FRAGMENT = <<~GRAPHQL
fragment ProductFields on Product {
id
title
featuredImage {
source: url
}
}
GRAPHQLdef self.parse(data)
OpenStruct.new(
id: data.id,
title: data.title,
featured_image: data.featuredImage&.source
)
end
end
``````rb
# app/graphql/get_products.rbclass GetProducts
include ShopifyGraphql::QueryLIMIT = 5
QUERY = <<~GRAPHQL
#{ProductFields::FRAGMENT}query($cursor: String) {
products(first: #{LIMIT}, after: $cursor) {
edges {
node {
... ProductFields
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
GRAPHQLdef 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)
endresponse.data = data
response
endprivate
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.rbclass ProductFields
FRAGMENT = <<~GRAPHQL
fragment ProductFields on Product {
id
title
featuredImage {
source: url
}
}
GRAPHQLdef self.parse(data)
OpenStruct.new(
id: data.id,
title: data.title,
featured_image: data.featuredImage&.source
)
end
end
``````rb
# app/graphql/get_products.rbclass GetProducts
include ShopifyGraphql::QueryLIMIT = 5
QUERY = <<~GRAPHQL
#{ProductFields::FRAGMENT}query($cursor: String) {
products(first: #{LIMIT}, after: $cursor) {
edges {
node {
... ProductFields
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
GRAPHQLdef call(&block)
response = execute(QUERY)
response.data.products.edges.each do |edge|
block.call ProductFields.parse(edge.node)
endwhile 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
endresponse
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.rbclass GetCollectionsWithProducts
include ShopifyGraphql::QueryCOLLECTIONS_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
}
}
}
GRAPHQLdef 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)
endresponse.data = data
response
endprivate
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.rbclass UpdateProduct
include ShopifyGraphql::MutationMUTATION = <<~GRAPHQL
mutation($input: ProductInput!) {
productUpdate(input: $input) {
product {
id
title
}
userErrors {
field
message
}
}
}
GRAPHQLdef 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
}
}
}
GRAPHQLresponse = 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:
```rb
shop = ShopifyGraphql::CurrentShop.call
puts shop.name
```Or with locales (requires `read_locales` scope):
```rb
shop = ShopifyGraphql::CurrentShop.call(with_locales: true)
puts shop.primary_locale
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`](/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:
```rb
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
```## 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:
```rb
# app/models/shop.rb
class Shop < ActiveRecord::Base
include ShopifyApp::ShopSessionStorageWithScopesdef self.system
new(
shopify_domain: "MYSHOPIFY_DOMAIN",
shopify_token: "API_ACCESS_TOKEN_FOR_CUSTOM_APP"
)
end
end
```Using this method, you should be able to make API calls like this:
```rb
Shop.system.with_shopify_session do
GetOrder.call(id: order.shopify_gid)
end
```If you're not using `shopify_app` gem, then you need to setup `ShopifyAPI::Context` manually:
```rb
# config/initializers/shopify_api.rb
ShopifyAPI::Context.setup(
api_key: "XXX",
api_secret_key: "XXXX",
scope: "read_orders,read_products",
is_embedded: false,
api_version: "2024-07",
is_private: true,
)
```And create another method in Shop model to make queries to your store:
```rb
# app/models/shop.rb
def Shop
def self.with_shopify_session(&block)
ShopifyAPI::Auth::Session.temp(
shop: "MYSHOPIFY_DOMAIN",
access_token: "API_ACCESS_TOKEN_FOR_CUSTOM_APP",
&block
)
end
end
```Using this method, you should be able to make API calls like this:
```rb
Shop.with_shopify_session do
GetOrder.call(id: order.shopify_gid)
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](https://github.com/Shopify/shopify_app/blob/main/docs/shopify_app/webhooks.md) 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`:
```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`:
```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`:
```rb
# config/initializers/shopify_app.rb
ShopifyApp.configure do |config|
# ...
config.after_authenticate_job = {job: "AfterAuthenticateJob", inline: true}
end
``````rb
# app/jobs/after_install_job.rb
class AfterInstallJob < ApplicationJob
def perform(shop)
# ...
update_webhooks(shop)
enddef 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`:
```rb
class Webhooks::AppUninstalledJob < ApplicationJob
queue_as :defaultdef 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](https://opensource.org/licenses/MIT).