WFDASHDRIVER-APP/scripts/check-pb-schema.mjs
Dave 4efce9290c feat(assigned): window by yesterday+today (driver-local timezone)
Assigned tab was previously unfiltered by date — every assigned row
in the system was shown, including items scheduled for the future
(e.g. operator pre-assigns for tomorrow, driver sees them today).

Live PB probe (2026-08-11) shows `assigned_at` is set to the
SCHEDULED pickup date, not the moment of assignment: 15 currently
assigned rows split evenly across Aug 18/19/20.

New filter: `assigned_at` must fall within a rolling 2-day window
in driver-local time (yesterday 00:00 → today 23:59:59.999). This
covers today's work plus any carryover from yesterday that didn't
get completed. Items scheduled for tomorrow or later stay hidden
until their day rolls around.

Window boundaries are computed in the driver's local timezone (this
is a browser app, so `new Date()` is local) then converted to UTC
ISO for the PB filter — works as long as driver + operator share a
timezone, which is the case for the current single-region
deployment. Empty-state copy updated so drivers understand why
nothing might show even when they know there are upcoming pickups.

check-pb-schema probe updated to match the new clauses so future
regressions fail the build instead of silently showing everything.
2026-08-18 15:46:37 +02:00

125 lines
5.5 KiB
JavaScript
Raw Permalink 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 = [
// Assigned-tab filter with the yesterday+today window. The ISO
// timestamps here are placeholders — PB only validates field names
// and operators at parse time, so any well-formed ISO string works.
// Keep in sync with getAssignedDayWindow in lib/pocketbase.ts.
{ collection: 'order_items', filter: 'drv_id = "placeholder" && item_status = "assigned" && assigned_at >= "2026-01-01T00:00:00.000Z" && assigned_at <= "2026-12-31T23:59:59.999Z"' },
{ 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.`);