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

Posted

技术标签:

【中文标题】NestJS/Mongoose:序列化不排除普通输出中的属性【英文标题】:NestJS/Mongoose: serialization does not exclude properties in plain output 【发布时间】:2021-08-02 00:57:56 【问题描述】:

我已经开始使用 NestJS,从我的旧 express/mongoose 项目迁移并立即撞到栅栏,只是遵循 NestJS 文档中的 MongoDB/序列化章节。我准备了以下架构

/////// 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);

在 app.module 中注册

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 mongoose 和序列化耦合的问题 - 为什么会这样

  @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());
  

有效(这也意味着对于每个需要创建实体的结果可枚举映射)

【问题讨论】:

我有一个问题要问你,你想隐藏每次选择还是创建? 这要么是关于 Nestjs 猫鼬和序列化耦合 - 为什么这个 @UseInterceptors(ClassSerializerInterceptor) ... return await this.userModel.findOne().lean() 不起作用而这个 @UseInterceptors(ClassSerializerInterceptor) ... return new User(await this.userModel.findOne().lean()) 起作用 最后有没有找到好的解决方案?我也面临同样的情况 最后我使用了构造函数和返回多个项目的方法,我最终使用了 async findAll(): Promise return (await this.userModel.find().lean(). exec()).map(user => new User(user)) 不太满意 【参考方案1】:

花了几个小时终于找到了post中描述的解决方案

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

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

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

【讨论】:

这个对我不起作用,唯一在 2021 年真正起作用的是用构造函数实例化对象。从服务返回单个对象时看起来不错,但返回数组时看起来很糟糕 @SergioArrighi 我上周使用了上面的代码,所以它可以在 2022 年使用 :) 感谢您告诉我,也许我的软件中存在一些特殊性,此解决方案不适用于此解决方案 @SergioArrighi 使用我在答案中添加的教程链接。对你有更多帮助 好的,所以我再次尝试,实际上我设法让@Exclude 工作,而无需从服务中实例化对象。但是......我无法让 @Expose( groups: [Role.Admin] ) 工作,而当我从服务中返回实例时它工作得很好。您是否也设法让 Expose 工作?【参考方案2】:

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

我们用于连接到 MongoDB 的 Mongoose 库和 获取实体不会返回我们的 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 作为,这样你就可以将它用于不同的角色。因此,对于普通用户,我们可以公开所需的东西

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.

【讨论】:

【参考方案3】:

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

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

// destructuring
const  password, ...newUser  = user;

return newUser;

【讨论】:

请添加更多详细信息以扩展您的答案,例如工作代码或文档引用。 这个库是一个很好的建议,但它目前不像github.com/d38u6/nestjs-mongoose-exclude/issues/1中报告的那样工作 这对我有用! :D 也许我在这里的回答可能会帮助遇到这个问题的人github.com/d38u6/nestjs-mongoose-exclude/issues/…【参考方案4】:

我认为我有解决办法

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

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

【讨论】:

不,我只想从 UI 部分隐藏该字段的值 - 因此,我可以进行 console.log(user.password) 或任何相等性检查,但返回用户实体(实体)没有有价值的道具到前端 如果你把它设为 false 将不会让用户获得密码,当你想将密码与 bcrypt 进行比较时,这会破坏代码 好点@mirsahib【参考方案5】:

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

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

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

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

【讨论】:

以上是关于NestJS/Mongoose:序列化不排除普通输出中的属性的主要内容,如果未能解决你的问题,请参考以下文章

无法使用 nestjs/mongoose 连接 mongoDB

在 @nestjs/mongoose 中设置 mongoose 全局选项

如何在nestjs mongoose typescript中引用嵌套文档

NestJS Mongoose 模式继承

NestJS - 如何自我引用 mongoDB 架构 @nestjs/mongoose?

@nestjs/mongoose 的文档