graphql 1.6.8 → 1.7.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 +5 -0
  3. data/lib/graphql/analysis/analyze_query.rb +21 -17
  4. data/lib/graphql/argument.rb +6 -2
  5. data/lib/graphql/backtrace.rb +50 -0
  6. data/lib/graphql/backtrace/inspect_result.rb +51 -0
  7. data/lib/graphql/backtrace/table.rb +120 -0
  8. data/lib/graphql/backtrace/traced_error.rb +55 -0
  9. data/lib/graphql/backtrace/tracer.rb +50 -0
  10. data/lib/graphql/enum_type.rb +1 -10
  11. data/lib/graphql/execution.rb +1 -2
  12. data/lib/graphql/execution/execute.rb +98 -89
  13. data/lib/graphql/execution/flatten.rb +40 -0
  14. data/lib/graphql/execution/lazy/resolve.rb +7 -7
  15. data/lib/graphql/execution/multiplex.rb +29 -29
  16. data/lib/graphql/field.rb +5 -1
  17. data/lib/graphql/internal_representation/node.rb +16 -0
  18. data/lib/graphql/invalid_name_error.rb +11 -0
  19. data/lib/graphql/language/parser.rb +11 -5
  20. data/lib/graphql/language/parser.y +11 -5
  21. data/lib/graphql/name_validator.rb +16 -0
  22. data/lib/graphql/object_type.rb +5 -0
  23. data/lib/graphql/query.rb +28 -7
  24. data/lib/graphql/query/context.rb +155 -52
  25. data/lib/graphql/query/literal_input.rb +36 -9
  26. data/lib/graphql/query/null_context.rb +7 -1
  27. data/lib/graphql/query/result.rb +63 -0
  28. data/lib/graphql/query/serial_execution/field_resolution.rb +3 -4
  29. data/lib/graphql/query/serial_execution/value_resolution.rb +3 -4
  30. data/lib/graphql/query/variables.rb +1 -1
  31. data/lib/graphql/schema.rb +31 -0
  32. data/lib/graphql/schema/traversal.rb +16 -1
  33. data/lib/graphql/schema/warden.rb +40 -4
  34. data/lib/graphql/static_validation/validator.rb +20 -18
  35. data/lib/graphql/subscriptions.rb +129 -0
  36. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +122 -0
  37. data/lib/graphql/subscriptions/event.rb +52 -0
  38. data/lib/graphql/subscriptions/instrumentation.rb +68 -0
  39. data/lib/graphql/tracing.rb +80 -0
  40. data/lib/graphql/tracing/active_support_notifications_tracing.rb +31 -0
  41. data/lib/graphql/version.rb +1 -1
  42. data/readme.md +1 -1
  43. data/spec/graphql/analysis/analyze_query_spec.rb +19 -0
  44. data/spec/graphql/argument_spec.rb +28 -0
  45. data/spec/graphql/backtrace_spec.rb +144 -0
  46. data/spec/graphql/define/assign_argument_spec.rb +12 -0
  47. data/spec/graphql/enum_type_spec.rb +1 -1
  48. data/spec/graphql/execution/execute_spec.rb +66 -0
  49. data/spec/graphql/execution/lazy_spec.rb +4 -3
  50. data/spec/graphql/language/parser_spec.rb +16 -0
  51. data/spec/graphql/object_type_spec.rb +14 -0
  52. data/spec/graphql/query/context_spec.rb +134 -27
  53. data/spec/graphql/query/result_spec.rb +29 -0
  54. data/spec/graphql/query/variables_spec.rb +13 -0
  55. data/spec/graphql/query_spec.rb +22 -0
  56. data/spec/graphql/schema/build_from_definition_spec.rb +2 -0
  57. data/spec/graphql/schema/traversal_spec.rb +70 -12
  58. data/spec/graphql/schema/warden_spec.rb +67 -1
  59. data/spec/graphql/schema_spec.rb +29 -0
  60. data/spec/graphql/static_validation/validator_spec.rb +16 -0
  61. data/spec/graphql/subscriptions_spec.rb +331 -0
  62. data/spec/graphql/tracing/active_support_notifications_tracing_spec.rb +57 -0
  63. data/spec/graphql/tracing_spec.rb +47 -0
  64. data/spec/spec_helper.rb +32 -0
  65. data/spec/support/star_wars/schema.rb +39 -0
  66. metadata +27 -4
  67. data/lib/graphql/execution/field_result.rb +0 -54
  68. data/lib/graphql/execution/selection_result.rb +0 -90
@@ -69,6 +69,8 @@ enum Color {
69
69
 
70
70
  # What a great type
71
71
  type Hello {
72
+ anEnum: Color
73
+
72
74
  # And a field to boot
73
75
  str: String
74
76
  }
@@ -2,10 +2,9 @@
2
2
  require "spec_helper"
3
3
 
4
4
  describe GraphQL::Schema::Traversal do
5
- def reduce_types(types)
5
+ def traversal(types)
6
6
  schema = GraphQL::Schema.define(orphan_types: types, resolve_type: :dummy)
7
- traversal = GraphQL::Schema::Traversal.new(schema, introspection: false)
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 = reduce_types([])
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 = reduce_types([Dummy::CheeseType])
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 = reduce_types([Dummy::DairyAppQueryType])
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 = reduce_types([type])
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 = reduce_types([type_parent])
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) { reduce_types([invalid_type]) }
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) { reduce_types([another_invalid_type]) }
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
- reduce_types([type_1, type_2])
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 = reduce_types([])
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
  {
@@ -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