graphql-batch-edge 0.4.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 +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +7 -0
- data/CONTRIBUTING.md +28 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/Rakefile +10 -0
- data/bin/console +7 -0
- data/bin/setup +5 -0
- data/examples/association_loader.rb +48 -0
- data/examples/record_loader.rb +25 -0
- data/graphql-batch.gemspec +27 -0
- data/lib/graphql/batch.rb +49 -0
- data/lib/graphql/batch/executor.rb +86 -0
- data/lib/graphql/batch/loader.rb +133 -0
- data/lib/graphql/batch/mutation_field_extension.rb +12 -0
- data/lib/graphql/batch/setup.rb +45 -0
- data/lib/graphql/batch/setup_multiplex.rb +20 -0
- data/lib/graphql/batch/version.rb +5 -0
- metadata +139 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f1cfb067d3ce1cf15b038bf8557342b45cdfa047
|
4
|
+
data.tar.gz: 2bf333fa584937a3e7b73ee5fd4e6921f167fe8b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f293f1fb48b1f1480655db9b0bf6f3a7851c3250f627d3c0ddc7b48e2bfcfed6d0378fedfa94a8fb65f6cfcb19066a4927a976dbe1b03ded76d6d2b52b5ccef7
|
7
|
+
data.tar.gz: d987a5823ace6f8d79bb8c17e257bc86f88c2da8369a623519d742297d8ab60edcb62251f6ca2bf4ad854aa3f30cab68ab2c9812c4437dd6afff25a379ef2a32
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
Types of contributions we welcome:
|
4
|
+
|
5
|
+
* Reporting issues with existing features
|
6
|
+
* Bug fixes
|
7
|
+
* Performance improvements
|
8
|
+
* Documentation and/or clearer interfaces
|
9
|
+
|
10
|
+
## Proposing Features
|
11
|
+
|
12
|
+
The main use case for this project is around batching queries
|
13
|
+
for GraphQL requests, but is open to changes that make it more
|
14
|
+
generic. This includes supporting concurrent or parallel executors.
|
15
|
+
|
16
|
+
When in doubt about whether we will be interested in including a
|
17
|
+
new feature in this project, please open an issue to propose the
|
18
|
+
feature so we can confirm the feature should be in scope for the
|
19
|
+
project before it is implemented.
|
20
|
+
|
21
|
+
## How To Contribute
|
22
|
+
|
23
|
+
1. Fork the [repository in github](https://github.com/Shopify/graphql-batch)
|
24
|
+
2. Create your feature branch (`git checkout -b fix-feature`)
|
25
|
+
3. Commit your changes (`git commit -am 'fix: Summarize change'`)
|
26
|
+
3. Make sure all tests pass (`bundle exec rake`)
|
27
|
+
4. Push to the branch (`git push origin fix-feature`)
|
28
|
+
5. [Create new pull request](https://github.com/Shopify/graphql-batch/pulls)
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Shopify Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
# GraphQL::Batch
|
2
|
+
|
3
|
+
[](https://travis-ci.org/Shopify/graphql-batch)
|
4
|
+
[](https://rubygems.org/gems/graphql-batch)
|
5
|
+
|
6
|
+
Provides an executor for the [`graphql` gem](https://github.com/rmosolgo/graphql-ruby) which allows queries to be batched.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'graphql-batch'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install graphql-batch
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
### Basic Usage
|
27
|
+
|
28
|
+
#### Schema Configuration
|
29
|
+
|
30
|
+
Require the library
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
require 'graphql/batch'
|
34
|
+
```
|
35
|
+
|
36
|
+
Define a custom loader, which is initialized with arguments that are used for grouping and a perform method for performing the batch load.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class RecordLoader < GraphQL::Batch::Loader
|
40
|
+
def initialize(model)
|
41
|
+
@model = model
|
42
|
+
end
|
43
|
+
|
44
|
+
def perform(ids)
|
45
|
+
@model.where(id: ids).each { |record| fulfill(record.id, record) }
|
46
|
+
ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
Use `GraphQL::Batch` as a plugin in your schema _after_ specifying the mutation
|
52
|
+
so that `GraphQL::Batch` can extend the mutation fields to clear the cache after
|
53
|
+
they are resolved (for graphql >= `1.5.0`).
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class MySchema < GraphQL::Schema
|
57
|
+
query MyQueryType
|
58
|
+
mutation MyMutationType
|
59
|
+
|
60
|
+
use GraphQL::Batch
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
For pre `1.5.0` versions:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
MySchema = GraphQL::Schema.define do
|
68
|
+
query MyQueryType
|
69
|
+
|
70
|
+
GraphQL::Batch.use(self)
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
#### Field Usage
|
75
|
+
|
76
|
+
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.
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
field :product, Types::Product, null: true do
|
80
|
+
argument :id, ID, required: true
|
81
|
+
end
|
82
|
+
|
83
|
+
def product(id:)
|
84
|
+
RecordLoader.for(Product).load(id)
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
The loader also supports batch loading an array of records instead of just a single record, via `load_many`. For example:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
field :products, [Types::Product, null: true], null: false do
|
92
|
+
argument :ids, [ID], required: true
|
93
|
+
end
|
94
|
+
|
95
|
+
def products(ids:)
|
96
|
+
RecordLoader.for(Product).load_many(ids)
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
Although this library doesn't have a dependency on active record,
|
101
|
+
the [examples directory](examples) has record and association loaders
|
102
|
+
for active record which handles edge cases like type casting ids
|
103
|
+
and overriding GraphQL::Batch::Loader#cache_key to load associations
|
104
|
+
on records with the same id.
|
105
|
+
|
106
|
+
### Promises
|
107
|
+
|
108
|
+
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`
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
def product_title(id:)
|
112
|
+
RecordLoader.for(Product).load(id).then do |product|
|
113
|
+
product.title
|
114
|
+
end
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
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.
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
def product_image(id:)
|
122
|
+
RecordLoader.for(Product).load(id).then do |product|
|
123
|
+
RecordLoader.for(Image).load(product.image_id)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
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.
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
def all_collections
|
132
|
+
Promise.all([
|
133
|
+
CountLoader.for(Shop, :smart_collections).load(context.shop_id),
|
134
|
+
CountLoader.for(Shop, :custom_collections).load(context.shop_id),
|
135
|
+
]).then do |results|
|
136
|
+
results.reduce(&:+)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
`.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
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
def product(id:)
|
145
|
+
# Try the cache first ...
|
146
|
+
CacheLoader.for(Product).load(args["id"]).then(nil, lambda do |exc|
|
147
|
+
# But if there's a connection error, go to the underlying database
|
148
|
+
raise exc unless exc.is_a?(Redis::BaseConnectionError)
|
149
|
+
logger.warn err.message
|
150
|
+
RecordLoader.for(Product).load(args["id"])
|
151
|
+
end)
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
## Unit Testing
|
156
|
+
|
157
|
+
Your loaders can be tested outside of a GraphQL query by doing the
|
158
|
+
batch loads in a block passed to GraphQL::Batch.batch. That method
|
159
|
+
will set up thread-local state to store the loaders, batch load any
|
160
|
+
promise returned from the block then clear the thread-local state
|
161
|
+
to avoid leaking state between tests.
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
def test_single_query
|
165
|
+
product = products(:snowboard)
|
166
|
+
title = GraphQL::Batch.batch do
|
167
|
+
RecordLoader.for(Product).load(product.id).then(&:title)
|
168
|
+
end
|
169
|
+
assert_equal product.title, title
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
## Development
|
174
|
+
|
175
|
+
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.
|
176
|
+
|
177
|
+
## Contributing
|
178
|
+
|
179
|
+
See our [contributing guidelines](CONTRIBUTING.md) for more information.
|
180
|
+
|
181
|
+
## License
|
182
|
+
|
183
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
@@ -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
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'graphql/batch/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "graphql-batch-edge"
|
8
|
+
spec.version = GraphQL::Batch::VERSION
|
9
|
+
spec.authors = ["Dylan Thacker-Smith"]
|
10
|
+
spec.email = ["gems@shopify.com"]
|
11
|
+
|
12
|
+
spec.summary = "A query batching executor for the graphql gem (Unofficial Edge Releases)"
|
13
|
+
spec.homepage = "https://github.com/Shopify/graphql-batch"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency "graphql", ">= 1.3", "< 2"
|
22
|
+
spec.add_runtime_dependency "promise.rb", "~> 0.7.2"
|
23
|
+
|
24
|
+
spec.add_development_dependency "byebug" if RUBY_ENGINE == 'ruby'
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "minitest"
|
27
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "graphql"
|
2
|
+
require "promise.rb"
|
3
|
+
|
4
|
+
module GraphQL
|
5
|
+
module Batch
|
6
|
+
BrokenPromiseError = ::Promise::BrokenError
|
7
|
+
class NoExecutorError < StandardError; end
|
8
|
+
|
9
|
+
def self.batch(executor_class: GraphQL::Batch::Executor)
|
10
|
+
begin
|
11
|
+
GraphQL::Batch::Executor.start_batch(executor_class)
|
12
|
+
::Promise.sync(yield)
|
13
|
+
ensure
|
14
|
+
GraphQL::Batch::Executor.end_batch
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.use(schema_defn, executor_class: GraphQL::Batch::Executor)
|
19
|
+
schema = schema_defn.target
|
20
|
+
if GraphQL::VERSION >= "1.6.0"
|
21
|
+
instrumentation = GraphQL::Batch::SetupMultiplex.new(schema, executor_class: executor_class)
|
22
|
+
schema_defn.instrument(:multiplex, instrumentation)
|
23
|
+
if schema.mutation
|
24
|
+
if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('1.9.0.pre3') &&
|
25
|
+
schema.mutation.metadata[:type_class]
|
26
|
+
require_relative "batch/mutation_field_extension"
|
27
|
+
schema.mutation.fields.each do |name, f|
|
28
|
+
field = f.metadata[:type_class]
|
29
|
+
field.extension(GraphQL::Batch::MutationFieldExtension)
|
30
|
+
end
|
31
|
+
else
|
32
|
+
schema_defn.instrument(:field, instrumentation)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
else
|
36
|
+
instrumentation = GraphQL::Batch::Setup.new(schema, executor_class: executor_class)
|
37
|
+
schema_defn.instrument(:query, instrumentation)
|
38
|
+
schema_defn.instrument(:field, instrumentation)
|
39
|
+
end
|
40
|
+
schema_defn.lazy_resolve(::Promise, :sync)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
require_relative "batch/version"
|
46
|
+
require_relative "batch/loader"
|
47
|
+
require_relative "batch/executor"
|
48
|
+
require_relative "batch/setup"
|
49
|
+
require_relative "batch/setup_multiplex"
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module GraphQL::Batch
|
2
|
+
class Executor
|
3
|
+
THREAD_KEY = :"#{name}.batched_queries"
|
4
|
+
private_constant :THREAD_KEY
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def current
|
8
|
+
Thread.current[THREAD_KEY]
|
9
|
+
end
|
10
|
+
|
11
|
+
def current=(executor)
|
12
|
+
Thread.current[THREAD_KEY] = executor
|
13
|
+
end
|
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
|
29
|
+
|
30
|
+
# Set to true when performing a batch query, otherwise, it is false.
|
31
|
+
#
|
32
|
+
# Can be used to detect unbatched queries in an ActiveSupport::Notifications.subscribe block.
|
33
|
+
attr_reader :loading
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
@loaders = {}
|
37
|
+
@loading = false
|
38
|
+
@nesting_level = 0
|
39
|
+
end
|
40
|
+
|
41
|
+
def loader(key)
|
42
|
+
@loaders[key] ||= yield.tap do |loader|
|
43
|
+
loader.executor = self
|
44
|
+
loader.loader_key = key
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def resolve(loader)
|
49
|
+
was_loading = @loading
|
50
|
+
@loading = true
|
51
|
+
loader.resolve
|
52
|
+
ensure
|
53
|
+
@loading = was_loading
|
54
|
+
end
|
55
|
+
|
56
|
+
def tick
|
57
|
+
resolve(@loaders.shift.last)
|
58
|
+
end
|
59
|
+
|
60
|
+
def wait_all
|
61
|
+
tick until @loaders.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
def clear
|
65
|
+
@loaders.clear
|
66
|
+
end
|
67
|
+
|
68
|
+
def increment_level
|
69
|
+
@nesting_level += 1
|
70
|
+
end
|
71
|
+
|
72
|
+
def decrement_level
|
73
|
+
@nesting_level -= 1
|
74
|
+
end
|
75
|
+
|
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
|
79
|
+
was_loading = @loading
|
80
|
+
@loading = false
|
81
|
+
yield
|
82
|
+
ensure
|
83
|
+
@loading = was_loading
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module GraphQL::Batch
|
2
|
+
class Loader
|
3
|
+
def self.for(*group_args)
|
4
|
+
loader_key = loader_key_for(*group_args)
|
5
|
+
executor = Executor.current
|
6
|
+
|
7
|
+
unless executor
|
8
|
+
raise GraphQL::Batch::NoExecutorError, 'Cannot create loader without'\
|
9
|
+
' an Executor. Wrap the call to `for` with `GraphQL::Batch.batch`'\
|
10
|
+
' or use `GraphQL::Batch::Setup` as a query instrumenter if'\
|
11
|
+
' using with `graphql-ruby`'
|
12
|
+
end
|
13
|
+
|
14
|
+
executor.loader(loader_key) { new(*group_args) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.loader_key_for(*group_args)
|
18
|
+
[self].concat(group_args)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.load(key)
|
22
|
+
self.for.load(key)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.load_many(keys)
|
26
|
+
self.for.load_many(keys)
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_accessor :loader_key, :executor
|
30
|
+
|
31
|
+
def load(key)
|
32
|
+
cache[cache_key(key)] ||= begin
|
33
|
+
queue << key
|
34
|
+
::Promise.new.tap { |promise| promise.source = self }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_many(keys)
|
39
|
+
::Promise.all(keys.map { |key| load(key) })
|
40
|
+
end
|
41
|
+
|
42
|
+
def resolve #:nodoc:
|
43
|
+
return if resolved?
|
44
|
+
load_keys = queue
|
45
|
+
@queue = nil
|
46
|
+
perform(load_keys)
|
47
|
+
check_for_broken_promises(load_keys)
|
48
|
+
rescue => err
|
49
|
+
reject_pending_promises(load_keys, err)
|
50
|
+
end
|
51
|
+
|
52
|
+
# For Promise#sync
|
53
|
+
def wait #:nodoc:
|
54
|
+
if executor
|
55
|
+
executor.resolve(self)
|
56
|
+
else
|
57
|
+
resolve
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def resolved?
|
62
|
+
@queue.nil? || @queue.empty?
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
# Fulfill the key with provided value, for use in #perform
|
68
|
+
def fulfill(key, value)
|
69
|
+
finish_resolve(key) do |promise|
|
70
|
+
promise.fulfill(value)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def reject(key, reason)
|
75
|
+
finish_resolve(key) do |promise|
|
76
|
+
promise.reject(reason)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns true when the key has already been fulfilled, otherwise returns false
|
81
|
+
def fulfilled?(key)
|
82
|
+
promise_for(key).fulfilled?
|
83
|
+
end
|
84
|
+
|
85
|
+
# Must override to load the keys and call #fulfill for each key
|
86
|
+
def perform(keys)
|
87
|
+
raise NotImplementedError
|
88
|
+
end
|
89
|
+
|
90
|
+
# Override to use a different key for the cache than the load key
|
91
|
+
def cache_key(load_key)
|
92
|
+
load_key
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def finish_resolve(key)
|
98
|
+
promise = promise_for(key)
|
99
|
+
return yield(promise) unless executor
|
100
|
+
executor.around_promise_callbacks do
|
101
|
+
yield promise
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def cache
|
106
|
+
@cache ||= {}
|
107
|
+
end
|
108
|
+
|
109
|
+
def queue
|
110
|
+
@queue ||= []
|
111
|
+
end
|
112
|
+
|
113
|
+
def promise_for(load_key)
|
114
|
+
cache.fetch(cache_key(load_key))
|
115
|
+
end
|
116
|
+
|
117
|
+
def reject_pending_promises(load_keys, err)
|
118
|
+
load_keys.each do |key|
|
119
|
+
next unless promise_for(key).pending?
|
120
|
+
|
121
|
+
reject(key, err)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def check_for_broken_promises(load_keys)
|
126
|
+
load_keys.each do |key|
|
127
|
+
next unless promise_for(key).pending?
|
128
|
+
|
129
|
+
reject(key, ::Promise::BrokenError.new("#{self.class} didn't fulfill promise for key #{key.inspect}"))
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module GraphQL::Batch
|
2
|
+
class MutationFieldExtension < GraphQL::Schema::FieldExtension
|
3
|
+
def resolve(object:, arguments:, **_rest)
|
4
|
+
GraphQL::Batch::Executor.current.clear
|
5
|
+
begin
|
6
|
+
::Promise.sync(yield(object, arguments))
|
7
|
+
ensure
|
8
|
+
GraphQL::Batch::Executor.current.clear
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module GraphQL::Batch
|
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
|
+
end
|
27
|
+
|
28
|
+
def initialize(schema, executor_class:)
|
29
|
+
@schema = schema
|
30
|
+
@executor_class = executor_class
|
31
|
+
end
|
32
|
+
|
33
|
+
def before_query(query)
|
34
|
+
Setup.start_batching(@executor_class)
|
35
|
+
end
|
36
|
+
|
37
|
+
def after_query(query)
|
38
|
+
Setup.end_batching
|
39
|
+
end
|
40
|
+
|
41
|
+
def instrument(type, field)
|
42
|
+
Setup.instrument_field(@schema, type, field)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
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
|
metadata
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: graphql-batch-edge
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dylan Thacker-Smith
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-06-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: graphql
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '2'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: promise.rb
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 0.7.2
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.7.2
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: byebug
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rake
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '10.0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '10.0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: minitest
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
description:
|
90
|
+
email:
|
91
|
+
- gems@shopify.com
|
92
|
+
executables: []
|
93
|
+
extensions: []
|
94
|
+
extra_rdoc_files: []
|
95
|
+
files:
|
96
|
+
- ".gitignore"
|
97
|
+
- ".travis.yml"
|
98
|
+
- CONTRIBUTING.md
|
99
|
+
- Gemfile
|
100
|
+
- LICENSE.txt
|
101
|
+
- README.md
|
102
|
+
- Rakefile
|
103
|
+
- bin/console
|
104
|
+
- bin/setup
|
105
|
+
- examples/association_loader.rb
|
106
|
+
- examples/record_loader.rb
|
107
|
+
- graphql-batch.gemspec
|
108
|
+
- lib/graphql/batch.rb
|
109
|
+
- lib/graphql/batch/executor.rb
|
110
|
+
- lib/graphql/batch/loader.rb
|
111
|
+
- lib/graphql/batch/mutation_field_extension.rb
|
112
|
+
- lib/graphql/batch/setup.rb
|
113
|
+
- lib/graphql/batch/setup_multiplex.rb
|
114
|
+
- lib/graphql/batch/version.rb
|
115
|
+
homepage: https://github.com/Shopify/graphql-batch
|
116
|
+
licenses:
|
117
|
+
- MIT
|
118
|
+
metadata: {}
|
119
|
+
post_install_message:
|
120
|
+
rdoc_options: []
|
121
|
+
require_paths:
|
122
|
+
- lib
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
requirements: []
|
134
|
+
rubyforge_project:
|
135
|
+
rubygems_version: 2.4.6
|
136
|
+
signing_key:
|
137
|
+
specification_version: 4
|
138
|
+
summary: A query batching executor for the graphql gem (Unofficial Edge Releases)
|
139
|
+
test_files: []
|