@veap/database exposes transaction() and uses AsyncLocalStorage to propagate the active Knex transaction through the asynchronous call chain. Queries that use the package's active connection resolver can therefore participate in the current transaction without every service receiving a transaction argument.
Basic transaction#
import { transaction } from "@veap/database";
await transaction(async () => {
await Account.create({ name: "Primary" });
await AuditEntry.create({ action: "account.created" });
});If the callback succeeds, the transaction commits. If it throws, Knex rolls it back and the error propagates.
Nested calls#
By default, a nested transaction() call reuses the current transaction context. Pass { savepoint: true } when the inner operation needs a savepoint boundary:
await transaction(async () => {
await updateAccount();
try {
await transaction(async () => {
await optionalOperation();
}, { savepoint: true });
} catch {
// The inner savepoint can roll back while the outer transaction continues.
}
});Database drivers differ in savepoint support. Verify nested behavior with the database engine you deploy.
Transaction boundaries#
Keep the transaction around the complete unit of work that must remain consistent. Avoid sending email or calling remote services while holding a database transaction open. For external side effects, consider an outbox or post-commit event strategy so a rollback does not leave the external system out of sync.
Common pitfalls#
- Do not run unrelated concurrent operations that should not share a transaction in the same async context.
- Do not assume transactions persist across process boundaries.
- Do not mix a separate Knex instance with the Veap-managed active transaction unless you explicitly pass and use the correct transaction object.
- Test rollback behavior, nested calls, and constraint violations with the production database engine.