Handle unsubscribe GraphQL subscription - graphql

I have an issue with subscription can't be unsubscribe.
Before we start, this is my setup: Apollo Client(graphql-ws) <-> Apollo Server(graphql-ws). On the server, I build a custom PubSub instead of using the one provided.
As you can see here, the client has sent a complete request to server with the id. However, the server is still sending more data to it. I have read somewhere that you have to send GQL_STOP, aka STOP instead. However, this is what Apollo Client is sending.
A bit of code:
Client subscription:
export const useGetDataThroughSubscription = (
resourceIds: number[],
startDate?: Date,
endDate?: Date
) => {
const variables = {
startTime: startDate?.toISOString() ?? '',
endTime: endDate?.toISOString() ?? '',
resourceIds,
};
return useGetDataSubscription({
variables,
...
})
}
Server pubsub:
const createPubSub = <TopicPayload extends { [key: string]: unknown }>(
emitter: EventEmitter = new EventEmitter()
) => ({
publish: <Topic extends Extract<keyof TopicPayload, string>>(
topic: Topic,
payload: TopicPayload[Topic]
) => {
emitter.emit(topic as string, payload);
},
async *subscribe<Topic extends Extract<keyof TopicPayload, string>>(
topic: Topic,
retrievalFunc: (value: TopicPayload[Topic]) => Promise<any>
): AsyncIterableIterator<TopicPayload[Topic]> {
const asyncIterator = on(emitter, topic);
for await (const [value] of asyncIterator) {
const data = await retrievalFunc(value);
yield data;
}
},
Server subscribe to event:
const resolver: Resolvers = {
Subscription: {
[onGetAllLocationsEvent]: {
async *subscribe(_a, _b, ctx) {
const locations = await ...;
yield locations;
const iterator = ctx.pubsub.subscribe(
onGetAllLocationsEvent,
async (id: number) => {
const location = ...;
return location;
}
);
for await (const data of iterator) {
if (data) {
yield [data];
}
}
},
resolve: (payload) => payload,
},
},
};
In this one, if instead of the for loop, I return iterator instead, then the server will send back a complete and stop the subscription all together. That's great, but I want to keep the connection open until client stop listening.
And server publish
ctx.pubsub.publish(onGetAllResourcesEvent, resource.id);
So how should I deal with this?

Related

Deno oak websocket must have a method that returns an async iterator

I am trying to build up a WebSocket with oak (not the native deno one).
The following code is how I build the server.
import {Application, Router, Context, send } from "https://deno.land/x/oak#v10.6.0/mod.ts";
const runWS = async (ctx: Context, next: () => Promise<unknown>) => {
try{
const ws = await ctx.upgrade();
ws.onopen = () => {
chatConnection(ws);
};
ws.onclose = () => { console.log('Disconnected from the client!');};
}catch{await next();}
}
let sockets = new Map<string, WebSocket>();
const chatConnection = async (ws: WebSocket) => {
console.log('new websocket, ws: ',ws);
const uid = globalThis.crypto.randomUUID();
sockets.set(uid,ws);
console.log('socket: ',sockets);
for await (const ev of ws){
console.log('ev: ', ev);
}
}
export const wsRoutes = new Router()
.get('/ws', runWS);
But in the for loop (at the end), for ws it says Type 'WebSocket' must have a '[Symbol.asyncIterator]()' method that returns an async iterator.. What's the deal with this and how to fix it?
The error message is providing you with useful information: the WebSocket is not AsyncIterable, which means that it cannot be used with a for await...of loop.
Here is the type documentation for WebSocket in Deno. It is (for the most part) the same as the WHATWG standard WebSocket that is documented on MDN.
If your intention is to respond to incoming message events, you'll need to attach an event listener:
webSocket.addEventListener("message", (messageEvent) => {
// Do something in response to each message event
});
Additional:
Here's an observation based on the code you've shown, but not in response to your question:
It's probably more ergonomic to store the sockets as the keys of your map, and the associated state data in the values. (This is the inverse of what you've shown). Here's an example of why:
import {
Router,
type RouterMiddleware,
} from "https://deno.land/x/oak#v10.6.0/mod.ts";
// You seem to want to log data to the console.
// This function will help you easily log only certain properties of objects:
/**
* Functional implementation of the type utility
* [`Pick<Type, Keys>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys)
*/
function pick<T, K extends keyof T>(
obj: T,
keys: readonly K[],
): Pick<T, K> {
const result = {} as Pick<T, K>;
for (const key of keys) result[key] = obj[key];
return result;
}
type SocketData = { id: string };
const socketMap = new Map<WebSocket, SocketData>();
// Do something when a connection is opened
function handleOpen(ev: Event, ws: WebSocket) {
const socketData: SocketData = { id: window.crypto.randomUUID() };
socketMap.set(ws, socketData);
console.log({
event: pick(ev, ["type"]),
socketData,
});
}
// Do something when an error occurs
function handleError(ev: Event, ws: WebSocket) {
const socketData = socketMap.get(ws);
console.log({
event: pick(ev, ["type"]),
socketData,
});
socketMap.delete(ws);
}
// Do something when a connection is closed
function handleClose(ev: CloseEvent, ws: WebSocket) {
ev.code; // number
ev.reason; // string
ev.wasClean; // boolean
const socketData = socketMap.get(ws);
console.log({
event: pick(ev, ["type", "code", "reason", "wasClean"]),
socketData,
});
socketMap.delete(ws);
}
// Do something when a message is received
// Change `unknown` to the type of message payloads used in your application.
// (for example, JSON messages are `string`)
function handleMessage(ev: MessageEvent<unknown>, ws: WebSocket) {
ev.data; // unknown
ev.lastEventId; // string
ev.ports; // readonly MessagePort[]
const socketData = socketMap.get(ws);
if (socketData) {
socketData.id; // string
}
console.log({
event: pick(ev, ["type", "data", "lastEventId", "ports"]),
socketData,
});
}
const webSocketMiddleware: RouterMiddleware<"/ws"> = async (ctx, next) => {
const ws = ctx.upgrade();
ws.addEventListener("open", (ev) => handleOpen(ev, ws));
ws.addEventListener("error", (ev) => handleError(ev, ws));
ws.addEventListener("close", (ev) => handleClose(ev, ws));
ws.addEventListener("message", (ev) => handleMessage(ev, ws));
await next();
};
export const router = new Router();
router.get("/ws", webSocketMiddleware);
This is my updated code. It avoids the problem entirely
import {Application, Router, Context, send } from "https://deno.land/x/oak#v10.6.0/mod.ts";
interface BroadcastObj{
name: string,
mssg: string
}
const runWS = async (ctx: Context, next: () => Promise<unknown>) => {
if(!ctx.isUpgradable){
ctx.throw(501);
}
const uid = globalThis.crypto.randomUUID();
try{
const ws = await ctx.upgrade();
ws.onopen = () => {
chatConnection(ws);
};
ws.onmessage = (m) => {
let mssg = m.data as string;
if(typeof(mssg) === 'string'){
chatMessage(JSON.parse(mssg));
}
};
ws.onerror = (e) => {console.log('error occured: ', e);};
ws.onclose = () => { chatDisconnect(uid);};
}catch{await next();}
}
let sockets = new Map<string, WebSocket>();
const chatConnection = async (ws: WebSocket, uid: string) => {
await sockets.set(uid,ws);
}
const chatMessage = async (msg: BroadcastObj) => {
await sockets.forEach((ws: WebSocket) => {
ws.send(JSON.stringify(msg));
});
}
const chatDisconnect = async (uid: string) => {
await sockets.delete(uid);
}
export const wsRoutes = new Router()
.get('/ws', runWS);

Could anyone provide a fastapi websocket endpoint which could connect with the example given for RTK Query streaming updates

I'm trying to get my head around RTK Query as it applies to websockets. The example given is
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query/react'
import { createEntityAdapter, EntityState } from '#reduxjs/toolkit'
import { isMessage } from './schemaValidators'
export type Channel = 'redux' | 'general'
export interface Message {
id: number
channel: Channel
userName: string
text: string
}
const messagesAdapter = createEntityAdapter<Message>()
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getMessages: build.query<EntityState<Message>, Channel>({
query: (channel) => `messages/${channel}`,
transformResponse(response: Message[]) {
return messagesAdapter.addMany(
messagesAdapter.getInitialState(),
response
)
},
async onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
) {
const ws = new WebSocket('ws://localhost:8080')
try {
await cacheDataLoaded
const listener = (event: MessageEvent) => {
const data = JSON.parse(event.data)
if (!isMessage(data) || data.channel !== arg) return
updateCachedData((draft) => {
messagesAdapter.upsertOne(draft, data)
})
}
ws.addEventListener('message', listener)
} catch {}
await cacheEntryRemoved
ws.close()
},
}),
}),
})
export const { useGetMessagesQuery } = api
for the frontend. It looks as though the idea is to make a request to /messages/{channel} and on successful receipt and caching of these messages to connect to a websocket api. I'm struggling to create a fastapi app that connects with this example so I can figure out the workings. Does anyone have an example they might be willing to please share?

NestJS Context is undefined in graphql subscription

can someone help me, why is the CONTEXT undefined inside my subscription?
#Subscription(returns => CommentsDto, {
filter: (payload, variables, context) => {
console.log({ payload, variables, context }) // <------------ context context undefined
const isSameCode = variables.code === payload.newComment.code
const isAuthorized = context.req.headers.clientauthorization === payload.clientauthorization
return isSameCode && isAuthorized
},
})
newComment(
#Context() context,
#Args(({ name: 'code', type: () => String })) code: string,
) {
console.log(context) // <------------ undefined
return this.publisherService.asyncIterator('newComment')
}
It is working for Queries and Mutatinos...
Graphql definition is:
const GraphQLDefinition = GraphQLModule.forRoot({
context: ({ req, connection }) => {
// subscriptions
if (connection) {
return { req: connection.context }
}
// queries and mutations
return { req }
},
installSubscriptionHandlers: true,
path: '/graphql',
playground: true,
})
Thank you for any help
Because the Req and Res are undefined in the case of subscriptions so when you try to log the context it is undefined.
For context to be available you need to change the guards that you are using to return the context which can be found in the connection variable.
Basically to summarize:
=> req, res used in http/query & mutations
=> connection used in webSockets/subscriptions
Now to get the context correctly you will have to perform these steps exactly:
Modify App module file to use the GraphqlModuleImport
Modify Extract User Guard and Auth guard (or whatever guards you are using)
to return data for both query/mutation and subscription case.
Receive data using the context in the subscription.
Add jwtTokenPayload extractor function in the Auth service.
Opitonal: Helper Functions and DTOs for Typescript.
1-Detail:
GraphQLModule.forRootAsync({
//import AuthModule for JWT headers at graphql subscriptions
imports: [AuthModule],
//inject Auth Service
inject: [AuthService],
useFactory: async (authService: AuthService) => ({
debug: true,
playground: true,
installSubscriptionHandlers: true,
// pass the original req and res object into the graphql context,
// get context with decorator `#Context() { req, res, payload, connection }: GqlContext`
// req, res used in http/query&mutations, connection used in webSockets/subscriptions
context: ({ req, res, payload, connection }: GqlContext) => ({
req,
res,
payload,
connection,
}),
// subscriptions/webSockets authentication
typePaths: ["./**/*.graphql"],
resolvers: { ...resolvers },
subscriptions: {
// get headers
onConnect: (connectionParams: ConnectionParams) => {
// convert header keys to lowercase
const connectionParamsLowerKeys: Object = mapKeysToLowerCase(
connectionParams,
);
// get authToken from authorization header
let authToken: string | false = false;
const val = connectionParamsLowerKeys["authorization"];
if (val != null && typeof val === "string") {
authToken = val.split(" ")[1];
}
if (authToken) {
// verify authToken/getJwtPayLoad
const jwtPayload: JwtPayload = authService.getJwtPayLoad(
authToken,
);
// the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
return {
currentUser: jwtPayload.username,
jwtPayload,
headers: connectionParamsLowerKeys,
};
}
throw new AuthenticationError("authToken must be provided");
},
},
definitions: {
path: join(process.cwd(), "src/graphql.classes.ts"),
outputAs: "class",
},
}),
}),
2-Detail:
My getRequest function example from the ExtractUserGuard class that extends the AuthGuard(jwt) class.
Change from:
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext().req;
return request;}
to this:
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
// req used in http queries and mutations, connection is used in websocket subscription connections, check AppModule
const { req, connection } = ctx.getContext();
// if subscriptions/webSockets, let it pass headers from connection.context to passport-jwt
const requestData =
connection && connection.context && connection.context.headers
? connection.context
: req;
return requestData;
}
3- Now you can get this data in your resolver.
#Subscription("testSubscription")
#UseGuards(ExtractUserGuard)
async testSubscription(
#Context("connection") connection: any,
): Promise<JSONObject> {
const subTopic = `${Subscriptions_Test_Event}.${connection.context.jwtPayload.email}`;
console.log("Listening to the event:", subTopic);
return this.pubSub.asyncIterator(subTopic);
}
4- For getting the jwtPayload using the token add the following function to your AuthService.
getJwtPayLoad(token: string): JwtPayload {
const jwtPayload = this.jwtService.decode(token);
return jwtPayload as JwtPayload;
}
5-Helper Functions and DTOs example (that I used in my project)
DTOs:
export interface JwtPayload {
username?: string;
expiration?: Date;
}
export interface GqlContext {
req: Request;
res: Response;
payload?: JwtPayload;
// required for subscription
connection: any;
}
export interface ConnectionParams {
authorization: string;
}
Helper Function:
export function mapKeysToLowerCase(
inputObject: Record<string, any>,
): Record<string, any> {
let key;
const keys = Object.keys(inputObject);
let n = keys.length;
const newobj: Record<string, any> = {};
while (n--) {
key = keys[n];
newobj[key.toLowerCase()] = inputObject[key];
}
return newobj;
}

Stitching secure subscriptions using makeRemoteExecutableSchema

We have implemented schema stitching where GraphQL server fetches schema from two remote servers and stitches them together. Everything was working fine when we were only working with Query and Mutations, but now we have a use-case where we even need to stitch Subscriptions and remote schema has auth implemented over it.
We are having a hard time figuring out on how to pass authorization token received in connectionParams from client to remote server via the gateway.
This is how we are introspecting schema:
API Gateway code:
const getLink = async(): Promise<ApolloLink> => {
const http = new HttpLink({uri: process.env.GRAPHQL_ENDPOINT, fetch:fetch})
const link = setContext((request, previousContext) => {
if (previousContext
&& previousContext.graphqlContext
&& previousContext.graphqlContext.request
&& previousContext.graphqlContext.request.headers
&& previousContext.graphqlContext.request.headers.authorization) {
const authorization = previousContext.graphqlContext.request.headers.authorization;
return {
headers: {
authorization
}
}
}
else {
return {};
}
}).concat(http);
const wsLink: any = new WebSocketLink(new SubscriptionClient(process.env.REMOTE_GRAPHQL_WS_ENDPOINT, {
reconnect: true,
// There is no way to update connectionParams dynamically without resetting connection
// connectionParams: () => {
// return { Authorization: wsAuthorization }
// }
}, ws));
// Following does not work
const wsLinkContext = setContext((request, previousContext) => {
let authToken = previousContext.graphqlContext.connection && previousContext.graphqlContext.connection.context ? previousContext.graphqlContext.connection.context.Authorization : null
return {
context: {
Authorization: authToken
}
}
}).concat(<any>wsLink);
const url = split(({query}) => {
const {kind, operation} = <any>getMainDefinition(<any>query);
return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLinkContext,
link)
return url;
}
const getSchema = async (): Promise < GraphQLSchema > => {
const link = await getLink();
return makeRemoteExecutableSchema({
schema: await introspectSchema(link),
link,
});
}
const linkSchema = `
extend type UserPayload {
user: User
}
`;
const schema: any = mergeSchemas({
schemas: [linkSchema, getSchema],
});
const server = new GraphQLServer({
schema: schema,
context: req => ({
...req,
})
});
Is there any way for achieving this using graphql-tools? Any help appreciated.
I have one working solution: the idea is to not create one instance of SubscriptionClient for the whole application. Instead, I'm creating the clients for each connection to the proxy server:
server.start({
port: 4000,
subscriptions: {
onConnect: (connectionParams, websocket, context) => {
return {
subscriptionClients: {
messageService: new SubscriptionClient(process.env.MESSAGE_SERVICE_SUBSCRIPTION_URL, {
connectionParams,
reconnect: true,
}, ws)
}
};
},
onDisconnect: async (websocket, context) => {
const params = await context.initPromise;
const { subscriptionClients } = params;
for (const key in subscriptionClients) {
subscriptionClients[key].close();
}
}
}
}, (options) => console.log('Server is running on http://localhost:4000'))
if you would have more remote schemas you would just create more instances of SubscriptionClient in the subscriptionClients map.
To use those clients in the remote schema you need to do two things:
expose them in the context:
const server = new GraphQLServer({
schema,
context: ({ connection }) => {
if (connection && connection.context) {
return connection.context;
}
}
});
use custom link implementation instead of WsLink
(operation, forward) => {
const context = operation.getContext();
const { graphqlContext: { subscriptionClients } } = context;
return subscriptionClients && subscriptionClients[clientName] && subscriptionClients[clientName].request(operation);
};
In this way, the whole connection params will be passed to the remote server.
The whole example can be found here: https://gist.github.com/josephktcheung/cd1b65b321736a520ae9d822ae5a951b
Disclaimer:
The code is not mine, as #josephktcheung outrun me with providing an example. I just helped with it a little. Here is the original discussion: https://github.com/apollographql/graphql-tools/issues/864
This is a working example of remote schema with subscription by webscoket and query and mutation by http. It can be secured by custom headers(params) and shown in this example.
Flow
Client request
-> context is created by reading req or connection(jwt is decoded and create user object in the context)
-> remote schema is executed
-> link is called
-> link is splitted by operation(wsLink for subscription, httpLink for queries and mutations)
-> wsLink or httpLink access to context created above (=graphqlContext)
-> wsLink or httpLink use context to created headers(authorization header with signed jwt in this example) for remote schema.
-> "subscription" or "query or mutation" are forwarded to remote server.
Note
Currently, ContextLink does not have any effect on WebsocketLink. So, instead of concat, we should create raw ApolloLink.
When creating context, checkout connection, not only req. The former will be available if the request is websocket, and it contains meta information user sends, like an auth token.
HttpLink expects global fetch with standard spec. Thus, do not use node-fetch, whose spec is incompatible (especially with typescript). Instead, use cross-fetch.
const wsLink = new ApolloLink(operation => {
// This is your context!
const context = operation.getContext().graphqlContext
// Create a new websocket link per request
return new WebSocketLink({
uri: "<YOUR_URI>",
options: {
reconnect: true,
connectionParams: { // give custom params to your websocket backend (e.g. to handle auth)
headers: {
authorization: jwt.sign(context.user, process.env.SUPER_SECRET),
foo: 'bar'
}
},
},
webSocketImpl: ws,
}).request(operation)
// Instead of using `forward()` of Apollo link, we directly use websocketLink's request method
})
const httpLink = setContext((_graphqlRequest, { graphqlContext }) => {
return {
headers: {
authorization: jwt.sign(graphqlContext.user, process.env.SUPER_SECRET),
},
}
}).concat(new HttpLink({
uri,
fetch,
}))
const link = split(
operation => {
const definition = getMainDefinition(operation.query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink, // <-- Executed if above function returns true
httpLink, // <-- Executed if above function returns false
)
const schema = await introspectSchema(link)
const executableSchema = makeRemoteExecutableSchema({
schema,
link,
})
const server = new ApolloServer({
schema: mergeSchemas([ executableSchema, /* ...anotherschemas */]),
context: ({ req, connection }) => {
let authorization;
if (req) { // when query or mutation is requested by http
authorization = req.headers.authorization
} else if (connection) { // when subscription is requested by websocket
authorization = connection.context.authorization
}
const token = authorization.replace('Bearer ', '')
return {
user: getUserFromToken(token),
}
},
})

Apollo GraphQL server; setting context to handle requests triggered by a fired subscription

I understand how to set the context object when creating a GraphQL server e.g.
const app = express();
app.use(GRAPHQL_URL, graphqlExpress({
schema,
context: {
foo: 'bar'
},
}));
so that the context object is passed to my resolvers when handling an incoming request.
However I'm not seeing this context object when the resolvers are triggered by a subscription (i.e. a client subscribes to a GraphQL subscription, and defines the shape of the data to be sent to them when the subscription fires); in that case the context appears to be an empty Object.
Is there way to ensure that my context object is set correctly when resolvers are called following a PubSub.publish() call?
I guess you are using the package subscription-transport-ws. In that case it is possible to add a context value in different execution steps.
See API. Two possible scenarios
If you have some kind of authentication. You could add a viewer in the context at the onConnect execution step. This is done at the first connection to the websocket and wont change until the connection is closed and opened again. See example.
If you want to add a context more dynamically you can add a kind of middleware before the execute step.It could look like this:
const middleware = (args) => new Promise((resolve, reject) => {
const [schema, document, root, context, variables, operation] = args;
context.foo = "bar"; // add something to context
resolve(args);
})
subscriptionServer = SubscriptionServer.create({
schema: executable.schema,
subscribe,
execute: (...args) => middleware(args).then(args => {
return execute(...args);
})
}, {
server: websocketServer,
path: "/graphql",
}, );
Here is my solution:
You can pass the context and do the authentication for graphql subscription(WebSocket )like this:
const server = new ApolloServer({
typeDefs,
resolvers,
context: contextFunction,
introspection: true,
subscriptions: {
onConnect: (
connectionParams: IWebSocketConnectionParams,
webSocket: WebSocket,
connectionContext: ConnectionContext,
) => {
console.log('websocket connect');
console.log('connectionParams: ', connectionParams);
if (connectionParams.token) {
const token: string = validateToken(connectionParams.token);
const userConnector = new UserConnector<IMemoryDB>(memoryDB);
let user: IUser | undefined;
try {
const userType: UserType = UserType[token];
user = userConnector.findUserByUserType(userType);
} catch (error) {
throw error;
}
const context: ISubscriptionContext = {
// pubsub: postgresPubSub,
pubsub,
subscribeUser: user,
userConnector,
locationConnector: new LocationConnector<IMemoryDB>(memoryDB),
};
return context;
}
throw new Error('Missing auth token!');
},
onDisconnect: (webSocket: WebSocket, connectionContext: ConnectionContext) => {
console.log('websocket disconnect');
},
},
});
You can pass the context argument of resolver using pubsub.publish method in your resolver like this:
addTemplate: (
__,
{ templateInput },
{ templateConnector, userConnector, requestingUser }: IAppContext,
): Omit<ICommonResponse, 'payload'> | undefined => {
if (userConnector.isAuthrized(requestingUser)) {
const commonResponse: ICommonResponse = templateConnector.add(templateInput);
if (commonResponse.payload) {
const payload = {
data: commonResponse.payload,
context: {
requestingUser,
},
};
templateConnector.publish(payload);
}
return _.omit(commonResponse, 'payload');
}
},
Now, we can get the http request context and subscription(websocket) context in
your Subscription resolver subscribe method like this:
Subscription: {
templateAdded: {
resolve: (
payload: ISubscriptionPayload<ITemplate, Pick<IAppContext, 'requestingUser'>>,
args: any,
subscriptionContext: ISubscriptionContext,
info: any,
): ITemplate => {
return payload.data;
},
subscribe: withFilter(templateIterator, templateFilter),
},
},
async function templateFilter(
payload?: ISubscriptionPayload<ITemplate, Pick<IAppContext, 'requestingUser'>>,
args?: any,
subscriptionContext?: ISubscriptionContext,
info?: any,
): Promise<boolean> {
console.count('templateFilter');
const NOTIFY: boolean = true;
const DONT_NOTIFY: boolean = false;
if (!payload || !subscriptionContext) {
return DONT_NOTIFY;
}
const { userConnector, locationConnector } = subscriptionContext;
const { data: template, context } = payload;
if (!subscriptionContext.subscribeUser || !context.requestingUser) {
return DONT_NOTIFY;
}
let results: IUser[];
try {
results = await Promise.all([
userConnector.findByEmail(subscriptionContext.subscribeUser.email),
userConnector.findByEmail(context.requestingUser.email),
]);
} catch (error) {
console.error(error);
return DONT_NOTIFY;
}
//...
return true;
}
As you can see, now we get the subscribe users(who establish the WebSocket connection with graphql webserver) and HTTP request user(who send the mutation to graphql webserver) from subscriptionContext and HTTP request context.
Then you can do the rest works if the return value of templateFilter function is truthy, then WebSocket will push message to subscribe user with payload.data, otherwise, it won't.
This templateFilter function will be executed multiple times depending on the count of subscribing users which means it's iterable. Now you get each subscribe user in this function and does your business logic to decide if push WebSocket message to the subscribe users(client-side) or not.
See github example repo
Articles:
GraphQL Subscription part 1
GraphQL Subscription part 2
If you're using Apollo v3, and graphql-ws, here's a docs-inspired way to achieve context resolution:
const wsContext = async (ctx, msg, args) => {
const token = ctx.connectionParams.authorization;
const currentUser = await findUser(token);
if(!currentUser) throw Error("wrong user token");
return { currentUser, foo: 'bar' };
};
useServer(
{
schema,
context: wsContext,
}
wsServer,
);
You could use it like so in your Apollo React client:
import { GraphQLWsLink } from '#apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
const wsLink = new GraphQLWsLink(createClient({
url: 'ws://localhost:4000/subscriptions',
connectionParams: {
authorization: user.authToken,
},
}));

Resources