I am using NestJS with TypeORM. I have several modules and each module contain service. There are some modules that import other modules and use their services.
// In Person Module
@Injectable()
class UserService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>
) {}
public async deleteUsersByCompany(companyId: string) {
const usersToDelete = await this.usersRepository
.createQueryBuilder('user')
.where('user.companyId = :companyId', { companyId })
.getMany();
if (usersToDelete.length > 0) {
await this.usersRepository.delete(
usersToDelete.map((user) => user.id),
);
}
}
...
}
// Company Module
@Injectable()
class CompanyService {
constructor(
@InjectRepository(Company)
private companysRepository: Repository<Company>,
private usersService: UserService
) {}
public async deleteCompany(companyId: string) {
const companyToDelete = await this.companysRepository.find(companyId);
if (companyToDelete) {
await usersService.deleteUsersByCompany(companyToDelete.id)
await this.companysRepository.delete(companyToDelete);
}
}
...
}
In this example:
usersService.deleteUsersByCompany to be atomic operationcompanyService.deleteCompany to be atomic operationusersService.deleteUsersByCompany and companyService.deleteCompany can be triggered by the userI want to wrap both actions in transaction. I've read NestJS documentation and TypeOrm documentation but couldn't find an answer how to do this properly.
The main thing here is that usersService.deleteUsersByCompany is being called by companyService.deleteCompany.
The easiest way to use transaction is using the EntityManager API, check a simple example from a microservice:
import { WRITE_CONNECTION } from '@my-api/common';
import { Injectable, Logger } from '@nestjs/common';
import { RpcException } from '@nestjs/microservices';
import { InjectEntityManager } from '@nestjs/typeorm';
import { EntityManager } from 'typeorm';
@Injectable()
export class MyService {
private logger = new Logger(MyService.name);
constructor(
@InjectEntityManager(WRITE_CONNECTION) private entityManager: EntityManager,
) {}
async saveSomething(data: string): Promise<void> {
try {
return await this.entityManager.transaction(async (entityManager) => {
const firstRepository = entityManager.getCustomRepository(FirstRepository);
const secondRepository = entityManager.getCustomRepository(SecondRepository);
const firstRecord = firstRepository.create({ data });
await firstRepository.save(firstRecord);
const secondRecord = secondRepository.create({ data });
await secondRepository.save(secondRecord);
// Save entities directly
await entityManager.save([...]);
});
} catch (error) {
this.logger.error(`Failed saving something, error ${error.message}`, error.stack);
throw new RpcException(error.message);
}
}
}
As you can see, we use the entityManager for read and write operations in the same transaction that can execute a rollback automatically if any of these operations fails.
For more details check the repository of TypeORM with examples here.
So if you want to wrap both actions in the same transaction, you need to call a method and pass the repository as a parameter. e.g:
async findOneItem(
itemId: number,
myRepository: MyRepository = this.myRepository, // Optional parameter if needed
) {
return await myRepository.findOne(itemId);
}