fix(reset-db): app_logs + tariff_drafts were uncategorized — add a drift guard
Both tables belonged to NO reset category and silently survived every reset, --all included (the hand-maintained table list lagged the schema twice). app_logs gets a new --diagnostics category; tariff_drafts joins --config. A drift guard now compares the category union against sqlite_master before doing anything and refuses on any uncategorized table, so the next new table forces a deliberate one-line decision instead of escaping by omission. Verified on a scratch DB: guard refuses a planted table (exit 1), --all lists both new tables, --diagnostics wipes app_logs. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -17,9 +17,17 @@
|
|||||||
// their credentials/plates, blocklist. KEEPS users, devices, config,
|
// their credentials/plates, blocklist. KEEPS users, devices, config,
|
||||||
// tariffs, subscription PLANS.
|
// tariffs, subscription PLANS.
|
||||||
// --config site_config, devices, setup_state (re-runs first-run setup),
|
// --config site_config, devices, setup_state (re-runs first-run setup),
|
||||||
// tariffs + tariff_versions, subscription_plans.
|
// tariffs + tariff_versions + tariff_drafts, subscription_plans.
|
||||||
// --users users, roles, role_permissions, auth sessions. (After this or --all,
|
// --users users, roles, role_permissions, auth sessions. (After this or --all,
|
||||||
// re-seed an admin: apps/server/scripts/seed-admin.mjs.)
|
// re-seed an admin: apps/server/scripts/seed-admin.mjs.)
|
||||||
|
// --diagnostics app_logs (the unsigned diagnostic store behind the /setup/logs
|
||||||
|
// viewer). Separate from --financial: logs are evidence about the BOX,
|
||||||
|
// not the traffic — wipe them only when handing over a blank slate.
|
||||||
|
//
|
||||||
|
// DRIFT GUARD: before doing anything, the script compares the union of the categories
|
||||||
|
// above against the tables actually present in the DB and REFUSES if any table is
|
||||||
|
// uncategorized — so a new table can't silently survive resets (app_logs and
|
||||||
|
// tariff_drafts did exactly that until 2026-07-08).
|
||||||
//
|
//
|
||||||
// Safety gates (BOTH required):
|
// Safety gates (BOTH required):
|
||||||
// 1. env RESET_ALLOWED=1 — a real booth never sets this.
|
// 1. env RESET_ALLOWED=1 — a real booth never sets this.
|
||||||
@@ -44,10 +52,39 @@ const CATEGORIES = {
|
|||||||
"subscriptions",
|
"subscriptions",
|
||||||
"blocklist",
|
"blocklist",
|
||||||
],
|
],
|
||||||
config: ["site_config", "devices", "setup_state", "tariff_versions", "tariffs", "subscription_plans"],
|
config: [
|
||||||
|
"site_config",
|
||||||
|
"devices",
|
||||||
|
"setup_state",
|
||||||
|
"tariff_drafts",
|
||||||
|
"tariff_versions",
|
||||||
|
"tariffs",
|
||||||
|
"subscription_plans",
|
||||||
|
],
|
||||||
users: ["sessions", "role_permissions", "users", "roles"],
|
users: ["sessions", "role_permissions", "users", "roles"],
|
||||||
|
diagnostics: ["app_logs"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Every user table in the DB must belong to a category above (internal bookkeeping
|
||||||
|
* like sqlite_* and drizzle's __* migration table excepted). Dies listing offenders —
|
||||||
|
* the fix is a one-line addition to CATEGORIES, decided deliberately, not by omission. */
|
||||||
|
function assertNoUncategorizedTables(sqlite) {
|
||||||
|
const known = new Set(Object.values(CATEGORIES).flat());
|
||||||
|
const actual = sqlite
|
||||||
|
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table'`)
|
||||||
|
.all()
|
||||||
|
.map((r) => r.name)
|
||||||
|
.filter((n) => !n.startsWith("sqlite_") && !n.startsWith("__"));
|
||||||
|
const uncategorized = actual.filter((n) => !known.has(n));
|
||||||
|
if (uncategorized.length > 0) {
|
||||||
|
die(
|
||||||
|
`schema drift — table(s) not covered by any reset category: ${uncategorized.join(", ")}\n` +
|
||||||
|
` add them to CATEGORIES in packages/db/scripts/reset-db.mjs (this guard exists so\n` +
|
||||||
|
` new tables can't silently survive resets).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseArgs(argv) {
|
function parseArgs(argv) {
|
||||||
const flags = new Set(argv.filter((a) => a.startsWith("--")).map((a) => a.slice(2)));
|
const flags = new Set(argv.filter((a) => a.startsWith("--")).map((a) => a.slice(2)));
|
||||||
const wantAll = flags.has("all");
|
const wantAll = flags.has("all");
|
||||||
@@ -75,7 +112,7 @@ async function main() {
|
|||||||
|
|
||||||
const { cats, autoYes, wantAll } = parseArgs(process.argv.slice(2));
|
const { cats, autoYes, wantAll } = parseArgs(process.argv.slice(2));
|
||||||
if (cats.length === 0) {
|
if (cats.length === 0) {
|
||||||
die("nothing to do — pass --all, --financial, --config, and/or --users");
|
die("nothing to do — pass --all, --financial, --config, --users, and/or --diagnostics");
|
||||||
}
|
}
|
||||||
|
|
||||||
// GATE 1: env opt-in. A production booth never sets this.
|
// GATE 1: env opt-in. A production booth never sets this.
|
||||||
@@ -86,6 +123,11 @@ async function main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open early: the drift guard must run BEFORE anything is printed or confirmed, so
|
||||||
|
// an uncategorized table aborts the whole run rather than surviving a "successful" reset.
|
||||||
|
const sqlite = new Database(dbPath);
|
||||||
|
assertNoUncategorizedTables(sqlite);
|
||||||
|
|
||||||
// Resolve the ordered, de-duplicated table list for the chosen categories.
|
// Resolve the ordered, de-duplicated table list for the chosen categories.
|
||||||
const tables = [];
|
const tables = [];
|
||||||
for (const c of cats) for (const t of CATEGORIES[c]) if (!tables.includes(t)) tables.push(t);
|
for (const c of cats) for (const t of CATEGORIES[c]) if (!tables.includes(t)) tables.push(t);
|
||||||
@@ -106,7 +148,6 @@ async function main() {
|
|||||||
if (!ok) die("confirmation did not match — aborted, nothing changed.");
|
if (!ok) die("confirmation did not match — aborted, nothing changed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const sqlite = new Database(dbPath);
|
|
||||||
try {
|
try {
|
||||||
// FKs OFF for the wipe so we can delete in any order without ordering hazards;
|
// FKs OFF for the wipe so we can delete in any order without ordering hazards;
|
||||||
// a single transaction makes it all-or-nothing.
|
// a single transaction makes it all-or-nothing.
|
||||||
|
|||||||
Reference in New Issue
Block a user