graphql 2.3.4 → 2.3.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install/mutation_root_generator.rb +2 -2
  3. data/lib/graphql/analysis/analyzer.rb +89 -0
  4. data/lib/graphql/analysis/field_usage.rb +82 -0
  5. data/lib/graphql/analysis/max_query_complexity.rb +20 -0
  6. data/lib/graphql/analysis/max_query_depth.rb +20 -0
  7. data/lib/graphql/analysis/query_complexity.rb +183 -0
  8. data/lib/graphql/analysis/{ast/query_depth.rb → query_depth.rb} +23 -25
  9. data/lib/graphql/analysis/visitor.rb +282 -0
  10. data/lib/graphql/analysis.rb +92 -1
  11. data/lib/graphql/dataloader/async_dataloader.rb +2 -0
  12. data/lib/graphql/dataloader/null_dataloader.rb +1 -1
  13. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +7 -4
  14. data/lib/graphql/execution/interpreter/runtime.rb +40 -59
  15. data/lib/graphql/execution/interpreter.rb +2 -2
  16. data/lib/graphql/language/nodes.rb +17 -22
  17. data/lib/graphql/language/parser.rb +54 -13
  18. data/lib/graphql/query/validation_pipeline.rb +2 -2
  19. data/lib/graphql/query.rb +1 -1
  20. data/lib/graphql/rubocop/graphql/base_cop.rb +1 -1
  21. data/lib/graphql/schema/addition.rb +21 -11
  22. data/lib/graphql/schema/argument.rb +19 -5
  23. data/lib/graphql/schema/directive.rb +2 -0
  24. data/lib/graphql/schema/field.rb +8 -0
  25. data/lib/graphql/schema/has_single_input_argument.rb +1 -0
  26. data/lib/graphql/schema/input_object.rb +1 -0
  27. data/lib/graphql/schema/introspection_system.rb +2 -2
  28. data/lib/graphql/schema/late_bound_type.rb +4 -0
  29. data/lib/graphql/schema/list.rb +2 -2
  30. data/lib/graphql/schema/member/has_arguments.rb +2 -35
  31. data/lib/graphql/schema/member/has_directives.rb +1 -1
  32. data/lib/graphql/schema/member/relay_shortcuts.rb +1 -1
  33. data/lib/graphql/schema/member/type_system_helpers.rb +1 -2
  34. data/lib/graphql/schema/resolver.rb +1 -0
  35. data/lib/graphql/schema/warden.rb +2 -3
  36. data/lib/graphql/schema.rb +20 -20
  37. data/lib/graphql/static_validation/rules/fields_will_merge.rb +1 -1
  38. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -1
  39. data/lib/graphql/subscriptions/broadcast_analyzer.rb +1 -1
  40. data/lib/graphql/subscriptions.rb +1 -1
  41. data/lib/graphql/type_kinds.rb +1 -1
  42. data/lib/graphql/version.rb +1 -1
  43. data/lib/graphql.rb +0 -8
  44. metadata +10 -11
  45. data/lib/graphql/analysis/ast/analyzer.rb +0 -91
  46. data/lib/graphql/analysis/ast/field_usage.rb +0 -84
  47. data/lib/graphql/analysis/ast/max_query_complexity.rb +0 -22
  48. data/lib/graphql/analysis/ast/max_query_depth.rb +0 -22
  49. data/lib/graphql/analysis/ast/query_complexity.rb +0 -185
  50. data/lib/graphql/analysis/ast/visitor.rb +0 -284
  51. data/lib/graphql/analysis/ast.rb +0 -94
@@ -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
- # Overide 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
@@ -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