Search code examples
node.jstypescriptexpresssequelize.jssequelize-typescript

Sequelize-Typescript One To Many Association with Repository returns 1 child entity when there's more than one


I have been working around using Sequelize-Typescript and I have stumbled upon the one-to-many association. Here I have "Album" and "Photos" as my entities. One "Album" has many "Photos". So here's my entity codes for your reference:

Album.ts

`

@Table({
  timestamps: true,
  deletedAt: "albumDeletedAt",
  paranoid: true,
  createdAt: true,
  updatedAt: true
})
export class Album extends Model {
  @PrimaryKey
  @Column({
      type: DataType.INTEGER,
      primaryKey: true,
      autoIncrement: true
  })
  declare id: number;

  @Column({
      type: DataType.STRING,
      allowNull: false
  })
  declare name: string;

  @Column({
     type: DataType.STRING,
     allowNull: true
  })
  declare description?: string;

  @Column({
      type: DataType.STRING,
      allowNull: false
  })
  declare thumbnailURL: string;

  @HasMany(() => Photo)
  declare photos: Photo[];

  @CreatedAt
  declare createdAt: Date;

  @UpdatedAt
  declare updatedAt: Date;
 }`

Photo.ts

import { BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, PrimaryKey, Table, 
UpdatedAt } from "sequelize-typescript";
import { Album } from "./album";

@Table({
  timestamps: true,
  deletedAt: "photoDeletedAt",
  paranoid: true,
  createdAt: true,
  updatedAt: true
})
export class Photo extends Model {
  @PrimaryKey
  @Column({
      type: DataType.INTEGER,
      primaryKey: true,
      autoIncrement: true
  })
  declare id: number;

  @Column({
     type: DataType.STRING,
     allowNull: false
  })
  declare name: string;

  @Column({
     type: DataType.STRING,
     allowNull: false
  })
  declare photoURL: string;

  @ForeignKey(() => Album)
  @Column({
     type: DataType.INTEGER,
     allowNull: true,
  })
  declare albumId: number;

  @BelongsTo(() => Album)
  declare album: Album;

  @CreatedAt
  declare createdAt: Date;

  @UpdatedAt
  declare updatedAt: Date;
}

AlbumRepository.ts

export class AlbumRepository {
private albumRepository: Repository<Album>;
private photoRepository: Repository<Photo>;

/**
 * This constructor injects the DatabaseMiddleware class created and annotated as "@Service()"
 * @param databaseMiddleware Database Middleware created
 */
constructor(public databaseMiddleware: DatabaseMiddleware) {
    this.albumRepository = databaseMiddleware.getDatabaseInstance().getRepository(Album);
    this.photoRepository = databaseMiddleware.getDatabaseInstance().getRepository(Photo);
}

/**
 * Creates an "Album" data in the database.
 * @param albumCreateDTO Album Create DTO data model
 * @returns "Album" data entity model (if successfully created in the database) or null.
 */
public async createAlbum(albumCreateDTO: AlbumCreateDto) {
    try {
        return await this.albumRepository.create({ ...albumCreateDTO }, { include: [this.photoRepository] });
    } catch (error) {
        console.error("Error while creating an album data to the database due to", error?.message);
        return null;
    }
}

/**
 * Find the album via it's ID
 * @param id The ID of the album
 * @returns Album (if exists) or null
 */
public async getAlbumById(id: number) : Promise<Album | null> {
    try {
        return await this.albumRepository.findByPk(id, { include: [this.photoRepository] }) ?? null;
    } catch (error) {
        console.error(`Error finding the album via ID for the ID of ${id} due to`, error);
        return null;
    }
}

}

And finally, my Sequelize-Typescript instance which is below:

export default class DatabaseMiddleware {
private databaseInstance: Sequelize;

constructor() {
    this.databaseInstance = new Sequelize({
        dialect: "mysql",
        host: process.env.DATABASE_HOST,
        database: process.env.DATABASE_NAME,
        username: process.env.DATABASE_USERNAME,
        password: process.env.DATABASE_PASSWORD,
        port: 3306,
        // storage: process.env.DATABASE_STORAGE_PATH,
        logging(sql, timing) {
            console.log("The SQL statement from Sequelize executed is", sql, timing);
        },
        query: { raw: true }
        models: [User, Album, Video, Photo],
        repositoryMode: true
    });
}

public async connectToDatabase() {
    try {
        await this.databaseInstance.authenticate();
        if (process.env.ENVIRONMENT_PROFILE === "development") {
            await this.databaseInstance.sync({ alter: true, force: true });
        }
        console.log("Connection to database has been established successfully");
    } catch (error) {
        console.error("Unable to connect to database due to", error);
    }
}

public getDatabaseInstance() {
    return this.databaseInstance;
}

}

I'm using class based style to create the Express.JS REST API and I'm adding dependency injection in every class that I use except the entities. I'm able to create an "Album" with the "Photos" and I'm able to see it on the database. But it's when I call the getAlbumById function in the repository that I only get 1 "Photo". If I have inserted 3 "Photo"s then, during the invocation of the function, it returns only 1 "Photo" when there's 3 of them in database.

UPDATE OF THE QUESTION'S DESCRIPTION

Here's how the query looks like after running with include: [this.photoRepository] in the AlbumRepository.ts.

First screenshot 1st Screenshot of the updated query

Second screenshot 2nd Screenshot of the updated query

If you have any solution, please do share them here. Thanks!

Error when removing raw: true flag from the AlbumRepository.ts which is the function getAlbumByID:

Error finding the album via ID for the ID of 1 due to TypeError: result.get is not a function at E:\Coding Projects\Projects\Back-Ends\Profilic\node_modules\sequelize\src\model.js:1885:39 at Array.reduce () at E:\Coding Projects\Projects\Back-Ends\Profilic\node_modules\sequelize\src\model.js:1884:19 at Array.map () at Function._findSeparate (E:\Coding Projects\Projects\Back-Ends\Profilic\node_modules\sequelize\src\model.js:1881:39) at Function.findAll (E:\Coding Projects\Projects\Back-Ends\Profilic\node_modules\sequelize\src\model.js:1840:24) at processTicksAndRejections (node:internal/process/task_queues:95:5) at async Function.findOne (E:\Coding Projects\Projects\Back-Ends\Profilic\node_modules\sequelize\src\model.js:1998:12) at async Function.findByPk (E:\Coding Projects\Projects\Back-Ends\Profilic\node_modules\sequelize\src\model.js:1955:12) at async AlbumRepository.getAlbumById (E:\Coding Projects\Projects\Back-Ends\Profilic\repositories\album.repository.ts:52:20)


Solution

  • It's been a long time since the last activity in this question. I've found the solution to my problem although there's a bug within ORM library.

    The solution is to add raw: true, nest: true to the transaction. For example:

    const searchedResult = await this.albumRepository.findByPk(id, { include: [this.photoRepository], raw: true, nest: true });.

    The result which I wanted is now coming as expected like below:

    {
    "name": "My Album",
    "description": "Personal Album",
    "thumbnailURL": "http://localhost:8080/uploads/pictures/my_album/my_album_thumbnail.jpg",
    "photos": {
        "id": 1,
        "name": "3840x2880_2887b808096152458f8e6b41cb5a8780.jpg",
        "photoURL": "http://192.168.1.15:8080/uploads/pictures/my_album/3840x2880_2887b808096152458f8e6b41cb5a8780.jpg",
        "albumId": 1,
        "createdAt": "2024-03-15T11:34:45.000Z",
        "updatedAt": "2024-03-15T11:34:45.000Z",
        "photoDeletedAt": null
    },
    "albumDeletedAt": null
    

    }

    This is because sequelize-typescript uses a library to safely deserialize child objects by adding the child object followed by the child attribute. For example:

    "photos.id": 1

    The flag nest: true, transform that to the usual conventional way of having a child object. Now, I mentioned about the bug. Since in my question's description, I have one-to-many relationship which is one Album.ts can have one or more Photo.ts. Now, in my database even though I have 3 Photo.ts of the same foreign key of Album.ts, it still returns one Photo in my Album. This bug is reported in the pull request issue in GitHub: bug issue in one-to-many relationship. Otherwise, all works well. Thanks again to those who have helped.

    GitHub link to the solution: Discussion leading to this solution