nartc / automapper-transformer-plugin

Typescript Transformer Plugin for @nartc/automapper

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Fix issue with inheritance

nartc opened this issue · comments

Hi @nartc, I was wondering if you had time to check this bug. Right now I'm experimenting something similar but thought that could also be worth mentioning.

The profiles I've implemented are:

@Profile()
export class BaseEntityProfile extends ProfileBase {
  constructor(mapper: AutoMapper) {
    super();
    mapper.createMap(BaseEntity, ResponseBaseEntity);
  }
}
@Profile()
export class UserProfile extends ProfileBase {
  constructor(mapper: AutoMapper) {
    super();
    mapper.createMap(User, ResponseUserDto, { includeBase: [BaseEntity, ResponseBaseEntity] });
  }
}
@Profile()
export class CampusUserProfile extends ProfileBase {
  constructor(mapper: AutoMapper) {
    super();
    mapper.createMap(CampusUser, ResponseCampusUserDto, { includeBase: [User, ResponseUserDto] });
  }
}

Everything works as expected when retrieving a user, in which only the first two profiles are used. However, when I try to use the third profile and map a class with a double inheritance, the properties from the first one are not mapped. So, to exemplify:

Retrieving a user:

{
    "data": {
        "id": "4bd4c8c4-1935-4e3a-8f89-fcb85a94a0c6", <- Extra property from BaseEntity
        "display_name": "John Doe",
        "email": "john_doe@gmail.com",
        "email_verified": false,
        "photo_url": null
    }
}

Retrieving a campus user:

{
    "data": {
        "display_name": "John Doe",
        "email": "john_doe@gmail.com",
        "email_verified": false,
        "photo_url": null,
        "campus": null <- Extra property from CampusUser
    }
}

Despite adding the @AutoMap() decorator to the id property as we discussed in the other issue, only when there's one level of inheritance seems to work.

The weird part is that all other properties from User class (displayName, email, emailVerified and photoURL) are mapped. None of them were annotated with the @AutoMap decorator (DTO and entity class).

So, the problem with the double inheritance could not be related to the plugin itself, maybe is more appropiate to open an issue in the core library. But I thought that the behaviour I mentioned with the decorator that seems to work sometimes is worth mentioning here.

Would you mind sending me the complete models + profiles for these 3 (simpler models are fine, doesn't have to be exact)

Here are the entities and dtos:

export class BaseEntity {
  @AutoMap()
  @PrimaryGeneratedColumn('uuid')
  readonly id!: string;

  @CreateDateColumn({ name: 'create_date', update: false })
  readonly createDate!: Date;

  @UpdateDateColumn({ name: 'update_date' })
  readonly updateDate!: Date;

  @DeleteDateColumn({ name: 'delete_date' })
  readonly deleteDate?: Date;

  @VersionColumn()
  readonly version!: number;

  constructor(partial: Partial<BaseEntity>) {
    Object.assign(this, partial);
  }
}
export class ResponseBaseEntity {
  @AutoMap()
  @Expose({ groups: [UserRole.ADMIN] })
  id!: string;

  constructor(partial: Partial<ResponseBaseEntity>) {
    Object.assign(this, partial);
  }
}
@Entity('users')
@TableInheritance({ column: { type: 'enum', enum: UserType, name: 'type' } })
export class User extends BaseEntity {
  displayName!: string;

  email!: string;

  emailVerified!: boolean;

  photoURL!: string;

  constructor(partial: Partial<User>) {
    super();
    Object.assign(this, partial);
  }
}
@Exclude()
export class ResponseUserDto extends ResponseBaseEntity {
  @Expose({ name: 'display_name', groups: [UserRole.ADMIN] })
  displayName!: string;

  @Expose({ groups: [UserRole.ADMIN] })
  @ApiProperty({ description: `User's email` })
  email!: string;

  @Expose({ name: 'email_verified', groups: [UserRole.ADMIN] })
  emailVerified!: boolean;

  @Expose({ name: 'photo_url', groups: [UserRole.ADMIN] })
  photoURL!: string;

  constructor(partial: Partial<ResponseUserDto>) {
    super();
    Object.assign(this, partial);
  }
}
@ChildEntity()
export class CampusUser extends User {
  @AutoMap(() => Campus)
  @ManyToOne(() => Campus, (campus) => campus.campusUsers) //* Could be null for the other child entities
  @JoinColumn({ name: 'campus_id' })
  readonly campus!: Campus;

  constructor(partial: Partial<CampusUser>) {
    super(partial);
    Object.assign(this, partial);
  }
}
@Exclude()
export class ResponseCampusUserDto extends ResponseUserDto {
  @Expose({ groups: [UserRole.ADMIN] })
  @AutoMap(() => ResponseCampusDto)
  campus!: ResponseCampusDto;

  constructor(partial: Partial<ResponseCampusUserDto>) {
    super(partial);
    Object.assign(this, partial);
  }
}

So, in summary, there you have all the entities, dtos and profiles in use. The @Exclude and @Expose shouldn't be bothering at all because, as I previously commented, the id property is mapped when a user is retrieved.

Update: again, adding @AutoMap to both entity and DTO (User and ResponseUserDto) seems to do the trick. Adding the decorator in one and not the other has strange behaviours, resulting in mapping properties in some situations and not in others.

@manuelnucci I just implemented a fix for inheritBaseMapping() but I can't seem to reproduce your case (where id does not exist in CampusUser). Can you please provide a runnable example? I tried running a test case based on your models and it does work after the fix. And after diving in the code, the problem does seem to be in the core library. But let's keep the conversation here.

@nartc I'll try tomorrow to build you a simple repo and reproduce the problem. Busy day today :)

Hi @nartc, sorry for the delay... Here's the repo. As you'll see, only when @AutoMap decorators are uncommented both in entities and dtos the mapping works as expected.

Let me know if you need anything else to fix this issue!

@manuelnucci try the latest version of nestjsx-automapper

@nartc I just tried the latest version in my own project. At first seemed to worked, but then I discovered that only goes one level up in the inheritance. Strangely, that particular behaviour that I described doesn't happen in the repo that I prepared for you, there you have to annotate both classes (BaseEntity and User).

So, all things considered, in my current project I have to annotate all the entities and dtos that go +1 level up in the inheritance.

I checked the changes you did to both libraries. The inheritance tests in the mapper library are correct, but without the plugin all the properties must be annotated and my error won't appear. Then, in the plugin library, the test that you ran used only one level of inheritance. Maybe if you run the test with two or more levels it will fail? I think here is where I'm right now.

@manuelnucci Sorry that you ran into such issue. Let's discuss here

Assume I have the following entities and dtos:

export class BaseEntity {
  id!: string;
}

export class User extends BaseEntity {
  name!: string;
}

export class CampusUser extends User {
  campusId!: string;
}

export class ResponseBaseEntity {
  id!: string;
}

export class ResponseUserDto extends ResponseBaseEntity {
  name!: string;
}

export class ResponseCampusUserDto extends ResponseUserDto {
  campusId!: string;
}

and the following profiles:

export class BaseEntityProfile extends ProfileBase {
  constructor(mapper: AutoMapper) {
    super();
    mapper.createMap(BaseEntity, ResponseBaseEntity);
  }
}

export class InheritanceUserProfile extends ProfileBase {
  constructor(mapper: AutoMapper) {
    super();
    mapper.createMap(User, ResponseUserDto, {
      includeBase: [BaseEntity, ResponseBaseEntity],
    });
  }
}

export class InheritanceCampusUserProfile extends ProfileBase {
  constructor(mapper: AutoMapper) {
    super();
    mapper.createMap(CampusUser, ResponseCampusUserDto, {
      includeBase: [User, ResponseUserDto],
    });
  }
}

The plugin will scan the entities and dtos and generate the static methods like following:

export class BaseEntity {
  id!: string;

  static __NARTC_AUTOMAPPER_METADATA_FACTORY() {
    return {
      id: () => String,
    };
  }
}

// @ts-ignore
export class User extends BaseEntity {
  name!: string;

  static __NARTC_AUTOMAPPER_METADATA_FACTORY() {
    return {
      name: () => String,
    };
  }
}

// @ts-ignore
export class CampusUser extends User {
  campusId!: string;

  static __NARTC_AUTOMAPPER_METADATA_FACTORY() {
    return {
      campusId: () => String,
    };
  }
}

export class ResponseBaseEntity {
  id!: string;

  static __NARTC_AUTOMAPPER_METADATA_FACTORY() {
    return {
      id: () => String,
    };
  }
}

// @ts-ignore
export class ResponseUserDto extends ResponseBaseEntity {
  name!: string;

  static __NARTC_AUTOMAPPER_METADATA_FACTORY() {
    return {
      name: () => String,
    };
  }
}

// @ts-ignore
export class ResponseCampusUserDto extends ResponseUserDto {
  campusId!: string;

  static __NARTC_AUTOMAPPER_METADATA_FACTORY() {
    return {
      campusId: () => String,
    };
  }
}

As you can see, the plugin does not really care about the inheritance at all. includeBase is what is going to handle the inheritances.

image

My test does run against the plugin generated classes and it will map all level of inheritances. Now to further debug your issue, I'd love to schedule a video call with you if you'd like. You can reach out to me via email at nartc7789@gmail.com

I figured that the problem would be something trivial based on the situation. Luckily, I've found it! It's the order in which the profiles are imported. More specifically, the profiles which make use of includeBase.

I was importing the profiles near the module that uses them based on your answer in SO and, therefore, depending in the order that NestJS loads the modules. For those profiles that deal with inheritance, the parent profiles must be imported before its corresponding childs.

So, yeah, everything works as expected now.

Thank you for your patience @nartc, it was really appreciated! And hopefully this comment will help others dealing with the same issue!!