Skip to content

Commit 0dfb1a3

Browse files
authored
Merge branch 'master' into fewer-dead-result-checks
2 parents 04eb0bb + 42fdde5 commit 0dfb1a3

File tree

9 files changed

+195
-100
lines changed

9 files changed

+195
-100
lines changed

lib/graphql/execution/interpreter/runtime.rb

Lines changed: 66 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -448,9 +448,14 @@ def evaluate_selection(result_name, field_ast_nodes_or_ast_node, owner_object, o
448448
total_args_count = field_defn.arguments(context).size
449449
if total_args_count == 0
450450
resolved_arguments = GraphQL::Execution::Interpreter::Arguments::EMPTY
451-
evaluate_selection_with_args(resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selections_result, parent_object, return_type, return_type_non_null)
451+
if field_defn.extras.size == 0
452+
evaluate_selection_with_resolved_keyword_args(
453+
NO_ARGS, resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selections_result, parent_object, return_type, return_type_non_null
454+
)
455+
else
456+
evaluate_selection_with_args(resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selections_result, parent_object, return_type, return_type_non_null)
457+
end
452458
else
453-
# TODO remove all arguments(...) usages?
454459
@query.arguments_cache.dataload_for(ast_node, field_defn, object) do |resolved_arguments|
455460
evaluate_selection_with_args(resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selections_result, parent_object, return_type, return_type_non_null)
456461
end
@@ -464,9 +469,13 @@ def evaluate_selection_with_args(arguments, field_defn, ast_node, field_ast_node
464469
next
465470
end
466471

467-
kwarg_arguments = if resolved_arguments.empty? && field_defn.extras.empty?
468-
# We can avoid allocating the `{ Symbol => Object }` hash in this case
469-
NO_ARGS
472+
kwarg_arguments = if field_defn.extras.empty?
473+
if resolved_arguments.empty?
474+
# We can avoid allocating the `{ Symbol => Object }` hash in this case
475+
NO_ARGS
476+
else
477+
resolved_arguments.keyword_arguments
478+
end
470479
else
471480
# Bundle up the extras, then make a new arguments instance
472481
# that includes the extras, too.
@@ -505,60 +514,65 @@ def evaluate_selection_with_args(arguments, field_defn, ast_node, field_ast_node
505514
resolved_arguments.keyword_arguments
506515
end
507516

508-
st = get_current_runtime_state
509-
st.current_field = field_defn
510-
st.current_object = object
511-
st.current_arguments = resolved_arguments
512-
st.current_result_name = result_name
513-
st.current_result = selection_result
514-
# Optimize for the case that field is selected only once
515-
if field_ast_nodes.nil? || field_ast_nodes.size == 1
516-
next_selections = ast_node.selections
517-
directives = ast_node.directives
518-
else
519-
next_selections = []
520-
directives = []
521-
field_ast_nodes.each { |f|
522-
next_selections.concat(f.selections)
523-
directives.concat(f.directives)
524-
}
525-
end
526-
527-
field_result = call_method_on_directives(:resolve, object, directives) do
528-
# Actually call the field resolver and capture the result
529-
app_result = begin
530-
query.current_trace.execute_field(field: field_defn, ast_node: ast_node, query: query, object: object, arguments: kwarg_arguments) do
531-
field_defn.resolve(object, kwarg_arguments, context)
532-
end
533-
rescue GraphQL::ExecutionError => err
534-
err
535-
rescue StandardError => err
536-
begin
537-
query.handle_or_reraise(err)
538-
rescue GraphQL::ExecutionError => ex_err
539-
ex_err
540-
end
517+
evaluate_selection_with_resolved_keyword_args(kwarg_arguments, resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selection_result, parent_object, return_type, return_type_non_null)
518+
end
519+
end
520+
521+
def evaluate_selection_with_resolved_keyword_args(kwarg_arguments, resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selection_result, parent_object, return_type, return_type_non_null) # rubocop:disable Metrics/ParameterLists
522+
st = get_current_runtime_state
523+
st.current_field = field_defn
524+
st.current_object = object
525+
st.current_arguments = resolved_arguments
526+
st.current_result_name = result_name
527+
st.current_result = selection_result
528+
# Optimize for the case that field is selected only once
529+
if field_ast_nodes.nil? || field_ast_nodes.size == 1
530+
next_selections = ast_node.selections
531+
directives = ast_node.directives
532+
else
533+
next_selections = []
534+
directives = []
535+
field_ast_nodes.each { |f|
536+
next_selections.concat(f.selections)
537+
directives.concat(f.directives)
538+
}
539+
end
540+
541+
field_result = call_method_on_directives(:resolve, object, directives) do
542+
# Actually call the field resolver and capture the result
543+
app_result = begin
544+
query.current_trace.execute_field(field: field_defn, ast_node: ast_node, query: query, object: object, arguments: kwarg_arguments) do
545+
field_defn.resolve(object, kwarg_arguments, context)
541546
end
542-
after_lazy(app_result, owner: owner_type, field: field_defn, ast_node: ast_node, owner_object: object, arguments: resolved_arguments, result_name: result_name, result: selection_result) do |inner_result|
543-
continue_value = continue_value(inner_result, owner_type, field_defn, return_type.non_null?, ast_node, result_name, selection_result)
544-
if HALT != continue_value
545-
continue_field(continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, resolved_arguments, result_name, selection_result)
546-
end
547+
rescue GraphQL::ExecutionError => err
548+
err
549+
rescue StandardError => err
550+
begin
551+
query.handle_or_reraise(err)
552+
rescue GraphQL::ExecutionError => ex_err
553+
ex_err
547554
end
548555
end
549-
550-
# If this field is a root mutation field, immediately resolve
551-
# all of its child fields before moving on to the next root mutation field.
552-
# (Subselections of this mutation will still be resolved level-by-level.)
553-
if is_eager_field
554-
Interpreter::Resolve.resolve_all([field_result], @dataloader)
555-
else
556-
# Return this from `after_lazy` because it might be another lazy that needs to be resolved
557-
field_result
556+
after_lazy(app_result, owner: owner_type, field: field_defn, ast_node: ast_node, owner_object: object, arguments: resolved_arguments, result_name: result_name, result: selection_result) do |inner_result|
557+
continue_value = continue_value(inner_result, owner_type, field_defn, return_type_non_null, ast_node, result_name, selection_result)
558+
if HALT != continue_value
559+
continue_field(continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, resolved_arguments, result_name, selection_result)
560+
end
558561
end
559562
end
563+
564+
# If this field is a root mutation field, immediately resolve
565+
# all of its child fields before moving on to the next root mutation field.
566+
# (Subselections of this mutation will still be resolved level-by-level.)
567+
if is_eager_field
568+
Interpreter::Resolve.resolve_all([field_result], @dataloader)
569+
else
570+
# Return this from `after_lazy` because it might be another lazy that needs to be resolved
571+
field_result
572+
end
560573
end
561574

575+
562576
def dead_result?(selection_result)
563577
selection_result.graphql_dead # || ((parent = selection_result.graphql_parent) && parent.graphql_dead)
564578
end

lib/graphql/filter.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
# frozen_string_literal: true
2+
require "graphql/deprecation"
3+
24
module GraphQL
35
# @api private
46
class Filter
5-
def initialize(only: nil, except: nil)
7+
def initialize(only: nil, except: nil, silence_deprecation_warning: false)
8+
if !silence_deprecation_warning
9+
GraphQL::Deprecation.warn("GraphQL::Filter is deprecated and will be removed in v2.1.0. Implement `visible?` on your schema members instead (https://graphql-ruby.org/authorization/visibility.html).")
10+
end
611
@only = only
712
@except = except
813
end
@@ -17,7 +22,7 @@ def merge(only: nil, except: nil)
1722
onlies = [self].concat(Array(only))
1823
merged_only = MergedOnly.build(onlies)
1924
merged_except = MergedExcept.build(Array(except))
20-
self.class.new(only: merged_only, except: merged_except)
25+
self.class.new(only: merged_only, except: merged_except, silence_deprecation_warning: true)
2126
end
2227

2328
private

lib/graphql/language/document_from_schema_definition.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def initialize(
2424
@include_built_in_directives = include_built_in_directives
2525
@include_one_of = false
2626

27-
filter = GraphQL::Filter.new(only: only, except: except)
27+
filter = GraphQL::Filter.new(only: only, except: except, silence_deprecation_warning: true)
2828
if @schema.respond_to?(:visible?)
2929
filter = filter.merge(only: @schema.method(:visible?))
3030
end

lib/graphql/query.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def initialize(schema, query_string = nil, query: nil, document: nil, context: n
105105
end
106106

107107
if context_tracers.any? && !(schema.trace_class <= GraphQL::Tracing::LegacyTrace)
108-
raise ArgumentError, "context[:tracers] and context[:backtrace] are not supported without `tracer_class(GraphQL::Tracing::LegacyTrace)` in the schema configuration, please add it."
108+
raise ArgumentError, "context[:tracers] and context[:backtrace] are not supported without `trace_class(GraphQL::Tracing::LegacyTrace)` in the schema configuration, please add it."
109109
end
110110

111111

lib/graphql/query/null_context.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def initialize
2424
@dataloader = GraphQL::Dataloader::NullDataloader.new
2525
@schema = NullSchema
2626
@warden = NullWarden.new(
27-
GraphQL::Filter.new,
27+
GraphQL::Filter.new(silence_deprecation_warning: true),
2828
context: self,
2929
schema: @schema,
3030
)

lib/graphql/schema.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ def find(path)
221221
end
222222

223223
def default_filter
224-
GraphQL::Filter.new(except: default_mask)
224+
GraphQL::Filter.new(except: default_mask, silence_deprecation_warning: true)
225225
end
226226

227227
def default_mask(new_mask = nil)

lib/graphql/schema/member/has_fields.rb

Lines changed: 80 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,42 +14,6 @@ def field(*args, **kwargs, &block)
1414
field_defn
1515
end
1616

17-
# @return [Hash<String => GraphQL::Schema::Field>] Fields on this object, keyed by name, including inherited fields
18-
def fields(context = GraphQL::Query::NullContext)
19-
warden = Warden.from_context(context)
20-
is_object = self.respond_to?(:kind) && self.kind.object?
21-
# Local overrides take precedence over inherited fields
22-
visible_fields = {}
23-
for ancestor in ancestors
24-
if ancestor.respond_to?(:own_fields) &&
25-
(is_object ? visible_interface_implementation?(ancestor, context, warden) : true)
26-
27-
ancestor.own_fields.each do |field_name, fields_entry|
28-
# Choose the most local definition that passes `.visible?` --
29-
# stop checking for fields by name once one has been found.
30-
if !visible_fields.key?(field_name) && (f = Warden.visible_entry?(:visible_field?, fields_entry, context, warden))
31-
visible_fields[field_name] = f
32-
end
33-
end
34-
end
35-
end
36-
visible_fields
37-
end
38-
39-
def get_field(field_name, context = GraphQL::Query::NullContext)
40-
warden = Warden.from_context(context)
41-
is_object = self.respond_to?(:kind) && self.kind.object?
42-
for ancestor in ancestors
43-
if ancestor.respond_to?(:own_fields) &&
44-
(is_object ? visible_interface_implementation?(ancestor, context, warden) : true) &&
45-
(f_entry = ancestor.own_fields[field_name]) &&
46-
(f = Warden.visible_entry?(:visible_field?, f_entry, context, warden))
47-
return f
48-
end
49-
end
50-
nil
51-
end
52-
5317
# A list of Ruby keywords.
5418
#
5519
# @api private
@@ -132,6 +96,86 @@ def all_field_definitions
13296
all_fields
13397
end
13498

99+
module InterfaceMethods
100+
def get_field(field_name, context = GraphQL::Query::NullContext)
101+
warden = Warden.from_context(context)
102+
for ancestor in ancestors
103+
if ancestor.respond_to?(:own_fields) &&
104+
(f_entry = ancestor.own_fields[field_name]) &&
105+
(f = Warden.visible_entry?(:visible_field?, f_entry, context, warden))
106+
return f
107+
end
108+
end
109+
nil
110+
end
111+
112+
# @return [Hash<String => GraphQL::Schema::Field>] Fields on this object, keyed by name, including inherited fields
113+
def fields(context = GraphQL::Query::NullContext)
114+
warden = Warden.from_context(context)
115+
# Local overrides take precedence over inherited fields
116+
visible_fields = {}
117+
for ancestor in ancestors
118+
if ancestor.respond_to?(:own_fields)
119+
ancestor.own_fields.each do |field_name, fields_entry|
120+
# Choose the most local definition that passes `.visible?` --
121+
# stop checking for fields by name once one has been found.
122+
if !visible_fields.key?(field_name) && (f = Warden.visible_entry?(:visible_field?, fields_entry, context, warden))
123+
visible_fields[field_name] = f
124+
end
125+
end
126+
end
127+
end
128+
visible_fields
129+
end
130+
end
131+
132+
module ObjectMethods
133+
def get_field(field_name, context = GraphQL::Query::NullContext)
134+
# Objects need to check that the interface implementation is visible, too
135+
warden = Warden.from_context(context)
136+
for ancestor in ancestors
137+
if ancestor.respond_to?(:own_fields) &&
138+
visible_interface_implementation?(ancestor, context, warden) &&
139+
(f_entry = ancestor.own_fields[field_name]) &&
140+
(f = Warden.visible_entry?(:visible_field?, f_entry, context, warden))
141+
return f
142+
end
143+
end
144+
nil
145+
end
146+
147+
# @return [Hash<String => GraphQL::Schema::Field>] Fields on this object, keyed by name, including inherited fields
148+
def fields(context = GraphQL::Query::NullContext)
149+
# Objects need to check that the interface implementation is visible, too
150+
warden = Warden.from_context(context)
151+
# Local overrides take precedence over inherited fields
152+
visible_fields = {}
153+
for ancestor in ancestors
154+
if ancestor.respond_to?(:own_fields) && visible_interface_implementation?(ancestor, context, warden)
155+
ancestor.own_fields.each do |field_name, fields_entry|
156+
# Choose the most local definition that passes `.visible?` --
157+
# stop checking for fields by name once one has been found.
158+
if !visible_fields.key?(field_name) && (f = Warden.visible_entry?(:visible_field?, fields_entry, context, warden))
159+
visible_fields[field_name] = f
160+
end
161+
end
162+
end
163+
end
164+
visible_fields
165+
end
166+
end
167+
168+
def self.included(child_class)
169+
# Included in an interface definition methods module
170+
child_class.include(InterfaceMethods)
171+
super
172+
end
173+
174+
def self.extended(child_class)
175+
child_class.extend(ObjectMethods)
176+
super
177+
end
178+
135179
private
136180

137181
def inherited(subclass)

lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,19 @@ def validate_field_selections(ast_node, resolved_type)
2626
msg = if resolved_type.nil?
2727
nil
2828
elsif resolved_type.kind.scalar? && ast_node.selections.any?
29-
if ast_node.selections.first.is_a?(GraphQL::Language::Nodes::InlineFragment)
30-
"Selections can't be made on scalars (%{node_name} returns #{resolved_type.graphql_name} but has inline fragments [#{ast_node.selections.map(&:type).map(&:name).join(", ")}])"
31-
else
32-
"Selections can't be made on scalars (%{node_name} returns #{resolved_type.graphql_name} but has selections [#{ast_node.selections.map(&:name).join(", ")}])"
29+
selection_strs = ast_node.selections.map do |n|
30+
case n
31+
when GraphQL::Language::Nodes::InlineFragment
32+
"\"... on #{n.type.name} { ... }\""
33+
when GraphQL::Language::Nodes::Field
34+
"\"#{n.name}\""
35+
when GraphQL::Language::Nodes::FragmentSpread
36+
"\"#{n.name}\""
37+
else
38+
raise "Invariant: unexpected selection node: #{n}"
39+
end
3340
end
41+
"Selections can't be made on scalars (%{node_name} returns #{resolved_type.graphql_name} but has selections [#{selection_strs.join(", ")}])"
3442
elsif resolved_type.kind.fields? && ast_node.selections.empty?
3543
"Field must have selections (%{node_name} returns #{resolved_type.graphql_name} but has no selections. Did you mean '#{ast_node.name} { ... }'?)"
3644
else

0 commit comments

Comments
 (0)