/** * One-shot migration: fields for the AI order assistant. * * orders.ai_quote_match json (per-order AI verdict) * order_items.ai_fill json (per-line fill record) * ai_settings.order_assistant_enabled bool * ai_settings.order_assistant_interval_minutes number * ai_settings.order_assistant_auto_link bool * ai_settings.order_assistant_fill_lines bool * ai_settings.order_assistant_prompt text (max 50000) * ai_settings.order_assistant_last_run_at text (max 40) * ai_settings.order_assistant_last_run_summary json * * Run with: * * npm run pb:migrate:order-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. * * The two order-assistant switches default to ON (see the design spec: * "Auto-link when sure" is on by default), but a fresh PB bool field * starts every existing row at `false`. So the first run that actually * adds these fields also seeds the ai_settings row with `true`. A later * re-run skips the seeding (the fields already exist), which keeps an * operator's deliberate `false` intact. * * 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; const TAG = "[order-assistant]"; type FieldDef = Record & { name: string }; const text = (name: string, max = 500): FieldDef => ({ name, type: "text", required: false, presentable: false, max, }); const json = (name: string, maxSize: number): FieldDef => ({ name, type: "json", required: false, presentable: false, maxSize, }); const ORDER_FIELDS: FieldDef[] = [json("ai_quote_match", 65536)]; const ORDER_ITEM_FIELDS: FieldDef[] = [json("ai_fill", 16384)]; const AI_SETTINGS_FIELDS: FieldDef[] = [ { name: "order_assistant_enabled", type: "bool", required: false, presentable: false }, { name: "order_assistant_interval_minutes", type: "number", required: false, presentable: false }, { name: "order_assistant_auto_link", type: "bool", required: false, presentable: false }, { name: "order_assistant_fill_lines", type: "bool", required: false, presentable: false }, text("order_assistant_prompt", 50000), text("order_assistant_last_run_at", 40), json("order_assistant_last_run_summary", 20000), ]; /** * Add the fields that don't exist yet. Returns true when at least one * field was actually added — the caller uses that to decide whether it * is the first run (and may seed the default-on switches). */ async function addMissingFields( pb: PocketBase, collectionName: string, wanted: FieldDef[], ): Promise { 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(`${TAG} ${collectionName}.${f.name} already exists — skipping.`); } } if (missing.length === 0) return false; await pb.collections.update(collection.id, { fields: [...collection.fields, ...missing], }); for (const f of missing) { console.log(`${TAG} ${collectionName}.${f.name} added.`); } return true; } /** * Turn the two default-on switches on for the existing ai_settings row. * Only called on the first run (when the fields were just added). */ async function seedDefaultOnSwitches(pb: PocketBase): Promise { const rows = await pb.collection("ai_settings").getFullList<{ id: string }>(); if (rows.length === 0) { console.log(`${TAG} no ai_settings row yet — the code defaults cover it.`); return; } await pb.collection("ai_settings").update(rows[0].id, { order_assistant_auto_link: true, order_assistant_fill_lines: true, }); console.log(`${TAG} ai_settings: auto-link + fill-lines defaults turned on.`); } 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(`${TAG} Authenticated against ${PB_URL}`); await addMissingFields(pb, "orders", ORDER_FIELDS); await addMissingFields(pb, "order_items", ORDER_ITEM_FIELDS); const settingsAdded = await addMissingFields(pb, "ai_settings", AI_SETTINGS_FIELDS); if (settingsAdded) await seedDefaultOnSwitches(pb); console.log(`${TAG} Done.`); } main().catch((err) => { console.error(`${TAG} Migration failed:`, err); process.exit(1); });