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

Viewed 3925

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?

3 Answers

Here is my solution:

  1. 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');
      },
    },
  });

  1. 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');
      }
    },
  1. 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:

For anyone using Appolo 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 Appolo 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,
  },
}));

Related