graphql 0.15.3 → 0.16.0

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.
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