graphql 2.6.9 → 2.6.11

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: 6a359d53729477046ffd6b51ef9345099defcdc95521579711fffe5ab79c0b5e
4
- data.tar.gz: 4c88aecd90fb26e02198f21f4bdf7a73f577ec37ac9ea9eb6c840c8a2957fc7a
3
+ metadata.gz: f449cab829c8314cf4cd1b51e225e70603f5dc16dfcb221d3204aa39bd9fe2cd
4
+ data.tar.gz: 6fb74ee40392f0663b728c76e72239596fc82271d2222e2d9170d8ad06579f6c
5
5
  SHA512:
6
- metadata.gz: d40bb56d8b5e77ca6f126f1528e2a074bbf679996dfb8ea5a12d646d028d547848dc36dea3a5fb73273fb7252135a5c7e904ec7d0a38790fe05164c5e81701b5
7
- data.tar.gz: e486b9492693cdc69375f8d3bc662e075a71ff45642f62cb2fd741959b2a2d1f9105fa14718b992ef2d3b3a809fd0f0d94ec66abf509137b3a7af05877527c23
6
+ metadata.gz: 2820887a516c9a015774633ac126924642ceb778b523d0563af9ba766f6a3bfa5fa89ad0420c9ea3ee5208fe8fcdd58101aff1777b44de98c8c12b01fd64be0d
7
+ data.tar.gz: 90856cb6b43ac1be362d3358820857c1c8cc038f19def34a119677a6924c7b1441eca628c9e26a2134ae52aaab7507cea3521e36cad2763614909c10d8a79577
@@ -36,10 +36,14 @@ module GraphQL
36
36
  run = task.graphql_async_dataloader_run
37
37
  trace = run.trace
38
38
  trace&.dataloader_fiber_yield(source)
39
- run.tasks_channel.push([:paused_task, task])
39
+ if !run.push_task_message(:paused_task, task)
40
+ task.stop
41
+ end
40
42
  condition = task.graphql_async_dataloader_condition
41
43
  condition.wait
42
- run.tasks_channel.push([:resumed_task, task])
44
+ if !run.push_task_message(:resumed_task, task)
45
+ task.stop
46
+ end
43
47
  trace&.dataloader_fiber_resume(source)
44
48
  nil
45
49
  end
@@ -69,7 +73,7 @@ module GraphQL
69
73
 
70
74
  attr_accessor :trace, :root_task
71
75
 
72
- attr_reader :jobs, :lazies_at_depth, :jobs_fiber_limit, :snoozed_jobs_condition, :snoozed_sources_condition, :tasks_channel
76
+ attr_reader :dataloader, :jobs, :lazies_at_depth, :jobs_fiber_limit, :snoozed_jobs_condition, :snoozed_sources_condition
73
77
 
74
78
  def jobs_bandwidth?
75
79
  running_count < @jobs_fiber_limit
@@ -84,6 +88,20 @@ module GraphQL
84
88
  @tasks_channel_task.cancel
85
89
  end
86
90
 
91
+ # Push to the tasks_channel, tolerating a closed channel: on the error path, `run_queue`
92
+ # closes the channel while sibling tasks can still run one more slice before
93
+ # `root_task.cancel` reaches them. Record `:task_error` payloads so they aren't lost, and
94
+ # return false so the caller can stop the task instead of raising `ClosedError` into user code.
95
+ def push_task_message(msg, data)
96
+ @tasks_channel.push([msg, data])
97
+ true
98
+ rescue Async::Queue::ClosedError
99
+ if msg == :task_error
100
+ @task_error ||= data
101
+ end
102
+ false
103
+ end
104
+
87
105
  def wait_for_activity
88
106
  @activity.wait
89
107
  end
@@ -175,11 +193,19 @@ module GraphQL
175
193
  end
176
194
 
177
195
  def active_run
178
- @pending_run || Async::Task.current?&.graphql_async_dataloader_run || raise(GraphQL::Error, "No available Run to append to, GraphQL-Ruby bug")
196
+ @pending_run || current_task_run || raise(GraphQL::Error, "No available Run to append to, GraphQL-Ruby bug")
197
+ end
198
+
199
+ # The current task's run, but only if it belongs to this dataloader. A different
200
+ # dataloader may be running inside one of our tasks (or vice versa), e.g. a query
201
+ # executed from a resolver or a subscription trigger; its run must not be reused.
202
+ def current_task_run
203
+ run = Async::Task.current?&.graphql_async_dataloader_run
204
+ run if run&.dataloader.equal?(self)
179
205
  end
180
206
 
181
207
  def run_isolated
182
- previous_run = Async::Task.current?&.graphql_async_dataloader_run
208
+ previous_run = current_task_run
183
209
  prev_pending_keys = {}
184
210
  # Clear pending loads but keep already-cached records
185
211
  # in case they are useful to the given block.
@@ -215,7 +241,7 @@ module GraphQL
215
241
 
216
242
  def run(trace_query_lazy: nil)
217
243
  trace = Fiber[:__graphql_current_multiplex]&.current_trace
218
- run = @pending_run || Async::Task.current?&.graphql_async_dataloader_run || raise(GraphQL::Error, "No available Run, GraphQL-Ruby internal bug")
244
+ run = @pending_run || current_task_run || raise(GraphQL::Error, "No available Run, GraphQL-Ruby internal bug")
219
245
  @pending_run = nil
220
246
  run.trace = trace
221
247
  first_pass = true
@@ -336,14 +362,14 @@ module GraphQL
336
362
  end
337
363
  nil
338
364
  rescue StandardError => err
339
- run.tasks_channel.push([:task_error, err])
365
+ run.push_task_message(:task_error, err)
340
366
  else
341
- run.tasks_channel.push([:finished_task, task])
367
+ run.push_task_message(:finished_task, task)
342
368
  ensure
343
369
  cleanup_fiber
344
370
  trace&.dataloader_fiber_exit
345
371
  end
346
- run.tasks_channel.push([:started_task, new_task])
372
+ run.push_task_message(:started_task, new_task)
347
373
  end
348
374
  end
349
375
  end
@@ -262,11 +262,12 @@ 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
266
  set_fiber_variables(fiber_vars)
267
267
  yield
268
+ ensure
268
269
  cleanup_fiber
269
- }
270
+ end
270
271
  end
271
272
 
272
273
  # Pre-warm the Dataloader cache with ActiveRecord objects which were loaded elsewhere.
@@ -359,6 +360,7 @@ module GraphQL
359
360
  while job = @pending_jobs.shift
360
361
  job.call
361
362
  end
363
+ ensure
362
364
  trace&.dataloader_fiber_exit
363
365
  end
364
366
  end
@@ -392,6 +394,7 @@ module GraphQL
392
394
  source.run_pending_keys
393
395
  trace&.end_dataloader_source(source)
394
396
  end
397
+ ensure
395
398
  trace&.dataloader_fiber_exit
396
399
  end
397
400
  end
@@ -744,7 +744,8 @@ module GraphQL
744
744
  when :dig
745
745
  objects.map { |o| o.dig(*@field_definition.execution_mode_key) }
746
746
  when :dataload
747
- if (k = @field_definition.execution_mode_key).is_a?(Class)
747
+ k = @field_definition.execution_mode_key
748
+ results = if k.is_a?(Class)
748
749
  context.dataload_all(k, objects)
749
750
  elsif (source_class = k[:with])
750
751
  if (batch_args = k[:by])
@@ -764,6 +765,12 @@ module GraphQL
764
765
  else
765
766
  raise ArgumentError, "Unexpected `dataload: ...` configuration: #{k.inspect}"
766
767
  end
768
+ method = k.is_a?(Hash) ? k[:method] : nil
769
+ if method
770
+ results.map { |r| r&.public_send(method) }
771
+ else
772
+ results
773
+ end
767
774
  when :resolver_class
768
775
  results = Array.new(objects.size, nil)
769
776
  ps = @pending_steps ||= []
@@ -22,6 +22,7 @@ module GraphQL
22
22
  # @param max_complexity [Integer, nil]
23
23
  # @return [Array<GraphQL::Query::Result>] One result per query
24
24
  def run_all(schema, query_options, context: {}, max_complexity: schema.max_complexity)
25
+ previous_multiplex = Fiber[:__graphql_current_multiplex]
25
26
  queries = query_options.map do |opts|
26
27
  query = case opts
27
28
  when Hash
@@ -131,7 +132,6 @@ module GraphQL
131
132
  queries.map { |q| q.result_values ||= {} }
132
133
  raise
133
134
  ensure
134
- Fiber[:__graphql_current_multiplex] = nil
135
135
  queries.map { |query|
136
136
  runtime = query.context.namespace(:interpreter_runtime)[:runtime]
137
137
  if runtime
@@ -140,6 +140,8 @@ module GraphQL
140
140
  }
141
141
  end
142
142
  end
143
+ ensure
144
+ Fiber[:__graphql_current_multiplex] = previous_multiplex
143
145
  end
144
146
  end
145
147
 
@@ -72,6 +72,7 @@ module GraphQL
72
72
  end
73
73
 
74
74
  def execute
75
+ previous_multiplex = Fiber[:__graphql_current_multiplex]
75
76
  Fiber[:__graphql_current_multiplex] = @multiplex
76
77
  isolated_steps = [[]]
77
78
  trace = @multiplex.current_trace
@@ -86,10 +87,10 @@ module GraphQL
86
87
  @schema.analysis_engine.analyze_multiplex(@multiplex, multiplex_analyzers)
87
88
  trace.end_analyze_multiplex(@multiplex, multiplex_analyzers)
88
89
 
89
- results = []
90
+ results = {}.compare_by_identity
90
91
  queries.each do |query|
91
92
  if query.validate && !query.valid?
92
- results << {
93
+ results[query] = {
93
94
  "errors" => query.static_errors.map(&:to_h)
94
95
  }
95
96
  next
@@ -124,8 +125,8 @@ module GraphQL
124
125
  end
125
126
  end
126
127
 
127
- queries.each_with_index.map do |query, idx|
128
- result = results[idx]
128
+ queries.map do |query|
129
+ result = results[query]
129
130
 
130
131
  fin_result = if (!@finalizers&.key?(query) && query.context.errors.empty?) || !query.valid?
131
132
  result
@@ -154,9 +155,19 @@ module GraphQL
154
155
  end
155
156
  query.result
156
157
  end
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
157
168
  end
158
169
  ensure
159
- Fiber[:__graphql_current_multiplex] = nil
170
+ Fiber[:__graphql_current_multiplex] = previous_multiplex
160
171
  end
161
172
 
162
173
  def gather_selections(type_defn, ast_selections, selections_step, query, all_selections, prototype_result, into:)
@@ -255,12 +266,12 @@ module GraphQL
255
266
  end
256
267
 
257
268
  if !auth_check
258
- results << {}
269
+ results[query] = {}
259
270
  return
260
271
  end
261
272
  end
262
273
 
263
- results << { "data" => data }
274
+ results[query] = { "data" => data }
264
275
  objects = [root_value]
265
276
  query.current_trace.objects(root_type, objects, query.context)
266
277
 
@@ -354,7 +365,7 @@ module GraphQL
354
365
  objects = [root_value]
355
366
  query.current_trace.objects(resolved_type, objects, query.context)
356
367
  runtime_type_at[data] = resolved_type
357
- results << { "data" => data }
368
+ results[query] = { "data" => data }
358
369
  isolated_steps[0] << SelectionsStep.new(
359
370
  parent_type: resolved_type,
360
371
  field_resolve_step: nil,
@@ -369,10 +380,10 @@ module GraphQL
369
380
  inner_type = root_type.unwrap
370
381
  case inner_type.kind.name
371
382
  when "SCALAR", "ENUM"
372
- results << run_isolated_scalar(root_type, query)
383
+ results[query] = run_isolated_scalar(root_type, query)
373
384
  else
374
385
  list_result = Array.new(root_value.size) { Hash.new.compare_by_identity }
375
- results << { "data" => list_result }
386
+ results[query] = { "data" => list_result }
376
387
  isolated_steps[0] << SelectionsStep.new(
377
388
  parent_type: inner_type,
378
389
  field_resolve_step: nil,
@@ -385,7 +396,7 @@ module GraphQL
385
396
  )
386
397
  end
387
398
  when "SCALAR", "ENUM"
388
- results << run_isolated_scalar(root_type, query)
399
+ results[query] = run_isolated_scalar(root_type, query)
389
400
  else
390
401
  raise "Unhandled root type kind: #{root_type.kind.name.inspect}"
391
402
  end
@@ -418,7 +429,7 @@ module GraphQL
418
429
  key = dummy_path.pop
419
430
  is_from_array = key.is_a?(Integer)
420
431
 
421
- if lazy?(value)
432
+ if resolves_lazies && lazy?(value)
422
433
  value = @schema.sync_lazy(value)
423
434
  end
424
435
  selections = partial.ast_nodes
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ # This error is raised when `Types::Float` is given a non-finite input value.
4
+ class FloatDecodingError < GraphQL::RuntimeTypeError
5
+ # The value which couldn't be decoded
6
+ attr_reader :float_value
7
+
8
+ def initialize(value)
9
+ @float_value = value
10
+ super("Float is not finite: #{value.inspect}.")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ # This error is raised when `Types::Float` is asked to return a non-finite value.
4
+ class FloatEncodingError < GraphQL::RuntimeTypeError
5
+ # The value which couldn't be encoded
6
+ attr_reader :float_value
7
+
8
+ # @return [GraphQL::Schema::Field] The field that returned a non-finite float
9
+ attr_reader :field
10
+
11
+ # @return [Array<String, Integer>] Where the field appeared in the GraphQL response
12
+ attr_reader :path
13
+
14
+ def initialize(value, context:)
15
+ @float_value = value
16
+ @field = context[:current_field]
17
+ @path = context[:current_path]
18
+ message = "Float is not finite: #{value.inspect}".dup
19
+ if @path
20
+ message << " @ #{@path.join(".")}"
21
+ end
22
+ if @field
23
+ message << " (#{@field.path})"
24
+ end
25
+ super("#{message}.")
26
+ end
27
+ end
28
+ end
@@ -9,6 +9,9 @@ module GraphQL
9
9
  end
10
10
  @string = graphql_str
11
11
  @filename = filename
12
+ if !@string.valid_encoding?
13
+ raise_parse_error("Parse error on bad Unicode escape sequence", nil, nil)
14
+ end
12
15
  @scanner = StringScanner.new(graphql_str)
13
16
  @pos = nil
14
17
  @max_tokens = max_tokens || Float::INFINITY
@@ -110,10 +113,6 @@ module GraphQL
110
113
  @scanner.pos += 1
111
114
  :UNKNOWN_CHAR
112
115
  end
113
- rescue ArgumentError => err
114
- if err.message == "invalid byte sequence in UTF-8"
115
- raise_parse_error("Parse error on bad Unicode escape sequence", nil, nil)
116
- end
117
116
  end
118
117
 
119
118
  def token_value
@@ -147,7 +146,7 @@ module GraphQL
147
146
  "\\r" => "\r",
148
147
  "\\t" => "\t",
149
148
  }
150
- UTF_8 = /\\u(?:([\dAa-f]{4})|\{([\da-f]{4,})\})(?:\\u([\dAa-f]{4}))?/i
149
+ UTF_8 = /\\u(?:([\da-f]{4})|\{([\da-f]+)\})(?:\\u([\da-f]{4}))?/i
151
150
  VALID_STRING = /\A(?:[^\\]|#{ESCAPES}|#{UTF_8})*\z/o
152
151
  ESCAPED = /(?:#{ESCAPES}|#{UTF_8})/o
153
152
 
@@ -163,7 +162,11 @@ module GraphQL
163
162
  if !str.valid_encoding? || !str.match?(VALID_STRING)
164
163
  raise_parse_error("Bad unicode escape in #{str.inspect}")
165
164
  else
166
- Lexer.replace_escaped_characters_in_place(str)
165
+ begin
166
+ Lexer.replace_escaped_characters_in_place(str)
167
+ rescue RangeError
168
+ raise_parse_error("Bad unicode escape in #{str.inspect}")
169
+ end
167
170
 
168
171
  if !str.valid_encoding?
169
172
  raise_parse_error("Bad unicode escape in #{str.inspect}")
@@ -289,7 +292,7 @@ module GraphQL
289
292
  QUOTE = '"'
290
293
  UNICODE_DIGIT = /[0-9A-Za-z]/
291
294
  FOUR_DIGIT_UNICODE = /#{UNICODE_DIGIT}{4}/
292
- N_DIGIT_UNICODE = %r{#{Punctuation::LCURLY}#{UNICODE_DIGIT}{4,}#{Punctuation::RCURLY}}x
295
+ N_DIGIT_UNICODE = %r{#{Punctuation::LCURLY}#{UNICODE_DIGIT}+#{Punctuation::RCURLY}}x
293
296
  UNICODE_ESCAPE = %r{\\u(?:#{FOUR_DIGIT_UNICODE}|#{N_DIGIT_UNICODE})}
294
297
  STRING_ESCAPE = %r{[\\][\\/bfnrt]}
295
298
  BLOCK_QUOTE = '"""'
@@ -34,8 +34,8 @@ module GraphQL
34
34
  JSON.generate(value)
35
35
  end
36
36
  rescue JSON::GeneratorError
37
- if Float::INFINITY == value
38
- "Infinity"
37
+ if value.is_a?(Float) && !value.finite?
38
+ value.to_s
39
39
  else
40
40
  raise
41
41
  end
@@ -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
@@ -178,6 +179,17 @@ module GraphQL
178
179
  # Apply `first` and `last` to `sliced_nodes`,
179
180
  # returning a new relation
180
181
  def limited_nodes
182
+ return @limited_nodes if @limited_nodes
183
+ if async_dataloader?
184
+ (@load_monitor ||= Monitor.new).synchronize do
185
+ build_limited_nodes
186
+ end
187
+ else
188
+ build_limited_nodes
189
+ end
190
+ end
191
+
192
+ def build_limited_nodes
181
193
  @limited_nodes ||= begin
182
194
  calculate_sliced_nodes_parameters
183
195
  if @sliced_nodes_null_relation
@@ -217,11 +229,24 @@ module GraphQL
217
229
  end
218
230
  end
219
231
 
232
+ def async_dataloader?
233
+ @context&.[](:dataloader).is_a?(GraphQL::Dataloader::AsyncDataloader)
234
+ end
235
+
220
236
  # Load nodes after applying first/last/before/after,
221
237
  # returns an array of nodes
222
238
  def load_nodes
223
239
  # Return an array so we can consistently use `.index(node)` on it
224
- @nodes ||= limited_nodes.to_a
240
+ return @nodes if @nodes
241
+ if async_dataloader?
242
+ # `AsyncDataloader` may resolve sibling fields (eg, `edges` and `pageInfo`)
243
+ # in separate Fibers, so several callers can get here before `@nodes` is set.
244
+ (@load_monitor ||= Monitor.new).synchronize do
245
+ @nodes ||= limited_nodes.to_a
246
+ end
247
+ else
248
+ @nodes = limited_nodes.to_a
249
+ end
225
250
  end
226
251
  end
227
252
  end
@@ -22,7 +22,7 @@ module GraphQL
22
22
  # It is possible there are other extension items in this error, so handle
23
23
  # a one level deep merge explicitly. However beyond that only show the
24
24
  # latest value and problems.
25
- super.merge({ "extensions" => { "value" => value, "problems" => validation_result.problems }}) do |key, oldValue, newValue|
25
+ super.merge({ "extensions" => { "value" => value_for_extensions, "problems" => validation_result.problems }}) do |key, oldValue, newValue|
26
26
  if oldValue.respond_to?(:merge)
27
27
  oldValue.merge(newValue)
28
28
  else
@@ -33,6 +33,21 @@ module GraphQL
33
33
 
34
34
  private
35
35
 
36
+ def value_for_extensions(value = @value)
37
+ case value
38
+ when Array
39
+ value.map { |item| value_for_extensions(item) }
40
+ when Hash
41
+ value.each_with_object({}) do |(key, item), result|
42
+ result[key] = value_for_extensions(item)
43
+ end
44
+ when Float
45
+ value.finite? ? value : value.to_s
46
+ else
47
+ value
48
+ end
49
+ end
50
+
36
51
  def problem_fields
37
52
  @problem_fields ||= @validation_result
38
53
  .problems
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)
@@ -287,8 +287,12 @@ module GraphQL
287
287
  end
288
288
  elsif type_defn.kind.enum?
289
289
  enum_values(type_defn)
290
+ elsif type_defn.kind.union?
291
+ loadable_possible_types(type_defn, @context)
292
+ elsif type_defn.kind.object? || type_defn.kind.interface?
293
+ interfaces(type_defn)
290
294
  end
291
- # Lots more to do here
295
+ possible_types(type_defn)
292
296
  end
293
297
  if @schema.query
294
298
  @schema.introspection_system.entry_points.each do |f|
@@ -304,6 +308,11 @@ module GraphQL
304
308
  end
305
309
  end
306
310
 
311
+ directives.each do |directive|
312
+ arguments(directive).each do |directive_argument|
313
+ argument(directive, directive_argument.graphql_name)
314
+ end
315
+ end
307
316
  end
308
317
 
309
318
  private
@@ -1338,9 +1338,9 @@ module GraphQL
1338
1338
 
1339
1339
  context.errors << execution_error
1340
1340
  execution_error
1341
- when GraphQL::UnresolvedTypeError, GraphQL::StringEncodingError, GraphQL::IntegerEncodingError
1341
+ when GraphQL::UnresolvedTypeError, GraphQL::StringEncodingError, GraphQL::FloatEncodingError, GraphQL::IntegerEncodingError
1342
1342
  raise type_error
1343
- when GraphQL::IntegerDecodingError
1343
+ when GraphQL::FloatDecodingError, GraphQL::IntegerDecodingError
1344
1344
  nil
1345
1345
  end
1346
1346
  end
@@ -15,6 +15,8 @@ module GraphQL
15
15
  # separately (which leads to exponential recursion through nested fragments),
16
16
  # we flatten all fragment spreads into a single field map and compare within it.
17
17
  NO_ARGS = GraphQL::EmptyObjects::EMPTY_HASH
18
+ EXCLUSIVE_COMPARISON = 1
19
+ NONEXCLUSIVE_COMPARISON = 2
18
20
 
19
21
  class Field
20
22
  attr_reader :node, :definition, :owner_type, :parents
@@ -33,6 +35,10 @@ module GraphQL
33
35
  def unwrapped_return_type
34
36
  @unwrapped_return_type ||= return_type&.unwrap
35
37
  end
38
+
39
+ def comparison_key
40
+ @comparison_key ||= [@node, @parents]
41
+ end
36
42
  end
37
43
 
38
44
  def initialize(*)
@@ -43,6 +49,9 @@ module GraphQL
43
49
  # Track which sub-selection node pairs have been compared to prevent
44
50
  # infinite recursion with cyclic fragments
45
51
  @compared_sub_selections = {}.compare_by_identity
52
+ @compared_field_groups = {}
53
+ @field_group_signatures = {}.compare_by_identity
54
+ @field_selection_signatures = {}.compare_by_identity
46
55
  # Cache mutually_exclusive? results for type pairs
47
56
  @mutually_exclusive_cache = {}.compare_by_identity
48
57
  # Cache collect_fields results for sub-selection comparison
@@ -215,21 +224,7 @@ module GraphQL
215
224
  end
216
225
 
217
226
  if all_same
218
- # All fields share a signature, so they can only conflict on
219
- # sub-selections. Deduplicate by AST node identity — fields from
220
- # the same node always have identical sub-selections.
221
- unique_nodes = fields.uniq { |f| f.node.object_id }
222
- i = 0
223
- while i < unique_nodes.size
224
- j = i + 1
225
- while j < unique_nodes.size
226
- if unique_nodes[i].node.selections.size > 0 || unique_nodes[j].node.selections.size > 0
227
- find_conflict(key, unique_nodes[i], unique_nodes[j])
228
- end
229
- j += 1
230
- end
231
- i += 1
232
- end
227
+ find_conflicts_between_selection_groups(key, fields)
233
228
  else
234
229
  groups = fields.group_by { |f| field_signature(f) }
235
230
  unique_groups = groups.values
@@ -243,22 +238,10 @@ module GraphQL
243
238
  gj += 1
244
239
  end
245
240
 
246
- # Within same group, deduplicate by AST node and compare all
247
- # pairs for sub-selection conflicts
241
+ # Within the same group, fields can only conflict on sub-selections.
248
242
  group = unique_groups[gi]
249
243
  if group.size >= 2
250
- unique_in_group = group.uniq { |f| f.node.object_id }
251
- ui = 0
252
- while ui < unique_in_group.size
253
- uj = ui + 1
254
- while uj < unique_in_group.size
255
- if unique_in_group[ui].node.selections.size > 0 || unique_in_group[uj].node.selections.size > 0
256
- find_conflict(key, unique_in_group[ui], unique_in_group[uj])
257
- end
258
- uj += 1
259
- end
260
- ui += 1
261
- end
244
+ find_conflicts_between_selection_groups(key, group)
262
245
  end
263
246
 
264
247
  gi += 1
@@ -279,6 +262,29 @@ module GraphQL
279
262
  end
280
263
  end
281
264
 
265
+ def find_conflicts_between_selection_groups(response_key, fields)
266
+ fields_by_selection = {}
267
+ fields.each do |field|
268
+ fields_by_selection[field_selection_signature(field)] ||= field
269
+ end
270
+
271
+ representatives = fields_by_selection.values
272
+ i = 0
273
+ while i < representatives.size
274
+ j = i + 1
275
+ while j < representatives.size
276
+ find_conflict(response_key, representatives[i], representatives[j])
277
+ j += 1
278
+ end
279
+ i += 1
280
+ end
281
+ end
282
+
283
+ def field_selection_signature(field)
284
+ node = field.node
285
+ @field_selection_signatures[node] ||= node.selections.map(&:to_query_string)
286
+ end
287
+
282
288
  def fields_same_signature?(f1, f2)
283
289
  n1 = f1.node
284
290
  n2 = f2.node
@@ -458,6 +464,7 @@ module GraphQL
458
464
  response_keys.each do |key, fields|
459
465
  fields2 = response_keys2[key]
460
466
  next unless fields2
467
+ next if field_groups_already_compared?(fields, fields2, mutually_exclusive)
461
468
 
462
469
  fields_arr = fields.is_a?(Field) ? [fields] : fields
463
470
  fields2_arr = fields2.is_a?(Field) ? [fields2] : fields2
@@ -475,6 +482,36 @@ module GraphQL
475
482
  end
476
483
  end
477
484
 
485
+ def field_groups_already_compared?(fields, fields2, mutually_exclusive)
486
+ signature1 = field_group_signature(fields)
487
+ signature2 = field_group_signature(fields2)
488
+ previous_comparisons = @compared_field_groups[signature1]
489
+ comparison_state = previous_comparisons && previous_comparisons[signature2]
490
+
491
+ if mutually_exclusive
492
+ return true if comparison_state
493
+ new_state = EXCLUSIVE_COMPARISON
494
+ else
495
+ return true if comparison_state == NONEXCLUSIVE_COMPARISON
496
+ new_state = NONEXCLUSIVE_COMPARISON
497
+ end
498
+
499
+ previous_comparisons ||= (@compared_field_groups[signature1] = {})
500
+ previous_comparisons[signature2] = new_state
501
+
502
+ reverse_comparisons = @compared_field_groups[signature2] ||= {}
503
+ reverse_comparisons[signature1] = new_state
504
+ false
505
+ end
506
+
507
+ def field_group_signature(fields)
508
+ if fields.is_a?(Field)
509
+ fields.comparison_key
510
+ else
511
+ @field_group_signatures[fields] ||= fields.map(&:comparison_key)
512
+ end
513
+ end
514
+
478
515
  def same_arguments?(field1, field2)
479
516
  arguments1 = field1.arguments
480
517
  arguments2 = field2.arguments
@@ -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
@@ -5,12 +5,26 @@ module GraphQL
5
5
  class Float < GraphQL::Schema::Scalar
6
6
  description "Represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point)."
7
7
 
8
- def self.coerce_input(value, _ctx)
9
- value.is_a?(Numeric) ? value.to_f : nil
8
+ def self.coerce_input(value, ctx)
9
+ return if !value.is_a?(Numeric)
10
+
11
+ value = value.to_f
12
+ if value.finite?
13
+ value
14
+ else
15
+ err = GraphQL::FloatDecodingError.new(value)
16
+ ctx.schema.type_error(err, ctx)
17
+ end
10
18
  end
11
19
 
12
- def self.coerce_result(value, _ctx)
13
- value.to_f
20
+ def self.coerce_result(value, ctx)
21
+ value = value.to_f
22
+ if value.finite?
23
+ value
24
+ else
25
+ err = GraphQL::FloatEncodingError.new(value, context: ctx)
26
+ ctx.schema.type_error(err, ctx)
27
+ end
14
28
  end
15
29
 
16
30
  default_scalar true
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "2.6.9"
3
+ VERSION = "2.6.11"
4
4
  end
data/lib/graphql.rb CHANGED
@@ -93,6 +93,8 @@ This is probably a bug in GraphQL-Ruby, please report this error on GitHub: http
93
93
  autoload :AnalysisError, "graphql/analysis_error"
94
94
  autoload :CoercionError, "graphql/coercion_error"
95
95
  autoload :InvalidNameError, "graphql/invalid_name_error"
96
+ autoload :FloatDecodingError, "graphql/float_decoding_error"
97
+ autoload :FloatEncodingError, "graphql/float_encoding_error"
96
98
  autoload :IntegerDecodingError, "graphql/integer_decoding_error"
97
99
  autoload :IntegerEncodingError, "graphql/integer_encoding_error"
98
100
  autoload :StringEncodingError, "graphql/string_encoding_error"
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.9
4
+ version: 2.6.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-08-17 00:00:00.000000000 Z
10
+ date: 2026-09-21 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64
@@ -460,6 +460,8 @@ files:
460
460
  - lib/graphql/execution/runner.rb
461
461
  - lib/graphql/execution/selections_step.rb
462
462
  - lib/graphql/execution_error.rb
463
+ - lib/graphql/float_decoding_error.rb
464
+ - lib/graphql/float_encoding_error.rb
463
465
  - lib/graphql/integer_decoding_error.rb
464
466
  - lib/graphql/integer_encoding_error.rb
465
467
  - lib/graphql/introspection.rb