javad26897
javad26897

Reputation: 483

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

Upvotes: 4

Views: 3989

Answers (1)

Ahmad Salman Khan
Ahmad Salman Khan

Reputation: 131

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:

  1. Modify App module file to use the GraphqlModuleImport
  2. Modify Extract User Guard and Auth guard (or whatever guards you are using) to return data for both query/mutation and subscription case.
  3. Receive data using the context in the subscription.
  4. Add jwtTokenPayload extractor function in the Auth service.
  5. 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;
}

Upvotes: 6

Related Questions