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
.
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)
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