From 5d222ff2816e777f1113a0b49609a84b1a267474 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Thu, 9 Jul 2026 22:28:50 +0200 Subject: [PATCH] feat(po-pdf): add token backfill migration script --- package.json | 4 +- .../migrations/2026-07-09-po-pdf-tokens.ts | 92 +++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 scripts/migrations/2026-07-09-po-pdf-tokens.ts diff --git a/package.json b/package.json index bc5f33f..5d5a1ec 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "pb:migrate:price-list-sku-normalised": "npx tsx scripts/migrations/2026-06-30-add-price-list-sku-normalised.ts", "pb:migrate:quote-items-sku": "npx tsx scripts/migrations/2026-06-30-add-quote-items-sku-and-pricing.ts", "pb:migrate:rfq-outbound": "npx tsx scripts/migrations/2026-06-30-create-rfq-outbound.ts", - "pb:migrate:sprint-categories": "npx tsx scripts/migrations/2026-07-02-add-sprint-categories-and-claims.ts", + "pb:migrate:sprint-categories": "npx tsx scripts/migrations/2026-07-02-add-sprint-categories-and-claims.ts", + "pb:migrate:po-pdf-tokens": "npx tsx scripts/migrations/2026-07-09-po-pdf-tokens.ts", "smoke:counter": "npx tsx scripts/smoke/counter.ts", "smoke:counter-concurrent": "npx tsx scripts/smoke/counter-concurrent.ts", "smoke:create-from-email": "npx tsx scripts/smoke/create-from-email.ts", @@ -56,6 +57,7 @@ "lucide-react": "^1.16.0", "next": "16.2.6", "next-themes": "^0.4.6", + "pdf-lib": "^1.17.1", "pocketbase": "^0.26.9", "react": "19.2.4", "react-dom": "19.2.4", diff --git a/scripts/migrations/2026-07-09-po-pdf-tokens.ts b/scripts/migrations/2026-07-09-po-pdf-tokens.ts new file mode 100644 index 0000000..d7a2ebd --- /dev/null +++ b/scripts/migrations/2026-07-09-po-pdf-tokens.ts @@ -0,0 +1,92 @@ +/** + * One-shot backfill — write `token` into every PurchaseOrderDocumentEntry + * in `purchase_orders.documents_history` that doesn't have one yet. + * + * Why this exists + * ──────────────── + * The PO PDF route used to store the human-readable filename in + * `documents_history[i].token`, which then 404'd when PB's file proxy + * was asked to resolve it. The fix in the route handler stores the + * real on-disk token (the slug PB returns from the upload) instead — + * but legacy rows created before that change still have `file` and no + * `token`, so the new GET handler would 404 on them. + * + * Strategy: + * - For each PO row, read `documents_history` and `documents`. + * - For each entry missing `token`, look up `documents[idx]` (or + * `documents[0]` as a fallback). The two arrays are kept in sync + * by the POST handler, so index alignment is correct. + * - Patch the row with the new history array. + * + * Idempotent — re-running this script is a no-op once every entry has + * a token. + * + * Run with (from WFDASH_NEXTJS so .env.local is found): + * npx tsx scripts/migrations/2026-07-09-po-pdf-tokens.ts + */ +import { getAdminPocketBase } from "@/lib/server/pocketbase-admin"; + +type Entry = { + version: number; + file: string; + token?: string; + generatedAt: string; + generatedBy: string; + reason: string; + status: "current" | "superseded"; +}; + +type PoRow = { + id: string; + documents?: string[] | string; + documents_history?: Entry[]; +}; + +async function main() { + const pb = await getAdminPocketBase(); + let patchedRows = 0; + let patchedEntries = 0; + let unresolvable = 0; + + const list = await pb.collection("purchase_orders").getList(1, 500, { + fields: "id,documents,documents_history", + filter: "documents_history != null", + }); + + for (const po of list.items) { + const history = po.documents_history ?? []; + if (history.length === 0) continue; + + const docsRaw = po.documents ?? []; + const docs = Array.isArray(docsRaw) ? docsRaw : docsRaw ? [docsRaw] : []; + + let changed = false; + const newHistory = history.map((entry, idx) => { + if (entry.token) return entry; + const token = docs[idx] || docs[0] || ""; + if (!token) { + unresolvable += 1; + return entry; + } + changed = true; + return { ...entry, token }; + }); + + if (changed) { + await pb.collection("purchase_orders").update(po.id, { + documents_history: newHistory, + }); + patchedRows += 1; + patchedEntries += newHistory.filter((e) => e.token).length - history.filter((e) => e.token).length; + } + } + + console.log( + `[po-pdf-tokens] rows patched=${patchedRows} entries patched=${patchedEntries} unresolvable=${unresolvable}`, + ); +} + +main().catch((err) => { + console.error("[po-pdf-tokens] failed:", err); + process.exit(1); +});