feat: add runMigrateCli wrapper at sub-path './cli'
Extracts the union of authd's three CLI scripts (db-migrate.ts, db-status.ts, db-stamp.ts) into one wrapper. Consumers wire a ~5-line shim that injects their own openDb / migrationsDir / config-loading logic. The wrapper returns an exit code rather than calling process.exit, which keeps it testable via PassThrough stream capture.
This commit is contained in:
107
src/cli.ts
Normal file
107
src/cli.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import type Database from 'better-sqlite3';
|
||||||
|
import {
|
||||||
|
applyMigrations,
|
||||||
|
listMigrations,
|
||||||
|
readAppliedRows,
|
||||||
|
stampMigration,
|
||||||
|
} from './migrate.js';
|
||||||
|
|
||||||
|
export type DbCommand = 'migrate' | 'status' | 'stamp';
|
||||||
|
|
||||||
|
export interface RunCliOptions {
|
||||||
|
/** Caller-controlled DB open. Wrapper closes it before returning. */
|
||||||
|
openDb: () => Database.Database;
|
||||||
|
migrationsDir: string;
|
||||||
|
command: DbCommand;
|
||||||
|
/** Required for `stamp`. */
|
||||||
|
version?: string;
|
||||||
|
/** Default false. Forwarded to applyMigrations. */
|
||||||
|
stampGenesis?: boolean;
|
||||||
|
/** Default 'users'. Forwarded to applyMigrations. */
|
||||||
|
genesisProbeTable?: string;
|
||||||
|
/** Default process.stdout / process.stderr. Wired for tests. */
|
||||||
|
stdout?: NodeJS.WritableStream;
|
||||||
|
stderr?: NodeJS.WritableStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runMigrateCli(opts: RunCliOptions): number {
|
||||||
|
const stdout = opts.stdout ?? process.stdout;
|
||||||
|
const stderr = opts.stderr ?? process.stderr;
|
||||||
|
|
||||||
|
if (opts.command === 'stamp') {
|
||||||
|
if (!opts.version) {
|
||||||
|
stderr.write('error: version required\n');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const db = opts.openDb();
|
||||||
|
try {
|
||||||
|
const file = stampMigration(db, opts.migrationsDir, opts.version);
|
||||||
|
stdout.write(`stamped:${file.version}\n`);
|
||||||
|
db.close();
|
||||||
|
return 0;
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
stderr.write(`error: ${msg}\n`);
|
||||||
|
db.close();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.command === 'status') {
|
||||||
|
const db = opts.openDb();
|
||||||
|
const files = listMigrations(opts.migrationsDir);
|
||||||
|
const appliedByVersion = new Map(
|
||||||
|
readAppliedRows(db).map((r) => [r.version, r]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let exitCode = 0;
|
||||||
|
const pending: string[] = [];
|
||||||
|
const mismatched: string[] = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const applied = appliedByVersion.get(file.version);
|
||||||
|
if (!applied) {
|
||||||
|
pending.push(file.version);
|
||||||
|
stdout.write(`pending:${file.version}\n`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (applied.checksum !== file.checksum) {
|
||||||
|
mismatched.push(file.version);
|
||||||
|
stdout.write(`checksum-mismatch:${file.version}\n`);
|
||||||
|
exitCode = 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
stdout.write(`applied:${file.version}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.length === 0 && mismatched.length === 0) {
|
||||||
|
stdout.write(`pending:none\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrate
|
||||||
|
const db = opts.openDb();
|
||||||
|
const summary = applyMigrations(db, opts.migrationsDir, {
|
||||||
|
stampGenesis: opts.stampGenesis,
|
||||||
|
genesisProbeTable: opts.genesisProbeTable,
|
||||||
|
logger: (msg) => stdout.write(`${msg}\n`),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (summary.stamped.length > 0) {
|
||||||
|
stdout.write(`stamped:${summary.stamped.join(',')}\n`);
|
||||||
|
}
|
||||||
|
if (summary.applied === 0 && summary.stamped.length === 0) {
|
||||||
|
stdout.write(`pending:none\n`);
|
||||||
|
} else if (summary.applied > 0) {
|
||||||
|
stdout.write(`applied:${summary.applied}\n`);
|
||||||
|
}
|
||||||
|
stdout.write(
|
||||||
|
`migrations: ${summary.applied + summary.alreadyApplied} applied, ${summary.pending} pending\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user