A query batching executor for the graphql gem
Перейти к файлу
Eric Walker d7e1627396 Allow nested queries. (#72) 2017-11-30 14:50:34 -05:00
bin Implement batched queries for fields that use the query result directly. 2015-09-19 23:41:44 -04:00
examples docs(example): Remove unnecessary type cast in RecordLoader example 2017-08-31 11:11:52 -04:00
lib/graphql Allow nested queries. (#72) 2017-11-30 14:50:34 -05:00
test Allow nested queries. (#72) 2017-11-30 14:50:34 -05:00
.gitignore Support graphql-ruby lazy resolution API (#37) 2016-11-22 16:51:51 -05:00
.travis.yml CI against Ruby 2.4 (#67) 2017-10-31 14:07:41 -04:00
CONTRIBUTING.md docs: Add a contributing file 2016-11-24 11:19:46 -05:00
Gemfile Use promise.rb 0.7.0.rc2 2016-02-17 14:30:17 -05:00
LICENSE.txt Shopify -> Shopify Inc. in LICENSE.txt 2015-09-20 00:54:53 -04:00
README.md Add Active Record AssociationLoader and RecordLoader examples (#64) 2017-08-15 08:08:10 -07:00
Rakefile Implement batched queries for fields that use the query result directly. 2015-09-19 23:41:44 -04:00
graphql-batch.gemspec Only include the byebug dev dependency with required ruby version. 2017-08-28 18:30:27 -04:00

README.md

GraphQL::Batch

Build Status Gem Version

Provides an executor for the graphql gem which allows queries to be batched.

Installation

Add this line to your application's Gemfile:

gem 'graphql-batch'

And then execute:

$ bundle

Or install it yourself as:

$ gem install graphql-batch

Usage

Basic Usage

Require the library

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.

class RecordLoader < GraphQL::Batch::Loader
  def initialize(model)
    @model = model
  end

  def 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 (for graphql >= 1.5.0).

MySchema = GraphQL::Schema.define do
  query MyQueryType

  use GraphQL::Batch
end

For pre 1.5.0 versions:

MySchema = GraphQL::Schema.define do
  query MyQueryType

  GraphQL::Batch.use(self)
end

The loader class can be used from the resolve proc 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.

resolve -> (obj, args, context) { RecordLoader.for(Product).load(args["id"]) }

Although this library doesn't have a dependency on active record, the examples directory 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 to provide a promise based API, so you can transform the query results using .then

resolve -> (obj, args, context) do
  RecordLoader.for(Product).load(args["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.

resolve -> (obj, args, context) do
  RecordLoader.for(Product).load(args["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.

resolve -> (obj, args, context) do
  Promise.all([
    CountLoader.for(Shop, :smart_collections).load(context.shop_id),
    CountLoader.for(Shop, :custom_collections).load(context.shop_id),
  ]).then do |results|
    results.reduce(&:+)
  end
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

resolve -> (obj, args, context) do
  CacheLoader.for(Product).load(args["id"]).then(nil, lambda do |exc|
    raise exc unless exc.is_a?(Redis::BaseConnectionError)
    logger.warn err.message
    RecordLoader.for(Product).load(args["id"])
  end)
end

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.

  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 for more information.

License

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