Implicit and execution scoped transactions/savepoints for drizzle-orm.
- Supports everything drizzle supports, with accurate types
- Includes a safe mode for migrating over existing codebases
- Has method decorators for AOP/Service based design patterns
Useful for keep transactions from blocking across functions/files/services.
Uses node:async_hooks, so this works in bun/deno/node (recommend that you use node 24+ for improved performance)
Based on this spec by @agcty
npm i drizzle-transaction-contextimport { createTransactionContext } from "drizzle-transaction-context";
import { Database } from "bun:sqlite";
import { drizzle } from "drizzle-orm/bun-sqlite";
const sqlite = new Database("");
const db = drizzle({ client: sqlite, schema });
const { useTransaction, withTransaction } = createTransactionContext(db);
async function addCustomerService(name: string, age: number) {
return withTransaction(async () => {
await addCustomerWithFixes(string, age);
});
}
async function addCustomerWithFixes(name: string, age: number) {
await addCustomer(name);
await execAgeFix(name, age);
}
async function addCustomer(name: string) {
const tx = useTransaction();
await tx.insert(schema.customers).values({ name });
}
async function execAgeFix(name: string, age: number) {
const tx = useTransaction();
await tx
.update(schema.customers)
.set({ age })
.where(eq(schema.customer.name, name));
}Savepoints are supported as well. withSavePoint must be called within the execution scope of withTransaction (safe mode will handle this for you).
Unlike transactions, savepoints can be nested, so you can call withSavePoint within withSavePoint with no warning or error.
Because of this, you can "name" your savepoints for logging/debugging
const {
useTransaction,
withTransaction,
withSavePoint,
useSavePoint,
contextDepth,
currentSavePointName,
} = createTransactionContext(db);
withTransaction(async () => {
withSavePoint(async () => {
withSavePoint(async () => {
savePointNest();
}, "sp2");
savePointNest();
}, "sp1");
});
function savePointNest() {
const sp = useSavePoint();
const depth = contextDepth();
const name = currentSavePointName();
console.log(name, depth);
}
//"sp2" 3
//"sp1" 2Big Spring fan? I gotchu:
You can initialize a transaction scope with @Transactional and a savepoint scope with @SavePoint.
const { Transactional, SavePoint, useTransaction, useSavePoint } =
createTransactionContext(db);
class A {
@Transactional
async insertCustomer() {
const tx = useTransaction();
const [result] = await tx
.insert(customer)
.values(g.customer())
.returning({ customer_id: customer.customer_id });
await this.insertOrder(result!.customer_id);
return result!.customer_id;
}
@SavePoint
private async insertOrder(customer_id: string) {
const sp = useSavePoint();
const [result] = await sp
.insert(order)
.values(g.order(customer_id))
.returning({ order_id: order.order_id });
await this.insertItem(result!.order_id);
}
@SavePoint("a")
private async insertItem(order_id: string) {
const sp = useSavePoint();
await sp.insert(items).values(g.item(order_id));
}
}
class B {
@Transactional({ accessMode: "read only" })
async getCustomer(customer_id: string) {
const tx = useTransaction();
const [result] = await tx
.select()
.from(customer)
.where(eq(customer.customer_id, customer_id));
return result;
}
}
const a = new A();
const b = new B();
const customer_id = await a.insertCustomer();
await b.getCustomer(customer_id);Safe mode handles common mistakes when working with transactions and log a stack trace to help remediate the issue.
By default, this option is set to false.
This is useful when you initially use this library and mix it with other data accessing functionality.
If not in safe mode, these will be thrown as errors.
It is highly recommended that you fix these issues before deploying or merging code.
These include:
- Calling
withTransactionwithin anotherwithTransactionexecution scope - Calling
withSavePointoutside of awithTransactionexecution scope - Calling
useTransactionoutside of awithTransactionexecution scope - Calling
useSavePointoutside of awithSavePointexecution scope
Creates a new transaction context. This context can be used for multiple transactions as long as they don't overlap in execution.
Accepts any drizzle driver that supports transactions (currently Postgres-like, MySQL-like, and SQLite-like).
Both options default to false
- safeMode: Enables safe mode as explained above
- silent: If enabled, will turn off warning logs from
safeMode. I highly recommend not doing this permanently.
Creates a new transaction scope, so useTransaction will return within it's execution scope
If in safe mode, another call of withTransaction within withTransaction will simply execute
within that scope.
In normal mode, it will throw a AlreadyRunningTransactionError.
config is the same as drizzles transaction config options
Returns the currently scoped transaction.
In safe mode, if there is no running transaction, this will return the database object.
In normal mode, it will throw a NoRunningTransactionError
True if in a withTransaction scope, false if otherwise
Creates a new execution scope for a savepoint
Calling this in another savepoint creates a nested savepoint
You can give the savepoint a name, but this is only used externally via this API for things like logging.
Returns the currently in scope savepoint.
In safe mode, if no savepoint is present, this function will call useTransaction to returning the currently in scope transaction.
This means that it's possible that it will return the database object instead and log two warnings.
true if in a safepoint context, false if otherwise
Returns the name of the save point if it was given in withSavePoint.
Useful for debugging and logging
Gives the current context depth, useful for logging and debugging.
Transaction scoped execution will return 1 always and savepoints will nested continuously eg: If you are in a save point in save point, this will be 3