WFDASHDRIVER-APP/scripts/check-pb-schema.mjs
Dave 22bf5dab81 feat(pocketbase): merge stock_purchase_items into Assigned list
Stock pickups live in stock_purchase_items (status="assigned_to_driver"),
not order_items. Previously getMyAssignedItems() only queried order_items,
so a driver with a stock pickup assigned to them saw nothing in the
Assigned tab or Map.

- getMyAssignedStockItems() fetches stock_purchase_items with the
  matching drv_id/status filter and expands purchase.supplier +
  stock_item. A toOrderItemFromStock() reshaper puts each row onto
  the OrderItem shape so the existing card/modal/map components
  render stock pickups unchanged; a kind:'pickup' discriminator
  tags them for the write side.
- handleCollect / handleProblem / handleBulkCollect / handleBulkProblem
  branch on item.kind to PATCH stock_purchase_items instead of
  order_items (status="collected" + quantity_received on collect,
  problem_flag + problem_type + problem_notes on problem; mirrors
  the order_items quantity-mismatch auto-notes).
- Same merge on app/map/page.tsx so the stock stop lands on the map.
- scripts/check-pb-schema.mjs now covers the stock filter so a future
  PB rename is caught at prebuild.
2026-08-17 22:33:10 +02:00

121 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// scripts/check-pb-schema.mjs
//
// Pre-build sanity check: probes every PocketBase filter used by the driver
// app against the live server and reports any reference to an unknown
// field. Catches the exact "added a clause to a filter, forgot to add the
// field to PB" class of bug that produced the July-2026 4×400 incident.
//
// Usage:
// npm run check-pb-schema
//
// Env (optional — defaults match this project's dev PB + e2e creds):
// NEXT_PUBLIC_POCKETBASE_URL=https://your-pb.example.com
// PB_USER_EMAIL=...
// PB_USER_PASSWORD=...
//
// Exit codes:
// 0 all filters and identifiers parse against the live schema
// 1 one or more filters reference unknown fields
// 2 couldn't authenticate against PB (fix env / network first)
import PocketBase from 'pocketbase';
// --- config ---------------------------------------------------------
const PB = process.env.NEXT_PUBLIC_POCKETBASE_URL
|| 'https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io';
const EMAIL = process.env.PB_USER_EMAIL || 'david.els1@outlook.com';
const PASSWORD = process.env.PB_USER_PASSWORD || '1234567890';
// One row per `filter:` string in `lib/pocketbase.ts`. Literal values are
// placeholders — we don't want to MATCH data, we want the filter to PARSE.
// PB validates identifier names at the parser level and 400s the moment
// it hits one it doesn't know, regardless of the value.
const FILTERS = [
{ collection: 'order_items', filter: 'drv_id = "placeholder" && item_status = "assigned"' },
{ collection: 'order_items', filter: 'item_status = "collection_pool"' },
{ collection: 'order_items', filter: 'problem_flag = true || item_status = "problem"' },
// Stock pickups live in their own collection — see
// getMyAssignedStockItems in lib/pocketbase.ts. The driver app
// was silently dropping these rows before this filter was added.
{ collection: 'stock_purchase_items', filter: 'drv_id = "placeholder" && status = "assigned_to_driver"' },
{ collection: 'driver_locations', filter: 'relation = "placeholder" && timestamp >= "2026-01-01T00:00:00Z"' },
];
// --- helpers (declared up here so they're in scope for the loop) ----
function truncate(s, n) {
return s.length > n ? s.slice(0, n - 1) + '…' : s;
}
// PB operator tokens we want to skip when picking identifiers out of a
// filter string. Anything not in here AND not in a quoted string AND
// matching `/^[a-zA-Z_]…/` is treated as a candidate field name.
const OPS = new Set(['&&', '||', '!=', '=', '<', '>', '<=', '>=']);
function extractIdentifiers(filter) {
const out = new Set();
// Strip quoted strings so their contents aren't picked up as idents.
const stripped = filter.replace(/"[^"]*"/g, '');
for (const tok of stripped.split(/[\s()]+/)) {
if (!tok) continue;
if (OPS.has(tok)) continue;
if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(tok)) continue;
// Reduce `expand.path` to just `expand` for the bisect probe —
// the relation FIELD is what PB validates, not the remote column.
out.add(tok.split('.')[0]);
}
return [...out];
}
// --- run ------------------------------------------------------------
const pb = new PocketBase(PB);
pb.autoCancellation(false);
console.log(`[check-pb-schema] target: ${PB}`);
try {
await pb.collection('users').authWithPassword(EMAIL, PASSWORD);
} catch (e) {
console.error(`[check-pb-schema] FATAL: cannot authenticate — ${e.message}`);
console.error(` check NEXT_PUBLIC_POCKETBASE_URL / PB_USER_EMAIL / PB_USER_PASSWORD`);
process.exit(2);
}
console.log(`[check-pb-schema] auth: OK (as ${pb.authStore.record?.id ?? EMAIL})\n`);
let failed = 0;
// Pass 1 — probe each filter at face value. Most filters are happy
// here and the loop is fast.
console.log('Pass 1 — probe every filter at face value:');
for (const { collection, filter } of FILTERS) {
try {
await pb.collection(collection).getList(1, 1, { filter });
console.log(` [OK] ${collection.padEnd(24)} ${truncate(filter, 70)}`);
} catch (e) {
failed++;
console.log(` [FAIL] ${collection.padEnd(24)} ${truncate(filter, 70)}`);
console.log(` status=${e.status ?? '??'} msg="${e.message}"`);
// Pass 2 — bisect: probe each top-level identifier in the filter
// as its own one-clause filter. Whatever 400s is an unknown field.
// Unknown fields 400 regardless of the operator/value used.
const suspects = extractIdentifiers(filter);
for (const id of suspects) {
try {
await pb.collection(collection).getList(1, 1, { filter: `${id} = ""` });
console.log(` · ok : ${id}`);
} catch (probeErr) {
console.log(` ⚠ unknown : ${id} (PB: ${probeErr.message})`);
}
}
}
}
console.log('');
if (failed > 0) {
console.error(`[check-pb-schema] ${failed} filter(s) reference unknown fields.`);
console.error(`[check-pb-schema] Either add the field to PB, or remove the clause from the filter.`);
console.error(`[check-pb-schema] See lib/pocketbase.ts header for verified-live schema notes.`);
process.exit(1);
}
console.log(`[check-pb-schema] all filters valid.`);