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
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module Analysis
4
+ # Depth first traversal through a query AST, calling AST analyzers
5
+ # along the way.
6
+ #
7
+ # The visitor is a special case of GraphQL::Language::StaticVisitor, visiting
8
+ # only the selected operation, providing helpers for common use cases such
9
+ # as skipped fields and visiting fragment spreads.
10
+ #
11
+ # @see {GraphQL::Analysis::Analyzer} AST Analyzers for queries
12
+ class Visitor < GraphQL::Language::StaticVisitor
13
+ def initialize(query:, analyzers:)
14
+ @analyzers = analyzers
15
+ @path = []
16
+ @object_types = []
17
+ @directives = []
18
+ @field_definitions = []
19
+ @argument_definitions = []
20
+ @directive_definitions = []
21
+ @rescued_errors = []
22
+ @query = query
23
+ @schema = query.schema
24
+ @response_path = []
25
+ @skip_stack = [false]
26
+ super(query.selected_operation)
27
+ end
28
+
29
+ # @return [GraphQL::Query] the query being visited
30
+ attr_reader :query
31
+
32
+ # @return [Array<GraphQL::ObjectType>] Types whose scope we've entered
33
+ attr_reader :object_types
34
+
35
+ # @return [Array<GraphQL::AnalysisError]
36
+ attr_reader :rescued_errors
37
+
38
+ def visit
39
+ return unless @document
40
+ super
41
+ end
42
+
43
+ # Visit Helpers
44
+
45
+ # @return [GraphQL::Execution::Interpreter::Arguments] Arguments for this node, merging default values, literal values and query variables
46
+ # @see {GraphQL::Query#arguments_for}
47
+ def arguments_for(ast_node, field_definition)
48
+ @query.arguments_for(ast_node, field_definition)
49
+ end
50
+
51
+ # @return [Boolean] If the visitor is currently inside a fragment definition
52
+ def visiting_fragment_definition?
53
+ @in_fragment_def
54
+ end
55
+
56
+ # @return [Boolean] If the current node should be skipped because of a skip or include directive
57
+ def skipping?
58
+ @skipping
59
+ end
60
+
61
+ # @return [Array<String>] The path to the response key for the current field
62
+ def response_path
63
+ @response_path.dup
64
+ end
65
+
66
+ # Visitor Hooks
67
+ [
68
+ :operation_definition, :fragment_definition,
69
+ :inline_fragment, :field, :directive, :argument, :fragment_spread
70
+ ].each do |node_type|
71
+ module_eval <<-RUBY, __FILE__, __LINE__
72
+ def call_on_enter_#{node_type}(node, parent)
73
+ @analyzers.each do |a|
74
+ begin
75
+ a.on_enter_#{node_type}(node, parent, self)
76
+ rescue AnalysisError => err
77
+ @rescued_errors << err
78
+ end
79
+ end
80
+ end
81
+
82
+ def call_on_leave_#{node_type}(node, parent)
83
+ @analyzers.each do |a|
84
+ begin
85
+ a.on_leave_#{node_type}(node, parent, self)
86
+ rescue AnalysisError => err
87
+ @rescued_errors << err
88
+ end
89
+ end
90
+ end
91
+
92
+ RUBY
93
+ end
94
+
95
+ def on_operation_definition(node, parent)
96
+ object_type = @schema.root_type_for_operation(node.operation_type)
97
+ @object_types.push(object_type)
98
+ @path.push("#{node.operation_type}#{node.name ? " #{node.name}" : ""}")
99
+ call_on_enter_operation_definition(node, parent)
100
+ super
101
+ call_on_leave_operation_definition(node, parent)
102
+ @object_types.pop
103
+ @path.pop
104
+ end
105
+
106
+ def on_fragment_definition(node, parent)
107
+ on_fragment_with_type(node) do
108
+ @path.push("fragment #{node.name}")
109
+ @in_fragment_def = false
110
+ call_on_enter_fragment_definition(node, parent)
111
+ super
112
+ @in_fragment_def = false
113
+ call_on_leave_fragment_definition(node, parent)
114
+ end
115
+ end
116
+
117
+ def on_inline_fragment(node, parent)
118
+ on_fragment_with_type(node) do
119
+ @path.push("...#{node.type ? " on #{node.type.name}" : ""}")
120
+ @skipping = @skip_stack.last || skip?(node)
121
+ @skip_stack << @skipping
122
+
123
+ call_on_enter_inline_fragment(node, parent)
124
+ super
125
+ @skipping = @skip_stack.pop
126
+ call_on_leave_inline_fragment(node, parent)
127
+ end
128
+ end
129
+
130
+ def on_field(node, parent)
131
+ @response_path.push(node.alias || node.name)
132
+ parent_type = @object_types.last
133
+ # This could be nil if the previous field wasn't found:
134
+ field_definition = parent_type && @schema.get_field(parent_type, node.name, @query.context)
135
+ @field_definitions.push(field_definition)
136
+ if !field_definition.nil?
137
+ next_object_type = field_definition.type.unwrap
138
+ @object_types.push(next_object_type)
139
+ else
140
+ @object_types.push(nil)
141
+ end
142
+ @path.push(node.alias || node.name)
143
+
144
+ @skipping = @skip_stack.last || skip?(node)
145
+ @skip_stack << @skipping
146
+
147
+ call_on_enter_field(node, parent)
148
+ super
149
+ @skipping = @skip_stack.pop
150
+ call_on_leave_field(node, parent)
151
+ @response_path.pop
152
+ @field_definitions.pop
153
+ @object_types.pop
154
+ @path.pop
155
+ end
156
+
157
+ def on_directive(node, parent)
158
+ directive_defn = @schema.directives[node.name]
159
+ @directive_definitions.push(directive_defn)
160
+ call_on_enter_directive(node, parent)
161
+ super
162
+ call_on_leave_directive(node, parent)
163
+ @directive_definitions.pop
164
+ end
165
+
166
+ def on_argument(node, parent)
167
+ argument_defn = if (arg = @argument_definitions.last)
168
+ arg_type = arg.type.unwrap
169
+ if arg_type.kind.input_object?
170
+ arg_type.get_argument(node.name, @query.context)
171
+ else
172
+ nil
173
+ end
174
+ elsif (directive_defn = @directive_definitions.last)
175
+ directive_defn.get_argument(node.name, @query.context)
176
+ elsif (field_defn = @field_definitions.last)
177
+ field_defn.get_argument(node.name, @query.context)
178
+ else
179
+ nil
180
+ end
181
+
182
+ @argument_definitions.push(argument_defn)
183
+ @path.push(node.name)
184
+ call_on_enter_argument(node, parent)
185
+ super
186
+ call_on_leave_argument(node, parent)
187
+ @argument_definitions.pop
188
+ @path.pop
189
+ end
190
+
191
+ def on_fragment_spread(node, parent)
192
+ @path.push("... #{node.name}")
193
+ @skipping = @skip_stack.last || skip?(node)
194
+ @skip_stack << @skipping
195
+
196
+ call_on_enter_fragment_spread(node, parent)
197
+ enter_fragment_spread_inline(node)
198
+ super
199
+ @skipping = @skip_stack.pop
200
+ leave_fragment_spread_inline(node)
201
+ call_on_leave_fragment_spread(node, parent)
202
+ @path.pop
203
+ end
204
+
205
+ # @return [GraphQL::BaseType] The current object type
206
+ def type_definition
207
+ @object_types.last
208
+ end
209
+
210
+ # @return [GraphQL::BaseType] The type which the current type came from
211
+ def parent_type_definition
212
+ @object_types[-2]
213
+ end
214
+
215
+ # @return [GraphQL::Field, nil] The most-recently-entered GraphQL::Field, if currently inside one
216
+ def field_definition
217
+ @field_definitions.last
218
+ end
219
+
220
+ # @return [GraphQL::Field, nil] The GraphQL field which returned the object that the current field belongs to
221
+ def previous_field_definition
222
+ @field_definitions[-2]
223
+ end
224
+
225
+ # @return [GraphQL::Directive, nil] The most-recently-entered GraphQL::Directive, if currently inside one
226
+ def directive_definition
227
+ @directive_definitions.last
228
+ end
229
+
230
+ # @return [GraphQL::Argument, nil] The most-recently-entered GraphQL::Argument, if currently inside one
231
+ def argument_definition
232
+ @argument_definitions.last
233
+ end
234
+
235
+ # @return [GraphQL::Argument, nil] The previous GraphQL argument
236
+ def previous_argument_definition
237
+ @argument_definitions[-2]
238
+ end
239
+
240
+ private
241
+
242
+ # Visit a fragment spread inline instead of visiting the definition
243
+ # by itself.
244
+ def enter_fragment_spread_inline(fragment_spread)
245
+ fragment_def = query.fragments[fragment_spread.name]
246
+
247
+ object_type = if fragment_def.type
248
+ @query.warden.get_type(fragment_def.type.name)
249
+ else
250
+ object_types.last
251
+ end
252
+
253
+ object_types << object_type
254
+
255
+ on_fragment_definition_children(fragment_def)
256
+ end
257
+
258
+ # Visit a fragment spread inline instead of visiting the definition
259
+ # by itself.
260
+ def leave_fragment_spread_inline(_fragment_spread)
261
+ object_types.pop
262
+ end
263
+
264
+ def skip?(ast_node)
265
+ dir = ast_node.directives
266
+ dir.any? && !GraphQL::Execution::DirectiveChecks.include?(dir, query)
267
+ end
268
+
269
+ def on_fragment_with_type(node)
270
+ object_type = if node.type
271
+ @query.warden.get_type(node.type.name)
272
+ else
273
+ @object_types.last
274
+ end
275
+ @object_types.push(object_type)
276
+ yield(node)
277
+ @object_types.pop
278
+ @path.pop
279
+ end
280
+ end
281
+ end
282
+ end
@@ -1,2 +1,93 @@
1
1
  # frozen_string_literal: true
2
- require "graphql/analysis/ast"
2
+ require "graphql/analysis/visitor"
3
+ require "graphql/analysis/analyzer"
4
+ require "graphql/analysis/field_usage"
5
+ require "graphql/analysis/query_complexity"
6
+ require "graphql/analysis/max_query_complexity"
7
+ require "graphql/analysis/query_depth"
8
+ require "graphql/analysis/max_query_depth"
9
+ require "timeout"
10
+
11
+ module GraphQL
12
+ module Analysis
13
+ AST = self
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::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::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::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
@@ -21,6 +21,7 @@ module GraphQL
21
21
  manager = spawn_fiber do
22
22
  while first_pass || job_fibers.any?
23
23
  first_pass = false
24
+ fiber_vars = get_fiber_variables
24
25
 
25
26
  while (f = (job_fibers.shift || spawn_job_fiber))
26
27
  if f.alive?
@@ -34,6 +35,7 @@ module GraphQL
34
35
  next_job_fibers.clear
35
36
 
36
37
  Sync do |root_task|
38
+ set_fiber_variables(fiber_vars)
37
39
  while source_tasks.any? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }
38
40
  while (task = source_tasks.shift || spawn_source_task(root_task, sources_condition))
39
41
  if task.alive?
@@ -8,7 +8,7 @@ module GraphQL
8
8
  # simple internal code while adding the option to add Dataloader.
9
9
  class NullDataloader < Dataloader
10
10
  # These are all no-ops because code was
11
- # executed sychronously.
11
+ # executed synchronously.
12
12
  def run; end
13
13
  def run_isolated; yield; end
14
14
  def yield
@@ -5,7 +5,7 @@ module GraphQL
5
5
  class Interpreter
6
6
  class Runtime
7
7
  module GraphQLResult
8
- def initialize(result_name, result_type, application_value, parent_result, is_non_null_in_parent)
8
+ def initialize(result_name, result_type, application_value, parent_result, is_non_null_in_parent, selections, is_eager)
9
9
  @graphql_parent = parent_result
10
10
  @graphql_application_value = application_value
11
11
  @graphql_result_type = result_type
@@ -16,6 +16,8 @@ module GraphQL
16
16
  @graphql_is_non_null_in_parent = is_non_null_in_parent
17
17
  # Jump through some hoops to avoid creating this duplicate storage if at all possible.
18
18
  @graphql_metadata = nil
19
+ @graphql_selections = selections
20
+ @graphql_is_eager = is_eager
19
21
  end
20
22
 
21
23
  def path
@@ -28,14 +30,15 @@ module GraphQL
28
30
  end
29
31
 
30
32
  attr_accessor :graphql_dead
31
- attr_reader :graphql_parent, :graphql_result_name, :graphql_is_non_null_in_parent, :graphql_application_value, :graphql_result_type
33
+ attr_reader :graphql_parent, :graphql_result_name, :graphql_is_non_null_in_parent,
34
+ :graphql_application_value, :graphql_result_type, :graphql_selections, :graphql_is_eager
32
35
 
33
36
  # @return [Hash] Plain-Ruby result data (`@graphql_metadata` contains Result wrapper objects)
34
37
  attr_accessor :graphql_result_data
35
38
  end
36
39
 
37
40
  class GraphQLResultHash
38
- def initialize(_result_name, _result_type, _application_value, _parent_result, _is_non_null_in_parent)
41
+ def initialize(_result_name, _result_type, _application_value, _parent_result, _is_non_null_in_parent, _selections, _is_eager)
39
42
  super
40
43
  @graphql_result_data = {}
41
44
  end
@@ -123,7 +126,7 @@ module GraphQL
123
126
  class GraphQLResultArray
124
127
  include GraphQLResult
125
128
 
126
- def initialize(_result_name, _result_type, _application_value, _parent_result, _is_non_null_in_parent)
129
+ def initialize(_result_name, _result_type, _application_value, _parent_result, _is_non_null_in_parent, _selections, _is_eager)
127
130
  super
128
131
  @graphql_result_data = []
129
132
  end