【问题标题】:Cast entity to dto将实体转换为 dto
【发布时间】:2019-04-22 01:13:30
【问题描述】:

只是想知道将 NestJS 实体对象转换为 DTO 的最佳方法。

假设我有以下内容:

import { IsString, IsNumber, IsBoolean } from 'class-validator';
import { Exclude } from 'class-transformer';

export class PhotoSnippetDto {
  @IsNumber()
  readonly id: number;

  @IsString()
  readonly name: string;

  constructor(props) {
    Object.assign(this, props);
  }
}

export class Photo {

  @IsNumber()
  id: number;

  @IsString()
  name: string;

  @IsString()
  description: string;

  @IsString()
  filename: string;

  @IsNumber()
  views: number;

  @IsBoolean()
  isPublished: boolean;

  @Exclude()
  @IsString()
  excludedPropery: string;

  constructor(props) {
    Object.assign(this, props);
  }
}

@Controller()
export class AppController {

  @Get()
  @UseInterceptors(ClassSerializerInterceptor)
  root(): PhotoSnippetDto {
    const photo = new Photo({
      id: 1,
      name: 'Photo 1',
      description: 'Photo 1 description',
      filename: 'photo.png',
      views: 10,
      isPublished: true,
      excludedPropery: 'Im excluded'
    });

    return new PhotoSnippetDto(photo);
  }

}

我期待 ClassSerializerInterceptor 将照片对象序列化到 DTO 并返回如下内容:

{
  id: 1,
  name: 'Photo 1'
}

但我收到的响应仍然包含所有属性:

{
  id = 1,
  name = 'Photo 1',
  description = 'Photo 1 description',
  filename = 'file.png',
  views = 10,
  isPublished = true
}

我基本上想去掉 DTO 中未定义的所有属性。

我知道 ClassSerializerInterceptor 在使用 @Exclude() 时工作​​得很好,我只是也希望它也能删除未定义的属性。

我很好奇解决这个问题的最佳方法?我知道我可以这样做:

@Get('test')
@UseInterceptors(ClassSerializerInterceptor)
test(): PhotoSnippetDto {
  const photo = new Photo({
    id: 1,
    name: 'Photo 1',
    description: 'Photo 1 description',
    filename: 'photo.png',
    views: 10,
    isPublished: true,
    excludedPropery: 'Im excluded'
  });
  const { id, name } = photo;
  return new PhotoSnippetDto({id, name});
}

但是,如果我想在响应中添加另一个属性,我必须做的不仅仅是将新属性添加到类中。我想知道是否有更好的“嵌套方式”。

【问题讨论】:

    标签: node.js typescript nestjs


    【解决方案1】:

    一种可能的选择是使用 @Exclude@Expose 装饰器标记您的 DTO 对象,然后使用 plainToClass 进行转换:

    @Exclude()
    export class PhotoSnippetDto {
       @Expose()
       @IsNumber()
       readonly id: number;
    
       @Expose()
       @IsString()
       readonly name: string;
    }
    

    假设您已经按照上述方式进行了装饰,那么您可以这样做:const dto = plainToClass(PhotoSnippetDto, photo);

    生成的对象是您期望的形式,只有idname 出现在最终对象上。如果您决定稍后公开更多属性,您只需将它们添加到您的 DTO 并使用@Expose 标记它们。

    此方法还允许您从 DTO 中删除使用 Object.assign 的构造函数

    【讨论】:

    • 谢谢 Jesse,我没有意识到 @Exclude 可以在类范围内使用。这正是我所追求的!
    • @Lewsmith 没问题!很高兴我能帮忙。非常感谢分享拦截器的完整实现,这看起来是一个很好的解决方案!甚至可能会为未来的 Nest 应用窃取它:P
    • 为什么是plainToClass呢?我们没有从 typeorm 获取原始实体,在这种情况下 classToClass 不是更好吗?
    【解决方案2】:

    因此,基于 Jesse 的出色回答,我最终使用 @Exclude() 和 @Expose() 创建了 DTO,以删除所有未公开的属性:

    import { IsString, IsEmail } from 'class-validator';
    import { Exclude, Expose } from 'class-transformer';
    
    @Exclude()
    export class PhotoSnippetDto {
       @Expose()
       @IsNumber()
       readonly id: number;
    
       @Expose()
       @IsString()
       readonly name: string;
    }
    

    然后我创建了一个通用的转换拦截器,它调用 plainToclass 来转换对象:

    import { Injectable, NestInterceptor, ExecutionContext } from '@nestjs/common';
    import { Observable } from 'rxjs';
    import { map } from 'rxjs/operators';
    import { plainToClass } from 'class-transformer';
    
    interface ClassType<T> {
        new(): T;
    }
    
    @Injectable()
    export class TransformInterceptor<T> implements NestInterceptor<Partial<T>, T> {
    
        constructor(private readonly classType: ClassType<T>) {}
    
        intercept(context: ExecutionContext, call$: Observable<Partial<T>>, ): Observable<T> {
            return call$.pipe(map(data => plainToClass(this.classType, data)));
        }
    }
    

    然后使用这个拦截器将数据转换为任意类型:

    @Get('test')
    @UseInterceptors(new TransformInterceptor(PhotoSnippetDto))
    test(): PhotoSnippetDto {
      const photo = new Photo({
        id: 1,
        name: 'Photo 1',
        description: 'Photo 1 description',
        filename: 'photo.png',
        views: 10,
        isPublished: true,
        excludedPropery: 'Im excluded'
      });
      return photo;
    }
    

    这给了我想要的东西:

    {
      id: 1,
      name: 'Photo 1'
    }
    

    绝对感觉更像巢!我可以在任何需要的地方使用相同的拦截器,并更改我只需要更改 DTO 的响应。

    快乐的日子。

    【讨论】:

    • 嘿,我尝试了您的解决方案,但出现运行时错误:Error: Nest can't resolve dependencies of the TransformInterceptor (?). Please make sure that the argument Object at index [0] is available in the ****Module context.
    • 快速问题,当我们想要排除属性时这应该可以工作,但是如果我们需要 DTO 中没有实体中的其他属性怎么办。另外,如果我想改名怎么办?
    【解决方案3】:

    对于所有类型转换,我开发了一个库 metamorphosis-nestjs 来简化对象的转换。
    它为 NestJS 添加了一个缺失的概念,即为您的转换器提供的所有转换提供可注入的转换服务,并提前注册到转换服务中(如 Java 应用程序中的 Spring Framework 提供的转换服务)。

    所以在你的情况下:

    1. npm install --save @fabio.formosa/metamorphosis-nest
      
    2. import { MetamorphosisNestModule } from '@fabio.formosa/metamorphosis-nest';
      
      @Module({
        imports: [MetamorphosisModule.register()],
        ...
      }
      export class MyApp{ }
      
    3. import { Convert, Converter } from '@fabio.formosa/metamorphosis';
      
      @Injectable()
      @Convert(Photo, PhotoSnippetDto )
      export default class PhotoToPhotoSnippetDtoConverter implements Converter<Photo, 
      PhotoSnippetDto> {
      
      public convert(source: Photo): PhotoSnippetDto {
        const target = new PhotoSnippetDto();
        target.id = source.id;
        target.name = source.name;
        return target;
       }
      }
      

    最后你可以随意注入和使用conversionService:

     const photoSnippetDto = <PhotoSnippetDto> await this.convertionService.convert(photo, PhotoSnippetDto);
    

    关于这个问题的其他答案,您可以在您的转换方法中使用 plainToClass:

     @Injectable()
     @Convert(Photo, PhotoSnippetDto )
     export default class PhotoToPhotoSnippetDtoConverter implements Converter<Photo, PhotoSnippetDto> {
    
       public convert(source: Photo): PhotoSnippetDto {
         return plainToClass(PhotoSnippetDto, source);
       }
     }
    

    如果你愿意,你也可以在拦截器中调用conversionService。

    获取自述文件以了解 metamorphosis-nestjs 的示例和所有好处。

    【讨论】:

      猜你喜欢
      • 2016-06-10
      • 2019-05-01
      • 2021-12-15
      • 2018-09-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-04-26
      • 2014-07-20
      相关资源
      最近更新 更多