Files
meseo-staging/scripts/check-raw-siteconversion.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

98 lines
4.9 KiB
JavaScript

/**
* Guard: no raw SiteConversion queries without countedConversionWhere
* ────────────────────────────────────────────────────────────────────
* Fails the build when a file contains a direct prisma.siteConversion
* count/findMany/groupBy call that neither:
* (a) imports countedConversionWhere, nor
* (b) carries an explicit "// intentionally unfiltered:" comment on
* the same or immediately preceding line.
*
* Admin/debug/backfill endpoints that legitimately need raw rows MUST
* add: // intentionally unfiltered: <reason>
* to suppress the check.
*
* Run: node scripts/check-raw-siteconversion.mjs
*/
import { readFileSync, readdirSync, statSync } from "fs";
import { join, relative } from "path";
const ROOT = join(new URL(".", import.meta.url).pathname, "..");
const SRC = join(ROOT, "src");
const RAW_CALL_RE = /prisma\.siteConversion\.(count|findMany|groupBy|aggregate)\s*\(/g;
// Lines that are part of a comment block should not be flagged
const COMMENT_LINE_RE = /^\s*(\*|\/\/|\/\*)/;
const UNFILTERED_COMMENT_RE = /\/\/\s*intentionally unfiltered:/i;
const IMPORT_HELPER_RE = /import\s+[^;]*countedConversionWhere[^;]*from/;
function walkFiles(dir, results = []) {
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
const stat = statSync(full);
if (stat.isDirectory()) {
// Skip node_modules and .next
if (entry === "node_modules" || entry === ".next") continue;
walkFiles(full, results);
} else if (full.endsWith(".ts") || full.endsWith(".tsx")) {
results.push(full);
}
}
return results;
}
const files = walkFiles(SRC);
const violations = [];
for (const filePath of files) {
const content = readFileSync(filePath, "utf8");
const hasHelper = IMPORT_HELPER_RE.test(content);
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!RAW_CALL_RE.test(line) || COMMENT_LINE_RE.test(line)) {
RAW_CALL_RE.lastIndex = 0;
continue;
}
RAW_CALL_RE.lastIndex = 0;
// If the file imports countedConversionWhere, it's opted in — trust the author.
if (hasHelper) continue;
// Check for an intentionally-unfiltered comment on this line or the line above.
const prevLine = i > 0 ? lines[i - 1] : "";
if (UNFILTERED_COMMENT_RE.test(line) || UNFILTERED_COMMENT_RE.test(prevLine)) continue;
violations.push(` ${relative(ROOT, filePath)}:${i + 1}${line.trim().slice(0, 80)}`);
}
}
if (violations.length > 0) {
console.error("");
console.error("╔══════════════════════════════════════════════════════════════════════╗");
console.error("║ RAW SITECONVERSION CHECK FAILED ║");
console.error("╠══════════════════════════════════════════════════════════════════════╣");
console.error("║ The following files query SiteConversion without using ║");
console.error("║ countedConversionWhere() or an explicit unfiltered comment. ║");
console.error("║ ║");
console.error("║ User-facing conversion numbers MUST route through the helper so ║");
console.error("║ isDuplicateOf / invalidatedReason / EXCLUDED_CONVERSION_TYPES are ║");
console.error("║ applied consistently. ║");
console.error("║ ║");
console.error("║ Fix options: ║");
console.error("║ 1. import { countedConversionWhere } from ║");
console.error('║ "@/lib/conversions/counted-where" ║');
console.error("║ and use it in the where clause. ║");
console.error("║ 2. Add: // intentionally unfiltered: <reason> ║");
console.error("║ on the line before the query for debug/admin/backfill paths. ║");
console.error("╚══════════════════════════════════════════════════════════════════════╝");
console.error("");
violations.forEach((v) => console.error(v));
console.error("");
process.exit(1);
}
console.log(`[check-raw-siteconversion] OK — ${files.length} files checked, no violations.`);
process.exit(0);