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