From 11cb025886a83adcf26c4c1f2e6bfec306fc4cb8 Mon Sep 17 00:00:00 2001 From: mijinummi Date: Fri, 27 Mar 2026 19:42:26 +0100 Subject: [PATCH] feat(database): implement transaction middleware with ACID guarantees, isolation levels, and rollback handling (#322) --- .../src/transaction/transaction.logger.ts | 14 +++++++ .../src/transaction/transaction.manager.ts | 36 ++++++++++++++++++ .../src/transaction/transaction.middleware.ts | 37 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 middleware/src/transaction/transaction.logger.ts create mode 100644 middleware/src/transaction/transaction.manager.ts create mode 100644 middleware/src/transaction/transaction.middleware.ts diff --git a/middleware/src/transaction/transaction.logger.ts b/middleware/src/transaction/transaction.logger.ts new file mode 100644 index 0000000..2e92309 --- /dev/null +++ b/middleware/src/transaction/transaction.logger.ts @@ -0,0 +1,14 @@ +import { Injectable, Logger } from '@nestjs/common'; + +@Injectable() +export class TransactionLogger { + private readonly logger = new Logger('Transaction'); + + log(message: string) { + this.logger.log(message); + } + + error(message: string, error: any) { + this.logger.error(`${message}: ${error.message}`, error.stack); + } +} diff --git a/middleware/src/transaction/transaction.manager.ts b/middleware/src/transaction/transaction.manager.ts new file mode 100644 index 0000000..8175ec7 --- /dev/null +++ b/middleware/src/transaction/transaction.manager.ts @@ -0,0 +1,36 @@ +import { DataSource, QueryRunner } from 'typeorm'; + +export class TransactionManager { + private queryRunner: QueryRunner; + + constructor(private readonly dataSource: DataSource) { + this.queryRunner = this.dataSource.createQueryRunner(); + } + + async startTransaction(isolation: 'READ COMMITTED' | 'REPEATABLE READ' | 'SERIALIZABLE' = 'READ COMMITTED') { + await this.queryRunner.connect(); + await this.queryRunner.startTransaction(isolation); + } + + async commitTransaction() { + await this.queryRunner.commitTransaction(); + await this.queryRunner.release(); + } + + async rollbackTransaction() { + await this.queryRunner.rollbackTransaction(); + await this.queryRunner.release(); + } + + async createSavepoint(name: string) { + await this.queryRunner.query(`SAVEPOINT ${name}`); + } + + async rollbackToSavepoint(name: string) { + await this.queryRunner.query(`ROLLBACK TO SAVEPOINT ${name}`); + } + + getManager() { + return this.queryRunner.manager; + } +} diff --git a/middleware/src/transaction/transaction.middleware.ts b/middleware/src/transaction/transaction.middleware.ts new file mode 100644 index 0000000..11bb680 --- /dev/null +++ b/middleware/src/transaction/transaction.middleware.ts @@ -0,0 +1,37 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import { DataSource } from 'typeorm'; +import { TransactionManager } from './transaction.manager'; +import { TransactionLogger } from './transaction.logger'; + +@Injectable() +export class TransactionMiddleware implements NestMiddleware { + constructor(private readonly dataSource: DataSource, private readonly logger: TransactionLogger) {} + + async use(req: Request, res: Response, next: NextFunction) { + const manager = new TransactionManager(this.dataSource); + + try { + await manager.startTransaction(); + + // Attach transaction manager to request for manual control if needed + (req as any).transactionManager = manager; + + res.on('finish', async () => { + if (res.statusCode >= 200 && res.statusCode < 400) { + await manager.commitTransaction(); + this.logger.log('Transaction committed successfully'); + } else { + await manager.rollbackTransaction(); + this.logger.log('Transaction rolled back due to error'); + } + }); + + next(); + } catch (error) { + await manager.rollbackTransaction(); + this.logger.error('Transaction failed', error); + next(error); + } + } +}