Skip to content

fix: Transform Metadata is lost when importing class via dependency #1518

Open
@whgoss

Description

@whgoss

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.

Screenshot 2023-04-28 at 9 20 46 AM

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).

Screenshot 2023-04-28 at 10 05 46 AM

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: needs triageIssues which needs to be reproduced to be verified report.type: fixIssues describing a broken feature.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions