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).
⚠️ Positional callback footgun
Section titled “⚠️ Positional callback footgun”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).
Type Parameters
Section titled “Type Parameters”TResult
Section titled “TResult”TResult
The return type of the executor function.
Parameters
Section titled “Parameters”operations
Section titled “operations”Operation<unknown>[]
The operations to compose. Their permissions are merged and deduplicated.
executor
Section titled “executor”(…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.
Returns
Section titled “Returns”Operation<TResult>
A single Operation with combined permissions.
Example
Section titled “Example”import { compose } from "@cfast/db";
const publishWorkflow = compose( [updatePost, insertAuditLog], async (doUpdate, doAudit) => { const updated = await doUpdate(); await doAudit(); return { published: true }; },);
// Inspect combined permissionspublishWorkflow.permissions;// => [{ action: "update", table: "posts" }, { action: "create", table: "audit_logs" }]
// Execute all sub-operationsawait publishWorkflow.run();