graphql-batch 0.3.3 → 0.3.10
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 +2 -0
- data/README.md +12 -0
- data/examples/association_loader.rb +48 -0
- data/examples/record_loader.rb +25 -0
- data/graphql-batch.gemspec +3 -1
- data/lib/graphql/batch/execution_strategy.rb +1 -1
- data/lib/graphql/batch/executor.rb +48 -26
- data/lib/graphql/batch/loader.rb +34 -20
- data/lib/graphql/batch/mutation_execution_strategy.rb +3 -1
- data/lib/graphql/batch/promise.rb +3 -5
- data/lib/graphql/batch/setup.rb +45 -5
- data/lib/graphql/batch/setup_multiplex.rb +20 -0
- data/lib/graphql/batch/version.rb +1 -1
- data/lib/graphql/batch.rb +17 -8
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c793255ae48cf638179b9e786d03493a469d1629
|
4
|
+
data.tar.gz: d235b788e4f9208747d7145e7d2599c2279319d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7b263790b1871d54d9258e7bd2541c17361b8177a79e07aac146f9d3ccf05570cf07f8aaac40cf4dbe76c3b6f1677f3cd957a32904742c069a133d3906044767
|
7
|
+
data.tar.gz: d00d449a8872286251cc21d555b54be0690002f697dddb8afc1f43e6b90d75e42367ad70f54c701fc75d89b04427b4d127879481c406a24555d3ada35ce0582f
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -72,6 +72,18 @@ The loader class can be used from the resolve proc for a graphql field by callin
|
|
72
72
|
resolve -> (obj, args, context) { RecordLoader.for(Product).load(args["id"]) }
|
73
73
|
```
|
74
74
|
|
75
|
+
The loader also supports batch loading an array of records instead of just a single record, via `load_many`. For example:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
resolve -> (obj, args, context) { RecordLoader.for(Product).load_many(args["ids"]) }
|
79
|
+
```
|
80
|
+
|
81
|
+
Although this library doesn't have a dependency on active record,
|
82
|
+
the [examples directory](examples) has record and association loaders
|
83
|
+
for active record which handles edge cases like type casting ids
|
84
|
+
and overriding GraphQL::Batch::Loader#cache_key to load associations
|
85
|
+
on records with the same id.
|
86
|
+
|
75
87
|
### Promises
|
76
88
|
|
77
89
|
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`
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class AssociationLoader < GraphQL::Batch::Loader
|
2
|
+
def self.validate(model, association_name)
|
3
|
+
new(model, association_name)
|
4
|
+
nil
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(model, association_name)
|
8
|
+
@model = model
|
9
|
+
@association_name = association_name
|
10
|
+
validate
|
11
|
+
end
|
12
|
+
|
13
|
+
def load(record)
|
14
|
+
raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
|
15
|
+
return Promise.resolve(read_association(record)) if association_loaded?(record)
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
# We want to load the associations on all records, even if they have the same id
|
20
|
+
def cache_key(record)
|
21
|
+
record.object_id
|
22
|
+
end
|
23
|
+
|
24
|
+
def perform(records)
|
25
|
+
preload_association(records)
|
26
|
+
records.each { |record| fulfill(record, read_association(record)) }
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def validate
|
32
|
+
unless @model.reflect_on_association(@association_name)
|
33
|
+
raise ArgumentError, "No association #{@association_name} on #{@model}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def preload_association(records)
|
38
|
+
::ActiveRecord::Associations::Preloader.new.preload(records, @association_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def read_association(record)
|
42
|
+
record.public_send(@association_name)
|
43
|
+
end
|
44
|
+
|
45
|
+
def association_loaded?(record)
|
46
|
+
record.association(@association_name).loaded?
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class RecordLoader < GraphQL::Batch::Loader
|
2
|
+
def initialize(model, column: model.primary_key, where: nil)
|
3
|
+
@model = model
|
4
|
+
@column = column.to_s
|
5
|
+
@column_type = model.type_for_attribute(@column)
|
6
|
+
@where = where
|
7
|
+
end
|
8
|
+
|
9
|
+
def load(key)
|
10
|
+
super(@column_type.cast(key))
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform(keys)
|
14
|
+
query(keys).each { |record| fulfill(record.public_send(@column), record) }
|
15
|
+
keys.each { |key| fulfill(key, nil) unless fulfilled?(key) }
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def query(keys)
|
21
|
+
scope = @model
|
22
|
+
scope = scope.where(@where) if @where
|
23
|
+
scope.where(@column => keys)
|
24
|
+
end
|
25
|
+
end
|
data/graphql-batch.gemspec
CHANGED
@@ -21,7 +21,9 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.add_runtime_dependency "graphql", ">= 0.8", "< 2"
|
22
22
|
spec.add_runtime_dependency "promise.rb", "~> 0.7.2"
|
23
23
|
|
24
|
-
|
24
|
+
if RUBY_ENGINE == 'ruby' && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.2")
|
25
|
+
spec.add_development_dependency "byebug"
|
26
|
+
end
|
25
27
|
spec.add_development_dependency "bundler", "~> 1.10"
|
26
28
|
spec.add_development_dependency "rake", "~> 10.0"
|
27
29
|
spec.add_development_dependency "minitest"
|
@@ -3,15 +3,29 @@ module GraphQL::Batch
|
|
3
3
|
THREAD_KEY = :"#{name}.batched_queries"
|
4
4
|
private_constant :THREAD_KEY
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
class << self
|
7
|
+
def current
|
8
|
+
Thread.current[THREAD_KEY]
|
9
|
+
end
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
def current=(executor)
|
12
|
+
Thread.current[THREAD_KEY] = executor
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
+
def start_batch(executor_class)
|
16
|
+
executor = Thread.current[THREAD_KEY] ||= executor_class.new
|
17
|
+
executor.increment_level
|
18
|
+
end
|
19
|
+
|
20
|
+
def end_batch
|
21
|
+
executor = current
|
22
|
+
unless executor
|
23
|
+
raise NoExecutorError, 'Cannot end a batch without an Executor.'
|
24
|
+
end
|
25
|
+
return unless executor.decrement_level < 1
|
26
|
+
self.current = nil
|
27
|
+
end
|
28
|
+
end
|
15
29
|
|
16
30
|
# Set to true when performing a batch query, otherwise, it is false.
|
17
31
|
#
|
@@ -21,44 +35,52 @@ module GraphQL::Batch
|
|
21
35
|
def initialize
|
22
36
|
@loaders = {}
|
23
37
|
@loading = false
|
38
|
+
@nesting_level = 0
|
24
39
|
end
|
25
40
|
|
26
|
-
def
|
27
|
-
|
41
|
+
def loader(key)
|
42
|
+
@loaders[key] ||= yield.tap do |loader|
|
43
|
+
loader.executor = self
|
44
|
+
loader.loader_key = key
|
45
|
+
end
|
28
46
|
end
|
29
47
|
|
30
|
-
def
|
31
|
-
@
|
48
|
+
def resolve(loader)
|
49
|
+
was_loading = @loading
|
50
|
+
@loading = true
|
51
|
+
loader.resolve
|
52
|
+
ensure
|
53
|
+
@loading = was_loading
|
32
54
|
end
|
33
55
|
|
34
56
|
def tick
|
35
|
-
resolve(shift)
|
57
|
+
resolve(@loaders.shift.last)
|
36
58
|
end
|
37
59
|
|
38
60
|
def wait_all
|
39
|
-
tick until loaders.empty?
|
61
|
+
tick until @loaders.empty?
|
40
62
|
end
|
41
63
|
|
42
64
|
def clear
|
43
|
-
loaders.clear
|
65
|
+
@loaders.clear
|
44
66
|
end
|
45
67
|
|
46
|
-
def
|
47
|
-
|
48
|
-
# that happen in the callback aren't interpreted as being performed in GraphQL::Batch::Loader#perform
|
49
|
-
with_loading(false) { yield }
|
68
|
+
def increment_level
|
69
|
+
@nesting_level += 1
|
50
70
|
end
|
51
71
|
|
52
|
-
|
72
|
+
def decrement_level
|
73
|
+
@nesting_level -= 1
|
74
|
+
end
|
53
75
|
|
54
|
-
def
|
76
|
+
def around_promise_callbacks
|
77
|
+
# We need to set #loading to false so that any queries that happen in the promise
|
78
|
+
# callback aren't interpreted as being performed in GraphQL::Batch::Loader#perform
|
55
79
|
was_loading = @loading
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
@loading = was_loading
|
61
|
-
end
|
80
|
+
@loading = false
|
81
|
+
yield
|
82
|
+
ensure
|
83
|
+
@loading = was_loading
|
62
84
|
end
|
63
85
|
end
|
64
86
|
end
|
data/lib/graphql/batch/loader.rb
CHANGED
@@ -1,21 +1,20 @@
|
|
1
1
|
module GraphQL::Batch
|
2
2
|
class Loader
|
3
|
-
|
3
|
+
NoExecutorError = GraphQL::Batch::NoExecutorError
|
4
|
+
deprecate_constant :NoExecutorError if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3")
|
4
5
|
|
5
6
|
def self.for(*group_args)
|
6
7
|
loader_key = loader_key_for(*group_args)
|
7
8
|
executor = Executor.current
|
8
9
|
|
9
10
|
unless executor
|
10
|
-
raise NoExecutorError,
|
11
|
-
|
12
|
-
|
11
|
+
raise GraphQL::Batch::NoExecutorError, 'Cannot create loader without'\
|
12
|
+
' an Executor. Wrap the call to `for` with `GraphQL::Batch.batch`'\
|
13
|
+
' or use `GraphQL::Batch::Setup` as a query instrumenter if'\
|
14
|
+
' using with `graphql-ruby`'
|
13
15
|
end
|
14
16
|
|
15
|
-
executor.
|
16
|
-
loader.loader_key = loader_key
|
17
|
-
loader.executor = executor
|
18
|
-
end
|
17
|
+
executor.loader(loader_key) { new(*group_args) }
|
19
18
|
end
|
20
19
|
|
21
20
|
def self.loader_key_for(*group_args)
|
@@ -35,7 +34,7 @@ module GraphQL::Batch
|
|
35
34
|
def load(key)
|
36
35
|
cache[cache_key(key)] ||= begin
|
37
36
|
queue << key
|
38
|
-
Promise.new.tap { |promise| promise.source = self }
|
37
|
+
::Promise.new.tap { |promise| promise.source = self }
|
39
38
|
end
|
40
39
|
end
|
41
40
|
|
@@ -50,9 +49,7 @@ module GraphQL::Batch
|
|
50
49
|
perform(load_keys)
|
51
50
|
check_for_broken_promises(load_keys)
|
52
51
|
rescue => err
|
53
|
-
|
54
|
-
promise.reject(err)
|
55
|
-
end
|
52
|
+
reject_pending_promises(load_keys, err)
|
56
53
|
end
|
57
54
|
|
58
55
|
# For Promise#sync
|
@@ -72,7 +69,15 @@ module GraphQL::Batch
|
|
72
69
|
|
73
70
|
# Fulfill the key with provided value, for use in #perform
|
74
71
|
def fulfill(key, value)
|
75
|
-
|
72
|
+
finish_resolve(key) do |promise|
|
73
|
+
promise.fulfill(value)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def reject(key, reason)
|
78
|
+
finish_resolve(key) do |promise|
|
79
|
+
promise.reject(reason)
|
80
|
+
end
|
76
81
|
end
|
77
82
|
|
78
83
|
# Returns true when the key has already been fulfilled, otherwise returns false
|
@@ -92,6 +97,14 @@ module GraphQL::Batch
|
|
92
97
|
|
93
98
|
private
|
94
99
|
|
100
|
+
def finish_resolve(key)
|
101
|
+
promise = promise_for(key)
|
102
|
+
return yield(promise) unless executor
|
103
|
+
executor.around_promise_callbacks do
|
104
|
+
yield promise
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
95
108
|
def cache
|
96
109
|
@cache ||= {}
|
97
110
|
end
|
@@ -104,18 +117,19 @@ module GraphQL::Batch
|
|
104
117
|
cache.fetch(cache_key(load_key))
|
105
118
|
end
|
106
119
|
|
107
|
-
def
|
120
|
+
def reject_pending_promises(load_keys, err)
|
108
121
|
load_keys.each do |key|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
end
|
122
|
+
next unless promise_for(key).pending?
|
123
|
+
|
124
|
+
reject(key, err)
|
113
125
|
end
|
114
126
|
end
|
115
127
|
|
116
128
|
def check_for_broken_promises(load_keys)
|
117
|
-
|
118
|
-
|
129
|
+
load_keys.each do |key|
|
130
|
+
next unless promise_for(key).pending?
|
131
|
+
|
132
|
+
reject(key, ::Promise::BrokenError.new("#{self.class} didn't fulfill promise for key #{key.inspect}"))
|
119
133
|
end
|
120
134
|
end
|
121
135
|
end
|
@@ -9,11 +9,13 @@ module GraphQL::Batch
|
|
9
9
|
strategy = execution_context.strategy
|
10
10
|
return super if strategy.enable_batching
|
11
11
|
|
12
|
+
GraphQL::Batch::Executor.current.clear
|
12
13
|
begin
|
13
14
|
strategy.enable_batching = true
|
14
|
-
strategy.deep_sync(Promise.sync(super))
|
15
|
+
strategy.deep_sync(::Promise.sync(super))
|
15
16
|
ensure
|
16
17
|
strategy.enable_batching = false
|
18
|
+
GraphQL::Batch::Executor.current.clear
|
17
19
|
end
|
18
20
|
end
|
19
21
|
end
|
@@ -1,8 +1,6 @@
|
|
1
1
|
module GraphQL::Batch
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
executor ? executor.defer { super } : super
|
6
|
-
end
|
2
|
+
Promise = ::Promise
|
3
|
+
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3")
|
4
|
+
deprecate_constant :Promise
|
7
5
|
end
|
8
6
|
end
|
data/lib/graphql/batch/setup.rb
CHANGED
@@ -1,14 +1,54 @@
|
|
1
1
|
module GraphQL::Batch
|
2
|
-
|
3
|
-
|
2
|
+
class Setup
|
3
|
+
class << self
|
4
|
+
def start_batching(executor_class)
|
5
|
+
GraphQL::Batch::Executor.start_batch(executor_class)
|
6
|
+
end
|
7
|
+
|
8
|
+
def end_batching
|
9
|
+
GraphQL::Batch::Executor.end_batch
|
10
|
+
end
|
11
|
+
|
12
|
+
def instrument_field(schema, type, field)
|
13
|
+
return field unless type == schema.mutation
|
14
|
+
old_resolve_proc = field.resolve_proc
|
15
|
+
field.redefine do
|
16
|
+
resolve ->(obj, args, ctx) {
|
17
|
+
GraphQL::Batch::Executor.current.clear
|
18
|
+
begin
|
19
|
+
::Promise.sync(old_resolve_proc.call(obj, args, ctx))
|
20
|
+
ensure
|
21
|
+
GraphQL::Batch::Executor.current.clear
|
22
|
+
end
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def before_query(query)
|
28
|
+
warn "Deprecated graphql-batch setup `instrument(:query, GraphQL::Batch::Setup)`, replace with `use GraphQL::Batch`"
|
29
|
+
start_batching(GraphQL::Batch::Executor)
|
30
|
+
end
|
31
|
+
|
32
|
+
def after_query(query)
|
33
|
+
end_batching
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(schema, executor_class:)
|
38
|
+
@schema = schema
|
39
|
+
@executor_class = executor_class
|
40
|
+
end
|
4
41
|
|
5
42
|
def before_query(query)
|
6
|
-
|
7
|
-
GraphQL::Batch::Executor.current = GraphQL::Batch::Executor.new
|
43
|
+
Setup.start_batching(@executor_class)
|
8
44
|
end
|
9
45
|
|
10
46
|
def after_query(query)
|
11
|
-
|
47
|
+
Setup.end_batching
|
48
|
+
end
|
49
|
+
|
50
|
+
def instrument(type, field)
|
51
|
+
Setup.instrument_field(@schema, type, field)
|
12
52
|
end
|
13
53
|
end
|
14
54
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module GraphQL::Batch
|
2
|
+
class SetupMultiplex
|
3
|
+
def initialize(schema, executor_class:)
|
4
|
+
@schema = schema
|
5
|
+
@executor_class = executor_class
|
6
|
+
end
|
7
|
+
|
8
|
+
def before_multiplex(multiplex)
|
9
|
+
Setup.start_batching(@executor_class)
|
10
|
+
end
|
11
|
+
|
12
|
+
def after_multiplex(multiplex)
|
13
|
+
Setup.end_batching
|
14
|
+
end
|
15
|
+
|
16
|
+
def instrument(type, field)
|
17
|
+
Setup.instrument_field(@schema, type, field)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/graphql/batch.rb
CHANGED
@@ -7,20 +7,28 @@ require "promise.rb"
|
|
7
7
|
module GraphQL
|
8
8
|
module Batch
|
9
9
|
BrokenPromiseError = ::Promise::BrokenError
|
10
|
-
class
|
10
|
+
class NoExecutorError < StandardError; end
|
11
11
|
|
12
|
-
def self.batch
|
13
|
-
raise NestedError if GraphQL::Batch::Executor.current
|
12
|
+
def self.batch(executor_class: GraphQL::Batch::Executor)
|
14
13
|
begin
|
15
|
-
GraphQL::Batch::Executor.
|
16
|
-
Promise.sync(yield)
|
14
|
+
GraphQL::Batch::Executor.start_batch(executor_class)
|
15
|
+
::Promise.sync(yield)
|
17
16
|
ensure
|
18
|
-
GraphQL::Batch::Executor.
|
17
|
+
GraphQL::Batch::Executor.end_batch
|
19
18
|
end
|
20
19
|
end
|
21
20
|
|
22
|
-
def self.use(schema_defn)
|
23
|
-
schema_defn.
|
21
|
+
def self.use(schema_defn, executor_class: GraphQL::Batch::Executor)
|
22
|
+
schema = schema_defn.target
|
23
|
+
if GraphQL::VERSION >= "1.6.0"
|
24
|
+
instrumentation = GraphQL::Batch::SetupMultiplex.new(schema, executor_class: executor_class)
|
25
|
+
schema_defn.instrument(:multiplex, instrumentation)
|
26
|
+
schema_defn.instrument(:field, instrumentation)
|
27
|
+
else
|
28
|
+
instrumentation = GraphQL::Batch::Setup.new(schema, executor_class: executor_class)
|
29
|
+
schema_defn.instrument(:query, instrumentation)
|
30
|
+
schema_defn.instrument(:field, instrumentation)
|
31
|
+
end
|
24
32
|
schema_defn.lazy_resolve(::Promise, :sync)
|
25
33
|
end
|
26
34
|
|
@@ -34,3 +42,4 @@ require_relative "batch/loader"
|
|
34
42
|
require_relative "batch/executor"
|
35
43
|
require_relative "batch/promise"
|
36
44
|
require_relative "batch/setup"
|
45
|
+
require_relative "batch/setup_multiplex"
|
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.3.
|
4
|
+
version: 0.3.10
|
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: 2018-08-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -116,6 +116,8 @@ files:
|
|
116
116
|
- Rakefile
|
117
117
|
- bin/console
|
118
118
|
- bin/setup
|
119
|
+
- examples/association_loader.rb
|
120
|
+
- examples/record_loader.rb
|
119
121
|
- graphql-batch.gemspec
|
120
122
|
- lib/graphql/batch.rb
|
121
123
|
- lib/graphql/batch/execution_strategy.rb
|
@@ -124,6 +126,7 @@ files:
|
|
124
126
|
- lib/graphql/batch/mutation_execution_strategy.rb
|
125
127
|
- lib/graphql/batch/promise.rb
|
126
128
|
- lib/graphql/batch/setup.rb
|
129
|
+
- lib/graphql/batch/setup_multiplex.rb
|
127
130
|
- lib/graphql/batch/version.rb
|
128
131
|
homepage: https://github.com/Shopify/graphql-batch
|
129
132
|
licenses:
|
@@ -145,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
145
148
|
version: '0'
|
146
149
|
requirements: []
|
147
150
|
rubyforge_project:
|
148
|
-
rubygems_version: 2.
|
151
|
+
rubygems_version: 2.6.14
|
149
152
|
signing_key:
|
150
153
|
specification_version: 4
|
151
154
|
summary: A query batching executor for the graphql gem
|