Skip to content

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

@Choo-Xing-Yu

Description

@Choo-Xing-Yu

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

  • Are controller level transactions are okay?
  • GET endpoints with transactions throw SQLITE_ERROR: cannot start a transaction within a transaction when hit concurrently, what's the proper way to fix this case? I was under the impression that REQUIRES_NEW (default) supports current transaction instead of starting new when there's one

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions