graphql-batch 0.3.3 → 0.3.10

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b69567be96fe5b00779872cbb3a6d1e14b3d2a28
4
- data.tar.gz: cfc2323ef559f92884560f68713b3d9cd41a3017
3
+ metadata.gz: c793255ae48cf638179b9e786d03493a469d1629
4
+ data.tar.gz: d235b788e4f9208747d7145e7d2599c2279319d1
5
5
  SHA512:
6
- metadata.gz: 423111d4a94fde2835dcfea8cafa234047100d0dd2bc248362b988537ee1dc1c35c86e5924f8db3730d95b09fbb20a49c3dfa3d6e8858271d1ad060a8303e086
7
- data.tar.gz: d31cc1b4cddb5db6d1650e51632b26f113e5414c6d9dc44916785b7a5911c0c953e9b1fd476395fea8d9eca2ad502e08a55ead3c7bec994ebdd4059284635980
6
+ metadata.gz: 7b263790b1871d54d9258e7bd2541c17361b8177a79e07aac146f9d3ccf05570cf07f8aaac40cf4dbe76c3b6f1677f3cd957a32904742c069a133d3906044767
7
+ data.tar.gz: d00d449a8872286251cc21d555b54be0690002f697dddb8afc1f43e6b90d75e42367ad70f54c701fc75d89b04427b4d127879481c406a24555d3ada35ce0582f
data/.travis.yml CHANGED
@@ -2,4 +2,6 @@ language: ruby
2
2
  rvm:
3
3
  - 2.1
4
4
  - 2.2
5
+ - 2.3
6
+ - 2.4
5
7
  before_install: gem install bundler -v 1.13.3
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
@@ -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
- spec.add_development_dependency "byebug" if RUBY_ENGINE == 'ruby'
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"
@@ -13,7 +13,7 @@ module GraphQL::Batch
13
13
 
14
14
  # Needed for MutationExecutionStrategy
15
15
  def deep_sync(result) #:nodoc:
16
- Promise.sync(as_promise_unless_resolved(result))
16
+ ::Promise.sync(as_promise_unless_resolved(result))
17
17
  end
18
18
 
19
19
  private
@@ -3,15 +3,29 @@ module GraphQL::Batch
3
3
  THREAD_KEY = :"#{name}.batched_queries"
4
4
  private_constant :THREAD_KEY
5
5
 
6
- def self.current
7
- Thread.current[THREAD_KEY]
8
- end
6
+ class << self
7
+ def current
8
+ Thread.current[THREAD_KEY]
9
+ end
9
10
 
10
- def self.current=(executor)
11
- Thread.current[THREAD_KEY] = executor
12
- end
11
+ def current=(executor)
12
+ Thread.current[THREAD_KEY] = executor
13
+ end
13
14
 
14
- attr_reader :loaders
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 resolve(loader)
27
- with_loading(true) { loader.resolve }
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 shift
31
- @loaders.shift.last
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 defer
47
- # Since we aren't actually deferring callbacks, we need to set #loading to false so that any queries
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
- private
72
+ def decrement_level
73
+ @nesting_level -= 1
74
+ end
53
75
 
54
- def with_loading(loading)
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
- begin
57
- @loading = loading
58
- yield
59
- ensure
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
@@ -1,21 +1,20 @@
1
1
  module GraphQL::Batch
2
2
  class Loader
3
- class NoExecutorError < StandardError; end
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, "Cannot create loader without an Executor."\
11
- " Wrap the call to `for` with `GraphQL::Batch.batch` or use"\
12
- " `GraphQL::Batch::Setup` as a query instrumenter if using with `graphql-ruby`"
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.loaders[loader_key] ||= new(*group_args).tap do |loader|
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
- each_pending_promise(load_keys) do |key, promise|
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
- promise_for(key).fulfill(value)
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 each_pending_promise(load_keys)
120
+ def reject_pending_promises(load_keys, err)
108
121
  load_keys.each do |key|
109
- promise = promise_for(key)
110
- if promise.pending?
111
- yield key, promise
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
- each_pending_promise(load_keys) do |key, promise|
118
- promise.reject(::Promise::BrokenError.new("#{self.class} didn't fulfill promise for key #{key.inspect}"))
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
- class Promise < ::Promise
3
- def defer
4
- executor = Executor.current
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
@@ -1,14 +1,54 @@
1
1
  module GraphQL::Batch
2
- module Setup
3
- extend self
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
- raise NestedError if GraphQL::Batch::Executor.current
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
- GraphQL::Batch::Executor.current = nil
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
@@ -1,5 +1,5 @@
1
1
  module GraphQL
2
2
  module Batch
3
- VERSION = "0.3.3"
3
+ VERSION = "0.3.10"
4
4
  end
5
5
  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 NestedError < StandardError; end
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.current = GraphQL::Batch::Executor.new
16
- Promise.sync(yield)
14
+ GraphQL::Batch::Executor.start_batch(executor_class)
15
+ ::Promise.sync(yield)
17
16
  ensure
18
- GraphQL::Batch::Executor.current = nil
17
+ GraphQL::Batch::Executor.end_batch
19
18
  end
20
19
  end
21
20
 
22
- def self.use(schema_defn)
23
- schema_defn.instrument(:query, GraphQL::Batch::Setup)
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.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: 2017-05-31 00:00:00.000000000 Z
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.5.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