Overview
There's one problem with our current setup—and you might not have noticed it unless you tried sending messages to a different conversation.
In this lesson, we will:
- Learn about the
withFilter
utility - Limit subscription message events to the conversation they belong to
Filtering subscription events
Let's take a closer look at the problem by setting up our database with a second conversation.
In Sandbox, run the following mutation. We'll provide a different recipientId
than the one we've been using.
mutation CreateConversation($recipientId: ID!) {createConversation(recipientId: $recipientId) {idcreatedAt}}
And in the Variables panel:
{"recipientId": "brise"}
Finally, make sure your Headers tab includes the following:
Authorization: Bearer eves
When we run the mutation, we should see that a new conversation was created! Great. Now let's query for all the conversations that our currently authenticated user is a participant in. Keeping the same Authorization
header, run the following query.
query GetConversations {conversations {idcreatedAtmessages {text}}}
When you run the operation, you should see that your user is a participant in at least two conversations (more if you already ran this mutation!). But so far, just one of our conversations should contain messages.
Pro tip: To reset your database, you can delete the dev.db
file in your messages
server, and then run npx prisma migrate dev
. This will revert the database to contain just one conversation.
Okay: time to highlight the problem. We are going to subscribe to one conversation ("wardy-eves-chat"
), but send messages to the other conversation ("eves-brise-chat"
).
Let's set up our operations. Make sure that you're including the same Authorization
header on both tabs.
In the first tab, the subscription operation.
subscription SubscribeToMessagesInConversation($messageInConversationId: ID!) {messageInConversation(id: $messageInConversationId) {textsentTime}}
And in the Variables panel:
{"messageInConversationId": "wardy-eves-chat"}
Fire off that operation—we should see that it's running now in the lower corner of the Response panel.
Now, onto the second operation: open up a new tab, and we'll paste in our mutation.
mutation SendMessageToConversation($message: NewMessageInput!) {sendMessage(message: $message) {idtextsentTo {idname}}}
This time, we'll update the conversationId
in our Variables panel to point to the other conversation.
{"message": {"conversationId": "eves-brise-chat","text": "You shouldn't be seeing me in the subscription..."}}
Send off that mutation, and... uh oh! We sent a message to one conversation, but it ended up in our subscription for another! 😱 Okay, so how do we fix this?
withFilter
We get another helpful utility from the graphql-subscriptions
library called withFilter
. We can use withFilter
to make sure that each subscriber gets only the data it needs.
withFilter
takes in two arguments: a function that returns a type that satisifes the AsyncIterator
interface (just like what we have now!), and a filtering function.
subscribe: withFilter(asyncIteratorFunction, filteringFunction);
Zooming into the filtering function, we'll have access to a few parameters: payload
, variables
, context
, and info
(we won't be using the last two).
payload
The payload
is what gets passed into our subscribe
function when an event gets published; it's the object of data that our mutation is sending. In our case, that's the new message submitted with the "NEW_MESSAGE_SENT"
event.
variables
The variables
parameter refers to the arguments defined for the subscription
type in our schema; they're the values that get sent whenever a subscription operation is executed. If we flip back to our schema file, we can see that Subscription.messageInConversation
takes in an id
field that refers to the conversation ID we're subscribing to.
Let's see withFilter
in action!
Using withFilter
Open up the Subscription.ts
resolver file and import withFilter
at the top.
import { withFilter } from "graphql-subscriptions";
Next, we'll jump down to our subscribe
line. We'll wrap the current function with a call to withFilter
.
messageInConversation: {subscribe: withFilter((_, __, { pubsub }) => {return pubsub.asyncIterableIterator(["NEW_MESSAGE_SENT"]);}),}
TypeScript will help us out immediately with a large error: withFilter
expects two arguments, but we've only provided one. Let's take care of that second argument, our filtering function next. Look for the closing bracket of the subscribe function, and let's add a comma and a new line. On that new line, we'll place an empty arrow function.
subscribe: withFilter((_, __, { pubsub }) => {return pubsub.asyncIterableIterator(["NEW_MESSAGE_SENT"])},() => {}),
This is our filtering function. It's where we'll place all of our logic to determine if the conversation a message has been sent to matches the conversation our subscription is listening to. Let's access the payload
and variables
parameters here.
(payload, variables) => {// return true if message conversation === subscription conversation// otherwise, return false!};
Let's start with variables
. This contains all of the variables that were sent with our original subscription operation. In this case, we have access to the conversation ID, denoted as id
. We can destructure variables
for its id
property.
(payload, { id }) => {// return true if message conversation === id// otherwise, return false!};
Next up, the payload
. We'll need to retrieve the conversation ID for that particular message.
If we jump back out to Mutation.ts
, in our sendMessage
resolver, we'll see that our publish
call doesn't actually pass along anything about the conversation ID the message is sent to. We just see message details here, and for an important reason: the data that we convey to our messageInConversation
field needs to look exactly like the Message
type it expects to return.
// Issue new message event for subscriptionawait pubsub.publish("NEW_MESSAGE_SENT", {messageInConversation: {id,text: messageText,sentFrom,sentTo,sentTime,},});
But we do have access to the conversation ID the message is being sent to; we pull that detail out of the message
input at the top of the sendMessage
resolver as conversationId
.
sendMessage: async (_, { message }, { dataSources, pubsub, userId }) => {const { conversationId, text } = message;// other mutation logic},
We need to somehow get this conversationId
into our filtering function; but we can't put it inside the messageInConversation
object we return.
Instead, we need to put it outside of that object, as a new property. Add a comma outside of the messageInConversation
object, and stick conversationId
on the line beneath it.
// Issue new message event for subscriptionawait pubsub.publish("NEW_MESSAGE_SENT", {messageInConversation: {id,text: messageText,sentFrom,sentTo,sentTime,},conversationId,});
This successfully includes the conversationId
on the payload
object (but not as a property on the Message
that is piped into our messageInConversation
resolver)!
So back in our filtering function, in resolvers/Subscription.ts
, we can now destructure the payload
object for the conversationId
property we just added to it.
subscribe: withFilter((_, __, { pubsub }) => {return pubsub.asyncIterableIterator(["NEW_MESSAGE_SENT"]);},({ conversationId }, { id }) => {});
But pretty quickly, we'll see a type error: Property 'conversationId' does not exist on type '{}'.
We can handle this by explicitly casting the payload
object to a type that already exists in our datasources/models.ts
file: NewMessageEvent
. This type reflects all of the keys we expect to be present on the object that is sent every time our mutation resolver executes pubsub.publish
for the "NEW_MESSAGE_SENT"
event.
export type NewMessageEvent = {messageInConversation: {id: number;sentTime: Date;text: string;sentFrom: string;sentTo: string;};conversationId: string;};
At the top of Subscription.ts
, let's import our type.
import { Resolvers } from "../__generated__/resolvers-types";import { withFilter } from "graphql-subscriptions";import { NewMessageEvent } from "../datasources/models";
Down where we destructure payload
for the conversationId
, let's add our type annotation.
subscribe: withFilter((_, __, { pubsub }) => {return pubsub.asyncIterableIterator(["NEW_MESSAGE_SENT"]);},({ conversationId }: NewMessageEvent, { id }) => {});
And finally we'll add the code to check for equality between the conversationId
and the id
.
export const Subscription: Resolvers = {Subscription: {messageInConversation: {subscribe: withFilter((_, __, { pubsub }) => {return pubsub.asyncIterableIterator(["NEW_MESSAGE_SENT"]);},({ conversationId }: NewMessageEvent, { id }) => {return conversationId === id;}),},},};
Subgraphs and rover dev
still running? Let's try it out again!
Testing in Explorer
Back in Explorer, let's run through those operations again.
In the first tab, the subscription operation.
subscription SubscribeToMessagesInConversation($messageInConversationId: ID!) {messageInConversation(id: $messageInConversationId) {textsentTime}}
And in the Variables panel:
{"messageInConversationId": "wardy-eves-chat"}
Subscription running? Good. Let's send a message to the same conversation.
mutation SendMessageToConversation($message: NewMessageInput!) {sendMessage(message: $message) {idtextsentTo {idname}}}
This time, we'll update the conversationId
in our Variables panel to point to the other conversation.
{"message": {"conversationId": "wardy-eves-chat","text": "I'm a valid message!"}}
Now tweak the conversation ID in your mutation operation. Rather than wardy-eves-chat
, let's send a message to eves-brise-chat
.
{"message": {"conversationId": "eves-brise-chat","text": "Shhh, I shouldn't show up in the subscription!"}}
And we'll see that this message, directed at a different conversation, no longer arrives in our subscription!
Practice
withFilter
utility help us solve?Key takeaways
- The
withFilter
utility lets us limit which events pass through to our subscription. We can define a filtering function that takes into account thepayload
of an event, along withvariables
passed to the subscription operation, to make sure that we're only transmitting valid events. - We can pass additional properties in the object sent with each
pubsub.publish
call from the mutation resolver, such as arguments passed into the mutation operation.
Up next
We've limited our subscription to transmit only those messages that belong to the conversation we're subscribed to. That's great! Right now, we're still a bit siloed: all of our work has been in the messages
subgraph. Up next, let's harness the power of the supergraph.
Share your questions and comments about this lesson
Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.