graphql 2.3.5 → 2.3.6
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.
Potentially problematic release.
This version of graphql might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/lib/graphql/analysis/analyzer.rb +89 -0
- data/lib/graphql/analysis/field_usage.rb +82 -0
- data/lib/graphql/analysis/max_query_complexity.rb +20 -0
- data/lib/graphql/analysis/max_query_depth.rb +20 -0
- data/lib/graphql/analysis/query_complexity.rb +183 -0
- data/lib/graphql/analysis/{ast/query_depth.rb → query_depth.rb} +23 -25
- data/lib/graphql/analysis/visitor.rb +282 -0
- data/lib/graphql/analysis.rb +92 -1
- data/lib/graphql/dataloader/async_dataloader.rb +2 -0
- data/lib/graphql/execution/interpreter.rb +1 -1
- data/lib/graphql/query/validation_pipeline.rb +2 -2
- data/lib/graphql/schema/argument.rb +19 -5
- data/lib/graphql/schema/directive.rb +2 -0
- data/lib/graphql/schema/field.rb +8 -0
- data/lib/graphql/schema/resolver.rb +1 -0
- data/lib/graphql/subscriptions/broadcast_analyzer.rb +1 -1
- data/lib/graphql/subscriptions.rb +1 -1
- data/lib/graphql/version.rb +1 -1
- metadata +10 -11
- data/lib/graphql/analysis/ast/analyzer.rb +0 -91
- data/lib/graphql/analysis/ast/field_usage.rb +0 -84
- data/lib/graphql/analysis/ast/max_query_complexity.rb +0 -22
- data/lib/graphql/analysis/ast/max_query_depth.rb +0 -22
- data/lib/graphql/analysis/ast/query_complexity.rb +0 -185
- data/lib/graphql/analysis/ast/visitor.rb +0 -284
- data/lib/graphql/analysis/ast.rb +0 -94
@@ -1,84 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
module GraphQL
|
3
|
-
module Analysis
|
4
|
-
module AST
|
5
|
-
class FieldUsage < Analyzer
|
6
|
-
def initialize(query)
|
7
|
-
super
|
8
|
-
@used_fields = Set.new
|
9
|
-
@used_deprecated_fields = Set.new
|
10
|
-
@used_deprecated_arguments = Set.new
|
11
|
-
@used_deprecated_enum_values = Set.new
|
12
|
-
end
|
13
|
-
|
14
|
-
def on_leave_field(node, parent, visitor)
|
15
|
-
field_defn = visitor.field_definition
|
16
|
-
field = "#{visitor.parent_type_definition.graphql_name}.#{field_defn.graphql_name}"
|
17
|
-
@used_fields << field
|
18
|
-
@used_deprecated_fields << field if field_defn.deprecation_reason
|
19
|
-
arguments = visitor.query.arguments_for(node, field_defn)
|
20
|
-
# If there was an error when preparing this argument object,
|
21
|
-
# then this might be an error or something:
|
22
|
-
if arguments.respond_to?(:argument_values)
|
23
|
-
extract_deprecated_arguments(arguments.argument_values)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def result
|
28
|
-
{
|
29
|
-
used_fields: @used_fields.to_a,
|
30
|
-
used_deprecated_fields: @used_deprecated_fields.to_a,
|
31
|
-
used_deprecated_arguments: @used_deprecated_arguments.to_a,
|
32
|
-
used_deprecated_enum_values: @used_deprecated_enum_values.to_a,
|
33
|
-
}
|
34
|
-
end
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
def extract_deprecated_arguments(argument_values)
|
39
|
-
argument_values.each_pair do |_argument_name, argument|
|
40
|
-
if argument.definition.deprecation_reason
|
41
|
-
@used_deprecated_arguments << argument.definition.path
|
42
|
-
end
|
43
|
-
|
44
|
-
arg_val = argument.value
|
45
|
-
|
46
|
-
next if arg_val.nil?
|
47
|
-
|
48
|
-
argument_type = argument.definition.type
|
49
|
-
if argument_type.non_null?
|
50
|
-
argument_type = argument_type.of_type
|
51
|
-
end
|
52
|
-
|
53
|
-
if argument_type.kind.input_object?
|
54
|
-
extract_deprecated_arguments(argument.original_value.arguments.argument_values) # rubocop:disable Development/ContextIsPassedCop -- runtime args instance
|
55
|
-
elsif argument_type.kind.enum?
|
56
|
-
extract_deprecated_enum_value(argument_type, arg_val)
|
57
|
-
elsif argument_type.list?
|
58
|
-
inner_type = argument_type.unwrap
|
59
|
-
case inner_type.kind
|
60
|
-
when TypeKinds::INPUT_OBJECT
|
61
|
-
argument.original_value.each do |value|
|
62
|
-
extract_deprecated_arguments(value.arguments.argument_values) # rubocop:disable Development/ContextIsPassedCop -- runtime args instance
|
63
|
-
end
|
64
|
-
when TypeKinds::ENUM
|
65
|
-
arg_val.each do |value|
|
66
|
-
extract_deprecated_enum_value(inner_type, value)
|
67
|
-
end
|
68
|
-
else
|
69
|
-
# Not a kind of input that we track
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def extract_deprecated_enum_value(enum_type, value)
|
76
|
-
enum_value = @query.warden.enum_values(enum_type).find { |ev| ev.value == value }
|
77
|
-
if enum_value&.deprecation_reason
|
78
|
-
@used_deprecated_enum_values << enum_value.path
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
@@ -1,22 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
module GraphQL
|
3
|
-
module Analysis
|
4
|
-
module AST
|
5
|
-
# Used under the hood to implement complexity validation,
|
6
|
-
# see {Schema#max_complexity} and {Query#max_complexity}
|
7
|
-
class MaxQueryComplexity < QueryComplexity
|
8
|
-
def result
|
9
|
-
return if subject.max_complexity.nil?
|
10
|
-
|
11
|
-
total_complexity = max_possible_complexity
|
12
|
-
|
13
|
-
if total_complexity > subject.max_complexity
|
14
|
-
GraphQL::AnalysisError.new("Query has complexity of #{total_complexity}, which exceeds max complexity of #{subject.max_complexity}")
|
15
|
-
else
|
16
|
-
nil
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
@@ -1,22 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
module GraphQL
|
3
|
-
module Analysis
|
4
|
-
module AST
|
5
|
-
class MaxQueryDepth < QueryDepth
|
6
|
-
def result
|
7
|
-
configured_max_depth = if query
|
8
|
-
query.max_depth
|
9
|
-
else
|
10
|
-
multiplex.schema.max_depth
|
11
|
-
end
|
12
|
-
|
13
|
-
if configured_max_depth && @max_depth > configured_max_depth
|
14
|
-
GraphQL::AnalysisError.new("Query has depth of #{@max_depth}, which exceeds max depth of #{configured_max_depth}")
|
15
|
-
else
|
16
|
-
nil
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
@@ -1,185 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
module GraphQL
|
3
|
-
module Analysis
|
4
|
-
# Calculate the complexity of a query, using {Field#complexity} values.
|
5
|
-
module AST
|
6
|
-
class QueryComplexity < Analyzer
|
7
|
-
# State for the query complexity calculation:
|
8
|
-
# - `complexities_on_type` holds complexity scores for each type
|
9
|
-
def initialize(query)
|
10
|
-
super
|
11
|
-
@skip_introspection_fields = !query.schema.max_complexity_count_introspection_fields
|
12
|
-
@complexities_on_type_by_query = {}
|
13
|
-
end
|
14
|
-
|
15
|
-
# Override this method to use the complexity result
|
16
|
-
def result
|
17
|
-
max_possible_complexity
|
18
|
-
end
|
19
|
-
|
20
|
-
# ScopedTypeComplexity models a tree of GraphQL types mapped to inner selections, ie:
|
21
|
-
# Hash<GraphQL::BaseType, Hash<String, ScopedTypeComplexity>>
|
22
|
-
class ScopedTypeComplexity < Hash
|
23
|
-
# A proc for defaulting empty namespace requests as a new scope hash.
|
24
|
-
DEFAULT_PROC = ->(h, k) { h[k] = {} }
|
25
|
-
|
26
|
-
attr_reader :field_definition, :response_path, :query
|
27
|
-
|
28
|
-
# @param parent_type [Class] The owner of `field_definition`
|
29
|
-
# @param field_definition [GraphQL::Field, GraphQL::Schema::Field] Used for getting the `.complexity` configuration
|
30
|
-
# @param query [GraphQL::Query] Used for `query.possible_types`
|
31
|
-
# @param response_path [Array<String>] The path to the response key for the field
|
32
|
-
# @return [Hash<GraphQL::BaseType, Hash<String, ScopedTypeComplexity>>]
|
33
|
-
def initialize(parent_type, field_definition, query, response_path)
|
34
|
-
super(&DEFAULT_PROC)
|
35
|
-
@parent_type = parent_type
|
36
|
-
@field_definition = field_definition
|
37
|
-
@query = query
|
38
|
-
@response_path = response_path
|
39
|
-
@nodes = []
|
40
|
-
end
|
41
|
-
|
42
|
-
# @return [Array<GraphQL::Language::Nodes::Field>]
|
43
|
-
attr_reader :nodes
|
44
|
-
|
45
|
-
def own_complexity(child_complexity)
|
46
|
-
@field_definition.calculate_complexity(query: @query, nodes: @nodes, child_complexity: child_complexity)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def on_enter_field(node, parent, visitor)
|
51
|
-
# We don't want to visit fragment definitions,
|
52
|
-
# we'll visit them when we hit the spreads instead
|
53
|
-
return if visitor.visiting_fragment_definition?
|
54
|
-
return if visitor.skipping?
|
55
|
-
return if @skip_introspection_fields && visitor.field_definition.introspection?
|
56
|
-
parent_type = visitor.parent_type_definition
|
57
|
-
field_key = node.alias || node.name
|
58
|
-
|
59
|
-
# Find or create a complexity scope stack for this query.
|
60
|
-
scopes_stack = @complexities_on_type_by_query[visitor.query] ||= [ScopedTypeComplexity.new(nil, nil, query, visitor.response_path)]
|
61
|
-
|
62
|
-
# Find or create the complexity costing node for this field.
|
63
|
-
scope = scopes_stack.last[parent_type][field_key] ||= ScopedTypeComplexity.new(parent_type, visitor.field_definition, visitor.query, visitor.response_path)
|
64
|
-
scope.nodes.push(node)
|
65
|
-
scopes_stack.push(scope)
|
66
|
-
end
|
67
|
-
|
68
|
-
def on_leave_field(node, parent, visitor)
|
69
|
-
# We don't want to visit fragment definitions,
|
70
|
-
# we'll visit them when we hit the spreads instead
|
71
|
-
return if visitor.visiting_fragment_definition?
|
72
|
-
return if visitor.skipping?
|
73
|
-
return if @skip_introspection_fields && visitor.field_definition.introspection?
|
74
|
-
scopes_stack = @complexities_on_type_by_query[visitor.query]
|
75
|
-
scopes_stack.pop
|
76
|
-
end
|
77
|
-
|
78
|
-
private
|
79
|
-
|
80
|
-
# @return [Integer]
|
81
|
-
def max_possible_complexity
|
82
|
-
@complexities_on_type_by_query.reduce(0) do |total, (query, scopes_stack)|
|
83
|
-
total + merged_max_complexity_for_scopes(query, [scopes_stack.first])
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
# @param query [GraphQL::Query] Used for `query.possible_types`
|
88
|
-
# @param scopes [Array<ScopedTypeComplexity>] Array of scoped type complexities
|
89
|
-
# @return [Integer]
|
90
|
-
def merged_max_complexity_for_scopes(query, scopes)
|
91
|
-
# Aggregate a set of all possible scope types encountered (scope keys).
|
92
|
-
# Use a hash, but ignore the values; it's just a fast way to work with the keys.
|
93
|
-
possible_scope_types = scopes.each_with_object({}) do |scope, memo|
|
94
|
-
memo.merge!(scope)
|
95
|
-
end
|
96
|
-
|
97
|
-
# Expand abstract scope types into their concrete implementations;
|
98
|
-
# overlapping abstracts coalesce through their intersecting types.
|
99
|
-
possible_scope_types.keys.each do |possible_scope_type|
|
100
|
-
next unless possible_scope_type.kind.abstract?
|
101
|
-
|
102
|
-
query.possible_types(possible_scope_type).each do |impl_type|
|
103
|
-
possible_scope_types[impl_type] ||= true
|
104
|
-
end
|
105
|
-
possible_scope_types.delete(possible_scope_type)
|
106
|
-
end
|
107
|
-
|
108
|
-
# Aggregate the lexical selections that may apply to each possible type,
|
109
|
-
# and then return the maximum cost among possible typed selections.
|
110
|
-
possible_scope_types.each_key.reduce(0) do |max, possible_scope_type|
|
111
|
-
# Collect inner selections from all scopes that intersect with this possible type.
|
112
|
-
all_inner_selections = scopes.each_with_object([]) do |scope, memo|
|
113
|
-
scope.each do |scope_type, inner_selections|
|
114
|
-
memo << inner_selections if types_intersect?(query, scope_type, possible_scope_type)
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
# Find the maximum complexity for the scope type among possible lexical branches.
|
119
|
-
complexity = merged_max_complexity(query, all_inner_selections)
|
120
|
-
complexity > max ? complexity : max
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def types_intersect?(query, a, b)
|
125
|
-
return true if a == b
|
126
|
-
|
127
|
-
a_types = query.possible_types(a)
|
128
|
-
query.possible_types(b).any? { |t| a_types.include?(t) }
|
129
|
-
end
|
130
|
-
|
131
|
-
# A hook which is called whenever a field's max complexity is calculated.
|
132
|
-
# Override this method to capture individual field complexity details.
|
133
|
-
#
|
134
|
-
# @param scoped_type_complexity [ScopedTypeComplexity]
|
135
|
-
# @param max_complexity [Numeric] Field's maximum complexity including child complexity
|
136
|
-
# @param child_complexity [Numeric, nil] Field's child complexity
|
137
|
-
def field_complexity(scoped_type_complexity, max_complexity:, child_complexity: nil)
|
138
|
-
end
|
139
|
-
|
140
|
-
# @param inner_selections [Array<Hash<String, ScopedTypeComplexity>>] Field selections for a scope
|
141
|
-
# @return [Integer] Total complexity value for all these selections in the parent scope
|
142
|
-
def merged_max_complexity(query, inner_selections)
|
143
|
-
# Aggregate a set of all unique field selection keys across all scopes.
|
144
|
-
# Use a hash, but ignore the values; it's just a fast way to work with the keys.
|
145
|
-
unique_field_keys = inner_selections.each_with_object({}) do |inner_selection, memo|
|
146
|
-
memo.merge!(inner_selection)
|
147
|
-
end
|
148
|
-
|
149
|
-
# Add up the total cost for each unique field name's coalesced selections
|
150
|
-
unique_field_keys.each_key.reduce(0) do |total, field_key|
|
151
|
-
composite_scopes = nil
|
152
|
-
field_cost = 0
|
153
|
-
|
154
|
-
# Collect composite selection scopes for further aggregation,
|
155
|
-
# leaf selections report their costs directly.
|
156
|
-
inner_selections.each do |inner_selection|
|
157
|
-
child_scope = inner_selection[field_key]
|
158
|
-
next unless child_scope
|
159
|
-
|
160
|
-
# Empty child scopes are leaf nodes with zero child complexity.
|
161
|
-
if child_scope.empty?
|
162
|
-
field_cost = child_scope.own_complexity(0)
|
163
|
-
field_complexity(child_scope, max_complexity: field_cost, child_complexity: nil)
|
164
|
-
else
|
165
|
-
composite_scopes ||= []
|
166
|
-
composite_scopes << child_scope
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
if composite_scopes
|
171
|
-
child_complexity = merged_max_complexity_for_scopes(query, composite_scopes)
|
172
|
-
|
173
|
-
# This is the last composite scope visited; assume it's representative (for backwards compatibility).
|
174
|
-
# Note: it would be more correct to score each composite scope and use the maximum possibility.
|
175
|
-
field_cost = composite_scopes.last.own_complexity(child_complexity)
|
176
|
-
field_complexity(composite_scopes.last, max_complexity: field_cost, child_complexity: child_complexity)
|
177
|
-
end
|
178
|
-
|
179
|
-
total + field_cost
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
185
|
-
end
|
@@ -1,284 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
module GraphQL
|
3
|
-
module Analysis
|
4
|
-
module AST
|
5
|
-
# Depth first traversal through a query AST, calling AST analyzers
|
6
|
-
# along the way.
|
7
|
-
#
|
8
|
-
# The visitor is a special case of GraphQL::Language::StaticVisitor, visiting
|
9
|
-
# only the selected operation, providing helpers for common use cases such
|
10
|
-
# as skipped fields and visiting fragment spreads.
|
11
|
-
#
|
12
|
-
# @see {GraphQL::Analysis::AST::Analyzer} AST Analyzers for queries
|
13
|
-
class Visitor < GraphQL::Language::StaticVisitor
|
14
|
-
def initialize(query:, analyzers:)
|
15
|
-
@analyzers = analyzers
|
16
|
-
@path = []
|
17
|
-
@object_types = []
|
18
|
-
@directives = []
|
19
|
-
@field_definitions = []
|
20
|
-
@argument_definitions = []
|
21
|
-
@directive_definitions = []
|
22
|
-
@rescued_errors = []
|
23
|
-
@query = query
|
24
|
-
@schema = query.schema
|
25
|
-
@response_path = []
|
26
|
-
@skip_stack = [false]
|
27
|
-
super(query.selected_operation)
|
28
|
-
end
|
29
|
-
|
30
|
-
# @return [GraphQL::Query] the query being visited
|
31
|
-
attr_reader :query
|
32
|
-
|
33
|
-
# @return [Array<GraphQL::ObjectType>] Types whose scope we've entered
|
34
|
-
attr_reader :object_types
|
35
|
-
|
36
|
-
# @return [Array<GraphQL::AnalysisError]
|
37
|
-
attr_reader :rescued_errors
|
38
|
-
|
39
|
-
def visit
|
40
|
-
return unless @document
|
41
|
-
super
|
42
|
-
end
|
43
|
-
|
44
|
-
# Visit Helpers
|
45
|
-
|
46
|
-
# @return [GraphQL::Execution::Interpreter::Arguments] Arguments for this node, merging default values, literal values and query variables
|
47
|
-
# @see {GraphQL::Query#arguments_for}
|
48
|
-
def arguments_for(ast_node, field_definition)
|
49
|
-
@query.arguments_for(ast_node, field_definition)
|
50
|
-
end
|
51
|
-
|
52
|
-
# @return [Boolean] If the visitor is currently inside a fragment definition
|
53
|
-
def visiting_fragment_definition?
|
54
|
-
@in_fragment_def
|
55
|
-
end
|
56
|
-
|
57
|
-
# @return [Boolean] If the current node should be skipped because of a skip or include directive
|
58
|
-
def skipping?
|
59
|
-
@skipping
|
60
|
-
end
|
61
|
-
|
62
|
-
# @return [Array<String>] The path to the response key for the current field
|
63
|
-
def response_path
|
64
|
-
@response_path.dup
|
65
|
-
end
|
66
|
-
|
67
|
-
# Visitor Hooks
|
68
|
-
[
|
69
|
-
:operation_definition, :fragment_definition,
|
70
|
-
:inline_fragment, :field, :directive, :argument, :fragment_spread
|
71
|
-
].each do |node_type|
|
72
|
-
module_eval <<-RUBY, __FILE__, __LINE__
|
73
|
-
def call_on_enter_#{node_type}(node, parent)
|
74
|
-
@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
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def call_on_leave_#{node_type}(node, parent)
|
84
|
-
@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
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
RUBY
|
94
|
-
end
|
95
|
-
|
96
|
-
def on_operation_definition(node, parent)
|
97
|
-
object_type = @schema.root_type_for_operation(node.operation_type)
|
98
|
-
@object_types.push(object_type)
|
99
|
-
@path.push("#{node.operation_type}#{node.name ? " #{node.name}" : ""}")
|
100
|
-
call_on_enter_operation_definition(node, parent)
|
101
|
-
super
|
102
|
-
call_on_leave_operation_definition(node, parent)
|
103
|
-
@object_types.pop
|
104
|
-
@path.pop
|
105
|
-
end
|
106
|
-
|
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
|
-
|
118
|
-
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)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
def on_field(node, parent)
|
132
|
-
@response_path.push(node.alias || node.name)
|
133
|
-
parent_type = @object_types.last
|
134
|
-
# This could be nil if the previous field wasn't found:
|
135
|
-
field_definition = parent_type && @schema.get_field(parent_type, node.name, @query.context)
|
136
|
-
@field_definitions.push(field_definition)
|
137
|
-
if !field_definition.nil?
|
138
|
-
next_object_type = field_definition.type.unwrap
|
139
|
-
@object_types.push(next_object_type)
|
140
|
-
else
|
141
|
-
@object_types.push(nil)
|
142
|
-
end
|
143
|
-
@path.push(node.alias || node.name)
|
144
|
-
|
145
|
-
@skipping = @skip_stack.last || skip?(node)
|
146
|
-
@skip_stack << @skipping
|
147
|
-
|
148
|
-
call_on_enter_field(node, parent)
|
149
|
-
super
|
150
|
-
@skipping = @skip_stack.pop
|
151
|
-
call_on_leave_field(node, parent)
|
152
|
-
@response_path.pop
|
153
|
-
@field_definitions.pop
|
154
|
-
@object_types.pop
|
155
|
-
@path.pop
|
156
|
-
end
|
157
|
-
|
158
|
-
def on_directive(node, parent)
|
159
|
-
directive_defn = @schema.directives[node.name]
|
160
|
-
@directive_definitions.push(directive_defn)
|
161
|
-
call_on_enter_directive(node, parent)
|
162
|
-
super
|
163
|
-
call_on_leave_directive(node, parent)
|
164
|
-
@directive_definitions.pop
|
165
|
-
end
|
166
|
-
|
167
|
-
def on_argument(node, parent)
|
168
|
-
argument_defn = if (arg = @argument_definitions.last)
|
169
|
-
arg_type = arg.type.unwrap
|
170
|
-
if arg_type.kind.input_object?
|
171
|
-
arg_type.get_argument(node.name, @query.context)
|
172
|
-
else
|
173
|
-
nil
|
174
|
-
end
|
175
|
-
elsif (directive_defn = @directive_definitions.last)
|
176
|
-
directive_defn.get_argument(node.name, @query.context)
|
177
|
-
elsif (field_defn = @field_definitions.last)
|
178
|
-
field_defn.get_argument(node.name, @query.context)
|
179
|
-
else
|
180
|
-
nil
|
181
|
-
end
|
182
|
-
|
183
|
-
@argument_definitions.push(argument_defn)
|
184
|
-
@path.push(node.name)
|
185
|
-
call_on_enter_argument(node, parent)
|
186
|
-
super
|
187
|
-
call_on_leave_argument(node, parent)
|
188
|
-
@argument_definitions.pop
|
189
|
-
@path.pop
|
190
|
-
end
|
191
|
-
|
192
|
-
def on_fragment_spread(node, parent)
|
193
|
-
@path.push("... #{node.name}")
|
194
|
-
@skipping = @skip_stack.last || skip?(node)
|
195
|
-
@skip_stack << @skipping
|
196
|
-
|
197
|
-
call_on_enter_fragment_spread(node, parent)
|
198
|
-
enter_fragment_spread_inline(node)
|
199
|
-
super
|
200
|
-
@skipping = @skip_stack.pop
|
201
|
-
leave_fragment_spread_inline(node)
|
202
|
-
call_on_leave_fragment_spread(node, parent)
|
203
|
-
@path.pop
|
204
|
-
end
|
205
|
-
|
206
|
-
# @return [GraphQL::BaseType] The current object type
|
207
|
-
def type_definition
|
208
|
-
@object_types.last
|
209
|
-
end
|
210
|
-
|
211
|
-
# @return [GraphQL::BaseType] The type which the current type came from
|
212
|
-
def parent_type_definition
|
213
|
-
@object_types[-2]
|
214
|
-
end
|
215
|
-
|
216
|
-
# @return [GraphQL::Field, nil] The most-recently-entered GraphQL::Field, if currently inside one
|
217
|
-
def field_definition
|
218
|
-
@field_definitions.last
|
219
|
-
end
|
220
|
-
|
221
|
-
# @return [GraphQL::Field, nil] The GraphQL field which returned the object that the current field belongs to
|
222
|
-
def previous_field_definition
|
223
|
-
@field_definitions[-2]
|
224
|
-
end
|
225
|
-
|
226
|
-
# @return [GraphQL::Directive, nil] The most-recently-entered GraphQL::Directive, if currently inside one
|
227
|
-
def directive_definition
|
228
|
-
@directive_definitions.last
|
229
|
-
end
|
230
|
-
|
231
|
-
# @return [GraphQL::Argument, nil] The most-recently-entered GraphQL::Argument, if currently inside one
|
232
|
-
def argument_definition
|
233
|
-
@argument_definitions.last
|
234
|
-
end
|
235
|
-
|
236
|
-
# @return [GraphQL::Argument, nil] The previous GraphQL argument
|
237
|
-
def previous_argument_definition
|
238
|
-
@argument_definitions[-2]
|
239
|
-
end
|
240
|
-
|
241
|
-
private
|
242
|
-
|
243
|
-
# Visit a fragment spread inline instead of visiting the definition
|
244
|
-
# by itself.
|
245
|
-
def enter_fragment_spread_inline(fragment_spread)
|
246
|
-
fragment_def = query.fragments[fragment_spread.name]
|
247
|
-
|
248
|
-
object_type = if fragment_def.type
|
249
|
-
@query.warden.get_type(fragment_def.type.name)
|
250
|
-
else
|
251
|
-
object_types.last
|
252
|
-
end
|
253
|
-
|
254
|
-
object_types << object_type
|
255
|
-
|
256
|
-
on_fragment_definition_children(fragment_def)
|
257
|
-
end
|
258
|
-
|
259
|
-
# Visit a fragment spread inline instead of visiting the definition
|
260
|
-
# by itself.
|
261
|
-
def leave_fragment_spread_inline(_fragment_spread)
|
262
|
-
object_types.pop
|
263
|
-
end
|
264
|
-
|
265
|
-
def skip?(ast_node)
|
266
|
-
dir = ast_node.directives
|
267
|
-
dir.any? && !GraphQL::Execution::DirectiveChecks.include?(dir, query)
|
268
|
-
end
|
269
|
-
|
270
|
-
def on_fragment_with_type(node)
|
271
|
-
object_type = if node.type
|
272
|
-
@query.warden.get_type(node.type.name)
|
273
|
-
else
|
274
|
-
@object_types.last
|
275
|
-
end
|
276
|
-
@object_types.push(object_type)
|
277
|
-
yield(node)
|
278
|
-
@object_types.pop
|
279
|
-
@path.pop
|
280
|
-
end
|
281
|
-
end
|
282
|
-
end
|
283
|
-
end
|
284
|
-
end
|
data/lib/graphql/analysis/ast.rb
DELETED
@@ -1,94 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
require "graphql/analysis/ast/visitor"
|
3
|
-
require "graphql/analysis/ast/analyzer"
|
4
|
-
require "graphql/analysis/ast/field_usage"
|
5
|
-
require "graphql/analysis/ast/query_complexity"
|
6
|
-
require "graphql/analysis/ast/max_query_complexity"
|
7
|
-
require "graphql/analysis/ast/query_depth"
|
8
|
-
require "graphql/analysis/ast/max_query_depth"
|
9
|
-
require "timeout"
|
10
|
-
|
11
|
-
module GraphQL
|
12
|
-
module Analysis
|
13
|
-
module AST
|
14
|
-
module_function
|
15
|
-
# Analyze a multiplex, and all queries within.
|
16
|
-
# Multiplex analyzers are ran for all queries, keeping state.
|
17
|
-
# Query analyzers are ran per query, without carrying state between queries.
|
18
|
-
#
|
19
|
-
# @param multiplex [GraphQL::Execution::Multiplex]
|
20
|
-
# @param analyzers [Array<GraphQL::Analysis::AST::Analyzer>]
|
21
|
-
# @return [Array<Any>] Results from multiplex analyzers
|
22
|
-
def analyze_multiplex(multiplex, analyzers)
|
23
|
-
multiplex_analyzers = analyzers.map { |analyzer| analyzer.new(multiplex) }
|
24
|
-
|
25
|
-
multiplex.current_trace.analyze_multiplex(multiplex: multiplex) do
|
26
|
-
query_results = multiplex.queries.map do |query|
|
27
|
-
if query.valid?
|
28
|
-
analyze_query(
|
29
|
-
query,
|
30
|
-
query.analyzers,
|
31
|
-
multiplex_analyzers: multiplex_analyzers
|
32
|
-
)
|
33
|
-
else
|
34
|
-
[]
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
multiplex_results = multiplex_analyzers.map(&:result)
|
39
|
-
multiplex_errors = analysis_errors(multiplex_results)
|
40
|
-
|
41
|
-
multiplex.queries.each_with_index do |query, idx|
|
42
|
-
query.analysis_errors = multiplex_errors + analysis_errors(query_results[idx])
|
43
|
-
end
|
44
|
-
multiplex_results
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
# @param query [GraphQL::Query]
|
49
|
-
# @param analyzers [Array<GraphQL::Analysis::AST::Analyzer>]
|
50
|
-
# @return [Array<Any>] Results from those analyzers
|
51
|
-
def analyze_query(query, analyzers, multiplex_analyzers: [])
|
52
|
-
query.current_trace.analyze_query(query: query) do
|
53
|
-
query_analyzers = analyzers
|
54
|
-
.map { |analyzer| analyzer.new(query) }
|
55
|
-
.tap { _1.select!(&:analyze?) }
|
56
|
-
|
57
|
-
analyzers_to_run = query_analyzers + multiplex_analyzers
|
58
|
-
if analyzers_to_run.any?
|
59
|
-
|
60
|
-
analyzers_to_run.select!(&:visit?)
|
61
|
-
if analyzers_to_run.any?
|
62
|
-
visitor = GraphQL::Analysis::AST::Visitor.new(
|
63
|
-
query: query,
|
64
|
-
analyzers: analyzers_to_run
|
65
|
-
)
|
66
|
-
|
67
|
-
# `nil` or `0` causes no timeout
|
68
|
-
Timeout::timeout(query.validate_timeout_remaining) do
|
69
|
-
visitor.visit
|
70
|
-
end
|
71
|
-
|
72
|
-
if visitor.rescued_errors.any?
|
73
|
-
return visitor.rescued_errors
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
query_analyzers.map(&:result)
|
78
|
-
else
|
79
|
-
[]
|
80
|
-
end
|
81
|
-
end
|
82
|
-
rescue Timeout::Error
|
83
|
-
[GraphQL::AnalysisError.new("Timeout on validation of query")]
|
84
|
-
rescue GraphQL::UnauthorizedError
|
85
|
-
# This error was raised during analysis and will be returned the client before execution
|
86
|
-
[]
|
87
|
-
end
|
88
|
-
|
89
|
-
def analysis_errors(results)
|
90
|
-
results.flatten.tap { _1.select! { |r| r.is_a?(GraphQL::AnalysisError) } }
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|