graphql 1.6.8 → 1.7.0

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