What is the correct way to work with transactions with NestJs and TypeORM?

Viewed 1748

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:

  1. I want usersService.deleteUsersByCompany to be atomic operation
  2. I want companyService.deleteCompany to be atomic operation
  3. Both usersService.deleteUsersByCompany and companyService.deleteCompany can be triggered by the user

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

  1. How can I support such "nested transactions"?
  2. Does QueryRunner replace the Repository pattern?
  3. Should my services hold both Repository and Connection instances? Repository for non-transactional operations and connection to get QueryRunner?
1 Answers

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);
}
Related