Skip to content

Commit 0a17a1e

Browse files
authored
Merge branch 'master' into add-default-validate-timeout-value
2 parents db5a9bd + 9302a55 commit 0a17a1e

16 files changed

Lines changed: 233 additions & 68 deletions

File tree

guides/authorization/visibility.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ For big schemas, this can be a worthwhile speed-up.
151151
- `Visibility` speeds up Rails app boot because it doesn't require all types to be loaded during boot and only loads types as they are used by queries.
152152
- `Visibility` supports predefined, reusable visibility profiles which speeds up queries using complicated `visible?` checks.
153153
- `Visibility` hides types differently in a few edge cases:
154-
- Previously, `Warden` hide interface and union types which had no possible types. `Visibility` doesn't check possible types (in order to support performance improvements), so those types must return `false` for `visible?` in the same cases where all possible types were hidden. Otherwise, that interface or union type will be visible but have no possible types.
155-
- Some other thing, see TODO
154+
- Previously, `Warden` hid interface and union types which had no possible types. `Visibility` doesn't check possible types (in order to support performance improvements), so those types must return `false` for `visible?` in the same cases where all possible types were hidden. Otherwise, that interface or union type will be visible but have no possible types.
155+
- When an object type is connected to the schema as a field return type or a union member, and also implements and interface, if the object type's _other_ connection(s) to the schema are hidden, then it won't appear as an implementer of that interface unless it's registered with `orphan_types` (either by the schema or interface). `Warden` used a "global" map of types so it could discover object types in this case, but `Visibility` doesn't have that global map. (Since time of writing, `Visibility` _does_ have some global type tracking, so maybe this could be fixed.)
156156
- When `Visibility` is used, several (Ruby-level) Schema introspection methods don't work because the caches they draw on haven't been calculated (`Schema.references_to`, `Schema.union_memberships`). If you're using these, please get in touch so that we can find a way forward.
157157

158158
### Migration Mode

lib/graphql/analysis.rb

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@
66
require "graphql/analysis/max_query_complexity"
77
require "graphql/analysis/query_depth"
88
require "graphql/analysis/max_query_depth"
9-
require "timeout"
10-
119
module GraphQL
1210
module Analysis
1311
AST = self
12+
13+
class TimeoutError < AnalysisError
14+
def initialize(...)
15+
super("Timeout on validation of query")
16+
end
17+
end
18+
1419
module_function
1520
# Analyze a multiplex, and all queries within.
1621
# Multiplex analyzers are ran for all queries, keeping state.
@@ -61,13 +66,11 @@ def analyze_query(query, analyzers, multiplex_analyzers: [])
6166
if !analyzers_to_run.empty?
6267
visitor = GraphQL::Analysis::Visitor.new(
6368
query: query,
64-
analyzers: analyzers_to_run
69+
analyzers: analyzers_to_run,
70+
timeout: query.validate_timeout_remaining,
6571
)
6672

67-
# `nil` or `0` causes no timeout
68-
Timeout::timeout(query.validate_timeout_remaining) do
69-
visitor.visit
70-
end
73+
visitor.visit
7174

7275
if !visitor.rescued_errors.empty?
7376
return visitor.rescued_errors
@@ -79,8 +82,8 @@ def analyze_query(query, analyzers, multiplex_analyzers: [])
7982
[]
8083
end
8184
end
82-
rescue Timeout::Error
83-
[GraphQL::AnalysisError.new("Timeout on validation of query")]
85+
rescue TimeoutError => err
86+
[err]
8487
rescue GraphQL::UnauthorizedError, GraphQL::ExecutionError
8588
# This error was raised during analysis and will be returned the client before execution
8689
[]

lib/graphql/analysis/visitor.rb

Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module Analysis
1010
#
1111
# @see {GraphQL::Analysis::Analyzer} AST Analyzers for queries
1212
class Visitor < GraphQL::Language::StaticVisitor
13-
def initialize(query:, analyzers:)
13+
def initialize(query:, analyzers:, timeout:)
1414
@analyzers = analyzers
1515
@path = []
1616
@object_types = []
@@ -24,6 +24,11 @@ def initialize(query:, analyzers:)
2424
@types = query.types
2525
@response_path = []
2626
@skip_stack = [false]
27+
@timeout_time = if timeout
28+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second) + timeout
29+
else
30+
Float::INFINITY
31+
end
2732
super(query.selected_operation)
2833
end
2934

@@ -72,28 +77,25 @@ def response_path
7277
module_eval <<-RUBY, __FILE__, __LINE__
7378
def call_on_enter_#{node_type}(node, parent)
7479
@analyzers.each do |a|
75-
begin
76-
a.on_enter_#{node_type}(node, parent, self)
77-
rescue AnalysisError => err
78-
@rescued_errors << err
79-
end
80+
a.on_enter_#{node_type}(node, parent, self)
81+
rescue AnalysisError => err
82+
@rescued_errors << err
8083
end
8184
end
8285
8386
def call_on_leave_#{node_type}(node, parent)
8487
@analyzers.each do |a|
85-
begin
86-
a.on_leave_#{node_type}(node, parent, self)
87-
rescue AnalysisError => err
88-
@rescued_errors << err
89-
end
88+
a.on_leave_#{node_type}(node, parent, self)
89+
rescue AnalysisError => err
90+
@rescued_errors << err
9091
end
9192
end
9293
9394
RUBY
9495
end
9596

9697
def on_operation_definition(node, parent)
98+
check_timeout
9799
object_type = @schema.root_type_for_operation(node.operation_type)
98100
@object_types.push(object_type)
99101
@path.push("#{node.operation_type}#{node.name ? " #{node.name}" : ""}")
@@ -104,31 +106,27 @@ def on_operation_definition(node, parent)
104106
@path.pop
105107
end
106108

107-
def on_fragment_definition(node, parent)
108-
on_fragment_with_type(node) do
109-
@path.push("fragment #{node.name}")
110-
@in_fragment_def = false
111-
call_on_enter_fragment_definition(node, parent)
112-
super
113-
@in_fragment_def = false
114-
call_on_leave_fragment_definition(node, parent)
115-
end
116-
end
117-
118109
def on_inline_fragment(node, parent)
119-
on_fragment_with_type(node) do
120-
@path.push("...#{node.type ? " on #{node.type.name}" : ""}")
121-
@skipping = @skip_stack.last || skip?(node)
122-
@skip_stack << @skipping
123-
124-
call_on_enter_inline_fragment(node, parent)
125-
super
126-
@skipping = @skip_stack.pop
127-
call_on_leave_inline_fragment(node, parent)
110+
check_timeout
111+
object_type = if node.type
112+
@types.type(node.type.name)
113+
else
114+
@object_types.last
128115
end
116+
@object_types.push(object_type)
117+
@path.push("...#{node.type ? " on #{node.type.name}" : ""}")
118+
@skipping = @skip_stack.last || skip?(node)
119+
@skip_stack << @skipping
120+
call_on_enter_inline_fragment(node, parent)
121+
super
122+
@skipping = @skip_stack.pop
123+
call_on_leave_inline_fragment(node, parent)
124+
@object_types.pop
125+
@path.pop
129126
end
130127

131128
def on_field(node, parent)
129+
check_timeout
132130
@response_path.push(node.alias || node.name)
133131
parent_type = @object_types.last
134132
# This could be nil if the previous field wasn't found:
@@ -156,6 +154,7 @@ def on_field(node, parent)
156154
end
157155

158156
def on_directive(node, parent)
157+
check_timeout
159158
directive_defn = @schema.directives[node.name]
160159
@directive_definitions.push(directive_defn)
161160
call_on_enter_directive(node, parent)
@@ -165,6 +164,7 @@ def on_directive(node, parent)
165164
end
166165

167166
def on_argument(node, parent)
167+
check_timeout
168168
argument_defn = if (arg = @argument_definitions.last)
169169
arg_type = arg.type.unwrap
170170
if arg_type.kind.input_object?
@@ -190,6 +190,7 @@ def on_argument(node, parent)
190190
end
191191

192192
def on_fragment_spread(node, parent)
193+
check_timeout
193194
@path.push("... #{node.name}")
194195
@skipping = @skip_stack.last || skip?(node)
195196
@skip_stack << @skipping
@@ -267,16 +268,10 @@ def skip?(ast_node)
267268
!dir.empty? && !GraphQL::Execution::DirectiveChecks.include?(dir, query)
268269
end
269270

270-
def on_fragment_with_type(node)
271-
object_type = if node.type
272-
@types.type(node.type.name)
273-
else
274-
@object_types.last
271+
def check_timeout
272+
if Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second) > @timeout_time
273+
raise GraphQL::Analysis::TimeoutError
275274
end
276-
@object_types.push(object_type)
277-
yield(node)
278-
@object_types.pop
279-
@path.pop
280275
end
281276
end
282277
end

lib/graphql/execution/interpreter/runtime.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ def continue_value(value, field, is_non_null, ast_node, result_name, selection_r
473473
# When this comes from a list item, use the parent object:
474474
parent_type = selection_result.is_a?(GraphQLResultArray) ? selection_result.graphql_parent.graphql_result_type : selection_result.graphql_result_type
475475
# This block is called if `result_name` is not dead. (Maybe a previous invalid nil caused it be marked dead.)
476-
err = parent_type::InvalidNullError.new(parent_type, field, value, ast_node)
476+
err = parent_type::InvalidNullError.new(parent_type, field, ast_node)
477477
schema.type_error(err, context)
478478
end
479479
else

lib/graphql/invalid_null_error.rb

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,12 @@ class InvalidNullError < GraphQL::Error
99
# @return [GraphQL::Field] The field which failed to return a value
1010
attr_reader :field
1111

12-
# @return [nil, GraphQL::ExecutionError] The invalid value for this field
13-
attr_reader :value
14-
1512
# @return [GraphQL::Language::Nodes::Field] the field where the error occurred
1613
attr_reader :ast_node
1714

18-
def initialize(parent_type, field, value, ast_node)
15+
def initialize(parent_type, field, ast_node)
1916
@parent_type = parent_type
2017
@field = field
21-
@value = value
2218
@ast_node = ast_node
2319
super("Cannot return null for non-nullable field #{@parent_type.graphql_name}.#{@field.graphql_name}")
2420
end

lib/graphql/language/lexer.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,21 @@ def initialize(graphql_str, filename: nil, max_tokens: nil)
1313
@pos = nil
1414
@max_tokens = max_tokens || Float::INFINITY
1515
@tokens_count = 0
16+
@finished = false
1617
end
1718

18-
def eos?
19-
@scanner.eos?
19+
def finished?
20+
@finished
2021
end
2122

2223
attr_reader :pos, :tokens_count
2324

2425
def advance
2526
@scanner.skip(IGNORE_REGEXP)
26-
return false if @scanner.eos?
27+
if @scanner.eos?
28+
@finished = true
29+
return false
30+
end
2731
@tokens_count += 1
2832
if @tokens_count > @max_tokens
2933
raise_parse_error("This query is too large to execute.")

lib/graphql/language/parser.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def document
110110
# Only ignored characters is not a valid document
111111
raise GraphQL::ParseError.new("Unexpected end of document", nil, nil, @graphql_str)
112112
end
113-
while !@lexer.eos?
113+
while !@lexer.finished?
114114
defns << definition
115115
end
116116
Document.new(pos: 0, definitions: defns, filename: @filename, source: self)

lib/graphql/schema.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -821,8 +821,8 @@ def subscription_execution_strategy(new_subscription_execution_strategy = nil, d
821821

822822
attr_writer :validate_timeout
823823

824-
def validate_timeout(new_validate_timeout = :not_provided)
825-
if new_validate_timeout != :not_provided
824+
def validate_timeout(new_validate_timeout = NOT_CONFIGURED)
825+
if !NOT_CONFIGURED.equal?(new_validate_timeout)
826826
@validate_timeout = new_validate_timeout
827827
elsif defined?(@validate_timeout)
828828
@validate_timeout

lib/graphql/schema/resolver.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ class Resolver
2222
include Schema::Member::GraphQLTypeNames
2323
# Really we only need description & comment from here, but:
2424
extend Schema::Member::BaseDSLMethods
25-
extend Member::BaseDSLMethods::ConfigurationExtension
2625
extend GraphQL::Schema::Member::HasArguments
2726
extend GraphQL::Schema::Member::HasValidators
2827
include Schema::Member::HasPath
@@ -404,6 +403,11 @@ def extensions
404403
end
405404
end
406405

406+
def inherited(child_class)
407+
child_class.description(description)
408+
super
409+
end
410+
407411
private
408412

409413
attr_reader :own_extensions

lib/graphql/static_validation/rules/fields_will_merge.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ def find_fields_and_fragments(selections, owner_type:, parents:, fields:, fragme
345345
fields << Field.new(node, definition, owner_type, parents)
346346
when GraphQL::Language::Nodes::InlineFragment
347347
fragment_type = node.type ? @types.type(node.type.name) : owner_type
348-
find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type: owner_type, fields: fields, fragment_spreads: fragment_spreads) if fragment_type
348+
find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type: fragment_type, fields: fields, fragment_spreads: fragment_spreads) if fragment_type
349349
when GraphQL::Language::Nodes::FragmentSpread
350350
fragment_spreads << FragmentSpread.new(node.name, parents)
351351
end

0 commit comments

Comments
 (0)