import { ServiceProvider } from "@zerotal/core"; import type { AppEnvironment } from "@zerotal/core"; import { Auditor } from "../Auditor.ts"; import { DatabaseDriver } from "../drivers/DatabaseDriver.ts"; import { NullDriver } from "../drivers/NullDriver.ts"; import { _pendingAuditable, registerAudit } from "../Auditable.ts"; import { auditSchemaConcern } from "../auditSchemaConcern.ts"; import type { AuditConfigShape } from "../types.ts"; /** * AuditProvider — registers the audit system with the Zerotal application. * * @example * // bootstrap/providers.ts * import { AuditProvider } from "@zerotal/audit"; * * export default [ * DatabaseProvider, * // ... * AuditProvider, * ]; * * @example * // config/audit.ts * import { AuditConfig } from "@zerotal/audit"; * export default AuditConfig({ driver: "database" }); */ export class AuditProvider extends ServiceProvider { static override provides = ["audit"] as const; static override environments: AppEnvironment[] = ["web", "console", "worker", "test", "repl"]; override onRegister(): void { // Provision the audit_logs table on boot (after models are discovered) — no // migration required. Idempotent and skipped for the null driver. this.app.registerConcern?.(auditSchemaConcern); this.app.container.singleton("audit", async (c) => { const configManager = await c.make("config"); const cfg: Partial = (configManager as { get(path: string): T | undefined }).get>( "audit", ) ?? {}; const resolved: AuditConfigShape = { driver: cfg.driver ?? "database", table: cfg.table ?? "audit_logs", pruneKeep: cfg.pruneKeep ?? 0, captureRequest: cfg.captureRequest ?? true, }; const driver = resolved.driver === "null" ? new NullDriver() : new DatabaseDriver(resolved.table); return new Auditor(driver, resolved); }); } /** * Resolve the `audit` singleton before anything reaches for it synchronously. * * `onBooted` drains the `Auditable` registry through `registerAudit`, which * calls `container.makeSync("audit")` — and a lazy singleton is not resolved * merely because boot has progressed. Without this the first app to compose * `Auditable` into a model dies on startup with "Singleton 'audit' has not * been resolved yet", which is the container telling us to do exactly this. */ override async onBooting(): Promise { await this.app.container.make("audit"); } /** * After boot: drain the `Auditable` registry. * Models composed with `Auditable(Base)` are wired to the live Auditor here * rather than at import time, so the container is guaranteed ready. */ override async onBooted(): Promise { for (const ModelClass of _pendingAuditable) { registerAudit(ModelClass); } _pendingAuditable.clear(); } }