graphql 2.5.19 → 2.5.20

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/dashboard/application_controller.rb +41 -0
  3. data/lib/graphql/dashboard/landings_controller.rb +9 -0
  4. data/lib/graphql/dashboard/statics_controller.rb +31 -0
  5. data/lib/graphql/dashboard/subscriptions.rb +2 -1
  6. data/lib/graphql/dashboard.rb +9 -74
  7. data/lib/graphql/dataloader/null_dataloader.rb +7 -3
  8. data/lib/graphql/execution/batching/field_compatibility.rb +150 -0
  9. data/lib/graphql/execution/batching/field_resolve_step.rb +408 -0
  10. data/lib/graphql/execution/batching/prepare_object_step.rb +112 -0
  11. data/lib/graphql/execution/batching/runner.rb +352 -0
  12. data/lib/graphql/execution/batching/selections_step.rb +37 -0
  13. data/lib/graphql/execution/batching.rb +62 -0
  14. data/lib/graphql/execution_error.rb +13 -10
  15. data/lib/graphql/introspection/directive_type.rb +7 -3
  16. data/lib/graphql/introspection/entry_points.rb +6 -2
  17. data/lib/graphql/introspection/enum_value_type.rb +5 -5
  18. data/lib/graphql/introspection/field_type.rb +13 -5
  19. data/lib/graphql/introspection/input_value_type.rb +21 -13
  20. data/lib/graphql/introspection/type_type.rb +64 -28
  21. data/lib/graphql/invalid_null_error.rb +11 -5
  22. data/lib/graphql/query/context.rb +3 -2
  23. data/lib/graphql/query/null_context.rb +9 -3
  24. data/lib/graphql/schema/argument.rb +4 -0
  25. data/lib/graphql/schema/build_from_definition.rb +26 -6
  26. data/lib/graphql/schema/field.rb +76 -1
  27. data/lib/graphql/schema/member/has_dataloader.rb +9 -0
  28. data/lib/graphql/schema/member/has_fields.rb +3 -0
  29. data/lib/graphql/schema/resolver.rb +3 -1
  30. data/lib/graphql/schema/visibility.rb +1 -1
  31. data/lib/graphql/schema.rb +33 -7
  32. data/lib/graphql/subscriptions.rb +1 -1
  33. data/lib/graphql/tracing/perfetto_trace.rb +1 -1
  34. data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
  35. data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
  36. data/lib/graphql/types/relay/has_node_field.rb +5 -8
  37. data/lib/graphql/types/relay/has_nodes_field.rb +5 -8
  38. data/lib/graphql/unauthorized_error.rb +5 -1
  39. data/lib/graphql/version.rb +1 -1
  40. data/lib/graphql.rb +3 -0
  41. metadata +11 -2
@@ -0,0 +1,408 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module Execution
4
+ module Batching
5
+ class FieldResolveStep
6
+ def initialize(parent_type:, runner:, key:, selections_step:)
7
+ @selections_step = selections_step
8
+ @key = key
9
+ @parent_type = parent_type
10
+ @ast_node = @ast_nodes = nil
11
+ @runner = runner
12
+ @field_definition = nil
13
+ @arguments = nil
14
+ @field_results = nil
15
+ @path = nil
16
+ @enqueued_authorization = false
17
+ @pending_authorize_steps_count = 0
18
+ @all_next_objects = nil
19
+ @all_next_results = nil
20
+ @static_type = nil
21
+ @next_selections = nil
22
+ @object_is_authorized = nil
23
+ end
24
+
25
+ attr_reader :ast_node, :key, :parent_type, :selections_step, :runner, :field_definition, :object_is_authorized, :arguments
26
+
27
+ def path
28
+ @path ||= [*@selections_step.path, @key].freeze
29
+ end
30
+
31
+ def ast_nodes
32
+ @ast_nodes ||= [@ast_node]
33
+ end
34
+
35
+ def append_selection(ast_node)
36
+ if @ast_node.nil?
37
+ @ast_node = ast_node
38
+ elsif @ast_nodes.nil?
39
+ @ast_nodes = [@ast_node, ast_node]
40
+ else
41
+ @ast_nodes << ast_node
42
+ end
43
+ nil
44
+ end
45
+
46
+ def coerce_arguments(argument_owner, ast_arguments_or_hash)
47
+ arg_defns = argument_owner.arguments(@selections_step.query.context)
48
+ if arg_defns.empty?
49
+ return EmptyObjects::EMPTY_HASH
50
+ end
51
+ args_hash = {}
52
+ if ast_arguments_or_hash.is_a?(Hash)
53
+ ast_arguments_or_hash.each do |key, value|
54
+ key_s = nil
55
+ arg_defn = arg_defns.each_value.find { |a|
56
+ a.keyword == key || a.graphql_name == (key_s ||= String(key))
57
+ }
58
+ arg_value = coerce_argument_value(arg_defn.type, value)
59
+ args_hash[arg_defn.keyword] = arg_value
60
+ end
61
+ else
62
+ ast_arguments_or_hash.each { |arg_node|
63
+ arg_defn = arg_defns[arg_node.name]
64
+ arg_value = coerce_argument_value(arg_defn.type, arg_node.value)
65
+ arg_key = arg_defn.keyword
66
+ args_hash[arg_key] = arg_value
67
+ }
68
+ end
69
+
70
+ arg_defns.each do |arg_graphql_name, arg_defn|
71
+ if arg_defn.default_value? && !args_hash.key?(arg_defn.keyword)
72
+ args_hash[arg_defn.keyword] = arg_defn.default_value
73
+ end
74
+ end
75
+
76
+ args_hash
77
+ end
78
+
79
+ def coerce_argument_value(arg_t, arg_value)
80
+ if arg_t.non_null?
81
+ arg_t = arg_t.of_type
82
+ end
83
+
84
+ if arg_value.is_a?(Language::Nodes::VariableIdentifier)
85
+ vars = @selections_step.query.variables
86
+ arg_value = if vars.key?(arg_value.name)
87
+ vars[arg_value.name]
88
+ elsif vars.key?(arg_value.name.to_sym)
89
+ vars[arg_value.name.to_sym]
90
+ end
91
+ elsif arg_value.is_a?(Language::Nodes::NullValue)
92
+ arg_value = nil
93
+ elsif arg_value.is_a?(Language::Nodes::Enum)
94
+ arg_value = arg_value.name
95
+ elsif arg_value.is_a?(Language::Nodes::InputObject)
96
+ arg_value = arg_value.arguments # rubocop:disable Development/ContextIsPassedCop
97
+ end
98
+
99
+ if arg_t.list?
100
+ if arg_value.nil?
101
+ arg_value
102
+ else
103
+ arg_value = Array(arg_value)
104
+ inner_t = arg_t.of_type
105
+ arg_value.map { |v| coerce_argument_value(inner_t, v) }
106
+ end
107
+ elsif arg_t.kind.leaf?
108
+ begin
109
+ ctx = @selections_step.query.context
110
+ arg_t.coerce_input(arg_value, ctx)
111
+ rescue GraphQL::UnauthorizedEnumValueError => enum_err
112
+ begin
113
+ @runner.schema.unauthorized_object(enum_err)
114
+ rescue GraphQL::ExecutionError => ex_err
115
+ ex_err
116
+ end
117
+ end
118
+ elsif arg_t.kind.input_object?
119
+ coerce_arguments(arg_t, arg_value)
120
+ else
121
+ raise "Unsupported argument value: #{arg_t.to_type_signature} / #{arg_value.class} (#{arg_value.inspect})"
122
+ end
123
+ end
124
+
125
+ # Implement that Lazy API
126
+ def value
127
+ @field_results = sync(@field_results)
128
+ @runner.add_step(self)
129
+ true
130
+ end
131
+
132
+ def sync(lazy)
133
+ if lazy.is_a?(Array)
134
+ lazy.map! { |l| sync(l)}
135
+ else
136
+ @runner.schema.sync_lazy(lazy)
137
+ end
138
+ rescue GraphQL::UnauthorizedError => auth_err
139
+ @runner.schema.unauthorized_object(auth_err)
140
+ rescue GraphQL::ExecutionError => err
141
+ err
142
+ end
143
+
144
+ def call
145
+ if @enqueued_authorization && @pending_authorize_steps_count == 0
146
+ enqueue_next_steps
147
+ elsif @field_results
148
+ build_results
149
+ else
150
+ execute_field
151
+ end
152
+ end
153
+
154
+ def add_graphql_error(err)
155
+ err.path = path
156
+ err.ast_nodes = ast_nodes
157
+ @selections_step.query.context.add_error(err)
158
+ err
159
+ end
160
+
161
+ module AlwaysAuthorized
162
+ def self.[](_key)
163
+ true
164
+ end
165
+ end
166
+
167
+ def execute_field
168
+ field_name = @ast_node.name
169
+ @field_definition = @selections_step.query.get_field(@parent_type, field_name) || raise("Invariant: no field found for #{@parent_type.to_type_signature}.#{ast_node.name}")
170
+ objects = @selections_step.objects
171
+ if field_name == "__typename"
172
+ # TODO handle custom introspection
173
+ @field_results = Array.new(objects.size, @parent_type.graphql_name)
174
+ @object_is_authorized = AlwaysAuthorized
175
+ build_results
176
+ return
177
+ end
178
+
179
+ @arguments = coerce_arguments(@field_definition, @ast_node.arguments) # rubocop:disable Development/ContextIsPassedCop
180
+
181
+
182
+ ctx = @selections_step.query.context
183
+
184
+ if (@runner.authorizes.fetch(@field_definition) { @runner.authorizes[@field_definition] = @field_definition.authorizes?(ctx) })
185
+ authorized_objects = []
186
+ @object_is_authorized = objects.map { |o|
187
+ is_authed = @field_definition.authorized?(o, @arguments, ctx)
188
+ if is_authed
189
+ authorized_objects << o
190
+ end
191
+ is_authed
192
+ }
193
+ else
194
+ authorized_objects = objects
195
+ @object_is_authorized = AlwaysAuthorized
196
+ end
197
+
198
+ @field_results = @field_definition.resolve_batch(self, authorized_objects, ctx, @arguments)
199
+
200
+ if @runner.resolves_lazies # TODO extract this
201
+ lazies = false
202
+ @field_results.each do |field_result|
203
+ if @runner.schema.lazy?(field_result)
204
+ lazies = true
205
+ break
206
+ elsif field_result.is_a?(Array)
207
+ field_result.each do |inner_fr|
208
+ if @runner.schema.lazy?(inner_fr)
209
+ break lazies = true
210
+ end
211
+ end
212
+ if lazies
213
+ break
214
+ end
215
+ end
216
+ end
217
+
218
+ if lazies
219
+ @runner.dataloader.lazy_at_depth(path.size, self)
220
+ else
221
+ build_results
222
+ end
223
+ else
224
+ build_results
225
+ end
226
+ end
227
+
228
+ def build_results
229
+ return_type = @field_definition.type
230
+ return_result_type = return_type.unwrap
231
+
232
+ if return_result_type.kind.composite?
233
+ @static_type = return_result_type
234
+ if @ast_nodes
235
+ @next_selections = []
236
+ @ast_nodes.each do |ast_node|
237
+ @next_selections.concat(ast_node.selections)
238
+ end
239
+ else
240
+ @next_selections = @ast_node.selections
241
+ end
242
+
243
+ @all_next_objects = []
244
+ @all_next_results = []
245
+
246
+ is_list = return_type.list?
247
+ is_non_null = return_type.non_null?
248
+ results = @selections_step.results
249
+ field_result_idx = 0
250
+ results.each_with_index do |result_h, i|
251
+ if @object_is_authorized[i]
252
+ result = @field_results[field_result_idx]
253
+ field_result_idx += 1
254
+ else
255
+ result = nil
256
+ end
257
+ build_graphql_result(result_h, @key, result, return_type, is_non_null, is_list, false)
258
+ end
259
+ @enqueued_authorization = true
260
+
261
+ if @pending_authorize_steps_count == 0
262
+ enqueue_next_steps
263
+ else
264
+ # Do nothing -- it will enqueue itself later
265
+ end
266
+ else
267
+ results = @selections_step.results
268
+ ctx = @selections_step.query.context
269
+ field_result_idx = 0
270
+ results.each_with_index do |result_h, i|
271
+ if @object_is_authorized[i]
272
+ field_result = @field_results[field_result_idx]
273
+ field_result_idx += 1
274
+ else
275
+ field_result = nil
276
+ end
277
+ result_h[@key] = if field_result.nil?
278
+ if return_type.non_null?
279
+ add_non_null_error(false)
280
+ else
281
+ nil
282
+ end
283
+ elsif field_result.is_a?(GraphQL::Error)
284
+ add_graphql_error(field_result)
285
+ else
286
+ # TODO `nil`s in [T!] types aren't handled
287
+ return_type.coerce_result(field_result, ctx)
288
+ end
289
+ end
290
+ end
291
+ end
292
+
293
+ def enqueue_next_steps
294
+ if !@all_next_results.empty?
295
+ @all_next_objects.compact!
296
+
297
+ if @static_type.kind.abstract?
298
+ next_objects_by_type = Hash.new { |h, obj_t| h[obj_t] = [] }.compare_by_identity
299
+ next_results_by_type = Hash.new { |h, obj_t| h[obj_t] = [] }.compare_by_identity
300
+
301
+ ctx = nil
302
+ @all_next_objects.each_with_index do |next_object, i|
303
+ result = @all_next_results[i]
304
+ if (object_type = @runner.runtime_types_at_result[result])
305
+ # OK
306
+ else
307
+ ctx ||= @selections_step.query.context
308
+ object_type, _unused_new_value = @runner.schema.resolve_type(@static_type, next_object, ctx)
309
+ end
310
+ next_objects_by_type[object_type] << next_object
311
+ next_results_by_type[object_type] << result
312
+ end
313
+
314
+ next_objects_by_type.each do |obj_type, next_objects|
315
+ @runner.add_step(SelectionsStep.new(
316
+ path: path,
317
+ parent_type: obj_type,
318
+ selections: @next_selections,
319
+ objects: next_objects,
320
+ results: next_results_by_type[obj_type],
321
+ runner: @runner,
322
+ query: @selections_step.query,
323
+ ))
324
+ end
325
+ else
326
+ @runner.add_step(SelectionsStep.new(
327
+ path: path,
328
+ parent_type: @static_type,
329
+ selections: @next_selections,
330
+ objects: @all_next_objects,
331
+ results: @all_next_results,
332
+ runner: @runner,
333
+ query: @selections_step.query,
334
+ ))
335
+ end
336
+ end
337
+ end
338
+
339
+ def authorized_finished
340
+ remaining = @pending_authorize_steps_count -= 1
341
+ if @enqueued_authorization && remaining == 0
342
+ @runner.add_step(self)
343
+ end
344
+ end
345
+
346
+ def add_non_null_error(is_from_array)
347
+ err = InvalidNullError.new(@parent_type, @field_definition, ast_nodes, is_from_array: is_from_array, path: path)
348
+ @runner.schema.type_error(err, @selections_step.query.context)
349
+ end
350
+
351
+ private
352
+
353
+ def build_graphql_result(graphql_result, key, field_result, return_type, is_nn, is_list, is_from_array) # rubocop:disable Metrics/ParameterLists
354
+ if field_result.nil?
355
+ if is_nn
356
+ graphql_result[key] = add_non_null_error(is_from_array)
357
+ else
358
+ graphql_result[key] = nil
359
+ end
360
+ elsif field_result.is_a?(GraphQL::Error)
361
+ graphql_result[key] = add_graphql_error(field_result)
362
+ elsif is_list
363
+ if is_nn
364
+ return_type = return_type.of_type
365
+ end
366
+ inner_type = return_type.of_type
367
+ inner_type_nn = inner_type.non_null?
368
+ inner_type_l = inner_type.list?
369
+ list_result = graphql_result[key] = []
370
+ field_result.each_with_index do |inner_f_r, i|
371
+ build_graphql_result(list_result, i, inner_f_r, inner_type, inner_type_nn, inner_type_l, true)
372
+ end
373
+ elsif @runner.resolves_lazies # Handle possible lazy resolve_type response
374
+ @pending_authorize_steps_count += 1
375
+ @runner.add_step(Batching::PrepareObjectStep.new(
376
+ static_type: @static_type,
377
+ object: field_result,
378
+ runner: @runner,
379
+ field_resolve_step: self,
380
+ graphql_result: graphql_result,
381
+ next_objects: @all_next_objects,
382
+ next_results: @all_next_results,
383
+ is_non_null: is_nn,
384
+ key: key,
385
+ is_from_array: is_from_array,
386
+ ))
387
+ else
388
+ next_result_h = {}
389
+ @all_next_results << next_result_h
390
+ @all_next_objects << field_result
391
+ @runner.static_types_at_result[next_result_h] = @static_type
392
+ graphql_result[key] = next_result_h
393
+ end
394
+ end
395
+ end
396
+
397
+ class RawValueFieldResolveStep < FieldResolveStep
398
+ def build_graphql_result(graphql_result, key, field_result, return_type, is_nn, is_list, is_from_array) # rubocop:disable Metrics/ParameterLists
399
+ if field_result.is_a?(Interpreter::RawValue)
400
+ graphql_result[key] = field_result.resolve
401
+ else
402
+ super
403
+ end
404
+ end
405
+ end
406
+ end
407
+ end
408
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module Execution
4
+ module Batching
5
+ class PrepareObjectStep
6
+ def initialize(static_type:, object:, runner:, graphql_result:, key:, is_non_null:, field_resolve_step:, next_objects:, next_results:, is_from_array:)
7
+ @static_type = static_type
8
+ @object = object
9
+ @runner = runner
10
+ @field_resolve_step = field_resolve_step
11
+ @is_non_null = is_non_null
12
+ @next_objects = next_objects
13
+ @next_results = next_results
14
+ @graphql_result = graphql_result
15
+ @resolved_type = nil
16
+ @authorized_value = nil
17
+ @authorization_error = nil
18
+ @key = key
19
+ @next_step = :resolve_type
20
+ @is_from_array = is_from_array
21
+ end
22
+
23
+ def value
24
+ if @authorized_value
25
+ @authorized_value = @field_resolve_step.sync(@authorized_value)
26
+ elsif @resolved_type
27
+ @resolved_type, _ignored_value = @field_resolve_step.sync(@resolved_type)
28
+ end
29
+ @runner.add_step(self)
30
+ end
31
+
32
+ def call
33
+ case @next_step
34
+ when :resolve_type
35
+ if @static_type.kind.abstract?
36
+ @resolved_type, _ignored_value = @runner.schema.resolve_type(@static_type, @object, @field_resolve_step.selections_step.query.context)
37
+ else
38
+ @resolved_type = @static_type
39
+ end
40
+ if @runner.resolves_lazies && @runner.schema.lazy?(@resolved_type)
41
+ @next_step = :authorize
42
+ @runner.dataloader.lazy_at_depth(@field_resolve_step.path.size, self)
43
+ else
44
+ authorize
45
+ end
46
+ when :authorize
47
+ authorize
48
+ when :create_result
49
+ create_result
50
+ else
51
+ raise ArgumentError, "This is a bug, unknown step: #{@next_step.inspect}"
52
+ end
53
+ end
54
+
55
+ def authorize
56
+ ctx = @field_resolve_step.selections_step.query.context
57
+ begin
58
+ @authorized_value = @resolved_type.authorized?(@object, ctx)
59
+ rescue GraphQL::UnauthorizedError => auth_err
60
+ @authorization_error = auth_err
61
+ end
62
+
63
+ if @runner.resolves_lazies && @runner.schema.lazy?(@authorized_value)
64
+ @runner.dataloader.lazy_at_depth(@field_resolve_step.path.size, self)
65
+ @next_step = :create_result
66
+ else
67
+ create_result
68
+ end
69
+ rescue GraphQL::Error => err
70
+ @graphql_result[@key] = @field_resolve_step.add_graphql_error(err)
71
+ end
72
+
73
+ def create_result
74
+ if !@authorized_value
75
+ @authorization_error ||= GraphQL::UnauthorizedError.new(object: @object, type: @resolved_type, context: @field_resolve_step.selections_step.query.context)
76
+ end
77
+
78
+ if @authorization_error
79
+ begin
80
+ new_obj = @runner.schema.unauthorized_object(@authorization_error)
81
+ if new_obj
82
+ @authorized_value = true
83
+ @object = new_obj
84
+ elsif @is_non_null
85
+ @graphql_result[@key] = @field_resolve_step.add_non_null_error(@is_from_array)
86
+ else
87
+ @graphql_result[@key] = @field_resolve_step.add_graphql_error(@authorization_error)
88
+ end
89
+ rescue GraphQL::Error => err
90
+ if @is_non_null
91
+ @graphql_result[@key] = @field_resolve_step.add_non_null_error(@is_from_array)
92
+ else
93
+ @graphql_result[@key] = @field_resolve_step.add_graphql_error(err)
94
+ end
95
+ end
96
+ end
97
+
98
+ if @authorized_value
99
+ next_result_h = {}
100
+ @next_results << next_result_h
101
+ @next_objects << @object
102
+ @graphql_result[@key] = next_result_h
103
+ @runner.runtime_types_at_result[next_result_h] = @resolved_type
104
+ @runner.static_types_at_result[next_result_h] = @static_type
105
+ end
106
+
107
+ @field_resolve_step.authorized_finished
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end