【问题标题】:NestJS/Mongoose: serialization does not exclude properties in plain outputNestJS/Mongoose:序列化不排除普通输出中的属性
【发布时间】: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)) } 不太满意

标签: javascript nestjs nestjs-mongoose


【解决方案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;
    

    【讨论】:

    【解决方案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

    【讨论】:

      猜你喜欢
      • 2012-05-15
      • 2021-08-29
      • 2016-07-25
      • 2012-04-27
      • 1970-01-01
      • 2015-12-06
      • 2022-07-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多