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