graphql 1.6.8 → 1.7.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 +5 -0
- data/lib/graphql/analysis/analyze_query.rb +21 -17
- data/lib/graphql/argument.rb +6 -2
- data/lib/graphql/backtrace.rb +50 -0
- data/lib/graphql/backtrace/inspect_result.rb +51 -0
- data/lib/graphql/backtrace/table.rb +120 -0
- data/lib/graphql/backtrace/traced_error.rb +55 -0
- data/lib/graphql/backtrace/tracer.rb +50 -0
- data/lib/graphql/enum_type.rb +1 -10
- data/lib/graphql/execution.rb +1 -2
- data/lib/graphql/execution/execute.rb +98 -89
- data/lib/graphql/execution/flatten.rb +40 -0
- data/lib/graphql/execution/lazy/resolve.rb +7 -7
- data/lib/graphql/execution/multiplex.rb +29 -29
- data/lib/graphql/field.rb +5 -1
- data/lib/graphql/internal_representation/node.rb +16 -0
- data/lib/graphql/invalid_name_error.rb +11 -0
- data/lib/graphql/language/parser.rb +11 -5
- data/lib/graphql/language/parser.y +11 -5
- data/lib/graphql/name_validator.rb +16 -0
- data/lib/graphql/object_type.rb +5 -0
- data/lib/graphql/query.rb +28 -7
- data/lib/graphql/query/context.rb +155 -52
- data/lib/graphql/query/literal_input.rb +36 -9
- data/lib/graphql/query/null_context.rb +7 -1
- data/lib/graphql/query/result.rb +63 -0
- data/lib/graphql/query/serial_execution/field_resolution.rb +3 -4
- data/lib/graphql/query/serial_execution/value_resolution.rb +3 -4
- data/lib/graphql/query/variables.rb +1 -1
- data/lib/graphql/schema.rb +31 -0
- data/lib/graphql/schema/traversal.rb +16 -1
- data/lib/graphql/schema/warden.rb +40 -4
- data/lib/graphql/static_validation/validator.rb +20 -18
- data/lib/graphql/subscriptions.rb +129 -0
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +122 -0
- data/lib/graphql/subscriptions/event.rb +52 -0
- data/lib/graphql/subscriptions/instrumentation.rb +68 -0
- data/lib/graphql/tracing.rb +80 -0
- data/lib/graphql/tracing/active_support_notifications_tracing.rb +31 -0
- data/lib/graphql/version.rb +1 -1
- data/readme.md +1 -1
- data/spec/graphql/analysis/analyze_query_spec.rb +19 -0
- data/spec/graphql/argument_spec.rb +28 -0
- data/spec/graphql/backtrace_spec.rb +144 -0
- data/spec/graphql/define/assign_argument_spec.rb +12 -0
- data/spec/graphql/enum_type_spec.rb +1 -1
- data/spec/graphql/execution/execute_spec.rb +66 -0
- data/spec/graphql/execution/lazy_spec.rb +4 -3
- data/spec/graphql/language/parser_spec.rb +16 -0
- data/spec/graphql/object_type_spec.rb +14 -0
- data/spec/graphql/query/context_spec.rb +134 -27
- data/spec/graphql/query/result_spec.rb +29 -0
- data/spec/graphql/query/variables_spec.rb +13 -0
- data/spec/graphql/query_spec.rb +22 -0
- data/spec/graphql/schema/build_from_definition_spec.rb +2 -0
- data/spec/graphql/schema/traversal_spec.rb +70 -12
- data/spec/graphql/schema/warden_spec.rb +67 -1
- data/spec/graphql/schema_spec.rb +29 -0
- data/spec/graphql/static_validation/validator_spec.rb +16 -0
- data/spec/graphql/subscriptions_spec.rb +331 -0
- data/spec/graphql/tracing/active_support_notifications_tracing_spec.rb +57 -0
- data/spec/graphql/tracing_spec.rb +47 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/star_wars/schema.rb +39 -0
- metadata +27 -4
- data/lib/graphql/execution/field_result.rb +0 -54
- data/lib/graphql/execution/selection_result.rb +0 -90
@@ -2,10 +2,9 @@
|
|
2
2
|
require "spec_helper"
|
3
3
|
|
4
4
|
describe GraphQL::Schema::Traversal do
|
5
|
-
def
|
5
|
+
def traversal(types)
|
6
6
|
schema = GraphQL::Schema.define(orphan_types: types, resolve_type: :dummy)
|
7
|
-
|
8
|
-
traversal.type_map
|
7
|
+
GraphQL::Schema::Traversal.new(schema, introspection: false)
|
9
8
|
end
|
10
9
|
|
11
10
|
it "finds types from directives" do
|
@@ -13,7 +12,7 @@ describe GraphQL::Schema::Traversal do
|
|
13
12
|
"Boolean" => GraphQL::BOOLEAN_TYPE, # `skip` argument
|
14
13
|
"String" => GraphQL::STRING_TYPE # `deprecated` argument
|
15
14
|
}
|
16
|
-
result =
|
15
|
+
result = traversal([]).type_map
|
17
16
|
assert_equal(expected.keys.sort, result.keys.sort)
|
18
17
|
assert_equal(expected, result.to_h)
|
19
18
|
end
|
@@ -31,13 +30,13 @@ describe GraphQL::Schema::Traversal do
|
|
31
30
|
"AnimalProduct" => Dummy::AnimalProductInterface,
|
32
31
|
"LocalProduct" => Dummy::LocalProductInterface,
|
33
32
|
}
|
34
|
-
result =
|
33
|
+
result = traversal([Dummy::CheeseType]).type_map
|
35
34
|
assert_equal(expected.keys.sort, result.keys.sort)
|
36
35
|
assert_equal(expected, result.to_h)
|
37
36
|
end
|
38
37
|
|
39
38
|
it "finds type from arguments" do
|
40
|
-
result =
|
39
|
+
result = traversal([Dummy::DairyAppQueryType]).type_map
|
41
40
|
assert_equal(Dummy::DairyProductInputType, result["DairyProductInput"])
|
42
41
|
end
|
43
42
|
|
@@ -47,7 +46,7 @@ describe GraphQL::Schema::Traversal do
|
|
47
46
|
connection :t, type.connection_type
|
48
47
|
end
|
49
48
|
|
50
|
-
result =
|
49
|
+
result = traversal([type]).type_map
|
51
50
|
expected_types = [
|
52
51
|
"ArgTypeTest", "ArgTypeTestConnection", "ArgTypeTestEdge",
|
53
52
|
"Boolean", "Int", "PageInfo", "String"
|
@@ -66,7 +65,7 @@ describe GraphQL::Schema::Traversal do
|
|
66
65
|
input_field :child, type_child
|
67
66
|
end
|
68
67
|
|
69
|
-
result =
|
68
|
+
result = traversal([type_parent]).type_map
|
70
69
|
expected = {
|
71
70
|
"Boolean" => GraphQL::BOOLEAN_TYPE,
|
72
71
|
"String" => GraphQL::STRING_TYPE,
|
@@ -92,11 +91,11 @@ describe GraphQL::Schema::Traversal do
|
|
92
91
|
}
|
93
92
|
|
94
93
|
it "raises an InvalidTypeError when passed nil" do
|
95
|
-
assert_raises(GraphQL::Schema::InvalidTypeError) {
|
94
|
+
assert_raises(GraphQL::Schema::InvalidTypeError) { traversal([invalid_type]) }
|
96
95
|
end
|
97
96
|
|
98
97
|
it "raises an InvalidTypeError when passed an object that isnt a GraphQL::BaseType" do
|
99
|
-
assert_raises(GraphQL::Schema::InvalidTypeError) {
|
98
|
+
assert_raises(GraphQL::Schema::InvalidTypeError) { traversal([another_invalid_type]) }
|
100
99
|
end
|
101
100
|
end
|
102
101
|
|
@@ -113,14 +112,14 @@ describe GraphQL::Schema::Traversal do
|
|
113
112
|
}
|
114
113
|
it "raises an error" do
|
115
114
|
assert_raises(RuntimeError) {
|
116
|
-
|
115
|
+
traversal([type_1, type_2])
|
117
116
|
}
|
118
117
|
end
|
119
118
|
end
|
120
119
|
|
121
120
|
describe "when getting a type which doesnt exist" do
|
122
121
|
it "raises an error" do
|
123
|
-
type_map =
|
122
|
+
type_map = traversal([]).type_map
|
124
123
|
assert_raises(KeyError) { type_map.fetch("SomeType") }
|
125
124
|
end
|
126
125
|
end
|
@@ -130,4 +129,63 @@ describe GraphQL::Schema::Traversal do
|
|
130
129
|
assert_equal Dummy::HoneyType, Dummy::Schema.types["Honey"]
|
131
130
|
end
|
132
131
|
end
|
132
|
+
|
133
|
+
it "finds all references to types from fields and arguments" do
|
134
|
+
c_type = GraphQL::InputObjectType.define do
|
135
|
+
name "C"
|
136
|
+
input_field :someField, GraphQL::STRING_TYPE
|
137
|
+
end
|
138
|
+
|
139
|
+
b_type = GraphQL::ObjectType.define do
|
140
|
+
name "B"
|
141
|
+
field :anotherField, !GraphQL::STRING_TYPE do |field|
|
142
|
+
field.argument :anArgument, c_type
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
a_type = GraphQL::ObjectType.define do
|
147
|
+
name "A"
|
148
|
+
field :someField, b_type
|
149
|
+
end
|
150
|
+
|
151
|
+
include_if_argument = GraphQL::Directive::IncludeDirective.arguments["if"]
|
152
|
+
skip_if_argument = GraphQL::Directive::SkipDirective.arguments["if"]
|
153
|
+
deprecated_reason_argument = GraphQL::Directive::DeprecatedDirective.arguments["reason"]
|
154
|
+
|
155
|
+
expected = {
|
156
|
+
"Boolean" => [include_if_argument, skip_if_argument],
|
157
|
+
"B" => [a_type.fields["someField"]],
|
158
|
+
"String" => [deprecated_reason_argument, b_type.fields["anotherField"], c_type.input_fields["someField"]],
|
159
|
+
"C" => [b_type.fields["anotherField"].arguments["anArgument"]]
|
160
|
+
}
|
161
|
+
|
162
|
+
assert_equal expected, traversal([a_type, b_type, c_type]).type_reference_map
|
163
|
+
end
|
164
|
+
|
165
|
+
it "finds unions from which types are members" do
|
166
|
+
b_type = GraphQL::ObjectType.define do
|
167
|
+
name "B"
|
168
|
+
end
|
169
|
+
|
170
|
+
c_type = GraphQL::ObjectType.define do
|
171
|
+
name "C"
|
172
|
+
end
|
173
|
+
|
174
|
+
union = GraphQL::UnionType.define do
|
175
|
+
name "AUnion"
|
176
|
+
possible_types [b_type]
|
177
|
+
end
|
178
|
+
|
179
|
+
another_union = GraphQL::UnionType.define do
|
180
|
+
name "AnotherUnion"
|
181
|
+
possible_types [b_type, c_type]
|
182
|
+
end
|
183
|
+
|
184
|
+
result = traversal([union, another_union, b_type, c_type]).union_memberships
|
185
|
+
expected = {
|
186
|
+
"B" => [union, another_union],
|
187
|
+
"C" => [another_union]
|
188
|
+
}
|
189
|
+
assert_equal expected, result
|
190
|
+
end
|
133
191
|
end
|
@@ -69,10 +69,29 @@ module MaskHelpers
|
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
72
|
+
CheremeInput = GraphQL::InputObjectType.define do
|
73
|
+
name "CheremeInput"
|
74
|
+
input_field :name, types.String
|
75
|
+
end
|
76
|
+
|
77
|
+
Chereme = GraphQL::ObjectType.define do
|
78
|
+
name "Chereme"
|
79
|
+
description "A basic unit of signed communication"
|
80
|
+
field :name, types.String.to_non_null_type
|
81
|
+
end
|
82
|
+
|
83
|
+
Character = GraphQL::ObjectType.define do
|
84
|
+
name "Character"
|
85
|
+
interfaces [LanguageMemberInterface]
|
86
|
+
field :code, types.Int
|
87
|
+
end
|
88
|
+
|
72
89
|
QueryType = GraphQL::ObjectType.define do
|
73
90
|
name "Query"
|
74
91
|
field :languages, LanguageType.to_list_type do
|
75
|
-
argument :within, WithinInputType, "Find languages nearby a point"
|
92
|
+
argument :within, WithinInputType, "Find languages nearby a point" do
|
93
|
+
metadata :hidden_argument_with_input_object, true
|
94
|
+
end
|
76
95
|
end
|
77
96
|
field :language, LanguageType do
|
78
97
|
metadata :hidden_field, true
|
@@ -81,6 +100,10 @@ module MaskHelpers
|
|
81
100
|
end
|
82
101
|
end
|
83
102
|
|
103
|
+
field :chereme, Chereme do
|
104
|
+
metadata :hidden_field, true
|
105
|
+
end
|
106
|
+
|
84
107
|
field :phonemes, PhonemeType.to_list_type do
|
85
108
|
argument :manners, MannerEnum.to_list_type, "Filter phonemes by manner of articulation"
|
86
109
|
end
|
@@ -101,6 +124,12 @@ module MaskHelpers
|
|
101
124
|
field :add_phoneme, PhonemeType do
|
102
125
|
argument :symbol, types.String
|
103
126
|
end
|
127
|
+
|
128
|
+
field :add_chereme, types.String do
|
129
|
+
argument :chereme, CheremeInput do
|
130
|
+
metadata :hidden_argument, true
|
131
|
+
end
|
132
|
+
end
|
104
133
|
end
|
105
134
|
|
106
135
|
module FilterInstrumentation
|
@@ -120,6 +149,7 @@ module MaskHelpers
|
|
120
149
|
query QueryType
|
121
150
|
mutation MutationType
|
122
151
|
subscription MutationType
|
152
|
+
orphan_types [Character]
|
123
153
|
resolve_type ->(type, obj, ctx) { PhonemeType }
|
124
154
|
instrument :query, FilterInstrumentation
|
125
155
|
end
|
@@ -225,6 +255,17 @@ describe GraphQL::Schema::Warden do
|
|
225
255
|
->(member, ctx) { member.metadata[:hidden_field] || member.metadata[:hidden_type] }
|
226
256
|
}
|
227
257
|
|
258
|
+
it "hides types if no other fields are using it" do
|
259
|
+
query_string = %|
|
260
|
+
{
|
261
|
+
Chereme: __type(name: "Chereme") { fields { name } }
|
262
|
+
}
|
263
|
+
|
|
264
|
+
|
265
|
+
res = MaskHelpers.query_with_mask(query_string, mask)
|
266
|
+
assert_nil res["data"]["Chereme"]
|
267
|
+
end
|
268
|
+
|
228
269
|
it "causes validation errors" do
|
229
270
|
query_string = %|{ phoneme(symbol: "ϕ") { name } }|
|
230
271
|
res = MaskHelpers.query_with_mask(query_string, mask)
|
@@ -370,6 +411,20 @@ describe GraphQL::Schema::Warden do
|
|
370
411
|
interfaces_names = res["data"]["__type"]["interfaces"].map { |i| i["name"] }
|
371
412
|
refute_includes interfaces_names, "LanguageMember"
|
372
413
|
end
|
414
|
+
|
415
|
+
it "hides implementations if they are not referenced anywhere else" do
|
416
|
+
query_string = %|
|
417
|
+
{
|
418
|
+
__type(name: "Character") {
|
419
|
+
fields { name }
|
420
|
+
}
|
421
|
+
}
|
422
|
+
|
|
423
|
+
|
424
|
+
res = MaskHelpers.query_with_mask(query_string, mask)
|
425
|
+
type = res["data"]["__type"]
|
426
|
+
assert_equal nil, type
|
427
|
+
end
|
373
428
|
end
|
374
429
|
end
|
375
430
|
|
@@ -379,6 +434,17 @@ describe GraphQL::Schema::Warden do
|
|
379
434
|
->(member, ctx) { member.metadata[:hidden_argument] || member.metadata[:hidden_input_type] }
|
380
435
|
}
|
381
436
|
|
437
|
+
it "hides types if no other fields or arguments are using it" do
|
438
|
+
query_string = %|
|
439
|
+
{
|
440
|
+
CheremeInput: __type(name: "CheremeInput") { fields { name } }
|
441
|
+
}
|
442
|
+
|
|
443
|
+
|
444
|
+
res = MaskHelpers.query_with_mask(query_string, mask)
|
445
|
+
assert_nil res["data"]["CheremeInput"]
|
446
|
+
end
|
447
|
+
|
382
448
|
it "isn't present in introspection" do
|
383
449
|
query_string = %|
|
384
450
|
{
|
data/spec/graphql/schema_spec.rb
CHANGED
@@ -17,6 +17,35 @@ describe GraphQL::Schema do
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
+
describe "#union_memberships" do
|
21
|
+
it "returns a list of unions that include the type" do
|
22
|
+
assert_equal [schema.types["Animal"], schema.types["AnimalAsCow"]], schema.union_memberships(schema.types["Cow"])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#root_types" do
|
27
|
+
it "returns a list of the schema's root types" do
|
28
|
+
assert_equal(
|
29
|
+
[
|
30
|
+
Dummy::DairyAppQueryType,
|
31
|
+
Dummy::DairyAppMutationType,
|
32
|
+
Dummy::SubscriptionType
|
33
|
+
],
|
34
|
+
schema.root_types
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#references_to" do
|
40
|
+
it "returns a list of Field and Arguments of that type" do
|
41
|
+
assert_equal [schema.types["Query"].fields["cow"]], schema.references_to("Cow")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "returns an empty list when type is not referenced by any field or argument" do
|
45
|
+
assert_equal [], schema.references_to("Goat")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
20
49
|
describe "#to_definition" do
|
21
50
|
it "prints out the schema definition" do
|
22
51
|
assert_equal schema.to_definition, GraphQL::Schema::Printer.print_schema(schema)
|
@@ -7,6 +7,22 @@ describe GraphQL::StaticValidation::Validator do
|
|
7
7
|
let(:validate) { true }
|
8
8
|
let(:errors) { validator.validate(query, validate: validate)[:errors].map(&:to_h) }
|
9
9
|
|
10
|
+
describe "tracing" do
|
11
|
+
let(:query_string) { "{ t: __typename }"}
|
12
|
+
|
13
|
+
it "emits a trace" do
|
14
|
+
traces = TestTracing.with_trace do
|
15
|
+
validator.validate(query)
|
16
|
+
end
|
17
|
+
|
18
|
+
assert_equal 3, traces.length
|
19
|
+
_lex_trace, _parse_trace, validate_trace = traces
|
20
|
+
assert_equal "validate", validate_trace[:key]
|
21
|
+
assert_equal true, validate_trace[:validate]
|
22
|
+
assert_instance_of GraphQL::Query, validate_trace[:query]
|
23
|
+
assert_instance_of Hash, validate_trace[:result]
|
24
|
+
end
|
25
|
+
end
|
10
26
|
|
11
27
|
describe "validation order" do
|
12
28
|
let(:document) { GraphQL.parse(query_string)}
|
@@ -0,0 +1,331 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
class InMemoryBackend
|
5
|
+
class Subscriptions < GraphQL::Subscriptions
|
6
|
+
attr_reader :deliveries, :pushes, :extra
|
7
|
+
|
8
|
+
def initialize(schema:, extra:)
|
9
|
+
super
|
10
|
+
@extra = extra
|
11
|
+
@queries = {}
|
12
|
+
@subscriptions = Hash.new { |h, k| h[k] = [] }
|
13
|
+
@deliveries = Hash.new { |h, k| h[k] = [] }
|
14
|
+
@pushes = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def write_subscription(query, events)
|
18
|
+
@queries[query.context[:socket]] = query
|
19
|
+
events.each do |ev|
|
20
|
+
# The `context` is functioning as subscription data.
|
21
|
+
# IRL you'd have some other model that persisted the subscription
|
22
|
+
@subscriptions[ev.topic] << ev.context
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def each_subscription_id(event)
|
27
|
+
@subscriptions[event.topic].each do |ctx|
|
28
|
+
yield(ctx[:socket])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def read_subscription(channel)
|
33
|
+
query = @queries[channel]
|
34
|
+
{
|
35
|
+
query_string: query.query_string,
|
36
|
+
operation_name: query.operation_name,
|
37
|
+
variables: query.provided_variables,
|
38
|
+
context: { me: query.context[:me] },
|
39
|
+
transport: :socket,
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def delete_subscription(channel)
|
44
|
+
query = @queries.delete(channel)
|
45
|
+
if query
|
46
|
+
@subscriptions.each do |key, contexts|
|
47
|
+
contexts.delete(query.context)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def deliver(channel, result)
|
53
|
+
@deliveries[channel] << result
|
54
|
+
end
|
55
|
+
|
56
|
+
def execute(channel, event, object)
|
57
|
+
@pushes << channel
|
58
|
+
super
|
59
|
+
end
|
60
|
+
|
61
|
+
# Just for testing:
|
62
|
+
def reset
|
63
|
+
@queries.clear
|
64
|
+
@subscriptions.clear
|
65
|
+
@deliveries.clear
|
66
|
+
@pushes.clear
|
67
|
+
end
|
68
|
+
|
69
|
+
def size
|
70
|
+
@subscriptions.size
|
71
|
+
end
|
72
|
+
|
73
|
+
def subscriptions
|
74
|
+
@subscriptions
|
75
|
+
end
|
76
|
+
end
|
77
|
+
# Just a random stateful object for tracking what happens:
|
78
|
+
class Payload
|
79
|
+
attr_reader :str
|
80
|
+
|
81
|
+
def initialize
|
82
|
+
@str = "Update"
|
83
|
+
@counter = 0
|
84
|
+
end
|
85
|
+
|
86
|
+
def int
|
87
|
+
@counter += 1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
SchemaDefinition = <<-GRAPHQL
|
92
|
+
type Subscription {
|
93
|
+
payload(id: ID!): Payload!
|
94
|
+
event(stream: StreamInput): Payload
|
95
|
+
myEvent(type: PayloadType): Payload
|
96
|
+
}
|
97
|
+
|
98
|
+
type Payload {
|
99
|
+
str: String!
|
100
|
+
int: Int!
|
101
|
+
}
|
102
|
+
|
103
|
+
input StreamInput {
|
104
|
+
userId: ID!
|
105
|
+
type: PayloadType = ONE
|
106
|
+
}
|
107
|
+
|
108
|
+
# Arbitrary "kinds" of payloads which may be
|
109
|
+
# subscribed to separately
|
110
|
+
enum PayloadType {
|
111
|
+
ONE
|
112
|
+
TWO
|
113
|
+
}
|
114
|
+
|
115
|
+
type Query {
|
116
|
+
dummy: Int
|
117
|
+
}
|
118
|
+
GRAPHQL
|
119
|
+
|
120
|
+
Resolvers = {
|
121
|
+
"Subscription" => {
|
122
|
+
"payload" => ->(o,a,c) { o },
|
123
|
+
"myEvent" => ->(o,a,c) { o },
|
124
|
+
"event" => ->(o,a,c) { o },
|
125
|
+
},
|
126
|
+
}
|
127
|
+
Schema = GraphQL::Schema.from_definition(SchemaDefinition, default_resolve: Resolvers).redefine do
|
128
|
+
use InMemoryBackend::Subscriptions,
|
129
|
+
extra: 123
|
130
|
+
end
|
131
|
+
|
132
|
+
# TODO don't hack this (no way to add metadata from IDL parser right now)
|
133
|
+
Schema.get_field("Subscription", "myEvent").subscription_scope = :me
|
134
|
+
end
|
135
|
+
|
136
|
+
describe GraphQL::Subscriptions do
|
137
|
+
before do
|
138
|
+
schema.subscriptions.reset
|
139
|
+
end
|
140
|
+
|
141
|
+
let(:root_object) {
|
142
|
+
OpenStruct.new(
|
143
|
+
payload: InMemoryBackend::Payload.new,
|
144
|
+
)
|
145
|
+
}
|
146
|
+
|
147
|
+
let(:schema) { InMemoryBackend::Schema }
|
148
|
+
let(:implementation) { schema.subscriptions }
|
149
|
+
let(:deliveries) { implementation.deliveries }
|
150
|
+
describe "pushing updates" do
|
151
|
+
it "sends updated data" do
|
152
|
+
query_str = <<-GRAPHQL
|
153
|
+
subscription ($id: ID!){
|
154
|
+
firstPayload: payload(id: $id) { str, int }
|
155
|
+
otherPayload: payload(id: "900") { int }
|
156
|
+
}
|
157
|
+
GRAPHQL
|
158
|
+
|
159
|
+
# Initial subscriptions
|
160
|
+
res_1 = schema.execute(query_str, context: { socket: "1" }, variables: { "id" => "100" }, root_value: root_object)
|
161
|
+
res_2 = schema.execute(query_str, context: { socket: "2" }, variables: { "id" => "200" }, root_value: root_object)
|
162
|
+
|
163
|
+
# Initial response is nil, no broadcasts yet
|
164
|
+
assert_equal(nil, res_1["data"])
|
165
|
+
assert_equal(nil, res_2["data"])
|
166
|
+
assert_equal [], deliveries["1"]
|
167
|
+
assert_equal [], deliveries["2"]
|
168
|
+
|
169
|
+
# Application stuff happens.
|
170
|
+
# The application signals graphql via `subscriptions.trigger`:
|
171
|
+
schema.subscriptions.trigger("payload", {"id" => "100"}, root_object.payload)
|
172
|
+
schema.subscriptions.trigger("payload", {"id" => "200"}, root_object.payload)
|
173
|
+
schema.subscriptions.trigger("payload", {"id" => "100"}, root_object.payload)
|
174
|
+
schema.subscriptions.trigger("payload", {"id" => "300"}, nil)
|
175
|
+
|
176
|
+
# Let's see what GraphQL sent over the wire:
|
177
|
+
assert_equal({"str" => "Update", "int" => 1}, deliveries["1"][0]["data"]["firstPayload"])
|
178
|
+
assert_equal({"str" => "Update", "int" => 2}, deliveries["2"][0]["data"]["firstPayload"])
|
179
|
+
assert_equal({"str" => "Update", "int" => 3}, deliveries["1"][1]["data"]["firstPayload"])
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe "subscribing" do
|
184
|
+
it "doesn't call the subscriptions for invalid queries" do
|
185
|
+
query_str = <<-GRAPHQL
|
186
|
+
subscription ($id: ID){
|
187
|
+
payload(id: $id) { str, int }
|
188
|
+
}
|
189
|
+
GRAPHQL
|
190
|
+
|
191
|
+
res = schema.execute(query_str, context: { socket: "1" }, variables: { "id" => "100" }, root_value: root_object)
|
192
|
+
assert_equal true, res.key?("errors")
|
193
|
+
assert_equal 0, implementation.size
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
describe "trigger" do
|
198
|
+
it "uses the provided queue" do
|
199
|
+
query_str = <<-GRAPHQL
|
200
|
+
subscription ($id: ID!){
|
201
|
+
payload(id: $id) { str, int }
|
202
|
+
}
|
203
|
+
GRAPHQL
|
204
|
+
|
205
|
+
schema.execute(query_str, context: { socket: "1" }, variables: { "id" => "8" }, root_value: root_object)
|
206
|
+
schema.subscriptions.trigger("payload", { "id" => "8"}, root_object.payload)
|
207
|
+
assert_equal ["1"], implementation.pushes
|
208
|
+
end
|
209
|
+
|
210
|
+
it "pushes errors" do
|
211
|
+
query_str = <<-GRAPHQL
|
212
|
+
subscription ($id: ID!){
|
213
|
+
payload(id: $id) { str, int }
|
214
|
+
}
|
215
|
+
GRAPHQL
|
216
|
+
|
217
|
+
schema.execute(query_str, context: { socket: "1" }, variables: { "id" => "8" }, root_value: root_object)
|
218
|
+
schema.subscriptions.trigger("payload", { "id" => "8"}, OpenStruct.new(str: nil, int: nil))
|
219
|
+
delivery = deliveries["1"].first
|
220
|
+
assert_equal nil, delivery.fetch("data")
|
221
|
+
assert_equal 1, delivery["errors"].length
|
222
|
+
end
|
223
|
+
|
224
|
+
it "coerces args" do
|
225
|
+
query_str = <<-GRAPHQL
|
226
|
+
subscription($type: PayloadType) {
|
227
|
+
e1: event(stream: { userId: "3", type: $type }) { int }
|
228
|
+
}
|
229
|
+
GRAPHQL
|
230
|
+
|
231
|
+
# Subscribe with explicit `TYPE`
|
232
|
+
schema.execute(query_str, context: { socket: "1" }, variables: { "type" => "ONE" }, root_value: root_object)
|
233
|
+
# Subscribe with default `TYPE`
|
234
|
+
schema.execute(query_str, context: { socket: "2" }, root_value: root_object)
|
235
|
+
# Subscribe with non-matching `TYPE`
|
236
|
+
schema.execute(query_str, context: { socket: "3" }, variables: { "type" => "TWO" }, root_value: root_object)
|
237
|
+
# Subscribe with explicit null
|
238
|
+
schema.execute(query_str, context: { socket: "4" }, variables: { "type" => nil }, root_value: root_object)
|
239
|
+
|
240
|
+
# Trigger the subscription with coerceable args, different orders:
|
241
|
+
schema.subscriptions.trigger("event", { "stream" => {"userId" => 3, "type" => "ONE"} }, OpenStruct.new(str: "", int: 1))
|
242
|
+
schema.subscriptions.trigger("event", { "stream" => {"type" => "ONE", "userId" => "3"} }, OpenStruct.new(str: "", int: 2))
|
243
|
+
# This is a non-trigger
|
244
|
+
schema.subscriptions.trigger("event", { "stream" => {"userId" => "3", "type" => "TWO"} }, OpenStruct.new(str: "", int: 3))
|
245
|
+
# These get default value of ONE
|
246
|
+
schema.subscriptions.trigger("event", { "stream" => {"userId" => "3"} }, OpenStruct.new(str: "", int: 4))
|
247
|
+
# Trigger with null updates subscriptionss to null
|
248
|
+
schema.subscriptions.trigger("event", { "stream" => {"userId" => 3, "type" => nil} }, OpenStruct.new(str: "", int: 5))
|
249
|
+
|
250
|
+
assert_equal [1,2,4], deliveries["1"].map { |d| d["data"]["e1"]["int"] }
|
251
|
+
|
252
|
+
# Same as socket_1
|
253
|
+
assert_equal [1,2,4], deliveries["2"].map { |d| d["data"]["e1"]["int"] }
|
254
|
+
|
255
|
+
# Received the "non-trigger"
|
256
|
+
assert_equal [3], deliveries["3"].map { |d| d["data"]["e1"]["int"] }
|
257
|
+
|
258
|
+
# Received the trigger with null
|
259
|
+
assert_equal [5], deliveries["4"].map { |d| d["data"]["e1"]["int"] }
|
260
|
+
end
|
261
|
+
|
262
|
+
it "allows context-scoped subscriptions" do
|
263
|
+
query_str = <<-GRAPHQL
|
264
|
+
subscription($type: PayloadType) {
|
265
|
+
myEvent(type: $type) { int }
|
266
|
+
}
|
267
|
+
GRAPHQL
|
268
|
+
|
269
|
+
# Subscriptions for user 1
|
270
|
+
schema.execute(query_str, context: { socket: "1", me: "1" }, variables: { "type" => "ONE" }, root_value: root_object)
|
271
|
+
schema.execute(query_str, context: { socket: "2", me: "1" }, variables: { "type" => "TWO" }, root_value: root_object)
|
272
|
+
# Subscription for user 2
|
273
|
+
schema.execute(query_str, context: { socket: "3", me: "2" }, variables: { "type" => "ONE" }, root_value: root_object)
|
274
|
+
|
275
|
+
schema.subscriptions.trigger("myEvent", { "type" => "ONE" }, OpenStruct.new(str: "", int: 1), scope: "1")
|
276
|
+
schema.subscriptions.trigger("myEvent", { "type" => "TWO" }, OpenStruct.new(str: "", int: 2), scope: "1")
|
277
|
+
schema.subscriptions.trigger("myEvent", { "type" => "ONE" }, OpenStruct.new(str: "", int: 3), scope: "2")
|
278
|
+
|
279
|
+
# Delivered to user 1
|
280
|
+
assert_equal [1], deliveries["1"].map { |d| d["data"]["myEvent"]["int"] }
|
281
|
+
assert_equal [2], deliveries["2"].map { |d| d["data"]["myEvent"]["int"] }
|
282
|
+
# Delivered to user 2
|
283
|
+
assert_equal [3], deliveries["3"].map { |d| d["data"]["myEvent"]["int"] }
|
284
|
+
end
|
285
|
+
|
286
|
+
describe "errors" do
|
287
|
+
class ErrorPayload
|
288
|
+
def int
|
289
|
+
raise "Boom!"
|
290
|
+
end
|
291
|
+
|
292
|
+
def str
|
293
|
+
raise GraphQL::ExecutionError.new("This is handled")
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
it "lets unhandled errors crash "do
|
298
|
+
query_str = <<-GRAPHQL
|
299
|
+
subscription($type: PayloadType) {
|
300
|
+
myEvent(type: $type) { int }
|
301
|
+
}
|
302
|
+
GRAPHQL
|
303
|
+
|
304
|
+
schema.execute(query_str, context: { socket: "1", me: "1" }, variables: { "type" => "ONE" }, root_value: root_object)
|
305
|
+
err = assert_raises(RuntimeError) {
|
306
|
+
schema.subscriptions.trigger("myEvent", { "type" => "ONE" }, ErrorPayload.new, scope: "1")
|
307
|
+
}
|
308
|
+
assert_equal "Boom!", err.message
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
it "sends query errors to the subscriptions" do
|
313
|
+
query_str = <<-GRAPHQL
|
314
|
+
subscription($type: PayloadType) {
|
315
|
+
myEvent(type: $type) { str }
|
316
|
+
}
|
317
|
+
GRAPHQL
|
318
|
+
|
319
|
+
schema.execute(query_str, context: { socket: "1", me: "1" }, variables: { "type" => "ONE" }, root_value: root_object)
|
320
|
+
schema.subscriptions.trigger("myEvent", { "type" => "ONE" }, ErrorPayload.new, scope: "1")
|
321
|
+
res = deliveries["1"].first
|
322
|
+
assert_equal "This is handled", res["errors"][0]["message"]
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
describe "implementation" do
|
327
|
+
it "is initialized with keywords" do
|
328
|
+
assert_equal 123, schema.subscriptions.extra
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|