Skip to content

Commit f5ae21a

Browse files
committed
Support finalizers which don't appear in the result; add some directive hooks; add Interface.resolver_methods
1 parent d77333c commit f5ae21a

13 files changed

Lines changed: 281 additions & 55 deletions

File tree

lib/graphql/execution.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class Skip < GraphQL::RuntimeError
1414
attr_accessor :path
1515
def ast_nodes=(_ignored); end
1616

17-
def assign_graphql_result(query, result_data, key)
17+
def finalize_graphql_result(query, result_data, key)
1818
result_data.delete(key)
1919
end
2020
end

lib/graphql/execution/interpreter/handles_raw_value.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class Interpreter
77
class RawValue
88
include GraphQL::Execution::Next::Finalizer
99

10-
def assign_graphql_result(query, result_data, result_key)
10+
def finalize_graphql_result(query, result_data, result_key)
1111
result_data[result_key] = @object
1212
end
1313

lib/graphql/execution/next.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def self.run_all(schema, query_options, context: {}, max_complexity: schema.max_
7070

7171
module Finalizer
7272
attr_accessor :path
73-
def assign_graphql_result(query, result_data, result_key)
73+
def finalize_graphql_result(query, result_data, result_key)
7474
raise RequiredImplementationMissingError
7575
end
7676
end

lib/graphql/execution/next/field_resolve_step.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,37 @@ def execute_field
368368
end
369369

370370
query.current_trace.begin_execute_field(@field_definition, @arguments, authorized_objects, query)
371+
372+
if @runner.uses_runtime_directives
373+
if @ast_nodes.nil? || @ast_nodes.size == 1
374+
directives = if @ast_node.directives.any?
375+
@ast_node.directives
376+
else
377+
nil
378+
end
379+
else
380+
directives = nil
381+
@ast_nodes.each do |n|
382+
if (d = n.directives).any? # rubocop:disable Development/NoneWithoutBlockCop
383+
directives ||= []
384+
directives.concat(d)
385+
end
386+
end
387+
end
388+
389+
if directives
390+
directives.each do |dir_node|
391+
if (dir_defn = @runner.runtime_directives[dir_node.name])
392+
# Skip or include won't be present
393+
result = dir_defn.resolve_field(ast_nodes, @parent_type, field_definition, authorized_objects, @arguments, ctx)
394+
if result.is_a?(Finalizer)
395+
result.path = path
396+
end
397+
end
398+
end
399+
end
400+
end
401+
371402
has_extensions = @field_definition.extensions.size > 0
372403
if has_extensions
373404
@extended = GraphQL::Schema::Field::ExtendedState.new(@arguments, authorized_objects)

lib/graphql/execution/next/runner.rb

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@ def initialize(multiplex, authorization:)
1212
@selected_operation = nil
1313
@dataloader = multiplex.context[:dataloader] ||= @schema.dataloader_class.new
1414
@resolves_lazies = @schema.resolves_lazies?
15+
16+
@runtime_directives = nil
17+
@schema.directives.each do |name, dir_class|
18+
if dir_class.runtime? && name != "if" && name != "skip"
19+
@runtime_directives ||= {}
20+
@runtime_directives[dir_class.graphql_name] = dir_class
21+
end
22+
end
23+
24+
if @runtime_directives.nil?
25+
@uses_runtime_directives = false
26+
@runtime_directives = EmptyObjects::EMPTY_HASH
27+
else
28+
@uses_runtime_directives = true
29+
end
30+
1531
@lazy_cache = resolves_lazies ? {}.compare_by_identity : nil
1632
@authorization = authorization
1733
if @authorization
@@ -21,6 +37,8 @@ def initialize(multiplex, authorization:)
2137
end
2238
end
2339

40+
attr_reader :runtime_directives, :uses_runtime_directives
41+
2442
def resolve_type(type, object, query)
2543
query.current_trace.begin_resolve_type(type, object, query.context)
2644
resolved_type, _ignored_new_value = query.resolve_type(type, object)
@@ -184,7 +202,7 @@ def execute
184202
result
185203
else
186204
data = result["data"]
187-
data = run_finalizers(data, query)
205+
data = run_finalizers(data, query, finalizers)
188206
errors = []
189207
query.context.errors.each do |err|
190208
if err.respond_to?(:to_h)
@@ -207,9 +225,10 @@ def execute
207225
Fiber[:__graphql_current_multiplex] = nil
208226
end
209227

210-
def gather_selections(type_defn, ast_selections, selections_step, query, prototype_result, into:)
228+
def gather_selections(type_defn, ast_selections, selections_step, query, all_selections, prototype_result, into:)
211229
ast_selections.each do |ast_selection|
212230
next if !directives_include?(query, ast_selection)
231+
213232
case ast_selection
214233
when GraphQL::Language::Nodes::Field
215234
key = ast_selection.alias || ast_selection.name
@@ -227,13 +246,21 @@ def gather_selections(type_defn, ast_selections, selections_step, query, prototy
227246
when GraphQL::Language::Nodes::InlineFragment
228247
type_condition = ast_selection.type&.name
229248
if type_condition.nil? || type_condition_applies?(query.context, type_defn, type_condition)
230-
gather_selections(type_defn, ast_selection.selections, selections_step, query, prototype_result, into: into)
249+
if uses_runtime_directives && ast_selection.directives.any?
250+
all_selections << (into = { __node: ast_selection })
251+
all_selections << (prototype_result = {})
252+
end
253+
gather_selections(type_defn, ast_selection.selections, selections_step, query, all_selections, prototype_result, into: into)
231254
end
232255
when GraphQL::Language::Nodes::FragmentSpread
233-
fragment_definition = query.document.definitions.find { |defn| defn.is_a?(GraphQL::Language::Nodes::FragmentDefinition) && defn.name == ast_selection.name }
256+
fragment_definition = query.fragments[ast_selection.name]
234257
type_condition = fragment_definition.type.name
235258
if type_condition_applies?(query.context, type_defn, type_condition)
236-
gather_selections(type_defn, fragment_definition.selections, selections_step, query, prototype_result, into: into)
259+
if uses_runtime_directives && ast_selection.directives.any?
260+
all_selections << (into = { __node: ast_selection })
261+
all_selections << (prototype_result = {})
262+
end
263+
gather_selections(type_defn, fragment_definition.selections, selections_step, query, all_selections, prototype_result, into: into)
237264
end
238265
else
239266
raise ArgumentError, "Unsupported graphql selection node: #{ast_selection.class} (#{ast_selection.inspect})"
@@ -252,8 +279,8 @@ def lazy?(object)
252279

253280
private
254281

255-
def run_finalizers(data, query)
256-
paths_to_check = query.finalizers.map(&:path)
282+
def run_finalizers(data, query, finalizers)
283+
paths_to_check = finalizers.map(&:path)
257284
paths_to_check.compact! # root-level auth errors currently come without a path
258285
# TODO dry with above?
259286
# This is also where a query-level "Step" would be used?
@@ -266,11 +293,19 @@ def run_finalizers(data, query)
266293
when "subscription"
267294
query.schema.subscription
268295
end
269-
check_object_result(query, data, root_type, selected_operation.selections, [], [], paths_to_check)
296+
paths_to_check.each_with_index do |path_to_check, idx|
297+
if path_to_check.empty?
298+
finalizer = finalizers[idx]
299+
# Path is already `[]`
300+
finalizer.finalize_graphql_result(query, data, nil)
301+
finalizers[idx] = nil
302+
end
303+
end
304+
check_object_result(query, data, root_type, selected_operation.selections, [], [], paths_to_check, finalizers)
270305
end
271306
end
272307

273-
def check_object_result(query, result_h, static_type, ast_selections, current_exec_path, current_result_path, paths_to_check)
308+
def check_object_result(query, result_h, static_type, ast_selections, current_exec_path, current_result_path, paths_to_check, finalizers) # rubocop:disable Metrics/ParameterLists
274309
current_path_len = current_exec_path.length
275310
ast_selections.each do |ast_selection|
276311
case ast_selection
@@ -279,7 +314,15 @@ def check_object_result(query, result_h, static_type, ast_selections, current_ex
279314
key = ast_selection.alias || ast_selection.name
280315
current_exec_path << key
281316
current_result_path << key
282-
if paths_to_check.any? { |path_to_check| path_to_check[current_path_len] == key }
317+
this_finalizer_idx = nil
318+
should_continue = false
319+
paths_to_check.each_with_index do |path_to_check, idx|
320+
if this_finalizer_idx.nil? && current_exec_path == path_to_check
321+
this_finalizer_idx = idx
322+
end
323+
should_continue ||= path_to_check[current_path_len] == key
324+
end
325+
if should_continue
283326
result_value = result_h[key]
284327
field_defn = query.context.types.field(static_type, ast_selection.name)
285328
result_type = field_defn.type
@@ -289,13 +332,20 @@ def check_object_result(query, result_h, static_type, ast_selections, current_ex
289332

290333
new_result_value = if result_value.is_a?(Finalizer)
291334
result_value.path = current_result_path.dup
292-
result_value.assign_graphql_result(query, result_h, key)
335+
result_value.finalize_graphql_result(query, result_h, key)
293336
result_h.key?(key) ? result_h[key] : :unassigned
337+
elsif this_finalizer_idx
338+
if (finalizer = finalizers[this_finalizer_idx])
339+
finalizer.path = current_result_path.dup
340+
finalizer.finalize_graphql_result(query, result_h, key)
341+
finalizers[this_finalizer_idx] = nil
342+
end
343+
result_value
294344
else
295345
if result_type.list?
296-
check_list_result(query, result_value, result_type.of_type, ast_selection.selections, current_exec_path, current_result_path, paths_to_check)
346+
check_list_result(query, result_value, result_type.of_type, ast_selection.selections, current_exec_path, current_result_path, paths_to_check, finalizers)
297347
elsif !result_type.kind.leaf?
298-
check_object_result(query, result_value, result_type, ast_selection.selections, current_exec_path, current_result_path, paths_to_check)
348+
check_object_result(query, result_value, result_type, ast_selection.selections, current_exec_path, current_result_path, paths_to_check, finalizers)
299349
else
300350
result_value
301351
end
@@ -315,22 +365,22 @@ def check_object_result(query, result_h, static_type, ast_selections, current_ex
315365
end
316366
when Language::Nodes::InlineFragment
317367
static_type_at_result = @static_type_at[result_h]
318-
if static_type_at_result && type_condition_applies?(query.context, static_type_at_result, ast_selection.type.name)
319-
result_h = check_object_result(query, result_h, static_type, ast_selection.selections, current_exec_path, current_result_path, paths_to_check)
368+
if static_type_at_result && ((t = ast_selection.type).nil? || type_condition_applies?(query.context, static_type_at_result, t.name))
369+
result_h = check_object_result(query, result_h, static_type, ast_selection.selections, current_exec_path, current_result_path, paths_to_check, finalizers)
320370
end
321371
when Language::Nodes::FragmentSpread
322372
fragment_defn = query.document.definitions.find { |defn| defn.is_a?(Language::Nodes::FragmentDefinition) && defn.name == ast_selection.name }
323373
static_type_at_result = @static_type_at[result_h]
324374
if static_type_at_result && type_condition_applies?(query.context, static_type_at_result, fragment_defn.type.name)
325-
result_h = check_object_result(query, result_h, static_type, fragment_defn.selections, current_exec_path, current_result_path, paths_to_check)
375+
result_h = check_object_result(query, result_h, static_type, fragment_defn.selections, current_exec_path, current_result_path, paths_to_check, finalizers)
326376
end
327377
end
328378
end
329379

330380
result_h
331381
end
332382

333-
def check_list_result(query, result_arr, inner_type, ast_selections, current_exec_path, current_result_path, paths_to_check)
383+
def check_list_result(query, result_arr, inner_type, ast_selections, current_exec_path, current_result_path, paths_to_check, finalizers) # rubocop:disable Metrics/ParameterLists
334384
inner_type_non_null = false
335385
if inner_type.non_null?
336386
inner_type_non_null = true
@@ -342,12 +392,12 @@ def check_list_result(query, result_arr, inner_type, ast_selections, current_exe
342392
current_result_path << idx
343393
new_result = if result_item.is_a?(Finalizer)
344394
result_item.path = current_result_path.dup
345-
result_item.assign_graphql_result(query, result_arr, idx)
395+
result_item.finalize_graphql_result(query, result_arr, idx)
346396
result_arr[idx]
347397
elsif inner_type.list?
348-
check_list_result(query, result_item, inner_type.of_type, ast_selections, current_exec_path, current_result_path, paths_to_check)
398+
check_list_result(query, result_item, inner_type.of_type, ast_selections, current_exec_path, current_result_path, paths_to_check, finalizers)
349399
elsif !inner_type.kind.leaf?
350-
check_object_result(query, result_item, inner_type, ast_selections, current_exec_path, current_result_path, paths_to_check)
400+
check_object_result(query, result_item, inner_type, ast_selections, current_exec_path, current_result_path, paths_to_check, finalizers)
351401
else
352402
result_item
353403
end

lib/graphql/execution/next/selections_step.rb

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,39 @@ def graphql_objects
2323
end
2424

2525
def call
26-
grouped_selections = {}
27-
prototype_result = @results.first
28-
@runner.gather_selections(@parent_type, @selections, self, self.query, prototype_result, into: grouped_selections)
29-
@results.each { |r| r.replace(prototype_result) }
30-
grouped_selections.each_value do |frs|
31-
@runner.add_step(frs)
26+
all_selections = [{}, {}]
27+
@runner.gather_selections(@parent_type, @selections, self, self.query, all_selections, all_selections[1], into: all_selections[0])
28+
replaced = false
29+
all_selections.each_slice(2) do |(grouped_selections, prototype_result)|
30+
if !replaced
31+
replaced = true
32+
@results.each { |r| r.replace(prototype_result) }
33+
else
34+
# TODO -- this is here to keep response order the same.
35+
# Should there only be a single prototype_result instead?
36+
# Or should it not do this here?
37+
@results.each { |r| r.merge!(prototype_result) }
38+
end
39+
if (directives_owner = grouped_selections.delete(:__node))
40+
directives = directives_owner.directives
41+
directives.each do |dir_node|
42+
dir_defn = @runner.runtime_directives[dir_node.name]
43+
result = case directives_owner
44+
when Language::Nodes::FragmentSpread
45+
dir_defn.resolve_fragment_spread(directives_owner, @parent_type, @objects, self.query.context)
46+
when Language::Nodes::InlineFragment
47+
dir_defn.resolve_inline_fragment(directives_owner, @parent_type, @objects, self.query.context)
48+
else
49+
raise ArgumentError, "Unhandled directive owner (#{directives_owner.class}): #{directives_owner.inspect}"
50+
end
51+
if result.is_a?(Finalizer)
52+
result.path = path
53+
end
54+
end
55+
end
56+
grouped_selections.each_value do |frs|
57+
@runner.add_step(frs)
58+
end
3259
end
3360
end
3461
end

lib/graphql/execution_error.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def initialize(message, ast_node: nil, ast_nodes: nil, options: nil, extensions:
3636
super(message)
3737
end
3838

39-
def assign_graphql_result(query, result_data, key)
39+
def finalize_graphql_result(query, result_data, key)
4040
result_data[key] = nil
4141
end
4242

lib/graphql/schema/directive.rb

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,25 @@ def default_graphql_name
3131

3232
def locations(*new_locations)
3333
if !new_locations.empty?
34+
is_runtime = false
3435
new_locations.each do |new_loc|
35-
if !LOCATIONS.include?(new_loc.to_sym)
36+
loc_sym = new_loc.to_sym
37+
if !LOCATIONS.include?(loc_sym)
3638
raise ArgumentError, "#{self} (#{self.graphql_name}) has an invalid directive location: `locations #{new_loc}` "
3739
end
40+
is_runtime ||= RUNTIME_LOCATIONS.include?(loc_sym)
3841
end
3942
@locations = new_locations
43+
@is_runtime = is_runtime
4044
else
4145
@locations ||= (superclass.respond_to?(:locations) ? superclass.locations : [])
4246
end
4347
end
4448

49+
def runtime?
50+
@is_runtime
51+
end
52+
4553
def default_directive(new_default_directive = nil)
4654
if new_default_directive != nil
4755
@default_directive = new_default_directive
@@ -106,6 +114,9 @@ def inherited(subclass)
106114
super
107115
subclass.class_exec do
108116
@default_graphql_name ||= nil
117+
@locations = locations
118+
@is_runtime = runtime?
119+
@repeatable = false
109120
end
110121
end
111122
end
@@ -177,13 +188,16 @@ def graphql_name
177188
end
178189

179190
LOCATIONS = [
180-
QUERY = :QUERY,
181-
MUTATION = :MUTATION,
182-
SUBSCRIPTION = :SUBSCRIPTION,
183-
FIELD = :FIELD,
184-
FRAGMENT_DEFINITION = :FRAGMENT_DEFINITION,
185-
FRAGMENT_SPREAD = :FRAGMENT_SPREAD,
186-
INLINE_FRAGMENT = :INLINE_FRAGMENT,
191+
*(RUNTIME_LOCATIONS = [
192+
QUERY = :QUERY,
193+
MUTATION = :MUTATION,
194+
SUBSCRIPTION = :SUBSCRIPTION,
195+
FIELD = :FIELD,
196+
FRAGMENT_DEFINITION = :FRAGMENT_DEFINITION,
197+
FRAGMENT_SPREAD = :FRAGMENT_SPREAD,
198+
INLINE_FRAGMENT = :INLINE_FRAGMENT,
199+
VARIABLE_DEFINITION = :VARIABLE_DEFINITION,
200+
]),
187201
SCHEMA = :SCHEMA,
188202
SCALAR = :SCALAR,
189203
OBJECT = :OBJECT,
@@ -195,7 +209,6 @@ def graphql_name
195209
ENUM_VALUE = :ENUM_VALUE,
196210
INPUT_OBJECT = :INPUT_OBJECT,
197211
INPUT_FIELD_DEFINITION = :INPUT_FIELD_DEFINITION,
198-
VARIABLE_DEFINITION = :VARIABLE_DEFINITION,
199212
]
200213

201214
DEFAULT_DEPRECATION_REASON = 'No longer supported'

0 commit comments

Comments
 (0)