graphql 1.12.11 → 1.12.12

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
  SHA256:
3
- metadata.gz: 9e46a535c108ff0ae582fcfa598b21cda8faad15142629b1cdae772f3d24cd7e
4
- data.tar.gz: 3185ec249d624c6b23920d9d9fb47167ac031b7645053ff705e3c04acc0fa571
3
+ metadata.gz: 296954a3b03f2fd5dae9fae56eaa08ea04b3dfb8f25923201ea27c67147c71fa
4
+ data.tar.gz: 4de323637c29b729a3eb3f07b620e2014515b37235c485002d016ead490d86e7
5
5
  SHA512:
6
- metadata.gz: 989c1074a6de63a31002d2bd92da719e1b08682d3eefe361add671a75c1991173b13cc6516f8ede8ff5bcf7499935e514cd8d7d57a2940e85160bc427538e99c
7
- data.tar.gz: cb40ba585e721d28856951312755ef84bdd698a7299b1a383dd4f9db77d8b137e8d21ee9a8784b14780b4230a5a5325520d0ceadf90f6bd055781cc7159257b5
6
+ metadata.gz: fdf50f6c6f170aa4c50d46356e4089c40a939bf3d0dfd4ab7bdb667d169f01a38320c971cdec1dde32353a20823c1756196312df5e425af41a157caa1179e845
7
+ data.tar.gz: 4202ca4741998ee1f5535eea14ff93c1a8529ff5085625d67171e75736f65f55d0433b93512c272e16f4f9a48dd630eba49cf37a54e49b0e9e3dad5f0496930c
@@ -77,6 +77,21 @@ module GraphQL
77
77
  nil
78
78
  end
79
79
 
80
+ # Use a self-contained queue for the work in the block.
81
+ def run_isolated
82
+ prev_queue = @pending_jobs
83
+ @pending_jobs = []
84
+ res = nil
85
+ # Make sure the block is inside a Fiber, so it can `Fiber.yield`
86
+ append_job {
87
+ res = yield
88
+ }
89
+ run
90
+ res
91
+ ensure
92
+ @pending_jobs = prev_queue
93
+ end
94
+
80
95
  # @api private Move along, move along
81
96
  def run
82
97
  # At a high level, the algorithm is:
@@ -10,6 +10,7 @@ module GraphQL
10
10
  # These are all no-ops because code was
11
11
  # executed sychronously.
12
12
  def run; end
13
+ def run_isolated; yield; end
13
14
  def yield; end
14
15
 
15
16
  def append_job
@@ -28,11 +28,12 @@ module GraphQL
28
28
  end
29
29
 
30
30
  def fetch(ast_node, argument_owner, parent_object)
31
- @storage[ast_node][argument_owner][parent_object]
32
31
  # If any jobs were enqueued, run them now,
33
32
  # since this might have been called outside of execution.
34
33
  # (The jobs are responsible for updating `result` in-place.)
35
- @dataloader.run
34
+ @dataloader.run_isolated do
35
+ @storage[ast_node][argument_owner][parent_object]
36
+ end
36
37
  # Ack, the _hash_ is updated, but the key is eventually
37
38
  # overridden with an immutable arguments instance.
38
39
  # The first call queues up the job,
@@ -24,12 +24,33 @@ module GraphQL
24
24
 
25
25
  class GraphQLResultHash < Hash
26
26
  include GraphQLResult
27
+
28
+ attr_accessor :graphql_merged_into
29
+
30
+ def []=(key, value)
31
+ # This is a hack.
32
+ # Basically, this object is merged into the root-level result at some point.
33
+ # But the problem is, some lazies are created whose closures retain reference to _this_
34
+ # object. When those lazies are resolved, they cause an update to this object.
35
+ #
36
+ # In order to return a proper top-level result, we have to update that top-level result object.
37
+ # In order to return a proper partial result (eg, for a directive), we have to update this object, too.
38
+ # Yowza.
39
+ if (t = @graphql_merged_into)
40
+ t[key] = value
41
+ end
42
+ super
43
+ end
27
44
  end
28
45
 
29
46
  class GraphQLResultArray < Array
30
47
  include GraphQLResult
31
48
  end
32
49
 
50
+ class GraphQLSelectionSet < Hash
51
+ attr_accessor :graphql_directives
52
+ end
53
+
33
54
  # @return [GraphQL::Query]
34
55
  attr_reader :query
35
56
 
@@ -50,6 +71,14 @@ module GraphQL
50
71
  @multiplex_context = query.multiplex.context
51
72
  @interpreter_context = @context.namespace(:interpreter)
52
73
  @response = GraphQLResultHash.new
74
+ # Identify runtime directives by checking which of this schema's directives have overridden `def self.resolve`
75
+ @runtime_directive_names = []
76
+ noop_resolve_owner = GraphQL::Schema::Directive.singleton_class
77
+ schema.directives.each do |name, dir_defn|
78
+ if dir_defn.method(:resolve).owner != noop_resolve_owner
79
+ @runtime_directive_names << name
80
+ end
81
+ end
53
82
  # A cache of { Class => { String => Schema::Field } }
54
83
  # Which assumes that MyObject.get_field("myField") will return the same field
55
84
  # during the lifetime of a query
@@ -62,6 +91,16 @@ module GraphQL
62
91
  "#<#{self.class.name} response=#{@response.inspect}>"
63
92
  end
64
93
 
94
+ def tap_or_each(obj_or_array)
95
+ if obj_or_array.is_a?(Array)
96
+ obj_or_array.each do |item|
97
+ yield(item, true)
98
+ end
99
+ else
100
+ yield(obj_or_array, false)
101
+ end
102
+ end
103
+
65
104
  # This _begins_ the execution. Some deferred work
66
105
  # might be stored up in lazies.
67
106
  # @return [void]
@@ -78,20 +117,40 @@ module GraphQL
78
117
  # Root .authorized? returned false.
79
118
  @response = nil
80
119
  else
81
- resolve_with_directives(object_proxy, root_operation) do # execute query level directives
120
+ resolve_with_directives(object_proxy, root_operation.directives) do # execute query level directives
82
121
  gathered_selections = gather_selections(object_proxy, root_type, root_operation.selections)
83
- # Make the first fiber which will begin execution
84
- @dataloader.append_job {
85
- evaluate_selections(
86
- path,
87
- context.scoped_context,
88
- object_proxy,
89
- root_type,
90
- root_op_type == "mutation",
91
- gathered_selections,
92
- @response,
93
- )
94
- }
122
+ # This is kind of a hack -- `gathered_selections` is an Array if any of the selections
123
+ # require isolation during execution (because of runtime directives). In that case,
124
+ # make a new, isolated result hash for writing the result into. (That isolated response
125
+ # is eventually merged back into the main response)
126
+ #
127
+ # Otherwise, `gathered_selections` is a hash of selections which can be
128
+ # directly evaluated and the results can be written right into the main response hash.
129
+ tap_or_each(gathered_selections) do |selections, is_selection_array|
130
+ if is_selection_array
131
+ selection_response = GraphQLResultHash.new
132
+ final_response = @response
133
+ else
134
+ selection_response = @response
135
+ final_response = nil
136
+ end
137
+
138
+ @dataloader.append_job {
139
+ set_all_interpreter_context(query.root_value, nil, nil, path)
140
+ resolve_with_directives(object_proxy, selections.graphql_directives) do
141
+ evaluate_selections(
142
+ path,
143
+ context.scoped_context,
144
+ object_proxy,
145
+ root_type,
146
+ root_op_type == "mutation",
147
+ selections,
148
+ selection_response,
149
+ final_response,
150
+ )
151
+ end
152
+ }
153
+ end
95
154
  end
96
155
  end
97
156
  delete_interpreter_context(:current_path)
@@ -101,15 +160,36 @@ module GraphQL
101
160
  nil
102
161
  end
103
162
 
104
- def gather_selections(owner_object, owner_type, selections, selections_by_name = {})
163
+ # @return [void]
164
+ def deep_merge_selection_result(from_result, into_result)
165
+ from_result.each do |key, value|
166
+ if !into_result.key?(key)
167
+ into_result[key] = value
168
+ else
169
+ case value
170
+ when Hash
171
+ deep_merge_selection_result(value, into_result[key])
172
+ else
173
+ # We have to assume that, since this passed the `fields_will_merge` selection,
174
+ # that the old and new values are the same.
175
+ # There's no special handling of arrays because currently, there's no way to split the execution
176
+ # of a list over several concurrent flows.
177
+ into_result[key] = value
178
+ end
179
+ end
180
+ end
181
+ from_result.graphql_merged_into = into_result
182
+ nil
183
+ end
184
+
185
+ def gather_selections(owner_object, owner_type, selections, selections_to_run = nil, selections_by_name = GraphQLSelectionSet.new)
105
186
  selections.each do |node|
106
187
  # Skip gathering this if the directive says so
107
188
  if !directives_include?(node, owner_object, owner_type)
108
189
  next
109
190
  end
110
191
 
111
- case node
112
- when GraphQL::Language::Nodes::Field
192
+ if node.is_a?(GraphQL::Language::Nodes::Field)
113
193
  response_key = node.alias || node.name
114
194
  selections = selections_by_name[response_key]
115
195
  # if there was already a selection of this field,
@@ -125,52 +205,77 @@ module GraphQL
125
205
  # No selection was found for this field yet
126
206
  selections_by_name[response_key] = node
127
207
  end
128
- when GraphQL::Language::Nodes::InlineFragment
129
- if node.type
130
- type_defn = schema.get_type(node.type.name)
131
- # Faster than .map{}.include?()
132
- query.warden.possible_types(type_defn).each do |t|
208
+ else
209
+ # This is an InlineFragment or a FragmentSpread
210
+ if @runtime_directive_names.any? && node.directives.any? { |d| @runtime_directive_names.include?(d.name) }
211
+ next_selections = GraphQLSelectionSet.new
212
+ next_selections.graphql_directives = node.directives
213
+ if selections_to_run
214
+ selections_to_run << next_selections
215
+ else
216
+ selections_to_run = []
217
+ selections_to_run << selections_by_name
218
+ selections_to_run << next_selections
219
+ end
220
+ else
221
+ next_selections = selections_by_name
222
+ end
223
+
224
+ case node
225
+ when GraphQL::Language::Nodes::InlineFragment
226
+ if node.type
227
+ type_defn = schema.get_type(node.type.name)
228
+
229
+ # Faster than .map{}.include?()
230
+ query.warden.possible_types(type_defn).each do |t|
231
+ if t == owner_type
232
+ gather_selections(owner_object, owner_type, node.selections, selections_to_run, next_selections)
233
+ break
234
+ end
235
+ end
236
+ else
237
+ # it's an untyped fragment, definitely continue
238
+ gather_selections(owner_object, owner_type, node.selections, selections_to_run, next_selections)
239
+ end
240
+ when GraphQL::Language::Nodes::FragmentSpread
241
+ fragment_def = query.fragments[node.name]
242
+ type_defn = schema.get_type(fragment_def.type.name)
243
+ possible_types = query.warden.possible_types(type_defn)
244
+ possible_types.each do |t|
133
245
  if t == owner_type
134
- gather_selections(owner_object, owner_type, node.selections, selections_by_name)
246
+ gather_selections(owner_object, owner_type, fragment_def.selections, selections_to_run, next_selections)
135
247
  break
136
248
  end
137
249
  end
138
250
  else
139
- # it's an untyped fragment, definitely continue
140
- gather_selections(owner_object, owner_type, node.selections, selections_by_name)
141
- end
142
- when GraphQL::Language::Nodes::FragmentSpread
143
- fragment_def = query.fragments[node.name]
144
- type_defn = schema.get_type(fragment_def.type.name)
145
- possible_types = query.warden.possible_types(type_defn)
146
- possible_types.each do |t|
147
- if t == owner_type
148
- gather_selections(owner_object, owner_type, fragment_def.selections, selections_by_name)
149
- break
150
- end
251
+ raise "Invariant: unexpected selection class: #{node.class}"
151
252
  end
152
- else
153
- raise "Invariant: unexpected selection class: #{node.class}"
154
253
  end
155
254
  end
156
- selections_by_name
255
+ selections_to_run || selections_by_name
157
256
  end
158
257
 
159
258
  NO_ARGS = {}.freeze
160
259
 
161
260
  # @return [void]
162
- def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections, selections_result)
261
+ def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections, selections_result, target_result) # rubocop:disable Metrics/ParameterLists
163
262
  set_all_interpreter_context(owner_object, nil, nil, path)
164
263
 
264
+ finished_jobs = 0
265
+ enqueued_jobs = gathered_selections.size
165
266
  gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
166
267
  @dataloader.append_job {
167
268
  evaluate_selection(
168
269
  path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection, selections_result
169
270
  )
271
+ finished_jobs += 1
272
+ if target_result && finished_jobs == enqueued_jobs
273
+ deep_merge_selection_result(selections_result, target_result)
274
+ end
170
275
  }
171
276
  end
172
277
 
173
- nil
278
+ selections_result
174
279
  end
175
280
 
176
281
  attr_reader :progress_path
@@ -292,12 +397,17 @@ module GraphQL
292
397
  # Optimize for the case that field is selected only once
293
398
  if field_ast_nodes.nil? || field_ast_nodes.size == 1
294
399
  next_selections = ast_node.selections
400
+ directives = ast_node.directives
295
401
  else
296
402
  next_selections = []
297
- field_ast_nodes.each { |f| next_selections.concat(f.selections) }
403
+ directives = []
404
+ field_ast_nodes.each { |f|
405
+ next_selections.concat(f.selections)
406
+ directives.concat(f.directives)
407
+ }
298
408
  end
299
409
 
300
- field_result = resolve_with_directives(object, ast_node) do
410
+ field_result = resolve_with_directives(object, directives) do
301
411
  # Actually call the field resolver and capture the result
302
412
  app_result = begin
303
413
  query.with_error_handling do
@@ -488,8 +598,39 @@ module GraphQL
488
598
  response_hash.graphql_result_name = result_name
489
599
  set_result(selection_result, result_name, response_hash)
490
600
  gathered_selections = gather_selections(continue_value, current_type, next_selections)
491
- evaluate_selections(path, context.scoped_context, continue_value, current_type, false, gathered_selections, response_hash)
492
- response_hash
601
+ # There are two possibilities for `gathered_selections`:
602
+ # 1. All selections of this object should be evaluated together (there are no runtime directives modifying execution).
603
+ # This case is handled below, and the result can be written right into the main `response_hash` above.
604
+ # In this case, `gathered_selections` is a hash of selections.
605
+ # 2. Some selections of this object have runtime directives that may or may not modify execution.
606
+ # That part of the selection is evaluated in an isolated way, writing into a sub-response object which is
607
+ # eventually merged into the final response. In this case, `gathered_selections` is an array of things to run in isolation.
608
+ # (Technically, it's possible that one of those entries _doesn't_ require isolation.)
609
+ tap_or_each(gathered_selections) do |selections, is_selection_array|
610
+ if is_selection_array
611
+ this_result = GraphQLResultHash.new
612
+ this_result.graphql_parent = selection_result
613
+ this_result.graphql_result_name = result_name
614
+ final_result = response_hash
615
+ else
616
+ this_result = response_hash
617
+ final_result = nil
618
+ end
619
+ set_all_interpreter_context(continue_value, nil, nil, path) # reset this mutable state
620
+ resolve_with_directives(continue_value, selections.graphql_directives) do
621
+ evaluate_selections(
622
+ path,
623
+ context.scoped_context,
624
+ continue_value,
625
+ current_type,
626
+ false,
627
+ selections,
628
+ this_result,
629
+ final_result,
630
+ )
631
+ this_result
632
+ end
633
+ end
493
634
  end
494
635
  end
495
636
  when "LIST"
@@ -534,13 +675,13 @@ module GraphQL
534
675
  end
535
676
  end
536
677
 
537
- def resolve_with_directives(object, ast_node, &block)
538
- return yield if ast_node.directives.empty?
539
- run_directive(object, ast_node, 0, &block)
678
+ def resolve_with_directives(object, directives, &block)
679
+ return yield if directives.nil? || directives.empty?
680
+ run_directive(object, directives, 0, &block)
540
681
  end
541
682
 
542
- def run_directive(object, ast_node, idx, &block)
543
- dir_node = ast_node.directives[idx]
683
+ def run_directive(object, directives, idx, &block)
684
+ dir_node = directives[idx]
544
685
  if !dir_node
545
686
  yield
546
687
  else
@@ -548,9 +689,9 @@ module GraphQL
548
689
  if !dir_defn.is_a?(Class)
549
690
  dir_defn = dir_defn.type_class || raise("Only class-based directives are supported (not `@#{dir_node.name}`)")
550
691
  end
551
- dir_args = arguments(nil, dir_defn, dir_node).keyword_arguments
692
+ dir_args = arguments(nil, dir_defn, dir_node)
552
693
  dir_defn.resolve(object, dir_args, context) do
553
- run_directive(object, ast_node, idx + 1, &block)
694
+ run_directive(object, directives, idx + 1, &block)
554
695
  end
555
696
  end
556
697
  end
@@ -559,7 +700,7 @@ module GraphQL
559
700
  def directives_include?(node, graphql_object, parent_type)
560
701
  node.directives.each do |dir_node|
561
702
  dir_defn = schema.directives.fetch(dir_node.name).type_class || raise("Only class-based directives are supported (not #{dir_node.name.inspect})")
562
- args = arguments(graphql_object, dir_defn, dir_node).keyword_arguments
703
+ args = arguments(graphql_object, dir_defn, dir_node)
563
704
  if !dir_defn.include?(graphql_object, args, context)
564
705
  return false
565
706
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.12.11"
3
+ VERSION = "1.12.12"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.12.11
4
+ version: 1.12.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-28 00:00:00.000000000 Z
11
+ date: 2021-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: benchmark-ips
@@ -150,6 +150,20 @@ dependencies:
150
150
  - - '='
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0.68'
153
+ - !ruby/object:Gem::Dependency
154
+ name: stackprof
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
153
167
  - !ruby/object:Gem::Dependency
154
168
  name: parser
155
169
  requirement: !ruby/object:Gem::Requirement