Aliheym / typeorm-transactional

A Transactional Method Decorator for TypeORM that uses Async Local Storage or cls-hooked to handle and propagate transactions between different repositories and service methods.
MIT License
213 stars 28 forks source link

[Question] @Transactional on controller level and nested transaction errors #37

Open Choo-Xing-Yu opened 1 year ago

Choo-Xing-Yu commented 1 year ago

Currently I have this on my NestJS application on the controller level

Controller level:

@Controller('supplier')
export class SupplierController {
  constructor(private readonly supplierService: SupplierService) {}

  @Transactional()
  @Post()
  async create(@Body() createSupplierDto: CreateSupplierDto) {
    return await this.supplierService.create(createSupplierDto);
  }

  @Transactional({ propagation: Propagation.SUPPORTS })
  @Get()
  async findAll() {
    return await this.supplierService.findAll();
  }

  @Transactional({ propagation: Propagation.SUPPORTS })
  @Get(':id')
  async findOne(@Param('id') id: string) {
    return await this.supplierService.findOne(+id);
  }

  @Transactional()
  @Patch(':id')
  async update(@Param('id') id: string, @Body() updateSupplierDto: UpdateSupplierDto) {
    return await this.supplierService.update(+id, updateSupplierDto);
  }

  @Transactional()
  @Delete(':id')
  async remove(@Param('id') id: string) {
    return await this.supplierService.remove(+id);
  }
}

Service Level:

@Injectable()
export class SupplierService {
  constructor(
    @InjectRepository(Supplier)
    private readonly supplierRepository: Repository<Supplier>,
    @Inject(forwardRef(() => MaterialService))
    private readonly materialService: MaterialService,
  ) { }

  async create(createSupplierDto: CreateSupplierDto) {
    await this.checkNoSameName(createSupplierDto);
    const newSupplier = this.supplierRepository.create(createSupplierDto);
    return await this.supplierRepository.save(newSupplier);
  }

  async findAll() {
    return await this.supplierRepository.find();
  }

  async findOne(id: number) {
    return await this.supplierRepository.findOneBy({ id });
  }

  async findOneByName(name: string) {
    return await this.supplierRepository.findOneBy({ name });
  }

  async update(id: number, updateSupplierDto: UpdateSupplierDto) {
    await this.checkNoSameName(updateSupplierDto);
    const supplier = await this.findOne(id);
    if (!supplier) {
      throw new NotFoundException('Supplier not found!');
    }
    return this.supplierRepository.save({ ...supplier, ...updateSupplierDto });
  }

  async remove(id: number) {
    const supplier = await this.findOne(id);
    if (!supplier) {
      throw new NotFoundException('Supplier not found!');
    }
    // since many materials can't survive without supplier
    // check before deleting this supplier that there's no material belonging to it
    const taggedMaterials = await this.materialService.findTaggedSupplier(id);
    if (taggedMaterials.length > 0) {
      throw new BadRequestException(
        ERROR_MESSAGE_FORMATS.SUPPLIER.TAGGED_MATERIALS(taggedMaterials.length),
      );
    }

    return this.supplierRepository.remove(supplier);
  }

  async checkNoSameName(dto: UpdateSupplierDto) {
    if (!dto || !dto.name) return;
    const sameNameSupplier = await this.findOneByName(dto.name);
    if (sameNameSupplier) {
      throw new BadRequestException(
        ERROR_MESSAGE_FORMATS.SUPPLIER.SAME_NAME(sameNameSupplier.id),
      );
    }
  }
}

Datasource

const SQLIITE = 'sqlite';

{
    type: SQLIITE,
    database: process.env.DATABASE_PATH.endsWith(`.${SQLIITE}`)
      ? process.env.DATABASE_PATH
      : `${process.env.DATABASE_PATH}.${SQLIITE}`,
    entities: ['dist/**/*.entity.js'],
    migrations: ['dist/db/migrations/*.js'],
}

I've noticed that the examples all dictate service-level transactions. But may I ask if controller level transactions are okay? Note: I have GET endpoints use the propagation level of SUPPORTS because for some reason when I have my FE do concurrent GET requests it'll throw a SQLITE_ERROR: cannot start a transaction within a transaction image

tldr