You can extend GraphQL::Schema::Subscription to create fields that can be subscribed to.
These classes support several behaviors:
Continue reading to set up subscription classes.
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. They’re both subclasses of GraphQL::Schema::Resolver.)
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 just like normal fields. They also accept a loads: option just like mutations. For example:
class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription # `room_id` loads a `room` argument :room_id, ID, 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: false field :message, Types::MessageType, null: false 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 remove null: false, 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.)
Usually, GraphQL-Ruby uses explicitly-passed arguments to determine when a trigger applies to an active subscription. But, you can use subscription_scope to configure implicit conditions on updates. When subscription_scope is configured, only triggers with a matching scope: value will cause clients to receive updates.
subscription_scope accepts a symbol and the given symbol will be looked up in context to find a scope value.
For example, this subscription will use context[:current_organization_id] as a scope:
class Subscriptions::EmployeeHired < Subscriptions::BaseSubscription # ... subscription_scope :current_organization_id end Clients subscribe without any arguments:
subscription { employeeHired { hireDate employee { name department } } } But .triggers are routed using scope:. So, if the subscriber’s context includes current_organization_id: 100, then the trigger must include the same scope: value:
MyAppSchema.subscriptions.trigger( # Field name :employee_hired, # Arguments {}, # Object { hire_date: Time.now, employee: new_employee }, # This corresponds to `context[:current_organization_id]` # in the original subscription: scope: 100 ) Scope is also used for determining whether subscribers can receive the same broadcast.
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:) super && 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.
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 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) { # ... } } } } After a client has registered a subscription, the application may trigger subscription updates with MySchema.subscriptions.trigger(...) (see the Triggers guide for more). Then, def update will be called for each client’s subscription. In this method you can:
unsubscribesuper (which returns object) or by returning a different value.NO_UPDATE to skip this updatePerhaps you don’t want to send updates to a certain subscriber. For example, if someone leaves a comment, you might want 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 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:
Arguments with loads: configurations will call unsubscribe if they are required: true (which is the default) and their ID doesn’t return a value. (It’s assumed that the subscribed object was deleted.)
You can provide a final update value with unsubscribe by passing a value to the method:
def update(room:) if room.archived? # Don't let anyone subscribe to messages on an archived room unsubscribe({message: "This room has been archived"}) else super end end Subscription methods can access query-related metadata by configuring extras [...] in the class definition. For example, to use a lookahead and the ast_node:
class Subscriptions::JobFinished < GraphQL::Schema::Subscription # ... extras [:lookahead, :ast_node] def subscribe(lookahead:, ast_node:) # ... end def update(lookahead:, ast_node:) # ... end end See the Extra Field Metadata for more information about available metadata.