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 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