Skip to main content

TransactionContext middlewares

Transaction-context middlewares wrap functions to run them within a transaction or defer them until a transaction commits. They let you apply transaction behavior without adding it to the function implementation.

Initial configuration​

Set up the MongoDB and SQLite databases and their transaction adapters, then register them with the resolver and call init(). MongoDB is registered as the primary (default) adapter, while SQLite is registered as the secondary adapter:

1. Set up the MongoDB database​

./samples/mongodb-database.ts
import { MongoClient } from "mongodb";

export const mongoClient = new MongoClient("mongodb://localhost:27017");
export const mongoDatabase = mongoClient.db("DATABASE_NAME");

2. Create the MongoDB transaction adapter​

./samples/mongodb-transaction-adapter.ts
import { MongodbTransactionAdapter } from "eridu-tech/transaction-context/mongodb-transaction-adapter";
import { mongoClient, mongoDatabase } from "./mongodb-database.js";

export const mongodbTransactionAdapter = new MongodbTransactionAdapter({
client: mongoClient,
database: mongoDatabase,
});

3. Set up the SQLite database​

./samples/sqlite-database.ts
import Sqlite from "better-sqlite3";
import { Kysely, SqliteDialect } from "kysely";

export const sqliteDatabase = new Kysely<any>({
dialect: new SqliteDialect({ database: new Sqlite(":memory:") }),
});

4. Create the SQLite transaction adapter​

./samples/sqlite-transaction-adapter.ts
import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter";
import { sqliteDatabase } from "./sqlite-database.js";

export const sqliteTransactionAdapter = new KyselyTransactionAdapter({
database: sqliteDatabase,
});

5. Configure the transaction context resolver​

./samples/transaction-context.ts
import { ExecutionContext } from "eridu-tech/execution-context";
import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter";
import { TransactionContextResolver } from "eridu-tech/transaction-context";
import { mongodbTransactionAdapter } from "./mongodb-transaction-adapter.js";
import { sqliteTransactionAdapter } from "./sqlite-transaction-adapter.js";

type Adapters = "primary" | "secondary";

const executionContext = new ExecutionContext(new AlsExecutionContextAdapter());

export const transactionContextResolver =
new TransactionContextResolver<Adapters>({
adapters: {
primary: mongodbTransactionAdapter,
secondary: sqliteTransactionAdapter,
},
defaultAdapter: "primary",
executionContext,
});

await transactionContextResolver.init();

withTransactionFactory middleware​

The transaction middleware intercepts function calls and runs them inside a transaction. When the wrapped function is invoked, the middleware delegates to the selected transaction context's run() method with the requested propagation mode, so the wrapped function executes within a transaction scope — joining, starting, or forbidding one depending on the mode.

The propagation mode defaults to REQUIRED: the wrapped function joins an existing transaction when one is active and starts a new one otherwise.

Usage​

./samples/with-transaction.ts
import { use } from "eridu-tech/middleware";
import { withTransactionFactory } from "eridu-tech/transaction-context/middlewares";
import { transactionContextResolver } from "./transaction-context.js";

const withTransaction = withTransactionFactory(transactionContextResolver);

const createUser = async (userId: string, name: string): Promise<string> => {
// ...
return "user-id";
};

// Uses the default adapter (`primary`) and REQUIRED propagation
const createUserInTransaction = use(createUser, withTransaction());

// Uses the `secondary` adapter
const createUserOnSecondary = use(
createUser,
withTransaction.use("secondary")(),
);

await createUserInTransaction("1", "Jose");
await createUserOnSecondary("2", "Mina");

Calling withTransaction() uses the transaction resolver's default adapter (primary in this example). Use withTransaction.use("secondary")() to run the wrapped function through a specific registered adapter.

info

Here is a complete list of settings for the withTransactionFactory function.

withAfterCommitFactory middleware​

The after-commit middleware intercepts function calls and registers them to run once the active transaction commits. When the wrapped function is invoked, the middleware delegates to transactionContext.afterCommit(), so the wrapped function is registered as an after-commit invocable instead of running immediately. This is useful for side effects that must not happen when the transaction is rolled back, such as sending emails or publishing events.

When no transaction is active, the wrapped function runs immediately, unless runIfNoTransaction is false, in which case it is discarded.

When using a TransactionContextResolver, afterCommit can be called within a transaction started by any configured adapter, for example resolver.use("primary").run(callback) or resolver.use("secondary").run(callback). The hook runs after the active transaction commits.

Usage​

./samples/with-after-commit.ts
import { use } from "eridu-tech/middleware";
import { withAfterCommitFactory } from "eridu-tech/transaction-context/middlewares";
import { transactionContextResolver } from "./transaction-context.js";

const withAfterCommit = withAfterCommitFactory(transactionContextResolver);

const sendWelcomeEmail = async (userId: string): Promise<void> => {
// ...
};

// Registers the wrapped function to run once the active transaction commits
const sendWelcomeEmailAfterCommit = use(sendWelcomeEmail, withAfterCommit());

const sendWelcomeEmailAfterCommitOrSkip = use(
sendWelcomeEmail,
// Discards the wrapped function when no transaction is active
withAfterCommit({ runIfNoTransaction: false }),
);

// Runs once the transaction commits
await transactionContextResolver
.use("primary")
.run(() => sendWelcomeEmailAfterCommit("1"));

// Does nothing, because no transaction is active
await sendWelcomeEmailAfterCommitOrSkip("2");
info

Here is a complete list of settings for the withAfterCommitFactory function.

Settings​

OptionTypeDefaultDescription
runIfNoTransactionbooleantrueWhether to invoke the wrapped function immediately when there is no active transaction, instead of discarding it

Further information​

For further information refer to eridu-tech/transaction-context API docs.