Class: GraphQL::Subscriptions

Inherits:
Object
  • Object
show all
Defined in:
lib/graphql/subscriptions.rb,
lib/graphql/subscriptions/event.rb,
lib/graphql/subscriptions/serialize.rb,
lib/graphql/subscriptions/instrumentation.rb,
lib/graphql/subscriptions/broadcast_analyzer.rb,
lib/graphql/subscriptions/action_cable_subscriptions.rb,
lib/graphql/subscriptions/default_subscription_resolve_extension.rb

Direct Known Subclasses

ActionCableSubscriptions

Defined Under Namespace

Modules: Serialize Classes: ActionCableSubscriptions, BroadcastAnalyzer, DefaultSubscriptionResolveExtension, Event, Instrumentation, InvalidTriggerError, SubscriptionScopeMissingError

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(schema:, validate_update: true, broadcast: false, default_broadcastable: false, **rest) ⇒ Subscriptions

Returns a new instance of Subscriptions.

Parameters:

  • schema (Class)

    the GraphQL schema this manager belongs to

  • validate_update (Boolean) (defaults to: true)

    If false, then validation is skipped when executing updates



43
44
45
46
47
48
49
50
# File 'lib/graphql/subscriptions.rb', line 43

def initialize(schema:, validate_update: true, broadcast: false, default_broadcastable: false, **rest)
  if broadcast
    schema.query_analyzer(Subscriptions::BroadcastAnalyzer)
  end
  @default_broadcastable = default_broadcastable
  @schema = schema
  @validate_update = validate_update
end

Instance Attribute Details

#default_broadcastableBoolean (readonly)

Returns Used when fields don't have broadcastable: explicitly set.

Returns:

  • (Boolean)

    Used when fields don't have broadcastable: explicitly set



53
54
55
# File 'lib/graphql/subscriptions.rb', line 53

def default_broadcastable
  @default_broadcastable
end

Class Method Details

.use(defn, options = {}) ⇒ Object

See Also:

  • for options, concrete implementations may add options.


26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/graphql/subscriptions.rb', line 26

def self.use(defn, options = {})
  schema = defn.is_a?(Class) ? defn : defn.target

  if schema.subscriptions(inherited: false)
    raise ArgumentError, "Can't reinstall subscriptions. #{schema} is using #{schema.subscriptions}, can't also add #{self}"
  end

  instrumentation = Subscriptions::Instrumentation.new(schema: schema)
  defn.instrument(:query, instrumentation)
  options[:schema] = schema
  schema.subscriptions = self.new(**options)
  schema.add_subscription_extension_if_necessary
  nil
end

Instance Method Details

#broadcastable?(query_str, **query_options) ⇒ Boolean

Returns if true, then a query like this one would be broadcasted.

Returns:

  • (Boolean)

    if true, then a query like this one would be broadcasted



236
237
238
239
240
241
242
243
# File 'lib/graphql/subscriptions.rb', line 236

def broadcastable?(query_str, **query_options)
  query = GraphQL::Query.new(@schema, query_str, **query_options)
  if !query.valid?
    raise "Invalid query: #{query.validation_errors.map(&:to_h).inspect}"
  end
  GraphQL::Analysis::AST.analyze_query(query, @schema.query_analyzers)
  query.context.namespace(:subscriptions)[:subscription_broadcastable]
end

#build_idString

Returns A new unique identifier for a subscription.

Returns:

  • (String)

    A new unique identifier for a subscription



219
220
221
# File 'lib/graphql/subscriptions.rb', line 219

def build_id
  SecureRandom.uuid
end

#delete_subscription(subscription_id) ⇒ Object

A subscription was terminated server-side. Clean up the database.

Parameters:

  • subscription_id (String)

Returns:

  • void.

Raises:



214
215
216
# File 'lib/graphql/subscriptions.rb', line 214

def delete_subscription(subscription_id)
  raise GraphQL::RequiredImplementationMissingError
end

#deliver(subscription_id, result) ⇒ void

This method returns an undefined value.

A subscription query was re-evaluated, returning result. The result should be send to subscription_id.

Parameters:

  • subscription_id (String)
  • result (Hash)

Raises:



197
198
199
# File 'lib/graphql/subscriptions.rb', line 197

def deliver(subscription_id, result)
  raise GraphQL::RequiredImplementationMissingError
end

#execute(subscription_id, event, object) ⇒ void

This method returns an undefined value.

Run the update query for this subscription and deliver it

See Also:

  • GraphQL::Subscriptions.{{#execute_update}
  • GraphQL::Subscriptions.{{#deliver}


161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/graphql/subscriptions.rb', line 161

def execute(subscription_id, event, object)
  res = execute_update(subscription_id, event, object)
  if !res.nil?
    deliver(subscription_id, res)

    if res.context.namespace(:subscriptions)[:unsubscribed]
      # `unsubscribe` was called, clean up on our side
      # The transport should also send `{more: false}` to client
      delete_subscription(subscription_id)
    end
  end

end

#execute_all(event, object) ⇒ void

This method returns an undefined value.

Event event occurred on object, Update all subscribers.

Parameters:

Raises:



180
181
182
# File 'lib/graphql/subscriptions.rb', line 180

def execute_all(event, object)
  raise GraphQL::RequiredImplementationMissingError
end

#execute_update(subscription_id, event, object) ⇒ GraphQL::Query::Result

event was triggered on object, and subscription_id was subscribed, so it should be updated.

Load subscription_id's GraphQL data, re-evaluate the query and return the result.

Parameters:

  • subscription_id (String)
  • event (GraphQL::Subscriptions::Event)

    The event which was triggered

  • object (Object)

    The value for the subscription field

Returns:



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/graphql/subscriptions.rb', line 107

def execute_update(subscription_id, event, object)
  # Lookup the saved data for this subscription
  query_data = read_subscription(subscription_id)
  if query_data.nil?
    delete_subscription(subscription_id)
    return nil
  end

  # Fetch the required keys from the saved data
  query_string = query_data.fetch(:query_string)
  variables = query_data.fetch(:variables)
  context = query_data.fetch(:context)
  operation_name = query_data.fetch(:operation_name)
  execute_options = {
    query: query_string,
    context: context,
    subscription_topic: event.topic,
    operation_name: operation_name,
    variables: variables,
    root_value: object,
  }

   # merge event's and query's context together
  context.merge!(event.context) unless event.context.nil? || context.nil?

  execute_options[:validate] = validate_update?(**execute_options)
  result = @schema.execute(**execute_options)
  subscriptions_context = result.context.namespace(:subscriptions)
  if subscriptions_context[:no_update]
    result = nil
  end

  if subscriptions_context[:unsubscribed] && !subscriptions_context[:final_update]
    # `unsubscribe` was called, clean up on our side
    # The transport should also send `{more: false}` to client
    delete_subscription(subscription_id)
    result = nil
  end

  result
end

#normalize_name(event_or_arg_name) ⇒ String

Convert a user-provided event name or argument to the equivalent in GraphQL.

By default, it converts the identifier to camelcase. Override this in a subclass to change the transformation.

Parameters:

  • event_or_arg_name (String, Symbol)

Returns:

  • (String)


231
232
233
# File 'lib/graphql/subscriptions.rb', line 231

def normalize_name(event_or_arg_name)
  Schema::Member::BuildType.camelize(event_or_arg_name.to_s)
end

#read_subscription(subscription_id) ⇒ Hash

The system wants to send an update to this subscription. Read its data and return it.

Parameters:

  • subscription_id (String)

Returns:

  • (Hash)

    Containing required keys

Raises:



188
189
190
# File 'lib/graphql/subscriptions.rb', line 188

def read_subscription(subscription_id)
  raise GraphQL::RequiredImplementationMissingError
end

#trigger(event_name, args, object, scope: nil, context: {}) ⇒ void

This method returns an undefined value.

Fetch subscriptions matching this field + arguments pair And pass them off to the queue.

Parameters:

  • event_name (String)
  • args (Hash<String, Symbol => Object])

    rgs [Hash Object]

  • object (Object)
  • scope (Symbol, String) (defaults to: nil)
  • context (Hash) (defaults to: {})


63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/graphql/subscriptions.rb', line 63

def trigger(event_name, args, object, scope: nil, context: {})
  # Make something as context-like as possible, even though there isn't a current query:
  dummy_query = GraphQL::Query.new(@schema, "{ __typename }", validate: false, context: context)
  context = dummy_query.context
  event_name = event_name.to_s

  # Try with the verbatim input first:
  field = @schema.get_field(@schema.subscription, event_name, context)

  if field.nil?
    # And if it wasn't found, normalize it:
    normalized_event_name = normalize_name(event_name)
    field = @schema.get_field(@schema.subscription, normalized_event_name, context)
    if field.nil?
      raise InvalidTriggerError, "No subscription matching trigger: #{event_name} (looked for #{@schema.subscription.graphql_name}.#{normalized_event_name})"
    end
  else
    # Since we found a field, the original input was already normalized
    normalized_event_name = event_name
  end

  # Normalize symbol-keyed args to strings, try camelizing them
  # Should this accept a real context somehow?
  normalized_args = normalize_arguments(normalized_event_name, field, args, GraphQL::Query::NullContext.instance)

  event = Subscriptions::Event.new(
    name: normalized_event_name,
    arguments: normalized_args,
    field: field,
    scope: scope,
    context: context,
  )
  execute_all(event, object)
end

#validate_update?(query:, context:, root_value:, subscription_topic:, operation_name:, variables:) ⇒ Boolean

Define this method to customize whether to validate this subscription when executing an update.

Returns:

  • (Boolean)

    defaults to true, or false if validate: false is provided.



153
154
155
# File 'lib/graphql/subscriptions.rb', line 153

def validate_update?(query:, context:, root_value:, subscription_topic:, operation_name:, variables:)
  @validate_update
end

#write_subscription(query, events) ⇒ void

This method returns an undefined value.

query was executed and found subscriptions to events. Update the database to reflect this new state.



206
207
208
# File 'lib/graphql/subscriptions.rb', line 206

def write_subscription(query, events)
  raise GraphQL::RequiredImplementationMissingError
end