Description
Description
This is a complicated setup, so bear with me.
I have two projects, Project A and Project B. Project A is a library released through GitHub that is then imported into Project B. Project A contains Javascript and TypeScript, and is transpiled into Javascript via Babel with the type information exposed alongside the transpiled Javascript via tsc --project tsconfig.types.json
(more on this later).
When trying to use the imported classes with the TypeStack @Transform()
decorator, the transform metadata does not appear to be coming across successfully from Project A to Project B. As you can see from the screenshot below, when running Project B the MetadataStorage
instance lists my imported class as a POJO instead of my class name (see entry 0 under _transformMetadatas
) and any associated properties are listed as undefined
. As a result, none of my @Transform()
decorators are working. Interestingly enough, all of my custom decorators in Project A are working fine in Project B, it's just transformations that appear to be broken.
Now, I have painstakingly followed all of the proposed solutions/workarounds related to this ticket #384 to ensure that my node_modules
structure is flattened. I've exposed class-transformer
, class-validator
, class-transformer-validator
and reflect-metadata
as peer dependencies in Project A and ensured the versions are the same in both projects. I've verified that those dependencies only exist a single time in my node_modules
directory in Project B and even pointed to the ./node_modules/class-transformer
in my tsconfig.json
(see files below). I also added import 'reflect-metadata';
as the first line of code in Project B.
My suspicion is that MetadataStorage
is using the transpiled Javascript object and disregarding any information in the corresponding .d.ts
file.
Project A tsconfig.json
{
"compilerOptions": {
"allowJs": true,
// Don't emit; allow Babel to transform files.
"noEmit": true,
"pretty": true,
// Disallow features that require cross-file information for emit.
"isolatedModules": true,
// Import non-ES modules as default imports.
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"outDir": "lib"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "lib/**/*"]
}
Project A tsconfig.types.json
for emitting type information
{
"extends": "./tsconfig",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"declarationDir": "./lib/core",
"isolatedModules": true,
"noEmit": false,
"allowJs": false,
"emitDeclarationOnly": true,
"emitDecoratorMetadata": true,
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "lib/**/*"]
}
Project B tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"isolatedModules": true,
"outDir": "lib",
"baseUrl": "./",
"paths": {
"class-transformer": [
"./node_modules/class-transformer"
]
},
},
"include": ["src/**/*"],
"exclude": ["node_modules", "lib/**/*"]
}
Minimal code-snippet showcasing the problem
To show what's happening in my project, this is the class I'm trying to use that's in Project A and imported into Project B.
import {
Expose, plainToInstance, Transform, Type,
} from 'class-transformer';
import { ValidateNested } from 'class-validator';
import { BandwidthWebhook } from './BandwidthWebhook';
export class BandwidthWebhookApiRequest {
@Expose()
@ValidateNested({ each: true })
@Type(() => BandwidthWebhook)
@Transform(({ obj }) => plainToInstance(BandwidthWebhook, obj?.body))
webhooks: BandwidthWebhook[];
}
export class BandwidthWebhook {
@Expose()
@IsString()
time: string;
@Expose()
@IsString()
to: string;
@Expose()
@IsOptional()
@IsString()
from?: string;
@Expose()
@IsOptional()
@IsNumber()
errorCode: number;
@Expose()
@IsString()
description: string;
@Expose()
@IsOptional()
@IsString()
type?: BandwidthWebhookType;
@Expose()
@IsOptional()
message?: BandwidthWebhookMessage;
@Expose()
@IsOptional()
@IsEnumOrArrayOfEnums(BandwidthWebhookEventType)
eventType?: BandwidthWebhookEventType;
}
Here's the class I'm trying to transform into a BandwidthWebhookApiRequest
via transformAndValidate()
(using this wrapper package https://www.npmjs.com/package/class-transformer-validator):
export class ApiRequestArgs {
constructor(request: any) {
this.params = request.params;
this.query = request.query;
this.body = request.body;
this.user = request.user;
}
readonly params: any;
readonly query: any;
readonly body: any;
readonly user: any;
}
Expected behavior
I would expect this ApiRequestArgs
to be successfully transformed into a BandwidthWebhookApiRequest
, with the ApiRequestArgs.body
property being transformed into a BandwidthWebhook
and mapped to the BandwidthWebhookApiRequest.webhooks
property. Something like this:
{
webhooks: [
{
time: "2023-04-28T14:37:29.242233Z",
to: "redacted",
from: undefined,
errorCode: undefined,
description: "Incoming message received",
type: "message-received",
message: {
id: "redacted",
owner: "redacted",
applicationId: "redacted",
time: "2023-04-28T14:37:29.152933Z",
segmentCount: 1,
direction: "in",
to: [
"redacted",
],
from: "redacted",
text: "Test",
},
eventType: undefined,
},
],
}
Actual behavior
What happens is that, because the transform metadata is malformed, it doesn't know how to apply custom transformation I set in my code and I'm simply given an empty BandwidthWebhookApiRequest
object.
After stepping through the class transformer code, the transform()
function in TransformOperationExecutor.js
tries to map the ApiRequestArgs.body
property to BandwidthWebhookApiRequest.body
instead of the BandwidthWebhookApiRequest.webhooks
(see the screenshot below).