nestjs graphql subscription how to get context/pass data to resolver or interceptor?

1k views Asked by At

I'm trying to use NestJS subscriptions, I have tried both graphql-ws and subscriptions-transport-ws.

I have a very basic use case -- an interceptor to take the authorization header and then set the user object on the connection or request object to be able to use by the resolvers.

I've looked through the docs and several other SO posts but none of the solutions worked.

Current syntax:

    const ctx = GqlExecutionContext.create(context).switchToWs().getClient(); // undefined
    const ctx = GqlExecutionContext.create(context).getContext(); // undefined

In the module subscriptions config, I can see the auth header being passed for the subscriptions-transport-ws block of code and the `graphql-ws- block of code.

But in the interceptor everything returned is undefined.

          subscriptions: {
            'graphql-ws': {
              path: '/apis/event-service/graphql',
              onConnect: (ctx) => {
                console.log('CONTEXT', ctx);
              },
            },
            'subscriptions-transport-ws': {
              path: '/apis/event-service/graphql',
              onConnect: (ctx) => {
                console.log('CONTEXT', ctx);
              },
            },
          },

How do I get the context?

1

There are 1 answers

0
Snowflyt On

I use graphql-ws in my NestJS app for subscription, and I also use a simple JwtAuthGuard in my app to take the authorization header for authentiacation.

After a few hours of debugging breakpoints, I think I understand how this process works, and this is my solution.

First, just enable graphql-ws and define the context in your AppModule.

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      sortSchema: true,
      context: ({ req, connectionParams, extra }) => ({
        req,
        connectionParams,
        extra,
      }),
      subscriptions: {
        'graphql-ws': true,
      },
    }),
  ],
})
export class AppModule {}

Then add a simple conditional to your JwtAuthGuard.

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  ...

  getRequest(context: ExecutionContext): any {
    const ctx = GqlExecutionContext.create(context);
    const { req, connectionParams } = ctx.getContext();
    return req.headers
      ? req
      : {
          ...req,
          // Convert the uppercase key to lowercase.
          headers: Object.entries(connectionParams.headers).reduce(
            (acc, [key, value]) => {
              acc[key.toLowerCase()] = value;
              return acc;
            },
            {},
          ),
        };
  }

In the proceeding code, I assume that you pass headers in connectionParams in your graphql-ws request. You can change it if you use a different way to pass your headers.

The logic is quite clear. In a graphql-ws subscription request, req does not have a field named headers. So we can judge whether a request is a subscription by checking if headers exist on req. If not, then the request is a subscription request, we get the headers field from connectionParams and add it to req.