You can extend GraphQL::Schema::Subscription
to create fields that can be subscribed to.
These classes support several behaviors:
First, add a base class for your application. You can hook up your base classes there:
# app/graphql/subscriptions/base_subscription.rb
class Subscriptions::BaseSubscription < GraphQL::Schema::Subscription
# Hook up base classes
object_class Types::BaseObject
field_class Types::BaseField
argument_class Types::BaseArgument
end
(This base class is a lot like the mutation base class.%20They’re%20both%20subclasses%20of%20%60GraphQL::Schema::Resolver%60.)
Define a class for each subscribable event in your system. For example, if you run a chat room, you might publish events whenever messages are posted in a room:
# app/graphql/subscriptions/message_was_posted.rb
class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription
end
Then, hook up the new class to the Subscription root type with the subscription:
option:
class Types::SubscriptionType < Types::BaseObject
field :message_was_posted, subscription: Subscriptions::MessageWasPosted
end
Now, it will be accessible as:
subscription {
messageWasPosted(roomId: "abcd") {
# ...
}
}
Subscription fields take arguments%20just%20like%20normal%20fields.%20They%20also%20accept%20a%20%20%5B%60loads:%60%20option%5D(/mutations/mutation_classes#auto-loading-arguments) just like mutations. For example:
class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription
# `room_id` loads a `room`
argument :room_id, ID, required: true, loads: Types::RoomType
# It's passed to other methods as `room`
def subscribe(room:)
# ...
end
def update(room:)
# ...
end
end
This can be invoked as
subscription($roomId: ID!) {
messageWasPosted(roomId: $roomId) {
# ...
}
}
If the ID doesn’t find an object, then the subscription will be unsubscribed (with #unsubscribe
, see below).
Like mutations, you can use a generated return type for subscriptions. When you add field(...)
s to a subscription, they’ll be added to the subscription’s generated return type. For example:
class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription
field :room, Types::RoomType, null: true
field :message, Types::MessageType, null: true
end
will generate:
type MessageWasPostedPayload {
room: Room!
message: Message!
}
Which you can use in queries like:
subscription($roomId: ID!) {
messageWasPosted(roomId: $roomId) {
room {
name
}
message {
author {
handle
}
body
postedAt
}
}
}
If you configure fields with null: true
, then you can return different data in the initial subscription and the subsequent updates. (See lifecycle methods below.)
Instead of a generated type, you can provide an already-configured type with payload_type
:
# Just return a message
payload_type Types::MessageType
(In that case, don’t return a hash from #subscribe
or #update
, return a message
object instead.)
Suppose a client is subscribing to messages in a chat room:
subscription($roomId: ID!) {
messageWasPosted(roomId: $roomId) {
message {
author { handle }
body
postedAt
}
}
}
You can implement #authorized?
to check that the user has permission to subscribe to these arguments (and receive updates for these arguments), for example:
def authorized?(room:)
context[:viewer].can_read_messages?(room)
end
The method may return false
or raise a GraphQL::ExecutionError
to halt execution.
This method is called before #subscribe
and #update
, described below. This way, if a user’s permissions have changed since they subscribed, they won’t receive updates unauthorized updates.
Also, if this method fails before calling #update
, then the client will be automatically unsubscribed (with #unsubscribe
).
def subscribe(**args)
is called when a client first sends a subscription { ... }
request. In this method, you can do a few things:
GraphQL::ExecutionError
to halt and return an error:no_response
to skip the initial responsesuper
to fall back to the default behavior (which is :no_response
).You can define this method to add initial responses or perform other logic before subscribing.
(Note: only supported when using the new Interpreter runtime)
By default, GraphQL-Ruby returns nothing (:no_response
) on an initial subscription. But, you may choose to override this and return a value in def subscribe
. For example:
class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription
# ...
field :room, Types::RoomType, null: true
def subscribe(room:)
# authorize, etc ...
# Return the room in the initial response
{
room: room
}
end
end
Now, a client can get some initial data with:
subscription($roomId: ID!) {
messageWasPosted(roomId: $roomId) {
room {
name
messages(last: 40) {
# ...
}
}
}
}
(Note: only supported when using the new Interpreter runtime)
After a client has registered a subscription, the application may trigger subscription updates with MySchema.subscriptions.trigger(...)
(see the Triggers guide%20for%20more). Then, def update
will be called for each client’s subscription. In this method you can:
unsubscribe
super
(which returns object
) or by returning a different value.:no_update
to skip this update(Note: only supported when using the new Interpreter runtime)
Perhaps you don’t want to send updates to a certain subscriber. For example, if someone leaves a comment, you might want to push to push the new comment to other subscribers, but not the commenter, who already has that comment data. You can accomplish this by returning :no_update
.
class Subscriptions::CommentWasAdded < Subscriptions::BaseSubscription
def update(post_id:)
comment = object # #<Comment ...>
if comment.author == context[:viewer]
:no_update
else
# Continue updating this client, since it's not the commenter
super
end
end
end
(Note: only supported when using the new Interpreter runtime)
By default, whatever object you pass to .trigger(event_name, args, object)
will be used for responding to subscription fields. But, you can return a different object from #update
to override this:
field :queue, Types::QueueType, null: false
# eg, `MySchema.subscriptions.trigger("queueWasUpdated", {name: "low-priority"}, :low_priority)`
def update(name:)
# Make a Queue object which _represents_ the queue with this name
queue = JobQueue.new(name)
# This object was passed to `.trigger`, but we're ignoring it:
object # => :low_priority
# return the queue instead:
{ queue: queue }
end
Within a subscription method, you may call unsubscribe
to terminate the client’s subscription, for example:
def update(room:)
if room.archived?
# Don't let anyone subscribe to messages on an archived room
unsubscribe
else
super
end
end
#unsubscribe
has the following effects:
#unsubscribe
does not halt the current update.
Arguments with loads:
configurations will call unsubscribe
if they are required: true
and their ID doesn’t return a value. (It’s assumed that the subscribed object was deleted.)