costco-grocery-list/scripts/db-stale-sql-tracker.js
Nico 77ae5be445
All checks were successful
Build & Deploy Costco Grocery List / build (push) Successful in 1m10s
Build & Deploy Costco Grocery List / verify-images (push) Successful in 3s
Build & Deploy Costco Grocery List / deploy (push) Successful in 11s
Build & Deploy Costco Grocery List / notify (push) Successful in 1s
refactor
2026-02-22 01:27:03 -08:00

188 lines
5.2 KiB
JavaScript

"use strict";
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const repoRoot = path.resolve(__dirname, "..");
const canonicalDir = path.resolve(repoRoot, "packages", "db", "migrations");
const legacyDir = path.resolve(repoRoot, "backend", "migrations");
const defaultReportPath = path.resolve(legacyDir, "stale-sql-report.json");
function parseArgs(argv) {
const args = new Set(argv);
return {
write: args.has("--write"),
failOnStale: args.has("--fail-on-stale"),
help: args.has("--help"),
};
}
function ensureDirectoryExists(dirPath, label) {
if (!fs.existsSync(dirPath)) {
throw new Error(`${label} directory not found: ${dirPath}`);
}
}
function sha256File(filePath) {
const hash = crypto.createHash("sha256");
hash.update(fs.readFileSync(filePath));
return hash.digest("hex");
}
function listFiles(dirPath) {
return fs
.readdirSync(dirPath)
.filter((name) => fs.statSync(path.join(dirPath, name)).isFile())
.sort((a, b) => a.localeCompare(b));
}
function listSqlFiles(dirPath) {
return listFiles(dirPath).filter((name) => name.toLowerCase().endsWith(".sql"));
}
function mapByNameWithHash(dirPath, names) {
const map = new Map();
for (const name of names) {
map.set(name, {
name,
path: path.join(dirPath, name),
sha256: sha256File(path.join(dirPath, name)),
});
}
return map;
}
function buildReport() {
ensureDirectoryExists(canonicalDir, "Canonical migrations");
ensureDirectoryExists(legacyDir, "Legacy migrations");
const canonicalSql = listSqlFiles(canonicalDir);
const legacySql = listSqlFiles(legacyDir);
const legacyNonSql = listFiles(legacyDir).filter(
(name) => !name.toLowerCase().endsWith(".sql")
);
const canonicalMap = mapByNameWithHash(canonicalDir, canonicalSql);
const legacyMap = mapByNameWithHash(legacyDir, legacySql);
const staleFiles = [];
for (const legacyName of legacySql) {
const legacyFile = legacyMap.get(legacyName);
const canonicalFile = canonicalMap.get(legacyName);
if (!canonicalFile) {
staleFiles.push({
filename: legacyName,
status: "STALE_ONLY_IN_BACKEND",
backend_sha256: legacyFile.sha256,
});
continue;
}
if (legacyFile.sha256 === canonicalFile.sha256) {
staleFiles.push({
filename: legacyName,
status: "STALE_DUPLICATE_OF_CANONICAL",
backend_sha256: legacyFile.sha256,
canonical_sha256: canonicalFile.sha256,
});
continue;
}
staleFiles.push({
filename: legacyName,
status: "STALE_DIVERGED_FROM_CANONICAL",
backend_sha256: legacyFile.sha256,
canonical_sha256: canonicalFile.sha256,
});
}
const canonicalOnly = canonicalSql
.filter((name) => !legacyMap.has(name))
.map((name) => ({
filename: name,
status: "CANONICAL_ONLY",
canonical_sha256: canonicalMap.get(name).sha256,
}));
return {
generated_at: new Date().toISOString(),
canonical_dir: path.relative(repoRoot, canonicalDir),
legacy_dir: path.relative(repoRoot, legacyDir),
stale_sql_files: staleFiles,
canonical_only_sql_files: canonicalOnly,
legacy_non_sql_files: legacyNonSql,
summary: {
stale_total: staleFiles.length,
stale_only_in_backend_total: staleFiles.filter(
(f) => f.status === "STALE_ONLY_IN_BACKEND"
).length,
stale_duplicate_total: staleFiles.filter(
(f) => f.status === "STALE_DUPLICATE_OF_CANONICAL"
).length,
stale_diverged_total: staleFiles.filter(
(f) => f.status === "STALE_DIVERGED_FROM_CANONICAL"
).length,
canonical_only_total: canonicalOnly.length,
},
};
}
function printReport(report) {
console.log("Stale SQL Tracker");
console.log(`- Canonical: ${report.canonical_dir}`);
console.log(`- Legacy: ${report.legacy_dir}`);
console.log(`- Generated: ${report.generated_at}`);
console.log("");
console.log(`Stale SQL files in legacy dir: ${report.summary.stale_total}`);
for (const stale of report.stale_sql_files) {
console.log(` - ${stale.filename} :: ${stale.status}`);
}
console.log("");
console.log(`Canonical-only SQL files: ${report.summary.canonical_only_total}`);
for (const canonicalOnly of report.canonical_only_sql_files) {
console.log(` - ${canonicalOnly.filename}`);
}
console.log("");
console.log(`Legacy non-SQL files: ${report.legacy_non_sql_files.length}`);
for (const nonSql of report.legacy_non_sql_files) {
console.log(` - ${nonSql}`);
}
}
function writeReport(report) {
fs.writeFileSync(defaultReportPath, JSON.stringify(report, null, 2) + "\n", "utf8");
console.log("");
console.log(`Wrote stale SQL report: ${path.relative(repoRoot, defaultReportPath)}`);
}
function main() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
console.log("Usage: node scripts/db-stale-sql-tracker.js [--write] [--fail-on-stale]");
process.exit(0);
}
const report = buildReport();
printReport(report);
if (options.write) {
writeReport(report);
}
if (options.failOnStale && report.summary.stale_total > 0) {
process.exit(1);
}
}
try {
main();
} catch (error) {
console.error(error.message);
process.exit(1);
}