Skip to content

Commit be22067

Browse files
committed
refactor(DeferredExecution) use IRep nodes instead of AST; properly propagate non-null errors
1 parent 010b9e5 commit be22067

File tree

6 files changed

+47
-126
lines changed

6 files changed

+47
-126
lines changed

lib/graphql/execution.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
require "graphql/execution/deferred_execution"
22
require "graphql/execution/directive_checks"
3-
require "graphql/execution/selection_on_type"
43
require "graphql/execution/typecast"

lib/graphql/execution/deferred_execution.rb

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,14 @@ class DeferredExecution
6666
# @return [Object] the initial result, without any defers
6767
def execute(ast_operation, root_type, query_object)
6868
collector = query_object.context[CONTEXT_PATCH_TARGET]
69+
irep_root = query_object.internal_representation[ast_operation.name]
6970

7071
scope = ExecScope.new(query_object)
7172
initial_thread = ExecThread.new
7273
initial_frame = ExecFrame.new(
73-
node: ast_operation,
74+
node: irep_root,
7475
value: query_object.root_value,
75-
type: root_type,
76+
type: irep_root.return_type,
7677
path: []
7778
)
7879

@@ -194,7 +195,7 @@ def initialize
194195

195196
# One step of execution. Each step in execution gets its own frame.
196197
#
197-
# - {ExecFrame#node} is the AST node which is being interpreted
198+
# - {ExecFrame#node} is the IRep node which is being interpreted
198199
# - {ExecFrame#path} is like a stack trace, it is used for patching deferred values
199200
# - {ExecFrame#value} is the object being exposed by GraphQL at this point
200201
# - {ExecFrame#type} is the GraphQL type which exposes {#value} at this point
@@ -239,15 +240,13 @@ def resolve_or_defer_frame(scope, thread, frame)
239240
# Determine this frame's result and return it
240241
# Any subselections marked as `@defer` will be deferred.
241242
def resolve_frame(scope, thread, frame)
242-
ast_node = frame.node
243+
ast_node = frame.node.ast_node
243244
case ast_node
244245
when Nodes::OperationDefinition
245246
resolve_selections(scope, thread, frame)
246247
when Nodes::Field
247248
type_defn = frame.type
248-
# Use scope because it provides dynamic fields too (like __typename)
249-
field_defn = scope.get_field(type_defn, ast_node.name)
250-
249+
field_defn = scope.get_field(type_defn, frame.node.definition.name)
251250
field_result = resolve_field_frame(scope, thread, frame, field_defn)
252251
return_type_defn = field_defn.type
253252

@@ -261,66 +260,65 @@ def resolve_frame(scope, thread, frame)
261260
else
262261
raise("No defined resolution for #{ast_node.class.name} (#{ast_node})")
263262
end
263+
rescue GraphQL::InvalidNullError => err
264+
if return_type_defn && return_type_defn.kind.non_null?
265+
raise(err)
266+
else
267+
err.parent_error? || thread.errors << err
268+
nil
269+
end
264270
end
265271

266272
# Recursively resolve selections on `outer_frame.node`.
267273
# Return a `Hash<String, Any>` of identifiers and results.
268274
# Deferred fields will be absent from the result.
269275
def resolve_selections(scope, thread, outer_frame)
270-
merged_selections = GraphQL::Execution::SelectionOnType.flatten(
271-
scope,
272-
outer_frame.value,
273-
outer_frame.type,
274-
outer_frame.node.selections,
275-
)
276+
merged_selections = outer_frame.node.children
277+
query = scope.query
276278

277-
resolved_selections = merged_selections.each_with_object({}) do |ast_selection, memo|
278-
selection_key = if ast_selection.is_a?(Nodes::Field)
279-
ast_selection.alias || ast_selection.name
280-
else
281-
ast_selection.name
279+
resolved_selections = merged_selections.each_with_object({}) do |(name, irep_selection), memo|
280+
field_applies_to_type = irep_selection.on_types.any? do |child_type|
281+
GraphQL::Execution::Typecast.compatible?(outer_frame.value, child_type, outer_frame.type, query.context)
282282
end
283+
if field_applies_to_type && !GraphQL::Execution::DirectiveChecks.skip?(irep_selection, query)
284+
selection_key = irep_selection.name
283285

284-
inner_frame = ExecFrame.new(
285-
node: ast_selection,
286-
path: outer_frame.path + [selection_key],
287-
type: outer_frame.type,
288-
value: outer_frame.value,
289-
)
286+
inner_frame = ExecFrame.new(
287+
node: irep_selection,
288+
path: outer_frame.path + [selection_key],
289+
type: outer_frame.type,
290+
value: outer_frame.value,
291+
)
290292

291-
inner_result = resolve_or_defer_frame(scope, thread, inner_frame)
292-
if inner_result != DEFERRED_RESULT
293-
memo[selection_key] = inner_result
293+
inner_result = resolve_or_defer_frame(scope, thread, inner_frame)
294+
if inner_result != DEFERRED_RESULT
295+
memo[selection_key] = inner_result
296+
end
294297
end
295298
end
296299
resolved_selections
297-
rescue GraphQL::InvalidNullError => err
298-
err.parent_error? || thread.errors << err
299-
nil
300300
end
301301

302302
# Resolve `field_defn` on `frame.node`, returning the value
303303
# of the {Field#resolve} proc.
304304
# It might be an error or an object, not ready for serialization yet.
305305
# @return [Object] the return value from `field_defn`'s resolve proc
306306
def resolve_field_frame(scope, thread, frame, field_defn)
307-
ast_node = frame.node
307+
ast_node = frame.node.ast_node
308308
type_defn = frame.type
309309
value = frame.value
310310
query = scope.query
311311

312312
# Build arguments according to query-string literals, default values, and query variables
313-
arguments = GraphQL::Query::LiteralInput.from_arguments(
314-
ast_node.arguments,
315-
field_defn.arguments,
316-
query.variables
317-
)
313+
arguments = query.arguments_for(frame.node)
318314

319315
# This is the last call in the middleware chain; it actually calls the user's resolve proc
320316
field_resolve_middleware_proc = -> (_parent_type, parent_object, field_definition, field_args, query_ctx, _next) {
321317
query_ctx.ast_node = ast_node
318+
query_ctx.irep_node = frame.node
322319
value = field_definition.resolve(parent_object, field_args, query_ctx)
323320
query_ctx.ast_node = nil
321+
query_ctx.irep_node = frame.node
324322
value
325323
}
326324

@@ -353,7 +351,7 @@ def resolve_field_frame(scope, thread, frame, field_defn)
353351
def resolve_value(scope, thread, frame, value, type_defn)
354352
if value.nil? || value.is_a?(GraphQL::ExecutionError)
355353
if type_defn.kind.non_null?
356-
raise GraphQL::InvalidNullError.new(frame.node.name, value)
354+
raise GraphQL::InvalidNullError.new(frame.node.ast_node.name, value)
357355
else
358356
nil
359357
end

lib/graphql/execution/directive_checks.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ module DirectiveChecks
1111
module_function
1212

1313
# @return [Boolean] Should this AST node be deferred?
14-
def defer?(ast_node)
15-
ast_node.directives.any? { |dir| dir.name == DEFER }
14+
def defer?(irep_node)
15+
irep_node.directives.any? { |dir| dir.on_node.ast_node == irep_node.ast_node && dir.name == DEFER }
1616
end
1717

1818
# @return [Boolean] Should this AST node be streamed?
19-
def stream?(ast_node)
20-
ast_node.directives.any? { |dir| dir.name == STREAM }
19+
def stream?(irep_node)
20+
irep_node.directives.any? { |dir| dir.name == STREAM }
2121
end
2222

2323
# This covers `@include(if:)` & `@skip(if:)`

lib/graphql/execution/selection_on_type.rb

Lines changed: 0 additions & 83 deletions
This file was deleted.

lib/graphql/internal_representation/node.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
module GraphQL
44
module InternalRepresentation
55
class Node
6-
def initialize(ast_node:, return_type: nil, on_types: Set.new, name: nil, definition: nil, children: {}, spreads: [], directives: Set.new)
6+
def initialize(ast_node:, return_type: nil, on_types: Set.new, name: nil, definition: nil, children: {}, spreads: [], directives: Set.new, on_node: nil)
77
@ast_node = ast_node
88
@return_type = return_type
99
@on_types = on_types
10+
@on_node = on_node
1011
@name = name
1112
@definition = definition
1213
@children = children
@@ -22,6 +23,8 @@ def initialize(ast_node:, return_type: nil, on_types: Set.new, name: nil, defini
2223
# @return [Set<GraphQL::Language::Nodes::Directive>]
2324
attr_reader :directives
2425

26+
# This might not be right: for example, if this irep node has multiple types or
27+
# if the `on_type` is an interface, you still need to look up the field on the runtime type.
2528
# @return [GraphQL::Field, GraphQL::Directive] The definition to use to execute this node
2629
attr_reader :definition
2730

@@ -31,6 +34,9 @@ def initialize(ast_node:, return_type: nil, on_types: Set.new, name: nil, defini
3134
# @return [GraphQL::Language::Nodes::AbstractNode] The AST node (or one of the nodes) where this was derived from
3235
attr_reader :ast_node
3336

37+
# @return [GraphQL::Rewrite::Node] The parent node, used to determin Directive owner
38+
attr_reader :on_node
39+
3440
# This may come from the previous field's return value or an explicitly-typed fragment
3541
# @example On-type from previous return value
3642
# {

lib/graphql/internal_representation/rewrite.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def validate(context)
6969
@parent_directives.last << Node.new(
7070
name: ast_node.name,
7171
ast_node: ast_node,
72+
on_node: @nodes.last,
7273
definition: context.directive_definition,
7374
)
7475
end

0 commit comments

Comments
 (0)