feat: v0.1.0 — sqlite-migrate package extraction from authd #1
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