graphql 1.12.11 → 1.12.12

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.

Potentially problematic release.


This version of graphql might be problematic. Click here for more details.

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