graphql 1.10.14 → 1.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/core.rb +8 -0
  3. data/lib/generators/graphql/templates/base_argument.erb +2 -0
  4. data/lib/generators/graphql/templates/base_enum.erb +2 -0
  5. data/lib/generators/graphql/templates/base_field.erb +2 -0
  6. data/lib/generators/graphql/templates/base_input_object.erb +2 -0
  7. data/lib/generators/graphql/templates/base_interface.erb +2 -0
  8. data/lib/generators/graphql/templates/base_mutation.erb +2 -0
  9. data/lib/generators/graphql/templates/base_object.erb +2 -0
  10. data/lib/generators/graphql/templates/base_scalar.erb +2 -0
  11. data/lib/generators/graphql/templates/base_union.erb +2 -0
  12. data/lib/generators/graphql/templates/enum.erb +2 -0
  13. data/lib/generators/graphql/templates/graphql_controller.erb +13 -9
  14. data/lib/generators/graphql/templates/interface.erb +2 -0
  15. data/lib/generators/graphql/templates/loader.erb +2 -0
  16. data/lib/generators/graphql/templates/mutation.erb +2 -0
  17. data/lib/generators/graphql/templates/mutation_type.erb +2 -0
  18. data/lib/generators/graphql/templates/object.erb +2 -0
  19. data/lib/generators/graphql/templates/query_type.erb +2 -0
  20. data/lib/generators/graphql/templates/scalar.erb +2 -0
  21. data/lib/generators/graphql/templates/schema.erb +2 -0
  22. data/lib/generators/graphql/templates/union.erb +2 -0
  23. data/lib/graphql.rb +3 -3
  24. data/lib/graphql/execution/interpreter.rb +1 -1
  25. data/lib/graphql/execution/interpreter/runtime.rb +26 -26
  26. data/lib/graphql/execution/multiplex.rb +1 -2
  27. data/lib/graphql/introspection/schema_type.rb +3 -3
  28. data/lib/graphql/invalid_null_error.rb +18 -0
  29. data/lib/graphql/language/nodes.rb +1 -0
  30. data/lib/graphql/language/visitor.rb +2 -2
  31. data/lib/graphql/pagination/connection.rb +18 -13
  32. data/lib/graphql/pagination/connections.rb +17 -4
  33. data/lib/graphql/query.rb +1 -2
  34. data/lib/graphql/query/context.rb +20 -1
  35. data/lib/graphql/query/fingerprint.rb +2 -0
  36. data/lib/graphql/query/validation_pipeline.rb +3 -0
  37. data/lib/graphql/schema.rb +26 -16
  38. data/lib/graphql/schema/build_from_definition.rb +7 -12
  39. data/lib/graphql/schema/build_from_definition/resolve_map.rb +3 -1
  40. data/lib/graphql/schema/enum_value.rb +1 -0
  41. data/lib/graphql/schema/field.rb +63 -77
  42. data/lib/graphql/schema/field/connection_extension.rb +42 -32
  43. data/lib/graphql/schema/loader.rb +19 -1
  44. data/lib/graphql/schema/member/has_fields.rb +15 -5
  45. data/lib/graphql/schema/mutation.rb +4 -0
  46. data/lib/graphql/schema/object.rb +1 -1
  47. data/lib/graphql/schema/resolver.rb +20 -0
  48. data/lib/graphql/schema/resolver/has_payload_type.rb +2 -1
  49. data/lib/graphql/schema/subscription.rb +3 -13
  50. data/lib/graphql/schema/union.rb +29 -0
  51. data/lib/graphql/schema/warden.rb +2 -4
  52. data/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb +4 -2
  53. data/lib/graphql/subscriptions.rb +69 -24
  54. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +66 -11
  55. data/lib/graphql/subscriptions/broadcast_analyzer.rb +84 -0
  56. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +21 -0
  57. data/lib/graphql/subscriptions/event.rb +16 -1
  58. data/lib/graphql/subscriptions/serialize.rb +22 -4
  59. data/lib/graphql/subscriptions/subscription_root.rb +3 -1
  60. data/lib/graphql/tracing.rb +1 -27
  61. data/lib/graphql/tracing/appoptics_tracing.rb +10 -2
  62. data/lib/graphql/tracing/platform_tracing.rb +25 -15
  63. data/lib/graphql/tracing/statsd_tracing.rb +42 -0
  64. data/lib/graphql/types/iso_8601_date_time.rb +2 -1
  65. data/lib/graphql/types/relay/base_connection.rb +6 -5
  66. data/lib/graphql/version.rb +1 -1
  67. metadata +5 -2
@@ -22,39 +22,49 @@ module GraphQL
22
22
  end
23
23
 
24
24
  def after_resolve(value:, object:, arguments:, context:, memo:)
25
- if value.is_a? GraphQL::ExecutionError
26
- # This isn't even going to work because context doesn't have ast_node anymore
27
- context.add_error(value)
28
- nil
29
- elsif value.nil?
30
- nil
31
- elsif value.is_a?(GraphQL::Pagination::Connection)
32
- # update the connection with some things that may not have been provided
33
- value.context ||= context
34
- value.first_value ||= arguments[:first]
35
- value.after_value ||= arguments[:after]
36
- value.last_value ||= arguments[:last]
37
- value.before_value ||= arguments[:before]
38
- if field.has_max_page_size? && !value.has_max_page_size_override?
39
- value.max_page_size = field.max_page_size
25
+ # rename some inputs to avoid conflicts inside the block
26
+ maybe_lazy = value
27
+ value = nil
28
+ context.schema.after_lazy(maybe_lazy) do |resolved_value|
29
+ value = resolved_value
30
+ if value.is_a? GraphQL::ExecutionError
31
+ # This isn't even going to work because context doesn't have ast_node anymore
32
+ context.add_error(value)
33
+ nil
34
+ elsif value.nil?
35
+ nil
36
+ elsif value.is_a?(GraphQL::Pagination::Connection)
37
+ # update the connection with some things that may not have been provided
38
+ value.context ||= context
39
+ value.parent ||= object.object
40
+ value.first_value ||= arguments[:first]
41
+ value.after_value ||= arguments[:after]
42
+ value.last_value ||= arguments[:last]
43
+ value.before_value ||= arguments[:before]
44
+ if field.has_max_page_size? && !value.has_max_page_size_override?
45
+ value.max_page_size = field.max_page_size
46
+ end
47
+ if context.schema.new_connections? && (custom_t = context.schema.connections.edge_class_for_field(@field))
48
+ value.edge_class = custom_t
49
+ end
50
+ value
51
+ elsif context.schema.new_connections?
52
+ wrappers = context.namespace(:connections)[:all_wrappers] ||= context.schema.connections.all_wrappers
53
+ context.schema.connections.wrap(field, object.object, value, arguments, context, wrappers: wrappers)
54
+ else
55
+ if object.is_a?(GraphQL::Schema::Object)
56
+ object = object.object
57
+ end
58
+ connection_class = GraphQL::Relay::BaseConnection.connection_for_nodes(value)
59
+ connection_class.new(
60
+ value,
61
+ arguments,
62
+ field: field,
63
+ max_page_size: field.max_page_size,
64
+ parent: object,
65
+ context: context,
66
+ )
40
67
  end
41
- value
42
- elsif context.schema.new_connections?
43
- wrappers = context.namespace(:connections)[:all_wrappers] ||= context.schema.connections.all_wrappers
44
- context.schema.connections.wrap(field, value, arguments, context, wrappers: wrappers)
45
- else
46
- if object.is_a?(GraphQL::Schema::Object)
47
- object = object.object
48
- end
49
- connection_class = GraphQL::Relay::BaseConnection.connection_for_nodes(value)
50
- connection_class.new(
51
- value,
52
- arguments,
53
- field: field,
54
- max_page_size: field.max_page_size,
55
- parent: object,
56
- context: context,
57
- )
58
68
  end
59
69
  end
60
70
  end
@@ -25,8 +25,15 @@ module GraphQL
25
25
  types[type["name"]] = type_object
26
26
  end
27
27
 
28
+ directives = []
29
+ schema.fetch("directives", []).each do |directive|
30
+ next if GraphQL::Schema.default_directives.include?(directive.fetch("name"))
31
+ directives << define_directive(directive, type_resolver)
32
+ end
33
+
28
34
  Class.new(GraphQL::Schema) do
29
35
  orphan_types(types.values)
36
+ directives(directives)
30
37
 
31
38
  def self.resolve_type(*)
32
39
  raise(GraphQL::RequiredImplementationMissingError, "This schema was loaded from string, so it can't resolve types for objects")
@@ -98,7 +105,7 @@ module GraphQL
98
105
  value(
99
106
  enum_value["name"],
100
107
  description: enum_value["description"],
101
- deprecation_reason: enum_value["deprecation_reason"],
108
+ deprecation_reason: enum_value["deprecationReason"],
102
109
  )
103
110
  end
104
111
  end
@@ -147,6 +154,16 @@ module GraphQL
147
154
  end
148
155
  end
149
156
 
157
+ def define_directive(directive, type_resolver)
158
+ loader = self
159
+ Class.new(GraphQL::Schema::Directive) do
160
+ graphql_name(directive["name"])
161
+ description(directive["description"])
162
+ locations(*directive["locations"].map(&:to_sym))
163
+ loader.build_arguments(self, directive["args"], type_resolver)
164
+ end
165
+ end
166
+
150
167
  public
151
168
 
152
169
  def build_fields(type_defn, fields, type_resolver)
@@ -156,6 +173,7 @@ module GraphQL
156
173
  field_hash["name"],
157
174
  type: type_resolver.call(field_hash["type"]),
158
175
  description: field_hash["description"],
176
+ deprecation_reason: field_hash["deprecationReason"],
159
177
  null: true,
160
178
  camelize: false,
161
179
  ) do
@@ -47,20 +47,22 @@ module GraphQL
47
47
  # A list of GraphQL-Ruby keywords.
48
48
  #
49
49
  # @api private
50
- GRAPHQL_RUBY_KEYWORDS = [:context, :object, :method, :raw_value]
50
+ GRAPHQL_RUBY_KEYWORDS = [:context, :object, :raw_value]
51
51
 
52
52
  # A list of field names that we should advise users to pick a different
53
53
  # resolve method name.
54
54
  #
55
55
  # @api private
56
- CONFLICT_FIELD_NAMES = Set.new(GRAPHQL_RUBY_KEYWORDS + RUBY_KEYWORDS)
56
+ CONFLICT_FIELD_NAMES = Set.new(GRAPHQL_RUBY_KEYWORDS + RUBY_KEYWORDS + Object.instance_methods)
57
57
 
58
58
  # Register this field with the class, overriding a previous one if needed.
59
59
  # @param field_defn [GraphQL::Schema::Field]
60
60
  # @return [void]
61
- def add_field(field_defn)
62
- if CONFLICT_FIELD_NAMES.include?(field_defn.resolver_method) && field_defn.original_name == field_defn.resolver_method && field_defn.method_conflict_warning?
63
- warn "#{self.graphql_name}'s `field :#{field_defn.name}` conflicts with a built-in method, use `resolver_method:` to pick a different resolver method for this field (for example, `resolver_method: :resolve_#{field_defn.resolver_method}` and `def resolve_#{field_defn.resolver_method}`). Or use `method_conflict_warning: false` to suppress this warning."
61
+ def add_field(field_defn, method_conflict_warning: field_defn.method_conflict_warning?)
62
+ # Check that `field_defn.original_name` equals `resolver_method` and `method_sym` --
63
+ # that shows that no override value was given manually.
64
+ if method_conflict_warning && CONFLICT_FIELD_NAMES.include?(field_defn.resolver_method) && field_defn.original_name == field_defn.resolver_method && field_defn.original_name == field_defn.method_sym
65
+ warn(conflict_field_name_warning(field_defn))
64
66
  end
65
67
  own_fields[field_defn.name] = field_defn
66
68
  nil
@@ -92,6 +94,14 @@ module GraphQL
92
94
  def own_fields
93
95
  @own_fields ||= {}
94
96
  end
97
+
98
+ private
99
+
100
+ # @param [GraphQL::Schema::Field]
101
+ # @return [String] A warning to give when this field definition might conflict with a built-in method
102
+ def conflict_field_name_warning(field_defn)
103
+ "#{self.graphql_name}'s `field :#{field_defn.original_name}` conflicts with a built-in method, use `resolver_method:` to pick a different resolver method for this field (for example, `resolver_method: :resolve_#{field_defn.resolver_method}` and `def resolve_#{field_defn.resolver_method}`). Or use `method_conflict_warning: false` to suppress this warning."
104
+ end
95
105
  end
96
106
  end
97
107
  end
@@ -78,6 +78,10 @@ module GraphQL
78
78
 
79
79
  private
80
80
 
81
+ def conflict_field_name_warning(field_defn)
82
+ "#{self.graphql_name}'s `field :#{field_defn.name}` conflicts with a built-in method, use `hash_key:` or `method:` to pick a different resolve behavior for this field (for example, `hash_key: :#{field_defn.resolver_method}_value`, and modify the return hash). Or use `method_conflict_warning: false` to suppress this warning."
83
+ end
84
+
81
85
  # Override this to attach self as `mutation`
82
86
  def generate_payload_type
83
87
  payload_class = super
@@ -74,7 +74,7 @@ module GraphQL
74
74
  # Set up a type-specific invalid null error to use when this object's non-null fields wrongly return `nil`.
75
75
  # It should help with debugging and bug tracker integrations.
76
76
  def inherited(child_class)
77
- child_class.const_set(:InvalidNullError, Class.new(GraphQL::InvalidNullError))
77
+ child_class.const_set(:InvalidNullError, GraphQL::InvalidNullError.subclass_for(child_class))
78
78
  super
79
79
  end
80
80
 
@@ -40,6 +40,7 @@ module GraphQL
40
40
  @arguments_by_keyword[arg.keyword] = arg
41
41
  end
42
42
  @arguments_loads_as_type = self.class.arguments_loads_as_type
43
+ @prepared_arguments = nil
43
44
  end
44
45
 
45
46
  # @return [Object] The application object this field is being resolved on
@@ -51,6 +52,10 @@ module GraphQL
51
52
  # @return [GraphQL::Schema::Field]
52
53
  attr_reader :field
53
54
 
55
+ def arguments
56
+ @prepared_arguments || raise("Arguments have not been prepared yet, still waiting for #load_arguments to resolve. (Call `.arguments` later in the code.)")
57
+ end
58
+
54
59
  # This method is _actually_ called by the runtime,
55
60
  # it does some preparation and then eventually calls
56
61
  # the user-defined `#resolve` method.
@@ -74,6 +79,7 @@ module GraphQL
74
79
  # for that argument, or may return a lazy object
75
80
  load_arguments_val = load_arguments(args)
76
81
  context.schema.after_lazy(load_arguments_val) do |loaded_args|
82
+ @prepared_arguments = loaded_args
77
83
  # Then call `authorized?`, which may raise or may return a lazy object
78
84
  authorized_val = if loaded_args.any?
79
85
  authorized?(**loaded_args)
@@ -250,6 +256,19 @@ module GraphQL
250
256
  @complexity || (superclass.respond_to?(:complexity) ? superclass.complexity : 1)
251
257
  end
252
258
 
259
+ def broadcastable(new_broadcastable)
260
+ @broadcastable = new_broadcastable
261
+ end
262
+
263
+ # @return [Boolean, nil]
264
+ def broadcastable?
265
+ if defined?(@broadcastable)
266
+ @broadcastable
267
+ else
268
+ (superclass.respond_to?(:broadcastable?) ? superclass.broadcastable? : nil)
269
+ end
270
+ end
271
+
253
272
  def field_options
254
273
  {
255
274
  type: type_expr,
@@ -261,6 +280,7 @@ module GraphQL
261
280
  null: null,
262
281
  complexity: complexity,
263
282
  extensions: extensions,
283
+ broadcastable: broadcastable?,
264
284
  }
265
285
  end
266
286
 
@@ -58,7 +58,8 @@ module GraphQL
58
58
  resolver_fields.each do |name, f|
59
59
  # Reattach the already-defined field here
60
60
  # (The field's `.owner` will still point to the mutation, not the object type, I think)
61
- add_field(f)
61
+ # Don't re-warn about a method conflict. Since this type is generated, it should be fixed in the resolver instead.
62
+ add_field(f, method_conflict_warning: false)
62
63
  end
63
64
  end
64
65
  end
@@ -12,16 +12,6 @@ module GraphQL
12
12
  #
13
13
  # Also, `#unsubscribe` terminates the subscription.
14
14
  class Subscription < GraphQL::Schema::Resolver
15
- class EarlyTerminationError < StandardError
16
- end
17
-
18
- # Raised when `unsubscribe` is called; caught by `subscriptions.rb`
19
- class UnsubscribedError < EarlyTerminationError
20
- end
21
-
22
- # Raised when `no_update` is returned; caught by `subscriptions.rb`
23
- class NoUpdateError < EarlyTerminationError
24
- end
25
15
  extend GraphQL::Schema::Resolver::HasPayloadType
26
16
  extend GraphQL::Schema::Member::HasFields
27
17
 
@@ -65,7 +55,7 @@ module GraphQL
65
55
  def resolve_update(**args)
66
56
  ret_val = args.any? ? update(**args) : update
67
57
  if ret_val == :no_update
68
- raise NoUpdateError
58
+ throw :graphql_no_subscription_update
69
59
  else
70
60
  ret_val
71
61
  end
@@ -90,14 +80,14 @@ module GraphQL
90
80
 
91
81
  # Call this to halt execution and remove this subscription from the system
92
82
  def unsubscribe
93
- raise UnsubscribedError
83
+ throw :graphql_subscription_unsubscribed
94
84
  end
95
85
 
86
+ READING_SCOPE = ::Object.new
96
87
  # Call this method to provide a new subscription_scope; OR
97
88
  # call it without an argument to get the subscription_scope
98
89
  # @param new_scope [Symbol]
99
90
  # @return [Symbol]
100
- READING_SCOPE = ::Object.new
101
91
  def self.subscription_scope(new_scope = READING_SCOPE)
102
92
  if new_scope != READING_SCOPE
103
93
  @subscription_scope = new_scope
@@ -14,6 +14,7 @@ module GraphQL
14
14
  def possible_types(*types, context: GraphQL::Query::NullContext, **options)
15
15
  if types.any?
16
16
  types.each do |t|
17
+ assert_valid_union_member(t)
17
18
  type_memberships << type_membership_class.new(self, t, **options)
18
19
  end
19
20
  else
@@ -55,6 +56,34 @@ module GraphQL
55
56
  def type_memberships
56
57
  @type_memberships ||= []
57
58
  end
59
+
60
+ # Update a type membership whose `.object_type` is a string or late-bound type
61
+ # so that the type membership's `.object_type` is the given `object_type`.
62
+ # (This is used for updating the union after the schema as lazily loaded the union member.)
63
+ # @api private
64
+ def assign_type_membership_object_type(object_type)
65
+ assert_valid_union_member(object_type)
66
+ type_memberships.each { |tm|
67
+ possible_type = tm.object_type
68
+ if possible_type.is_a?(String) && (possible_type == object_type.name)
69
+ # This is a match of Ruby class names, not graphql names,
70
+ # since strings are used to refer to constants.
71
+ tm.object_type = object_type
72
+ elsif possible_type.is_a?(LateBoundType) && possible_type.graphql_name == object_type.graphql_name
73
+ tm.object_type = object_type
74
+ end
75
+ }
76
+ nil
77
+ end
78
+
79
+ private
80
+
81
+ def assert_valid_union_member(type_defn)
82
+ if type_defn.is_a?(Module) && !type_defn.is_a?(Class)
83
+ # it's an interface type, defined as a module
84
+ raise ArgumentError, "Union possible_types can only be object types (not interface types), remove #{type_defn.graphql_name} (#{type_defn.inspect})"
85
+ end
86
+ end
58
87
  end
59
88
  end
60
89
  end
@@ -40,7 +40,6 @@ module GraphQL
40
40
  # @param filter [<#call(member)>] Objects are hidden when `.call(member, ctx)` returns true
41
41
  # @param context [GraphQL::Query::Context]
42
42
  # @param schema [GraphQL::Schema]
43
- # @param deep_check [Boolean]
44
43
  def initialize(filter, context:, schema:)
45
44
  @schema = schema.interpreter? ? schema : schema.graphql_definition
46
45
  # Cache these to avoid repeated hits to the inheritance chain when one isn't present
@@ -51,7 +50,7 @@ module GraphQL
51
50
  @visibility_cache = read_through { |m| filter.call(m, context) }
52
51
  end
53
52
 
54
- # @return [Array<GraphQL::BaseType>] Visible types in the schema
53
+ # @return [Hash<String, GraphQL::BaseType>] Visible types in the schema
55
54
  def types
56
55
  @types ||= begin
57
56
  vis_types = {}
@@ -91,7 +90,6 @@ module GraphQL
91
90
 
92
91
  # @return [GraphQL::Field, nil] The field named `field_name` on `parent_type`, if it exists
93
92
  def get_field(parent_type, field_name)
94
-
95
93
  @visible_parent_fields ||= read_through do |type|
96
94
  read_through do |f_name|
97
95
  field_defn = @schema.get_field(type, f_name)
@@ -200,7 +198,7 @@ module GraphQL
200
198
  if (iface_field_defn = interface_type.get_field(field_defn.graphql_name))
201
199
  any_interface_has_field = true
202
200
 
203
- if visible?(interface_type) && visible_field?(interface_type, iface_field_defn)
201
+ if interfaces(type_defn).include?(interface_type) && visible_field?(interface_type, iface_field_defn)
204
202
  any_interface_has_visible_field = true
205
203
  end
206
204
  end
@@ -126,8 +126,9 @@ module GraphQL
126
126
  node_variables
127
127
  .select { |name, usage| usage.declared? && !usage.used? }
128
128
  .each { |var_name, usage|
129
+ declared_by_error_name = usage.declared_by.name || "anonymous #{usage.declared_by.operation_type}"
129
130
  add_error(GraphQL::StaticValidation::VariablesAreUsedAndDefinedError.new(
130
- "Variable $#{var_name} is declared by #{usage.declared_by.name} but not used",
131
+ "Variable $#{var_name} is declared by #{declared_by_error_name} but not used",
131
132
  nodes: usage.declared_by,
132
133
  path: usage.path,
133
134
  name: var_name,
@@ -139,8 +140,9 @@ module GraphQL
139
140
  node_variables
140
141
  .select { |name, usage| usage.used? && !usage.declared? }
141
142
  .each { |var_name, usage|
143
+ used_by_error_name = usage.used_by.name || "anonymous #{usage.used_by.operation_type}"
142
144
  add_error(GraphQL::StaticValidation::VariablesAreUsedAndDefinedError.new(
143
- "Variable $#{var_name} is used by #{usage.used_by.name} but not declared",
145
+ "Variable $#{var_name} is used by #{used_by_error_name} but not declared",
144
146
  nodes: usage.ast_node,
145
147
  path: usage.path,
146
148
  name: var_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,42 +90,64 @@ 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?
92
- # Jump down to the `delete_subscription` call
93
- raise GraphQL::Schema::Subscription::UnsubscribedError
103
+ delete_subscription(subscription_id)
104
+ return nil
94
105
  end
106
+
95
107
  # Fetch the required keys from the saved data
96
108
  query_string = query_data.fetch(:query_string)
97
109
  variables = query_data.fetch(:variables)
98
110
  context = query_data.fetch(:context)
99
111
  operation_name = query_data.fetch(:operation_name)
100
- # Re-evaluate the saved query
101
- result = @schema.execute(
102
- query: query_string,
103
- context: context,
104
- subscription_topic: event.topic,
105
- operation_name: operation_name,
106
- variables: variables,
107
- root_value: object,
108
- )
109
- deliver(subscription_id, result)
110
- rescue GraphQL::Schema::Subscription::NoUpdateError
111
- # This update was skipped in user code; do nothing.
112
- rescue GraphQL::Schema::Subscription::UnsubscribedError
113
- # `unsubscribe` was called, clean up on our side
114
- # TODO also send `{more: false}` to client?
115
- delete_subscription(subscription_id)
112
+ result = nil
113
+ # this will be set to `false` unless `.execute` is terminated
114
+ # with a `throw :graphql_subscription_unsubscribed`
115
+ unsubscribed = true
116
+ catch(:graphql_subscription_unsubscribed) do
117
+ catch(:graphql_no_subscription_update) do
118
+ # Re-evaluate the saved query,
119
+ # but if it terminates early with a `throw`,
120
+ # it will stay `nil`
121
+ result = @schema.execute(
122
+ query: query_string,
123
+ context: context,
124
+ subscription_topic: event.topic,
125
+ operation_name: operation_name,
126
+ variables: variables,
127
+ root_value: object,
128
+ )
129
+ end
130
+ unsubscribed = false
131
+ end
132
+
133
+ if unsubscribed
134
+ # `unsubscribe` was called, clean up on our side
135
+ # TODO also send `{more: false}` to client?
136
+ delete_subscription(subscription_id)
137
+ end
138
+
139
+ result
140
+ end
141
+
142
+ # Run the update query for this subscription and deliver it
143
+ # @see {#execute_update}
144
+ # @see {#deliver}
145
+ # @return [void]
146
+ def execute(subscription_id, event, object)
147
+ res = execute_update(subscription_id, event, object)
148
+ if !res.nil?
149
+ deliver(subscription_id, res)
150
+ end
116
151
  end
117
152
 
118
153
  # Event `event` occurred on `object`,
@@ -185,6 +220,16 @@ module GraphQL
185
220
  Schema::Member::BuildType.camelize(event_or_arg_name.to_s)
186
221
  end
187
222
 
223
+ # @return [Boolean] if true, then a query like this one would be broadcasted
224
+ def broadcastable?(query_str, **query_options)
225
+ query = GraphQL::Query.new(@schema, query_str, **query_options)
226
+ if !query.valid?
227
+ raise "Invalid query: #{query.validation_errors.map(&:to_h).inspect}"
228
+ end
229
+ GraphQL::Analysis::AST.analyze_query(query, @schema.query_analyzers)
230
+ query.context.namespace(:subscriptions)[:subscription_broadcastable]
231
+ end
232
+
188
233
  private
189
234
 
190
235
  # Recursively normalize `args` as belonging to `arg_owner`: