Files
meseo-staging/scripts/prisma-safety-check.mjs
T
Andres Resiri 70daeb214a
CI / Quality gate (push) Has been cancelled
Initial project commit for Coolify staging
2026-08-03 10:45:41 -04:00

118 lines
4.9 KiB
JavaScript

/**
* Prisma Schema Safety Check
* ───────────────────────────
* Runs before `prisma db push` in the build pipeline.
* Aborts the build if the pending schema diff contains any destructive
* operations: DROP COLUMN, DROP TABLE, DROP CONSTRAINT, DROP INDEX,
* or ALTER COLUMN ... DROP.
*
* DROP INDEX is included because db push silently drops any index that
* is not declared in schema.prisma, even if it is a large production
* index (e.g. SiteEvent_sessionId_timestamp_idx on the ~22GB SiteEvent
* table). Losing such an index causes full table scans on hot queries.
*
* Override: set ALLOW_SCHEMA_DROPS=true in the build environment to skip.
*/
import { execSync } from "child_process";
const DATABASE_URL = process.env.DATABASE_URL;
if (!DATABASE_URL) {
console.log("[prisma-safety] DATABASE_URL not set — skipping safety check");
process.exit(0);
}
if (process.env.ALLOW_SCHEMA_DROPS === "true") {
console.log("[prisma-safety] ALLOW_SCHEMA_DROPS=true — skipping drop check");
process.exit(0);
}
console.log("[prisma-safety] Computing schema diff against live database...");
// Retry up to 3 times to handle transient P1001 connection errors on cold starts.
const MAX_RETRIES = 3;
let diffSql;
let lastErr;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
diffSql = execSync(
"npx prisma migrate diff --from-url $DATABASE_URL --to-schema-datamodel prisma/schema.prisma --script",
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] },
);
lastErr = null;
break;
} catch (err) {
lastErr = err;
const stderr = err.stderr ?? "";
const isTransient =
stderr.includes("P1001") ||
stderr.includes("Could not connect") ||
stderr.includes("connection");
if (isTransient && attempt < MAX_RETRIES) {
console.warn(`[prisma-safety] Connection attempt ${attempt} failed — retrying...`);
// brief pause before retry (synchronous busy-wait, safe in build context)
const wait = attempt * 2000;
const end = Date.now() + wait;
while (Date.now() < end) { /* spin */ }
continue;
}
if (isTransient) {
console.warn("[prisma-safety] Could not connect to database after retries — skipping safety check");
console.warn("[prisma-safety]", stderr.split("\n")[0]);
process.exit(0);
}
console.error("[prisma-safety] Failed to compute schema diff:");
console.error(stderr || err.message);
process.exit(1);
}
}
if (lastErr) {
// Should not reach here, but guard anyway.
console.warn("[prisma-safety] Exhausted retries — skipping safety check");
process.exit(0);
}
// Patterns that signal data-destructive or performance-destructive operations.
// DROP INDEX is included: db push silently drops undeclared indexes, turning
// hot queries into full table scans (root cause of the SiteEvent incident).
const DROP_PATTERNS = [
/^\s*ALTER TABLE\s+\S+\s+DROP COLUMN\s+/im,
/^\s*DROP TABLE\s+/im,
/^\s*DROP CONSTRAINT\s+/im,
/^\s*ALTER TABLE\s+\S+\s+DROP CONSTRAINT\s+/im,
/^\s*ALTER COLUMN\s+\S+\s+DROP\s+/im,
/^\s*DROP INDEX\s+/im,
];
const matchedStatements = DROP_PATTERNS
.flatMap((pattern) => {
const lines = diffSql.split("\n");
return lines.filter((line) => pattern.test(line));
})
.filter((line, i, arr) => arr.indexOf(line) === i); // deduplicate
if (matchedStatements.length > 0) {
console.error("");
console.error("╔══════════════════════════════════════════════════════════════╗");
console.error("║ PRISMA SAFETY CHECK FAILED — BUILD ABORTED ║");
console.error("╠══════════════════════════════════════════════════════════════╣");
console.error("║ The schema diff would execute the following destructive ║");
console.error("║ SQL statements that could permanently delete production ║");
console.error("║ data or drop production indexes (causing full table scans): ║");
console.error("╚══════════════════════════════════════════════════════════════╝");
console.error("");
matchedStatements.forEach((stmt) => console.error(" ⚠", stmt.trim()));
console.error("");
console.error(" Full diff SQL:");
console.error(" " + diffSql.split("\n").join("\n "));
console.error("");
console.error(" If this drop is intentional, set ALLOW_SCHEMA_DROPS=true");
console.error(" in the Vercel build environment and redeploy.");
console.error("");
process.exit(1);
}
console.log("[prisma-safety] No destructive operations found. Build can proceed.");
process.exit(0);