130 lines
4.5 KiB
TypeScript
130 lines
4.5 KiB
TypeScript
|
|
import "./_env-loader";
|
||
|
|
|
||
|
|
import { getAdminPocketBase } from "@/lib/server/pocketbase-admin";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Migration — 2026-08-13-fix-delivery-notes-relations.ts
|
||
|
|
*
|
||
|
|
* `delivery_notes` was created with relation fields (`customer`,
|
||
|
|
* `company`, `order`, `created_by`) but the live schema has
|
||
|
|
* `collectionId: null` on those fields, so any `expand=...` query
|
||
|
|
* silently returns nothing. The list page relies on
|
||
|
|
* `expand=company.company` for per-company grouping, so all DNs
|
||
|
|
* collapse into a single "No company" bucket — and crucially, the
|
||
|
|
* records remain in the table, but grouped wrong (and reported
|
||
|
|
* missing when the operator searches by number).
|
||
|
|
*
|
||
|
|
* This migration patches the relation targets so expansion resolves,
|
||
|
|
* then backfills `delivery_notes.company` from the parent order's
|
||
|
|
* company where it's currently empty (defensive — most records
|
||
|
|
* already have the right value from the original create path).
|
||
|
|
*
|
||
|
|
* Run:
|
||
|
|
* npx tsx scripts/migrations/2026-08-13-fix-delivery-notes-relations.ts
|
||
|
|
*/
|
||
|
|
|
||
|
|
const CUSTOMERS = "customers";
|
||
|
|
const COMPANIES = "companies";
|
||
|
|
const ORDERS = "orders";
|
||
|
|
const USERS = "users";
|
||
|
|
const DELIVERY_NOTES = "delivery_notes";
|
||
|
|
|
||
|
|
async function patchRelation(
|
||
|
|
pb: Awaited<ReturnType<typeof getAdminPocketBase>>,
|
||
|
|
collectionId: string,
|
||
|
|
fieldName: string,
|
||
|
|
targetCollectionId: string,
|
||
|
|
log: string[],
|
||
|
|
): Promise<void> {
|
||
|
|
const collection = await pb.collections.getOne(collectionId);
|
||
|
|
const field = (collection.fields ?? []).find((f) => f.name === fieldName);
|
||
|
|
if (!field) {
|
||
|
|
log.push(` ✗ ${fieldName} not found on ${collection.name}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (field.type !== "relation") {
|
||
|
|
log.push(` ✗ ${fieldName} is not a relation (type=${field.type})`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const opts = (field.options ?? {}) as { collectionId?: string | null };
|
||
|
|
if (opts.collectionId === targetCollectionId) {
|
||
|
|
log.push(` · ${fieldName}.collectionId already set — skip`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
log.push(` → patching ${fieldName}.collectionId to ${targetCollectionId}`);
|
||
|
|
await pb.collections.update(collection.id, {
|
||
|
|
fields: [
|
||
|
|
{ ...field, options: { ...opts, collectionId: targetCollectionId } },
|
||
|
|
],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async function backfillCompany(
|
||
|
|
pb: Awaited<ReturnType<typeof getAdminPocketBase>>,
|
||
|
|
log: string[],
|
||
|
|
): Promise<{ updated: number; skipped: number; failed: number }> {
|
||
|
|
let updated = 0;
|
||
|
|
let skipped = 0;
|
||
|
|
let failed = 0;
|
||
|
|
const records = await pb.collection(DELIVERY_NOTES).getFullList<{
|
||
|
|
id: string;
|
||
|
|
company: string;
|
||
|
|
order: string;
|
||
|
|
}>({ filter: "company = '' || company = null" });
|
||
|
|
log.push(` · ${records.length} delivery note(s) need a company backfill`);
|
||
|
|
for (const dn of records) {
|
||
|
|
if (!dn.order) {
|
||
|
|
skipped += 1;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const order = await pb.collection(ORDERS).getOne(dn.order, {
|
||
|
|
fields: "company",
|
||
|
|
});
|
||
|
|
const company = (order as unknown as { company?: string }).company;
|
||
|
|
if (!company) {
|
||
|
|
skipped += 1;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
await pb.collection(DELIVERY_NOTES).update(dn.id, { company });
|
||
|
|
updated += 1;
|
||
|
|
} catch (err) {
|
||
|
|
failed += 1;
|
||
|
|
log.push(
|
||
|
|
` ✗ ${dn.id} (${dn.delivery_note_number || dn.id}): ${err instanceof Error ? err.message : String(err)}`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return { updated, skipped, failed };
|
||
|
|
}
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
const log: string[] = [];
|
||
|
|
log.push("[migration:fix-delivery-notes-relations] starting");
|
||
|
|
const pb = await getAdminPocketBase();
|
||
|
|
|
||
|
|
const customersCol = await pb.collections.getOne(CUSTOMERS);
|
||
|
|
const companiesCol = await pb.collections.getOne(COMPANIES);
|
||
|
|
const ordersCol = await pb.collections.getOne(ORDERS);
|
||
|
|
const usersCol = await pb.collections.getOne(USERS);
|
||
|
|
|
||
|
|
log.push("[migration:fix-delivery-notes-relations] step 1 — patch relation targets");
|
||
|
|
await patchRelation(pb, DELIVERY_NOTES, "customer", customersCol.id, log);
|
||
|
|
await patchRelation(pb, DELIVERY_NOTES, "company", companiesCol.id, log);
|
||
|
|
await patchRelation(pb, DELIVERY_NOTES, "order", ordersCol.id, log);
|
||
|
|
await patchRelation(pb, DELIVERY_NOTES, "created_by", usersCol.id, log);
|
||
|
|
|
||
|
|
log.push("[migration:fix-delivery-notes-relations] step 2 — backfill empty company from order");
|
||
|
|
const { updated, skipped, failed } = await backfillCompany(pb, log);
|
||
|
|
log.push(
|
||
|
|
`[migration:fix-delivery-notes-relations] updated=${updated} skipped=${skipped} failed=${failed}`,
|
||
|
|
);
|
||
|
|
|
||
|
|
log.push("[migration:fix-delivery-notes-relations] done");
|
||
|
|
for (const line of log) console.log(line);
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch((err) => {
|
||
|
|
console.error("[migration:fix-delivery-notes-relations] fatal:", err);
|
||
|
|
process.exit(1);
|
||
|
|
});
|