README.md in graphql-batch-0.1.0 vs README.md in graphql-batch-0.2.0
- old
+ new
@@ -1,6 +1,6 @@
-# Graphql::Batch
+# GraphQL::Batch
Provides an executor for the [`graphql` gem](https://github.com/rmosolgo/graphql-ruby) which allows queries to be batched.
## Installation
@@ -26,92 +26,116 @@
```ruby
require 'graphql/batch'
```
-Define a GraphQL::Batch::Query derived class. Use group_key to specify which queries can be reduced into a batch query, and an execute class method which makes that batch query and passes the result to each individual query.
+Define a custom loader, which is initialized with arguments that are used for grouping and an perform method for performing the batch load.
```ruby
-class FindQuery < GraphQL::Batch::Query
- attr_reader :model, :id
-
- def initialize(model, id, &block)
+class RecordLoader < GraphQL::Batch::Loader
+ def initialize(model)
@model = model
- @id = id
- super(&block)
end
- # super returns the class name
- def group_key
- "#{super}:#{model.name}"
+ def perform(ids)
+ @model.where(id: ids).each { |record| fulfill(record.id, record) }
+ ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
end
-
- def self.execute(queries)
- model = queries.first.model
- ids = queries.map(&:id)
- records_by_id = model.where(id: ids).index_by(&:id)
- queries.each do |query|
- query.complete(records_by_id[query.id])
- end
- end
end
```
-When defining your schema, using the graphql gem, return a your batch query object from the resolve proc.
+Use the batch execution strategy with your schema
```ruby
-resolve -> (obj, args, context) { FindQuery.new(Product, args["id"]) }
+MySchema = GraphQL::Schema.new(query: MyQueryType)
+MySchema.query_execution_strategy = GraphQL::Batch::ExecutionStrategy
+MySchema.mutation_execution_strategy = GraphQL::Batch::ExecutionStrategy
```
-Use the batch execution strategy with your schema
+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.
```ruby
-MySchema = GraphQL::Schema.new(query: MyQueryType)
-MySchema.query_execution_strategy = GraphQL::Batch::ExecutionStrategy
+resolve -> (obj, args, context) { RecordLoader.for(Product).load(args["id"]) }
```
-### Query Dependant Computed Fields
+### Promises
-If you don't want to use a query result directly, then you can pass a block which gets called after the query completes.
+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
resolve -> (obj, args, context) do
- FindQuery.new(Product, args["id"]) do |product|
+ 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.
```ruby
resolve -> (obj, args, context) do
- FindQuery.new(Product, args["id"]) do |product|
- FindQuery.new(Image, product.image_id)
+ 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 other one, then you can use GraphQL::Batch::QueryGroup, which allows each query in the group to be batched with other queries.
+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
resolve -> (obj, args, context) do
- smart_collection_query = CountQuery.new(SmartCollection, context.shop_id)
- custom_collection_query = CountQuery.new(CustomCollection, context.shop_id)
-
- QueryGroup.new([smart_collection_query, custom_collection_query]) do
- smart_collection_query.result + custom_collection_query.result
+ 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
+
+```ruby
+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
+
+GraphQL::Batch::Promise#sync can be used to wait for a promise to be resolved and return its result. This can be useful for debugging and unit testing loaders.
+
+```ruby
+ def test_single_query
+ product = products(:snowboard)
+ query = RecordLoader.for(Product).load(args["id"]).then(&:title)
+ assert_equal product.title, query.sync
+ end
+```
+
+Use GraphQL::Batch::Promise.all instead of Promise.all to be able to call sync on the returned promise.
+
+```
+ def test_batch_query
+ products = [products(:snowboard), products(:jacket)]
+ query1 = RecordLoader.for(Product).load(products(:snowboard).id).then(&:title)
+ query2 = RecordLoader.for(Product).load(products(:jacket).id).then(&:title)
+ results = GraphQL::Batch::Promise.all([query1, query2]).sync
+ assert_equal products(:snowboard).title, results[0]
+ assert_equal products(:jacket).title, results[1]
+ 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
-Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/graphql-batch.
+Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/graphql-batch.
## License
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).