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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 840c0d49f547bfb3a44878e0631df87f3b287179a077806169480a8d2820b30b
4
- data.tar.gz: 498c331da5d7f3818054def18b1a964f1ff97699b79b7be1635eefbad0dcc946
3
+ metadata.gz: 5fa7fb8135053075bd0575c9248d4e98521d0591fa26fbddc910125b1b8ea5d6
4
+ data.tar.gz: 982e434c521e7989f65364b7dd3e1540857a5110fc589e55b94196d802b6c6b0
5
5
  SHA512:
6
- metadata.gz: 0cc7bdbeff9a1c70f28c50ffa93740020173a969d34e9aa8872073f721555ebe1c67fa74e13d012d6c547bc4aa6515ab7dafe173dd75bfdea9fe0c4a2b256ac8
7
- data.tar.gz: a466d22dd0e14f95b7273faedc68d3ff214e0f72411e491f454d037b1b74034a67b1eb6334cd567334cf5856c1b91e65a38024955426079c317c43eee47557c7
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
- # TODO remove support for global tracers
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 :queryType, GraphQL::Schema::LateBoundType.new("__Type"), "The type that query operations will be rooted at.", null: false
13
- field :mutationType, GraphQL::Schema::LateBoundType.new("__Type"), "If this server supports mutation, the type that mutation operations will be rooted at.", null: true
14
- field :subscriptionType, GraphQL::Schema::LateBoundType.new("__Type"), "If this server support subscription, the type that subscription operations will be rooted at.", null: true
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
- def initialize(item, connection)
193
+ attr_reader :node
194
+
195
+ def initialize(node, connection)
189
196
  @connection = connection
190
- @item = item
197
+ @node = node
191
198
  end
192
199
 
193
- def node
194
- @item
200
+ def parent
201
+ @connection.parent
195
202
  end
196
203
 
197
204
  def cursor
198
- @connection.cursor_for(@item)
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, object, arguments, context, wrappers: all_wrappers)
68
+ def wrap(field, parent, items, arguments, context, wrappers: all_wrappers)
69
69
  impl = nil
70
70
 
71
- object.class.ancestors.each { |cls|
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 #{object.class} during #{field.path} (#{object.inspect})"
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
- object,
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],
@@ -96,8 +96,7 @@ module GraphQL
96
96
  @fragments = nil
97
97
  @operations = nil
98
98
  @validate = validate
99
- # TODO: remove support for global tracers
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
@@ -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
- is_subscription_root = (definition.name == "Subscription" && (schema_definition.nil? || schema_definition.subscription.nil?)) || (schema_definition && (definition.name == schema_definition.subscription))
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:, extend_subscription_root:)
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
- # TODO fragile hack. formalize this API?
324
- define_singleton_method :resolve_field_method do |obj, args, ctx|
325
- default_resolve.call(self, obj.object, args, ctx)
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
@@ -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 || @underscored_name
241
- resolver_method ||= @underscored_name.to_sym
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, ctx)
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
- # Apply field extensions
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(obj, ruby_kwargs, field_ctx)
708
- query_ctx = field_ctx.query.context
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 extended_obj.is_a?(GraphQL::Schema::Object)
712
- extended_obj = extended_obj.object
659
+ if obj.is_a?(GraphQL::Schema::Object)
660
+ obj = obj.object
713
661
  end
714
- extended_obj = @resolver_class.new(object: extended_obj, context: query_ctx, field: self)
662
+ obj = @resolver_class.new(object: obj, context: query_ctx, field: self)
715
663
  end
716
664
 
717
- if extended_obj.respond_to?(@resolver_method)
718
- if extended_args.any?
719
- extended_obj.public_send(@resolver_method, **extended_args)
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
- extended_obj.public_send(@resolver_method)
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
- resolve_field_method(extended_obj, extended_args, query_ctx)
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, and deliver the result.
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 [void]
88
- def execute(subscription_id, event, object)
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
- result = @schema.execute(
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
- channel.stream_from(EVENT_PREFIX + event.topic, coder: ActiveSupport::JSON) do |message|
124
- execute(subscription_id, event, @serializer.load(message))
125
- nil
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
- # Extend this module in your subscription root when using {GraphQL::Execution::Interpreter}.
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
 
@@ -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)
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.10.12"
3
+ VERSION = "1.11.0"
4
4
  end
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.10.12
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