NestJS 在 DTO 创建期间执行验证之前使用 ValidationPipe 转换属性

BSm*_*ith 13 validation dto class-validator nestjs class-transformer

我使用内置的 NestJS ValidationPipe 以及类验证器和类转换器来验证和清理入站 JSON 正文有效负载。我面临的一种情况是入站 JSON 对象中混合使用大小写属性名称。我想在新的 TypeScript NestJS API 中纠正这些属性并将其映射到标准的驼峰式模型,这样我就不会将遗留系统中不匹配的模式耦合到我们的新 API 和新标准,本质上是使用 @Transform DTO 作为应用程序其余部分的隔离机制。例如,入站 JSON 对象的属性:

"propertyone",
"PROPERTYTWO",
"PropertyThree"
Run Code Online (Sandbox Code Playgroud)

应该映射到

"propertyOne",
"propertyTwo",
"propertyThree"
Run Code Online (Sandbox Code Playgroud)

我想使用 @Transform 来完成此任务,但我认为我的方法不正确。我想知道是否需要编写自定义 ValidationPipe。这是我目前的方法。

控制器:

"propertyone",
"PROPERTYTWO",
"PropertyThree"
Run Code Online (Sandbox Code Playgroud)

测试我模型:

"propertyOne",
"propertyTwo",
"propertyThree"
Run Code Online (Sandbox Code Playgroud)

测试我请求Dto:

import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { TestMeRequestDto } from './testmerequest.dto';

@Controller('test')
export class TestController {
  constructor() {}

  @Post()
  @UsePipes(new ValidationPipe({ transform: true }))
  async get(@Body() testMeRequestDto: TestMeRequestDto): Promise<TestMeResponseDto> {
    const response = do something useful here... ;
    return response;
  }
}
Run Code Online (Sandbox Code Playgroud)

用于 POST 到控制器的示例有效负载:

import { IsNotEmpty } from 'class-validator';

export class TestMeModel {
  @IsNotEmpty()
  someTestProperty!: string;
}
Run Code Online (Sandbox Code Playgroud)

我遇到的问题:

  1. 变换似乎没有效果。类验证器告诉我每个属性都不能为空。例如,如果我将“propertyone”更改为“propertyOne”,则类验证器验证对于该属性来说很好,例如它会看到该值。其他两个属性也是如此。如果我对它们进行驼峰式封装,那么类验证器就会很高兴。这是验证发生之前转换未运行的症状吗?
  2. 这个很奇怪。当我调试和评估 TestMeRequestDto 对象时,我可以看到 simpleModel 属性包含一个包含属性名称“sometestproperty”的对象,即使 TestMeModel 的类定义具有驼峰式“someTestProperty”。为什么 @Type(() => TestMeModel) 不尊重该属性名称的正确大小写?“test4”的值存在于该属性中,因此它知道如何理解该值并分配它。
  3. 还是很奇怪,TestMeModel 上“someTestProperty”属性的 @IsNotEmpty() 验证没有失败,例如,它看到“test4”值并且满意,即使示例 JSON 负载中的入站属性名称是“sometestproperty” ,全部小写。

来自社区的任何见解和指导将不胜感激。谢谢!

Jay*_*iel 12

您可能需要使用类转换器文档的高级用法部分。本质上,你@Transform()需要看起来这样:

import { IsNotEmpty, ValidateNested } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { TestMeModel } from './testme.model';

export class TestMeRequestDto {
  @IsNotEmpty()
  @Transform((value, obj) => obj.propertyone.valueOf())
  propertyOne!: string;

  @IsNotEmpty()
  @Transform((value, obj) => obj.PROPERTYTWO.valueOf())
  propertyTwo!: string;

  @IsNotEmpty()
  @Transform((value, obj) => obj.PropertyThree.valueOf())
  propertyThree!: string;

  @ValidateNested({ each: true })
  @Type(() => TestMeModel)
  simpleModel!: TestMeModel

}
Run Code Online (Sandbox Code Playgroud)

这应该需要传入的有效负载

{
  "propertyone": "value1",
  "PROPERTYTWO": "value2",
  "PropertyThree": "value3",
}
Run Code Online (Sandbox Code Playgroud)

并把它变成你想象的DTO。

编辑 2020 年 12 月 30 日

所以我最初的使用想法并@Transform()没有像预想的那样工作,这真是令人失望,因为它看起来很好。所以你可以做的并不是那么干,但它仍然可以与类转换器一起使用,这是一个胜利。通过使用@Exclude()@Expose()您可以使用属性访问器作为奇怪的命名属性的别名,如下所示:

class CorrectedDTO {
  @Expose()
  get propertyOne() {
    return this.propertyONE;
  }
  @Expose()
  get propertyTwo(): string {
    return this.PROPERTYTWO;
  }
  @Expose()
  get propertyThree(): string {
    return this.PrOpErTyThReE;
  }
  @Exclude({ toPlainOnly: true })
  propertyONE: string;
  @Exclude({ toPlainOnly: true })
  PROPERTYTWO: string;
  @Exclude({ toPlainOnly: true })
  PrOpErTyThReE: string;
}
Run Code Online (Sandbox Code Playgroud)

现在,您可以访问dto.propertyOne并获取预期的属性,并且当您这样做时,classToPlain它将删除propertyONE和其他属性(如果您使用 Nest 的序列化拦截器。否则,在辅助管道中,您可能plainToClass(NewDTO, classToPlain(value))只有NewDTO更正的字段)。

您可能想要研究的另一件事是自动映射器,看看它是否具有更好的功能来完成类似的事情。

如果你有兴趣,这是我用来测试这个的 StackBlitz


eol*_*eol 11

作为 Jay 优秀答案的替代方案,您还可以创建一个自定义管道,在其中保留将请求负载映射/转换为所需 DTO 的逻辑。它可以像这样简单:

export class RequestConverterPipe implements PipeTransform{
  transform(body: any, metadata: ArgumentMetadata): TestMeRequestDto {
    const result = new TestMeRequestDto();
    // can of course contain more sophisticated mapping logic
    result.propertyOne = body.propertyone;
    result.propertyTwo = body.PROPERTYTWO;
    result.propertyThree = body.PropertyThree;
    return result;
  }

export class TestMeRequestDto {
  @IsNotEmpty()
  propertyOne: string;
  @IsNotEmpty()
  propertyTwo: string;
  @IsNotEmpty()
  propertyThree: string;
}
Run Code Online (Sandbox Code Playgroud)

然后,您可以在控制器中像这样使用它(但您需要确保顺序正确,即必须RequestConverterPipe在 之前运行ValidationPipe,这也意味着ValidationPipe不能全局设置):

@UsePipes(new RequestConverterPipe(), new ValidationPipe())
async post(@Body() requestDto: TestMeRequestDto): Promise<TestMeResponseDto> {
  // ...
}
Run Code Online (Sandbox Code Playgroud)

  • 这是另一个好方法。需要注意的是,您必须在“ValidationPipe”之前运行“RequestConverterPipe”,这意味着“ValidationPipe”无法全局设置。 (2认同)
  • 我将您的回复标记为答案,因为这似乎是最简单的前进道路。虽然 DTO 上的排除和暴露装饰器可以工作,但我觉得我给 DTO 添加了额外的重量,并且希望尽可能多地保留 DTO 之外的逻辑。我希望在架构中保持清晰分离的线路,让 DTO 尽可能精简以履行其作为传输实体的角色,不执行转换,并将转换操作留在实现 PipeTransform 的类中。 (2认同)