graphql 2.6.3 → 2.6.5

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: 16abc9c2a5eda0da251dbe7e14433241bc7d038d2527926832ceb29336fb857a
4
- data.tar.gz: c89bd2a4b340ca30bfa7b823f51e4671972355d54bd56fdaed598c13a415c3b3
3
+ metadata.gz: 8ece536c0702c8789a3a5c79de82af4bd2ff806a3209d0ac4da88fc968629d19
4
+ data.tar.gz: 0063f1ea552e7782029083eb027138a0725af89a65265dc643469f2227184134
5
5
  SHA512:
6
- metadata.gz: 6452d2a517c502b4a8934582d060885a5f6462de244a1f331433f1076dc57a879cdd081e147f70925064a99607e0794fcf6eadad7a3f4a9111a5ab979c552554
7
- data.tar.gz: b6a0219606d905fc885ab2192596d1acb1d92bce407defc751bfafb1a7199e9c32275c1d3202f3246251e72b80554698b1f54920f76178f1ddc81b41d628bacd
6
+ metadata.gz: 2ee406b8ad164004af45d98e3b5dc70d69396f008a80cc68ef83a4bff212698c0a181644dc2b513e8bd1e71d2baf7d1ba2901ad05f497199556b171a6dfb11ab
7
+ data.tar.gz: 2ffd4e01742bd3b0feb207a316b1935d1edff05be8b28afb1c42e5ba47b4df916df802ee15617bb30bd45cdf6fd4f7a59c22b5d73cbabb3d8ee6658f3246d8ef
@@ -6,7 +6,23 @@ module Graphql
6
6
  module FieldExtractor
7
7
  def fields
8
8
  columns = []
9
- columns += (klass&.columns&.map { |c| generate_column_string(c) } || [])
9
+ if (model_columns = klass&.columns)
10
+ filter = if defined?(ActiveSupport::ParameterFilter)
11
+ fp = if defined?(Rails) && Rails.application && (app_config = Rails.application.config.filter_parameters).present? && !app_config.empty?
12
+ app_config
13
+ elsif ActiveSupport.respond_to?(:filter_parameters)
14
+ ActiveSupport.filter_parameters
15
+ else
16
+ []
17
+ end
18
+ ActiveSupport::ParameterFilter.new(fp, mask: nil)
19
+ else
20
+ nil
21
+ end
22
+ columns += model_columns
23
+ .select { |c| filter ? filter.filter_param(c.name, c.name) : true }
24
+ .map { |c| generate_column_string(c) }
25
+ end
10
26
  columns + custom_fields
11
27
  end
12
28
 
@@ -29,14 +29,14 @@ module GraphQL
29
29
  future_complexity
30
30
  end
31
31
  when nil
32
- subject.logger.warn <<~GRAPHQL
32
+ subject.logger.warn <<~MESSAGE
33
33
  GraphQL-Ruby's complexity cost system is getting some "breaking fixes" in a future version. See the migration notes at https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#complexity_cost_calculation_mode_for-class_method
34
34
 
35
35
  To opt into the future behavior, configure your schema (#{subject.schema.name ? subject.schema.name : subject.schema.ancestors}) with:
36
36
 
37
37
  complexity_cost_calculation_mode(:future) # or `:legacy`, `:compare`
38
38
 
39
- GRAPHQL
39
+ MESSAGE
40
40
  max_possible_complexity(mode: :legacy)
41
41
  else
42
42
  raise ArgumentError, "Expected `:future`, `:legacy`, `:compare`, or `nil` from `#{query.schema}.complexity_cost_calculation_mode_for` but got: #{query.schema.complexity_cost_calculation_mode.inspect}"
@@ -1,86 +1,324 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL
3
4
  class Dataloader
4
5
  class AsyncDataloader < Dataloader
6
+ def self.use(...)
7
+ if !Async::Task.method_defined?(:cancel)
8
+ Async::Task.alias_method(:cancel, :stop)
9
+ end
10
+ if !Async::Task.method_defined?(:graphql_async_dataloader_run)
11
+ Async::Task.attr_accessor(:graphql_async_dataloader_run)
12
+ Async::Task.attr_accessor(:graphql_async_dataloader_condition)
13
+ end
14
+ super
15
+ end
16
+
17
+ def initialize(...)
18
+ super
19
+ create_pending_run
20
+ end
21
+
22
+ def create_pending_run
23
+ jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
24
+ @pending_run = Run.new(total_fiber_limit, jobs_fiber_limit)
25
+ end
26
+
5
27
  def yield(source = Fiber[:__graphql_current_dataloader_source])
6
- trace = Fiber[:__graphql_current_multiplex]&.current_trace
28
+ task = Async::Task.current
29
+ run = task.graphql_async_dataloader_run
30
+ trace = run.trace
7
31
  trace&.dataloader_fiber_yield(source)
8
- if (condition = Fiber[:graphql_dataloader_next_tick])
9
- condition.wait
10
- else
11
- Fiber.yield
12
- end
32
+ run.finished_tasks.push(task)
33
+ condition = task.graphql_async_dataloader_condition
34
+ condition.wait
35
+ run.started_tasks.push(task)
13
36
  trace&.dataloader_fiber_resume(source)
14
37
  nil
15
38
  end
16
39
 
17
- def run(trace_query_lazy: nil)
18
- trace = Fiber[:__graphql_current_multiplex]&.current_trace
19
- jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
20
- job_fibers = []
21
- next_job_fibers = []
22
- source_tasks = []
23
- next_source_tasks = []
24
- first_pass = true
25
- sources_condition = Async::Condition.new
26
- manager = spawn_fiber do
27
- trace&.begin_dataloader(self)
28
- while first_pass || !job_fibers.empty?
29
- first_pass = false
30
- fiber_vars = get_fiber_variables
40
+ class Run
41
+ def initialize(total_fiber_limit, jobs_fiber_limit)
42
+ @root_task = nil
43
+ @trace = nil
44
+ @jobs = []
31
45
 
32
- run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace)
46
+ @total_fiber_limit = total_fiber_limit
47
+ @jobs_fiber_limit = jobs_fiber_limit
48
+ @lazies_at_depth = Hash.new { |h, k| h[k] = [] }
33
49
 
34
- Sync do |root_task|
35
- set_fiber_variables(fiber_vars)
36
- while !source_tasks.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }
37
- while (task = (source_tasks.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size + next_source_tasks.size) < total_fiber_limit) && spawn_source_task(root_task, sources_condition, trace))))
38
- if task.alive?
39
- root_task.yield # give the source task a chance to run
40
- next_source_tasks << task
41
- end
42
- end
43
- sources_condition.signal
44
- source_tasks.concat(next_source_tasks)
45
- next_source_tasks.clear
50
+ @finished_tasks = nil
51
+ @started_tasks = nil
52
+ @started_count_task = nil
53
+ @finished_count_task = nil
54
+ @finished_all_tasks = nil
55
+ @finished_first_pass = nil
56
+
57
+ @snoozed_jobs_condition = Async::Condition.new
58
+ @snoozed_sources_condition = Async::Condition.new
59
+ end
60
+
61
+ attr_accessor :trace, :root_task
62
+
63
+ attr_reader :jobs, :lazies_at_depth, :jobs_fiber_limit, :total_fiber_limit, :finished_tasks, :started_tasks, :snoozed_jobs_condition, :snoozed_sources_condition
64
+
65
+ def jobs_bandwidth?
66
+ running_count < jobs_fiber_limit
67
+ end
68
+
69
+ def allowed_sources_tasks
70
+ within_limit = total_fiber_limit - running_count
71
+ if within_limit < 1
72
+ 1
73
+ else
74
+ within_limit
75
+ end
76
+ end
77
+
78
+ def close_queues
79
+ @finished_tasks.close
80
+ @finished_count_task.cancel
81
+
82
+ @started_tasks.close
83
+ @started_count_task.cancel
84
+ end
85
+
86
+ def running_count
87
+ @snoozed_jobs_condition.instance_variable_get(:@ready).num_waiting +
88
+ @snoozed_sources_condition.instance_variable_get(:@ready).num_waiting +
89
+ @started_count +
90
+ @started_tasks.size -
91
+ @finished_count
92
+ end
93
+
94
+ def wait_for_queues
95
+ if !@finished_first_pass.resolved?
96
+ @finished_first_pass.resolve(true)
97
+ end
98
+
99
+ @finished_all_tasks.wait
100
+ @finished_all_tasks = Async::Promise.new
101
+ end
102
+
103
+ def new_queues
104
+ @finished_tasks = Async::Queue.new
105
+ @finished_count = 0
106
+ @started_tasks = Async::Queue.new
107
+ @started_count = 0
108
+ @finished_first_pass = Async::Promise.new
109
+ @finished_all_tasks = Async::Promise.new
110
+
111
+ @started_count_task = @root_task.async do |task|
112
+ @finished_first_pass.wait
113
+ while task = @started_tasks.wait
114
+ @started_count += 1
115
+ if task.status == :initialized # could also be resumed after waiting
116
+ task.run
46
117
  end
47
118
  end
119
+ end
48
120
 
49
- if !@lazies_at_depth.empty?
50
- with_trace_query_lazy(trace_query_lazy) do
51
- run_next_pending_lazies(job_fibers, trace)
52
- run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace)
121
+ @finished_count_task = @root_task.async do |task|
122
+ while t_or_err = @finished_tasks.wait
123
+ if t_or_err.is_a?(StandardError)
124
+ @finished_all_tasks.reject(t_or_err)
125
+ else
126
+ @finished_count += 1
127
+ if @finished_count == @started_count
128
+ @finished_all_tasks.resolve(true)
129
+ end
53
130
  end
54
131
  end
55
132
  end
56
- trace&.end_dataloader(self)
57
133
  end
58
134
 
59
- manager.resume
60
- if manager.alive?
61
- raise "Invariant: Manager didn't terminate successfully: #{manager}"
135
+ def running?
136
+ @snoozed_jobs_condition.waiting? || @snoozed_sources_condition.waiting?
62
137
  end
138
+ end
139
+
140
+ def append_job(callable = nil, &block)
141
+ active_run.jobs.push(callable || block)
142
+ nil
143
+ end
144
+
145
+ def lazy_at_depth(depth, lazy)
146
+ active_run.lazies_at_depth[depth] << lazy
147
+ end
148
+
149
+ def active_run
150
+ @pending_run || Async::Task.current?&.graphql_async_dataloader_run || raise(GraphQL::Error, "No available Run to append to, GraphQL-Ruby bug")
151
+ end
63
152
 
153
+ def run_isolated
154
+ previous_run = Async::Task.current?&.graphql_async_dataloader_run
155
+ prev_pending_keys = {}
156
+ # Clear pending loads but keep already-cached records
157
+ # in case they are useful to the given block.
158
+ @source_cache.each do |source_class, batched_sources|
159
+ batched_sources.each do |batch_args, batched_source_instance|
160
+ if batched_source_instance.pending?
161
+ prev_pending_keys[batched_source_instance] = batched_source_instance.pending.dup
162
+ batched_source_instance.pending.clear
163
+ end
164
+ end
165
+ end
166
+
167
+ res = nil
168
+ create_pending_run
169
+ @pending_run.jobs << -> { res = yield }
170
+ run
171
+ res
172
+ ensure
173
+ if previous_run
174
+ Async::Task.current.graphql_async_dataloader_run = previous_run
175
+ # clear the one created in #run:
176
+ @pending_run = nil
177
+ end
178
+ prev_pending_keys.each do |source_instance, pending|
179
+ pending.each do |key, value|
180
+ if !source_instance.results.key?(key)
181
+ source_instance.pending[key] = value
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ def run(trace_query_lazy: nil)
188
+ trace = Fiber[:__graphql_current_multiplex]&.current_trace
189
+ run = @pending_run || Async::Task.current?&.graphql_async_dataloader_run || raise(GraphQL::Error, "No available Run, GraphQL-Ruby internal bug")
190
+ @pending_run = nil
191
+ run.trace = trace
192
+ first_pass = true
193
+ trace&.begin_dataloader(self)
194
+ fiber_vars = get_fiber_variables
195
+ raised_error = nil
196
+ jobs = run.jobs
197
+ Sync do |_maybe_new_task|
198
+ # Make sure there's a new task instance to hold `.graphql_...` state:
199
+ task = Async::Task.new do |root_task|
200
+ run.root_task = root_task
201
+ root_task.graphql_async_dataloader_run = run
202
+ set_fiber_variables(fiber_vars)
203
+
204
+ while first_pass || run.running? || !jobs.empty?
205
+ first_pass = false
206
+ run_pending_steps(run)
207
+ run_sources(run)
208
+
209
+ if !run.lazies_at_depth.empty?
210
+ with_trace_query_lazy(trace_query_lazy) do
211
+ run_next_pending_lazies(run)
212
+ run_pending_steps(run)
213
+ end
214
+ end
215
+ end
216
+ rescue StandardError => err
217
+ raised_error = err
218
+ root_task.cancel
219
+ end
220
+
221
+ task.run
222
+ task.wait
223
+ end
224
+ create_pending_run
225
+ if raised_error
226
+ raise raised_error
227
+ end
228
+ trace&.end_dataloader(self)
64
229
  rescue UncaughtThrowError => e
65
230
  throw e.tag, e.value
66
231
  end
67
232
 
68
233
  private
69
234
 
70
- def run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace)
71
- while (f = (job_fibers.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size) < jobs_fiber_limit) && spawn_job_fiber(trace))))
72
- if f.alive?
73
- finished = run_fiber(f)
74
- if !finished
75
- next_job_fibers << f
235
+ def run_pending_steps(run)
236
+ run.new_queues
237
+
238
+ if (unsnoozed = run.snoozed_jobs_condition.waiting?)
239
+ run.snoozed_jobs_condition.signal
240
+ end
241
+ pending_jobs = run.jobs
242
+ while (!pending_jobs.empty? && (has_limit = run.jobs_bandwidth?)) || (unsnoozed)
243
+ unsnoozed = false
244
+ if has_limit
245
+ spawn_job_task(run)
246
+ end
247
+ run.wait_for_queues
248
+ end
249
+ ensure
250
+ run.close_queues
251
+ end
252
+
253
+ def spawn_job_task(run)
254
+ pending_jobs = run.jobs
255
+ if !pending_jobs.empty?
256
+ fiber_vars = get_fiber_variables
257
+ new_task = Async::Task.new(run.root_task) do |task|
258
+ run.trace&.dataloader_spawn_execution_fiber(pending_jobs)
259
+ task.graphql_async_dataloader_run = run
260
+ task.graphql_async_dataloader_condition = run.snoozed_jobs_condition
261
+ set_fiber_variables(fiber_vars)
262
+ while job = pending_jobs.shift
263
+ job.call
76
264
  end
265
+ ensure
266
+ cleanup_fiber
267
+ run.finished_tasks.push($! || task)
268
+ run.trace&.dataloader_fiber_exit
77
269
  end
270
+ run.started_tasks.push(new_task)
271
+ new_task
78
272
  end
79
- job_fibers.concat(next_job_fibers)
80
- next_job_fibers.clear
81
273
  end
82
274
 
83
- def spawn_source_task(parent_task, condition, trace)
275
+ def run_sources(run)
276
+ run.new_queues
277
+
278
+ if (unsnoozed = run.snoozed_sources_condition.waiting?)
279
+ run.snoozed_sources_condition.signal
280
+ end
281
+
282
+ allowed_tasks = run.allowed_sources_tasks
283
+ while (has_pending = @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) } ) || unsnoozed
284
+ unsnoozed = false
285
+ if has_pending
286
+ spawn_source_task(run, allowed_tasks)
287
+ end
288
+ run.wait_for_queues
289
+ end
290
+ ensure
291
+ run.close_queues
292
+ end
293
+
294
+ #### TODO DRY Had to duplicate to remove spawn_job_fiber
295
+ def run_next_pending_lazies(run)
296
+ smallest_depth = nil
297
+ run.lazies_at_depth.each_key do |depth_key|
298
+ smallest_depth ||= depth_key
299
+ if depth_key < smallest_depth
300
+ smallest_depth = depth_key
301
+ end
302
+ end
303
+
304
+ if smallest_depth
305
+ lazies = run.lazies_at_depth.delete(smallest_depth)
306
+ if !lazies.empty?
307
+ begin
308
+ run.new_queues
309
+ lazies.each_with_index do |l, idx|
310
+ append_job { l.value }
311
+ end
312
+ spawn_job_task(run) # Todo what was the last `true` condition?
313
+ run.wait_for_queues
314
+ ensure
315
+ run.close_queues
316
+ end
317
+ end
318
+ end
319
+ end
320
+
321
+ def spawn_source_task(run, num_tasks)
84
322
  pending_sources = nil
85
323
  @source_cache.each_value do |source_by_batch_params|
86
324
  source_by_batch_params.each_value do |source|
@@ -92,18 +330,30 @@ module GraphQL
92
330
  end
93
331
 
94
332
  if pending_sources
333
+ if num_tasks == Float::INFINITY
334
+ num_tasks = pending_sources.size
335
+ end
95
336
  fiber_vars = get_fiber_variables
96
- parent_task.async do
97
- trace&.dataloader_spawn_source_fiber(pending_sources)
98
- set_fiber_variables(fiber_vars)
99
- Fiber[:graphql_dataloader_next_tick] = condition
100
- pending_sources.each do |s|
101
- trace&.begin_dataloader_source(s)
102
- s.run_pending_keys
103
- trace&.end_dataloader_source(s)
337
+ trace = run.trace
338
+ num_tasks.times do
339
+ new_task = Async::Task.new(run.root_task) do |task|
340
+ task.graphql_async_dataloader_run = run
341
+ task.graphql_async_dataloader_condition = run.snoozed_sources_condition
342
+ trace&.dataloader_spawn_source_fiber(pending_sources)
343
+ set_fiber_variables(fiber_vars)
344
+ while (source = pending_sources.shift)
345
+ trace&.begin_dataloader_source(source)
346
+ source.run_pending_keys
347
+ trace&.end_dataloader_source(source)
348
+ end
349
+ nil
350
+ ensure
351
+ run.finished_tasks.push($! || task)
352
+ cleanup_fiber
353
+ trace&.dataloader_fiber_exit
104
354
  end
105
- cleanup_fiber
106
- trace&.dataloader_fiber_exit
355
+ run.started_tasks.push(new_task)
356
+ new_task
107
357
  end
108
358
  end
109
359
  end
@@ -89,7 +89,8 @@ module GraphQL
89
89
  sync(pending_keys)
90
90
  end
91
91
 
92
- result_keys.map { |k| result_for(k) }
92
+ result_keys.map! { |k| result_for(k) }
93
+ result_keys
93
94
  end
94
95
 
95
96
  # Subclasses must implement this method to return a value for each of `keys`
@@ -138,22 +139,25 @@ module GraphQL
138
139
  # @api private
139
140
  # @return [void]
140
141
  def run_pending_keys
141
- if !@fetching.empty?
142
- @fetching.each_key { |k| @pending.delete(k) }
143
- end
142
+ @fetching.each_key { |k| @pending.delete(k) }
144
143
  return if @pending.empty?
145
144
  fetch_h = @pending
146
- @pending = {}
147
145
  @fetching.merge!(fetch_h)
146
+ @pending = {}
148
147
  results = fetch(fetch_h.values)
149
- fetch_h.each_with_index do |(key, _value), idx|
148
+ idx = 0
149
+
150
+ fetch_h.each_key do |key|
150
151
  @results[key] = results[idx]
152
+ @fetching.delete(key)
153
+ idx += 1
151
154
  end
152
155
  nil
153
156
  rescue StandardError => error
154
- fetch_h.each_key { |key| @results[key] = error }
155
- ensure
156
- fetch_h && fetch_h.each_key { |k| @fetching.delete(k) }
157
+ fetch_h.each_key { |key|
158
+ @results[key] = error
159
+ @fetching.delete(key)
160
+ }
157
161
  end
158
162
 
159
163
  # These arguments are given to `dataloader.with(source_class, ...)`. The object
@@ -171,7 +175,11 @@ module GraphQL
171
175
  # @param batch_kwargs [Hash]
172
176
  # @return [Object]
173
177
  def self.batch_key_for(*batch_args, **batch_kwargs)
174
- [*batch_args, **batch_kwargs]
178
+ if batch_kwargs.any? # rubocop:disable Development/NoneWithoutBlockCop
179
+ [*batch_args, **batch_kwargs]
180
+ else
181
+ batch_args
182
+ end
175
183
  end
176
184
 
177
185
  # Clear any already-loaded objects for this source
@@ -58,7 +58,7 @@ module GraphQL
58
58
  end
59
59
 
60
60
  def initialize(nonblocking: self.class.default_nonblocking, fiber_limit: self.class.default_fiber_limit)
61
- @source_cache = Hash.new { |h, k| h[k] = {} }
61
+ @source_cache = Hash.new { |h, k| h[k] = {} }.compare_by_identity
62
62
  @pending_jobs = []
63
63
  if !nonblocking.nil?
64
64
  @nonblocking = nonblocking
@@ -49,11 +49,12 @@ module GraphQL
49
49
  end
50
50
 
51
51
  def value
52
+ return nil if @selections_step.killed
52
53
  query = @selections_step.query
53
54
  set_current_field
54
- query.current_trace.begin_execute_field(@field_definition, @arguments, @field_results, query)
55
+ query.current_trace.begin_execute_field(@field_definition, @field_results, @arguments, query)
55
56
  sync(@field_results)
56
- query.current_trace.end_execute_field(@field_definition, @arguments, @field_results, query, @field_results)
57
+ query.current_trace.end_execute_field(@field_definition, @field_results, @arguments, query, @field_results)
57
58
  @runner.add_step(self)
58
59
  true
59
60
  ensure
@@ -79,7 +80,9 @@ module GraphQL
79
80
  end
80
81
 
81
82
  def call
83
+ return nil if @selections_step.killed
82
84
  set_current_field if @field_definition
85
+
83
86
  if @enqueued_authorization
84
87
  enqueue_next_steps
85
88
  elsif @finish_extension_idx
@@ -102,24 +105,64 @@ module GraphQL
102
105
  set_current_field(nil)
103
106
  end
104
107
 
105
- def add_graphql_error(err)
108
+ def add_graphql_error(result, key, err, return_type: @field_definition.type)
106
109
  err.path = path
107
110
  if err.ast_node.nil?
108
111
  err.ast_nodes = ast_nodes
109
112
  end
110
- @selections_step.query.context.add_error(err)
113
+ @runner.add_finalizer(@selections_step.query, result, key, err)
114
+ if !err.is_a?(GraphQL::Execution::Skip)
115
+ field_type = return_type
116
+ should_propagate_null = field_type.non_null?
117
+ while (should_propagate_null == false && field_type.kind.wraps?)
118
+ field_type = field_type.of_type
119
+ should_propagate_null = field_type.non_null?
120
+ end
121
+ if should_propagate_null
122
+ propagate_nulls
123
+ end
124
+ end
111
125
  err
112
126
  end
113
127
 
128
+ def propagate_nulls
129
+ propagating_null = true
130
+ highest_nulled_depth = path.size
131
+ highest_list_depth = nil
132
+ current_field_step = self
133
+ while current_field_step
134
+ return_type = current_field_step.field_definition.type
135
+ if propagating_null && return_type.non_null?
136
+ highest_nulled_depth = current_field_step.path.size
137
+ else
138
+ propagating_null = false
139
+ end
140
+
141
+ if return_type.list?
142
+ highest_list_depth = current_field_step.path.size
143
+ end
144
+
145
+ current_field_step = current_field_step.selections_step.field_resolve_step
146
+ end
147
+
148
+ if highest_list_depth.nil? || highest_nulled_depth <= highest_list_depth
149
+ kill_field_step = self
150
+ while kill_field_step && highest_nulled_depth <= kill_field_step.path.size
151
+ kill_field_step.selections_step.killed = true
152
+ kill_field_step = kill_field_step.selections_step.field_resolve_step
153
+ end
154
+ end
155
+ end
156
+
114
157
  def build_errors_result(errors, single_error)
115
158
  first_error = errors.nil? ? single_error : errors.pop
116
- @field_results = error_instance_array(@selections_step.objects.size, first_error)
159
+ @field_results = [first_error]
160
+ @results = [@selections_step.results.first]
117
161
  if errors
118
162
  errors.each do |e|
119
- add_graphql_error(e)
163
+ add_graphql_error(@results.first, key, e)
120
164
  end
121
165
  end
122
- @results ||= @selections_step.results
123
166
  build_results
124
167
  end
125
168
 
@@ -226,7 +269,7 @@ module GraphQL
226
269
  authorized_results << @results[i]
227
270
  end
228
271
  rescue GraphQL::ExecutionError => exec_err
229
- add_graphql_error(exec_err)
272
+ add_graphql_error(@results[i], key, exec_err)
230
273
  end
231
274
  end
232
275
  i += 1
@@ -244,7 +287,7 @@ module GraphQL
244
287
  @was_scoped = true
245
288
  end
246
289
 
247
- query.current_trace.begin_execute_field(@field_definition, @arguments, authorized_objects, query)
290
+ query.current_trace.begin_execute_field(@field_definition, authorized_objects, @arguments, query)
248
291
 
249
292
  if @runner.uses_runtime_directives
250
293
  if @ast_nodes.nil? || @ast_nodes.size == 1
@@ -319,7 +362,7 @@ module GraphQL
319
362
  @field_results = resolve_batch(authorized_objects, ctx, @arguments)
320
363
  end
321
364
 
322
- query.current_trace.end_execute_field(@field_definition, @arguments, authorized_objects, query, @field_results)
365
+ query.current_trace.end_execute_field(@field_definition, authorized_objects, @arguments, query, @field_results)
323
366
 
324
367
  if any_lazy_results?
325
368
  @runner.dataloader.lazy_at_depth(path.size, self)
@@ -331,12 +374,12 @@ module GraphQL
331
374
  end
332
375
  end
333
376
  rescue GraphQL::ExecutionError => err
334
- add_graphql_error(err)
377
+ build_errors_result(nil, err)
335
378
  rescue StandardError => stderr
336
379
  begin
337
380
  @selections_step.query.handle_or_reraise(stderr, field: @field_definition, arguments: @arguments, object: nil)
338
381
  rescue GraphQL::ExecutionError => err
339
- add_graphql_error(err)
382
+ add_graphql_error(@results[0], key, err)
340
383
  end
341
384
  end
342
385
 
@@ -458,13 +501,13 @@ module GraphQL
458
501
  end
459
502
 
460
503
  def finish_leaf_result(result_h, key, field_result, return_type, ctx)
461
- final_field_result = build_leaf_result(field_result, return_type, ctx, false)
504
+ final_field_result = build_leaf_result(result_h, key, field_result, return_type, ctx, false)
462
505
 
463
506
  @directive_finalizers&.each { |f| @runner.add_finalizer(ctx.query, result_h, key, f) }
464
507
  result_h[@key] = final_field_result
465
508
  end
466
509
 
467
- def build_leaf_result(field_result, return_type, ctx, is_from_array)
510
+ def build_leaf_result(result_h, result_key, field_result, return_type, ctx, is_from_array)
468
511
  if field_result.nil?
469
512
  if return_type.non_null?
470
513
  add_non_null_error(is_from_array)
@@ -473,7 +516,7 @@ module GraphQL
473
516
  end
474
517
  elsif field_result.is_a?(Finalizer)
475
518
  if field_result.is_a?(GraphQL::RuntimeError)
476
- add_graphql_error(field_result)
519
+ add_graphql_error(result_h, result_key, field_result, return_type: return_type)
477
520
  else
478
521
  field_result.path = path
479
522
  @runner.add_finalizer(ctx.query, result_h, key, field_result)
@@ -484,7 +527,11 @@ module GraphQL
484
527
  end
485
528
 
486
529
  inner_type = return_type.of_type
487
- field_result.map { |item| build_leaf_result(item, inner_type, ctx, true) }
530
+ result_a = Array.new(field_result.size)
531
+ field_result.each_with_index do |item, idx|
532
+ result_a[idx] = build_leaf_result(result_a, idx, item, inner_type, ctx, true)
533
+ end
534
+ result_a
488
535
  else
489
536
  return_type.coerce_result(field_result, ctx)
490
537
  end
@@ -526,6 +573,7 @@ module GraphQL
526
573
  query.current_trace.objects(obj_type, next_objects, ctx)
527
574
  @runner.add_step(SelectionsStep.new(
528
575
  path: path,
576
+ field_resolve_step: self,
529
577
  parent_type: obj_type,
530
578
  selections: @next_selections,
531
579
  objects: next_objects,
@@ -538,6 +586,7 @@ module GraphQL
538
586
  query.current_trace.objects(@static_type, @all_next_objects, ctx)
539
587
  @runner.add_step(SelectionsStep.new(
540
588
  path: path,
589
+ field_resolve_step: self,
541
590
  parent_type: @static_type,
542
591
  selections: @next_selections,
543
592
  objects: @all_next_objects,
@@ -558,7 +607,11 @@ module GraphQL
558
607
 
559
608
  def add_non_null_error(is_from_array)
560
609
  err = @parent_type::InvalidNullError.new(@parent_type, @field_definition, ast_nodes, is_from_array: is_from_array, path: path)
561
- @runner.schema.type_error(err, @selections_step.query.context)
610
+ nn_result = @runner.schema.type_error(err, @selections_step.query.context)
611
+ if nn_result.nil?
612
+ propagate_nulls
613
+ end
614
+ nn_result
562
615
  end
563
616
 
564
617
  def set_current_field(new_value = @field_definition)
@@ -576,7 +629,7 @@ module GraphQL
576
629
  end
577
630
  elsif field_result.is_a?(Finalizer)
578
631
  graphql_result[key] = if field_result.is_a?(GraphQL::RuntimeError)
579
- add_graphql_error(field_result)
632
+ add_graphql_error(graphql_result, key, field_result)
580
633
  else
581
634
  field_result.path = path
582
635
  @runner.add_finalizer(@selections_step.query, graphql_result, key, field_result)
@@ -33,14 +33,15 @@ module GraphQL
33
33
 
34
34
  targets.each_with_index do |target, idx|
35
35
  if target.is_a?(Hash)
36
- if target[key].equal?(err)
36
+ value_at_key = target[key]
37
+ if value_at_key.equal?(err)
37
38
  tf = @finalizers[target] ||= {}.compare_by_identity
38
39
  tf[key] = err
39
40
  @finalizers_count += 1
40
- elsif (arr = target[key]).is_a?(Array)
41
- arr.each_with_index do |el, idx|
41
+ elsif value_at_key.is_a?(Array)
42
+ value_at_key.each_with_index do |el, idx|
42
43
  if el.equal?(err)
43
- tf = @finalizers[arr] ||= {}.compare_by_identity
44
+ tf = @finalizers[value_at_key] ||= {}.compare_by_identity
44
45
  tf[idx] = err
45
46
  @finalizers_count += 1
46
47
  end
@@ -150,7 +151,7 @@ module GraphQL
150
151
  when Language::Nodes::InlineFragment
151
152
  static_type_at_result = @static_type_at[result_h]
152
153
  if static_type_at_result && (
153
- (t = ast_selection.type).nil? ||
154
+ (t = ast_selection.type).nil? ||
154
155
  @runner.type_condition_applies?(@query.context, static_type_at_result, t.name)
155
156
  )
156
157
  result_h = check_object_result(result_h, parent_type, ast_selection.selections)
@@ -170,9 +171,7 @@ module GraphQL
170
171
  end
171
172
 
172
173
  def check_list_result(result_arr, inner_type, ast_selections)
173
- inner_type_non_null = false
174
- if inner_type.non_null?
175
- inner_type_non_null = true
174
+ if (inner_type_non_null = inner_type.non_null?)
176
175
  inner_type = inner_type.of_type
177
176
  end
178
177
 
@@ -492,12 +492,6 @@ module GraphQL
492
492
  end
493
493
  end
494
494
  end
495
- # If this field is a root mutation field, immediately resolve
496
- # all of its child fields before moving on to the next root mutation field.
497
- # (Subselections of this mutation will still be resolved level-by-level.)
498
- if selection_result.graphql_is_eager
499
- @dataloader.run
500
- end
501
495
  end
502
496
 
503
497
  def set_result(selection_result, result_name, value, is_child_result, is_non_null)
@@ -73,6 +73,10 @@ module GraphQL
73
73
  @field_resolve_step.set_current_field(nil)
74
74
  end
75
75
 
76
+ def add_field_error(err)
77
+ @field_resolve_step.add_graphql_error(@graphql_result, @key, err)
78
+ end
79
+
76
80
  def authorize
77
81
  if @field_resolve_step.was_scoped && !@resolved_type.reauthorize_scoped_objects
78
82
  @authorized_value = @object
@@ -96,13 +100,13 @@ module GraphQL
96
100
  create_result
97
101
  end
98
102
  rescue GraphQL::RuntimeError => err
99
- @graphql_result[@key] = @field_resolve_step.add_graphql_error(err)
103
+ @graphql_result[@key] = add_field_error(err)
100
104
  rescue StandardError => err
101
105
  query ||= @field_resolve_step.selections_step.query
102
106
  begin
103
107
  query.handle_or_reraise(err, field: @field_resolve_step.field_definition, arguments: @field_resolve_step.arguments, object: @object) # rubocop:disable Development/ContextIsPassedCop
104
108
  rescue GraphQL::RuntimeError => err
105
- @graphql_result[@key] = @field_resolve_step.add_graphql_error(err)
109
+ @graphql_result[@key] = add_field_error(err)
106
110
  end
107
111
  end
108
112
 
@@ -120,13 +124,13 @@ module GraphQL
120
124
  elsif @is_non_null
121
125
  @graphql_result[@key] = @field_resolve_step.add_non_null_error(@is_from_array)
122
126
  else
123
- @graphql_result[@key] = @field_resolve_step.add_graphql_error(@authorization_error)
127
+ @graphql_result[@key] = add_field_error(@authorization_error)
124
128
  end
125
129
  rescue GraphQL::RuntimeError => err
126
130
  if @is_non_null
127
131
  @graphql_result[@key] = @field_resolve_step.add_non_null_error(@is_from_array)
128
132
  else
129
- @graphql_result[@key] = @field_resolve_step.add_graphql_error(err)
133
+ @graphql_result[@key] = add_field_error(err)
130
134
  end
131
135
  end
132
136
  end
@@ -298,6 +298,7 @@ module GraphQL
298
298
  if query.query?
299
299
  isolated_steps[0] << SelectionsStep.new(
300
300
  parent_type: root_type,
301
+ field_resolve_step: nil,
301
302
  selections: selected_operation.selections,
302
303
  objects: objects,
303
304
  results: [data],
@@ -317,6 +318,7 @@ module GraphQL
317
318
  isolated_steps << [SelectionsStep.new(
318
319
  clobber: false, # `data` is being shared among several selections steps
319
320
  parent_type: root_type,
321
+ field_resolve_step: field_resolve_step,
320
322
  selections: field_resolve_step.ast_nodes || Array(field_resolve_step.ast_node),
321
323
  objects: objects,
322
324
  results: [data],
@@ -332,6 +334,7 @@ module GraphQL
332
334
  end
333
335
  isolated_steps[0] << SelectionsStep.new(
334
336
  parent_type: root_type,
337
+ field_resolve_step: nil,
335
338
  selections: selected_operation.selections,
336
339
  objects: objects,
337
340
  results: [data],
@@ -355,6 +358,7 @@ module GraphQL
355
358
  results << { "data" => data }
356
359
  isolated_steps[0] << SelectionsStep.new(
357
360
  parent_type: resolved_type,
361
+ field_resolve_step: nil,
358
362
  selections: selected_operation.selections,
359
363
  objects: objects,
360
364
  results: [data],
@@ -372,6 +376,7 @@ module GraphQL
372
376
  results << { "data" => list_result }
373
377
  isolated_steps[0] << SelectionsStep.new(
374
378
  parent_type: inner_type,
379
+ field_resolve_step: nil,
375
380
  selections: selected_operation.selections,
376
381
  objects: root_value,
377
382
  results: list_result,
@@ -420,6 +425,7 @@ module GraphQL
420
425
  selections = partial.ast_nodes
421
426
  dummy_ss = SelectionsStep.new(
422
427
  parent_type: nil,
428
+ field_resolve_step: nil,
423
429
  selections: selections,
424
430
  objects: nil,
425
431
  results: nil,
@@ -2,8 +2,9 @@
2
2
  module GraphQL
3
3
  module Execution
4
4
  class SelectionsStep
5
- def initialize(parent_type:, selections:, objects:, results:, runner:, query:, path:, clobber: true)
5
+ def initialize(parent_type:, field_resolve_step:, selections:, objects:, results:, runner:, query:, path:, clobber: true)
6
6
  @path = path
7
+ @field_resolve_step = field_resolve_step
7
8
  @parent_type = parent_type
8
9
  @selections = selections
9
10
  @runner = runner
@@ -12,10 +13,13 @@ module GraphQL
12
13
  @query = query
13
14
  @graphql_objects = nil
14
15
  @all_selections = nil
16
+ @killed = false
15
17
  @clobber = clobber
16
18
  end
17
19
 
18
- attr_reader :path, :query, :objects, :results
20
+ attr_reader :path, :query, :objects, :results, :field_resolve_step
21
+
22
+ attr_accessor :killed
19
23
 
20
24
  def graphql_objects
21
25
  @graphql_objects ||= @objects.map do |obj|
@@ -26,6 +26,10 @@ module GraphQL
26
26
  end
27
27
 
28
28
  def finalize_graphql_result(query, result_data, key)
29
+ add_this_error = !query.context.errors.any? { |e| e.eql?(self) }
30
+ if add_this_error
31
+ query.context.add_error(self)
32
+ end
29
33
  if ast_node.is_a?(GraphQL::Language::Nodes::Directive)
30
34
  # This is for backwards compatibility ... what does the spec say?
31
35
  result_data.delete(key)
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "2.6.3"
3
+ VERSION = "2.6.5"
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.3
4
+ version: 2.6.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-05-26 00:00:00.000000000 Z
10
+ date: 2026-07-06 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64