首页 > 解决方案 > 在 graphql 订阅中未定义 NestJS 上下文

问题描述

有人可以帮助我,为什么我的订阅中未定义 CONTEXT?

@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')
}

它适用于查询和突变体......

Graphql 定义为:

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,

})

感谢您的任何帮助

标签: graphqlnestjs

解决方案


因为在订阅的情况下 Req 和 Res 是未定义的,所以当您尝试记录上下文时它是未定义的。

要使上下文可用,您需要更改用于返回可在连接变量中找到的上下文的守卫。基本上总结一下:

  • => req, res 用于 http/query & 突变
  • => webSockets/subscriptions 中使用的连接

现在要正确获取上下文,您必须准确执行以下步骤:

  1. 修改 App 模块文件以使用 GraphqlModuleImport
  2. 修改 Extract User Guard 和 Auth guard(或您正在使用的任何防护)以返回查询/变异和订阅案例的数据。
  3. 使用订阅中的上下文接收数据。
  4. 在 Auth 服务中添加 jwtTokenPayload 提取器功能。
  5. 可选:Typescript 的辅助函数和 DTO。

1-细节:

 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-详细信息:ExtractUserGuard 类中的我的 getRequest 函数示例,它扩展了 AuthGuard(jwt) 类。

更改自:

  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    const request = ctx.getContext().req;
    return request;}

对此:


 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-现在您可以在解析器中获取此数据。

  @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- 要使用令牌获取 jwtPayload,请将以下函数添加到您的 AuthService。

  getJwtPayLoad(token: string): JwtPayload {
    const jwtPayload = this.jwtService.decode(token);
    return jwtPayload as JwtPayload;
  }

5-Helper 函数和 DTO 示例(我在项目中使用)

DTO:

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

辅助功能:

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

推荐阅读