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
@@ -23,7 +23,6 @@ module GraphQL
23
23
  GraphQL::StaticValidation::VariableDefaultValuesAreCorrectlyTyped,
24
24
  GraphQL::StaticValidation::VariablesAreUsedAndDefined,
25
25
  GraphQL::StaticValidation::VariableUsagesAreAllowed,
26
- GraphQL::StaticValidation::DocumentDoesNotExceedMaxDepth,
27
26
  ]
28
27
  end
29
28
  end
@@ -89,18 +89,14 @@ module GraphQL
89
89
  def push(stack, node)
90
90
  parent_type = stack.object_types.last
91
91
  parent_type = parent_type.unwrap
92
- if parent_type.kind.fields?
93
- field_class = stack.schema.get_field(parent_type, node.name)
94
- stack.field_definitions.push(field_class)
95
- if !field_class.nil?
96
- next_object_type = field_class.type
97
- stack.object_types.push(next_object_type)
98
- else
99
- stack.object_types.push(nil)
100
- end
92
+
93
+ field_definition = stack.schema.get_field(parent_type, node.name)
94
+ stack.field_definitions.push(field_definition)
95
+ if !field_definition.nil?
96
+ next_object_type = field_definition.type
97
+ stack.object_types.push(next_object_type)
101
98
  else
102
- stack.field_definitions.push(nil)
103
- stack.object_types.push(parent_type)
99
+ stack.object_types.push(nil)
104
100
  end
105
101
  end
106
102
 
@@ -29,7 +29,7 @@ module GraphQL
29
29
  end
30
30
 
31
31
  @errors = []
32
- @visitor = GraphQL::Language::Visitor.new
32
+ @visitor = GraphQL::Language::Visitor.new(document)
33
33
  @type_stack = GraphQL::StaticValidation::TypeStack.new(schema, visitor)
34
34
  end
35
35
 
@@ -37,6 +37,16 @@ module GraphQL
37
37
  @type_stack.object_types
38
38
  end
39
39
 
40
+ # @return [GraphQL::BaseType] The current object type
41
+ def type_definition
42
+ object_types.last
43
+ end
44
+
45
+ # @return [GraphQL::BaseType] The type which the current type came from
46
+ def parent_type_definition
47
+ object_types[-2]
48
+ end
49
+
40
50
  # @return [GraphQL::Field, nil] The most-recently-entered GraphQL::Field, if currently inside one
41
51
  def field_definition
42
52
  @type_stack.field_definitions.last
@@ -17,16 +17,26 @@ module GraphQL
17
17
  @rules = rules
18
18
  end
19
19
 
20
- # Validate `document` against the schema. Returns an array of message hashes.
21
- # @param document [GraphQL::Language::Nodes::Document]
20
+ # Validate `query` against the schema. Returns an array of message hashes.
21
+ # @param query [GraphQL::Query]
22
22
  # @return [Array<Hash>]
23
23
  def validate(query)
24
24
  context = GraphQL::StaticValidation::ValidationContext.new(query)
25
+ rewrite = GraphQL::InternalRepresentation::Rewrite.new
26
+
27
+ # Put this first so its enters and exits are always called
28
+ rewrite.validate(context)
25
29
  @rules.each do |rules|
26
30
  rules.new.validate(context)
27
31
  end
28
- context.visitor.visit(query.document)
29
- context.errors.map(&:to_h)
32
+
33
+ context.visitor.visit
34
+
35
+ {
36
+ errors: context.errors.map(&:to_h),
37
+ # If there were errors, the irep is garbage
38
+ irep: context.errors.none? ? rewrite.operations : nil,
39
+ }
30
40
  end
31
41
  end
32
42
  end
@@ -1,3 +1,3 @@
1
1
  module GraphQL
2
- VERSION = "0.15.3"
2
+ VERSION = "0.16.0"
3
3
  end
data/readme.md CHANGED
@@ -13,6 +13,9 @@ A Ruby implementation of [GraphQL](http://graphql.org/).
13
13
  - [Defining Your Schema](http://www.rubydoc.info/github/rmosolgo/graphql-ruby/file/guides/defining_your_schema.md)
14
14
  - [Executing Queries](http://www.rubydoc.info/github/rmosolgo/graphql-ruby/file/guides/executing_queries.md)
15
15
  - [Testing](http://www.rubydoc.info/github/rmosolgo/graphql-ruby/file/guides/testing.md)
16
+ - [Code Reuse](http://www.rubydoc.info/github/rmosolgo/graphql-ruby/file/guides/code_reuse.md)
17
+ - [Security](http://www.rubydoc.info/github/rmosolgo/graphql-ruby/file/guides/security.md)
18
+
16
19
 
17
20
  - [API Documentation](http://www.rubydoc.info/github/rmosolgo/graphql-ruby)
18
21
 
@@ -119,16 +122,14 @@ If you're building a backend for [Relay](http://facebook.github.io/relay/), you'
119
122
 
120
123
  ## To Do
121
124
 
122
- - __1.0 items:__
123
- - Non-nulls should _propagate_ to the next non-null field (all the way up to data, if need be)
124
- - Add docs for shared behaviors & DRY code
125
- - Subscriptions
126
- - Is there something to do at the graphql-ruby level to make this easier for specific implementations?
127
125
  - Accept type name as `type` argument?
128
126
  - Goal: accept `field :post, "Post"` to look up a type named `"Post"` in the schema
129
127
  - Problem: how does a field know which schema to look up the name from?
130
128
  - Problem: how can we load types in Rails without accessing the constant?
131
- - Customizable complexity validator
132
- - Types / fields can define their "weight" in a query
133
- - Queries can be executed with a "max weight", or Schema can have a default
134
- - During validation, we make sure the query doesn't exceed "max weight"
129
+ - Maybe support by third-party library? `type("Post!")` could implement "type_missing", keeps `graphql-ruby` very simple
130
+ - If we eval'd `define { ... }` blocks _lazily_, would that work around all circular dependency issues?
131
+ - `QueryComplexity` improvements:
132
+ - Better detection of union / interface possibilities? Right now they're summed
133
+ - Type check improvements:
134
+ - Use catch-all type/field/argument definitions instead of terminating traversal
135
+ - Reduce ad-hoc traversals?
@@ -0,0 +1,50 @@
1
+ require "spec_helper"
2
+
3
+ describe GraphQL::Analysis do
4
+ class TypeCollector
5
+ def initial_value(query)
6
+ []
7
+ end
8
+
9
+ def call(memo, visit_type, irep_node)
10
+ if visit_type == :enter
11
+ memo + [irep_node.return_type]
12
+ else
13
+ memo
14
+ end
15
+ end
16
+ end
17
+
18
+ describe ".analyze_query" do
19
+ let(:node_counter) {
20
+ -> (memo, visit_type, irep_node) {
21
+ memo ||= Hash.new { |h,k| h[k] = 0 }
22
+ visit_type == :enter && memo[irep_node.ast_node.class] += 1
23
+ memo
24
+ }
25
+ }
26
+ let(:type_collector) { TypeCollector.new }
27
+ let(:analyzers) { [type_collector, node_counter] }
28
+ let(:reduce_result) { GraphQL::Analysis.analyze_query(query, analyzers) }
29
+ let(:query) { GraphQL::Query.new(DummySchema, query_string) }
30
+ let(:query_string) {%|
31
+ {
32
+ cheese(id: 1) {
33
+ id
34
+ flavor
35
+ }
36
+ }
37
+ |}
38
+
39
+ it "calls the defined analyzers" do
40
+ collected_types, node_counts = reduce_result
41
+ expected_visited_types = [QueryType, CheeseType, GraphQL::INT_TYPE, GraphQL::STRING_TYPE]
42
+ assert_equal expected_visited_types, collected_types
43
+ expected_node_counts = {
44
+ GraphQL::Language::Nodes::OperationDefinition => 1,
45
+ GraphQL::Language::Nodes::Field => 3,
46
+ }
47
+ assert_equal expected_node_counts, node_counts
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,62 @@
1
+ require "spec_helper"
2
+
3
+ describe GraphQL::Analysis::MaxQueryComplexity do
4
+ before do
5
+ @prev_max_complexity = DummySchema.max_complexity
6
+ end
7
+
8
+ after do
9
+ DummySchema.max_complexity = @prev_max_complexity
10
+ end
11
+
12
+
13
+ let(:result) { DummySchema.execute(query_string) }
14
+ let(:query_string) {%|
15
+ {
16
+ a: cheese(id: 1) { id }
17
+ b: cheese(id: 1) { id }
18
+ c: cheese(id: 1) { id }
19
+ d: cheese(id: 1) { id }
20
+ e: cheese(id: 1) { id }
21
+ }
22
+ |}
23
+
24
+ describe "when a query goes over max complexity" do
25
+ before do
26
+ DummySchema.max_complexity = 9
27
+ end
28
+
29
+ it "returns an error" do
30
+ assert_equal "Query has complexity of 10, which exceeds max complexity of 9", result["errors"][0]["message"]
31
+ end
32
+ end
33
+
34
+ describe "when there is no max complexity" do
35
+ before do
36
+ DummySchema.max_complexity = nil
37
+ end
38
+ it "doesn't error" do
39
+ assert_equal nil, result["errors"]
40
+ end
41
+ end
42
+
43
+ describe "when the query is less than the max complexity" do
44
+ before do
45
+ DummySchema.max_complexity = 99
46
+ end
47
+ it "doesn't error" do
48
+ assert_equal nil, result["errors"]
49
+ end
50
+ end
51
+
52
+ describe "when complexity is overriden at query-level" do
53
+ before do
54
+ DummySchema.max_complexity = 100
55
+ end
56
+ let(:result) { DummySchema.execute(query_string, max_complexity: 7) }
57
+
58
+ it "is applied" do
59
+ assert_equal "Query has complexity of 10, which exceeds max complexity of 7", result["errors"][0]["message"]
60
+ end
61
+ end
62
+ end
@@ -1,11 +1,15 @@
1
1
  require "spec_helper"
2
2
 
3
- describe GraphQL::StaticValidation::DocumentDoesNotExceedMaxDepth do
4
- let(:rule) { GraphQL::StaticValidation::DocumentDoesNotExceedMaxDepth }
5
- let(:validator) { GraphQL::StaticValidation::Validator.new(schema: DummySchema, rules: [rule]) }
6
- let(:query) { GraphQL::Query.new(DummySchema, query_string) }
7
- let(:errors) { validator.validate(query) }
3
+ describe GraphQL::Analysis::MaxQueryDepth do
4
+ before do
5
+ @prev_max_depth = DummySchema.max_depth
6
+ end
7
+
8
+ after do
9
+ DummySchema.max_depth = @prev_max_depth
10
+ end
8
11
 
12
+ let(:result) { DummySchema.execute(query_string) }
9
13
  let(:query_string) { "
10
14
  {
11
15
  cheese(id: 1) {
@@ -26,48 +30,43 @@ describe GraphQL::StaticValidation::DocumentDoesNotExceedMaxDepth do
26
30
 
27
31
  describe "when the query is deeper than max depth" do
28
32
  it "adds an error message for a too-deep query" do
29
- assert_equal 1, errors.length
33
+ assert_equal "Query has depth of 7, which exceeds max depth of 5", result["errors"][0]["message"]
30
34
  end
31
35
  end
32
36
 
33
37
  describe "when the query specifies a different max_depth" do
34
- let(:query) { GraphQL::Query.new(DummySchema, query_string, max_depth: 100) }
38
+ let(:result) { DummySchema.execute(query_string, max_depth: 100) }
39
+
35
40
  it "obeys that max_depth" do
36
- assert_equal 0, errors.length
41
+ assert_equal nil, result["errors"]
37
42
  end
38
43
  end
39
44
 
40
45
  describe "When the query is not deeper than max_depth" do
41
46
  before do
42
- @prev_max_depth = DummySchema.max_depth
43
47
  DummySchema.max_depth = 100
44
48
  end
45
49
 
46
- after do
47
- DummySchema.max_depth = @prev_max_depth
48
- end
49
-
50
50
  it "doesn't add an error" do
51
- assert_equal 0, errors.length
51
+ assert_equal nil, result["errors"]
52
52
  end
53
53
  end
54
54
 
55
55
  describe "when the max depth isn't set" do
56
56
  before do
57
- @prev_max_depth = DummySchema.max_depth
58
57
  DummySchema.max_depth = nil
59
58
  end
60
59
 
61
- after do
62
- DummySchema.max_depth = @prev_max_depth
63
- end
64
-
65
60
  it "doesn't add an error message" do
66
- assert_equal 0, errors.length
61
+ assert_equal nil, result["errors"]
67
62
  end
68
63
  end
69
64
 
70
65
  describe "when a fragment exceeds max depth" do
66
+ before do
67
+ DummySchema.max_depth = 4
68
+ end
69
+
71
70
  let(:query_string) { "
72
71
  {
73
72
  cheese(id: 1) {
@@ -95,7 +94,7 @@ describe GraphQL::StaticValidation::DocumentDoesNotExceedMaxDepth do
95
94
  "}
96
95
 
97
96
  it "adds an error message for a too-deep query" do
98
- assert_equal 1, errors.length
97
+ assert_equal 1, result["errors"].length
99
98
  end
100
99
  end
101
100
  end
@@ -0,0 +1,235 @@
1
+ require "spec_helper"
2
+
3
+ describe GraphQL::Analysis::QueryComplexity do
4
+ let(:complexities) { [] }
5
+ let(:query_complexity) { GraphQL::Analysis::QueryComplexity.new { |this_query, complexity| complexities << this_query << complexity } }
6
+ let(:reduce_result) { GraphQL::Analysis.analyze_query(query, [query_complexity]) }
7
+ let(:variables) { {} }
8
+ let(:query) { GraphQL::Query.new(DummySchema, query_string, variables: variables) }
9
+
10
+ describe "simple queries" do
11
+ let(:query_string) {%|
12
+ query cheeses($isSkipped: Boolean = false){
13
+ # complexity of 3
14
+ cheese1: cheese(id: 1) {
15
+ id
16
+ flavor
17
+ }
18
+
19
+ # complexity of 4
20
+ cheese2: cheese(id: 2) @skip(if: $isSkipped) {
21
+ similarCheese(source: SHEEP) {
22
+ ... on Cheese {
23
+ similarCheese(source: SHEEP) {
24
+ id
25
+ }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ |}
31
+
32
+ it "sums the complexity" do
33
+ reduce_result
34
+ assert_equal complexities, [query, 7]
35
+ end
36
+
37
+ describe "when skipped by directives" do
38
+ let(:variables) { { "isSkipped" => true } }
39
+ it "doesn't include skipped fields" do
40
+ reduce_result
41
+ assert_equal complexities, [query, 3]
42
+ end
43
+ end
44
+ end
45
+
46
+ describe "query with fragments" do
47
+ let(:query_string) {%|
48
+ {
49
+ # complexity of 3
50
+ cheese1: cheese(id: 1) {
51
+ id
52
+ flavor
53
+ }
54
+
55
+ # complexity of 7
56
+ cheese2: cheese(id: 2) {
57
+ ... cheeseFields1
58
+ ... cheeseFields2
59
+ }
60
+ }
61
+
62
+ fragment cheeseFields1 on Cheese {
63
+ similarCow: similarCheese(source: COW) {
64
+ id
65
+ ... cheeseFields2
66
+ }
67
+ }
68
+
69
+ fragment cheeseFields2 on Cheese {
70
+ similarSheep: similarCheese(source: SHEEP) {
71
+ id
72
+ }
73
+ }
74
+ |}
75
+
76
+ it "counts all fragment usages, not the definitions" do
77
+ reduce_result
78
+ assert_equal complexities, [query, 10]
79
+ end
80
+
81
+ describe "mutually exclusive types" do
82
+ let(:query_string) {%|
83
+ {
84
+ favoriteEdible {
85
+ # 1 for everybody
86
+ fatContent
87
+
88
+ # 1 for everybody
89
+ ... on Edible {
90
+ origin
91
+ }
92
+
93
+ # 1 for honey
94
+ ... on Sweetener {
95
+ sweetness
96
+ }
97
+
98
+ # 2 for milk
99
+ ... milkFields
100
+ # 1 for cheese
101
+ ... cheeseFields
102
+ # 1 for honey
103
+ ... honeyFields
104
+ # 1 for milk + cheese
105
+ ... dairyProductFields
106
+ }
107
+ }
108
+
109
+ fragment milkFields on Milk {
110
+ id
111
+ source
112
+ }
113
+
114
+ fragment cheeseFields on Cheese {
115
+ source
116
+ }
117
+
118
+ fragment honeyFields on Honey {
119
+ flowerType
120
+ }
121
+
122
+ fragment dairyProductFields on DairyProduct {
123
+ ... on Cheese {
124
+ flavor
125
+ }
126
+
127
+ ... on Milk {
128
+ flavors
129
+ }
130
+ }
131
+ |}
132
+
133
+ it "gets the max among options" do
134
+ reduce_result
135
+ assert_equal 5, complexities.last
136
+ end
137
+ end
138
+
139
+
140
+ describe "when there are no selections on any object types" do
141
+ let(:query_string) {%|
142
+ {
143
+ favoriteEdible {
144
+ # 1 for everybody
145
+ fatContent
146
+
147
+ # 1 for everybody
148
+ ... on Edible { origin }
149
+
150
+ # 1 for honey
151
+ ... on Sweetener { sweetness }
152
+ }
153
+ }
154
+ |}
155
+
156
+ it "gets the max among interface types" do
157
+ reduce_result
158
+ assert_equal 3, complexities.last
159
+ end
160
+ end
161
+
162
+ describe "redundant fields" do
163
+ let(:query_string) {%|
164
+ {
165
+ favoriteEdible {
166
+ fatContent
167
+ # this is executed separately and counts separately:
168
+ aliasedFatContent: fatContent
169
+
170
+ ... on Edible {
171
+ fatContent
172
+ }
173
+
174
+ ... edibleFields
175
+ }
176
+ }
177
+
178
+ fragment edibleFields on Edible {
179
+ fatContent
180
+ }
181
+ |}
182
+
183
+ it "only counts them once" do
184
+ reduce_result
185
+ assert_equal 3, complexities.last
186
+ end
187
+ end
188
+ end
189
+
190
+
191
+ describe "custom complexities" do
192
+ let(:query) { GraphQL::Query.new(complexity_schema, query_string) }
193
+ let(:complexity_schema) {
194
+ complexity_type = GraphQL::ObjectType.define do
195
+ name "Complexity"
196
+ field :value, types.Int, complexity: 0.1 do
197
+ resolve -> (obj, args, ctx) { obj }
198
+ end
199
+ field :complexity, -> { complexity_type } do
200
+ argument :value, types.Int
201
+ complexity -> (ctx, args, child_complexity) { args[:value] + child_complexity }
202
+ resolve -> (obj, args, ctx) { args[:value] }
203
+ end
204
+ end
205
+
206
+ query_type = GraphQL::ObjectType.define do
207
+ name "Query"
208
+ field :complexity, -> { complexity_type } do
209
+ argument :value, types.Int
210
+ complexity -> (ctx, args, child_complexity) { args[:value] + child_complexity }
211
+ resolve -> (obj, args, ctx) { args[:value] }
212
+ end
213
+ end
214
+
215
+ GraphQL::Schema.new(query: query_type)
216
+ }
217
+ let(:query_string) {%|
218
+ {
219
+ a: complexity(value: 3) { value }
220
+ b: complexity(value: 6) {
221
+ value
222
+ complexity(value: 1) {
223
+ value
224
+ }
225
+ }
226
+ }
227
+ |}
228
+
229
+ it "sums the complexity" do
230
+ reduce_result
231
+ # 10 from `complexity`, `0.3` from `value`
232
+ assert_equal complexities, [query, 10.3]
233
+ end
234
+ end
235
+ end