NestJS/Mongoose:序列化不排除普通输出中的属性



我已经开始玩NestJS,从我的旧express/mongoose项目迁移,并立即撞上了围栏,只是遵循NestJS文档中的MongoDB/serializations章节。我准备了以下架构

/////// schema
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import * as mongoose from 'mongoose';
import { Exclude, Expose } from 'class-transformer';
export type UserDocument = User & mongoose.Document;
@Schema()
export class User {
@Prop()
@Exclude()
_id: String
@Expose()
get id(): String { return this._id ? `${this._id}` : undefined }
@Prop()
name: string
@Prop({ unique: true })
login: string
@Exclude()
@Prop()
password: string        
}
export const UserSchema = SchemaFactory.createForClass(User);

在应用程序模块中注册了它

MongooseModule.forRoot('mongodb://localhost/old_project'), 
MongooseModule.forFeature([ { name: User.name, schema: UserSchema } ]),

并尝试跟踪呼叫,期望结果中没有显示密码属性

/////// controller
@UseInterceptors(ClassSerializerInterceptor)
@Get('default')
async default(): Promise<User> {
let u = new User();
u.name = 'Kos';
u.password = "secret";
u.login = 'k@o.s'
return u;
}

// returns
// {"name":"Kos","login":"k@o.s"}
@Get('first_raw')
async firstRaw(): Promise<User> {
return this.userModel.findOne()
}

@Get('first_lean')
async firstLean(): Promise<User> {
return this.userModel.findOne().lean()
}

//both return
// {"_id":"5f8731a36fc003421db08921","name":"Kos","login":"kos","password":"secret","__v":0}
@UseInterceptors(ClassSerializerInterceptor)
@Get('first_raw_stripped')
async firstRawStripped(): Promise<User> {
return this.userModel.findOne()
}

//returns
// {"$__":{"strictMode":true,"selected":{},"getters":{},"_id":"5f8731a36fc003421db08921","wasPopulated":false,"activePaths":{"paths":{"_id":"init","name":"init","login":"init","password":"init","__v":"init"},"states":{"ignore":{},"default":{},"init":{"_id":true,"name":true,"login":true,"password":true,"__v":true},"modify":{},"require":{}},"stateNames":["require","modify","init","default","ignore"]},"pathsToScopes":{},"cachedRequired":{},"$setCalled":[],"emitter":{"_events":{},"_eventsCount":0,"_maxListeners":0},"$options":{"skipId":true,"isNew":false,"willInit":true,"defaults":true}},"isNew":false,"$locals":{},"$op":null,"_doc":{"_id":"5f8731a36fc003421db08921","name":"Kos","login":"kos","password":"secret","__v":0},"$init":true}
@UseInterceptors(ClassSerializerInterceptor)
@Get('first_lean_stripped')
async firstLeanStripped(): Promise<User> {
return this.userModel.findOne().lean()
}

//returns
// {"_id":"5f8731a36fc003421db08921","name":"Kos","login":"kos","password":"secret","__v":0}

最后,我发现只有手动实例化User类才能以某种方式执行它应该做的事情,因此我将构造函数添加到User类中

constructor(partial?: Partial<User>) {
if (partial)
Object.assign(this, partial);
}

然后它最终返回了预期的内容 - 结果中没有密码道具

@UseInterceptors(ClassSerializerInterceptor)
@Get('first')
async first(): Promise<User> {
return new User(await this.userModel.findOne().lean());
}

//finally returns what's expected
// {"name":"Kos","login":"kos","__v":0,"id":"5f8731a36fc003421db08921"}

我错过了什么吗?不知何故,它似乎有点压倒性...

更新:这是关于 NestJS 猫鼬和序列化耦合的问题 - 为什么会这样

@UseInterceptors(ClassSerializerInterceptor)
@Get('first')
async first(): Promise<User> {
return await this.userModel.findOne().lean();
}

不起作用,这

@UseInterceptors(ClassSerializerInterceptor)
@Get('first')
async first(): Promise<User> {
return new User(await this.userModel.findOne().lean());
}

工作(这也意味着每个结果都需要实体创建的可枚举映射)

花了几个小时后,我终于找到了本文中描述的解决方案

我们用于连接到MongoDB和获取实体的Mongoose库不会返回User类的实例。因此,ClassSerializerInterceptor无法开箱即用。

首先:为猫鼬序列化创建一个拦截器:

mongooseClassSerializer.interceptor.ts

import {
ClassSerializerInterceptor,
PlainLiteralObject,
Type,
} from '@nestjs/common';
import { ClassTransformOptions, plainToClass } from 'class-transformer';
import { Document } from 'mongoose';

function MongooseClassSerializerInterceptor(
classToIntercept: Type,
): typeof ClassSerializerInterceptor {
return class Interceptor extends ClassSerializerInterceptor {
private changePlainObjectToClass(document: PlainLiteralObject) {
if (!(document instanceof Document)) {
return document;
}

return plainToClass(classToIntercept, document.toJSON());
}

private prepareResponse(
response: PlainLiteralObject | PlainLiteralObject[],
) {
if (Array.isArray(response)) {
return response.map(this.changePlainObjectToClass);
}

return this.changePlainObjectToClass(response);
}

serialize(
response: PlainLiteralObject | PlainLiteralObject[],
options: ClassTransformOptions,
) {
return super.serialize(this.prepareResponse(response), options);
}
};
}

export default MongooseClassSerializerInterceptor;

更新控制器以应用此侦听器:

@UseInterceptors(MongooseClassSerializerInterceptor(User))

您的模型(模式)应如下所示:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { Exclude, Transform } from 'class-transformer';

export type UserDocument = User & Document;

@Schema()
export class User {
@Transform(({ value }) => value.toString())
_id: string;

@Prop({ unique: true })
email: string;

@Prop()
name: string;

@Prop()
@Exclude()
password: string;
}

export const UserSchema = SchemaFactory.createForClass(User);

正如@Ali Sherafat所解释的那样,不幸的是,解决方案对我不起作用。

我们用于连接到MongoDB的猫鼬库和 获取实体不会返回 User 类的实例。 因此,ClassSerializerInterceptor 不能开箱即用。

我们肯定需要interceptor for mongoose serialization.因此,我想出了另一个类似的解决方案,并进行修改。

为猫鼬序列化创建拦截器,如下所示

import {
CallHandler,
ExecutionContext,
NestInterceptor,
UseInterceptors,
} from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import { map, Observable } from 'rxjs';
interface ClassConstructor {
new ( ...args: any[ ] ): { };
}
export function MongooseClassSerializerInterceptor( dto: any ) {
return UseInterceptors( new SerializeInterceptor( dto ) );
}
export class SerializeInterceptor implements NestInterceptor {
constructor( private dto: any ) { }
intercept( context: ExecutionContext, handler: CallHandler ): Observable< any > {
return handler.handle( ).pipe(
map( ( data: any ) => { 
return plainToClass( this.dto, data, { 
excludeExtraneousValues: true
} )
} )
)
}
}

创建用户 dto as,这样您就可以将其用于不同的角色。因此,对于普通用户,我们可以公开所需的内容

import { Expose } from "class-transformer";
export class UserDto {

@Expose( )
id: number;

@Expose( )
name: string;
@Expose( )
login: string;

}

现在在您的控制器中使用@MongooseClassSerializerInterceptor( UserDto )

当想要基于某些角色返回响应时,在架构中使用exclude不是很灵活,e.g in required case admin may have access to more fields than normal user or vice-versa. In that case this is better approach.

我注意到你没有使用: [1]:https://www.npmjs.com/package/nestjs-mongoose-exclude。

我意识到它不是太出名,也没有很多东西可以下载,但你必须给小包一个机会。如果不想使用此包,可以在返回对象之前执行以下操作:

// destructuring
const { password, ...newUser } = user;
return newUser;

猫鼬内置了自己的抑制 toJson 方法,您可以在为模型创建架构时使用它。

export const UserSchema = (() =>   
const userSchema = SchemaFactory.createForClass(User);
schema.set('toJSON', {
transform: function (_, ret) {
delete ret.password;
},
});
return emailSchema;
})();


我认为我有解决方案

@Schema()
export class User {
@Prop({select: false})
password: string;
@Prop()
username: string;
}

当你对装饰器执行此道具时,MONGO 内部属性的值在查找中被忽略。

NestJS文档明确指出,它需要是一个类 - 而不是一个普通对象 - 才能使序列化正常工作。请参阅此处的红色警告:https://docs.nestjs.com/techniques/serialization#exclude-properties

这就是为什么当你把它包装在类构造函数中时,它可以正常工作。

正确的方法似乎是不要像您那样向模型添加构造函数,而是使用@InjectModel装饰器将架构/模型注入服务中,以便findOne方法返回类而不是普通对象:https://docs.nestjs.com/techniques/serialization#exclude-properties

注册架构后,可以使用@InjectModel()修饰器将Cat模型注入CatsService

如果你的任何npm包像(猫鼬排除),那么它只会排除单个对象而不是嵌套对象,如果你实现自己的自定义拦截器,那么@Expose()组将不起作用。

如果发现黑客,请将所有这些问题放在一起

import { Role } from "@type/UserType"
import { Exclude } from "class-transformer"
import { HydratedDocument, ObjectId } from "mongoose"
import { Prop, SchemaFactory, Schema } from "@nestjs/mongoose"
export type UserDocument = HydratedDocument<User>
@Schema({
timestamps: true,
versionKey: false
})
export class User {
toObject(): Partial<User> {
throw new Error("Method not implemented.")
}
@Exclude()
_id: ObjectId
@Prop({
type: String,
required: true
})
name: string
@Prop({
unique: true,
type: String,
required: true
})
email: string
@Exclude()
@Prop({ type: String })
password: string
@Prop({
type: String,
default: Role.USER
})
role: Role
@Prop({
type: String,
select: false
})
token: string
constructor(partial: Partial<User>) {
Object.assign(this, partial)
}
}
export const UserSchema = SchemaFactory.createForClass(User)

import { SignUpDto } from "@dto/UserDto"
import { Model, FilterQuery } from "mongoose"
import { StaticError } from "@type/ErrorType"
import { InjectModel } from "@nestjs/mongoose"
import { User, UserDocument } from "@schema/UserSchema"
import { Injectable, NotFoundException } from "@nestjs/common"
import { IUserRepository } from "@irepository/IUserRepository"
@Injectable()
export class UserRepository implements IUserRepository {
constructor(@InjectModel(User.name) private readonly userModel: Model<UserDocument>) { }
public async signUp(signUpDto: SignUpDto): Promise<User> {
const user: User = await this.userModel.create(signUpDto)
return new User(user.toObject())
}
}

app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)))

所有事情都按预期工作,例如使用组公开和嵌套排除

最新更新