8. Using withFilter
4m

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 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 . We'll provide a different recipientId than the one we've been using.

mutation CreateConversation($recipientId: ID!) {
createConversation(recipientId: $recipientId) {
id
createdAt
}
}

And in the Variables panel:

{
"recipientId": "brise"
}

Finally, make sure your Headers tab includes the following:

Authorization: Bearer eves

When we run the , we should see that a new conversation was created! Great. Now let's for all the conversations that our currently authenticated user is a participant in. Keeping the same Authorization header, run the following .

query GetConversations {
conversations {
id
createdAt
messages {
text
}
}
}

When you run the , you should see that your user is a participant in at least two conversations (more if you already ran this !). But so far, just one of our conversations should contain messages.

http://localhost:4000

A screenshot of Sandbox, showing two active conversations

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 . Make sure that you're including the same Authorization header on both tabs.

In the first tab, the .

subscription SubscribeToMessagesInConversation($messageInConversationId: ID!) {
messageInConversation(id: $messageInConversationId) {
text
sentTime
}
}

And in the Variables panel:

Variables
{
"messageInConversationId": "wardy-eves-chat"
}

Fire off that —we should see that it's running now in the lower corner of the Response panel.

Now, onto the second : open up a new tab, and we'll paste in our .

mutation SendMessageToConversation($message: NewMessageInput!) {
sendMessage(message: $message) {
id
text
sentTo {
id
name
}
}
}

This time, we'll update the conversationId in our Variables panel to point to the other conversation.

Variables
{
"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?

http://localhost:4000

A screenshot of Sandbox, showing how a message sent to one conversation ended up in a subscription for another

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 : 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 is sending. In our case, that's the new message submitted with the "NEW_MESSAGE_SENT" event.

variables

The variables parameter refers to the defined for the subscription type in our schema; they're the values that get sent whenever a is executed. If we flip back to our schema file, we can see that Subscription.messageInConversation takes in an id that refers to the conversation ID we're subscribing to.

Let's see withFilter in action!

Using withFilter

Open up the Subscription.ts file and import withFilter at the top.

resolvers/Subscription.ts
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 , 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 is listening to. Let's access the payload and variables parameters here.

The filtering function
(payload, variables) => {
// return true if message conversation === subscription conversation
// otherwise, return false!
};

Let's start with variables. This contains all of the that were sent with our original . 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 , 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 needs to look exactly like the Message type it expects to return.

// Issue new message event for subscription
await 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 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 subscription
await 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 )!

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.

resolvers/Subscription.ts
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 executes pubsub.publish for the "NEW_MESSAGE_SENT" event.

datasources/models.ts
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.

true
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.

resolvers/Subscription.ts
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;
}
),
},
},
};

and rover dev still running? Let's try it out again!

Testing in Explorer

Back in Explorer, let's run through those again.

In the first tab, the .

subscription SubscribeToMessagesInConversation($messageInConversationId: ID!) {
messageInConversation(id: $messageInConversationId) {
text
sentTime
}
}

And in the Variables panel:

Variables
{
"messageInConversationId": "wardy-eves-chat"
}

running? Good. Let's send a message to the same conversation.

mutation SendMessageToConversation($message: NewMessageInput!) {
sendMessage(message: $message) {
id
text
sentTo {
id
name
}
}
}

This time, we'll update the conversationId in our Variables panel to point to the other conversation.

Variables
{
"message": {
"conversationId": "wardy-eves-chat",
"text": "I'm a valid message!"
}
}
http://localhost:4000

A screenshot of Sandbox, showing a valid subscription response

Now tweak the conversation ID in your . Rather than wardy-eves-chat, let's send a message to eves-brise-chat.

Variables
{
"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 !

http://localhost:4000

A screenshot of Sandbox, with the message we sent correctly not appearing in the subscription

Practice

What problem can the withFilter utility help us solve?

Key takeaways

  • The withFilter utility lets us limit which events pass through to our . We can define a filtering function that takes into account the payload of an event, along with variables passed to the , 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 , such as passed into the mutation .

Up next

We've limited our 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 . Up next, let's harness the power of the .


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.