Overview
Schema? Check. Locally-running router? Check. Next up—let's send some message events!
In this lesson, we will:
- Learn about the
PubSub
class and its application with subscriptions - Configure our mutation resolver to publish a particular event
Publishing and subscribing to messages
We've added fields to the schema, but right now, there's nothing to take care of resolving them. We need to finish the code for Mutation.sendMessage
, which will allow us to send a new message to a particular conversation, as well as for Subscription.listenForMessageInConversation
, which will notify us whenever a new message is sent.
So how do we tie these two resolvers together? After all, anytime the mutation operation is submitted, we want our subscription to somehow know about it!
We'll solve this problem by equipping our server with a way to both "publish" and "subscribe" to events.
The PubSub
class
To both publish and subscribe to events, we'll use a package called graphql-subscriptions. This package gives us an implementation of the publish-subscribe messaging pattern called PubSub
. We can create a new instance of this class to start emitting and listening to events, such as changes to our data.
Open up a new terminal to the messages
directory and install graphql-subscriptions
.
npm install graphql-subscriptions@2.0.0
When the installation completes, navigate to the datasources/context.ts
file in the messages
directory. This file contains the function that sets the context for our server: everything that we return here is made accessible to our resolver functions on their third positional argument, contextValue
.
Note: Need a refresher on resolver arguments? Check out our introductory course on GraphQL with TypeScript and Apollo Server.
We want a PubSub
instance to be available to all of our resolvers, so this is just the place to instantiate it. At the top of the file, import PubSub
from graphql-subscriptions
. Then, we'll create a new instance called pubsub
.
import { PubSub } from "graphql-subscriptions";const pubsub = new PubSub();
Down in our createContext
function, let's include the pubsub
property on the object returned.
export const createContext = async ({req,}: StandaloneServerContextFunctionArgument) => {const token = req.headers.authorization || "";const userId = token.split(" ")[1];return {userId,pubsub,dataSources: {db: new PrismaDbClient(),},};};
Note: Using PubSub
like this automatically restricts the publishing-subscribing system to a single server instance. In production, you'll want this event system to be shared by all server instances (using something like Redis, rather than merely keeping events in-memory). Check out this page in the official Apollo documentation for a list of PubSub
libraries approved for production.
Because we're using TypeScript, we need to make a similar update to our types/DataSourceContext.ts
file.
import { PrismaDbClient } from "../datasources/prisma/client";import { PubSub } from "graphql-subscriptions";// This interface is used with graphql-codegen to generate types for resolvers contextexport interface DataSourceContext {userId: string;pubsub: PubSub;dataSources: {db: PrismaDbClient;};}
Note that we don't need to instantiate PubSub
here: because we're providing our type definitions, it's enough merely to pass the class name.
Defining the Mutation
resolver
Now that pubsub
is available to our resolvers, we can jump to our Mutation
and add the logic that triggers a new event.
Open up resolvers/Mutation.ts
. We'll find the majority of the function is already provided. Let's go ahead and uncomment it!
This resolver accepts a message and adds it to the database, but it's not yet triggering an "event" we can pick up on in our subscription.
sendMessage: async (_, { message }, { dataSources, userId }) => {const { conversationId, text } = message;const {id,text: messageText,sentFrom,sentTo,sentTime,...messageAttributes} = await dataSources.db.sendMessageToConversation({conversationId,text,userId,});// Return all of the message that was createdreturn {id,text: messageText,sentFrom,sentTo,sentTime,...messageAttributes,};};
Note: If you see some type errors showing up, try re-running your subgraph with npm run dev
. If you're using VSCode, you can also open up the command palette with Command + P, type > Typescript: Restart TS Server
, and hit enter.
To publish an event every time we send a new message to a conversation, we'll use the pubsub
instance we put on our server's context. We can access it here by further destructuring the resolver's third positional argument.
sendMessage: async (_, { message }, { dataSources, pubsub, userId }) => {// ... mutation};
Let's trigger our event once a message has been saved to the database successfully. We'll make some space just before returning the message from our function, and await calling the pubsub.publish
method, passing in an event string of "NEW_MESSAGE_SENT"
.
await pubsub.publish("NEW_MESSAGE_SENT");// Return all of the message that was created
This uses our server's pubsub
instance to "publish" an event of type "NEW_MESSAGE_SENT"
: and anything that's listening for this same type will hear about it! (But nothing's listening for it yet.)
But now we have a TypeScript error to deal with: we're not passing a payload to our pubsub.publish
call, and it expects this as a second argument. For now, we'll update it to accept an empty object.
await pubsub.publish("NEW_MESSAGE_SENT", {});
Send some messages
At this point, we should be able to send some messages to a given conversation.
Make sure both your rover dev
process and subgraphs are running! Jump over to Sandbox where the local router is running on http://localhost:4000.
Let's run a mutation to send a message to conversation "wardy-eves-chat"
. We can access the SendMessageToConversation
operation we saved in our Operation Collections, or copy-paste the operation below:
mutation SendMessageToConversation($message: NewMessageInput!) {sendMessage(message: $message) {idtextsentTo {idname}}}
And in the Variables panel:
{"message": {"text": "Are you interested in booking another stay aboard?","conversationId": "wardy-eves-chat"}}
And we need to make sure that Headers also includes an Authorization
header. (Because it's a chat between wardy
and eves
, you can alternate which ID you reference below.)
Authorization: Bearer eves
Authorization: Bearer wardy
To see the full history of messages sent and received, we can querying for conversations we're a part of. Run the query below to retrieve a list of conversations along with their messages.
query GetConversations {conversations {messages {textsentFrom {id}}}}
And we need to make sure that Headers also includes an Authorization
header.
Authorization: Bearer eves
Everything working? Let's move on!
Practice
PubSub
are true?Key takeaways
- The
PubSub
class from thegraphql-subscriptions
library is an implementation of the Publish/Subscribe pattern, which allows us to "publish" events in one resolver and "subscribe to" them in another. - When calling the
PubSub
publish
method, we pass along a specific event name along with a payload argument. - In production environments, we should use a
PubSub
implementation backed by an external storage solution (such as Redis) so that all server instances can access the same store of events.
Up next
We've got our mutation set up to publish events. In the next lesson, we'll hook up the last piece: subscribing to those events.
Share your questions and comments about this lesson
This course is currently in
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.