'IntersectionType doesn't work with class-transformer decorators _when_ bundled via esbuild, even when esbuild plugins are used
So this question is related to IntersectionType doesn't work with class-transformer. In there the solution was to upgrade from nestjs 7 to 8 (effectively bumps up @nestjs/swagger from 4.5.12-next.1 to 5.1.2). Alas, that's not the end of the story. What I am seeing is that the decorators from class-transformer are not inherited by the IntersectionType when bundled with esbuild (even with recommended plugins). But the class-validate decorators are inherited as expected.
To reproduce, clone https://github.com/adrian-grajdeanu/nestjs-issue0 Next I'm including and discussing code from above repo.
Here is a demo code:
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',
});
When running the above via ts-node, all is well:
$ pnpx ts-node cc.ts
In @Type
In parseOptionalBoolean: parsing 0 (string)
In parseOptionalBoolean: parsed false (boolean)
Frankenstein { count: 10, category: 'foo', flag: false }
$
Note that code inside @Type and @Transform is run and validation/transform succeeds.
But bundle this cc.ts with esbuild. Here is the bundling script:
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);
});
Run this
$ pnpx ts-node bundle.ts
$
and it produces c.js bundle file. If I run this one
$ 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
}
$
Note that code in @Type and @Transform decorators is not run and validation fails. The class is not transformed at all (I placed a shim in front of plainToClass function from class-transform, and inspected the before and after). But the validation fails, because the class-validate decorators are inherited by the IntersectionType.
It is a matter of IntersectionType! I have tried without it and all works as expected. I know there are issues with esbuild and decorators, and I have specifically used the esbuildDecorators plugin from @anatine/esbuild-decorators.
Here is my 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"]
}
and 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"
}
}
Am I missing something or is this finally a dead-end for this approach I took in the project?
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
