Sequelize-typescript: populating the connecting object in many-to-many relationship

Created on 17 Jul 2017  路  12Comments  路  Source: RobinBuschmann/sequelize-typescript

Hi!

I am not sure whether this is just missing feature, or I just don't understand enough of the library/sequelize, but i'll put this here anyway..

I have implemented a many-to-many relationship as described with ForeignKey's. However I am not sure how to control the query output of the connecting object. This is the object connecting Team and User, and having number as additional info:


@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;
}

When I query for eg. teams, I do get the following object out:

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

Things I want to improve here:
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,
   }
 }

Any ideas whether this is possible?

(edit: I've also posted a question about this in stackoverlow: https://stackoverflow.com/questions/45130037/sequelize-typescript-many-to-many-relationship-model-data-with)

question

Most helpful comment

Ah ok, this is because source can be null and $add expects a non-null value as second parameter. If findOne does not find any model, it resolves with null. So you need to check if source is truthy:

  Source
  .findOne<Source>()
  .then(source => {
    if(source) {
      contentItem.$add("Source", source);
    }
  })

All 12 comments

Hi @satellink sequelize does not seem to provide any functionality that is intent to really solve your first issue. But you could explicitly set modelName in the Table annotation to membership:

@Table({modelName: 'membership'})
export default class TeamPlayer extends Model<TeamPlayer> { ... }

This will result in what you want. So that you will get membership instead of TeamPlayer. As I already said, modelName was not intent to be used for that. But as far as I know, this should currently not lead to any issues (The name of the table will still be TeamPlayer unless you change it with tableName property).
But I will investigate this a little bit deeper - Probably there is something sequelize-typescript can do to finally solve this.

Regarding 2.:
When retrieving relational data by defining the include options, you can tell sequelize which attributes of the through table you want to present on your model as well:

Team.findAll({
  include: [{
    model: User,
    through: {
       attributes: ['number']
    }
  }]
})

I hope this helps.

Hi, thanks for the answer!

Regarding 1) - I actually remember to try to implement that with pure sequelize too, and it was not trivial. It would be cool if this could be somehow solved!

I ended up implementing the TeamPlayer as a class with its own identifier at this point, and test the uniqueness of the relationship in the code level. But I would love to see a solution with the foreign keys, as that would be much simpler.

thanks for the tip about the through attributes; I completely overlooked that feature.

You're welcome :)

Regarding:

I ended up implementing the TeamPlayer as a class with its own identifier at this point, and test the uniqueness of the relationship in the code level. But I would love to see a solution with the foreign keys, as that would be much simpler.

I don't really understand what you approach is. Can you provide an example?

This is what I do now:

team.ts

@Table
export default class Team extends Model<Team> {
  @AllowNull(false)
  @Column
  name: string;

  @HasMany(() => TeamPlayer, 'teamId')
  players: TeamPlayer[];
}

user.ts

@Table
export default class User extends Model<User> {

  @AllowNull(false)
  @Column
  firstName: string;

  @AllowNull(false)
  @Column
  lastName: string;

  @HasMany(() => TeamPlayer, 'userId')
  teams: TeamPlayer[];
}

team.player.ts

@Table
export default class TeamPlayer extends Model<TeamPlayer> {

  @BelongsTo(() => Team, 'teamId')
  team: Team;

  @BelongsTo(() => User, 'userId')
  user: User;

  @Column
  number: number;
}

And then in code I do simple check that the TeamPlayer with the same userId & teamId does not exist yet.

Why do you need to check this manually? You could define teamId and userId as primary keys of TeamPlayer.

Your answer made me play around a bit; I though that I cannot use @ForeignKey directly on the team and user parameters (which I've already tried), but realized that I can actually make it work like this:

  @BelongsTo(() => Team, 'teamId')
  team: Team;

  @PrimaryKey
  @Column
  teamId: number;

  @BelongsTo(() => User, 'userId')
  user: User;

  @PrimaryKey
  @Column
  userId: number;

..and if you add @ForeignKey to the userId and teamId properties, you can omit the foreign keys passed to @BelongsTo and @HasMany, like:

  @BelongsTo(() => Team)
  team: Team;

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

  @BelongsTo(() => User)
  user: User;

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

  /* ... */

  @HasMany(() => TeamPlayer)
  teams: TeamPlayer[];

  /* ... */

  @HasMany(() => TeamPlayer)
  players: TeamPlayer[];

Hi I'm having a similar issue. I have tried both defining my join model explicitly as in this thread and using decorators, as the example below. When trying to create a ContentItem and assign it a Source I get the same webpack compile error in both scenarios in the same line that involves the method $add.

Webpack Compile Error

error TS2345: Argument of type 'Source | null' is not assignable to parameter of 
type 'Model<Source | null> | Model<Source | null>[]'.
  Type 'null' is not assignable to type 'Model<Source | null> | Model<Source | null>[]'.

Add a source to a new content item

const contentItem = new ContentItem({
  title: requestBody.title,
  url: requestBody.url
});

return Promise.resolve(
  Source.findOne<Source>()
  .then(source => {
    contentItem.$add("Source", source);
    return contentItem;
  })
  .then(contentItem => contentItem.save())
  .catch(e => {
    console.log("ERR", e)
    return e
  })
)

Source Model

import {Column, Model, Table, Unique, BelongsToMany} from "sequelize-typescript";

@Table({
  tableName: "Sources",
})
export class Source extends Model<Source> implements ISource {
  @BelongsToMany(
    () => ContentItem,
    "SourceContentItem",
    "contentItemId",
    "sourceId"
  )
  contentItems: ContentItem[];  

  @Column
  get name(): string {
    return this.getDataValue("name");
  }

  set name(value: string) {
    this.setDataValue("name", value);
  }

  @Unique
  @Column
  get uri(): string {
    return this.getDataValue("uri");
  }

  set uri(value: string) {
    this.setDataValue("uri", value);
  }
}

ContentItem Model

import {Column, Model, Table, BelongsToMany, Unique} from "sequelize-typescript";

@Table({
  tableName: "ContentItems",
  timestamps: true,
})
export class ContentItem extends Model<ContentItem> implements IContentItem {
  @BelongsToMany(
    () => Source,
    "SourceContentItem",
    "sourceId",
    "contentItemId"
  )
  sources: Source[];

  @Column
  get title(): string {
    return this.getDataValue("title");
  }

  set title(value: string) {
    this.setDataValue("title", value);
  }

  @Unique
  @Column
  get url(): string {
    return this.getDataValue("url");
  }

  set url(value: string) {
    this.setDataValue("url", value);
  }
}

Hey, thanks for contributing. Does this error points to any file? Or can you provide any stack trace? Or even better an example project? thank you.

btw: You don't need to use get/set accessors, if you don't add any additional functionality in your getters and setters (sequelize will call this.getDataValue(...) internally anyway). Simply write:

@Column
title: string;

@Unique
@Column
url: string;

// .. and so on

Hey thanks for the prompt response. The only error that webpack is showing is that one and it points to line contentItem.$add("Source", source); .

Argument of type 'Source | null' is not assignable to parameter of type 'Mode<Source | null>'.

I will make a small repo reproducing the error and add you as a contributor in a few minutes. :)

Ah ok, this is because source can be null and $add expects a non-null value as second parameter. If findOne does not find any model, it resolves with null. So you need to check if source is truthy:

  Source
  .findOne<Source>()
  .then(source => {
    if(source) {
      contentItem.$add("Source", source);
    }
  })

Oh wow, I didn't even know compilers could check for that. Thank you so much this resolved it!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ReneHollander picture ReneHollander  路  3Comments

mikew picture mikew  路  4Comments

nandox5 picture nandox5  路  3Comments

josecolella picture josecolella  路  4Comments

KAMAELUA picture KAMAELUA  路  4Comments