UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

57 lines (54 loc) 2.27 kB
import { createMiddleware, type Middleware } from "alepha"; import type { PgTransactionConfig } from "drizzle-orm/pg-core/session"; import { DatabaseProvider } from "../providers/drivers/DatabaseProvider.ts"; export interface TransactionalOptions { /** * PostgreSQL transaction configuration (isolation level, access mode, etc.). */ config?: PgTransactionConfig; } /** * Middleware that wraps handler execution in a database transaction. * * All Repository operations inside the handler automatically participate in * the transaction — no explicit `{ tx }` drilling required. * * Nesting is safe: if the handler is already inside a `transactional()` block, * the outer transaction is reused. * * ```typescript * class OrderService { * createOrder = $action({ * use: [$transactional()], * handler: async ({ body }) => { * await this.orders.create(body); // auto-uses tx * await this.audit.create({ ... }); // auto-uses tx * // throw → auto rollback, return → auto commit * }, * }); * } * ``` */ // TODO: support savepoints for nested transactions. // Today, a nested $transactional reuses the outer tx (correct for ACID), but // there is no way to roll back ONLY the inner block while preserving the // outer one. Drizzle exposes savepoints (`tx.transaction(...)` inside an // existing tx maps to `SAVEPOINT` on Postgres). Plan: // 1. Detect "already inside a tx" in DatabaseProvider.transactional(). // 2. When nested, open a savepoint instead of reusing the outer scope. // 3. On inner throw, ROLLBACK TO SAVEPOINT (don't bubble unless re-thrown). // 4. Add an option { savepoint?: boolean } on $transactional (default true // when nested) so users can opt out for "inherit-and-bubble" semantics. // Needs care around connection-bound state and Drizzle's tx proxy. export const $transactional = (options?: TransactionalOptions): Middleware => { return createMiddleware({ name: "$transactional", options, handler: ({ alepha, next }) => { const provider = alepha.inject(DatabaseProvider); return async (...args: any[]) => { return provider.transactional(() => next(...args), options?.config); }; }, }); };