graphql-batch 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +3 -2
- data/README.md +61 -37
- data/graphql-batch.gemspec +3 -1
- data/lib/graphql/batch.rb +11 -3
- data/lib/graphql/batch/execution_strategy.rb +39 -35
- data/lib/graphql/batch/executor.rb +35 -0
- data/lib/graphql/batch/loader.rb +51 -0
- data/lib/graphql/batch/promise.rb +7 -0
- data/lib/graphql/batch/version.rb +1 -1
- metadata +34 -6
- data/lib/graphql/batch/query.rb +0 -29
- data/lib/graphql/batch/query_container.rb +0 -30
- data/lib/graphql/batch/query_group.rb +0 -35
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4daba918a3d53b660bb662a01fa76be31ab34507
|
4
|
+
data.tar.gz: a0bf9e1268a286ddd00c1dc60c74769fceb060ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d532cace4412ef4a5e2aff115841b03590f9a72feb9f75086d1092849f19de4287fa6fd7e8dd4608868283499cf4cea17c414bd718d1c9bdb46c0817d75aac6
|
7
|
+
data.tar.gz: e6de630224733d0e5dcdcbdea70610910e463862164460e03fe8e4b9f91cca634c28ca2be867d60dd6c3355e13610dac0c5f73edb210daae501e883a0adc0e52
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# GraphQL::Batch
|
2
2
|
|
3
3
|
Provides an executor for the [`graphql` gem](https://github.com/rmosolgo/graphql-ruby) which allows queries to be batched.
|
4
4
|
|
@@ -28,54 +28,42 @@ Require the library
|
|
28
28
|
require 'graphql/batch'
|
29
29
|
```
|
30
30
|
|
31
|
-
Define a
|
31
|
+
Define a custom loader, which is initialized with arguments that are used for grouping and an perform method for performing the batch load.
|
32
32
|
|
33
33
|
```ruby
|
34
|
-
class
|
35
|
-
|
36
|
-
|
37
|
-
def initialize(model, id, &block)
|
34
|
+
class RecordLoader < GraphQL::Batch::Loader
|
35
|
+
def initialize(model)
|
38
36
|
@model = model
|
39
|
-
@id = id
|
40
|
-
super(&block)
|
41
|
-
end
|
42
|
-
|
43
|
-
# super returns the class name
|
44
|
-
def group_key
|
45
|
-
"#{super}:#{model.name}"
|
46
37
|
end
|
47
38
|
|
48
|
-
def
|
49
|
-
model
|
50
|
-
ids
|
51
|
-
records_by_id = model.where(id: ids).index_by(&:id)
|
52
|
-
queries.each do |query|
|
53
|
-
query.complete(records_by_id[query.id])
|
54
|
-
end
|
39
|
+
def perform(ids)
|
40
|
+
@model.where(id: ids).each { |record| fulfill(record.id, record) }
|
41
|
+
ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
|
55
42
|
end
|
56
43
|
end
|
57
44
|
```
|
58
45
|
|
59
|
-
|
46
|
+
Use the batch execution strategy with your schema
|
60
47
|
|
61
48
|
```ruby
|
62
|
-
|
49
|
+
MySchema = GraphQL::Schema.new(query: MyQueryType)
|
50
|
+
MySchema.query_execution_strategy = GraphQL::Batch::ExecutionStrategy
|
51
|
+
MySchema.mutation_execution_strategy = GraphQL::Batch::ExecutionStrategy
|
63
52
|
```
|
64
53
|
|
65
|
-
|
54
|
+
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.
|
66
55
|
|
67
56
|
```ruby
|
68
|
-
|
69
|
-
MySchema.query_execution_strategy = GraphQL::Batch::ExecutionStrategy
|
57
|
+
resolve -> (obj, args, context) { RecordLoader.for(Product).load(args["id"]) }
|
70
58
|
```
|
71
59
|
|
72
|
-
###
|
60
|
+
### Promises
|
73
61
|
|
74
|
-
|
62
|
+
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`
|
75
63
|
|
76
64
|
```ruby
|
77
65
|
resolve -> (obj, args, context) do
|
78
|
-
|
66
|
+
RecordLoader.for(Product).load(args["id"]).then do |product|
|
79
67
|
product.title
|
80
68
|
end
|
81
69
|
end
|
@@ -85,23 +73,59 @@ You may also need to do another query that depends on the first one to get the r
|
|
85
73
|
|
86
74
|
```ruby
|
87
75
|
resolve -> (obj, args, context) do
|
88
|
-
|
89
|
-
|
76
|
+
RecordLoader.for(Product).load(args["id"]).then do |product|
|
77
|
+
RecordLoader.for(Image).load(product.image_id)
|
90
78
|
end
|
91
79
|
end
|
92
80
|
```
|
93
81
|
|
94
|
-
If the second query doesn't depend on the
|
82
|
+
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.
|
95
83
|
|
96
84
|
```ruby
|
97
85
|
resolve -> (obj, args, context) do
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
86
|
+
Promise.all([
|
87
|
+
CountLoader.for(Shop, :smart_collections).load(context.shop_id),
|
88
|
+
CountLoader.for(Shop, :custom_collections).load(context.shop_id),
|
89
|
+
]).then do |results|
|
90
|
+
results.reduce(&:+)
|
103
91
|
end
|
104
92
|
end
|
93
|
+
|
94
|
+
`.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
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
resolve -> (obj, args, context) do
|
98
|
+
CacheLoader.for(Product).load(args["id"]).then(nil, lambda do |exc|
|
99
|
+
raise exc unless exc.is_a?(Redis::BaseConnectionError)
|
100
|
+
logger.warn err.message
|
101
|
+
RecordLoader.for(Product).load(args["id"])
|
102
|
+
end)
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
## Unit Testing
|
107
|
+
|
108
|
+
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.
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
def test_single_query
|
112
|
+
product = products(:snowboard)
|
113
|
+
query = RecordLoader.for(Product).load(args["id"]).then(&:title)
|
114
|
+
assert_equal product.title, query.sync
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
Use GraphQL::Batch::Promise.all instead of Promise.all to be able to call sync on the returned promise.
|
119
|
+
|
120
|
+
```
|
121
|
+
def test_batch_query
|
122
|
+
products = [products(:snowboard), products(:jacket)]
|
123
|
+
query1 = RecordLoader.for(Product).load(products(:snowboard).id).then(&:title)
|
124
|
+
query2 = RecordLoader.for(Product).load(products(:jacket).id).then(&:title)
|
125
|
+
results = GraphQL::Batch::Promise.all([query1, query2]).sync
|
126
|
+
assert_equal products(:snowboard).title, results[0]
|
127
|
+
assert_equal products(:jacket).title, results[1]
|
128
|
+
end
|
105
129
|
```
|
106
130
|
|
107
131
|
## Development
|
@@ -110,7 +134,7 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
110
134
|
|
111
135
|
## Contributing
|
112
136
|
|
113
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
137
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/graphql-batch.
|
114
138
|
|
115
139
|
## License
|
116
140
|
|
data/graphql-batch.gemspec
CHANGED
@@ -18,8 +18,10 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_runtime_dependency "graphql", "~>0.8"
|
21
|
+
spec.add_runtime_dependency "graphql", "~> 0.8"
|
22
|
+
spec.add_runtime_dependency "promise.rb", "~> 0.7.0.rc2"
|
22
23
|
|
24
|
+
spec.add_development_dependency "byebug" if RUBY_ENGINE == 'ruby'
|
23
25
|
spec.add_development_dependency "bundler", "~> 1.10"
|
24
26
|
spec.add_development_dependency "rake", "~> 10.0"
|
25
27
|
spec.add_development_dependency "minitest"
|
data/lib/graphql/batch.rb
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
require "graphql"
|
2
|
+
require "promise.rb"
|
3
|
+
|
4
|
+
module GraphQL
|
5
|
+
module Batch
|
6
|
+
BrokenPromiseError = Class.new(StandardError)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
2
10
|
require_relative "batch/version"
|
3
|
-
require_relative "batch/
|
4
|
-
require_relative "batch/
|
5
|
-
require_relative "batch/
|
11
|
+
require_relative "batch/loader"
|
12
|
+
require_relative "batch/executor"
|
13
|
+
require_relative "batch/promise"
|
6
14
|
require_relative "batch/execution_strategy"
|
@@ -1,57 +1,61 @@
|
|
1
1
|
module GraphQL::Batch
|
2
2
|
class ExecutionStrategy < GraphQL::Query::SerialExecution
|
3
|
-
|
3
|
+
def execute(_, _, _)
|
4
|
+
as_promise(super).sync
|
5
|
+
ensure
|
6
|
+
GraphQL::Batch::Executor.current.clear
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
4
10
|
|
5
|
-
def
|
6
|
-
|
11
|
+
def as_promise(result)
|
12
|
+
GraphQL::Batch::Promise.resolve(as_promise_unless_resolved(result))
|
7
13
|
end
|
8
14
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
+
def as_promise_unless_resolved(result)
|
16
|
+
all_promises = []
|
17
|
+
each_promise(result) do |obj, key, promise|
|
18
|
+
obj[key] = nil
|
19
|
+
all_promises << promise.then do |value|
|
20
|
+
obj[key] = value
|
21
|
+
as_promise_unless_resolved(value)
|
15
22
|
end
|
16
|
-
result
|
17
23
|
end
|
24
|
+
return result if all_promises.empty?
|
25
|
+
Promise.all(all_promises).then { result }
|
18
26
|
end
|
19
27
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
28
|
+
def each_promise(obj, &block)
|
29
|
+
case obj
|
30
|
+
when Array
|
31
|
+
obj.each_with_index do |value, idx|
|
32
|
+
each_promise_in_entry(obj, idx, value, &block)
|
33
|
+
end
|
34
|
+
when Hash
|
35
|
+
obj.each do |key, value|
|
36
|
+
each_promise_in_entry(obj, key, value, &block)
|
27
37
|
end
|
28
|
-
result_hash
|
29
38
|
end
|
30
39
|
end
|
31
40
|
|
32
|
-
|
33
|
-
|
41
|
+
def each_promise_in_entry(obj, key, value, &block)
|
42
|
+
if value.is_a?(::Promise)
|
43
|
+
yield obj, key, value
|
44
|
+
else
|
45
|
+
each_promise(value, &block)
|
46
|
+
end
|
47
|
+
end
|
34
48
|
|
49
|
+
class FieldResolution < GraphQL::Query::SerialExecution::FieldResolution
|
35
50
|
def get_finished_value(raw_value)
|
36
|
-
if raw_value.is_a?(
|
37
|
-
raw_value.
|
38
|
-
|
39
|
-
|
51
|
+
if raw_value.is_a?(::Promise)
|
52
|
+
raw_value.then(->(result) { super(result) }, lambda do |error|
|
53
|
+
error.is_a?(GraphQL::ExecutionError) ? super(error) : raise(error)
|
54
|
+
end)
|
40
55
|
else
|
41
56
|
super
|
42
57
|
end
|
43
58
|
end
|
44
|
-
|
45
|
-
def register_queries(query_container)
|
46
|
-
query_container.each_query do |query|
|
47
|
-
execution_strategy.batched_queries[query.group_key] << query
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def query_completed(query)
|
52
|
-
result_key = ast_node.alias || ast_node.name
|
53
|
-
@result_hash[result_key] = get_finished_value(query.result)
|
54
|
-
end
|
55
59
|
end
|
56
60
|
end
|
57
61
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module GraphQL::Batch
|
2
|
+
class Executor
|
3
|
+
THREAD_KEY = :"#{name}.batched_queries"
|
4
|
+
private_constant :THREAD_KEY
|
5
|
+
|
6
|
+
def self.current
|
7
|
+
Thread.current[THREAD_KEY] ||= new
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :loaders
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@loaders = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def shift
|
17
|
+
@loaders.shift.last
|
18
|
+
end
|
19
|
+
|
20
|
+
def tick
|
21
|
+
shift.resolve
|
22
|
+
end
|
23
|
+
|
24
|
+
def wait(promise)
|
25
|
+
tick while promise.pending? && !loaders.empty?
|
26
|
+
if promise.pending?
|
27
|
+
promise.reject(BrokenPromiseError.new("Promise wasn't fulfilled after all queries were loaded"))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def clear
|
32
|
+
loaders.clear
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module GraphQL::Batch
|
2
|
+
class Loader
|
3
|
+
def self.for(*group_args)
|
4
|
+
Executor.current.loaders[group_args] ||= new(*group_args)
|
5
|
+
end
|
6
|
+
|
7
|
+
def promises_by_key
|
8
|
+
@promises_by_key ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def keys
|
12
|
+
promises_by_key.keys
|
13
|
+
end
|
14
|
+
|
15
|
+
def load(key)
|
16
|
+
promises_by_key[key] ||= Promise.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def fulfill(key, value)
|
20
|
+
promises_by_key[key].fulfill(value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def fulfilled?(key)
|
24
|
+
promises_by_key[key].fulfilled?
|
25
|
+
end
|
26
|
+
|
27
|
+
# batch load keys and fulfill promises
|
28
|
+
def perform(keys)
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
def resolve
|
33
|
+
perform(keys)
|
34
|
+
check_for_broken_promises
|
35
|
+
rescue => err
|
36
|
+
promises_by_key.each do |key, promise|
|
37
|
+
promise.reject(err)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def check_for_broken_promises
|
44
|
+
promises_by_key.each do |key, promise|
|
45
|
+
if promise.pending?
|
46
|
+
promise.reject(BrokenPromiseError.new("#{self.class} didn't fulfill promise for key #{key.inspect}"))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-batch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dylan Thacker-Smith
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-03-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -24,6 +24,34 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: promise.rb
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.7.0.rc2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.7.0.rc2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: byebug
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: bundler
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -84,9 +112,9 @@ files:
|
|
84
112
|
- graphql-batch.gemspec
|
85
113
|
- lib/graphql/batch.rb
|
86
114
|
- lib/graphql/batch/execution_strategy.rb
|
87
|
-
- lib/graphql/batch/
|
88
|
-
- lib/graphql/batch/
|
89
|
-
- lib/graphql/batch/
|
115
|
+
- lib/graphql/batch/executor.rb
|
116
|
+
- lib/graphql/batch/loader.rb
|
117
|
+
- lib/graphql/batch/promise.rb
|
90
118
|
- lib/graphql/batch/version.rb
|
91
119
|
homepage: https://github.com/Shopify/graphql-batch
|
92
120
|
licenses:
|
@@ -108,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
108
136
|
version: '0'
|
109
137
|
requirements: []
|
110
138
|
rubyforge_project:
|
111
|
-
rubygems_version: 2.2.
|
139
|
+
rubygems_version: 2.2.3
|
112
140
|
signing_key:
|
113
141
|
specification_version: 4
|
114
142
|
summary: A query batching executor for the graphql gem
|
data/lib/graphql/batch/query.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
module GraphQL::Batch
|
2
|
-
class Query < QueryContainer
|
3
|
-
def initialize(&block)
|
4
|
-
@block = block
|
5
|
-
end
|
6
|
-
|
7
|
-
# batched queries with the same key are merged together
|
8
|
-
def group_key
|
9
|
-
self.class.name
|
10
|
-
end
|
11
|
-
|
12
|
-
def each_query
|
13
|
-
yield self
|
14
|
-
end
|
15
|
-
|
16
|
-
def complete(result)
|
17
|
-
if @block
|
18
|
-
result = @block.call(result)
|
19
|
-
@block = nil
|
20
|
-
end
|
21
|
-
super(result)
|
22
|
-
end
|
23
|
-
|
24
|
-
# execute queries, with the same group_key, as a batch
|
25
|
-
def self.execute(queries)
|
26
|
-
raise NotImplementedError
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
@@ -1,30 +0,0 @@
|
|
1
|
-
module GraphQL::Batch
|
2
|
-
class QueryContainer
|
3
|
-
attr_accessor :query_listener, :result
|
4
|
-
|
5
|
-
def each_query
|
6
|
-
raise NotImplementedError
|
7
|
-
end
|
8
|
-
|
9
|
-
def complete(result)
|
10
|
-
if result.is_a?(QueryContainer)
|
11
|
-
result.query_listener = self
|
12
|
-
register_queries(result)
|
13
|
-
else
|
14
|
-
if instance_variable_defined?(:@result)
|
15
|
-
raise "Query was already completed"
|
16
|
-
end
|
17
|
-
@result = result
|
18
|
-
query_listener.query_completed(self)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def query_completed(query)
|
23
|
-
complete(query.result)
|
24
|
-
end
|
25
|
-
|
26
|
-
def register_queries(query_container)
|
27
|
-
query_listener.register_queries(query_container)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
@@ -1,35 +0,0 @@
|
|
1
|
-
module GraphQL::Batch
|
2
|
-
class QueryGroup < QueryContainer
|
3
|
-
def initialize(queries, &block)
|
4
|
-
@pending_queries = queries.dup
|
5
|
-
@pending_queries.each do |query|
|
6
|
-
query.query_listener = self
|
7
|
-
end
|
8
|
-
@block = block
|
9
|
-
raise ArgumentError, "QueryGroup requires a block" unless block
|
10
|
-
end
|
11
|
-
|
12
|
-
def each_query
|
13
|
-
@pending_queries.each do |query_container|
|
14
|
-
query_container.each_query do |query|
|
15
|
-
yield query
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def query_completed(query)
|
21
|
-
@pending_queries.delete(query)
|
22
|
-
if @pending_queries.empty?
|
23
|
-
result = @block.call
|
24
|
-
@block = nil
|
25
|
-
if result.is_a?(QueryContainer)
|
26
|
-
result.query_listener = self
|
27
|
-
@pending_queries << result
|
28
|
-
register_queries(result)
|
29
|
-
else
|
30
|
-
complete(result)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|