GraphQL 中 ClassSerializerInterceptor 的 NestJS 问题

NestJS problem with ClassSerializerInterceptor in GraphQL

我正在尝试使用 class-transformer and NestJS。我正在构建一个基于 Mongoose 和 GraphQL 的 API。 这就是我在我的一个 GraphQL 对象中使用 Exclude 装饰器的方式:

@ObjectType('user')
export class User extends Teacher {
  @Field()
  login: string;
    
  @Field()
  @Exclude()
  password: string;
}

这是我在 UserResolver 中与 ClassSerializerInterceptor 一起使用的方法:

@UseInterceptors(ClassSerializerInterceptor)
@Mutation(returns => User, { name: 'editUserById', nullable: true })
async editById(
  @Args('id') id: string,  
  @Args({ name: "item", type: () => UserInputType }) item: UserInputType
) {
  const user = await this.usersService.editById(id, item);
  return user;
}

我想做的是从这个突变用户字段中获取没有密码的用户字段(这在 GraphQL 对象中被排除)。但不幸的是,所有字段都是空的。来自 GraphQL playground 的错误示例:

{
  "errors": [
    {
      "message": "Cannot return null for non-nullable field user.firstName.",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "editUserById",
        "firstName"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR"
      }
    }
  ],
  "data": {
    "editUserById": null
  }
}

教师对象:

@ObjectType('teacher')
@InputType()
export class Teacher {
  @Field()
  _id: string;

  @Field()
  firstName: string;

  @Field()
  lastName: string;

  @Field(type => [String], { nullable: true })
  schoolsIds: string[];

  @Field(type => [School], { nullable: true })
  schools: School[];
}

没有拦截器,一切正常(除了密码仍然可见)。知道我做错了什么吗?

您已经告诉 GraphQL 您将 returning 一个 User 对象,该对象具有不可为空的字段 password。您还告诉 class-transformer,当 plainToClass 为 运行 时,必须删除 password 字段 。所以现在 GraphQL 很不高兴,因为你已经打破了你说应该存在的对象契约(returned 对象 必须 有一个 password 字段)因为你已经告诉另一个图书馆删除那个字段。

这里有几个选项:

  1. 使用 @Field({ nullable: true }) 告诉 GraphQL 这个字段不必 returned。这仍然意味着有人可以查询该字段,并且它将始终是 undefined 虽然

  2. 删除 password @Field() 注释,因为它不应在 user 查询中 returned 并且只保留在 @InputType()对象。

编辑

感谢您从这个答案的评论中得到答案,您正在 return 从数据库中获取对象而不是 User class 的实例。如果您的数据库 returns 具有 class-t运行sformer 装饰器,您将需要检查那里可能发生的情况(例如可能排除字段)。另外,请注意 returning 猫鼬对象确实有 和 class-t运行sformer 直接,你可能需要 t运行sform 数据库 return 到普通的 JSON 然后到 class 以便 class-transformer 正常工作

你可以用这个例子https://gist.github.com/EndyKaufman/aa8a7ebb750540217a08fac72292a9c2

import { CallHandler, ClassSerializerInterceptor, ExecutionContext, Injectable, Logger, Module, PlainLiteralObject, SetMetadata, Type } from '@nestjs/common';
import { APP_INTERCEPTOR, Reflector } from '@nestjs/core';
import { ClassTransformOptions, plainToClass } from 'class-transformer';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export const GQL_RETURN_TYPE = 'GQL_RETURN_TYPE';

export const GqlReturn = (type: Type<any>) => SetMetadata('GQL_RETURN_TYPE', type);

@Resolver()
export class PaymentBalanceResolver {
    @GqlReturn(BalanceDto)
    @Query(() => BalanceDto, {
        nullable: false,
    })
    getUserBalance(@CurrentUser() currentUser: CurrentUserType): Observable<BalanceDto> {
        return this.paymentBalanceService.getUserBalance(currentUser.id);
    }
}

@Module({
    providers: [
        {
            provide: APP_INTERCEPTOR,
            useFactory: (reflector: any): ClassSerializerInterceptor => new ClassSerializerInterceptor(reflector),
            inject: [Reflector],
        },
        PaymentBalanceResolver,
    ],
})
export class AppModule {}

@Injectable()
export class CoreClassSerializerInterceptor extends ClassSerializerInterceptor {
    private readonly logger = new Logger(CoreClassSerializerInterceptor.name);

    constructor(protected readonly reflector: any, defaultOptions?: ClassTransformOptions) {
        super(reflector, defaultOptions);
        this.logger.log('create');
    }

    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        if ((context.getType() as string) === 'graphql') {
            const op = context.getArgByIndex(3).operation.operation;
            if (op === 'subscription') {
                return next.handle();
            }
            const contextOptions = this.getContextOptions(context);
            const options = {
                ...this.defaultOptions,
                ...contextOptions,
            };
            return next
                .handle()
                .pipe(
                    map((res: PlainLiteralObject | Array<PlainLiteralObject>) =>
                        this.serialize(res, { ...options, returnClass: Reflect.getMetadata(GQL_RETURN_TYPE, context.getHandler()) })
                    )
                );
        }
        return next.handle();
    }

    serialize(
        response: PlainLiteralObject | Array<PlainLiteralObject>,
        options: ClassTransformOptions & { returnClass: any }
    ): PlainLiteralObject | Array<PlainLiteralObject> {
        try {
            const result = super.serialize(options.returnClass ? plainToClass(options.returnClass, response) : response, options);
            return result;
        } catch (err) {
            this.logger.debug(response);
            this.logger.error(err);
            throw err;
        }
    }
}