Search code examples
postgresqltypescriptmany-to-manysequelize.js

sequelize-typescript many-to-many relationship model data with


I am using sequelize with sequelize-typescript library, and am trying to achieve the following relationship:

Team.ts

@Scopes({
  withPlayers: {
    include: [{model: () => User}]
  }
})
@Table
export default class Team extends Model<Team> {

  @AllowNull(false)
  @Column
  name: string;

  @BelongsToMany(() => User, () => TeamPlayer)
  players: User[];
}

User.ts

@Scopes({
  withTeams: {
    include: [{model: () => Team, include: [ () => User ]}]
  }
})
@Table
export default class User extends Model<User> {

  @AllowNull(false)
  @Column
  firstName: string;

  @AllowNull(false)
  @Column
  lastName: string;

  @BelongsToMany(() => Team, () => TeamPlayer)
  teams: Team[];
}

TeamPlayer.ts

@DefaultScope({
  include: [() => Team, () => User],
  attributes: ['number']
})
@Table
export default class TeamPlayer extends Model<TeamPlayer> {

  @ForeignKey(() => User)
  @Column
  userId: number;

  @ForeignKey(() => Team)
  @Column
  teamId: number;

  @Unique
  @Column
  number: number;
}

Now when querying for player, you get the object with the following data:

{
  "id": 1,
  "name": "Doe's Team",
  "players": [
    {
      "id": 1,
      "firstName": "John",
      "lastName": "Doe",
      "TeamPlayer": {
        "userId": 1,
        "teamId": 1,
        "number": 32
    }
 }]
}

Now there are couple of things that I cannot get done..

1) I want to rename the TeamPlayer to something like "membership"; but not by changing the name of the class 2) the content of TeamPlayer should not have the id`s, but I want it to contain the data of the team, for example:

{
  "firstName": "John",
  "lastName": "Doe"
  "membership": {
     "number": 32
 }

In the above classes, I tried to set a scope to the TeamPlayer class to only include number inside the TeamMember inclusion, but no effect.

I used to have the TeamPlayer class have direct memberships to team and player, but that solution added redundant id to the TeamPlayer class, and also did not prevent duplicate memberships in the team. I could indeed manually (= in code) prevent duplicates in these situations, but that does not feel elegant.


Solution

  • I ended up solving this by adding one-to-many relationships from TeamPlayer to User and Team, and also figured out the way to make the teamId + userId pair unique by adding two more fields with @ForeignKey like this:

    TeamPlayer.ts

    @Table
    export default class TeamPlayer extends Model<TeamPlayer> {
    
      @BelongsTo(() => Team)
      team: Team;
    
      @ForeignKey(() => Team)
      @PrimaryKey
      @Column
      teamId: number;
    
      @BelongsTo(() => User)
      user: User;
    
      @ForeignKey(() => User)
      @PrimaryKey
      @Column
      userId: number;
    
      @Column
      number: number;
    }
    

    User.ts

    @Table
    export default class User extends Model<User> {
    
      @AllowNull(false)
      @Column
      firstName: string;
    
      @AllowNull(false)
      @Column
      lastName: string;
    
      @HasMany(() => TeamPlayer)
      teams: TeamPlayer[];
    }
    

    Team.ts

    export default class Team extends Model<Team> {
      @AllowNull(false)
      @Column
      name: string;
    
      @HasMany(() => TeamPlayer)
      players: TeamPlayer[];
    }
    

    This solution allows me to control the included query attributes via scopes, and gives me proper object output.