graphql 0.15.3 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +4 -1
  3. data/lib/graphql/analysis.rb +5 -0
  4. data/lib/graphql/analysis/analyze_query.rb +73 -0
  5. data/lib/graphql/analysis/max_query_complexity.rb +25 -0
  6. data/lib/graphql/analysis/max_query_depth.rb +25 -0
  7. data/lib/graphql/analysis/query_complexity.rb +122 -0
  8. data/lib/graphql/analysis/query_depth.rb +54 -0
  9. data/lib/graphql/analysis_error.rb +4 -0
  10. data/lib/graphql/base_type.rb +7 -0
  11. data/lib/graphql/define/assign_object_field.rb +2 -1
  12. data/lib/graphql/field.rb +25 -3
  13. data/lib/graphql/input_object_type.rb +1 -1
  14. data/lib/graphql/internal_representation.rb +2 -0
  15. data/lib/graphql/internal_representation/node.rb +81 -0
  16. data/lib/graphql/internal_representation/rewrite.rb +177 -0
  17. data/lib/graphql/language/visitor.rb +15 -9
  18. data/lib/graphql/object_type.rb +1 -1
  19. data/lib/graphql/query.rb +66 -7
  20. data/lib/graphql/query/context.rb +10 -3
  21. data/lib/graphql/query/directive_resolution.rb +5 -5
  22. data/lib/graphql/query/serial_execution.rb +5 -3
  23. data/lib/graphql/query/serial_execution/field_resolution.rb +22 -15
  24. data/lib/graphql/query/serial_execution/operation_resolution.rb +7 -5
  25. data/lib/graphql/query/serial_execution/selection_resolution.rb +20 -105
  26. data/lib/graphql/query/serial_execution/value_resolution.rb +15 -12
  27. data/lib/graphql/schema.rb +7 -2
  28. data/lib/graphql/schema/timeout_middleware.rb +67 -0
  29. data/lib/graphql/static_validation/all_rules.rb +0 -1
  30. data/lib/graphql/static_validation/type_stack.rb +7 -11
  31. data/lib/graphql/static_validation/validation_context.rb +11 -1
  32. data/lib/graphql/static_validation/validator.rb +14 -4
  33. data/lib/graphql/version.rb +1 -1
  34. data/readme.md +10 -9
  35. data/spec/graphql/analysis/analyze_query_spec.rb +50 -0
  36. data/spec/graphql/analysis/max_query_complexity_spec.rb +62 -0
  37. data/spec/graphql/{static_validation/rules/document_does_not_exceed_max_depth_spec.rb → analysis/max_query_depth_spec.rb} +20 -21
  38. data/spec/graphql/analysis/query_complexity_spec.rb +235 -0
  39. data/spec/graphql/analysis/query_depth_spec.rb +80 -0
  40. data/spec/graphql/directive_spec.rb +1 -0
  41. data/spec/graphql/internal_representation/rewrite_spec.rb +120 -0
  42. data/spec/graphql/introspection/schema_type_spec.rb +1 -0
  43. data/spec/graphql/language/visitor_spec.rb +14 -4
  44. data/spec/graphql/non_null_type_spec.rb +31 -0
  45. data/spec/graphql/query/context_spec.rb +24 -1
  46. data/spec/graphql/query_spec.rb +6 -2
  47. data/spec/graphql/schema/timeout_middleware_spec.rb +180 -0
  48. data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +1 -1
  49. data/spec/graphql/static_validation/rules/arguments_are_defined_spec.rb +1 -1
  50. data/spec/graphql/static_validation/rules/directives_are_defined_spec.rb +1 -1
  51. data/spec/graphql/static_validation/rules/directives_are_in_valid_locations_spec.rb +1 -1
  52. data/spec/graphql/static_validation/rules/fields_are_defined_on_type_spec.rb +1 -1
  53. data/spec/graphql/static_validation/rules/fields_have_appropriate_selections_spec.rb +1 -1
  54. data/spec/graphql/static_validation/rules/fields_will_merge_spec.rb +1 -1
  55. data/spec/graphql/static_validation/rules/fragment_spreads_are_possible_spec.rb +1 -1
  56. data/spec/graphql/static_validation/rules/fragment_types_exist_spec.rb +1 -1
  57. data/spec/graphql/static_validation/rules/fragments_are_finite_spec.rb +1 -1
  58. data/spec/graphql/static_validation/rules/fragments_are_on_composite_types_spec.rb +1 -1
  59. data/spec/graphql/static_validation/rules/fragments_are_used_spec.rb +1 -1
  60. data/spec/graphql/static_validation/rules/required_arguments_are_present_spec.rb +1 -1
  61. data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +1 -1
  62. data/spec/graphql/static_validation/rules/variable_usages_are_allowed_spec.rb +1 -1
  63. data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +1 -1
  64. data/spec/graphql/static_validation/rules/variables_are_used_and_defined_spec.rb +1 -1
  65. data/spec/graphql/static_validation/validator_spec.rb +1 -1
  66. data/spec/support/dairy_app.rb +22 -1
  67. metadata +29 -5
  68. data/lib/graphql/static_validation/rules/document_does_not_exceed_max_depth.rb +0 -79
@@ -0,0 +1,2 @@
1
+ require "graphql/internal_representation/node"
2
+ require "graphql/internal_representation/rewrite"
@@ -0,0 +1,81 @@
1
+ require "set"
2
+
3
+ module GraphQL
4
+ module InternalRepresentation
5
+ class Node
6
+ def initialize(ast_node:, return_type: nil, on_types: Set.new, name: nil, definition: nil, children: {}, spreads: [], directives: Set.new)
7
+ @ast_node = ast_node
8
+ @return_type = return_type
9
+ @on_types = on_types
10
+ @name = name
11
+ @definition = definition
12
+ @children = children
13
+ @spreads = spreads
14
+ @directives = directives
15
+ end
16
+
17
+ # Note: by the time this gets out of the Rewrite phase, this will be empty -- it's emptied out when fragments are merged back in
18
+ # @return [Array<GraphQL::Language::Nodes::FragmentSpreads>] Fragment names that were spread in this node
19
+ attr_reader :spreads
20
+
21
+ # These are the compiled directives from fragment spreads, inline fragments, and the field itself
22
+ # @return [Set<GraphQL::Language::Nodes::Directive>]
23
+ attr_reader :directives
24
+
25
+ # @return [GraphQL::Field, GraphQL::Directive] The definition to use to execute this node
26
+ attr_reader :definition
27
+
28
+ # @return [String] the name to use for the result in the response hash
29
+ attr_reader :name
30
+
31
+ # @return [GraphQL::Language::Nodes::AbstractNode] The AST node (or one of the nodes) where this was derived from
32
+ attr_reader :ast_node
33
+
34
+ # This may come from the previous field's return value or an explicitly-typed fragment
35
+ # @example On-type from previous return value
36
+ # {
37
+ # person(id: 1) {
38
+ # firstName # => on_type is person
39
+ # }
40
+ # }
41
+ # @example On-type from explicit type condition
42
+ # {
43
+ # node(id: $nodeId) {
44
+ # ... on Nameable {
45
+ # firstName # => on_type is Nameable
46
+ # }
47
+ # }
48
+ # }
49
+ # @return [Set<GraphQL::ObjectType, GraphQL::InterfaceType>] the types this field applies to
50
+ attr_reader :on_types
51
+
52
+ # @return [GraphQL::BaseType]
53
+ attr_reader :return_type
54
+
55
+ # @return [Array<GraphQL::Query::Node>]
56
+ attr_reader :children
57
+
58
+ def inspect(indent = 0)
59
+ own_indent = " " * indent
60
+ self_inspect = "#{own_indent}<Node #{name} (#{definition ? definition.name + ": " : ""}{#{on_types.to_a.join("|")}} -> #{return_type})>"
61
+ if children.any?
62
+ self_inspect << " {\n#{children.values.map { |n| n.inspect(indent + 2 )}.join("\n")}\n#{own_indent}}"
63
+ end
64
+ self_inspect
65
+ end
66
+
67
+ def dup
68
+ self.class.new({
69
+ ast_node: ast_node,
70
+ return_type: return_type,
71
+ on_types: on_types,
72
+ name: name,
73
+ definition: definition,
74
+ children: children,
75
+ spreads: spreads,
76
+ directives: directives,
77
+ })
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,177 @@
1
+ module GraphQL
2
+ module InternalRepresentation
3
+ # Convert an AST into a tree of {InternalRepresentation::Node}s
4
+ #
5
+ # This rides along with {StaticValidation}, building a tree of nodes.
6
+ #
7
+ # However, if any errors occurred during validation, the resulting tree is bogus.
8
+ # (For example, `nil` could have been pushed instead of a type.)
9
+ class Rewrite
10
+ include GraphQL::Language
11
+
12
+ # @return [Hash<String => InternalRepresentation::Node>] internal representation of each query root (operation, fragment)
13
+ attr_reader :operations
14
+
15
+ def initialize
16
+ # { String => Node } Tracks the roots of the query
17
+ @operations = {}
18
+ @fragments = {}
19
+ # [String...] fragments which don't have fragments inside them
20
+ @independent_fragments = []
21
+ # Tracks the current node during traversal
22
+ # Stack<InternalRepresentation::Node>
23
+ @nodes = []
24
+ # This tracks dependencies from fragment to Node where it was used
25
+ # { frag_name => [dependent_node, dependent_node]}
26
+ @fragment_spreads = Hash.new { |h, k| h[k] = []}
27
+ # [Nodes::Directive ... ] directive affecting the current scope
28
+ @parent_directives = []
29
+ end
30
+
31
+ def validate(context)
32
+ visitor = context.visitor
33
+
34
+ visitor[Nodes::OperationDefinition].enter << -> (ast_node, prev_ast_node) {
35
+ node = Node.new(
36
+ return_type: context.type_definition.unwrap,
37
+ ast_node: ast_node,
38
+ )
39
+ @nodes.push(node)
40
+ @operations[ast_node.name] = node
41
+ }
42
+
43
+ visitor[Nodes::Field].enter << -> (ast_node, prev_ast_node) {
44
+ parent_node = @nodes.last
45
+ node_name = ast_node.alias || ast_node.name
46
+ # This node might not be novel, eg inside an inline fragment
47
+ # but it could contain new type information, which is captured below.
48
+ # (StaticValidation ensures that merging fields is fair game)
49
+ node = parent_node.children[node_name] ||= begin
50
+ Node.new(
51
+ return_type: context.type_definition && context.type_definition.unwrap,
52
+ ast_node: ast_node,
53
+ name: node_name,
54
+ definition: context.field_definition,
55
+ )
56
+ end
57
+ node.on_types.add(context.parent_type_definition.unwrap)
58
+ @nodes.push(node)
59
+ @parent_directives.push([])
60
+ }
61
+
62
+ visitor[Nodes::InlineFragment].enter << -> (ast_node, prev_ast_node) {
63
+ @parent_directives.push([])
64
+ }
65
+
66
+ visitor[Nodes::Directive].enter << -> (ast_node, prev_ast_node) {
67
+ # It could be a query error where a directive is somewhere it shouldn't be
68
+ if @parent_directives.any?
69
+ @parent_directives.last << Node.new(
70
+ name: ast_node.name,
71
+ ast_node: ast_node,
72
+ definition: context.directive_definition,
73
+ )
74
+ end
75
+ }
76
+
77
+ visitor[Nodes::FragmentSpread].enter << -> (ast_node, prev_ast_node) {
78
+ # Record _both sides_ of the dependency
79
+ spread_node = Node.new(
80
+ name: ast_node.name,
81
+ ast_node: ast_node,
82
+ )
83
+ parent_node = @nodes.last
84
+ # The parent node has a reference to the fragment
85
+ parent_node.spreads.push(spread_node)
86
+ # And keep a reference from the fragment to the parent node
87
+ @fragment_spreads[ast_node.name].push(parent_node)
88
+ @nodes.push(spread_node)
89
+ @parent_directives.push([])
90
+ }
91
+
92
+ visitor[Nodes::FragmentDefinition].enter << -> (ast_node, prev_ast_node) {
93
+ node = Node.new(
94
+ name: ast_node.name,
95
+ return_type: context.type_definition,
96
+ ast_node: ast_node,
97
+ )
98
+ @nodes.push(node)
99
+ @fragments[ast_node.name] = node
100
+ }
101
+
102
+ visitor[Nodes::InlineFragment].leave << -> (ast_node, prev_ast_node) {
103
+ @parent_directives.pop
104
+ }
105
+
106
+ visitor[Nodes::FragmentSpread].leave << -> (ast_node, prev_ast_node) {
107
+ # Capture any directives that apply to this spread
108
+ # so that they can be applied to fields when
109
+ # the fragment is merged in later
110
+ spread_node = @nodes.pop
111
+ spread_node.directives.merge(@parent_directives.flatten)
112
+ @parent_directives.pop
113
+ }
114
+
115
+ visitor[Nodes::FragmentDefinition].leave << -> (ast_node, prev_ast_node) {
116
+ # This fragment doesn't depend on any others,
117
+ # we should save it as the starting point for dependency resolution
118
+ frag_node = @nodes.pop
119
+ if frag_node.spreads.none?
120
+ @independent_fragments << frag_node
121
+ end
122
+ }
123
+
124
+ visitor[Nodes::OperationDefinition].leave << -> (ast_node, prev_ast_node) {
125
+ @nodes.pop
126
+ }
127
+
128
+ visitor[Nodes::Field].leave << -> (ast_node, prev_ast_node) {
129
+ # Pop this field's node
130
+ # and record any directives that were visited
131
+ # during this field & before it (eg, inline fragments)
132
+ field_node = @nodes.pop
133
+ field_node.directives.merge(@parent_directives.flatten)
134
+ @parent_directives.pop
135
+ }
136
+
137
+ visitor[Nodes::Document].leave << -> (ast_node, prev_ast_node) {
138
+ # Resolve fragment dependencies. Start with fragments with no
139
+ # dependencies and work along the spreads.
140
+ while fragment_node = @independent_fragments.pop
141
+ fragment_usages = @fragment_spreads[fragment_node.name]
142
+ while dependent_node = fragment_usages.pop
143
+ # remove self from dependent_node.spreads
144
+ rejected_spread_nodes = dependent_node.spreads.select { |spr| spr.name == fragment_node.name }
145
+ rejected_spread_nodes.each { |r_node| dependent_node.spreads.delete(r_node) }
146
+
147
+ # resolve the dependency (merge into dependent node)
148
+ deep_merge(dependent_node, fragment_node, rejected_spread_nodes.first.directives)
149
+
150
+ if dependent_node.spreads.none? && dependent_node.ast_node.is_a?(Nodes::FragmentDefinition)
151
+ @independent_fragments.push(dependent_node)
152
+ end
153
+ end
154
+ end
155
+ }
156
+ end
157
+
158
+ private
159
+
160
+ # Merge the chilren from `fragment_node` into `parent_node`. Merge `directives` into each of those fields.
161
+ def deep_merge(parent_node, fragment_node, directives)
162
+ fragment_node.children.each do |name, child_node|
163
+ deep_merge_child(parent_node, name, child_node, directives)
164
+ end
165
+ end
166
+
167
+ # Merge `node` into `parent_node`'s children, as `name`, applying `extra_directives`
168
+ def deep_merge_child(parent_node, name, node, extra_directives)
169
+ child_node = parent_node.children[name] ||= node.dup
170
+ node.children.each do |merge_child_name, merge_child_node|
171
+ deep_merge_child(child_node, merge_child_name, merge_child_node, [])
172
+ end
173
+ child_node.directives.merge(extra_directives)
174
+ end
175
+ end
176
+ end
177
+ end
@@ -4,12 +4,12 @@ module GraphQL
4
4
  #
5
5
  # @example Create a visitor, add hooks, then search a document
6
6
  # total_field_count = 0
7
- # visitor = GraphQL::Language::Visitor.new
7
+ # visitor = GraphQL::Language::Visitor.new(document)
8
8
  # # Whenever you find a field, increment the field count:
9
9
  # visitor[GraphQL::Language::Nodes::Field] << -> (node) { total_field_count += 1 }
10
10
  # # When we finish, print the field count:
11
11
  # visitor[GraphQL::Language::Nodes::Document].leave << -> (node) { p total_field_count }
12
- # visitor.visit(document)
12
+ # visitor.visit
13
13
  # # => 6
14
14
  #
15
15
  class Visitor
@@ -22,7 +22,8 @@ module GraphQL
22
22
  # @return [Array<Proc>] Hooks to call when leaving _any_ node
23
23
  attr_reader :leave
24
24
 
25
- def initialize
25
+ def initialize(document)
26
+ @document = document
26
27
  @visitors = {}
27
28
  @enter = []
28
29
  @leave = []
@@ -38,17 +39,22 @@ module GraphQL
38
39
  @visitors[node_class] ||= NodeVisitor.new
39
40
  end
40
41
 
41
- # Visit `root` and all children, applying hooks as you go
42
- # @param root [GraphQL::Language::Nodes::AbstractNode] some node to start parsing on
42
+ # Visit `document` and all children, applying hooks as you go
43
43
  # @return [void]
44
- def visit(root, parent=nil)
45
- begin_visit(root, parent) &&
46
- root.children.reduce(true) { |memo, child| memo && visit(child, root) }
47
- end_visit(root, parent)
44
+ def visit
45
+ visit_node(@document, nil)
48
46
  end
49
47
 
50
48
  private
51
49
 
50
+ def visit_node(node, parent)
51
+ begin_hooks_ok = begin_visit(node, parent)
52
+ if begin_hooks_ok
53
+ node.children.reduce(true) { |memo, child| memo && visit_node(child, node) }
54
+ end
55
+ end_visit(node, parent)
56
+ end
57
+
52
58
  def begin_visit(node, parent)
53
59
  self.class.apply_hooks(enter, node, parent)
54
60
  node_visitor = self[node.class]
@@ -24,7 +24,7 @@ module GraphQL
24
24
  accepts_definitions :interfaces, field: GraphQL::Define::AssignObjectField
25
25
  attr_accessor :name, :description
26
26
 
27
- # @return [Hash<String, GraphQL::Field>] Map String fieldnames to their {GraphQL::Field} implementations
27
+ # @return [Hash<String => GraphQL::Field>] Map String fieldnames to their {GraphQL::Field} implementations
28
28
  attr_accessor :fields
29
29
 
30
30
  def initialize
@@ -8,7 +8,7 @@ module GraphQL
8
8
  end
9
9
  end
10
10
 
11
- attr_reader :schema, :document, :context, :fragments, :operations, :root_value, :max_depth
11
+ attr_reader :schema, :document, :context, :fragments, :operations, :root_value, :max_depth, :query_string
12
12
 
13
13
  # Prepare query `query_string` on `schema`
14
14
  # @param schema [GraphQL::Schema]
@@ -18,11 +18,21 @@ module GraphQL
18
18
  # @param validate [Boolean] if true, `query_string` will be validated with {StaticValidation::Validator}
19
19
  # @param operation_name [String] if the query string contains many operations, this is the one which should be executed
20
20
  # @param root_value [Object] the object used to resolve fields on the root type
21
- def initialize(schema, query_string = nil, document: nil, context: nil, variables: {}, validate: true, operation_name: nil, root_value: nil, max_depth: nil)
21
+ # @param max_depth [Numeric] the maximum number of nested selections allowed for this query (falls back to schema-level value)
22
+ # @param max_complexity [Numeric] the maximum field complexity for this query (falls back to schema-level value)
23
+ def initialize(schema, query_string = nil, document: nil, context: nil, variables: {}, validate: true, operation_name: nil, root_value: nil, max_depth: nil, max_complexity: nil)
22
24
  fail ArgumentError, "a query string or document is required" unless query_string || document
23
25
 
24
26
  @schema = schema
25
27
  @max_depth = max_depth || schema.max_depth
28
+ @max_complexity = max_complexity || schema.max_complexity
29
+ @query_analyzers = schema.query_analyzers.dup
30
+ if @max_depth
31
+ @query_analyzers << GraphQL::Analysis::MaxQueryDepth.new(@max_depth)
32
+ end
33
+ if @max_complexity
34
+ @query_analyzers << GraphQL::Analysis::MaxQueryComplexity.new(@max_complexity)
35
+ end
26
36
  @context = Context.new(query: self, values: context)
27
37
  @root_value = root_value
28
38
  @validate = validate
@@ -30,6 +40,7 @@ module GraphQL
30
40
  @fragments = {}
31
41
  @operations = {}
32
42
  @provided_variables = variables
43
+ @query_string = query_string
33
44
 
34
45
  @document = document || GraphQL.parse(query_string)
35
46
  @document.definitions.each do |part|
@@ -39,15 +50,20 @@ module GraphQL
39
50
  @operations[part.name] = part
40
51
  end
41
52
  end
53
+
54
+ @arguments_cache = {}
42
55
  end
43
56
 
44
57
  # Get the result for this query, executing it once
45
58
  def result
46
- if @validate && validation_errors.any?
47
- return { "errors" => validation_errors }
59
+ @result ||= begin
60
+ if @validate && (validation_errors.any? || analysis_errors.any?)
61
+ { "errors" => validation_errors + analysis_errors}
62
+ else
63
+ Executor.new(self).result
64
+ end
48
65
  end
49
66
 
50
- @result ||= Executor.new(self).result
51
67
  end
52
68
 
53
69
 
@@ -71,10 +87,39 @@ module GraphQL
71
87
  )
72
88
  end
73
89
 
74
- private
90
+ def internal_representation
91
+ @internal_representation ||= begin
92
+ perform_validation
93
+ @internal_representation
94
+ end
95
+ end
75
96
 
76
97
  def validation_errors
77
- @validation_errors ||= schema.static_validator.validate(self)
98
+ @validation_errors ||= begin
99
+ perform_validation
100
+ @validation_errors
101
+ end
102
+ end
103
+
104
+ # Node-level cache for calculating arguments. Used during execution and query analysis.
105
+ # @return [GraphQL::Query::Arguments] Arguments for this node, merging default values, literal values and query variables
106
+ def arguments_for(irep_node)
107
+ @arguments_cache[irep_node] ||= begin
108
+ GraphQL::Query::LiteralInput.from_arguments(
109
+ irep_node.ast_node.arguments,
110
+ irep_node.definition.arguments,
111
+ self.variables
112
+ )
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def perform_validation
119
+ validation_result = schema.static_validator.validate(self)
120
+ @validation_errors = validation_result[:errors]
121
+ @internal_representation = validation_result[:irep]
122
+ nil
78
123
  end
79
124
 
80
125
 
@@ -89,6 +134,20 @@ module GraphQL
89
134
  operations[operation_name]
90
135
  end
91
136
  end
137
+
138
+ def analysis_errors
139
+ @analysis_errors ||= begin
140
+ if validation_errors.any?
141
+ # Can't reduce an invalid query
142
+ []
143
+ elsif @query_analyzers.any?
144
+ reduce_results = GraphQL::Analysis.analyze_query(self, @query_analyzers)
145
+ reduce_results.select { |r| r.is_a?(GraphQL::AnalysisError) }.map(&:to_h)
146
+ else
147
+ []
148
+ end
149
+ end
150
+ end
92
151
  end
93
152
  end
94
153