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,80 @@
1
+ require "spec_helper"
2
+
3
+ describe GraphQL::Analysis::QueryDepth do
4
+ let(:depths) { [] }
5
+ let(:query_depth) { GraphQL::Analysis::QueryDepth.new { |query, max_depth| depths << query << max_depth } }
6
+ let(:reduce_result) { GraphQL::Analysis.analyze_query(query, [query_depth]) }
7
+ let(:query) { GraphQL::Query.new(DummySchema, query_string, variables: variables) }
8
+ let(:variables) { {} }
9
+
10
+ describe "simple queries" do
11
+ let(:query_string) {%|
12
+ query cheeses($isIncluded: Boolean = true){
13
+ # depth of 2
14
+ cheese1: cheese(id: 1) {
15
+ id
16
+ flavor
17
+ }
18
+
19
+ # depth of 4
20
+ cheese2: cheese(id: 2) @include(if: $isIncluded) {
21
+ similarCheese(source: SHEEP) {
22
+ ... on Cheese {
23
+ similarCheese(source: SHEEP) {
24
+ id
25
+ }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ |}
31
+
32
+ it "finds the max depth" do
33
+ reduce_result
34
+ assert_equal depths, [query, 4]
35
+ end
36
+
37
+ describe "with directives" do
38
+ let(:variables) { { "isIncluded" => false } }
39
+ it "doesn't count skipped fields" do
40
+ reduce_result
41
+ assert_equal depths.last, 2
42
+ end
43
+ end
44
+ end
45
+
46
+ describe "query with fragments" do
47
+ let(:query_string) {%|
48
+ {
49
+ # depth of 2
50
+ cheese1: cheese(id: 1) {
51
+ id
52
+ flavor
53
+ }
54
+
55
+ # depth of 4
56
+ cheese2: cheese(id: 2) {
57
+ ... cheeseFields1
58
+ }
59
+ }
60
+
61
+ fragment cheeseFields1 on Cheese {
62
+ similarCheese(source: COW) {
63
+ id
64
+ ... cheeseFields2
65
+ }
66
+ }
67
+
68
+ fragment cheeseFields2 on Cheese {
69
+ similarCheese(source: SHEEP) {
70
+ id
71
+ }
72
+ }
73
+ |}
74
+
75
+ it "finds the max depth" do
76
+ reduce_result
77
+ assert_equal depths, [query, 4]
78
+ end
79
+ end
80
+ end
@@ -25,6 +25,7 @@ describe GraphQL::Directive do
25
25
  fragment dontSkipIdField on Cheese { dontSkipId: id @skip(if: false) }
26
26
  |
27
27
  }
28
+
28
29
  it "intercepts fields" do
29
30
  expected = { "data" =>{
30
31
  "cheese" => {
@@ -0,0 +1,120 @@
1
+ require "spec_helper"
2
+
3
+ describe GraphQL::InternalRepresentation::Rewrite do
4
+ let(:validator) { GraphQL::StaticValidation::Validator.new(schema: DummySchema) }
5
+ let(:query) { GraphQL::Query.new(DummySchema, query_string) }
6
+ let(:rewrite_result) {
7
+ validator.validate(query)[:irep]
8
+ }
9
+ describe "plain queries" do
10
+ let(:query_string) {%|
11
+ query getCheeses {
12
+ cheese1: cheese(id: 1) {
13
+ id1: id
14
+ id2: id
15
+ id3: id
16
+ }
17
+ cheese2: cheese(id: 2) {
18
+ id
19
+ }
20
+ }
21
+ |}
22
+ it "produces a tree of nodes" do
23
+ op_node = rewrite_result["getCheeses"]
24
+
25
+ assert_equal 2, op_node.children.length
26
+ assert_equal QueryType, op_node.return_type
27
+ first_field = op_node.children.values.first
28
+ assert_equal 3, first_field.children.length
29
+ assert_equal [QueryType], first_field.on_types.to_a
30
+ assert_equal CheeseType, first_field.return_type
31
+
32
+ second_field = op_node.children.values.last
33
+ assert_equal 1, second_field.children.length
34
+ assert_equal [QueryType], second_field.on_types.to_a
35
+ assert_equal CheeseType, second_field.return_type
36
+ end
37
+ end
38
+
39
+ describe "dynamic fields" do
40
+ let(:query_string) {%|
41
+ {
42
+ cheese(id: 1) {
43
+ typename: __typename
44
+ }
45
+ }
46
+ |}
47
+
48
+ it "gets dynamic field definitions" do
49
+ cheese_field = rewrite_result[nil].children["cheese"]
50
+ typename_field = cheese_field.children["typename"]
51
+ assert_equal "__typename", typename_field.definition.name
52
+ end
53
+ end
54
+
55
+ describe "merging fragments" do
56
+ let(:query_string) {%|
57
+ {
58
+ cheese(id: 1) {
59
+ id1: id
60
+ ... {
61
+ id2: id
62
+ }
63
+
64
+ fatContent
65
+ ... on Edible {
66
+ fatContent
67
+ origin
68
+ }
69
+ ... cheeseFields
70
+
71
+ ... similarCheeseField
72
+ }
73
+ }
74
+
75
+ fragment cheeseFields on Cheese {
76
+ fatContent
77
+ flavor
78
+ similarCow: similarCheese(source: COW) {
79
+ similarCowSource: source,
80
+ id
81
+ ... similarCowFields
82
+ }
83
+ }
84
+
85
+ fragment similarCowFields on Cheese {
86
+ similarCheese(source: SHEEP) {
87
+ source
88
+ }
89
+ }
90
+
91
+ fragment similarCheeseField on Cheese {
92
+ # deep fragment merge
93
+ similarCow: similarCheese(source: COW) {
94
+ similarCowSource: source,
95
+ fatContent
96
+ similarCheese(source: SHEEP) {
97
+ flavor
98
+ }
99
+ }
100
+ }
101
+ |}
102
+
103
+ it "puts all fragment members as children" do
104
+ op_node = rewrite_result[nil]
105
+
106
+ cheese_field = op_node.children["cheese"]
107
+ assert_equal ["id1", "id2", "fatContent", "origin", "similarCow", "flavor"], cheese_field.children.keys
108
+ # Merge:
109
+ similar_cow_field = cheese_field.children["similarCow"]
110
+ assert_equal ["similarCowSource", "fatContent", "similarCheese", "id"], similar_cow_field.children.keys
111
+ # Deep merge:
112
+ similar_sheep_field = similar_cow_field.children["similarCheese"]
113
+ assert_equal ["flavor", "source"], similar_sheep_field.children.keys
114
+
115
+ assert_equal Set.new([EdibleInterface]), cheese_field.children["origin"].on_types
116
+ assert_equal Set.new([CheeseType, EdibleInterface]), cheese_field.children["fatContent"].on_types
117
+ assert_equal Set.new([CheeseType]), cheese_field.children["flavor"].on_types
118
+ end
119
+ end
120
+ end
@@ -21,6 +21,7 @@ describe GraphQL::Introspection::SchemaType do
21
21
  {"name"=>"cheese"},
22
22
  {"name"=>"cow"},
23
23
  {"name"=>"dairy"},
24
+ {"name"=>"deepNonNull"},
24
25
  {"name"=>"error"},
25
26
  {"name"=>"executionError"},
26
27
  {"name"=>"favoriteEdible"},
@@ -2,13 +2,23 @@ require "spec_helper"
2
2
 
3
3
  describe GraphQL::Language::Visitor do
4
4
  let(:document) { GraphQL.parse("
5
- query cheese { cheese(id: 1) { flavor, source, producers(first: 3) { name } } }
5
+ query cheese {
6
+ cheese(id: 1) {
7
+ flavor,
8
+ source,
9
+ producers(first: 3) {
10
+ name
11
+ }
12
+ ... cheeseFields
13
+ }
14
+ }
15
+
6
16
  fragment cheeseFields on Cheese { flavor }
7
17
  ")}
8
18
  let(:counts) { {fields_entered: 0, arguments_entered: 0, arguments_left: 0, argument_names: []} }
9
19
 
10
20
  let(:visitor) do
11
- v = GraphQL::Language::Visitor.new
21
+ v = GraphQL::Language::Visitor.new(document)
12
22
  v[GraphQL::Language::Nodes::Field] << -> (node, parent) { counts[:fields_entered] += 1 }
13
23
  # two ways to set up enter hooks:
14
24
  v[GraphQL::Language::Nodes::Argument] << -> (node, parent) { counts[:argument_names] << node.name }
@@ -21,7 +31,7 @@ describe GraphQL::Language::Visitor do
21
31
 
22
32
  it "calls hooks during a depth-first tree traversal" do
23
33
  assert_equal(2, visitor[GraphQL::Language::Nodes::Argument].enter.length)
24
- visitor.visit(document)
34
+ visitor.visit
25
35
  assert_equal(6, counts[:fields_entered])
26
36
  assert_equal(2, counts[:arguments_entered])
27
37
  assert_equal(2, counts[:arguments_left])
@@ -32,7 +42,7 @@ describe GraphQL::Language::Visitor do
32
42
  describe "Visitor::SKIP" do
33
43
  it "skips the rest of the node" do
34
44
  visitor[GraphQL::Language::Nodes::Document] << -> (node, parent) { GraphQL::Language::Visitor::SKIP }
35
- visitor.visit(document)
45
+ visitor.visit
36
46
  assert_equal(0, counts[:fields_entered])
37
47
  end
38
48
  end
@@ -0,0 +1,31 @@
1
+ require "spec_helper"
2
+
3
+ describe GraphQL::NonNullType do
4
+ describe "when a non-null field returns null" do
5
+ it "nulls out the parent selection" do
6
+ query_string = %|{ cow { name cantBeNullButIs } }|
7
+ result = DummySchema.execute(query_string)
8
+ assert_equal({"cow" => nil }, result["data"])
9
+ assert_equal([{"message"=>"Cannot return null for non-nullable field cantBeNullButIs"}], result["errors"])
10
+ end
11
+
12
+ it "propagates the null up to the next nullable field" do
13
+ query_string = %|
14
+ {
15
+ nn1: deepNonNull {
16
+ nni1: nonNullInt(returning: 1)
17
+ nn2: deepNonNull {
18
+ nni2: nonNullInt(returning: 2)
19
+ nn3: deepNonNull {
20
+ nni3: nonNullInt
21
+ }
22
+ }
23
+ }
24
+ }
25
+ |
26
+ result = DummySchema.execute(query_string)
27
+ assert_equal(nil, result["data"])
28
+ assert_equal([{"message"=>"Cannot return null for non-nullable field nonNullInt"}], result["errors"])
29
+ end
30
+ end
31
+ end
@@ -9,7 +9,9 @@ describe GraphQL::Query::Context do
9
9
  field :contextAstNodeName, types.String do
10
10
  resolve -> (target, args, ctx) { ctx.ast_node.class.name }
11
11
  end
12
-
12
+ field :contextIrepNodeName, types.String do
13
+ resolve -> (target, args, ctx) { ctx.irep_node.class.name }
14
+ end
13
15
  field :queryName, types.String do
14
16
  resolve -> (target, args, ctx) { ctx.query.class.name }
15
17
  end
@@ -39,6 +41,17 @@ describe GraphQL::Query::Context do
39
41
  end
40
42
  end
41
43
 
44
+ describe "access to the InternalRepresentation node" do
45
+ let(:query_string) { %|
46
+ query getCtx { contextIrepNodeName }
47
+ |}
48
+
49
+ it "provides access to the AST node" do
50
+ expected = {"data" => {"contextIrepNodeName" => "GraphQL::InternalRepresentation::Node"}}
51
+ assert_equal(expected, result)
52
+ end
53
+ end
54
+
42
55
  describe "access to the query" do
43
56
  let(:query_string) { %|
44
57
  query getCtx { queryName }
@@ -57,4 +70,14 @@ describe GraphQL::Query::Context do
57
70
  assert_equal(nil, context[:some_key])
58
71
  end
59
72
  end
73
+
74
+ describe "assigning values" do
75
+ let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema), values: nil) }
76
+
77
+ it "allows you to assign new contexts" do
78
+ assert_equal(nil, context[:some_key])
79
+ context[:some_key] = "wow!"
80
+ assert_equal("wow!", context[:some_key])
81
+ end
82
+ end
60
83
  end
@@ -45,14 +45,14 @@ describe GraphQL::Query do
45
45
 
46
46
  describe "when passed no query string or document" do
47
47
  it 'fails with an ArgumentError' do
48
- -> {
48
+ assert_raises(ArgumentError) {
49
49
  GraphQL::Query.new(
50
50
  schema,
51
51
  variables: query_variables,
52
52
  operation_name: operation_name,
53
53
  max_depth: max_depth,
54
54
  )
55
- }.must_raise ArgumentError
55
+ }
56
56
  end
57
57
  end
58
58
 
@@ -133,6 +133,10 @@ describe GraphQL::Query do
133
133
  assert_equal(GraphQL::Language::Nodes::FragmentDefinition, query.fragments["cheeseFields"].class)
134
134
  end
135
135
 
136
+ it "exposes the original string" do
137
+ assert_equal(query_string, query.query_string)
138
+ end
139
+
136
140
  describe "merging fragments with different keys" do
137
141
  let(:query_string) { %|
138
142
  query getCheeseFieldsThroughDairy {
@@ -0,0 +1,180 @@
1
+ require "spec_helper"
2
+
3
+ describe GraphQL::Schema::TimeoutMiddleware do
4
+ let(:max_seconds) { 2 }
5
+ let(:timeout_middleware) { GraphQL::Schema::TimeoutMiddleware.new(max_seconds: 2) }
6
+ let(:timeout_schema) {
7
+
8
+ sleep_for_seconds_resolve = -> (obj, args, ctx) {
9
+ sleep(args[:seconds])
10
+ args[:seconds]
11
+ }
12
+
13
+ nested_sleep_type = GraphQL::ObjectType.define do
14
+ name "NestedSleep"
15
+ field :seconds, types.Float do
16
+ resolve -> (obj, args, ctx) { obj }
17
+ end
18
+
19
+ field :nestedSleep, -> { nested_sleep_type } do
20
+ argument :seconds, !types.Float
21
+ resolve(sleep_for_seconds_resolve)
22
+ end
23
+ end
24
+
25
+ query_type = GraphQL::ObjectType.define do
26
+ name "Query"
27
+ field :sleepFor, types.Float do
28
+ argument :seconds, !types.Float
29
+ resolve(sleep_for_seconds_resolve)
30
+ end
31
+
32
+ field :nestedSleep, nested_sleep_type do
33
+ argument :seconds, !types.Float
34
+ resolve(sleep_for_seconds_resolve)
35
+ end
36
+ end
37
+
38
+ schema = GraphQL::Schema.new(query: query_type)
39
+ schema.middleware << timeout_middleware
40
+ schema
41
+ }
42
+
43
+ let(:result) { timeout_schema.execute(query_string) }
44
+
45
+ describe "timeout part-way through" do
46
+ let(:query_string) {%|
47
+ {
48
+ a: sleepFor(seconds: 0.7)
49
+ b: sleepFor(seconds: 0.7)
50
+ c: sleepFor(seconds: 0.7)
51
+ d: sleepFor(seconds: 0.7)
52
+ e: sleepFor(seconds: 0.7)
53
+ }
54
+ |}
55
+ it "returns a partial response and error messages" do
56
+ expected_data = {
57
+ "a"=>0.7,
58
+ "b"=>0.7,
59
+ "c"=>0.7,
60
+ "d"=>nil,
61
+ "e"=>nil,
62
+ }
63
+
64
+ expected_errors = [
65
+ {
66
+ "message"=>"Timeout on Query.sleepFor",
67
+ "locations"=>[{"line"=>6, "column"=>9}]
68
+ },
69
+ {
70
+ "message"=>"Timeout on Query.sleepFor",
71
+ "locations"=>[{"line"=>7, "column"=>9}]
72
+ },
73
+ ]
74
+ assert_equal expected_data, result["data"]
75
+ assert_equal expected_errors, result["errors"]
76
+ end
77
+ end
78
+
79
+ describe "timeout in nested fields" do
80
+ let(:query_string) {%|
81
+ {
82
+ a: nestedSleep(seconds: 1) {
83
+ seconds
84
+ b: nestedSleep(seconds: 0.4) {
85
+ seconds
86
+ c: nestedSleep(seconds: 0.4) {
87
+ seconds
88
+ d: nestedSleep(seconds: 0.4) {
89
+ seconds
90
+ e: nestedSleep(seconds: 0.4) {
91
+ seconds
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ |}
99
+ it "returns a partial response and error messages" do
100
+ expected_data = {
101
+ "a" => {
102
+ "seconds" => 1.0,
103
+ "b" => {
104
+ "seconds" => 0.4,
105
+ "c" => {
106
+ "seconds"=>0.4,
107
+ "d" => {
108
+ "seconds"=>nil,
109
+ "e"=>nil
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ expected_errors = [
116
+ {
117
+ "message"=>"Timeout on NestedSleep.seconds",
118
+ "locations"=>[{"line"=>10, "column"=>15}]
119
+ },
120
+ {
121
+ "message"=>"Timeout on NestedSleep.nestedSleep",
122
+ "locations"=>[{"line"=>11, "column"=>15}]
123
+ },
124
+ ]
125
+
126
+ assert_equal expected_data, result["data"]
127
+ assert_equal expected_errors, result["errors"]
128
+ end
129
+ end
130
+
131
+ describe "long-running fields" do
132
+ let(:query_string) {%|
133
+ {
134
+ a: sleepFor(seconds: 0.7)
135
+ b: sleepFor(seconds: 0.7)
136
+ c: sleepFor(seconds: 1.5)
137
+ d: sleepFor(seconds: 0.1)
138
+ }
139
+ |}
140
+ it "doesn't terminate long-running field execution" do
141
+ expected_data = {
142
+ "a"=>0.7,
143
+ "b"=>0.7,
144
+ "c"=>1.5,
145
+ "d"=>nil,
146
+ }
147
+
148
+ expected_errors = [
149
+ {
150
+ "message"=>"Timeout on Query.sleepFor",
151
+ "locations"=>[{"line"=>6, "column"=>9}]
152
+ },
153
+ ]
154
+
155
+ assert_equal expected_data, result["data"]
156
+ assert_equal expected_errors, result["errors"]
157
+ end
158
+ end
159
+
160
+ describe "with a custom block" do
161
+ let(:timeout_middleware) {
162
+ GraphQL::Schema::TimeoutMiddleware.new(max_seconds: 2) do |err, query|
163
+ raise("Query timed out after 2s: #{query.class.name}")
164
+ end
165
+ }
166
+ let(:query_string) {%|
167
+ {
168
+ a: sleepFor(seconds: 0.7)
169
+ b: sleepFor(seconds: 0.7)
170
+ c: sleepFor(seconds: 0.7)
171
+ d: sleepFor(seconds: 0.7)
172
+ e: sleepFor(seconds: 0.7)
173
+ }
174
+ |}
175
+
176
+ it "calls the block" do
177
+ assert_raises(RuntimeError) { result }
178
+ end
179
+ end
180
+ end