📚 Audit-log series — stack on master (merge bottom → top)
- #10603 — part 1: Tier-1 audited dbUtils +
AuditAction enum → master (reland of #10514, reverted in #10584)
- #10515 — part 2:
auditedTransaction (branded AuditedTx) → #10603
- #10516 — part 3: Tier-2 pipeline + coverage middleware → #10515
- #10517 — part 4: read API (internal-dashboard) → #10516
Supersedes the production-based stack (#10355 / #10457 / #10510 / #10511 / #10513).
You are here: #10515
Description
Part 2 (on master, stacks on #10514). Adds auditedTransaction — the correct way to commit several audited mutations atomically — and statically kills the "outerTx swallows rollback" footgun via a branded tx.
auditedTransaction(body) owns the $transaction, stashes the tx in AsyncLocalStorage; mutate()/query() transparently join it (no tx threading). Any failure rolls the whole unit back and writes ONE failure row for the sub-op that failed.
- Branded
AuditedTx (effect Brand.nominal — identity at runtime, no cast): the brand constructor is module-private, so only auditedTransaction can mint a usable tx. body(tx: AuditedTx) — non-audited writes in the unit take AuditedTx, so prisma.$transaction((tx) => mutate({ outerTx: tx })) won't type-check. The footgun is a compile error.
outerTx kept but @deprecated; migrating the live call sites (provision.ts, join/clanker) to the runner is the follow-up.
await auditedTransaction(async (tx) => {
const p = await DB_PROJECT_CREATE.create({ ... }) // joins ambiently
await UserOrgMapping.DB.update.delete({ ..., tx }) // tx: AuditedTx — type-checked
return p
})
How did I test this PR
Added unit tests (commit / Err-rollback / throw-rollback). ⚠️ Static-only in this worktree (no deps) — CI typecheck/lint is the backstop.
🤖 Generated with Claude Code