graphql 1.9.6 → 1.9.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/lib/generators/graphql/install_generator.rb +2 -1
- data/lib/generators/graphql/templates/base_field.erb +7 -0
- data/lib/graphql/analysis/ast/visitor.rb +5 -2
- data/lib/graphql/execution/multiplex.rb +5 -1
- data/lib/graphql/query.rb +6 -1
- data/lib/graphql/relay/array_connection.rb +1 -1
- data/lib/graphql/schema.rb +15 -1
- data/lib/graphql/schema/argument.rb +5 -1
- data/lib/graphql/schema/input_object.rb +21 -12
- data/lib/graphql/schema/introspection_system.rb +6 -1
- data/lib/graphql/schema/member/has_arguments.rb +4 -2
- data/lib/graphql/schema/resolver.rb +8 -3
- data/lib/graphql/schema/subscription.rb +22 -0
- data/lib/graphql/schema/timeout.rb +109 -0
- data/lib/graphql/types/relay/base_edge.rb +0 -3
- data/lib/graphql/upgrader/member.rb +148 -111
- data/lib/graphql/version.rb +1 -1
- data/readme.md +1 -1
- data/spec/fixtures/upgrader/mutation.original.rb +28 -0
- data/spec/fixtures/upgrader/mutation.transformed.rb +28 -0
- data/spec/graphql/analysis/ast_spec.rb +27 -0
- data/spec/graphql/execution/instrumentation_spec.rb +34 -6
- data/spec/graphql/execution/multiplex_spec.rb +11 -0
- data/spec/graphql/internal_representation/rewrite_spec.rb +6 -1
- data/spec/graphql/schema/input_object_spec.rb +56 -7
- data/spec/graphql/schema/introspection_system_spec.rb +24 -0
- data/spec/graphql/schema/subscription_spec.rb +65 -0
- data/spec/graphql/schema/timeout_spec.rb +206 -0
- data/spec/integration/mongoid/star_trek/schema.rb +1 -2
- data/spec/integration/rails/graphql/input_object_spec.rb +19 -0
- data/spec/integration/rails/graphql/relay/array_connection_spec.rb +47 -28
- data/spec/integration/rails/graphql/schema_spec.rb +18 -0
- data/spec/integration/tmp/app/graphql/types/date_type.rb +14 -0
- data/spec/integration/tmp/dummy/Gemfile +50 -0
- data/spec/integration/tmp/dummy/README.md +24 -0
- data/spec/integration/tmp/dummy/Rakefile +6 -0
- data/spec/integration/tmp/dummy/app/assets/config/manifest.js +3 -0
- data/spec/integration/tmp/dummy/app/assets/javascripts/application.js +16 -0
- data/spec/integration/tmp/dummy/app/assets/javascripts/cable.js +13 -0
- data/spec/integration/tmp/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/integration/tmp/dummy/app/channels/application_cable/channel.rb +5 -0
- data/spec/integration/tmp/dummy/app/channels/application_cable/connection.rb +5 -0
- data/spec/integration/tmp/dummy/app/controllers/application_controller.rb +4 -0
- data/spec/integration/tmp/dummy/app/controllers/graphql_controller.rb +44 -0
- data/spec/integration/tmp/dummy/app/helpers/application_helper.rb +3 -0
- data/spec/integration/tmp/dummy/app/jobs/application_job.rb +3 -0
- data/spec/integration/tmp/dummy/app/mailers/application_mailer.rb +5 -0
- data/spec/integration/tmp/dummy/app/mydirectory/dummy_schema.rb +5 -0
- data/spec/integration/tmp/dummy/app/mydirectory/mutations/update_name.rb +15 -0
- data/spec/integration/tmp/dummy/app/mydirectory/types/base_enum.rb +5 -0
- data/spec/integration/tmp/dummy/app/mydirectory/types/base_input_object.rb +5 -0
- data/spec/integration/tmp/dummy/app/mydirectory/types/base_interface.rb +6 -0
- data/spec/integration/tmp/dummy/app/mydirectory/types/base_object.rb +5 -0
- data/spec/integration/tmp/dummy/app/mydirectory/types/base_scalar.rb +5 -0
- data/spec/integration/tmp/dummy/app/mydirectory/types/base_union.rb +5 -0
- data/spec/integration/tmp/dummy/app/mydirectory/types/mutation_type.rb +12 -0
- data/spec/integration/tmp/dummy/app/mydirectory/types/query_type.rb +14 -0
- data/spec/integration/tmp/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/integration/tmp/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/spec/integration/tmp/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/spec/integration/tmp/dummy/bin/bundle +3 -0
- data/spec/integration/tmp/dummy/bin/rails +4 -0
- data/spec/integration/tmp/dummy/bin/rake +4 -0
- data/spec/integration/tmp/dummy/bin/setup +34 -0
- data/spec/integration/tmp/dummy/bin/update +29 -0
- data/spec/integration/tmp/dummy/config.ru +5 -0
- data/spec/integration/tmp/dummy/config/application.rb +26 -0
- data/spec/integration/tmp/dummy/config/boot.rb +4 -0
- data/spec/integration/tmp/dummy/config/cable.yml +9 -0
- data/spec/integration/tmp/dummy/config/environment.rb +6 -0
- data/spec/integration/tmp/dummy/config/environments/development.rb +52 -0
- data/spec/integration/tmp/dummy/config/environments/production.rb +84 -0
- data/spec/integration/tmp/dummy/config/environments/test.rb +43 -0
- data/spec/integration/tmp/dummy/config/initializers/application_controller_renderer.rb +9 -0
- data/spec/integration/tmp/dummy/config/initializers/assets.rb +12 -0
- data/spec/integration/tmp/dummy/config/initializers/backtrace_silencers.rb +8 -0
- data/spec/integration/tmp/dummy/config/initializers/cookies_serializer.rb +6 -0
- data/spec/integration/tmp/dummy/config/initializers/filter_parameter_logging.rb +5 -0
- data/spec/integration/tmp/dummy/config/initializers/inflections.rb +17 -0
- data/spec/integration/tmp/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/integration/tmp/dummy/config/initializers/new_framework_defaults.rb +24 -0
- data/spec/integration/tmp/dummy/config/initializers/session_store.rb +4 -0
- data/spec/integration/tmp/dummy/config/initializers/wrap_parameters.rb +10 -0
- data/spec/integration/tmp/dummy/config/locales/en.yml +23 -0
- data/spec/integration/tmp/dummy/config/puma.rb +48 -0
- data/spec/integration/tmp/dummy/config/routes.rb +9 -0
- data/spec/integration/tmp/dummy/config/secrets.yml +22 -0
- data/spec/integration/tmp/dummy/db/seeds.rb +8 -0
- data/spec/integration/tmp/dummy/log/test.log +0 -0
- data/spec/integration/tmp/dummy/public/404.html +67 -0
- data/spec/integration/tmp/dummy/public/422.html +67 -0
- data/spec/integration/tmp/dummy/public/500.html +66 -0
- data/spec/integration/tmp/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/spec/integration/tmp/dummy/public/apple-touch-icon.png +0 -0
- data/spec/integration/tmp/dummy/public/favicon.ico +0 -0
- data/spec/integration/tmp/dummy/public/robots.txt +5 -0
- data/spec/integration/tmp/dummy/test/test_helper.rb +8 -0
- data/spec/support/jazz.rb +6 -0
- data/spec/support/star_wars/schema.rb +1 -2
- metadata +171 -6
- data/spec/integration/tmp/app/graphql/types/bird_type.rb +0 -7
@@ -62,23 +62,45 @@ describe GraphQL::Schema do
|
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
65
|
+
# This is how you might add queries from a persisted query backend
|
66
|
+
|
67
|
+
class QueryStringInstrumenter
|
68
|
+
def before_query(query)
|
69
|
+
if query.context[:extra_query_string] && query.query_string.nil?
|
70
|
+
query.query_string = query.context[:extra_query_string]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def after_query(query)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
65
78
|
let(:query_type) {
|
66
|
-
GraphQL::
|
67
|
-
|
68
|
-
field :int,
|
69
|
-
argument :value,
|
70
|
-
|
79
|
+
Class.new(GraphQL::Schema::Object) do
|
80
|
+
graphql_name "Query"
|
81
|
+
field :int, Integer, null: true do
|
82
|
+
argument :value, Integer, required: false
|
83
|
+
end
|
84
|
+
|
85
|
+
def int(value:)
|
86
|
+
value
|
71
87
|
end
|
72
88
|
end
|
73
89
|
}
|
74
90
|
|
75
91
|
let(:schema) {
|
76
92
|
spec = self
|
77
|
-
GraphQL::Schema
|
93
|
+
Class.new(GraphQL::Schema) do
|
78
94
|
query(spec.query_type)
|
79
95
|
instrument(:query, FirstInstrumenter.new)
|
80
96
|
instrument(:query, SecondInstrumenter.new)
|
81
97
|
instrument(:query, ExecutionErrorInstrumenter.new)
|
98
|
+
instrument(:query, QueryStringInstrumenter.new)
|
99
|
+
|
100
|
+
if TESTING_INTERPRETER
|
101
|
+
use GraphQL::Analysis::AST
|
102
|
+
use GraphQL::Execution::Interpreter
|
103
|
+
end
|
82
104
|
end
|
83
105
|
}
|
84
106
|
|
@@ -114,6 +136,12 @@ describe GraphQL::Schema do
|
|
114
136
|
assert_equal "Raised from instrumenter before_query", res["errors"].first["message"]
|
115
137
|
refute res.key?("data"), "The query doesn't run"
|
116
138
|
end
|
139
|
+
|
140
|
+
it "can assign a query string there" do
|
141
|
+
context = { extra_query_string: "{ __typename }"}
|
142
|
+
res = schema.execute(nil, context: context)
|
143
|
+
assert_equal "Query", res["data"]["__typename"]
|
144
|
+
end
|
117
145
|
end
|
118
146
|
|
119
147
|
describe "within a multiplex" do
|
@@ -134,6 +134,17 @@ describe GraphQL::Execution::Multiplex do
|
|
134
134
|
end
|
135
135
|
end
|
136
136
|
|
137
|
+
describe "max_complexity" do
|
138
|
+
it "can successfully calculate complexity" do
|
139
|
+
message = "Query has complexity of 11, which exceeds max complexity of 10"
|
140
|
+
results = multiplex(queries, max_complexity: 10)
|
141
|
+
|
142
|
+
results.each do |res|
|
143
|
+
assert_equal message, res["errors"][0]["message"]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
137
148
|
describe "after_query when errors are raised" do
|
138
149
|
class InspectQueryInstrumentation
|
139
150
|
class << self
|
@@ -125,7 +125,12 @@ describe GraphQL::InternalRepresentation::Rewrite do
|
|
125
125
|
nut_selections = plant_selection.typed_children[schema.types["Nut"]]
|
126
126
|
# `... on Tree`, `... on Nut`, and `NutFields`, but not `... on Fruit { ... on Tree }`
|
127
127
|
|
128
|
-
|
128
|
+
if RUBY_VERSION < "2.5"
|
129
|
+
# Ruby 2.5 changed how hash key collisions worked a little bit.
|
130
|
+
# This test is "broken", but honestly, it doesn't matter.
|
131
|
+
# Apparently nobody uses `IrepNode#ast_nodes`, and that's for the best.
|
132
|
+
assert_equal 3, nut_selections["leafType"].ast_nodes.size
|
133
|
+
end
|
129
134
|
|
130
135
|
# Multi-level merging when including fragments:
|
131
136
|
habitats_selections = nut_selections["habitats"].typed_children[schema.types["Habitat"]]
|
@@ -40,7 +40,7 @@ describe GraphQL::Schema::InputObject do
|
|
40
40
|
ensemble_class = Class.new(subclass) do
|
41
41
|
argument :ensemble_id, GraphQL::Types::ID, required: false, loads: Jazz::Ensemble
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
assert_equal 3, subclass.arguments.size
|
45
45
|
assert_equal ["arg1", "arg2", "arg3"], subclass.arguments.keys
|
46
46
|
assert_equal ["String!", "Int!", "Int!"], subclass.arguments.values.map { |a| a.type.to_type_signature }
|
@@ -87,8 +87,32 @@ describe GraphQL::Schema::InputObject do
|
|
87
87
|
end
|
88
88
|
end
|
89
89
|
|
90
|
+
class Mutation < GraphQL::Schema::Object
|
91
|
+
class TouchInstrument < GraphQL::Schema::Mutation
|
92
|
+
class InstrumentInput < GraphQL::Schema::InputObject
|
93
|
+
argument :instrument_id, ID, required: true, loads: Jazz::InstrumentType
|
94
|
+
end
|
95
|
+
|
96
|
+
argument :input_obj, InstrumentInput, required: true
|
97
|
+
field :instrument_name_method, String, null: false
|
98
|
+
field :instrument_name_key, String, null: false
|
99
|
+
|
100
|
+
def resolve(input_obj:)
|
101
|
+
# Make sure both kinds of access work the same:
|
102
|
+
{
|
103
|
+
instrument_name_method: input_obj.instrument.name,
|
104
|
+
instrument_name_key: input_obj[:instrument].name,
|
105
|
+
}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
field :touch_instrument, mutation: TouchInstrument
|
110
|
+
end
|
111
|
+
|
112
|
+
|
90
113
|
class Schema < GraphQL::Schema
|
91
114
|
query(Query)
|
115
|
+
mutation(Mutation)
|
92
116
|
if TESTING_INTERPRETER
|
93
117
|
use GraphQL::Execution::Interpreter
|
94
118
|
end
|
@@ -109,7 +133,7 @@ describe GraphQL::Schema::InputObject do
|
|
109
133
|
GRAPHQL
|
110
134
|
|
111
135
|
res = InputObjectPrepareTest::Schema.execute(query_str, context: { multiply_by: 3 })
|
112
|
-
expected_obj = [{ a: 1, b2: 2, c: 9, d2: 12, e2: 30, instrument: "
|
136
|
+
expected_obj = [{ a: 1, b2: 2, c: 9, d2: 12, e2: 30, instrument: Jazz::Models::Instrument.new("Drum Kit", "PERCUSSION") }.inspect, "Drum Kit"]
|
113
137
|
assert_equal expected_obj, res["data"]["inputs"]
|
114
138
|
end
|
115
139
|
|
@@ -127,6 +151,21 @@ describe GraphQL::Schema::InputObject do
|
|
127
151
|
assert_equal("boom!", res["errors"][0]["extensions"]["problems"][0]["explanation"])
|
128
152
|
assert_equal(input, res["errors"][0]["extensions"]["value"])
|
129
153
|
end
|
154
|
+
|
155
|
+
it "loads input object arguments" do
|
156
|
+
query_str = <<-GRAPHQL
|
157
|
+
mutation {
|
158
|
+
touchInstrument(inputObj: { instrumentId: "Instrument/Drum Kit" }) {
|
159
|
+
instrumentNameMethod
|
160
|
+
instrumentNameKey
|
161
|
+
}
|
162
|
+
}
|
163
|
+
GRAPHQL
|
164
|
+
|
165
|
+
res = InputObjectPrepareTest::Schema.execute(query_str)
|
166
|
+
assert_equal "Drum Kit", res["data"]["touchInstrument"]["instrumentNameMethod"]
|
167
|
+
assert_equal "Drum Kit", res["data"]["touchInstrument"]["instrumentNameKey"]
|
168
|
+
end
|
130
169
|
end
|
131
170
|
|
132
171
|
describe "loading application object(s)" do
|
@@ -238,7 +277,7 @@ describe GraphQL::Schema::InputObject do
|
|
238
277
|
end
|
239
278
|
end
|
240
279
|
|
241
|
-
describe
|
280
|
+
describe 'hash conversion behavior' do
|
242
281
|
module InputObjectToHTest
|
243
282
|
class TestInput1 < GraphQL::Schema::InputObject
|
244
283
|
graphql_name "TestInput1"
|
@@ -258,16 +297,26 @@ describe GraphQL::Schema::InputObject do
|
|
258
297
|
TestInput2.to_graphql
|
259
298
|
end
|
260
299
|
|
261
|
-
|
300
|
+
before do
|
262
301
|
arg_values = {a: 1, b: 2, c: { d: 3, e: 4, instrumentId: "Instrument/Drum Kit"}}
|
263
302
|
|
264
|
-
input_object = InputObjectToHTest::TestInput2.new(
|
303
|
+
@input_object = InputObjectToHTest::TestInput2.new(
|
265
304
|
arg_values,
|
266
|
-
context:
|
305
|
+
context: OpenStruct.new(schema: Jazz::Schema),
|
267
306
|
defaults_used: Set.new
|
268
307
|
)
|
308
|
+
end
|
269
309
|
|
270
|
-
|
310
|
+
describe "#to_h" do
|
311
|
+
it "returns a symbolized, aliased, ruby keyword style hash" do
|
312
|
+
assert_equal({ a: 1, b: 2, input_object: { d: 3, e: 4, instrument: Jazz::Models::Instrument.new("Drum Kit", "PERCUSSION") } }, @input_object.to_h)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
describe "#to_hash" do
|
317
|
+
it "returns the same results as #to_h (aliased)" do
|
318
|
+
assert_equal(@input_object.to_h, @input_object.to_hash)
|
319
|
+
end
|
271
320
|
end
|
272
321
|
end
|
273
322
|
|
@@ -53,4 +53,28 @@ describe GraphQL::Schema::IntrospectionSystem do
|
|
53
53
|
assert_equal [], ensembles_field["args"]
|
54
54
|
end
|
55
55
|
end
|
56
|
+
|
57
|
+
describe "#disable_introspection_entry_points" do
|
58
|
+
let(:schema) { Jazz::Schema }
|
59
|
+
|
60
|
+
it "allows entry point introspection by default" do
|
61
|
+
res = schema.execute("{ __schema { types { name } } }")
|
62
|
+
assert res
|
63
|
+
|
64
|
+
types = res["data"]["__schema"]["types"]
|
65
|
+
refute_empty types
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "when entry points introspection is disabled" do
|
69
|
+
let(:schema) { Jazz::SchemaWithoutIntrospection }
|
70
|
+
|
71
|
+
it "returns error" do
|
72
|
+
res = schema.execute("{ __schema { types { name } } }")
|
73
|
+
assert res
|
74
|
+
|
75
|
+
assert_nil res["data"]
|
76
|
+
assert_equal ["Field '__schema' doesn't exist on type 'Query'"], res["errors"].map { |e| e["message"] }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
56
80
|
end
|
@@ -68,6 +68,12 @@ describe GraphQL::Schema::Subscription do
|
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
|
+
class DirectTootWasTooted < BaseSubscription
|
72
|
+
subscription_scope :viewer
|
73
|
+
field :toot, Toot, null: false
|
74
|
+
field :user, User, null: false
|
75
|
+
end
|
76
|
+
|
71
77
|
# Test initial response, which returns all users
|
72
78
|
class UsersJoined < BaseSubscription
|
73
79
|
class UsersJoinedManualPayload < GraphQL::Schema::Object
|
@@ -97,6 +103,7 @@ describe GraphQL::Schema::Subscription do
|
|
97
103
|
class Subscription < GraphQL::Schema::Object
|
98
104
|
extend GraphQL::Subscriptions::SubscriptionRoot
|
99
105
|
field :toot_was_tooted, subscription: TootWasTooted
|
106
|
+
field :direct_toot_was_tooted, subscription: DirectTootWasTooted
|
100
107
|
field :users_joined, subscription: UsersJoined
|
101
108
|
field :new_users_joined, subscription: NewUsersJoined
|
102
109
|
end
|
@@ -437,4 +444,62 @@ describe GraphQL::Schema::Subscription do
|
|
437
444
|
assert_equal 1, in_memory_subscription_count
|
438
445
|
end
|
439
446
|
end
|
447
|
+
|
448
|
+
describe "`subscription_scope` method" do
|
449
|
+
it "provdes a subscription scope that is recognized in the schema" do
|
450
|
+
scoped_subscription = SubscriptionFieldSchema::get_field("Subscription", "directTootWasTooted")
|
451
|
+
|
452
|
+
assert_equal :viewer, scoped_subscription.subscription_scope
|
453
|
+
end
|
454
|
+
|
455
|
+
it "provides a subscription scope that is used in execution" do
|
456
|
+
res = exec_query <<-GRAPHQL, context: { viewer: :me }
|
457
|
+
subscription {
|
458
|
+
directTootWasTooted {
|
459
|
+
toot { body }
|
460
|
+
}
|
461
|
+
}
|
462
|
+
GRAPHQL
|
463
|
+
assert_equal 1, in_memory_subscription_count
|
464
|
+
|
465
|
+
# Only the subscription with scope :me should be in the mailbox
|
466
|
+
obj = OpenStruct.new(toot: { body: "Hello from matz!" }, user: SubscriptionFieldSchema::USERS["matz"])
|
467
|
+
SubscriptionFieldSchema.subscriptions.trigger(:direct_toot_was_tooted, {}, obj, scope: :me)
|
468
|
+
SubscriptionFieldSchema.subscriptions.trigger(:direct_toot_was_tooted, {}, obj, scope: :not_me)
|
469
|
+
mailbox = res.context[:subscription_mailbox]
|
470
|
+
|
471
|
+
assert_equal 1, mailbox.length
|
472
|
+
|
473
|
+
expected_response = {
|
474
|
+
"data" => {
|
475
|
+
"directTootWasTooted" => {
|
476
|
+
"toot" => {
|
477
|
+
"body" => "Hello from matz!"
|
478
|
+
}
|
479
|
+
}
|
480
|
+
}
|
481
|
+
}
|
482
|
+
|
483
|
+
assert_equal expected_response, mailbox.first
|
484
|
+
end
|
485
|
+
|
486
|
+
it "allows for proper inheritance of the class's configuration in subclasses" do
|
487
|
+
# Make a subclass without an explicit configuration
|
488
|
+
class DirectTootSubclass < SubscriptionFieldSchema::DirectTootWasTooted
|
489
|
+
end
|
490
|
+
# Then check if the field options got the inherited value
|
491
|
+
direct_toot_options = DirectTootSubclass.field_options
|
492
|
+
assert_equal :viewer, direct_toot_options[:subscription_scope]
|
493
|
+
end
|
494
|
+
|
495
|
+
it "allows for setting the subscription scope value to nil" do
|
496
|
+
class PrivateSubscription < SubscriptionFieldSchema::BaseSubscription
|
497
|
+
subscription_scope :private
|
498
|
+
end
|
499
|
+
|
500
|
+
PrivateSubscription.subscription_scope nil
|
501
|
+
|
502
|
+
assert_nil PrivateSubscription.subscription_scope
|
503
|
+
end
|
504
|
+
end
|
440
505
|
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe GraphQL::Schema::Timeout do
|
5
|
+
let(:max_seconds) { 1 }
|
6
|
+
let(:timeout_class) { GraphQL::Schema::Timeout }
|
7
|
+
let(:timeout_schema) {
|
8
|
+
nested_sleep_type = Class.new(GraphQL::Schema::Object) do
|
9
|
+
graphql_name "NestedSleep"
|
10
|
+
|
11
|
+
field :seconds, Float, null: true
|
12
|
+
|
13
|
+
def seconds
|
14
|
+
object
|
15
|
+
end
|
16
|
+
|
17
|
+
field :nested_sleep, GraphQL::Schema::LateBoundType.new(graphql_name), null: true do
|
18
|
+
argument :seconds, Float, required: true
|
19
|
+
end
|
20
|
+
|
21
|
+
def nested_sleep(seconds:)
|
22
|
+
sleep(seconds)
|
23
|
+
seconds
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
query_type = Class.new(GraphQL::Schema::Object) do
|
28
|
+
graphql_name "Query"
|
29
|
+
|
30
|
+
field :sleep_for, Float, null: true do
|
31
|
+
argument :seconds, Float, required: true
|
32
|
+
end
|
33
|
+
|
34
|
+
def sleep_for(seconds:)
|
35
|
+
sleep(seconds)
|
36
|
+
seconds
|
37
|
+
end
|
38
|
+
|
39
|
+
field :nested_sleep, nested_sleep_type, null: true do
|
40
|
+
argument :seconds, Float, required: true
|
41
|
+
end
|
42
|
+
|
43
|
+
def nested_sleep(seconds:)
|
44
|
+
sleep(seconds)
|
45
|
+
seconds
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
schema = Class.new(GraphQL::Schema) do
|
50
|
+
query query_type
|
51
|
+
if TESTING_INTERPRETER
|
52
|
+
use GraphQL::Execution::Interpreter
|
53
|
+
end
|
54
|
+
end
|
55
|
+
schema.use timeout_class, max_seconds: max_seconds
|
56
|
+
schema
|
57
|
+
}
|
58
|
+
|
59
|
+
let(:result) { timeout_schema.execute(query_string) }
|
60
|
+
|
61
|
+
describe "timeout part-way through" do
|
62
|
+
let(:query_string) {%|
|
63
|
+
{
|
64
|
+
a: sleepFor(seconds: 0.4)
|
65
|
+
b: sleepFor(seconds: 0.4)
|
66
|
+
c: sleepFor(seconds: 0.4)
|
67
|
+
d: sleepFor(seconds: 0.4)
|
68
|
+
e: sleepFor(seconds: 0.4)
|
69
|
+
}
|
70
|
+
|}
|
71
|
+
it "returns a partial response and error messages" do
|
72
|
+
expected_data = {
|
73
|
+
"a"=>0.4,
|
74
|
+
"b"=>0.4,
|
75
|
+
"c"=>0.4,
|
76
|
+
"d"=>nil,
|
77
|
+
"e"=>nil,
|
78
|
+
}
|
79
|
+
|
80
|
+
expected_errors = [
|
81
|
+
{
|
82
|
+
"message"=>"Timeout on Query.sleepFor",
|
83
|
+
"locations"=>[{"line"=>6, "column"=>9}],
|
84
|
+
"path"=>["d"]
|
85
|
+
},
|
86
|
+
{
|
87
|
+
"message"=>"Timeout on Query.sleepFor",
|
88
|
+
"locations"=>[{"line"=>7, "column"=>9}],
|
89
|
+
"path"=>["e"]
|
90
|
+
},
|
91
|
+
]
|
92
|
+
assert_equal expected_data, result["data"]
|
93
|
+
assert_equal expected_errors, result["errors"]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "timeout in nested fields" do
|
98
|
+
let(:query_string) {%|
|
99
|
+
{
|
100
|
+
a: nestedSleep(seconds: 0.3) {
|
101
|
+
seconds
|
102
|
+
b: nestedSleep(seconds: 0.3) {
|
103
|
+
seconds
|
104
|
+
c: nestedSleep(seconds: 0.3) {
|
105
|
+
seconds
|
106
|
+
d: nestedSleep(seconds: 0.4) {
|
107
|
+
seconds
|
108
|
+
e: nestedSleep(seconds: 0.4) {
|
109
|
+
seconds
|
110
|
+
}
|
111
|
+
}
|
112
|
+
}
|
113
|
+
}
|
114
|
+
}
|
115
|
+
}
|
116
|
+
|}
|
117
|
+
|
118
|
+
it "returns a partial response and error messages" do
|
119
|
+
expected_data = {
|
120
|
+
"a" => {
|
121
|
+
"seconds" => 0.3,
|
122
|
+
"b" => {
|
123
|
+
"seconds" => 0.3,
|
124
|
+
"c" => {
|
125
|
+
"seconds"=>0.3,
|
126
|
+
"d" => {
|
127
|
+
"seconds"=>nil,
|
128
|
+
"e"=>nil
|
129
|
+
}
|
130
|
+
}
|
131
|
+
}
|
132
|
+
}
|
133
|
+
}
|
134
|
+
expected_errors = [
|
135
|
+
{
|
136
|
+
"message"=>"Timeout on NestedSleep.seconds",
|
137
|
+
"locations"=>[{"line"=>10, "column"=>15}],
|
138
|
+
"path"=>["a", "b", "c", "d", "seconds"]
|
139
|
+
},
|
140
|
+
{
|
141
|
+
"message"=>"Timeout on NestedSleep.nestedSleep",
|
142
|
+
"locations"=>[{"line"=>11, "column"=>15}],
|
143
|
+
"path"=>["a", "b", "c", "d", "e"]
|
144
|
+
},
|
145
|
+
]
|
146
|
+
|
147
|
+
assert_equal expected_data, result["data"]
|
148
|
+
assert_equal expected_errors, result["errors"]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
describe "long-running fields" do
|
153
|
+
let(:query_string) {%|
|
154
|
+
{
|
155
|
+
a: sleepFor(seconds: 0.2)
|
156
|
+
b: sleepFor(seconds: 0.2)
|
157
|
+
c: sleepFor(seconds: 0.8)
|
158
|
+
d: sleepFor(seconds: 0.1)
|
159
|
+
}
|
160
|
+
|}
|
161
|
+
it "doesn't terminate long-running field execution" do
|
162
|
+
expected_data = {
|
163
|
+
"a"=>0.2,
|
164
|
+
"b"=>0.2,
|
165
|
+
"c"=>0.8,
|
166
|
+
"d"=>nil,
|
167
|
+
}
|
168
|
+
|
169
|
+
expected_errors = [
|
170
|
+
{
|
171
|
+
"message"=>"Timeout on Query.sleepFor",
|
172
|
+
"locations"=>[{"line"=>6, "column"=>9}],
|
173
|
+
"path"=>["d"]
|
174
|
+
},
|
175
|
+
]
|
176
|
+
|
177
|
+
assert_equal expected_data, result["data"]
|
178
|
+
assert_equal expected_errors, result["errors"]
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
describe "with a custom block" do
|
183
|
+
let(:timeout_class) do
|
184
|
+
Class.new(GraphQL::Schema::Timeout) do
|
185
|
+
def handle_timeout(err, query)
|
186
|
+
raise("Query timed out after 2s: #{err.message}")
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
let(:query_string) {%|
|
192
|
+
{
|
193
|
+
a: sleepFor(seconds: 0.4)
|
194
|
+
b: sleepFor(seconds: 0.4)
|
195
|
+
c: sleepFor(seconds: 0.4)
|
196
|
+
d: sleepFor(seconds: 0.4)
|
197
|
+
e: sleepFor(seconds: 0.4)
|
198
|
+
}
|
199
|
+
|}
|
200
|
+
|
201
|
+
it "calls the block" do
|
202
|
+
err = assert_raises(RuntimeError) { result }
|
203
|
+
assert_equal "Query timed out after 2s: Timeout on Query.sleepFor", err.message
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|