Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
whgoss opened this issue Apr 28, 2023 · 6 comments
Open

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

whgoss opened this issue Apr 28, 2023 · 6 comments
Labels
status: needs triage Issues which needs to be reproduced to be verified report. type: fix Issues describing a broken feature.

Comments

@whgoss
Copy link

whgoss commented Apr 28, 2023

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

@whgoss whgoss added status: needs triage Issues which needs to be reproduced to be verified report. type: fix Issues describing a broken feature. labels Apr 28, 2023
@kalani96746
Copy link

kalani96746 commented May 9, 2023

@whgoss Iʻm having this same issue...Iʻm not sure if theres a solution.

When I import from a real folder in the project it works..

..when I import the domain objects via paths..it doesnʻt..

    "paths": {
        "*": ["src/*"],
        "@domain/*": [
            "../../../../code/angular/src/app/domain/*.ts"
        ],
    },

This is a huge problem with how i want to use this...
I have scripts running using ts-node for dev-ops stuff... and front-end stuff in angular...
I donʻt want to keep two copies of the object.

It works top level...but when the object contains @type annotations and other..they are ignored if its from the defined path in tsconfig...but not if its directly in the src folder.

Iʻm wondering about symbolic links instead but this wouldnʻt really work for a real team environment..

@kalani96746
Copy link

iʻm further along....i need access to metadata storage somehow to attempt to fix it...
@whgoss what setup are you using? (like that debugger)
Iʻm running via ts-node command line and iʻm not able to see the debugger stuff you are.

@whgoss
Copy link
Author

whgoss commented Jun 5, 2023

@kalani96746 I'm using VS Code and am able to attach to my running process with that, enabling the breakpoints etc that are visible in the screenshots. As for the process itself, it's running in a Docker container but I'm sure there's a way to connect to a process outside Docker. Let me know if you need any more details.

@GNURub
Copy link

GNURub commented Aug 11, 2023

I am having the same issue, I have a package (common) with the entities of my application, in order to use them in the backend and in the frontend. When I use an entity imported from the package, the decorators of the entities are not working.

@nitinet
Copy link

nitinet commented Jan 21, 2024

I am facing the same issue where my common models are not transformed from plain objets. I checked and found that "defaultMetadataStorage" object is initialised as a separate object for every dependency. "_typeMetadatas" in this object is blank when main project is searching for metadata of the class

@diffy0712
Copy link

Hello,

it is indeed really hard to reproduce, just the setup would take up a lot of time.
So please if you need help on this share some example code(repo or something), which we could have look at.

Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: needs triage Issues which needs to be reproduced to be verified report. type: fix Issues describing a broken feature.
Development

No branches or pull requests

5 participants