@beignet/core
Version:
Core framework primitives for Beignet
49 lines (46 loc) • 1.81 kB
text/typescript
import type { AuditLogPort } from "../ports/index.js";
import { getActiveRequestContext } from "./request-context.js";
/**
* Wrap an audit log port so missing `actor`, `tenant`, `requestId`, and
* `traceId` fields are filled from the ambient request context at record time.
*
* The server keeps the ambient context current for requests (including
* identity elevated by hooks) and for service contexts created for jobs,
* listeners, schedules, and tasks. Because enrichment happens when
* `record(...)` runs, the wrapper also works for ports rebuilt per
* transaction inside a unit of work — wrap both the top-level audit port and
* the per-transaction rebuild.
*
* Fields provided on the entry always win; only missing fields are filled.
* When no ambient context is active (for example on runtimes without
* `AsyncLocalStorage` propagation), entries pass through unchanged.
*
* @example
* ```ts
* const audit = createAmbientAuditLog(
* createInstrumentedAuditLog({
* audit: createDrizzleSqliteAuditLogPort(db),
* instrumentation: ports,
* }),
* );
* await audit.record({ action: "posts.publish", resource: { type: "post", id } });
* ```
*
* @param audit - Underlying audit log port to write enriched entries to.
* @returns An audit log port that fills missing context fields before writing.
*/
export function createAmbientAuditLog(audit: AuditLogPort): AuditLogPort {
return {
record(entry) {
const context = getActiveRequestContext();
if (!context) return audit.record(entry);
return audit.record({
...entry,
actor: entry.actor ?? context.actor,
tenant: entry.tenant ?? context.tenant,
requestId: entry.requestId ?? context.requestId,
traceId: entry.traceId ?? context.traceId,
});
},
};
}