graphql 1.10.12 → 1.11.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/execution/interpreter.rb +1 -1
- data/lib/graphql/execution/multiplex.rb +1 -2
- data/lib/graphql/introspection/schema_type.rb +3 -3
- data/lib/graphql/pagination/connection.rb +13 -6
- data/lib/graphql/pagination/connections.rb +5 -4
- data/lib/graphql/query.rb +1 -2
- data/lib/graphql/schema.rb +15 -0
- data/lib/graphql/schema/build_from_definition.rb +7 -12
- data/lib/graphql/schema/field.rb +53 -76
- data/lib/graphql/schema/field/connection_extension.rb +2 -1
- data/lib/graphql/schema/resolver.rb +14 -0
- data/lib/graphql/schema/subscription.rb +1 -1
- data/lib/graphql/schema/warden.rb +0 -1
- data/lib/graphql/subscriptions.rb +41 -8
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +49 -5
- data/lib/graphql/subscriptions/broadcast_analyzer.rb +84 -0
- data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +21 -0
- data/lib/graphql/subscriptions/event.rb +16 -1
- data/lib/graphql/subscriptions/subscription_root.rb +3 -1
- data/lib/graphql/tracing.rb +0 -27
- data/lib/graphql/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5fa7fb8135053075bd0575c9248d4e98521d0591fa26fbddc910125b1b8ea5d6
|
4
|
+
data.tar.gz: 982e434c521e7989f65364b7dd3e1540857a5110fc589e55b94196d802b6c6b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 10d507db573e239973114d7ef6cdff1d3532165f408dc0e93e5f63e0247ec552dbf3c93d53a85df1f8d00325126e1c101d9aae8e38879771d1aaa4f7238b98da
|
7
|
+
data.tar.gz: 127f3b84714f80ca41196d2ebc415f694164283d52e3cbacea25a6ec164deac88e70f4447a6af340a3ce8599ebf50489d490522fa509e112bf44d1d33eaf0fba
|
@@ -26,7 +26,7 @@ module GraphQL
|
|
26
26
|
schema_class.query_execution_strategy(GraphQL::Execution::Interpreter)
|
27
27
|
schema_class.mutation_execution_strategy(GraphQL::Execution::Interpreter)
|
28
28
|
schema_class.subscription_execution_strategy(GraphQL::Execution::Interpreter)
|
29
|
-
|
29
|
+
schema_class.add_subscription_extension_if_necessary
|
30
30
|
GraphQL::Schema::Object.include(HandlesRawValue)
|
31
31
|
end
|
32
32
|
|
@@ -34,8 +34,7 @@ module GraphQL
|
|
34
34
|
@schema = schema
|
35
35
|
@queries = queries
|
36
36
|
@context = context
|
37
|
-
|
38
|
-
@tracers = schema.tracers + GraphQL::Tracing.tracers + (context[:tracers] || [])
|
37
|
+
@tracers = schema.tracers + (context[:tracers] || [])
|
39
38
|
# Support `context: {backtrace: true}`
|
40
39
|
if context[:backtrace] && !@tracers.include?(GraphQL::Backtrace::Tracer)
|
41
40
|
@tracers << GraphQL::Backtrace::Tracer
|
@@ -9,9 +9,9 @@ module GraphQL
|
|
9
9
|
"query, mutation, and subscription operations."
|
10
10
|
|
11
11
|
field :types, [GraphQL::Schema::LateBoundType.new("__Type")], "A list of all types supported by this server.", null: false
|
12
|
-
field :
|
13
|
-
field :
|
14
|
-
field :
|
12
|
+
field :query_type, GraphQL::Schema::LateBoundType.new("__Type"), "The type that query operations will be rooted at.", null: false
|
13
|
+
field :mutation_type, GraphQL::Schema::LateBoundType.new("__Type"), "If this server supports mutation, the type that mutation operations will be rooted at.", null: true
|
14
|
+
field :subscription_type, GraphQL::Schema::LateBoundType.new("__Type"), "If this server support subscription, the type that subscription operations will be rooted at.", null: true
|
15
15
|
field :directives, [GraphQL::Schema::LateBoundType.new("__Directive")], "A list of all directives supported by this server.", null: false
|
16
16
|
|
17
17
|
def types
|
@@ -26,6 +26,9 @@ module GraphQL
|
|
26
26
|
# @return [GraphQL::Query::Context]
|
27
27
|
attr_accessor :context
|
28
28
|
|
29
|
+
# @return [Object] the object this collection belongs to
|
30
|
+
attr_accessor :parent
|
31
|
+
|
29
32
|
# Raw access to client-provided values. (`max_page_size` not applied to first or last.)
|
30
33
|
attr_accessor :before_value, :after_value, :first_value, :last_value
|
31
34
|
|
@@ -49,13 +52,15 @@ module GraphQL
|
|
49
52
|
|
50
53
|
# @param items [Object] some unpaginated collection item, like an `Array` or `ActiveRecord::Relation`
|
51
54
|
# @param context [Query::Context]
|
55
|
+
# @param parent [Object] The object this collection belongs to
|
52
56
|
# @param first [Integer, nil] The limit parameter from the client, if it provided one
|
53
57
|
# @param after [String, nil] A cursor for pagination, if the client provided one
|
54
58
|
# @param last [Integer, nil] Limit parameter from the client, if provided
|
55
59
|
# @param before [String, nil] A cursor for pagination, if the client provided one.
|
56
60
|
# @param max_page_size [Integer, nil] A configured value to cap the result size. Applied as `first` if neither first or last are given.
|
57
|
-
def initialize(items, context: nil, first: nil, after: nil, max_page_size: :not_given, last: nil, before: nil)
|
61
|
+
def initialize(items, parent: nil, context: nil, first: nil, after: nil, max_page_size: :not_given, last: nil, before: nil)
|
58
62
|
@items = items
|
63
|
+
@parent = parent
|
59
64
|
@context = context
|
60
65
|
@first_value = first
|
61
66
|
@after_value = after
|
@@ -185,17 +190,19 @@ module GraphQL
|
|
185
190
|
# A wrapper around paginated items. It includes a {cursor} for pagination
|
186
191
|
# and could be extended with custom relationship-level data.
|
187
192
|
class Edge
|
188
|
-
|
193
|
+
attr_reader :node
|
194
|
+
|
195
|
+
def initialize(node, connection)
|
189
196
|
@connection = connection
|
190
|
-
@
|
197
|
+
@node = node
|
191
198
|
end
|
192
199
|
|
193
|
-
def
|
194
|
-
@
|
200
|
+
def parent
|
201
|
+
@connection.parent
|
195
202
|
end
|
196
203
|
|
197
204
|
def cursor
|
198
|
-
@connection.cursor_for(@
|
205
|
+
@cursor ||= @connection.cursor_for(@node)
|
199
206
|
end
|
200
207
|
end
|
201
208
|
end
|
@@ -65,21 +65,22 @@ module GraphQL
|
|
65
65
|
|
66
66
|
# Used by the runtime to wrap values in connection wrappers.
|
67
67
|
# @api Private
|
68
|
-
def wrap(field,
|
68
|
+
def wrap(field, parent, items, arguments, context, wrappers: all_wrappers)
|
69
69
|
impl = nil
|
70
70
|
|
71
|
-
|
71
|
+
items.class.ancestors.each { |cls|
|
72
72
|
impl = wrappers[cls]
|
73
73
|
break if impl
|
74
74
|
}
|
75
75
|
|
76
76
|
if impl.nil?
|
77
|
-
raise ImplementationMissingError, "Couldn't find a connection wrapper for #{
|
77
|
+
raise ImplementationMissingError, "Couldn't find a connection wrapper for #{items.class} during #{field.path} (#{items.inspect})"
|
78
78
|
end
|
79
79
|
|
80
80
|
impl.new(
|
81
|
-
|
81
|
+
items,
|
82
82
|
context: context,
|
83
|
+
parent: parent,
|
83
84
|
max_page_size: field.max_page_size || context.schema.default_max_page_size,
|
84
85
|
first: arguments[:first],
|
85
86
|
after: arguments[:after],
|
data/lib/graphql/query.rb
CHANGED
@@ -96,8 +96,7 @@ module GraphQL
|
|
96
96
|
@fragments = nil
|
97
97
|
@operations = nil
|
98
98
|
@validate = validate
|
99
|
-
|
100
|
-
@tracers = schema.tracers + GraphQL::Tracing.tracers + (context ? context.fetch(:tracers, []) : [])
|
99
|
+
@tracers = schema.tracers + (context ? context.fetch(:tracers, []) : [])
|
101
100
|
# Support `ctx[:backtrace] = true` for wrapping backtraces
|
102
101
|
if context && context[:backtrace] && !@tracers.include?(GraphQL::Backtrace::Tracer)
|
103
102
|
@tracers << GraphQL::Backtrace::Tracer
|
data/lib/graphql/schema.rb
CHANGED
@@ -1075,6 +1075,7 @@ module GraphQL
|
|
1075
1075
|
raise GraphQL::Error, "Second definition of `subscription(...)` (#{new_subscription_object.inspect}) is invalid, already configured with #{@subscription_object.inspect}"
|
1076
1076
|
else
|
1077
1077
|
@subscription_object = new_subscription_object
|
1078
|
+
add_subscription_extension_if_necessary
|
1078
1079
|
add_type_and_traverse(new_subscription_object, root: true)
|
1079
1080
|
nil
|
1080
1081
|
end
|
@@ -1648,6 +1649,20 @@ module GraphQL
|
|
1648
1649
|
end
|
1649
1650
|
end
|
1650
1651
|
|
1652
|
+
# @api private
|
1653
|
+
def add_subscription_extension_if_necessary
|
1654
|
+
if interpreter? && !defined?(@subscription_extension_added) && subscription && self.subscriptions
|
1655
|
+
@subscription_extension_added = true
|
1656
|
+
if subscription.singleton_class.ancestors.include?(Subscriptions::SubscriptionRoot)
|
1657
|
+
warn("`extend Subscriptions::SubscriptionRoot` is no longer required; you may remove it from #{self}'s `subscription` root type (#{subscription}).")
|
1658
|
+
else
|
1659
|
+
subscription.fields.each do |name, field|
|
1660
|
+
field.extension(Subscriptions::DefaultSubscriptionResolveExtension)
|
1661
|
+
end
|
1662
|
+
end
|
1663
|
+
end
|
1664
|
+
end
|
1665
|
+
|
1651
1666
|
private
|
1652
1667
|
|
1653
1668
|
def lazy_methods
|
@@ -43,9 +43,7 @@ module GraphQL
|
|
43
43
|
when GraphQL::Language::Nodes::EnumTypeDefinition
|
44
44
|
types[definition.name] = build_enum_type(definition, type_resolver)
|
45
45
|
when GraphQL::Language::Nodes::ObjectTypeDefinition
|
46
|
-
|
47
|
-
should_extend_subscription_root = is_subscription_root && interpreter
|
48
|
-
types[definition.name] = build_object_type(definition, type_resolver, default_resolve: default_resolve, extend_subscription_root: should_extend_subscription_root)
|
46
|
+
types[definition.name] = build_object_type(definition, type_resolver, default_resolve: default_resolve)
|
49
47
|
when GraphQL::Language::Nodes::InterfaceTypeDefinition
|
50
48
|
types[definition.name] = build_interface_type(definition, type_resolver)
|
51
49
|
when GraphQL::Language::Nodes::UnionTypeDefinition
|
@@ -204,7 +202,7 @@ module GraphQL
|
|
204
202
|
end
|
205
203
|
end
|
206
204
|
|
207
|
-
def build_object_type(object_type_definition, type_resolver, default_resolve
|
205
|
+
def build_object_type(object_type_definition, type_resolver, default_resolve:)
|
208
206
|
builder = self
|
209
207
|
type_def = nil
|
210
208
|
typed_resolve_fn = ->(field, obj, args, ctx) { default_resolve.call(type_def, field, obj, args, ctx) }
|
@@ -214,10 +212,6 @@ module GraphQL
|
|
214
212
|
graphql_name(object_type_definition.name)
|
215
213
|
description(object_type_definition.description)
|
216
214
|
ast_node(object_type_definition)
|
217
|
-
if extend_subscription_root
|
218
|
-
# This has to come before `field ...` configurations since it modifies them
|
219
|
-
extend Subscriptions::SubscriptionRoot
|
220
|
-
end
|
221
215
|
|
222
216
|
object_type_definition.interfaces.each do |interface_name|
|
223
217
|
interface_defn = type_resolver.call(interface_name)
|
@@ -303,7 +297,7 @@ module GraphQL
|
|
303
297
|
|
304
298
|
field_definitions.map do |field_definition|
|
305
299
|
type_name = resolve_type_name(field_definition.type)
|
306
|
-
|
300
|
+
resolve_method_name = "resolve_field_#{field_definition.name}"
|
307
301
|
owner.field(
|
308
302
|
field_definition.name,
|
309
303
|
description: field_definition.description,
|
@@ -315,14 +309,15 @@ module GraphQL
|
|
315
309
|
ast_node: field_definition,
|
316
310
|
method_conflict_warning: false,
|
317
311
|
camelize: false,
|
312
|
+
resolver_method: resolve_method_name,
|
318
313
|
) do
|
319
314
|
builder.build_arguments(self, field_definition.arguments, type_resolver)
|
320
315
|
|
321
316
|
# Don't do this for interfaces
|
322
317
|
if default_resolve
|
323
|
-
|
324
|
-
|
325
|
-
default_resolve.call(
|
318
|
+
owner.send(:define_method, resolve_method_name) do |**args|
|
319
|
+
field_instance = self.class.get_field(field_definition.name)
|
320
|
+
default_resolve.call(field_instance, object, args, context)
|
326
321
|
end
|
327
322
|
end
|
328
323
|
end
|
data/lib/graphql/schema/field.rb
CHANGED
@@ -191,9 +191,10 @@ module GraphQL
|
|
191
191
|
# @param subscription_scope [Symbol, String] A key in `context` which will be used to scope subscription payloads
|
192
192
|
# @param extensions [Array<Class, Hash<Class => Object>>] Named extensions to apply to this field (see also {#extension})
|
193
193
|
# @param trace [Boolean] If true, a {GraphQL::Tracing} tracer will measure this scalar field
|
194
|
+
# @param broadcastable [Boolean] Whether or not this field can be distributed in subscription broadcasts
|
194
195
|
# @param ast_node [Language::Nodes::FieldDefinition, nil] If this schema was parsed from definition, this AST node defined the field
|
195
196
|
# @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
|
196
|
-
def initialize(type: nil, name: nil, owner: nil, null: nil, field: nil, function: nil, description: nil, deprecation_reason: nil, method: nil, hash_key: nil, resolver_method: nil, resolve: nil, connection: nil, max_page_size: :not_given, scope: nil, introspection: false, camelize: true, trace: nil, complexity: 1, ast_node: nil, extras: [], extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, arguments: EMPTY_HASH, &definition_block)
|
197
|
+
def initialize(type: nil, name: nil, owner: nil, null: nil, field: nil, function: nil, description: nil, deprecation_reason: nil, method: nil, hash_key: nil, resolver_method: nil, resolve: nil, connection: nil, max_page_size: :not_given, scope: nil, introspection: false, camelize: true, trace: nil, complexity: 1, ast_node: nil, extras: [], extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: nil, arguments: EMPTY_HASH, &definition_block)
|
197
198
|
if name.nil?
|
198
199
|
raise ArgumentError, "missing first `name` argument or keyword `name:`"
|
199
200
|
end
|
@@ -237,8 +238,8 @@ module GraphQL
|
|
237
238
|
end
|
238
239
|
|
239
240
|
# TODO: I think non-string/symbol hash keys are wrongly normalized (eg `1` will not work)
|
240
|
-
method_name = method || hash_key ||
|
241
|
-
resolver_method ||=
|
241
|
+
method_name = method || hash_key || name_s
|
242
|
+
resolver_method ||= name_s.to_sym
|
242
243
|
|
243
244
|
@method_str = method_name.to_s
|
244
245
|
@method_sym = method_name.to_sym
|
@@ -251,6 +252,7 @@ module GraphQL
|
|
251
252
|
@max_page_size = max_page_size == :not_given ? nil : max_page_size
|
252
253
|
@introspection = introspection
|
253
254
|
@extras = extras
|
255
|
+
@broadcastable = broadcastable
|
254
256
|
@resolver_class = resolver_class
|
255
257
|
@scope = scope
|
256
258
|
@trace = trace
|
@@ -295,6 +297,13 @@ module GraphQL
|
|
295
297
|
end
|
296
298
|
end
|
297
299
|
|
300
|
+
# If true, subscription updates with this field can be shared between viewers
|
301
|
+
# @return [Boolean, nil]
|
302
|
+
# @see GraphQL::Subscriptions::BroadcastAnalyzer
|
303
|
+
def broadcastable?
|
304
|
+
@broadcastable
|
305
|
+
end
|
306
|
+
|
298
307
|
# @param text [String]
|
299
308
|
# @return [String]
|
300
309
|
def description(text = nil)
|
@@ -534,7 +543,7 @@ module GraphQL
|
|
534
543
|
@resolve_proc.call(extended_obj, args, ctx)
|
535
544
|
end
|
536
545
|
else
|
537
|
-
public_send_field(after_obj, ruby_args,
|
546
|
+
public_send_field(after_obj, ruby_args, query_ctx)
|
538
547
|
end
|
539
548
|
else
|
540
549
|
err = GraphQL::UnauthorizedFieldError.new(object: inner_obj, type: obj.class, context: ctx, field: self)
|
@@ -558,30 +567,7 @@ module GraphQL
|
|
558
567
|
application_object = object.object
|
559
568
|
ctx.schema.after_lazy(self.authorized?(application_object, args, ctx)) do |is_authorized|
|
560
569
|
if is_authorized
|
561
|
-
|
562
|
-
with_extensions(object, args, ctx) do |extended_obj, extended_args|
|
563
|
-
field_receiver = if @resolver_class
|
564
|
-
resolver_obj = if extended_obj.is_a?(GraphQL::Schema::Object)
|
565
|
-
extended_obj.object
|
566
|
-
else
|
567
|
-
extended_obj
|
568
|
-
end
|
569
|
-
@resolver_class.new(object: resolver_obj, context: ctx, field: self)
|
570
|
-
else
|
571
|
-
extended_obj
|
572
|
-
end
|
573
|
-
|
574
|
-
if field_receiver.respond_to?(@resolver_method)
|
575
|
-
# Call the method with kwargs, if there are any
|
576
|
-
if extended_args.any?
|
577
|
-
field_receiver.public_send(@resolver_method, **extended_args)
|
578
|
-
else
|
579
|
-
field_receiver.public_send(@resolver_method)
|
580
|
-
end
|
581
|
-
else
|
582
|
-
resolve_field_method(field_receiver, extended_args, ctx)
|
583
|
-
end
|
584
|
-
end
|
570
|
+
public_send_field(object, args, ctx)
|
585
571
|
else
|
586
572
|
err = GraphQL::UnauthorizedFieldError.new(object: application_object, type: object.class, context: ctx, field: self)
|
587
573
|
ctx.schema.unauthorized_field(err)
|
@@ -597,43 +583,6 @@ module GraphQL
|
|
597
583
|
err
|
598
584
|
end
|
599
585
|
|
600
|
-
# Find a way to resolve this field, checking:
|
601
|
-
#
|
602
|
-
# - Hash keys, if the wrapped object is a hash;
|
603
|
-
# - A method on the wrapped object;
|
604
|
-
# - Or, raise not implemented.
|
605
|
-
#
|
606
|
-
# This can be overridden by defining a method on the object type.
|
607
|
-
# @param obj [GraphQL::Schema::Object]
|
608
|
-
# @param ruby_kwargs [Hash<Symbol => Object>]
|
609
|
-
# @param ctx [GraphQL::Query::Context]
|
610
|
-
def resolve_field_method(obj, ruby_kwargs, ctx)
|
611
|
-
if obj.object.is_a?(Hash)
|
612
|
-
inner_object = obj.object
|
613
|
-
if inner_object.key?(@method_sym)
|
614
|
-
inner_object[@method_sym]
|
615
|
-
else
|
616
|
-
inner_object[@method_str]
|
617
|
-
end
|
618
|
-
elsif obj.object.respond_to?(@method_sym)
|
619
|
-
if ruby_kwargs.any?
|
620
|
-
obj.object.public_send(@method_sym, **ruby_kwargs)
|
621
|
-
else
|
622
|
-
obj.object.public_send(@method_sym)
|
623
|
-
end
|
624
|
-
else
|
625
|
-
raise <<-ERR
|
626
|
-
Failed to implement #{@owner.graphql_name}.#{@name}, tried:
|
627
|
-
|
628
|
-
- `#{obj.class}##{@resolver_method}`, which did not exist
|
629
|
-
- `#{obj.object.class}##{@method_sym}`, which did not exist
|
630
|
-
- Looking up hash key `#{@method_sym.inspect}` or `#{@method_str.inspect}` on `#{obj.object}`, but it wasn't a Hash
|
631
|
-
|
632
|
-
To implement this field, define one of the methods above (and check for typos)
|
633
|
-
ERR
|
634
|
-
end
|
635
|
-
end
|
636
|
-
|
637
586
|
# @param ctx [GraphQL::Query::Context::FieldResolutionContext]
|
638
587
|
def fetch_extra(extra_name, ctx)
|
639
588
|
if extra_name != :path && extra_name != :ast_node && respond_to?(extra_name)
|
@@ -704,24 +653,52 @@ module GraphQL
|
|
704
653
|
end
|
705
654
|
end
|
706
655
|
|
707
|
-
def public_send_field(
|
708
|
-
query_ctx
|
709
|
-
with_extensions(obj, ruby_kwargs, query_ctx) do |extended_obj, extended_args|
|
656
|
+
def public_send_field(unextended_obj, unextended_ruby_kwargs, query_ctx)
|
657
|
+
with_extensions(unextended_obj, unextended_ruby_kwargs, query_ctx) do |obj, ruby_kwargs|
|
710
658
|
if @resolver_class
|
711
|
-
if
|
712
|
-
|
659
|
+
if obj.is_a?(GraphQL::Schema::Object)
|
660
|
+
obj = obj.object
|
713
661
|
end
|
714
|
-
|
662
|
+
obj = @resolver_class.new(object: obj, context: query_ctx, field: self)
|
715
663
|
end
|
716
664
|
|
717
|
-
|
718
|
-
|
719
|
-
|
665
|
+
# Find a way to resolve this field, checking:
|
666
|
+
#
|
667
|
+
# - A method on the type instance;
|
668
|
+
# - Hash keys, if the wrapped object is a hash;
|
669
|
+
# - A method on the wrapped object;
|
670
|
+
# - Or, raise not implemented.
|
671
|
+
#
|
672
|
+
if obj.respond_to?(@resolver_method)
|
673
|
+
# Call the method with kwargs, if there are any
|
674
|
+
if ruby_kwargs.any?
|
675
|
+
obj.public_send(@resolver_method, **ruby_kwargs)
|
720
676
|
else
|
721
|
-
|
677
|
+
obj.public_send(@resolver_method)
|
678
|
+
end
|
679
|
+
elsif obj.object.is_a?(Hash)
|
680
|
+
inner_object = obj.object
|
681
|
+
if inner_object.key?(@method_sym)
|
682
|
+
inner_object[@method_sym]
|
683
|
+
else
|
684
|
+
inner_object[@method_str]
|
685
|
+
end
|
686
|
+
elsif obj.object.respond_to?(@method_sym)
|
687
|
+
if ruby_kwargs.any?
|
688
|
+
obj.object.public_send(@method_sym, **ruby_kwargs)
|
689
|
+
else
|
690
|
+
obj.object.public_send(@method_sym)
|
722
691
|
end
|
723
692
|
else
|
724
|
-
|
693
|
+
raise <<-ERR
|
694
|
+
Failed to implement #{@owner.graphql_name}.#{@name}, tried:
|
695
|
+
|
696
|
+
- `#{obj.class}##{@resolver_method}`, which did not exist
|
697
|
+
- `#{obj.object.class}##{@method_sym}`, which did not exist
|
698
|
+
- Looking up hash key `#{@method_sym.inspect}` or `#{@method_str.inspect}` on `#{obj.object}`, but it wasn't a Hash
|
699
|
+
|
700
|
+
To implement this field, define one of the methods above (and check for typos)
|
701
|
+
ERR
|
725
702
|
end
|
726
703
|
end
|
727
704
|
end
|
@@ -31,6 +31,7 @@ module GraphQL
|
|
31
31
|
elsif value.is_a?(GraphQL::Pagination::Connection)
|
32
32
|
# update the connection with some things that may not have been provided
|
33
33
|
value.context ||= context
|
34
|
+
value.parent ||= object.object
|
34
35
|
value.first_value ||= arguments[:first]
|
35
36
|
value.after_value ||= arguments[:after]
|
36
37
|
value.last_value ||= arguments[:last]
|
@@ -41,7 +42,7 @@ module GraphQL
|
|
41
42
|
value
|
42
43
|
elsif context.schema.new_connections?
|
43
44
|
wrappers = context.namespace(:connections)[:all_wrappers] ||= context.schema.connections.all_wrappers
|
44
|
-
context.schema.connections.wrap(field, value, arguments, context, wrappers: wrappers)
|
45
|
+
context.schema.connections.wrap(field, object.object, value, arguments, context, wrappers: wrappers)
|
45
46
|
else
|
46
47
|
if object.is_a?(GraphQL::Schema::Object)
|
47
48
|
object = object.object
|
@@ -250,6 +250,19 @@ module GraphQL
|
|
250
250
|
@complexity || (superclass.respond_to?(:complexity) ? superclass.complexity : 1)
|
251
251
|
end
|
252
252
|
|
253
|
+
def broadcastable(new_broadcastable)
|
254
|
+
@broadcastable = new_broadcastable
|
255
|
+
end
|
256
|
+
|
257
|
+
# @return [Boolean, nil]
|
258
|
+
def broadcastable?
|
259
|
+
if defined?(@broadcastable)
|
260
|
+
@broadcastable
|
261
|
+
else
|
262
|
+
(superclass.respond_to?(:broadcastable?) ? superclass.broadcastable? : nil)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
253
266
|
def field_options
|
254
267
|
{
|
255
268
|
type: type_expr,
|
@@ -261,6 +274,7 @@ module GraphQL
|
|
261
274
|
null: null,
|
262
275
|
complexity: complexity,
|
263
276
|
extensions: extensions,
|
277
|
+
broadcastable: broadcastable?,
|
264
278
|
}
|
265
279
|
end
|
266
280
|
|
@@ -93,11 +93,11 @@ module GraphQL
|
|
93
93
|
raise UnsubscribedError
|
94
94
|
end
|
95
95
|
|
96
|
+
READING_SCOPE = ::Object.new
|
96
97
|
# Call this method to provide a new subscription_scope; OR
|
97
98
|
# call it without an argument to get the subscription_scope
|
98
99
|
# @param new_scope [Symbol]
|
99
100
|
# @return [Symbol]
|
100
|
-
READING_SCOPE = ::Object.new
|
101
101
|
def self.subscription_scope(new_scope = READING_SCOPE)
|
102
102
|
if new_scope != READING_SCOPE
|
103
103
|
@subscription_scope = new_scope
|
@@ -91,7 +91,6 @@ module GraphQL
|
|
91
91
|
|
92
92
|
# @return [GraphQL::Field, nil] The field named `field_name` on `parent_type`, if it exists
|
93
93
|
def get_field(parent_type, field_name)
|
94
|
-
|
95
94
|
@visible_parent_fields ||= read_through do |type|
|
96
95
|
read_through do |f_name|
|
97
96
|
field_defn = @schema.get_field(type, f_name)
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "securerandom"
|
3
|
+
require "graphql/subscriptions/broadcast_analyzer"
|
3
4
|
require "graphql/subscriptions/event"
|
4
5
|
require "graphql/subscriptions/instrumentation"
|
5
6
|
require "graphql/subscriptions/serialize"
|
@@ -7,6 +8,7 @@ if defined?(ActionCable)
|
|
7
8
|
require "graphql/subscriptions/action_cable_subscriptions"
|
8
9
|
end
|
9
10
|
require "graphql/subscriptions/subscription_root"
|
11
|
+
require "graphql/subscriptions/default_subscription_resolve_extension"
|
10
12
|
|
11
13
|
module GraphQL
|
12
14
|
class Subscriptions
|
@@ -29,14 +31,25 @@ module GraphQL
|
|
29
31
|
defn.instrument(:field, instrumentation)
|
30
32
|
options[:schema] = schema
|
31
33
|
schema.subscriptions = self.new(**options)
|
34
|
+
schema.add_subscription_extension_if_necessary
|
32
35
|
nil
|
33
36
|
end
|
34
37
|
|
35
38
|
# @param schema [Class] the GraphQL schema this manager belongs to
|
36
|
-
def initialize(schema:, **rest)
|
39
|
+
def initialize(schema:, broadcast: false, default_broadcastable: false, **rest)
|
40
|
+
if broadcast
|
41
|
+
if !schema.using_ast_analysis?
|
42
|
+
raise ArgumentError, "`broadcast: true` requires AST analysis, add `using GraphQL::Analysis::AST` to your schema or see https://graphql-ruby.org/queries/ast_analysis.html."
|
43
|
+
end
|
44
|
+
schema.query_analyzer(Subscriptions::BroadcastAnalyzer)
|
45
|
+
end
|
46
|
+
@default_broadcastable = default_broadcastable
|
37
47
|
@schema = schema
|
38
48
|
end
|
39
49
|
|
50
|
+
# @return [Boolean] Used when fields don't have `broadcastable:` explicitly set
|
51
|
+
attr_reader :default_broadcastable
|
52
|
+
|
40
53
|
# Fetch subscriptions matching this field + arguments pair
|
41
54
|
# And pass them off to the queue.
|
42
55
|
# @param event_name [String]
|
@@ -77,15 +90,13 @@ module GraphQL
|
|
77
90
|
# `event` was triggered on `object`, and `subscription_id` was subscribed,
|
78
91
|
# so it should be updated.
|
79
92
|
#
|
80
|
-
# Load `subscription_id`'s GraphQL data, re-evaluate the query
|
81
|
-
#
|
82
|
-
# This is where a queue may be inserted to push updates in the background.
|
93
|
+
# Load `subscription_id`'s GraphQL data, re-evaluate the query and return the result.
|
83
94
|
#
|
84
95
|
# @param subscription_id [String]
|
85
96
|
# @param event [GraphQL::Subscriptions::Event] The event which was triggered
|
86
97
|
# @param object [Object] The value for the subscription field
|
87
|
-
# @return [
|
88
|
-
def
|
98
|
+
# @return [GraphQL::Query::Result]
|
99
|
+
def execute_update(subscription_id, event, object)
|
89
100
|
# Lookup the saved data for this subscription
|
90
101
|
query_data = read_subscription(subscription_id)
|
91
102
|
if query_data.nil?
|
@@ -98,7 +109,7 @@ module GraphQL
|
|
98
109
|
context = query_data.fetch(:context)
|
99
110
|
operation_name = query_data.fetch(:operation_name)
|
100
111
|
# Re-evaluate the saved query
|
101
|
-
|
112
|
+
@schema.execute(
|
102
113
|
query: query_string,
|
103
114
|
context: context,
|
104
115
|
subscription_topic: event.topic,
|
@@ -106,13 +117,25 @@ module GraphQL
|
|
106
117
|
variables: variables,
|
107
118
|
root_value: object,
|
108
119
|
)
|
109
|
-
deliver(subscription_id, result)
|
110
120
|
rescue GraphQL::Schema::Subscription::NoUpdateError
|
111
121
|
# This update was skipped in user code; do nothing.
|
122
|
+
nil
|
112
123
|
rescue GraphQL::Schema::Subscription::UnsubscribedError
|
113
124
|
# `unsubscribe` was called, clean up on our side
|
114
125
|
# TODO also send `{more: false}` to client?
|
115
126
|
delete_subscription(subscription_id)
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
|
130
|
+
# Run the update query for this subscription and deliver it
|
131
|
+
# @see {#execute_update}
|
132
|
+
# @see {#deliver}
|
133
|
+
# @return [void]
|
134
|
+
def execute(subscription_id, event, object)
|
135
|
+
res = execute_update(subscription_id, event, object)
|
136
|
+
if !res.nil?
|
137
|
+
deliver(subscription_id, res)
|
138
|
+
end
|
116
139
|
end
|
117
140
|
|
118
141
|
# Event `event` occurred on `object`,
|
@@ -185,6 +208,16 @@ module GraphQL
|
|
185
208
|
Schema::Member::BuildType.camelize(event_or_arg_name.to_s)
|
186
209
|
end
|
187
210
|
|
211
|
+
# @return [Boolean] if true, then a query like this one would be broadcasted
|
212
|
+
def broadcastable?(query_str, **query_options)
|
213
|
+
query = GraphQL::Query.new(@schema, query_str, **query_options)
|
214
|
+
if !query.valid?
|
215
|
+
raise "Invalid query: #{query.validation_errors.map(&:to_h).inspect}"
|
216
|
+
end
|
217
|
+
GraphQL::Analysis::AST.analyze_query(query, @schema.query_analyzers)
|
218
|
+
query.context.namespace(:subscriptions)[:subscription_broadcastable]
|
219
|
+
end
|
220
|
+
|
188
221
|
private
|
189
222
|
|
190
223
|
# Recursively normalize `args` as belonging to `arg_owner`:
|
@@ -8,7 +8,7 @@ module GraphQL
|
|
8
8
|
#
|
9
9
|
# - No queueing system; ActiveJob should be added
|
10
10
|
# - Take care to reload context when re-delivering the subscription. (see {Query#subscription_update?})
|
11
|
-
# - Avoid the async ActionCable adapter and use the redis or PostgreSQL adapters instead. Otherwise calling #trigger won't work from background jobs or the Rails console.
|
11
|
+
# - Avoid the async ActionCable adapter and use the redis or PostgreSQL adapters instead. Otherwise calling #trigger won't work from background jobs or the Rails console.
|
12
12
|
#
|
13
13
|
# @example Adding ActionCableSubscriptions to your schema
|
14
14
|
# class MySchema < GraphQL::Schema
|
@@ -90,6 +90,7 @@ module GraphQL
|
|
90
90
|
# A per-process map of subscriptions to deliver.
|
91
91
|
# This is provided by Rails, so let's use it
|
92
92
|
@subscriptions = Concurrent::Map.new
|
93
|
+
@events = Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new { |h2, k2| h2[k2] = Concurrent::Array.new } }
|
93
94
|
@serializer = serializer
|
94
95
|
super
|
95
96
|
end
|
@@ -120,10 +121,44 @@ module GraphQL
|
|
120
121
|
channel.stream_from(stream)
|
121
122
|
@subscriptions[subscription_id] = query
|
122
123
|
events.each do |event|
|
123
|
-
|
124
|
-
|
125
|
-
|
124
|
+
# Setup a new listener to run all events with this topic in this process
|
125
|
+
setup_stream(channel, event)
|
126
|
+
# Add this event to the list of events to be updated
|
127
|
+
@events[event.topic][event.fingerprint] << event
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Every subscribing channel is listening here, but only one of them takes any action.
|
132
|
+
# This is so we can reuse payloads when possible, and make one payload to send to
|
133
|
+
# all subscribers.
|
134
|
+
#
|
135
|
+
# But the problem is, any channel could close at any time, so each channel has to
|
136
|
+
# be ready to take over the primary position.
|
137
|
+
#
|
138
|
+
# To make sure there's always one-and-only-one channel building payloads,
|
139
|
+
# let the listener belonging to the first event on the list be
|
140
|
+
# the one to build and publish payloads.
|
141
|
+
#
|
142
|
+
def setup_stream(channel, initial_event)
|
143
|
+
topic = initial_event.topic
|
144
|
+
channel.stream_from(EVENT_PREFIX + topic, coder: ActiveSupport::JSON) do |message|
|
145
|
+
object = @serializer.load(message)
|
146
|
+
events_by_fingerprint = @events[topic]
|
147
|
+
events_by_fingerprint.each do |_fingerprint, events|
|
148
|
+
if events.any? && events.first == initial_event
|
149
|
+
# The fingerprint has told us that this response should be shared by all subscribers,
|
150
|
+
# so just run it once, then deliver the result to every subscriber
|
151
|
+
first_event = events.first
|
152
|
+
first_subscription_id = first_event.context.fetch(:subscription_id)
|
153
|
+
result = execute_update(first_subscription_id, first_event, object)
|
154
|
+
# Having calculated the result _once_, send the same payload to all subscribers
|
155
|
+
events.each do |event|
|
156
|
+
subscription_id = event.context.fetch(:subscription_id)
|
157
|
+
deliver(subscription_id, result)
|
158
|
+
end
|
159
|
+
end
|
126
160
|
end
|
161
|
+
nil
|
127
162
|
end
|
128
163
|
end
|
129
164
|
|
@@ -140,7 +175,16 @@ module GraphQL
|
|
140
175
|
|
141
176
|
# The channel was closed, forget about it.
|
142
177
|
def delete_subscription(subscription_id)
|
143
|
-
@subscriptions.delete(subscription_id)
|
178
|
+
query = @subscriptions.delete(subscription_id)
|
179
|
+
events = query.context.namespace(:subscriptions)[:events]
|
180
|
+
events.each do |event|
|
181
|
+
ev_by_fingerprint = @events[event.topic]
|
182
|
+
ev_for_fingerprint = ev_by_fingerprint[event.fingerprint]
|
183
|
+
ev_for_fingerprint.delete(event)
|
184
|
+
if ev_for_fingerprint.empty?
|
185
|
+
ev_by_fingerprint.delete(event.fingerprint)
|
186
|
+
end
|
187
|
+
end
|
144
188
|
end
|
145
189
|
end
|
146
190
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
class Subscriptions
|
5
|
+
# Detect whether the current operation:
|
6
|
+
# - Is a subscription operation
|
7
|
+
# - Is completely broadcastable
|
8
|
+
#
|
9
|
+
# Assign the result to `context.namespace(:subscriptions)[:subscription_broadcastable]`
|
10
|
+
# @api private
|
11
|
+
# @see Subscriptions#broadcastable? for a public API
|
12
|
+
class BroadcastAnalyzer < GraphQL::Analysis::AST::Analyzer
|
13
|
+
def initialize(subject)
|
14
|
+
super
|
15
|
+
@default_broadcastable = subject.schema.subscriptions.default_broadcastable
|
16
|
+
# Maybe this will get set to false while analyzing
|
17
|
+
@subscription_broadcastable = true
|
18
|
+
end
|
19
|
+
|
20
|
+
# Only analyze subscription operations
|
21
|
+
def analyze?
|
22
|
+
@query.subscription?
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_enter_field(node, parent, visitor)
|
26
|
+
if (@subscription_broadcastable == false) || visitor.skipping?
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
current_field = visitor.field_definition
|
31
|
+
apply_broadcastable(current_field)
|
32
|
+
|
33
|
+
current_type = visitor.parent_type_definition
|
34
|
+
if current_type.kind.interface?
|
35
|
+
pt = @query.possible_types(current_type)
|
36
|
+
pt.each do |object_type|
|
37
|
+
ot_field = @query.get_field(object_type, current_field.graphql_name)
|
38
|
+
if !ot_field
|
39
|
+
binding.pry
|
40
|
+
end
|
41
|
+
# Inherited fields would be exactly the same object;
|
42
|
+
# only check fields that are overrides of the inherited one
|
43
|
+
if ot_field && ot_field != current_field
|
44
|
+
apply_broadcastable(ot_field)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Assign the result to context.
|
51
|
+
# (This method is allowed to return an error, but we don't need to)
|
52
|
+
# @return [void]
|
53
|
+
def result
|
54
|
+
query.context.namespace(:subscriptions)[:subscription_broadcastable] = @subscription_broadcastable
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Modify `@subscription_broadcastable` based on `field_defn`'s configuration (and/or the default value)
|
61
|
+
def apply_broadcastable(field_defn)
|
62
|
+
current_field_broadcastable = field_defn.introspection? || field_defn.broadcastable?
|
63
|
+
case current_field_broadcastable
|
64
|
+
when nil
|
65
|
+
# If the value wasn't set, mix in the default value:
|
66
|
+
# - If the default is false and the current value is true, make it false
|
67
|
+
# - If the default is true and the current value is true, it stays true
|
68
|
+
# - If the default is false and the current value is false, keep it false
|
69
|
+
# - If the default is true and the current value is false, keep it false
|
70
|
+
@subscription_broadcastable = @subscription_broadcastable && @default_broadcastable
|
71
|
+
when false
|
72
|
+
# One non-broadcastable field is enough to make the whole subscription non-broadcastable
|
73
|
+
@subscription_broadcastable = false
|
74
|
+
when true
|
75
|
+
# Leave `@broadcastable_query` true if it's already true,
|
76
|
+
# but don't _set_ it to true if it was set to false by something else.
|
77
|
+
# Actually, just leave it!
|
78
|
+
else
|
79
|
+
raise ArgumentError, "Unexpected `.broadcastable?` value for #{field_defn.path}: #{current_field_broadcastable}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
class Subscriptions
|
4
|
+
class DefaultSubscriptionResolveExtension < GraphQL::Subscriptions::SubscriptionRoot::Extension
|
5
|
+
def resolve(context:, object:, arguments:)
|
6
|
+
has_override_implementation = @field.resolver ||
|
7
|
+
object.respond_to?(@field.resolver_method)
|
8
|
+
|
9
|
+
if !has_override_implementation
|
10
|
+
if context.query.subscription_update?
|
11
|
+
object.object
|
12
|
+
else
|
13
|
+
context.skip
|
14
|
+
end
|
15
|
+
else
|
16
|
+
yield(object, arguments)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -6,7 +6,6 @@ module GraphQL
|
|
6
6
|
# - Subscribed to by `subscription { ... }`
|
7
7
|
# - Triggered by `MySchema.subscriber.trigger(name, arguments, obj)`
|
8
8
|
#
|
9
|
-
# An array of `Event`s are passed to `store.register(query, events)`.
|
10
9
|
class Event
|
11
10
|
# @return [String] Corresponds to the Subscription root field name
|
12
11
|
attr_reader :name
|
@@ -53,6 +52,22 @@ module GraphQL
|
|
53
52
|
Serialize.dump_recursive([scope, name, sorted_h])
|
54
53
|
end
|
55
54
|
|
55
|
+
# @return [String] a logical identifier for this event. (Stable when the query is broadcastable.)
|
56
|
+
def fingerprint
|
57
|
+
@fingerprint ||= begin
|
58
|
+
# When this query has been flagged as broadcastable,
|
59
|
+
# use a generalized, stable fingerprint so that
|
60
|
+
# duplicate subscriptions can be evaluated and distributed in bulk.
|
61
|
+
# (`@topic` includes field, args, and subscription scope already.)
|
62
|
+
if @context.namespace(:subscriptions)[:subscription_broadcastable]
|
63
|
+
"#{@topic}/#{@context.query.fingerprint}"
|
64
|
+
else
|
65
|
+
# not broadcastable, build a unique ID for this event
|
66
|
+
@context.schema.subscriptions.build_id
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
56
71
|
class << self
|
57
72
|
private
|
58
73
|
def stringify_args(arg_owner, args)
|
@@ -2,9 +2,11 @@
|
|
2
2
|
|
3
3
|
module GraphQL
|
4
4
|
class Subscriptions
|
5
|
-
#
|
5
|
+
# @api private
|
6
|
+
# @deprecated This module is no longer needed.
|
6
7
|
module SubscriptionRoot
|
7
8
|
def self.extended(child_cls)
|
9
|
+
warn "`extend GraphQL::Subscriptions::SubscriptionRoot` is no longer required; you can remove it from your Subscription type (#{child_cls})"
|
8
10
|
child_cls.include(InstanceMethods)
|
9
11
|
end
|
10
12
|
|
data/lib/graphql/tracing.rb
CHANGED
@@ -16,8 +16,6 @@ end
|
|
16
16
|
module GraphQL
|
17
17
|
# Library entry point for performance metric reporting.
|
18
18
|
#
|
19
|
-
# __Warning:__ Installing/uninstalling tracers is not thread-safe. Do it during application boot only.
|
20
|
-
#
|
21
19
|
# @example Sending custom events
|
22
20
|
# query.trace("my_custom_event", { ... }) do
|
23
21
|
# # do stuff ...
|
@@ -86,31 +84,6 @@ module GraphQL
|
|
86
84
|
end
|
87
85
|
end
|
88
86
|
|
89
|
-
class << self
|
90
|
-
# Install a tracer to receive events.
|
91
|
-
# @param tracer [<#trace(key, metadata)>]
|
92
|
-
# @return [void]
|
93
|
-
# @deprecated See {Schema#tracer} or use `context: { tracers: [...] }`
|
94
|
-
def install(tracer)
|
95
|
-
warn("GraphQL::Tracing.install is deprecated, add it to the schema with `tracer(my_tracer)` instead.")
|
96
|
-
if !tracers.include?(tracer)
|
97
|
-
@tracers << tracer
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
# @deprecated See {Schema#tracer} or use `context: { tracers: [...] }`
|
102
|
-
def uninstall(tracer)
|
103
|
-
@tracers.delete(tracer)
|
104
|
-
end
|
105
|
-
|
106
|
-
# @deprecated See {Schema#tracer} or use `context: { tracers: [...] }`
|
107
|
-
def tracers
|
108
|
-
@tracers ||= []
|
109
|
-
end
|
110
|
-
end
|
111
|
-
# Initialize the array
|
112
|
-
tracers
|
113
|
-
|
114
87
|
module NullTracer
|
115
88
|
module_function
|
116
89
|
def trace(k, v)
|
data/lib/graphql/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robert Mosolgo
|
@@ -681,6 +681,8 @@ files:
|
|
681
681
|
- lib/graphql/string_type.rb
|
682
682
|
- lib/graphql/subscriptions.rb
|
683
683
|
- lib/graphql/subscriptions/action_cable_subscriptions.rb
|
684
|
+
- lib/graphql/subscriptions/broadcast_analyzer.rb
|
685
|
+
- lib/graphql/subscriptions/default_subscription_resolve_extension.rb
|
684
686
|
- lib/graphql/subscriptions/event.rb
|
685
687
|
- lib/graphql/subscriptions/instrumentation.rb
|
686
688
|
- lib/graphql/subscriptions/serialize.rb
|