Nestjs 响应序列化与对象数组

Nestjs Response Serialization with array of objects

我想通过 nestjs 序列化技术序列化控制器响应。我没有找到任何方法,我的解决方案如下:

用户实体

export type UserRoleType = "admin" | "editor" | "ghost";

@Entity()
export class User {
    @PrimaryGeneratedColumn() id: number;

    @Column('text')
        username: string;
    @Column('text') 
        password: string;
    @Column({
        type: "enum",
        enum: ["admin", "editor", "ghost"],
        default: "ghost"
    })
    roles: UserRoleType;
        @Column({ nullable: true })
                profileId: number;  
}

用户响应类

import { Exclude } from 'class-transformer';

export class UserResponse {
    id: number;

    username: string;

    @Exclude()
    roles: string;

    @Exclude()
    password: string;

    @Exclude()
    profileId: number;  

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

import { Exclude, Type } from 'class-transformer';
import { User } from 'src/_entities/user.entity';
import { UserResponse } from './user.response';

export class UsersResponse {

    @Type(() => UserResponse)
    users: User[]   

    constructor() { }
}

控制器

@Controller('user')
export class UsersController {
    constructor(
        private readonly userService: UserService
    ) {

    }
    @UseInterceptors(ClassSerializerInterceptor)
    @Get('all')
    async findAll(
    ): Promise<UsersResponse> {
        let users = await this.userService.findAll().catch(e => { throw new   NotAcceptableException(e) })
        let rsp =new UsersResponse() 
        rsp.users = users
        return rsp
    }

有效,但我必须将数据库查询结果显式分配给响应用户成员。 有没有更好的办法?非常感谢

这里是实际响应和想要的结果,以便更好地解释。

此方法的结果

{
  "users": [
    {
      "id": 1,
      "username": "a"
    },
    {
      "id": 2,
      "username": "bbbbbb"
    }
  ]
}

想要结果

{
    {
      "id": 1,
      "username": "a"
    },
    {
      "id": 2,
      "username": "bbbbbb"
    }
}

我建议直接将 @Exclude 装饰器放在实体 class User 上,而不是复制 UserResponse 中的属性。以下答案假定您已经这样做了。


平坦响应

如果你看一下 ClassSerializerInterceptor 的代码,你会发现它自动处理数组:

return isArray
  ? (response as PlainLiteralObject[]).map(item =>
      this.transformToPlain(item, options),
    )
  : this.transformToPlain(response, options);

但是,它只会转换它们,如果你直接 return 数组,所以 return users 而不是 return {users: users}:

@UseInterceptors(ClassSerializerInterceptor)
@Get('all')
async findAll(): Promise<User> {
    return this.userService.findAll()
}

嵌套响应

如果您需要嵌套响应,那么您的方式是一个很好的解决方案。 或者,您可以直接调用 class-transformer 的 serialize 而不是使用 ClassSerializerInterceptor。它还自动处理数组:

import { serialize } from 'class-transformer';

@Get('all')
async findAll(): Promise<UsersResponse> {
  const users: User[] = await this.userService.findAll();
  return {users: serialize(users)};
}

哇,多简单啊,如果我知道!完美,这解决了我的问题。另外,您对使用 class-transformer @Exclue() 装饰器的用户实体的建议。

而且我知道在这个用例中我不需要自定义 UsersResponse class。 这个解决方案正是我一直在寻找的,但我跳过了这个非常简单的方法

非常感谢您的超级快速回答和问题解决方案。

罗斯托克向柏林问好:)

这是我的最终方法:

控制器

@UseInterceptors(ClassSerializerInterceptor)
@Get('all')
async findAll(
): Promise<User> {
    return await this.userService.findAll().catch(e => { throw new NotAcceptableException(e) })
}

用户实体

import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn, OneToMany } from 'typeorm';
import { Profile } from './profile.entity';
import { Photo } from './photo.entity';
import { Album } from './album.entity';
import { Exclude } from 'class-transformer';

export type UserRoleType = "admin" | "editor" | "ghost";

@Entity()
export class User {
    @PrimaryGeneratedColumn() id: number;
    @Column('text')
    username: string;

    @Exclude()
    @Column('text')
    password: string;

    @Column({
        type: "enum",
        enum: ["admin", "editor", "ghost"],
        default: "ghost"
    })
    roles: UserRoleType;

    @Exclude()
    @Column({ nullable: true })
    profileId: number;

    @OneToMany(type => Photo, photo => photo.user)
    photos: Photo[];

    @OneToMany(type => Album, albums => albums.user)
    albums: Album[];

    @OneToOne(type => Profile, profile => profile.user)
    @JoinColumn()
    profile: Profile;
}

响应结果

[
  {
    "id": 1,
    "username": "a",
    "roles": "admin"
  },
  {
    "id": 2,
    "username": "bbbbbb",
    "roles": "ghost"
  }
]

我有其他方法可以解决您的问题。 您可以从 Controller 中删除 @UseInterceptors(ClassSerializerInterceptor)。而是使用 serializedeserialize 函数。

import { serialize, deserialize } from 'class-transformer';
import { User } from './users.entity';

@Get('all')
async findAll() {
  const users = serialize(await this.userService.findAll());
  return {
     status: 200,
     message: 'ok',
     users: deserialize(User, users)
  };
}

它对单个数据也有效

import { Param } from '@nestjs/common';    
import { serialize, deserialize } from 'class-transformer';
import { User } from './users.entity';

@Get(':id')
async findById(@Param('id') id: number) {
  const user = serialize(await this.userService.findById(id));
  return {
    status: 200,
    message: 'ok',
    user: deserialize(User, user)
  };
}

你的方法是nestjs推荐的,但是有问题。您将某些属性排除在外,以免暴露给客户端。如果您在一个有管理员的项目中工作,而管理员想要查看有关用户或产品的所有数据怎么办。如果您排除实体中的字段,您的管理员也不会看到这些字段。相反,让实体保持原样,并为每个控制器或每个请求处理程序编写 dto,在这个 dto 中只列出您想要公开的属性。

然后编写自定义拦截器并为 ecah 实体创建特定的 dto。例如,在您的示例中,您创建了一个 userDto:

import { Expose } from 'class-transformer';

// this is a serizalization dto
export class UserDto {
  @Expose()
  id: number;
  @Expose()
  roles: UserRoleType;
  @Expose()
  albums: Album[];
 // Basically you list what you wanna expose here
}

自定义拦截器有点乱:

import {
  UseInterceptors,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToClass } from 'class-transformer';

// Normally user entity goes into the interceptor and nestjs turns it into the JSON. But we we ill turn it to User DTO which will have all the serialization rules.then nest will take dto and turn it to the json and send it back as response


export class SerializerInterceptor implements NestInterceptor {
    // dto is the variable. so you can use this class for different entities
    constructor(private dto:any){

    }
  intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
   // you can write some code to run before request is handled
    return handler.handle().pipe(
      // data is the incoming user entity
      map((data: any) => {
        return plainToClass(this.dto, data, {
          //   this takes care of everything. this will expose things that are set in the UserDto
          excludeExtraneousValues: true,
        });
      }),
    );
  }
}

现在你在控制器中使用这个:

// See we passed UserDto. for different entities, we would just write a new dto for that entity and our custom interceptor would stay reusable
@UseInterceptors(new SerializerInterceptor(UserDto))
@Get('all')
    async findAll(
    ): Promise<UsersResponse> {
        let users = await this.userService.findAll().catch(e => { throw new   NotAcceptableException(e) })
        let rsp =new UsersResponse() 
        rsp.users = users
        return rsp
    }