Search code examples
mikro-orm

Mikro-ORM nested collection type safe access


I am unable to access properties of a nested loaded entities. I have the following structure

Treatment OneToMany TreatmentCycle

TreatmentCycle ManyToOne Physician

Physician ManyToOne User

User ManyToOne Country

I need to map this to another view model for presenting but I am unable to map treatmentCycle->physician->user properties

Code:

const treatment = await this.em.findOne(Treatment, id, {
  populate: [
    "diagnosis",
    "physician",
    "physician.user",
    "country",
    "medication",
    "treatmentCycles",
    "treatmentCycles.physician",
    "treatmentCycles.physician.user",
    "treatmentCycles.physician.user.country",
    "treatmentCycles.physician.profession",
    "treatmentCycles.physician.hospital",
    "treatmentCycles.medicationRegimen",
    "treatmentInterruptions",
  ],
});

When trying to map here result the following TS2339: Property  user  does not exist on type  { id: string; } & Reference<Physician> although I loaded in the relation previously. Same error applies for the hospital and profession

this.treatmentCycles = treatment.treatmentCycles.getItems().map((cycle) => {
  return {
    physician: {
      id: cycle.physician.id,
      userId: cycle.physician.user.id,
      fullName: `${cycle.physician.user.firstName} ${cycle.physician.user.lastName}`,
      email: cycle.physician.user.email,
      profession: cycle.physician.profession.identifier,
      hospitalIdentifier: cycle.physician.hospital.identifier,
      country: {
        id: cycle.physician.user.country.id,
        code: cycle.physician.user.country.$.code,
        name: cycle.physician.user.country.name,
      },
    },
  };
});

Am I missing something here or is there another way to do it?

EDIT

Entity definition for Treatment

@Entity({
  tableName: "treatment",
  repository: () => TreatmentRepository,
})
export default class Treatment extends AggregateRoot<Treatment, "id"> {
  [EntityRepositoryType]?: TreatmentRepository;

  @ManyToOne(() => Country, { ref: true })
  country!: Ref<Country>;

  @Enum({
    items: () => AvailableTreatmentStatuses,
    array: false,
    default: [AvailableTreatmentStatuses.ONGOING],
  })
  status!: AvailableTreatmentStatuses;

  @Property({
    type: "date",
    nullable: false,
    fieldName: "start_date",
  })
  startDate!: Date;

  @Property({
    type: "date",
    nullable: true,
    default: null,
    fieldName: "end_date",
  })
  endDate: Date | null = null;

  @Enum({
    items: () => AvailableTreatmentEndReason,
    array: false,
    default: null,
    nullable: true,
    fieldName: "end_reason",
  })
  reasonEnded: AvailableTreatmentEndReason | null = null;

  @ManyToOne(() => Diagnosis, { ref: true })
  diagnosis!: Ref<Diagnosis>;

  @ManyToOne(() => Medication, { ref: true })
  medication!: Ref<Medication>;

  @ManyToOne(() => Physician, { ref: true })
  physician!: Ref<Physician>;

  @OneToMany(() => TreatmentCycle, (treatmentCycle) => treatmentCycle.treatment)
  treatmentCycles = new Collection<TreatmentCycle>(this);

  @OneToMany(
    () => TreatmentInterruption,
    (treatmentInterruption) => treatmentInterruption.treatment
  )
  treatmentInterruptions = new Collection<TreatmentInterruption>(this);

  @Property({ persist: false })
  prescriptionsCount!: number;

  @Property({ persist: false })
  interruptionsCount!: number;

  constructor(payload: {
    countryId: string;
    status: AvailableTreatmentStatuses;
    startDate: Date;
    endDate?: Date | null;
    endReason?: AvailableTreatmentEndReason | null;
    diagnosisId: string;
    medicationId: string;
    physicianId: string;
  }) {
    super();
    if (payload.endReason && !payload.endDate) {
      throw new UnableToCreateNewTreatmentException(
        "Cannot add ended treatment without an end date"
      );
    }

    if (!payload.endReason && payload.endDate) {
      throw new UnableToCreateNewTreatmentException(
        "Cannot add ended treatment without an end reason"
      );
    }

    this.status = payload.status;
    this.startDate = payload.startDate;
    this.endDate = payload.endDate;
    this.reasonEnded = payload.endReason;
    this.country = Reference.createFromPK(Country, payload.countryId);
    this.diagnosis = Reference.createFromPK(Diagnosis, payload.diagnosisId);
    this.medication = Reference.createFromPK(Medication, payload.medicationId);
    this.physician = Reference.createFromPK(Physician, payload.physicianId);
  }
}

Solution

  • Since you are using the Reference wrapper (Ref type), your relations are mapped to it instead of the underlying entity. This means that to access them, you need to use one of the ways to access the wrapped entity. There are several ways to do it:

    1. since you have populated the relation, you should have a type-safe getter under $ symbol
    -cycle.physician.user.firstName
    +cycle.physician.$.user.$.firstName
    
    1. alternatively, you can use Reference.load() which is async and ensures the relation is loaded
    -cycle.physician.user.firstName
    +(await (await cycle.physician.load()).user.load()).firstName
    
    1. when you know it's loaded but it's not properly typed (with the Loaded type), you can use Reference.unwrap() which is sync and only gives you the underlying entity without checking its loaded state.
    -cycle.physician.user.firstName
    +cycle.physician.unwrap().user.unwrap().firstName
    

    https://mikro-orm.io/docs/next/guide/type-safety#reference-wrapper