typestack / class-transformer

Decorator-based transformation, serialization, and deserialization between objects and classes.
MIT License
6.66k stars 487 forks source link

fix: Transform `value` and `obj.key` are different for mongodb 4.x / mongoose 6.0 #879

Open hasezoey opened 2 years ago

hasezoey commented 2 years ago

Description

when using Transform with mongoose 6.0 (6.0.0-rc1) and / or mongodb 4.x.x, the value and obj.key are different values

(Note: in this case key is for _id which is mongoose.Types.ObjectId instance)

Minimal code-snippet showcasing the problem

// NodeJS: 16.6.1
// MongoDB: 4.2-bionic (Docker)
import { getModelForClass, prop } from "@typegoose/typegoose"; // @typegoose/typegoose@8.1.1
import * as mongoose from "mongoose"; // mongoose@6.0.0-rc1
import { classToPlain, Exclude, Expose, plainToClass, Transform } from "class-transformer"; // class-transformer@0.4.0

// re-implement base Document to allow class-transformer to serialize/deserialize its properties
// This class is needed, otherwise "_id" and "__v" would be excluded from the output
class DocumentCT {
  @Expose()
  // makes sure that when deserializing from a Mongoose Object, ObjectId is serialized into a string
  @Transform(
    (value: any) => {
      if ("value" in value) {
        console.log("value", value);
        if (value.value !== value.obj[value.key]) {
          console.log("value and obj.key are different!");
        }

        return value.value.toString(); // because "toString" is also a wrapper for "toHexString"
      }

      return "unknown value";
    },
    { toClassOnly: true }
  )
  public _id!: string;

  @Expose()
  public __v!: number;
}

@Exclude()
class Account extends DocumentCT {
  @prop()
  @Expose()
  public email?: string;

  @prop()
  @Expose({ groups: ["admin"] })
  public password?: string;
}

const AccountModel = getModelForClass(Account);

(async () => {
  await mongoose.connect(`mongodb://localhost:27017/`, { dbName: "verifyMASTER" });

  let id: string;
  // Init

  const { _id } = await AccountModel.create({
    email: "somebody@gmail.com",
    password: "secret",
  } as Account);
  // note here that _id is an ObjectId, hence the toString()
  // otherwise it will have the shape of : { _bsonType: 'ObjectId', id: ArrayBuffer }
  id = _id.toString();

  // first test

  console.log("saved", id);
  // lean return a Plain Old Javascript Object
  const pojo = await AccountModel.findById(id).orFail().lean().exec();
  console.log("pojo", pojo, pojo._id.toString() === id);
  // deserialize Plain Old Javascript Object into an instance of the Account class
  // serialize Account instance back to a Plain Old Javascript Object, applying class-transformer's magic
  const serialized = classToPlain(plainToClass(Account, pojo));
  console.log("ctp", serialized, serialized._id === id);
  // the reason for doing a transformation round-trip here
  // is that class-transformer can only works it magic on an instance of a class with its annotation
  console.log(serialized === {
    _id: id,
    __v: 0,
    email: "somebody@gmail.com",
  });

  await mongoose.disconnect();
})();
Output Note: some comments added, starting with `# ` ``` yarn run v1.22.10 $ ts-node ./src/test.ts saved 6116693687f80aa238917279 pojo { _id: new ObjectId("6116693687f80aa238917279"), email: 'somebody@gmail.com', password: 'secret', __v: 0 } true value { value: new ObjectId("6116693687f80aa23891727c"), # this is different key: '_id', obj: { _id: new ObjectId("6116693687f80aa238917279"), # with this email: 'somebody@gmail.com', password: 'secret', __v: 0 }, type: 0, options: { enableCircularCheck: false, enableImplicitConversion: false, excludeExtraneousValues: false, excludePrefixes: undefined, exposeDefaultValues: false, exposeUnsetFields: true, groups: undefined, ignoreDecorators: false, strategy: undefined, targetMaps: undefined, version: undefined } } value and obj.key are different! ctp { _id: '6116693687f80aa23891727c', __v: 0, email: 'somebody@gmail.com' } false false Done in 2.94s. ```

Reproduction Repository & Branch: https://github.com/typegoose/typegoose-testing/tree/verifyCTandM6Issue

Note: the example is taken from typegoose's tests and minified for reproduction (the linked branch is where typegoose is using mongoose 6.0)

Steps:

Expected behavior

Expected that value and obj.key to be the same property

Actual behavior

value and obj.key are different


PS: this might also just be a question, i just dont know what pre-transformations are applied, i just noticed that the value are different

iamchathu commented 5 months ago

Is there any fix for this?

hasezoey commented 5 months ago

this project has not been updated since the creation of this issue, so there is no "official" fix, typegoose uses a workaround for _id, which would need to be repeated for all ObjectIds and likely does not apply to all use-cases (or maybe there is some kind of global transform, i dont know)