WFDASHDRIVER-APP/scripts/check-pb-schema.mjs
SJCoetz 85662380a2 feat: add order item photo uploads and fix PB bugs
- fix driver_locations schema mismatch: use `relation` instead of `driver` per live PB schema
- remove invalid `ready_for_collection` filter that caused silent 400 errors
- add order_item_attachments support for driver pickup photos with proper error handling:
  * replace legacy collection_photo field with new collection for multiple per-item files
  * upload photos after updating item status to avoid stranding assigned items
  * show warning toast if upload fails instead of success confirmation
  * update toast messages to reflect photo upload status
- add pre-build schema check script to catch invalid PB field references pre-deploy
- add corresponding npm scripts and prebuild hook for the schema check
- add e2e test scripts for collection and upload flows
- update type definitions and pocketbase SDK helpers for new schema and functionality
2026-07-08 01:35:26 +02:00

118 lines
4.9 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"' },
{ collection: 'order_item_attachments', filter: 'order_item = "placeholder"' },
{ 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.`);