126 lines
4.5 KiB
TypeScript
126 lines
4.5 KiB
TypeScript
|
|
/**
|
||
|
|
* Backfill — flip `ready_for_collection` to `true` on every row that is
|
||
|
|
* already `item_status = "assigned"` but never had the flag set.
|
||
|
|
*
|
||
|
|
* Why this exists as a sibling to the original migration:
|
||
|
|
*
|
||
|
|
* `2026-07-07-order-items-ready-for-collection.ts` introduced the
|
||
|
|
* `ready_for_collection` bool AND backfilled every existing
|
||
|
|
* `collection_pool` row to `true`. It deliberately did NOT touch
|
||
|
|
* rows that were already `assigned` — at the time the migration
|
||
|
|
* ran, the theory was "assigned items have already been hand-vetted
|
||
|
|
* by the operator, so the flag should already be on them, so we
|
||
|
|
* don't need to backfill".
|
||
|
|
*
|
||
|
|
* That theory was wrong. In practice, several `assigned` rows had
|
||
|
|
* been created via paths that did not set the flag (direct PB admin
|
||
|
|
* edits, an older assignment workflow that pre-dated the column).
|
||
|
|
* The driver app's `getMyAssignedItems` filter requires
|
||
|
|
* `ready_for_collection = true`, so the driver sees an empty
|
||
|
|
* Assigned tab even though they have real stops in the DB.
|
||
|
|
*
|
||
|
|
* This script mirrors the original pool-backfill shape so the gap
|
||
|
|
* closes without forcing the operator to re-toggle every row in the
|
||
|
|
* dashboard.
|
||
|
|
*
|
||
|
|
* Idempotent — only flips rows where `ready_for_collection != true`.
|
||
|
|
*
|
||
|
|
* Run with (from WFDASH_NEXTJS so .env.local is found):
|
||
|
|
* npx tsx scripts/migrations/2026-07-07-backfill-assigned-rfc.ts
|
||
|
|
*/
|
||
|
|
/* eslint-disable */
|
||
|
|
import PocketBase from "pocketbase";
|
||
|
|
import { readFileSync } from "node:fs";
|
||
|
|
import { resolve } from "node:path";
|
||
|
|
|
||
|
|
function loadEnvFile(name: string) {
|
||
|
|
try {
|
||
|
|
const raw = readFileSync(resolve(process.cwd(), name), "utf8");
|
||
|
|
for (const line of raw.split("\n")) {
|
||
|
|
const trimmed = line.trim();
|
||
|
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
||
|
|
const m = trimmed.match(/^([A-Z0-9_]+)\s*=\s*(.+)$/i);
|
||
|
|
if (m && !process.env[m[1]]) {
|
||
|
|
process.env[m[1]] = m[2].replace(/^["']|["']$/g, "").trim();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
/* fine — rely on shell env */
|
||
|
|
}
|
||
|
|
}
|
||
|
|
loadEnvFile(".env");
|
||
|
|
loadEnvFile(".env.local");
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
const url = process.env.NEXT_PUBLIC_POCKETBASE_URL;
|
||
|
|
const email = process.env.PB_ADMIN_EMAIL;
|
||
|
|
const password = process.env.PB_ADMIN_PASSWORD;
|
||
|
|
if (!url || !email || !password) {
|
||
|
|
throw new Error(
|
||
|
|
"Set NEXT_PUBLIC_POCKETBASE_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD (e.g. sourced from .env.local).",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const pb = new PocketBase(url);
|
||
|
|
await pb.collection("_superusers").authWithPassword(email, password);
|
||
|
|
console.log(`[backfill-assigned-rfc] Authenticated against ${url}`);
|
||
|
|
|
||
|
|
// Sanity check — make sure the column exists before we query on it.
|
||
|
|
const col = await pb.collections.getOne("order_items");
|
||
|
|
const hasField = (col.fields as Array<{ name: string }>).some(
|
||
|
|
(f) => f.name === "ready_for_collection",
|
||
|
|
);
|
||
|
|
if (!hasField) {
|
||
|
|
throw new Error(
|
||
|
|
"ready_for_collection field missing on order_items — run 2026-07-07-order-items-ready-for-collection.ts first.",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find every assigned row that isn't already ready.
|
||
|
|
// `!= true` matches false AND null — null is the default for rows
|
||
|
|
// created before the column existed, plus any rows where the
|
||
|
|
// boolean was never written.
|
||
|
|
const rows = await pb.collection("order_items").getFullList({
|
||
|
|
filter: 'item_status = "assigned" && ready_for_collection != true',
|
||
|
|
fields: "id,item_status,ready_for_collection,drv_id",
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log(
|
||
|
|
`[backfill-assigned-rfc] candidates (assigned, rfc!=true): ${rows.length}`,
|
||
|
|
);
|
||
|
|
|
||
|
|
let flipped = 0;
|
||
|
|
let failed = 0;
|
||
|
|
for (const row of rows) {
|
||
|
|
try {
|
||
|
|
await pb.collection("order_items").update(row.id, {
|
||
|
|
ready_for_collection: true,
|
||
|
|
});
|
||
|
|
flipped++;
|
||
|
|
} catch (err) {
|
||
|
|
failed++;
|
||
|
|
console.error(
|
||
|
|
`[backfill-assigned-rfc] update failed for ${row.id}:`,
|
||
|
|
err instanceof Error ? err.message : err,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Post-state snapshot — confirm the filter now returns 0.
|
||
|
|
const remaining = await pb.collection("order_items").getFullList({
|
||
|
|
filter: 'item_status = "assigned" && ready_for_collection != true',
|
||
|
|
fields: "id",
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log(`[backfill-assigned-rfc] flipped: ${flipped}`);
|
||
|
|
console.log(`[backfill-assigned-rfc] failed: ${failed}`);
|
||
|
|
console.log(
|
||
|
|
`[backfill-assigned-rfc] post-state: ${remaining.length} assigned rows still have rfc!=true`,
|
||
|
|
);
|
||
|
|
console.log("[backfill-assigned-rfc] Done.");
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch((err) => {
|
||
|
|
console.error("[backfill-assigned-rfc] failed:", err);
|
||
|
|
process.exit(1);
|
||
|
|
});
|