2

所以这个问题与IntersectionType doesn't work with class-transformer有关。在那里,解决方案是从 nestjs 7 升级到 8(有效地将 @nestjs/swagger 从 4.5.12-next.1 升级到 5.1.2)。唉,这不是故事的结局。我所看到的是,当与 esbuild 捆绑时(即使使用推荐的插件),来自 class-transformer 的装饰器不会被 IntersectionType 继承。但是 class-validate 装饰器是按预期继承的。

要重现,请克隆https://github.com/adrian-grajdeanu/nestjs-issue0 接下来我将包含并讨论来自上述 repo 的代码。

这是一个演示代码:

import "reflect-metadata"
import { ValidationError, BadRequestException, ValidationPipe } from '@nestjs/common';
import { Transform, Type } from "class-transformer";
import { IsBoolean, IsInt, IsString, Max, MaxLength, Min } from "class-validator";
import { IntersectionType } from '@nestjs/mapped-types';

const optionalBooleanMapper = new Map([
  ['1', true],
  ['0', false],
]);
function parseOptionalBoolean({ value }: any) {
  console.log(`In parseOptionalBoolean: parsing ${value} (${typeof value})`);
  const bValue = optionalBooleanMapper.get(value.toLowerCase());
  console.log(`In parseOptionalBoolean: parsed ${bValue} (${typeof bValue})`);
  return bValue === undefined ? value : bValue;
}

export class Props {
  @IsInt()
  @Type(() => {
    console.log('In @Type');
    return Number;
  })
  @Min(1)
  @Max(1024)
  count?: number;

  @IsBoolean()
  @Transform(parseOptionalBoolean)
  flag?: boolean;
}
export class Category {
  @IsString()
  @MaxLength(1024)
  category!: String;
}

class Frankenstein extends IntersectionType(Category, Props) {}

const pipe = new ValidationPipe({
  transform: true,
  whitelist: true,
  forbidNonWhitelisted: true,
  exceptionFactory: (errors: ValidationError[]) => new BadRequestException({
    message: 'Validation failed',
    payload: errors.map(error => Object.values(error.constraints || {})).reduce((prev, curr) => [...prev, ...curr], []),
  }),
});


async function validate(arg: any) {
  try {
    const obj = await pipe.transform(arg, {
      type: 'query',
      metatype: Frankenstein,
    });
    console.log(obj);
  } catch (e) {
    console.log(e);
  }
}
validate({
  count: '10',
  category: 'foo',
  flag: '0',
});

通过 ts-node 运行上述程序时,一切正常:

$ pnpx ts-node cc.ts
In @Type
In parseOptionalBoolean: parsing 0 (string)
In parseOptionalBoolean: parsed false (boolean)
Frankenstein { count: 10, category: 'foo', flag: false }
$

请注意,@Type 和 @Transform 内的代码已运行并且验证/转换成功。

但是将此 cc.ts 与 esbuild 捆绑在一起。这是捆绑脚本:

import { build } from 'esbuild';
import { esbuildDecorators } from '@anatine/esbuild-decorators';

build({
  "plugins": [
    esbuildDecorators({
      tsconfig: './tsconfig.json',
      cwd: process.cwd(),
    })
  ],
  "entryPoints": [
    "cc.ts"
  ],
  "external": [
    "@nestjs/microservices",
    "@nestjs/websockets",
    "class-transformer/storage"
  ],
  "minify": true,
  "bundle": true,
  "target": "node14",
  "platform": "node",
  "mainFields": [
    "module",
    "main"
  ],
  "outfile": "c.js"
})
.catch((e) => {
  console.error('Failed to bundle');
  console.error(e);
  process.exit(1);
});

运行这个

$ pnpx ts-node bundle.ts
$

它会生成 c.js 捆绑文件。如果我运行这个

$ pnpx node c.js
uA: Validation failed
    at d2.exceptionFactory (.../c.js:17:199649)
    at d2.transform (.../c.js:17:177741)
    at async M1e (.../c.js:17:199822) {
  response: {
    message: 'Validation failed',
    payload: [
      'count must not be greater than 1024',
      'count must not be less than 1',
      'count must be an integer number',
      'flag must be a boolean value'
    ]
  },
  status: 400
}
$

请注意,@Type 和 @Transform 装饰器中的代码未运行并且验证失败。该类根本没有转换(我在类转换中的 plainToClass 函数前面放置了一个垫片,并检查了之前和之后)。但是验证失败,因为类验证装饰器是由 IntersectionType 继承的。

这是 IntersectionType 的问题!我试过没有它,一切都按预期工作。我知道 esbuild 和装饰器存在问题,我专门使用了来自 @anatine/esbuild-decorators 的 esbuildDecorators 插件。

这是我的 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2018",
    "module": "commonjs",
    "lib": [
      "es2018"
    ],
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedParameters": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "typeRoots": [
      "./node_modules/@types"
    ],
    "moduleResolution": "node",
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "exclude": [
    "node_modules"
  ],
  "include": ["bin/**/*.ts", "lib/**/*.ts"]
}

和 package.json

{
  "name": "main",
  "version": "0.1.0",
  "bin": {
    "main": "bin/main.js"
  },
  "devDependencies": {
    "@anatine/esbuild-decorators": "^0.2.18",
    "@types/node": "10.17.27",
    "esbuild": "^0.14.21",
    "ts-node": "^9.1.1",
    "typescript": "^4.5.5"
  },
  "dependencies": {
    "@nestjs/common": "^8.2.6",
    "@nestjs/core": "^8.2.6",
    "@nestjs/mapped-types": "^1.0.1",
    "@nestjs/platform-express": "^8.2.6",
    "@nestjs/swagger": "^5.2.0",
    "ajv": "^8.8.2",
    "cache-manager": "^3.4.1",
    "class-transformer": "^0.5.1",
    "class-validator": "^0.13.2",
    "express": "^4.17.2",
    "reflect-metadata": "^0.1.13",
    "regenerator-runtime": "^0.13.9",
    "rxjs": "^7.5.2",
    "source-map-support": "^0.5.21",
    "swagger-ui-express": "^4.3.0"
  }
}

我是否遗漏了什么,或者这最终是我在项目中采用的这种方法的死胡同?

4

0 回答 0