graphql 1.10.12 → 1.11.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|