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 +4 -4
- data/lib/generators/graphql/field_extractor.rb +17 -1
- data/lib/graphql/analysis/query_complexity.rb +2 -2
- data/lib/graphql/dataloader/async_dataloader.rb +310 -60
- data/lib/graphql/dataloader/source.rb +18 -10
- data/lib/graphql/dataloader.rb +1 -1
- data/lib/graphql/execution/field_resolve_step.rb +71 -18
- data/lib/graphql/execution/finalize.rb +7 -8
- data/lib/graphql/execution/interpreter/runtime.rb +0 -6
- data/lib/graphql/execution/prepare_object_step.rb +8 -4
- data/lib/graphql/execution/runner.rb +6 -0
- data/lib/graphql/execution/selections_step.rb +6 -2
- data/lib/graphql/execution_error.rb +4 -0
- data/lib/graphql/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ece536c0702c8789a3a5c79de82af4bd2ff806a3209d0ac4da88fc968629d19
|
|
4
|
+
data.tar.gz: 0063f1ea552e7782029083eb027138a0725af89a65265dc643469f2227184134
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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 <<~
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
task = Async::Task.current
|
|
29
|
+
run = task.graphql_async_dataloader_run
|
|
30
|
+
trace = run.trace
|
|
7
31
|
trace&.dataloader_fiber_yield(source)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
trace&.
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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|
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
data/lib/graphql/dataloader.rb
CHANGED
|
@@ -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, @
|
|
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, @
|
|
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
|
|
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 =
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
41
|
-
|
|
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[
|
|
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
|
-
|
|
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 =
|
|
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] =
|
|
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] =
|
|
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] =
|
|
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] =
|
|
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)
|
data/lib/graphql/version.rb
CHANGED
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.
|
|
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-
|
|
10
|
+
date: 2026-07-06 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base64
|