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.
- checksums.yaml +4 -4
- data/lib/graphql.rb +4 -1
- data/lib/graphql/analysis.rb +5 -0
- data/lib/graphql/analysis/analyze_query.rb +73 -0
- data/lib/graphql/analysis/max_query_complexity.rb +25 -0
- data/lib/graphql/analysis/max_query_depth.rb +25 -0
- data/lib/graphql/analysis/query_complexity.rb +122 -0
- data/lib/graphql/analysis/query_depth.rb +54 -0
- data/lib/graphql/analysis_error.rb +4 -0
- data/lib/graphql/base_type.rb +7 -0
- data/lib/graphql/define/assign_object_field.rb +2 -1
- data/lib/graphql/field.rb +25 -3
- data/lib/graphql/input_object_type.rb +1 -1
- data/lib/graphql/internal_representation.rb +2 -0
- data/lib/graphql/internal_representation/node.rb +81 -0
- data/lib/graphql/internal_representation/rewrite.rb +177 -0
- data/lib/graphql/language/visitor.rb +15 -9
- data/lib/graphql/object_type.rb +1 -1
- data/lib/graphql/query.rb +66 -7
- data/lib/graphql/query/context.rb +10 -3
- data/lib/graphql/query/directive_resolution.rb +5 -5
- data/lib/graphql/query/serial_execution.rb +5 -3
- data/lib/graphql/query/serial_execution/field_resolution.rb +22 -15
- data/lib/graphql/query/serial_execution/operation_resolution.rb +7 -5
- data/lib/graphql/query/serial_execution/selection_resolution.rb +20 -105
- data/lib/graphql/query/serial_execution/value_resolution.rb +15 -12
- data/lib/graphql/schema.rb +7 -2
- data/lib/graphql/schema/timeout_middleware.rb +67 -0
- data/lib/graphql/static_validation/all_rules.rb +0 -1
- data/lib/graphql/static_validation/type_stack.rb +7 -11
- data/lib/graphql/static_validation/validation_context.rb +11 -1
- data/lib/graphql/static_validation/validator.rb +14 -4
- data/lib/graphql/version.rb +1 -1
- data/readme.md +10 -9
- data/spec/graphql/analysis/analyze_query_spec.rb +50 -0
- data/spec/graphql/analysis/max_query_complexity_spec.rb +62 -0
- data/spec/graphql/{static_validation/rules/document_does_not_exceed_max_depth_spec.rb → analysis/max_query_depth_spec.rb} +20 -21
- data/spec/graphql/analysis/query_complexity_spec.rb +235 -0
- data/spec/graphql/analysis/query_depth_spec.rb +80 -0
- data/spec/graphql/directive_spec.rb +1 -0
- data/spec/graphql/internal_representation/rewrite_spec.rb +120 -0
- data/spec/graphql/introspection/schema_type_spec.rb +1 -0
- data/spec/graphql/language/visitor_spec.rb +14 -4
- data/spec/graphql/non_null_type_spec.rb +31 -0
- data/spec/graphql/query/context_spec.rb +24 -1
- data/spec/graphql/query_spec.rb +6 -2
- data/spec/graphql/schema/timeout_middleware_spec.rb +180 -0
- data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/arguments_are_defined_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/directives_are_defined_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/directives_are_in_valid_locations_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fields_are_defined_on_type_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fields_have_appropriate_selections_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fields_will_merge_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fragment_spreads_are_possible_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fragment_types_exist_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fragments_are_finite_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fragments_are_on_composite_types_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fragments_are_used_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/required_arguments_are_present_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/variable_usages_are_allowed_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/variables_are_used_and_defined_spec.rb +1 -1
- data/spec/graphql/static_validation/validator_spec.rb +1 -1
- data/spec/support/dairy_app.rb +22 -1
- metadata +29 -5
- 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
|
@@ -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
|
@@ -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 {
|
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
|
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
|
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
|
data/spec/graphql/query_spec.rb
CHANGED
@@ -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
|
-
}
|
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
|