alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
57 lines (54 loc) • 2.27 kB
text/typescript
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);
};
},
});
};