Skip to content

compose

compose<TResult>(operations, executor): Operation<TResult>

Defined in: packages/db/src/compose.ts:89

Merges multiple Operations into a single operation with combined, deduplicated permissions and an executor function for controlling data flow.

compose() itself does not check permissions — it only merges them. Each sub-operation’s .run() still performs its own permission check when the executor calls it. This enables data dependencies between operations (e.g., using an insert result’s ID in an audit log).

The executor callback receives one run function per entry in operations, in array order. TypeScript cannot enforce that the parameter names line up with the operations they correspond to, so a callback that swaps (runUpdate, runVersion) for (runVersion, runUpdate) — or that simply forgets a parameter — silently invokes the wrong sub-operation. The wiki tracking issue (#182) was a real bug of this shape: a runVersion parameter shadowed an outer-scope variable and was never invoked.

Prefer composeSequentialCallback for any workflow with data dependencies. It accepts an async (db) => ... callback that uses the normal db builders by reference, so the binding is by name (not by position) and there is no way to wire the wrong op to the wrong handler:

// ✗ Footgun: parameter order must match array order, not type-checked.
const op = compose(
[updatePost, bumpVersion],
async (runUpdate, runVersion) => {
await runUpdate(); // What if `runVersion` was renamed and never called?
await runVersion();
},
);
// ✓ Preferred: by-name binding via composeSequentialCallback.
const op = composeSequentialCallback(db, async (tx) => {
await tx.update(posts).set({ ... }).where(...).run();
await tx.update(postVersions).set({ ... }).where(...).run();
});

Use compose() only when you genuinely need to interleave non-db logic between sub-operations and you have a small, fixed number of ops where the footgun is easy to read around. For everything else, reach for composeSequential (no callback at all) or composeSequentialCallback (by-name binding).

TResult

The return type of the executor function.

Operation<unknown>[]

The operations to compose. Their permissions are merged and deduplicated.

(…runs) => TResult | Promise<TResult>

A function that receives a run function for each operation (in order). You control execution order, data flow between operations, and the return value. Must declare exactly one parameter per operation — TypeScript cannot enforce this, but a regression test in this package asserts that swapping the count surfaces undefined run calls at runtime.

Operation<TResult>

A single Operation with combined permissions.

import { compose } from "@cfast/db";
const publishWorkflow = compose(
[updatePost, insertAuditLog],
async (doUpdate, doAudit) => {
const updated = await doUpdate();
await doAudit();
return { published: true };
},
);
// Inspect combined permissions
publishWorkflow.permissions;
// => [{ action: "update", table: "posts" }, { action: "create", table: "audit_logs" }]
// Execute all sub-operations
await publishWorkflow.run();