WFDASH-WEBAPP/scripts/add-collection-field.ts

120 lines
4.4 KiB
TypeScript
Raw Normal View History

feat(collection-pool): back-to-pending, collection date, and a safe notes feed Gives the operator two new controls on a collection-pool stop, and fixes the audit feed they write into. Back-to-pending (`items/[id]/back-to-pending`) A stop has to come back when the driver can't take it — no-show, wrong date, supplier pushed the delivery — or when the supplier confirmation was premature. A row can be assigned to a driver and/or marked ready at the same time, so the route clears both in one write rather than making the operator unwind them in the wrong order. Collection date (`items/[id]/collection-date`) `order_items.collection_date` was a PB date field that existed but nothing read or wrote it. On a PENDING stop it is a forecast — the supplier said goods land on the 20th, so the stop stays un-assignable because promising a driver a pickup for goods that don't exist yet would be a lie. On a READY stop it is the confirmed collection day. Notes feed (`src/lib/note-feed.ts`) `notes` on order_items / stock_purchase_items is NOT free text — it is a JSON array of NoteEntry objects, and every consumer parses it as one. The assign route's "mark ready" path wrote a plain string into it, which silently destroys the entire history: the next parse falls back to an empty feed. All writers now go through `appendNote` with the row's current value. Lives in `src/lib` rather than a page view model because both the client pages and the server routes need it, and an API route must never import from `app/(app)`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-15 22:44:49 +00:00
/**
* Add a field to a LIVE PocketBase collection, idempotently.
*
* Exists because this repo's `pb_schema (1).json` is a stale export:
* it lists fields the database does not have (and omits whole
* collections). PocketBase SILENTLY DROPS unknown fields on update — no
* error, no failed response — so code written against a phantom field
* appears to work while storing nothing.
*
* SAFETY:
* - Prints the current state and exits 0 if the field already exists.
* - Sends the FULL `fields[]` array back with one addition.
* `PATCH /api/collections/:c/fields/:id` 404s on PocketBase, so the
* collection-level PATCH is the supported route.
* - Re-reads afterwards and diffs field-by-field, failing loudly if
* anything other than the addition changed.
*
* Usage:
* npx tsx scripts/add-collection-field.ts order_items collection_date date
* npx tsx scripts/add-collection-field.ts order_items pool_entered_at date
*/
import { config } from "dotenv";
config({ path: ".env.local" });
config({ path: ".env" });
const url = process.env.NEXT_PUBLIC_POCKETBASE_URL || "";
const email = process.env.PB_ADMIN_EMAIL || "";
const password = process.env.PB_ADMIN_PASSWORD || "";
type Field = Record<string, unknown> & { name: string; type: string };
const [, , collectionArg, nameArg, typeArg] = process.argv;
if (!collectionArg || !nameArg || !typeArg) {
console.error(
"usage: npx tsx scripts/add-collection-field.ts <collection> <field_name> <type>",
);
process.exit(2);
}
async function auth(): Promise<string> {
const res = await fetch(`${url}/api/collections/_superusers/auth-with-password`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ identity: email, password }),
});
if (!res.ok) throw new Error(`auth failed: ${res.status} ${await res.text()}`);
return ((await res.json()) as { token: string }).token;
}
async function getCollection(token: string, name: string) {
const res = await fetch(`${url}/api/collections/${name}`, {
headers: { Authorization: token },
});
if (!res.ok) throw new Error(`get ${name} failed: ${res.status} ${await res.text()}`);
return (await res.json()) as { fields: Field[] };
}
async function main() {
if (!url || !email || !password) throw new Error("missing PB env vars");
const token = await auth();
const before = await getCollection(token, collectionArg);
console.log(`${collectionArg}: ${before.fields.length} fields`);
const existing = before.fields.find((f) => f.name === nameArg);
if (existing) {
console.log(`\n${nameArg} already exists (${existing.type}) — nothing to do.`);
return;
}
// Copy the shape of an existing field of the same type so the new one
// matches its siblings (PB stores per-field config beyond type/name).
const template = before.fields.find((f) => f.type === typeArg);
const newField: Field = {
...(template ?? {}),
id: `fld_${nameArg.replace(/[^a-z0-9]/gi, "_").slice(0, 11)}`,
name: nameArg,
type: typeArg,
required: false,
presentable: false,
hidden: false,
system: false,
};
const fields = [...before.fields, newField];
const patchRes = await fetch(`${url}/api/collections/${collectionArg}`, {
method: "PATCH",
headers: { Authorization: token, "Content-Type": "application/json" },
body: JSON.stringify({ fields }),
});
if (!patchRes.ok) {
throw new Error(`patch failed: ${patchRes.status} ${await patchRes.text()}`);
}
const after = await getCollection(token, collectionArg);
const added = after.fields.filter((f) => !before.fields.some((b) => b.name === f.name));
const removed = before.fields.filter((f) => !after.fields.some((a) => a.name === f.name));
const changed = after.fields.filter((a) => {
const b = before.fields.find((x) => x.name === a.name);
return b && JSON.stringify(b) !== JSON.stringify(a);
});
console.log(`\nadded: ${added.map((f) => `${f.name} (${f.type})`).join(", ") || "(none)"}`);
console.log(`removed: ${removed.map((f) => f.name).join(", ") || "(none)"}`);
console.log(`changed: ${changed.map((f) => f.name).join(", ") || "(none)"}`);
if (added.length !== 1 || removed.length > 0 || changed.length > 0) {
throw new Error("UNEXPECTED schema drift — inspect before proceeding.");
}
console.log(`\nOK: ${nameArg} added, nothing else moved.`);
}
void main().catch((err: unknown) => {
console.error(`FAILED: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
});