- Add ItemDetailModal showing PO document (view/download/share/email) + buyer notes - Add FileText (PO) and MessageSquare (notes) buttons to compact item list with popovers - Fix photo upload: append to order_items.order_item_attachements (PB file field) - Add colored left sidebar stripe per supplier group for visual distinction - Fix popover z-index so they appear above following cards (overflow-visible + z-50) - Extend PurchaseOrder type with documents, email_payload, supplier - Add order_item_attachements and expand.purchase_order to OrderItem - onShowDetails prop wired through SupplierGroup and AreaGroup Co-authored-by: Orca <help@stably.ai>
117 lines
4.8 KiB
JavaScript
117 lines
4.8 KiB
JavaScript
#!/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"' },
|
||
{ 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.`);
|