/** * One-shot migration: fields for the AI quote assistant. * * quote_items.ai_fill json (per-line match/fill record) * ai_settings.quote_assistant_enabled bool * ai_settings.quote_assistant_interval_minutes number * ai_settings.quote_assistant_min_confidence text (max 10) * ai_settings.quote_assistant_prompt text (max 50000) * ai_settings.quote_assistant_last_run_at text (max 40) * ai_settings.quote_assistant_last_run_summary json * * Run with: * * npm run pb:migrate:quote-assistant * * Idempotent — fields that already exist are skipped. Manual step on the * live PB (like select values and pb_hooks). * * NOTE: PB text fields with `max: 0` cap at 5000 chars, NOT unlimited, so * the prompt field gets an explicit max of 50000. * * Rollback: delete the fields in the PB admin UI. */ import "./_env-loader"; import PocketBase from "pocketbase"; const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || process.env.PB_URL || "http://127.0.0.1:8090"; const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL; const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD; type FieldDef = Record & { name: string }; const QUOTE_ITEM_FIELDS: FieldDef[] = [ { name: "ai_fill", type: "json", required: false, presentable: false, maxSize: 200000 }, ]; const AI_SETTINGS_FIELDS: FieldDef[] = [ { name: "quote_assistant_enabled", type: "bool", required: false, presentable: false }, { name: "quote_assistant_interval_minutes", type: "number", required: false, presentable: false }, { name: "quote_assistant_min_confidence", type: "text", required: false, presentable: false, max: 10 }, { name: "quote_assistant_prompt", type: "text", required: false, presentable: false, max: 50000 }, { name: "quote_assistant_last_run_at", type: "text", required: false, presentable: false, max: 40 }, { name: "quote_assistant_last_run_summary", type: "json", required: false, presentable: false, maxSize: 20000 }, ]; async function addMissingFields(pb: PocketBase, collectionName: string, wanted: FieldDef[]) { const collection = await pb.collections.getOne(collectionName); const existing = new Set(collection.fields.map((f: { name: string }) => f.name)); const missing = wanted.filter((f) => !existing.has(f.name)); for (const f of wanted) { if (existing.has(f.name)) { console.log(`[quote-assistant] ${collectionName}.${f.name} already exists — skipping.`); } } if (missing.length === 0) return; await pb.collections.update(collection.id, { fields: [...collection.fields, ...missing], }); for (const f of missing) { console.log(`[quote-assistant] ${collectionName}.${f.name} added.`); } } async function main() { if (!ADMIN_EMAIL || !ADMIN_PASSWORD) { throw new Error("Set PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD (e.g. from .env.local)."); } const pb = new PocketBase(PB_URL); await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD); console.log(`[quote-assistant] Authenticated against ${PB_URL}`); await addMissingFields(pb, "quote_items", QUOTE_ITEM_FIELDS); await addMissingFields(pb, "ai_settings", AI_SETTINGS_FIELDS); console.log("[quote-assistant] Done."); } main().catch((err) => { console.error("[quote-assistant] Migration failed:", err); process.exit(1); });