typescriptpostgresqlnestjsrelationshiptypeorm

NestJS TypeORM How to have a ManyToOne relationship member which type is conditional?


I have an entity Message that has a sender (User) and a receiver, but the receiver can either be a User or a Channel (both entities have the messagesIn member), and I want to implement this using TypeORM relationships.

Would It be possible to do something like this with TypeORM (Postgres) / NestJS (or is there any conventional other way to achieve the same goal) ?

// message.entity.ts

@Entity()
export class Message {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  content: string;

  @ManyToOne(() => User, (snd) => snd.messagesOut)
  sender: User;

  // That line doesn't compile
  @ManyToOne(() => User | Channel, (rcv) => rcv.messagesIn)
  receiver: User | Channel;
};

Solution

  • What you're trying to do isn't possible because TypeORM needs to know exactly what entity you are trying to populate so then it can query in the right place.

    TypeORM has some mechanisms that allows you to create a single table that all the records, both from User and Channel, will be inserted in, see more here.

    But, if you want to have two dedicates table for each entity (aka Table Per Type Inheritance), I would suggest create another entity called Receiver that contains common properties, that are related to the messaging mechanism, between User and Channel, plus a property called type, that will define if the Receiver is a User or a Channel; and then reference the Receiver entity in each one of the two tables.

    export enum ReceiverType {
      USER = 'USER',
      CHANNEL = 'CHANNEL',
    }
    
    @Entity()
    export class Receiver {
      @PrimaryGeneratedColumn('uuid')
      id: string;
    
      // all common properties that you want related to messaging goes here
    
      @OneToMany(() => Message, (msg) => msg.receiver)
      messagesIn: Message[];
    
      @Column({
        type: "enum",
        enum: ReceiverType,
      })
      type: ReceiverType
    }
    
    @Entity()
    export class User {
      // user properties...
    
      @OneToOne(() => Receiver)
      @JoinColumn()
      receiver: Receiver
    }
    
    @Entity()
    export class Channel {
      // channel properties...
    
      @OneToOne(() => Receiver)
      @JoinColumn()
      receiver: Receiver
    }
    

    Then, in the Message entity, you will be able to reference Receiver:

    @Entity()
    export class Message {
      @PrimaryGeneratedColumn('uuid')
      id: string;
    
      @Column()
      content: string;
    
      @ManyToOne(() => User, (snd) => snd.messagesOut)
      sender: User;
    
      @ManyToOne(() => Receiver, (rcv) => rcv.messagesIn)
      receiver: Receiver;
    };
    

    So, when you get a Message entity through the repository, you will receive something like:

    {
      id: string;
      content: string;
      sender: { ... };
      receiver: {
        // common properties you defined...
        messagesIn: [ ... ],
        type: ReceiverType,
      }
    }
    

    With this information in hands, you will be able to access the data that you want and is necessary for the message sending flow, and, if you need some specific information that is only present in the User or Channel entity, you can get the type from Receiver and retrieve the respective entity by the receiver id, something like:

    if (message.receiver.type === ReceiverType.USER) {
      const user = await userRepository.findOne({
        where: {
          receiver: { id: message.receiver.id },
        },
      });
    
      ...
    }
    
    if (message.receiver.type === ReceiverType.CHANNEL) {
      const user = await channelRepository.findOne({
        where: {
          receiver: { id: message.receiver.id },
        },
      });
    
      ...
    }