graphql 2.6.10 → 2.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6c191887acba06a4030f962822e9106e9d3aa9d5550c8cd25ea76f343927f1b
4
- data.tar.gz: b984ad9a61a391b4b8a42ade3bb9c63dc79ab2b3ee987845d655dc7038df1255
3
+ metadata.gz: 24a0e699fe2f72dd869173b2bc6aa96ec8d40e738a293bc56b9176debca03861
4
+ data.tar.gz: 18b16bf0c467f5f8dc0c00ea2bcf2fbaf53283edc9f09a3789918523090a97ce
5
5
  SHA512:
6
- metadata.gz: 7deac13ce0bbc579609e28575a309caa46086196f1381cfc5fbd6a5770b2059e123cb047cc835c573ad76be1df264a3b0d52d8692dbf161ad0d0248f8cfed2a3
7
- data.tar.gz: 1362c0867ad8bbbe086b1999c710f447de6ea16b53b8a05a83dc39cba6c7b7d8d5cccdc388a3dff2f42c1b5135ff59a4c78dd2d53badd34b28718a73aa331b92
6
+ metadata.gz: 74a34839ced3a300d20ca370cb37a7c88c56345ecb056e9b9caa7a424fdb7573c5a014b80a32772cc2a5eb40d83e568c5d0875d93b029b1663dda9547fc06ba1
7
+ data.tar.gz: 65dcfea9802c9fb682e6595ef2d2d224a256c0650ff24fa57774eac420b27e594b3c432a104d76c8e033c659a608940661ea4503e0737f63d7839ff36f5f4405
@@ -254,6 +254,8 @@ module GraphQL
254
254
  task = Async::Task.new do |root_task|
255
255
  run.root_task = root_task
256
256
  root_task.graphql_async_dataloader_run = run
257
+ # Tasks inherit the spawning fiber's storage; give this run its own execution state.
258
+ Fiber[:__graphql_runtime_info] = nil
257
259
  set_fiber_variables(fiber_vars)
258
260
 
259
261
  while first_pass || run.running? || !jobs.empty?
@@ -262,11 +262,14 @@ module GraphQL
262
262
 
263
263
  def spawn_fiber
264
264
  fiber_vars = get_fiber_variables
265
- Fiber.new(blocking: !@nonblocking) {
265
+ Fiber.new(blocking: !@nonblocking) do
266
+ # Fibers inherit the spawning fiber's storage; give each one its own execution state.
267
+ Fiber[:__graphql_runtime_info] = nil
266
268
  set_fiber_variables(fiber_vars)
267
269
  yield
270
+ ensure
268
271
  cleanup_fiber
269
- }
272
+ end
270
273
  end
271
274
 
272
275
  # Pre-warm the Dataloader cache with ActiveRecord objects which were loaded elsewhere.
@@ -359,6 +362,7 @@ module GraphQL
359
362
  while job = @pending_jobs.shift
360
363
  job.call
361
364
  end
365
+ ensure
362
366
  trace&.dataloader_fiber_exit
363
367
  end
364
368
  end
@@ -392,6 +396,7 @@ module GraphQL
392
396
  source.run_pending_keys
393
397
  trace&.end_dataloader_source(source)
394
398
  end
399
+ ensure
395
400
  trace&.dataloader_fiber_exit
396
401
  end
397
402
  end
@@ -2,6 +2,10 @@
2
2
  module GraphQL
3
3
  module Execution
4
4
  class FieldResolveStep
5
+ # Only these built-in fields on Relay connection and edge types return already-scoped items.
6
+ # Custom fields on those types must still be authorized normally.
7
+ SCOPED_RELAY_FIELD_NAMES = [:node, :nodes, :edges].freeze
8
+
5
9
  def initialize(parent_type:, runner:, key:, selections_step:)
6
10
  @selections_step = selections_step
7
11
  @key = key
@@ -283,7 +287,7 @@ module GraphQL
283
287
  authorized_objects = objects
284
288
  end
285
289
 
286
- if @parent_type.default_relay? && authorized_objects.all? { |o| o.respond_to?(:was_authorized_by_scope_items?) && o.was_authorized_by_scope_items? }
290
+ if @parent_type.default_relay? && SCOPED_RELAY_FIELD_NAMES.include?(@field_definition.original_name) && authorized_objects.all? { |o| o.respond_to?(:was_authorized_by_scope_items?) && o.was_authorized_by_scope_items? }
287
291
  @was_scoped = true
288
292
  end
289
293
 
@@ -527,7 +531,7 @@ module GraphQL
527
531
  end
528
532
 
529
533
  inner_type = return_type.of_type
530
- result_a = Array.new(field_result.size)
534
+ result_a = field_result.is_a?(Array) ? Array.new(field_result.size) : []
531
535
  field_result.each_with_index do |item, idx|
532
536
  result_a[idx] = build_leaf_result(result_a, idx, item, inner_type, ctx, true)
533
537
  end
@@ -645,11 +649,18 @@ module GraphQL
645
649
  list_result = graphql_result[key] = []
646
650
  @directive_finalizers&.each { |f| @runner.add_finalizer(@selections_step.query, list_result, nil, f) }
647
651
  i = 0
648
- s = field_result.size
649
- while i < s
650
- inner_f_r = field_result[i]
651
- build_graphql_result(list_result, i, inner_f_r, inner_type, inner_type_nn, inner_type_l, true)
652
- i += 1
652
+ if field_result.is_a?(Array)
653
+ s = field_result.size
654
+ while i < s
655
+ inner_f_r = field_result[i]
656
+ build_graphql_result(list_result, i, inner_f_r, inner_type, inner_type_nn, inner_type_l, true)
657
+ i += 1
658
+ end
659
+ else
660
+ field_result.each do |inner_f_r|
661
+ build_graphql_result(list_result, i, inner_f_r, inner_type, inner_type_nn, inner_type_l, true)
662
+ i += 1
663
+ end
653
664
  end
654
665
  elsif @runner.resolves_lazies || (
655
666
  @static_type.kind.object? ?
@@ -619,7 +619,9 @@ module GraphQL
619
619
  when Array
620
620
  # It's an array full of execution errors; add them all.
621
621
  if !value.empty? && value.all?(GraphQL::ExecutionError)
622
- list_type_at_all = (field && (field.type.list?))
622
+ list_type = field && field.type
623
+ list_type = list_type.of_type if list_type && list_type.non_null?
624
+ list_type_at_all = list_type && list_type.list?
623
625
  if selection_result.nil? || !selection_result.graphql_dead
624
626
  value.each_with_index do |error, index|
625
627
  error.ast_node ||= ast_node
@@ -627,7 +629,7 @@ module GraphQL
627
629
  context.errors << error
628
630
  end
629
631
  if selection_result
630
- if list_type_at_all
632
+ if list_type_at_all && !list_type.of_type.non_null?
631
633
  result_without_errors = value.map { |v| v.is_a?(GraphQL::ExecutionError) ? nil : v }
632
634
  set_result(selection_result, result_name, result_without_errors, false, is_non_null)
633
635
  else
@@ -798,6 +800,8 @@ module GraphQL
798
800
 
799
801
  def resolve_list_item(inner_value, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, this_idx, response_list, owner_type, was_scoped, runtime_state) # rubocop:disable Metrics/ParameterLists
800
802
  runtime_state ||= get_current_runtime_state
803
+ runtime_state.current_field = field
804
+ runtime_state.current_arguments = arguments
801
805
  runtime_state.current_result_name = this_idx
802
806
  runtime_state.current_result = response_list
803
807
  call_method_on_directives(:resolve_each, owner_object, ast_node.directives) do
@@ -87,10 +87,10 @@ module GraphQL
87
87
  @schema.analysis_engine.analyze_multiplex(@multiplex, multiplex_analyzers)
88
88
  trace.end_analyze_multiplex(@multiplex, multiplex_analyzers)
89
89
 
90
- results = []
90
+ results = {}.compare_by_identity
91
91
  queries.each do |query|
92
92
  if query.validate && !query.valid?
93
- results << {
93
+ results[query] = {
94
94
  "errors" => query.static_errors.map(&:to_h)
95
95
  }
96
96
  next
@@ -125,8 +125,8 @@ module GraphQL
125
125
  end
126
126
  end
127
127
 
128
- queries.each_with_index.map do |query, idx|
129
- result = results[idx]
128
+ queries.map do |query|
129
+ result = results[query]
130
130
 
131
131
  fin_result = if (!@finalizers&.key?(query) && query.context.errors.empty?) || !query.valid?
132
132
  result
@@ -155,12 +155,16 @@ module GraphQL
155
155
  end
156
156
  query.result
157
157
  end
158
- end
159
- rescue SystemStackError => err
160
- @multiplex.queries.map do |query|
161
- @schema.query_stack_error(query, err)
162
- query.result_values ||= { "errors" => query.context.errors.map(&:to_h) }
163
- query.result
158
+ rescue SystemStackError => err
159
+ queries.map do |query|
160
+ @schema.query_stack_error(query, err)
161
+ query.result_values ||= { "errors" => query.context.errors.map(&:to_h) }
162
+ query.result
163
+ end
164
+ rescue Exception
165
+ # Assign values here so that the query's `@executed` becomes true
166
+ queries.map { |q| q.result_values ||= {} }
167
+ raise
164
168
  end
165
169
  ensure
166
170
  Fiber[:__graphql_current_multiplex] = previous_multiplex
@@ -262,12 +266,12 @@ module GraphQL
262
266
  end
263
267
 
264
268
  if !auth_check
265
- results << {}
269
+ results[query] = {}
266
270
  return
267
271
  end
268
272
  end
269
273
 
270
- results << { "data" => data }
274
+ results[query] = { "data" => data }
271
275
  objects = [root_value]
272
276
  query.current_trace.objects(root_type, objects, query.context)
273
277
 
@@ -361,7 +365,7 @@ module GraphQL
361
365
  objects = [root_value]
362
366
  query.current_trace.objects(resolved_type, objects, query.context)
363
367
  runtime_type_at[data] = resolved_type
364
- results << { "data" => data }
368
+ results[query] = { "data" => data }
365
369
  isolated_steps[0] << SelectionsStep.new(
366
370
  parent_type: resolved_type,
367
371
  field_resolve_step: nil,
@@ -376,10 +380,10 @@ module GraphQL
376
380
  inner_type = root_type.unwrap
377
381
  case inner_type.kind.name
378
382
  when "SCALAR", "ENUM"
379
- results << run_isolated_scalar(root_type, query)
383
+ results[query] = run_isolated_scalar(root_type, query)
380
384
  else
381
385
  list_result = Array.new(root_value.size) { Hash.new.compare_by_identity }
382
- results << { "data" => list_result }
386
+ results[query] = { "data" => list_result }
383
387
  isolated_steps[0] << SelectionsStep.new(
384
388
  parent_type: inner_type,
385
389
  field_resolve_step: nil,
@@ -392,7 +396,7 @@ module GraphQL
392
396
  )
393
397
  end
394
398
  when "SCALAR", "ENUM"
395
- results << run_isolated_scalar(root_type, query)
399
+ results[query] = run_isolated_scalar(root_type, query)
396
400
  else
397
401
  raise "Unhandled root type kind: #{root_type.kind.name.inspect}"
398
402
  end
@@ -425,7 +429,7 @@ module GraphQL
425
429
  key = dummy_path.pop
426
430
  is_from_array = key.is_a?(Integer)
427
431
 
428
- if lazy?(value)
432
+ if resolves_lazies && lazy?(value)
429
433
  value = @schema.sync_lazy(value)
430
434
  end
431
435
  selections = partial.ast_nodes
@@ -65,24 +65,25 @@ module GraphQL
65
65
  end
66
66
 
67
67
  def column_at(pos)
68
- next_line_idx = lines_at.bsearch_index { |l| l >= pos } || 0
69
- if next_line_idx > 0
70
- line_pos = @lines_at[next_line_idx - 1]
71
- pos - line_pos
68
+ next_line_idx = lines_at.bsearch_index { |l| l >= pos } || @lines_at.size
69
+ line_start = next_line_idx == 0 ? 0 : @lines_at[next_line_idx - 1] + 1
70
+ if @graphql_str.ascii_only?
71
+ pos - line_start + 1
72
72
  else
73
- pos + 1
73
+ @graphql_str.byteslice(line_start, pos - line_start).force_encoding(Encoding::UTF_8).length + 1
74
74
  end
75
75
  end
76
76
 
77
77
  private
78
78
 
79
- # @return [Array<Integer>] Positions of each line break in the original string
79
+ # @return [Array<Integer>] Byte offsets of each line break in the original string
80
80
  def lines_at
81
81
  @lines_at ||= begin
82
+ graphql_str = @graphql_str.b
82
83
  la = []
83
84
  idx = 0
84
85
  while idx
85
- idx = @graphql_str.index("\n", idx)
86
+ idx = graphql_str.index("\n", idx)
86
87
  if idx
87
88
  la << idx
88
89
  idx += 1
@@ -27,7 +27,11 @@ module GraphQL
27
27
  private
28
28
 
29
29
  def index_from_cursor(cursor)
30
- decode(cursor).to_i
30
+ index = Integer(decode(cursor), 10, exception: false)
31
+ if index.nil? || index <= 0
32
+ raise GraphQL::ExecutionError, "Invalid cursor: #{cursor.inspect}"
33
+ end
34
+ [index, items.length + 1].min
31
35
  end
32
36
 
33
37
  # Populate all the pagination info _once_,
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require "graphql/pagination/connection"
3
+ require "monitor"
3
4
 
4
5
  module GraphQL
5
6
  module Pagination
@@ -87,7 +88,11 @@ module GraphQL
87
88
 
88
89
  # @return [Integer]
89
90
  def offset_from_cursor(cursor)
90
- decode(cursor).to_i
91
+ offset = Integer(decode(cursor), 10, exception: false)
92
+ if offset.nil? || offset <= 0
93
+ raise GraphQL::ExecutionError, "Invalid cursor: #{cursor.inspect}"
94
+ end
95
+ offset
91
96
  end
92
97
 
93
98
  # Abstract this operation so we can always ignore inputs less than zero.
@@ -178,6 +183,17 @@ module GraphQL
178
183
  # Apply `first` and `last` to `sliced_nodes`,
179
184
  # returning a new relation
180
185
  def limited_nodes
186
+ return @limited_nodes if @limited_nodes
187
+ if async_dataloader?
188
+ (@load_monitor ||= Monitor.new).synchronize do
189
+ build_limited_nodes
190
+ end
191
+ else
192
+ build_limited_nodes
193
+ end
194
+ end
195
+
196
+ def build_limited_nodes
181
197
  @limited_nodes ||= begin
182
198
  calculate_sliced_nodes_parameters
183
199
  if @sliced_nodes_null_relation
@@ -217,15 +233,19 @@ module GraphQL
217
233
  end
218
234
  end
219
235
 
236
+ def async_dataloader?
237
+ @context&.dataloader.is_a?(GraphQL::Dataloader::AsyncDataloader)
238
+ end
239
+
220
240
  # Load nodes after applying first/last/before/after,
221
241
  # returns an array of nodes
222
242
  def load_nodes
223
243
  # Return an array so we can consistently use `.index(node)` on it
224
244
  return @nodes if @nodes
225
- if (@context[:dataloader].is_a?(GraphQL::Dataloader::AsyncDataloader))
245
+ if async_dataloader?
226
246
  # `AsyncDataloader` may resolve sibling fields (eg, `edges` and `pageInfo`)
227
247
  # in separate Fibers, so several callers can get here before `@nodes` is set.
228
- (@load_lock ||= Mutex.new).synchronize do
248
+ (@load_monitor ||= Monitor.new).synchronize do
229
249
  @nodes ||= limited_nodes.to_a
230
250
  end
231
251
  else
@@ -35,7 +35,11 @@ module GraphQL
35
35
  value_was_provided = @provided_variables.key?(variable_name)
36
36
  max_errors = schema.validate_max_errors - @errors.count if schema.validate_max_errors
37
37
  begin
38
- validation_result = variable_type.validate_input(provided_value, ctx, max_errors: max_errors)
38
+ validation_result = if value_was_provided || default_value.nil?
39
+ variable_type.validate_input(provided_value, ctx, max_errors: max_errors)
40
+ else
41
+ GraphQL::Query::InputValidationResult::VALID
42
+ end
39
43
  if validation_result.valid?
40
44
  if value_was_provided
41
45
  # Add the variable if a value was provided
data/lib/graphql/query.rb CHANGED
@@ -287,7 +287,8 @@ module GraphQL
287
287
  # @return [GraphQL::Query::Result] A Hash-like GraphQL response, with `"data"` and/or `"errors"` keys
288
288
  def result
289
289
  if !@executed
290
- Execution::Interpreter.run_all(@schema, [self], context: @context)
290
+ execution_engine = @schema.default_execution_next ? Execution::Next : Execution::Interpreter
291
+ execution_engine.run_all(@schema, [self], context: @context)
291
292
  end
292
293
  @result ||= Query::Result.new(query: self, values: @result_values)
293
294
  end
@@ -84,6 +84,11 @@ module GraphQL
84
84
  yield
85
85
  end
86
86
 
87
+ def resolve_field(...); end
88
+ def resolve_fragment_spread(...); end
89
+ def resolve_inline_fragment(...); end
90
+ def resolve_operation(...); end
91
+
87
92
  def validate!(arguments, context)
88
93
  Schema::Validator.validate!(validators, self, context, arguments)
89
94
  end
@@ -397,7 +397,7 @@ module GraphQL
397
397
  # This union/interface is used in `loads:` but not otherwise visible to this query
398
398
  context.types.loadable_possible_types(arg_loads_type, context).include?(application_object_type)
399
399
  else
400
- true
400
+ arg_loads_type == application_object_type
401
401
  end
402
402
  else
403
403
  context.types.possible_types(arg_loads_type).include?(application_object_type)
@@ -33,30 +33,49 @@ module GraphQL
33
33
 
34
34
  def freeze
35
35
  @cached_visible.default_proc = nil
36
+ @cached_visible.freeze
36
37
  @cached_visible_fields.default_proc = nil
37
38
  @cached_visible_fields.each do |type, fields|
38
39
  fields.default_proc = nil
40
+ fields.freeze
39
41
  end
42
+ @cached_visible_fields.freeze
40
43
  @cached_visible_arguments.default_proc = nil
41
44
  @cached_visible_arguments.each do |type, fields|
42
45
  fields.default_proc = nil
46
+ fields.freeze
43
47
  end
48
+ @cached_visible_arguments.freeze
44
49
  @cached_parent_fields.default_proc = nil
45
50
  @cached_parent_fields.each do |type, fields|
46
51
  fields.default_proc = nil
52
+ fields.freeze
47
53
  end
54
+ @cached_parent_fields.freeze
48
55
  @cached_parent_arguments.default_proc = nil
49
56
  @cached_parent_arguments.each do |type, args|
50
57
  args.default_proc = nil
58
+ args.freeze
51
59
  end
60
+ @cached_parent_arguments.freeze
52
61
  @cached_possible_types.default_proc = nil
62
+ @cached_possible_types.freeze
53
63
  @cached_enum_values.default_proc = nil
64
+ @cached_enum_values.freeze
54
65
  @cached_fields.default_proc = nil
66
+ @cached_fields.freeze
55
67
  @cached_arguments.default_proc = nil
68
+ @cached_arguments.freeze
56
69
  @loadable_possible_types.default_proc = nil
70
+ @loadable_possible_types.freeze
57
71
  @cached_field_result.default_proc = nil
58
- @cached_field_result.each { |_, h| h.default_proc = nil }
72
+ @cached_field_result.each do |_, h|
73
+ h.default_proc = nil
74
+ h.freeze
75
+ end
76
+ @cached_field_result.freeze
59
77
  @cached_type_result.default_proc = nil
78
+ @cached_type_result.freeze
60
79
  super
61
80
  end
62
81
 
@@ -277,18 +296,31 @@ module GraphQL
277
296
  arguments(f).each do |arg|
278
297
  argument(f, arg.graphql_name)
279
298
  end
299
+
300
+ resolver_class = f.resolver
301
+ if resolver_class
302
+ arguments(resolver_class).each do |arg|
303
+ argument(resolver_class, arg.graphql_name)
304
+ end
305
+ end
280
306
  end
281
307
  @schema.introspection_system.dynamic_fields.each do |f|
282
308
  field(type_defn, f.graphql_name)
283
309
  end
310
+ interfaces(type_defn)
284
311
  elsif type_defn.kind.input_object?
285
312
  arguments(type_defn).each do |arg|
286
313
  argument(type_defn, arg.graphql_name)
287
314
  end
288
315
  elsif type_defn.kind.enum?
289
316
  enum_values(type_defn)
317
+ elsif type_defn.kind.union?
318
+ loadable_possible_types(type_defn, @context)
319
+ @schema.introspection_system.dynamic_fields.each do |f|
320
+ field(type_defn, f.graphql_name)
321
+ end
290
322
  end
291
- # Lots more to do here
323
+ possible_types(type_defn)
292
324
  end
293
325
  if @schema.query
294
326
  @schema.introspection_system.entry_points.each do |f|
@@ -304,6 +336,25 @@ module GraphQL
304
336
  end
305
337
  end
306
338
 
339
+ directives.each do |directive|
340
+ arguments(directive).each do |directive_argument|
341
+ argument(directive, directive_argument.graphql_name)
342
+ end
343
+ end
344
+
345
+ @cached_arguments.each do |owner, args|
346
+ @cached_parent_arguments[owner]
347
+
348
+ args.each do |arg|
349
+ if (loads_type = arg.loads)
350
+ loadable?(loads_type, @context)
351
+ possible_types(loads_type)
352
+ if loads_type.kind.union?
353
+ loadable_possible_types(loads_type, @context)
354
+ end
355
+ end
356
+ end
357
+ end
307
358
  end
308
359
 
309
360
  private
@@ -16,6 +16,7 @@ module GraphQL
16
16
  @schema = context.schema
17
17
  @inline_fragment_paths = {}
18
18
  @field_unwrapped_types = {}.compare_by_identity
19
+ @child_positions = {}.compare_by_identity
19
20
  super(document)
20
21
  end
21
22
 
@@ -159,6 +160,7 @@ module GraphQL
159
160
  @current_argument_definition = @parent_argument_definition
160
161
  @parent_argument_definition = prev_parent
161
162
  @path_depth -= 1
163
+ @child_positions.delete(node)
162
164
  end
163
165
 
164
166
  def on_fragment_spread(node, parent)
@@ -171,7 +173,7 @@ module GraphQL
171
173
  def on_input_object(node, parent)
172
174
  arg_defn = @current_argument_definition
173
175
  if arg_defn && arg_defn.type.list?
174
- @path[@path_depth] = parent.children.index(node)
176
+ @path[@path_depth] = child_positions(parent)[node]
175
177
  @path_depth += 1
176
178
  super
177
179
  @path_depth -= 1
@@ -180,6 +182,14 @@ module GraphQL
180
182
  end
181
183
  end
182
184
 
185
+ def child_positions(parent)
186
+ @child_positions[parent] ||= begin
187
+ positions = {}.compare_by_identity
188
+ parent.children.each_with_index { |child, index| positions[child] ||= index }
189
+ positions
190
+ end
191
+ end
192
+
183
193
  # @return [GraphQL::BaseType] The current object type
184
194
  def type_definition
185
195
  @current_object_type
@@ -9,18 +9,53 @@ module GraphQL
9
9
  'Schema is not configured for subscriptions',
10
10
  nodes: node
11
11
  ))
12
- elsif node.selections.size != 1
13
- add_error(GraphQL::StaticValidation::NotSingleSubscriptionError.new(
14
- 'A subscription operation may only have one selection',
15
- nodes: node,
16
- ))
17
12
  else
13
+ root_fields = subscription_root_fields(node)
14
+ if root_fields.map { |field| field.alias || field.name }.uniq.size != 1
15
+ add_error(GraphQL::StaticValidation::NotSingleSubscriptionError.new(
16
+ 'A subscription operation may only have one selection',
17
+ nodes: node,
18
+ ))
19
+ elsif root_fields.any? { |field| field.name.start_with?("__") }
20
+ add_error(GraphQL::StaticValidation::NotSingleSubscriptionError.new(
21
+ 'A subscription operation may not select an introspection field',
22
+ nodes: node,
23
+ ))
24
+ end
18
25
  super
19
26
  end
20
27
  else
21
28
  super
22
29
  end
23
30
  end
31
+
32
+ private
33
+
34
+ def subscription_root_fields(node)
35
+ root_type = context.types.subscription_root
36
+ fields = []
37
+ selections = node.selections.dup
38
+ visited_fragments = {}
39
+
40
+ while (selection = selections.pop)
41
+ case selection
42
+ when GraphQL::Language::Nodes::Field
43
+ fields << selection
44
+ when GraphQL::Language::Nodes::FragmentSpread
45
+ next if visited_fragments[selection.name]
46
+ visited_fragments[selection.name] = true
47
+ fragment = context.fragments[selection.name]
48
+ selections << fragment if fragment
49
+ when GraphQL::Language::Nodes::InlineFragment, GraphQL::Language::Nodes::FragmentDefinition
50
+ fragment_type = selection.type ? @types.type(selection.type.name) : root_type
51
+ if fragment_type && fragment_type.kind.composite? && @types.possible_types(fragment_type).include?(root_type)
52
+ selections.concat(selection.selections)
53
+ end
54
+ end
55
+ end
56
+
57
+ fields
58
+ end
24
59
  end
25
60
  end
26
61
  end
@@ -13,6 +13,8 @@ module GraphQL
13
13
  TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S.%N%z" # eg '2020-01-01 23:59:59.123456789+05:00'
14
14
  TIMESTAMP_CLASS_NAMES = ["Date", "DateTime", "Time"].freeze
15
15
  OPEN_STRUCT_KEY = "__ostruct__"
16
+ HASH_KEY = "__graphql_hash__"
17
+ RESERVED_KEYS = [GLOBALID_KEY, SYMBOL_KEY, SYMBOL_KEYS_KEY, TIMESTAMP_KEY, OPEN_STRUCT_KEY, HASH_KEY].freeze
16
18
 
17
19
  module_function
18
20
 
@@ -57,7 +59,7 @@ module GraphQL
57
59
  # @return [Object] An object that load Global::Identification recursive
58
60
  def load_value(value)
59
61
  if value.is_a?(Array)
60
- is_gids = (v1 = value[0]).is_a?(Hash) && v1.size == 1 && v1[GLOBALID_KEY]
62
+ is_gids = !value.empty? && value.all? { |v| v.is_a?(Hash) && v.size == 1 && v[GLOBALID_KEY] }
61
63
  if is_gids
62
64
  # Assume it's an array of global IDs
63
65
  ids = value.map { |v| v[GLOBALID_KEY] }
@@ -91,6 +93,10 @@ module GraphQL
91
93
  when OPEN_STRUCT_KEY
92
94
  ostruct_values = load_value(value[OPEN_STRUCT_KEY])
93
95
  OpenStruct.new(ostruct_values)
96
+ when HASH_KEY
97
+ value[HASH_KEY].each_with_object({}) do |(k, v), loaded_h|
98
+ loaded_h[load_value(k)] = load_value(v)
99
+ end
94
100
  else
95
101
  key = value.keys.first
96
102
  { key => load_value(value[key]) }
@@ -98,15 +104,21 @@ module GraphQL
98
104
  else
99
105
  loaded_h = {}
100
106
  sym_keys = value.fetch(SYMBOL_KEYS_KEY, [])
107
+ symbol_key_values = sym_keys.is_a?(Hash) ? sym_keys : nil
101
108
  value.each do |k, v|
102
109
  if k == SYMBOL_KEYS_KEY
103
110
  next
104
111
  end
105
- if sym_keys.include?(k)
112
+ if !symbol_key_values && sym_keys.include?(k)
106
113
  k = k.to_sym
107
114
  end
108
115
  loaded_h[k] = load_value(v)
109
116
  end
117
+ if symbol_key_values
118
+ symbol_key_values.each do |k, v|
119
+ loaded_h[k.to_sym] = load_value(v)
120
+ end
121
+ end
110
122
  loaded_h
111
123
  end
112
124
  else
@@ -120,16 +132,31 @@ module GraphQL
120
132
  if obj.is_a?(Array)
121
133
  obj.map{|item| dump_value(item)}
122
134
  elsif obj.is_a?(Hash)
135
+ has_colliding_symbol_key = obj.any? { |k, _v| k.is_a?(Symbol) && obj.key?(k.to_s) }
136
+ if obj.any? { |k, _v| RESERVED_KEYS.include?(k.to_s) }
137
+ return {
138
+ HASH_KEY => obj.map { |k, v| [dump_value(k.is_a?(Symbol) ? k : k.to_s), dump_value(v)] },
139
+ }
140
+ end
123
141
  symbol_keys = nil
142
+ symbol_key_values = nil
124
143
  dumped_h = {}
125
144
  obj.each do |k, v|
126
- dumped_h[k.to_s] = dump_value(v)
127
- if k.is_a?(Symbol)
145
+ dumped_v = dump_value(v)
146
+ if has_colliding_symbol_key && k.is_a?(Symbol)
147
+ symbol_key_values ||= {}
148
+ symbol_key_values[k.to_s] = dumped_v
149
+ else
150
+ dumped_h[k.to_s] = dumped_v
151
+ end
152
+ if !has_colliding_symbol_key && k.is_a?(Symbol)
128
153
  symbol_keys ||= Set.new
129
154
  symbol_keys << k.to_s
130
155
  end
131
156
  end
132
- if symbol_keys
157
+ if symbol_key_values
158
+ dumped_h[SYMBOL_KEYS_KEY] = symbol_key_values
159
+ elsif symbol_keys
133
160
  dumped_h[SYMBOL_KEYS_KEY] = symbol_keys.to_a
134
161
  end
135
162
  dumped_h
@@ -37,13 +37,18 @@ module GraphQL
37
37
  scope.set_transaction_name(transaction_name(query))
38
38
  end
39
39
  end
40
- span.set_data('graphql.document', query.query_string)
40
+ if graphql_data_collection.nil? || graphql_data_collection.document
41
+ span.set_data('graphql.document', query.query_string)
42
+ end
41
43
  if query.selected_operation_name
42
44
  span.set_data('graphql.operation.name', query.selected_operation_name)
43
45
  end
44
46
  if query.selected_operation
45
47
  span.set_data('graphql.operation.type', query.selected_operation.operation_type)
46
48
  end
49
+ if graphql_data_collection&.variables && !query.provided_variables.empty?
50
+ span.set_data('graphql.variables', query.provided_variables)
51
+ end
47
52
  end
48
53
  end
49
54
 
@@ -55,6 +60,13 @@ module GraphQL
55
60
 
56
61
  private
57
62
 
63
+ def graphql_data_collection
64
+ @graphql_data_collection ||= if Sentry.respond_to?(:configuration)
65
+ configuration = Sentry.configuration
66
+ configuration.data_collection.graphql if configuration.respond_to?(:data_collection)
67
+ end
68
+ end
69
+
58
70
  def operation_name(query)
59
71
  selected_op = query.selected_operation
60
72
  if selected_op
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "2.6.10"
3
+ VERSION = "2.6.12"
4
4
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.10
4
+ version: 2.6.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-08-27 00:00:00.000000000 Z
10
+ date: 2026-10-14 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64