3

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?

wabrit
  • 217
  • 2
  • 14

3 Answers3

1

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

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

  2. 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",
}, );
Locco0_0
  • 3,420
  • 5
  • 30
  • 42
  • Many thanks for your response; the middleware interceptit is definitely the way I would need to go as I need the context to be modified based on information in the "trigger event". – wabrit Aug 10 '17 at 08:59
  • However it doesn't seem to be working for me - if I define my own execute function in the way suggested above, I can see it being set into the subscription server, but it never hits my method and always goes through the **standard** graphql execute() method. Looking in `graphql\subscription\subscribe:subscribeImpl()` it appears to be always calling the default one from `graphql\execution\execute.js`. – wabrit Aug 10 '17 at 09:07
  • Ok i think i did not understand your question correctly. You subscribe on the client. And in the subscription you add a parameter, which states how the result should look like? – Locco0_0 Aug 10 '17 at 10:50
  • Not quite - I'm mapping external events (messages on a remote queue that arrive via STOMP from an external broker) to GraphQL subscription events; there's a bit of metadata that arrives with the message that I need to set into the GraphQL context so that downstream resolvers have access to it. – wabrit Aug 10 '17 at 12:22
  • I see. So you could try adding the middleware in the `subscribe` functionality or maybe you can manipulate the subscription in the `onOperation` function. See [API](https://github.com/apollographql/subscriptions-transport-ws) – Locco0_0 Aug 10 '17 at 13:25
  • I don't think either of those injection points will help alas; the execute () injection would have been the right approach, except that it doesn't appear to work (see earlier comment). – wabrit Aug 10 '17 at 13:40
0

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:

Lin Du
  • 88,126
  • 95
  • 281
  • 483
-1

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

SecondThread
  • 108
  • 2
  • 11
exaucae
  • 2,071
  • 1
  • 14
  • 24