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: toClassOnly still expose the name in instanceToPlain #1324

Open
oldriverno1 opened this issue Aug 30, 2022 · 6 comments · May be fixed by #1701
Open

fix: toClassOnly still expose the name in instanceToPlain #1324

oldriverno1 opened this issue Aug 30, 2022 · 6 comments · May be fixed by #1701
Assignees
Labels
type: fix Issues describing a broken feature.

Comments

@oldriverno1
Copy link

I have a DTO class and I want some specific key to be covert with different name only in instanceToPlain operation

the dto class:

export class CreateEventDto {
  @Expose({ toClassOnly: true, name: 'type' }) 
  eventTypeId: number;
}

I can get it to work in plainToInstance
for example:

const obj = { type: 2 };
console.log(plainToInstance(CreateEventDto, obj));  // log CreateEventDto { eventTypeId: 2 }

but if I convert it again using instanceToPlain, the result is showing { type: 2 } instead of { eventTypeId: 2 }

const obj = { type: 2 };
const dto = plainToInstance(CreateEventDto, obj);
console.log(dto);  // log CreateEventDto { eventTypeId: 2 }
console.log(instanceToPlain(dto)); // log { type: 2 }, but I expect it to be { eventTypeId: 2 }

Is there anything I'm doing wrong? or any proper way to this kind of conversion?
Thanks for answering :)

@oldriverno1 oldriverno1 added the type: question Questions about the usage of the library. label Aug 30, 2022
@codyburrito
Copy link

Having this same problem. Is this only happening in newer versions? Is there a version to rollback to that this still works?

@diffy0712 diffy0712 self-assigned this May 9, 2024
@diffy0712 diffy0712 added type: fix Issues describing a broken feature. type: question Questions about the usage of the library. and removed type: question Questions about the usage of the library. type: fix Issues describing a broken feature. labels May 9, 2024
@Jake13f
Copy link

Jake13f commented May 14, 2024

Any updates on this? Still seems to be an issue. Is there a consensus on any workarounds for the time being?

@diffy0712
Copy link

This seems to be a bug and needs fix.

Until it is fixed, you could create another getter for toPlainOnly and exclude the current on toPlainOnly.

If I am correct it should work, but I will write an example workaround later.

@diffy0712
Copy link

For a quick (but not nice) fix you could do

export class CreateEventDto {
  @Expose()
  @Exclude( { toPlainOnly: true })
  type: number;

  @Expose()
  get eventTypeId() {
    return this.type;
  }
}

@Jake13f
Copy link

Jake13f commented May 14, 2024

@diffy0712 Thanks for the response and the temporary workaround. This will unfortunately be too verbose as I have a ton of exposed properties currently that are toClassOnly as I am importing data from another system and want it permanently in my format instead.

My Temporary Solution

Not sure if its relevant or if this is a correct solution but for my situation, I am using NestJS and the issue arises when I send data back to the client since its transforming my models to plain before sending. My temporary workaround that seems to be working is as follows. Posting incase it helps anyone else or you have any feedback for the time being on why I shouldn't do it this way haha. The super.transformToPlain(plainOrClass, options); call I believe is just a direct call to the instanceToPlain() method but wrapped by nestjs. https://github.com/nestjs/nest/blob/master/packages/common/serializer/class-serializer.interceptor.ts#L84

Edited
I noticed my temporary workaround wasn't working with nested objects so I implemented a recursive solution to hopefully fix all of those correctly as well.

import {
 ClassSerializerContextOptions,
 ClassSerializerInterceptor,
 NestInterceptor,
 PlainLiteralObject,
} from '@nestjs/common';

import { defaultMetadataStorage } from 'class-transformer/cjs/storage.js';
import type { MetadataStorage } from 'class-transformer/types/MetadataStorage';

/**
* This serializer is being used to fix an issue with the class-transformer library where
* instanceToPlain is not abiding by certain expose flags. This aims to remedy incorrectly keyed properties
* when specified as toClassOnly.
*
* Once the issue is resolved in the package, this should be able to be removed.
* @see https://github.com/typestack/class-transformer/issues/1324
*/
export class AppClassSerializerInterceptor
 extends ClassSerializerInterceptor
 implements NestInterceptor
{
 /** Load in the default local storage... I realize this isn't supposed to be public and used */
 private readonly metadata: MetadataStorage = defaultMetadataStorage;

 override transformToPlain(
   plainOrClass: any,
   options: ClassSerializerContextOptions,
 ): PlainLiteralObject {
   // Perform normal transform. This will have incorrect keys as it will have ignored the toClassOnly
   const result = super.transformToPlain(plainOrClass, options);

   // Recursively fix the result object and nested class objects
   this.fixExposedKeys(plainOrClass, result);

   return result;
 }

 /**
  * Traverse the resulting object and compare it with class instance version. Patch the result model recursively
  * checking nested objects.
  */
 private fixExposedKeys(plainOrClass: any, result: PlainLiteralObject) {
   // Get all the fields that are exposed and their options for the target class
   const exposed = this.metadata.getExposedMetadatas(plainOrClass.constructor);
   const toRevert = new Map<string, string>();

   // Cache this classes exposed properties to lookup
   for (const field of exposed) {
     if (
       field.options.toClassOnly &&
       field.options.name &&
       field.propertyName
     ) {
       toRevert.set(field.options.name, field.propertyName);
     }
   }

   for (const key in result) {
     const val = result[key];
     const correctKey = toRevert.get(key) ?? key;

     // Convert the keys back to the originals and delete the incorrectly converted ones.
     if (correctKey !== key) {
       result[correctKey] = val;
       delete result[key];
     }

     // We need to also fix nested models...
     if (val && typeof val === 'object') {
       this.fixExposedKeys(plainOrClass[correctKey], val);
     }
   }
 }
}

@diffy0712
Copy link

Thank you for sharing your solution.
I think it looks alright and a pretty nice workaround.

@diffy0712 diffy0712 added type: fix Issues describing a broken feature. and removed type: question Questions about the usage of the library. labels May 14, 2024
@diffy0712 diffy0712 changed the title question: toClassOnly still expose the name in instanceToPlain fix: toClassOnly still expose the name in instanceToPlain May 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: fix Issues describing a broken feature.
4 participants