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
字段)因为你已经告诉另一个图书馆删除那个字段。
这里有几个选项:
使用 @Field({ nullable: true })
告诉 GraphQL 这个字段不必 returned。这仍然意味着有人可以查询该字段,并且它将始终是 undefined
虽然
删除 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;
}
}
}
我正在尝试使用 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
字段)因为你已经告诉另一个图书馆删除那个字段。
这里有几个选项:
使用
@Field({ nullable: true })
告诉 GraphQL 这个字段不必 returned。这仍然意味着有人可以查询该字段,并且它将始终是undefined
虽然删除
password
@Field()
注释,因为它不应在user
查询中 returned 并且只保留在@InputType()
对象。
编辑
感谢您从这个答案的评论中得到答案,您正在 return 从数据库中获取对象而不是 User
class 的实例。如果您的数据库 returns 具有 class-t运行sformer 装饰器,您将需要检查那里可能发生的情况(例如可能排除字段)。另外,请注意 returning 猫鼬对象确实有 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;
}
}
}