/** * One-shot migration: create the `audit_log` collection. * * Run with: * * npx tsx scripts/migrations/2026-09-25-create-audit-log.ts * * One row per create / update / delete on quotes, quote_items, * quote_attachments, orders, order_items and delivery_notes. Rows are * written ONLY by the PB hook `pocketbase/pb_hooks/audit_log.pb.js` * (which runs as the app, so it bypasses the API rules below). Nobody * can create, edit or delete an audit row through the API — the * create/update/delete rules are superuser-only (null). * * Read side: `src/lib/server/audit-log-service.ts` (admin client), * surfaced on the quote / order / delivery-note detail pages. * * Idempotent — safe to re-run; exits early if the collection exists. * * Rollback: remove the hook file from pb_hooks, then delete the * `audit_log` collection in the PB admin UI. Nothing references it. */ import "./_env-loader"; import PocketBase from "pocketbase"; const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090"; const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL; const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD; // Explicit `max` on every text field: PB treats `max: 0` as a 5000-char // cap, not "unlimited", and fails the insert with a generic 400. const text = (name: string, max: number, required = false) => ({ name, type: "text", required, max, }); async function main() { if (!ADMIN_EMAIL || !ADMIN_PASSWORD) { throw new Error( "Set PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD env vars (e.g. sourced from .env.local)." ); } const pb = new PocketBase(PB_URL); await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD); console.log(`[audit-log] Authenticated against ${PB_URL}`); const existing = await pb.collections .getFullList() .then((all) => all.find((c) => c.name === "audit_log")); if (existing) { console.log("[audit-log] audit_log already exists — skipping."); return; } await pb.collections.create({ name: "audit_log", type: "base", // Any logged-in dashboard user may read the trail; only the hook // (running as the app) may write it. listRule: "@request.auth.id != ''", viewRule: "@request.auth.id != ''", createRule: null, updateRule: null, deleteRule: null, fields: [ // Which record changed. text("collection_name", 64, true), text("record_id", 32, true), // Which detail page the change rolls up to: the quote for a // quote_item, the order for an order_item, etc. { name: "parent_type", type: "select", required: true, maxSelect: 1, values: ["quote", "order", "delivery_note"], }, text("parent_id", 32, true), // create | update | delete | pdf_generated | attachment_added text("action", 32, true), // Human context for the row, e.g. the item description or quote number. text("label", 500), // [{ field, from, to }] — values truncated by the hook. { name: "changes", type: "json", required: false, maxSize: 200000 }, // Who: resolved PB user id + display name, or a hint for // automations (n8n / agent) that write with the superuser key. text("actor_id", 32), text("actor_name", 200), text("actor_email", 200), // web | server | agent | mcp | n8n | system text("source", 32), // Extra lookup keys, space separated: "dn:DN26071401 quote:abc". text("refs", 500), { name: "created", type: "autodate", onCreate: true, onUpdate: false }, ], indexes: [ "CREATE INDEX idx_audit_log_parent ON audit_log (parent_type, parent_id, created)", "CREATE INDEX idx_audit_log_record ON audit_log (collection_name, record_id)", ], }); console.log("[audit-log] Created audit_log collection. Migration complete."); console.log( "[audit-log] Next: copy pocketbase/pb_hooks/audit_log.pb.js into the PB server's pb_hooks/ directory and restart PB." ); } main().catch((err) => { console.error("[audit-log] Migration failed:", err); process.exit(1); });