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
@@ -14,9 +14,22 @@ module GraphQL
|
|
14
14
|
else
|
15
15
|
case type
|
16
16
|
when GraphQL::ScalarType
|
17
|
-
|
17
|
+
# TODO smell
|
18
|
+
# This gets used for plain values during subscriber.trigger
|
19
|
+
if variables
|
20
|
+
type.coerce_input(ast_node, variables.context)
|
21
|
+
else
|
22
|
+
type.coerce_isolated_input(ast_node)
|
23
|
+
end
|
18
24
|
when GraphQL::EnumType
|
19
|
-
|
25
|
+
# TODO smell
|
26
|
+
# This gets used for plain values sometimes
|
27
|
+
v = ast_node.is_a?(GraphQL::Language::Nodes::Enum) ? ast_node.name : ast_node
|
28
|
+
if variables
|
29
|
+
type.coerce_input(v, variables.context)
|
30
|
+
else
|
31
|
+
type.coerce_isolated_input(v)
|
32
|
+
end
|
20
33
|
when GraphQL::NonNullType
|
21
34
|
LiteralInput.coerce(type.of_type, ast_node, variables)
|
22
35
|
when GraphQL::ListType
|
@@ -26,7 +39,9 @@ module GraphQL
|
|
26
39
|
[LiteralInput.coerce(type.of_type, ast_node, variables)]
|
27
40
|
end
|
28
41
|
when GraphQL::InputObjectType
|
29
|
-
|
42
|
+
# TODO smell: handling AST vs handling plain Ruby
|
43
|
+
next_args = ast_node.is_a?(Hash) ? ast_node : ast_node.arguments
|
44
|
+
from_arguments(next_args, type.arguments, variables)
|
30
45
|
end
|
31
46
|
end
|
32
47
|
end
|
@@ -43,7 +58,14 @@ module GraphQL
|
|
43
58
|
# Variables is nil when making .defaults_for
|
44
59
|
context = variables ? variables.context : nil
|
45
60
|
values_hash = {}
|
46
|
-
indexed_arguments = ast_arguments
|
61
|
+
indexed_arguments = case ast_arguments
|
62
|
+
when Hash
|
63
|
+
ast_arguments
|
64
|
+
when Array
|
65
|
+
ast_arguments.each_with_object({}) { |a, memo| memo[a.name] = a }
|
66
|
+
else
|
67
|
+
raise ArgumentError, "Unexpected ast_arguments: #{ast_arguments}"
|
68
|
+
end
|
47
69
|
|
48
70
|
argument_defns.each do |arg_name, arg_defn|
|
49
71
|
ast_arg = indexed_arguments[arg_name]
|
@@ -51,12 +73,17 @@ module GraphQL
|
|
51
73
|
# If the value is a variable,
|
52
74
|
# only add a value if the variable is actually present.
|
53
75
|
# Otherwise, coerce the value in the AST, prepare the value and add it.
|
54
|
-
|
55
|
-
|
76
|
+
#
|
77
|
+
# TODO: since indexed_arguments can come from a plain Ruby hash,
|
78
|
+
# have to check for `false` or `nil` as hash values. This is getting smelly :S
|
79
|
+
if indexed_arguments.key?(arg_name)
|
80
|
+
arg_value = ast_arg.is_a?(GraphQL::Language::Nodes::Argument) ? ast_arg.value : ast_arg
|
81
|
+
|
82
|
+
value_is_a_variable = arg_value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
|
56
83
|
|
57
|
-
if (!value_is_a_variable || (value_is_a_variable && variables.key?(
|
84
|
+
if (!value_is_a_variable || (value_is_a_variable && variables.key?(arg_value.name)))
|
58
85
|
|
59
|
-
value = coerce(arg_defn.type,
|
86
|
+
value = coerce(arg_defn.type, arg_value, variables)
|
60
87
|
value = arg_defn.prepare(value, context)
|
61
88
|
|
62
89
|
if value.is_a?(GraphQL::ExecutionError)
|
@@ -64,7 +91,7 @@ module GraphQL
|
|
64
91
|
raise value
|
65
92
|
end
|
66
93
|
|
67
|
-
values_hash[
|
94
|
+
values_hash[arg_name] = value
|
68
95
|
end
|
69
96
|
end
|
70
97
|
|
@@ -3,12 +3,18 @@ module GraphQL
|
|
3
3
|
class Query
|
4
4
|
# This object can be `ctx` in places where there is no query
|
5
5
|
class NullContext
|
6
|
+
class NullWarden < GraphQL::Schema::Warden
|
7
|
+
def visible?(t); true; end
|
8
|
+
def visible_field?(t); true; end
|
9
|
+
def visible_type?(t); true; end
|
10
|
+
end
|
11
|
+
|
6
12
|
attr_reader :schema, :query, :warden
|
7
13
|
|
8
14
|
def initialize
|
9
15
|
@query = nil
|
10
16
|
@schema = GraphQL::Schema.new
|
11
|
-
@warden =
|
17
|
+
@warden = NullWarden.new(
|
12
18
|
GraphQL::Filter.new,
|
13
19
|
context: self,
|
14
20
|
schema: @schema,
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
class Query
|
5
|
+
# A result from {Schema#execute}.
|
6
|
+
# It provides the requested data and
|
7
|
+
# access to the {Query} and {Query::Context}.
|
8
|
+
class Result
|
9
|
+
extend GraphQL::Delegate
|
10
|
+
|
11
|
+
def initialize(query:, values:)
|
12
|
+
@query = query
|
13
|
+
@to_h = values
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [GraphQL::Query] The query that was executed
|
17
|
+
attr_reader :query
|
18
|
+
|
19
|
+
# @return [Hash] The resulting hash of "data" and/or "errors"
|
20
|
+
attr_reader :to_h
|
21
|
+
|
22
|
+
def_delegators :@query, :context, :mutation?, :query?, :subscription?
|
23
|
+
|
24
|
+
def_delegators :@to_h, :[], :keys, :values, :to_json, :as_json
|
25
|
+
|
26
|
+
# Delegate any hash-like method to the underlying hash.
|
27
|
+
def method_missing(method_name, *args, &block)
|
28
|
+
if @to_h.respond_to?(method_name)
|
29
|
+
@to_h.public_send(method_name, *args, &block)
|
30
|
+
else
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def respond_to_missing?(method_name, include_private = false)
|
36
|
+
@to_h.respond_to?(method_name) || super
|
37
|
+
end
|
38
|
+
|
39
|
+
def inspect
|
40
|
+
"#<GraphQL::Query::Result @query=... @to_h=#{@to_h} >"
|
41
|
+
end
|
42
|
+
|
43
|
+
# A result is equal to another object when:
|
44
|
+
#
|
45
|
+
# - The other object is a Hash whose value matches `result.to_h`
|
46
|
+
# - The other object is a Result whose value matches `result.to_h`
|
47
|
+
#
|
48
|
+
# (The query is ignored for comparing result equality.)
|
49
|
+
#
|
50
|
+
# @return [Boolean]
|
51
|
+
def ==(other)
|
52
|
+
case other
|
53
|
+
when Hash
|
54
|
+
@to_h == other
|
55
|
+
when Query::Result
|
56
|
+
@to_h == other.to_h
|
57
|
+
else
|
58
|
+
super
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -12,11 +12,10 @@ module GraphQL
|
|
12
12
|
@target = target
|
13
13
|
@query = query_ctx.query
|
14
14
|
@field = irep_node.definition
|
15
|
-
@field_ctx = query_ctx.
|
15
|
+
@field_ctx = query_ctx.spawn_child(
|
16
16
|
key: irep_node.name,
|
17
|
-
|
18
|
-
|
19
|
-
field: field,
|
17
|
+
object: target,
|
18
|
+
irep_node: irep_node,
|
20
19
|
)
|
21
20
|
@arguments = @query.arguments_for(irep_node, @field)
|
22
21
|
end
|
@@ -23,11 +23,10 @@ module GraphQL
|
|
23
23
|
result = []
|
24
24
|
i = 0
|
25
25
|
value.each do |inner_value|
|
26
|
-
inner_ctx = query_ctx.
|
26
|
+
inner_ctx = query_ctx.spawn_child(
|
27
27
|
key: i,
|
28
|
-
|
29
|
-
|
30
|
-
field: field_defn,
|
28
|
+
object: inner_value,
|
29
|
+
irep_node: selection,
|
31
30
|
)
|
32
31
|
|
33
32
|
result << resolve(
|
data/lib/graphql/schema.rb
CHANGED
@@ -81,6 +81,10 @@ module GraphQL
|
|
81
81
|
:cursor_encoder,
|
82
82
|
:raise_definition_error
|
83
83
|
|
84
|
+
# Single, long-lived instance of the provided subscriptions class, if there is one.
|
85
|
+
# @return [GraphQL::Subscriptions]
|
86
|
+
attr_accessor :subscriptions
|
87
|
+
|
84
88
|
# @return [MiddlewareChain] MiddlewareChain which is applied to fields during execution
|
85
89
|
attr_accessor :middleware
|
86
90
|
|
@@ -212,6 +216,14 @@ module GraphQL
|
|
212
216
|
end
|
213
217
|
end
|
214
218
|
|
219
|
+
# @return [Array<GraphQL::BaseType>] The root types of this schema
|
220
|
+
def root_types
|
221
|
+
@root_types ||= begin
|
222
|
+
rebuild_artifacts
|
223
|
+
@root_types
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
215
227
|
# @see [GraphQL::Schema::Warden] Restricted access to members of a schema
|
216
228
|
# @return [GraphQL::Schema::TypeMap] `{ name => type }` pairs of types in this schema
|
217
229
|
def types
|
@@ -221,6 +233,22 @@ module GraphQL
|
|
221
233
|
end
|
222
234
|
end
|
223
235
|
|
236
|
+
# Returns a list of Arguments and Fields referencing a certain type
|
237
|
+
# @param type_name [String]
|
238
|
+
# @return [Hash]
|
239
|
+
def references_to(type_name)
|
240
|
+
rebuild_artifacts unless defined?(@type_reference_map)
|
241
|
+
@type_reference_map.fetch(type_name, [])
|
242
|
+
end
|
243
|
+
|
244
|
+
# Returns a list of Union types in which a type is a member
|
245
|
+
# @param type [GraphQL::ObjectType]
|
246
|
+
# @return [Array<GraphQL::UnionType>] list of union types of which the type is a member
|
247
|
+
def union_memberships(type)
|
248
|
+
rebuild_artifacts unless defined?(@union_memberships)
|
249
|
+
@union_memberships.fetch(type.name, [])
|
250
|
+
end
|
251
|
+
|
224
252
|
# Execute a query on itself. Raises an error if the schema definition is invalid.
|
225
253
|
# @see {Query#initialize} for arguments.
|
226
254
|
# @return [Hash] query result, ready to be serialized as JSON
|
@@ -544,7 +572,10 @@ module GraphQL
|
|
544
572
|
@rebuilding_artifacts = true
|
545
573
|
traversal = Traversal.new(self)
|
546
574
|
@types = traversal.type_map
|
575
|
+
@root_types = [query, mutation, subscription]
|
547
576
|
@instrumented_field_map = traversal.instrumented_field_map
|
577
|
+
@type_reference_map = traversal.type_reference_map
|
578
|
+
@union_memberships = traversal.union_memberships
|
548
579
|
end
|
549
580
|
ensure
|
550
581
|
@rebuilding_artifacts = false
|
@@ -10,6 +10,12 @@ module GraphQL
|
|
10
10
|
# @return [Hash<String => Hash<String => GraphQL::Field>>]
|
11
11
|
attr_reader :instrumented_field_map
|
12
12
|
|
13
|
+
# @return [Hash<String => Array<GraphQL::Field || GraphQL::Argument || GraphQL::Directive>]
|
14
|
+
attr_reader :type_reference_map
|
15
|
+
|
16
|
+
# @return [Hash<String => Array<GraphQL::BaseType>]
|
17
|
+
attr_reader :union_memberships
|
18
|
+
|
13
19
|
# @param schema [GraphQL::Schema]
|
14
20
|
def initialize(schema, introspection: true)
|
15
21
|
@schema = schema
|
@@ -21,6 +27,8 @@ module GraphQL
|
|
21
27
|
|
22
28
|
@type_map = {}
|
23
29
|
@instrumented_field_map = Hash.new { |h, k| h[k] = {} }
|
30
|
+
@type_reference_map = Hash.new { |h, k| h[k] = [] }
|
31
|
+
@union_memberships = Hash.new { |h, k| h[k] = [] }
|
24
32
|
visit(schema, nil)
|
25
33
|
end
|
26
34
|
|
@@ -40,6 +48,7 @@ module GraphQL
|
|
40
48
|
visit_roots.each { |t| visit(t, t.name) }
|
41
49
|
when GraphQL::Directive
|
42
50
|
member.arguments.each do |name, argument|
|
51
|
+
@type_reference_map[argument.type.unwrap.to_s] << argument
|
43
52
|
visit(argument.type, "Directive argument #{member.name}.#{name}")
|
44
53
|
end
|
45
54
|
when GraphQL::BaseType
|
@@ -56,9 +65,13 @@ module GraphQL
|
|
56
65
|
when GraphQL::InterfaceType
|
57
66
|
visit_fields(type_defn)
|
58
67
|
when GraphQL::UnionType
|
59
|
-
type_defn.possible_types.each
|
68
|
+
type_defn.possible_types.each do |t|
|
69
|
+
@union_memberships[t.name] << type_defn
|
70
|
+
visit(t, "Possible type for #{type_defn.name}")
|
71
|
+
end
|
60
72
|
when GraphQL::InputObjectType
|
61
73
|
type_defn.arguments.each do |name, arg|
|
74
|
+
@type_reference_map[arg.type.unwrap.to_s] << arg
|
62
75
|
visit(arg.type, "Input field #{type_defn.name}.#{name}")
|
63
76
|
end
|
64
77
|
end
|
@@ -78,8 +91,10 @@ module GraphQL
|
|
78
91
|
inst.instrument(type_defn, defn)
|
79
92
|
end
|
80
93
|
@instrumented_field_map[type_defn.name][instrumented_field_defn.name] = instrumented_field_defn
|
94
|
+
@type_reference_map[instrumented_field_defn.type.unwrap.name] << instrumented_field_defn
|
81
95
|
visit(instrumented_field_defn.type, "Field #{type_defn.name}.#{instrumented_field_defn.name}'s return type")
|
82
96
|
instrumented_field_defn.arguments.each do |name, arg|
|
97
|
+
@type_reference_map[arg.type.unwrap.to_s] << arg
|
83
98
|
visit(arg.type, "Argument #{name} on #{type_defn.name}.#{instrumented_field_defn.name}")
|
84
99
|
end
|
85
100
|
end
|
@@ -45,14 +45,14 @@ module GraphQL
|
|
45
45
|
|
46
46
|
# @return [Array<GraphQL::BaseType>] Visible types in the schema
|
47
47
|
def types
|
48
|
-
@types ||= @schema.types.each_value.select { |t|
|
48
|
+
@types ||= @schema.types.each_value.select { |t| visible_type?(t) }
|
49
49
|
end
|
50
50
|
|
51
51
|
# @return [GraphQL::BaseType, nil] The type named `type_name`, if it exists (else `nil`)
|
52
52
|
def get_type(type_name)
|
53
53
|
@visible_types ||= read_through do |name|
|
54
54
|
type_defn = @schema.types.fetch(name, nil)
|
55
|
-
if type_defn &&
|
55
|
+
if type_defn && visible_type?(type_defn)
|
56
56
|
type_defn
|
57
57
|
else
|
58
58
|
nil
|
@@ -81,7 +81,7 @@ module GraphQL
|
|
81
81
|
|
82
82
|
# @return [Array<GraphQL::BaseType>] The types which may be member of `type_defn`
|
83
83
|
def possible_types(type_defn)
|
84
|
-
@visible_possible_types ||= read_through { |type_defn| @schema.possible_types(type_defn).select { |t|
|
84
|
+
@visible_possible_types ||= read_through { |type_defn| @schema.possible_types(type_defn).select { |t| visible_type?(t) } }
|
85
85
|
@visible_possible_types[type_defn]
|
86
86
|
end
|
87
87
|
|
@@ -126,8 +126,44 @@ module GraphQL
|
|
126
126
|
|
127
127
|
private
|
128
128
|
|
129
|
+
def union_memberships(obj_type)
|
130
|
+
@unions ||= read_through { |obj_type| @schema.union_memberships(obj_type).select { |u| visible?(u) } }
|
131
|
+
@unions[obj_type]
|
132
|
+
end
|
133
|
+
|
129
134
|
def visible_field?(field_defn)
|
130
|
-
visible?(field_defn) &&
|
135
|
+
visible?(field_defn) && visible_type?(field_defn.type.unwrap)
|
136
|
+
end
|
137
|
+
|
138
|
+
def visible_type?(type_defn)
|
139
|
+
visible?(type_defn) && (
|
140
|
+
root_type?(type_defn) ||
|
141
|
+
type_defn.introspection? ||
|
142
|
+
referenced?(type_defn) ||
|
143
|
+
visible_abstract_type?(type_defn) ||
|
144
|
+
possible_types?(type_defn)
|
145
|
+
)
|
146
|
+
end
|
147
|
+
|
148
|
+
def root_type?(type_defn)
|
149
|
+
@schema.root_types.include?(type_defn)
|
150
|
+
end
|
151
|
+
|
152
|
+
def referenced?(type_defn)
|
153
|
+
members = @schema.references_to(type_defn.unwrap.name)
|
154
|
+
members.any? { |m| visible?(m) }
|
155
|
+
end
|
156
|
+
|
157
|
+
def visible_abstract_type?(type_defn)
|
158
|
+
type_defn.kind.object? && (
|
159
|
+
interfaces(type_defn).any? ||
|
160
|
+
union_memberships(type_defn).any?
|
161
|
+
)
|
162
|
+
end
|
163
|
+
|
164
|
+
def possible_types?(type_defn)
|
165
|
+
(type_defn.kind.union? || type_defn.kind.interface?) &&
|
166
|
+
@schema.possible_types(type_defn).any? { |t| visible_type?(t) }
|
131
167
|
end
|
132
168
|
|
133
169
|
def visible?(member)
|
@@ -22,29 +22,31 @@ module GraphQL
|
|
22
22
|
# @param query [GraphQL::Query]
|
23
23
|
# @return [Array<Hash>]
|
24
24
|
def validate(query, validate: true)
|
25
|
-
|
26
|
-
|
25
|
+
GraphQL::Tracing.trace("validate", { validate: validate, query: query }) do
|
26
|
+
context = GraphQL::StaticValidation::ValidationContext.new(query)
|
27
|
+
rewrite = GraphQL::InternalRepresentation::Rewrite.new
|
27
28
|
|
28
|
-
|
29
|
-
|
29
|
+
# Put this first so its enters and exits are always called
|
30
|
+
rewrite.validate(context)
|
30
31
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
# If the caller opted out of validation, don't attach these
|
33
|
+
if validate
|
34
|
+
@rules.each do |rules|
|
35
|
+
rules.new.validate(context)
|
36
|
+
end
|
35
37
|
end
|
36
|
-
end
|
37
38
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
context.visitor.visit
|
40
|
+
# Post-validation: allow validators to register handlers on rewritten query nodes
|
41
|
+
rewrite_result = rewrite.document
|
42
|
+
GraphQL::InternalRepresentation::Visit.visit_each_node(rewrite_result.operation_definitions, context.each_irep_node_handlers)
|
42
43
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
44
|
+
{
|
45
|
+
errors: context.errors,
|
46
|
+
# If there were errors, the irep is garbage
|
47
|
+
irep: context.errors.any? ? nil : rewrite_result,
|
48
|
+
}
|
49
|
+
end
|
48
50
|
end
|
49
51
|
end
|
50
52
|
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "graphql/subscriptions/event"
|
3
|
+
require "graphql/subscriptions/instrumentation"
|
4
|
+
if defined?(ActionCable)
|
5
|
+
require "graphql/subscriptions/action_cable_subscriptions"
|
6
|
+
end
|
7
|
+
|
8
|
+
module GraphQL
|
9
|
+
class Subscriptions
|
10
|
+
def self.use(defn, options = {})
|
11
|
+
schema = defn.target
|
12
|
+
options[:schema] = schema
|
13
|
+
schema.subscriptions = self.new(options)
|
14
|
+
instrumentation = Subscriptions::Instrumentation.new(schema: schema)
|
15
|
+
defn.instrument(:field, instrumentation)
|
16
|
+
defn.instrument(:query, instrumentation)
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(kwargs)
|
21
|
+
@schema = kwargs[:schema]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Fetch subscriptions matching this field + arguments pair
|
25
|
+
# And pass them off to the queue.
|
26
|
+
# @param event_name [String]
|
27
|
+
# @param args [Hash]
|
28
|
+
# @param object [Object]
|
29
|
+
# @param scope [Symbol, String]
|
30
|
+
# @return [void]
|
31
|
+
def trigger(event_name, args, object, scope: nil)
|
32
|
+
field = @schema.get_field("Subscription", event_name)
|
33
|
+
if !field
|
34
|
+
raise "No subscription matching trigger: #{event_name}"
|
35
|
+
end
|
36
|
+
|
37
|
+
event = Subscriptions::Event.new(
|
38
|
+
name: event_name,
|
39
|
+
arguments: args,
|
40
|
+
field: field,
|
41
|
+
scope: scope,
|
42
|
+
)
|
43
|
+
execute_all(event, object)
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(schema:, **rest)
|
47
|
+
@schema = schema
|
48
|
+
end
|
49
|
+
|
50
|
+
# `event` was triggered on `object`, and `subscription_id` was subscribed,
|
51
|
+
# so it should be updated.
|
52
|
+
#
|
53
|
+
# Load `subscription_id`'s GraphQL data, re-evaluate the query, and deliver the result.
|
54
|
+
#
|
55
|
+
# This is where a queue may be inserted to push updates in the background.
|
56
|
+
#
|
57
|
+
# @param subscription_id [String]
|
58
|
+
# @param event [GraphQL::Subscriptions::Event] The event which was triggered
|
59
|
+
# @param object [Object] The value for the subscription field
|
60
|
+
# @return [void]
|
61
|
+
def execute(subscription_id, event, object)
|
62
|
+
# Lookup the saved data for this subscription
|
63
|
+
query_data = read_subscription(subscription_id)
|
64
|
+
# Fetch the required keys from the saved data
|
65
|
+
query_string = query_data.fetch(:query_string)
|
66
|
+
variables = query_data.fetch(:variables)
|
67
|
+
context = query_data.fetch(:context)
|
68
|
+
operation_name = query_data.fetch(:operation_name)
|
69
|
+
# Re-evaluate the saved query
|
70
|
+
result = @schema.execute(
|
71
|
+
{
|
72
|
+
query: query_string,
|
73
|
+
context: context,
|
74
|
+
subscription_topic: event.topic,
|
75
|
+
operation_name: operation_name,
|
76
|
+
variables: variables,
|
77
|
+
root_value: object,
|
78
|
+
}
|
79
|
+
)
|
80
|
+
deliver(subscription_id, result)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Event `event` occurred on `object`,
|
84
|
+
# Update all subscribers.
|
85
|
+
# @param event [Subscriptions::Event]
|
86
|
+
# @param object [Object]
|
87
|
+
# @return [void]
|
88
|
+
def execute_all(event, object)
|
89
|
+
each_subscription_id(event) do |subscription_id|
|
90
|
+
execute(subscription_id, event, object)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get each `subscription_id` subscribed to `event.topic` and yield them
|
95
|
+
# @param event [GraphQL::Subscriptions::Event]
|
96
|
+
# @yieldparam subscription_id [String]
|
97
|
+
# @return [void]
|
98
|
+
def each_subscription_id(event)
|
99
|
+
raise NotImplementedError
|
100
|
+
end
|
101
|
+
|
102
|
+
# The system wants to send an update to this subscription.
|
103
|
+
# Read its data and return it.
|
104
|
+
# @param subscription_id [String]
|
105
|
+
# @return [Hash] Containing required keys
|
106
|
+
def read_subscription(subscription_id)
|
107
|
+
raise NotImplementedError
|
108
|
+
end
|
109
|
+
|
110
|
+
# A subscription query was re-evaluated, returning `result`.
|
111
|
+
# The result should be send to `subscription_id`.
|
112
|
+
# @param subscription_id [String]
|
113
|
+
# @param result [Hash]
|
114
|
+
# @param context [GraphQL::Query::Context]
|
115
|
+
# @return [void]
|
116
|
+
def deliver(subscription_id, result, context)
|
117
|
+
raise NotImplementedError
|
118
|
+
end
|
119
|
+
|
120
|
+
# `query` was executed and found subscriptions to `events`.
|
121
|
+
# Update the database to reflect this new state.
|
122
|
+
# @param query [GraphQL::Query]
|
123
|
+
# @param events [Array<GraphQL::Subscriptions::Event>]
|
124
|
+
# @return [void]
|
125
|
+
def write_subscription(query, events)
|
126
|
+
raise NotImplementedError
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|