From b5ecd91ec83b732bf1b038c9d56542550a795eea Mon Sep 17 00:00:00 2001 From: Ryder Date: Wed, 21 Oct 2026 14:29:52 +0000 Subject: [PATCH] feat(database): add DatabaseTransactionService atomic transaction wrapper (#420) --- .../database-transaction.service.spec.ts | 37 +++++++++++++++++++ src/database/database-transaction.service.ts | 22 +++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/database/database-transaction.service.spec.ts create mode 100644 src/database/database-transaction.service.ts diff --git a/src/database/database-transaction.service.spec.ts b/src/database/database-transaction.service.spec.ts new file mode 100644 index 0000000..6e7bd14 --- /dev/null +++ b/src/database/database-transaction.service.spec.ts @@ -0,0 +1,37 @@ +import { describe, it, expect, vi } from 'vitest'; +import { Prisma } from '@prisma/client'; +import { DatabaseTransactionService } from './database-transaction.service'; +import { PrismaTransactionService } from './prisma-transaction.service'; + +describe('DatabaseTransactionService (#420)', () => { + it('commits multi-step callbacks through PrismaTransactionService', async () => { + const fakeTx = {} as Prisma.TransactionClient; + const prismaTx = { + run: vi.fn().mockImplementation(async (fn: (tx: Prisma.TransactionClient) => Promise) => + fn(fakeTx), + ), + } as unknown as PrismaTransactionService; + + const service = new DatabaseTransactionService(prismaTx); + const outcome = await service.executeInTransaction(async (tx) => { + expect(tx).toBe(fakeTx); + return 'committed'; + }); + + expect(outcome).toBe('committed'); + expect(prismaTx.run).toHaveBeenCalledTimes(1); + }); + + it('propagates errors and aborts the transaction when a mid-step write fails', async () => { + const prismaTx = { + run: vi.fn().mockRejectedValue(new Error('mid-transaction ledger write failure')), + } as unknown as PrismaTransactionService; + + const service = new DatabaseTransactionService(prismaTx); + await expect( + service.executeInTransaction(async () => { + throw new Error('mid-transaction ledger write failure'); + }), + ).rejects.toThrow('mid-transaction ledger write failure'); + }); +}); diff --git a/src/database/database-transaction.service.ts b/src/database/database-transaction.service.ts new file mode 100644 index 0000000..ab1a05d --- /dev/null +++ b/src/database/database-transaction.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { + PrismaTransactionService, + TransactionOptions, +} from './prisma-transaction.service'; + +/** + * High-level injectable wrapper over PrismaTransactionService that guarantees + * atomic execution and rollback across multi-step ledger, budget, and audit writes. + */ +@Injectable() +export class DatabaseTransactionService { + constructor(private readonly prismaTx: PrismaTransactionService) {} + + async executeInTransaction( + operation: (tx: Prisma.TransactionClient) => Promise, + options?: TransactionOptions, + ): Promise { + return this.prismaTx.run(operation, options); + } +}