scripts: add backfill CLI with dry-run default

Wires backfill-canonical-layer.ts to a CLI entry point exposed as
`npm run backfill:canonical`. Defaults to --dry-run; requires --apply
to actually write. Supports --sport=<uuid> to limit to a single sport.
Exits 2 if the backfill reports errors (e.g., surface-Elo conflicts).
This commit is contained in:
Chris Parsons 2026-05-01 20:59:13 +00:00
parent 8186dbb525
commit 6f3438b5d2
No known key found for this signature in database
2 changed files with 79 additions and 0 deletions

View file

@ -9,6 +9,7 @@
"db:generate": "dotenv -- drizzle-kit generate",
"db:migrate": "dotenv -- drizzle-kit migrate",
"db:sync-prod": "bash scripts/sync-prod-db.sh",
"backfill:canonical": "dotenv -- tsx scripts/backfill-cli.ts",
"dev": "NODE_OPTIONS='--import ./instrument.server.mjs' dotenv -- tsx watch server.ts",
"start": "NODE_ENV=production NODE_OPTIONS='--import ./instrument.server.mjs' node dist/server.js",
"start:production": "NODE_ENV=production NODE_OPTIONS='--import ./instrument.server.mjs' node dist/server.js",

78
scripts/backfill-cli.ts Normal file
View file

@ -0,0 +1,78 @@
/**
* CLI entry point for the Phase 2 canonical backfill.
*
* Usage:
* npm run backfill:canonical -- [--dry-run | --apply] [--sport=<uuid>]
*
* Defaults to --dry-run for safety. No writes will be issued unless
* --apply is passed explicitly.
*/
import { runBackfill } from "./backfill-canonical-layer";
import type { BackfillOptions } from "./backfill-canonical-layer";
function printHelp(): void {
console.log(
[
"Usage: backfill-cli [options]",
"",
"Options:",
" --dry-run Run without writing (default).",
" --apply Actually write to the database.",
" --sport=<uuid> Only backfill for the given sport id.",
" --help Show this message.",
].join("\n"),
);
}
function parseArgs(): BackfillOptions {
const args = process.argv.slice(2);
const opts: BackfillOptions = { dryRun: true }; // dry-run by default for safety
for (const a of args) {
if (a === "--apply") {
opts.dryRun = false;
} else if (a === "--dry-run") {
opts.dryRun = true;
} else if (a.startsWith("--sport=")) {
opts.sportId = a.slice("--sport=".length);
} else if (a === "--help" || a === "-h") {
printHelp();
process.exit(0);
} else {
console.error(`unknown arg: ${a}`);
process.exit(1);
}
}
return opts;
}
async function main() {
const opts = parseArgs();
console.log(
`Running backfill (dryRun=${opts.dryRun}, sportId=${opts.sportId ?? "all"})`,
);
const report = await runBackfill(opts);
console.log("---");
console.log(`tournamentsCreated: ${report.tournamentsCreated}`);
console.log(`tournamentsLinked: ${report.tournamentsLinked}`);
console.log(`participantsCreated: ${report.participantsCreated}`);
console.log(`participantsLinked: ${report.participantsLinked}`);
console.log(`tournamentResultsCreated: ${report.tournamentResultsCreated}`);
console.log(`surfaceElosCreated: ${report.surfaceElosCreated}`);
if (report.warnings.length) {
console.log("\nWARNINGS:");
for (const w of report.warnings) console.log(` ${w}`);
}
if (report.errors.length) {
console.log("\nERRORS:");
for (const e of report.errors) console.log(` ${e}`);
process.exit(2);
}
}
main().catch((e) => {
console.error(e);
process.exit(1);
});