// Adds the `site` relation on `assets` → `sites`, so every machine can be // assigned to the yard it is actually parked at. // // Idempotent: safe to re-run. Exits non-zero if `sites` is missing, because // creating the relation without a target collection would leave a dangling // field rather than an obvious failure. // // Usage: node scripts/setup-site-assignment.mjs import PocketBase from "pocketbase"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; const __dirname = dirname(fileURLToPath(import.meta.url)); const PB = process.env.NEXT_PUBLIC_PB_URL; const EMAIL = process.env.PB_ADMIN; const PASS = process.env.PB_PASS; const missing = []; if (!PB) missing.push("NEXT_PUBLIC_PB_URL"); if (!EMAIL) missing.push("PB_ADMIN"); if (!PASS) missing.push("PB_PASS"); if (missing.length) { console.error(`✗ Missing required env: ${missing.join(", ")}`); console.error(" Copy .env.example to .env.local and fill in."); process.exit(1); } const pb = new PocketBase(PB); pb.autoCancellation(false); console.log(`→ Connecting to ${PB} as ${EMAIL}`); await pb.collection("_superusers").authWithPassword(EMAIL, PASS); const sites = await pb.collections.getOne("sites").catch(() => null); if (!sites) { console.error("✗ Collection `sites` does not exist. Create it first, then re-run."); process.exit(1); } const siteCount = (await pb.collection("sites").getList(1, 1)).totalItems; console.log(` ✓ found \`sites\` with ${siteCount} row(s)`); const assets = await pb.collections.getOne("assets"); const existing = assets.fields.find((f) => f.name === "site"); if (existing) { // PocketBase refuses to repoint an existing relation (it throws // `validation_field_relation_change`). If an earlier run created the field // against the wrong collection, say so instead of silently leaving it broken // — every assignment will fail with `validation_missing_rel_records`. if (existing.collectionId !== sites.id) { const used = (await pb.collection("assets").getFullList()).filter((a) => a.site).length; if (used > 0) { console.error(`✗ assets.site points at the wrong collection and ${used} asset(s) already use it.`); console.error(" Fix by hand: drop the `site` field in the PocketBase UI, then re-run this script."); process.exit(1); } console.log(" · assets.site points at the wrong collection and is unused — recreating it."); assets.fields = assets.fields.filter((f) => f.name !== "site"); await pb.collections.update(assets.id, { fields: assets.fields }); } else { console.log(" ✓ assets.site already exists and targets `sites` — nothing to do."); process.exit(0); } } const rand = (n) => Math.random().toString(36).slice(2, 2 + n); assets.fields.push({ name: "site", type: "relation", required: false, presentable: false, hidden: false, system: false, collectionId: sites.id, // self-relation: a site may be scoped to a parent site minSelect: null, maxSelect: 1, maxSize: 0, // Deleting a site must NEVER delete the machines parked at it. This is the // opposite of monthly_income.asset, which deliberately cascades. cascadeDelete: false, id: "relation" + rand(15), }); const updated = await pb.collections.update(assets.id, { fields: assets.fields }); const field = updated.fields.find((f) => f.name === "site"); if (!field) { console.error("✗ assets.site missing after update — PocketBase rejected the change."); process.exit(1); } console.log(` ✓ assets.site added (maxSelect=${field.maxSelect}, cascadeDelete=${field.cascadeDelete})`); console.log("\nNext: assign machines from /plant → Edit → Site, or run `npm run seed`.");