Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/shopify/graphql-batch
A query batching executor for the graphql gem
https://github.com/shopify/graphql-batch
batch graphql promise
Last synced: about 1 month ago
JSON representation
A query batching executor for the graphql gem
- Host: GitHub
- URL: https://github.com/shopify/graphql-batch
- Owner: Shopify
- License: mit
- Created: 2015-09-20T04:57:22.000Z (about 9 years ago)
- Default Branch: main
- Last Pushed: 2024-04-05T17:53:52.000Z (7 months ago)
- Last Synced: 2024-04-14T09:45:26.008Z (7 months ago)
- Topics: batch, graphql, promise
- Language: Ruby
- Size: 207 KB
- Stars: 1,404
- Watchers: 460
- Forks: 119
- Open Issues: 18
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE.txt
Awesome Lists containing this project
README
# GraphQL::Batch
[![Build Status](https://github.com/Shopify/graphql-batch/actions/workflows/ci.yml/badge.svg)](https://github.com/Shopify/graphql-batch/actions)
[![Gem Version](https://badge.fury.io/rb/graphql-batch.svg)](https://rubygems.org/gems/graphql-batch)Provides an executor for the [`graphql` gem](https://github.com/rmosolgo/graphql-ruby) which allows queries to be batched.
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'graphql-batch'
```And then execute:
$ bundle
Or install it yourself as:
$ gem install graphql-batch
## Usage
### Basic Usage
#### Schema Configuration
Require the library
```ruby
require 'graphql/batch'
```Define a custom loader, which is initialized with arguments that are used for grouping and a perform method for performing the batch load.
```ruby
class RecordLoader < GraphQL::Batch::Loader
def initialize(model)
@model = model
enddef perform(ids)
@model.where(id: ids).each { |record| fulfill(record.id, record) }
ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
end
end
```Use `GraphQL::Batch` as a plugin in your schema _after_ specifying the mutation
so that `GraphQL::Batch` can extend the mutation fields to clear the cache after
they are resolved.```ruby
class MySchema < GraphQL::Schema
query MyQueryType
mutation MyMutationTypeuse GraphQL::Batch
end
```#### Field Usage
The loader class can be used from the resolver for a graphql field by calling `.for` with the grouping arguments to get a loader instance, then call `.load` on that instance with the key to load.
```ruby
field :product, Types::Product, null: true do
argument :id, ID, required: true
enddef product(id:)
RecordLoader.for(Product).load(id)
end
```The loader also supports batch loading an array of records instead of just a single record, via `load_many`. For example:
```ruby
field :products, [Types::Product, null: true], null: false do
argument :ids, [ID], required: true
enddef products(ids:)
RecordLoader.for(Product).load_many(ids)
end
```Although this library doesn't have a dependency on active record,
the [examples directory](examples) has record and association loaders
for active record which handles edge cases like type casting ids
and overriding GraphQL::Batch::Loader#cache_key to load associations
on records with the same id.### Promises
GraphQL::Batch::Loader#load returns a Promise using the [promise.rb gem](https://rubygems.org/gems/promise.rb) to provide a promise based API, so you can transform the query results using `.then`
```ruby
def product_title(id:)
RecordLoader.for(Product).load(id).then do |product|
product.title
end
end
```You may also need to do another query that depends on the first one to get the result, in which case the query block can return another query.
```ruby
def product_image(id:)
RecordLoader.for(Product).load(id).then do |product|
RecordLoader.for(Image).load(product.image_id)
end
end
```If the second query doesn't depend on the first one, then you can use Promise.all, which allows each query in the group to be batched with other queries.
```ruby
def all_collections
Promise.all([
CountLoader.for(Shop, :smart_collections).load(context.shop_id),
CountLoader.for(Shop, :custom_collections).load(context.shop_id),
]).then(&:sum)
end
````.then` can optionally take two lambda arguments, the first of which is equivalent to passing a block to `.then`, and the second one handles exceptions. This can be used to provide a fallback
```ruby
def product(id:)
# Try the cache first ...
CacheLoader.for(Product).load(id).then(nil, lambda do |exc|
# But if there's a connection error, go to the underlying database
raise exc unless exc.is_a?(Redis::BaseConnectionError)
logger.warn err.message
RecordLoader.for(Product).load(id)
end)
end
```### Priming the Cache
You can prime the loader cache with a specific value, which can be useful in certain situations.
```ruby
def liked_products
liked_products = Product.where(liked: true).load
liked_products.each do |product|
RecordLoader.for(Product).prime(product.id, product)
end
end
```Priming will add key/value to the loader cache only if it didn't exist before.
## Unit Testing
Your loaders can be tested outside of a GraphQL query by doing the
batch loads in a block passed to `GraphQL::Batch.batch`. That method
will set up thread-local state to store the loaders, batch load any
promise returned from the block then clear the thread-local state
to avoid leaking state between tests.```ruby
def test_single_query
product = products(:snowboard)
title = GraphQL::Batch.batch do
RecordLoader.for(Product).load(product.id).then(&:title)
end
assert_equal product.title, title
end
```## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
## Contributing
See our [contributing guidelines](CONTRIBUTING.md) for more information.
## License
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).