153 lines
4.3 KiB
TypeScript
153 lines
4.3 KiB
TypeScript
#!/usr/bin/env tsx
|
|
import { backfillLegacyApiKeyHashes, DEFAULT_LEGACY_API_KEY_HASH_BACKFILL_BATCH_SIZE } from "@hivegrid/db/api-keys.server";
|
|
import type { LegacyApiKeyHashBackfillResult } from "@hivegrid/db/api-keys.server";
|
|
|
|
export type ApiKeyMigrationCliEnv = NodeJS.ProcessEnv & {
|
|
API_KEY_HASH_SECRET?: string;
|
|
JWT_SECRET?: string;
|
|
};
|
|
|
|
export type ApiKeyMigrationCliDependencies = {
|
|
backfillLegacyApiKeyHashes?: typeof backfillLegacyApiKeyHashes;
|
|
stdout?: Pick<typeof process.stdout, "write">;
|
|
stderr?: Pick<typeof process.stderr, "write">;
|
|
};
|
|
|
|
type ApiKeyMigrationCliOptions = {
|
|
dryRun: boolean;
|
|
batchSize: number;
|
|
};
|
|
|
|
function parseApiKeyMigrationArgs(argv: string[]): ApiKeyMigrationCliOptions {
|
|
let dryRun = true;
|
|
let batchSize = DEFAULT_LEGACY_API_KEY_HASH_BACKFILL_BATCH_SIZE;
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
|
|
if (arg === "--dry-run") {
|
|
dryRun = true;
|
|
continue;
|
|
}
|
|
|
|
if (arg === "--execute") {
|
|
dryRun = false;
|
|
continue;
|
|
}
|
|
|
|
if (arg === "--batch-size") {
|
|
const value = argv[index + 1];
|
|
if (!value || value.startsWith("--")) {
|
|
throw new Error("Invalid --batch-size: expected a positive integer.");
|
|
}
|
|
batchSize = parseBatchSize(value);
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (arg.startsWith("--batch-size=")) {
|
|
batchSize = parseBatchSize(arg.slice("--batch-size=".length));
|
|
continue;
|
|
}
|
|
|
|
throw new Error(`Unknown argument: ${arg}`);
|
|
}
|
|
|
|
return { dryRun, batchSize };
|
|
}
|
|
|
|
function parseBatchSize(value: string): number {
|
|
if (!/^\d+$/.test(value)) {
|
|
throw new Error("Invalid --batch-size: expected a positive integer.");
|
|
}
|
|
|
|
const batchSize = Number(value);
|
|
if (!Number.isSafeInteger(batchSize) || batchSize < 1) {
|
|
throw new Error("Invalid --batch-size: expected a positive integer.");
|
|
}
|
|
|
|
return batchSize;
|
|
}
|
|
|
|
function resolveHashSecret(env: ApiKeyMigrationCliEnv): string | undefined {
|
|
return env.API_KEY_HASH_SECRET?.trim() || env.JWT_SECRET?.trim() || undefined;
|
|
}
|
|
|
|
function writeLine(stream: Pick<typeof process.stdout, "write">, value: string): void {
|
|
stream.write(`${value}\n`);
|
|
}
|
|
|
|
function summarizeResult(result: LegacyApiKeyHashBackfillResult): Omit<LegacyApiKeyHashBackfillResult, "failures"> & { failureIds: string[] } {
|
|
return {
|
|
scanned: result.scanned,
|
|
updated: result.updated,
|
|
skipped: result.skipped,
|
|
failed: result.failed,
|
|
failureIds: result.failures.map((failure) => failure.id),
|
|
};
|
|
}
|
|
|
|
function redactCliError(error: unknown): string {
|
|
if (error instanceof Error) {
|
|
return error.name || "Error";
|
|
}
|
|
|
|
return "UnknownError";
|
|
}
|
|
|
|
export async function runApiKeyMigrationCli(
|
|
argv: string[] = process.argv.slice(2),
|
|
env: ApiKeyMigrationCliEnv = process.env,
|
|
dependencies: ApiKeyMigrationCliDependencies = {},
|
|
): Promise<number> {
|
|
const stdout = dependencies.stdout ?? process.stdout;
|
|
const stderr = dependencies.stderr ?? process.stderr;
|
|
const migrate = dependencies.backfillLegacyApiKeyHashes ?? backfillLegacyApiKeyHashes;
|
|
|
|
let options: ApiKeyMigrationCliOptions;
|
|
try {
|
|
options = parseApiKeyMigrationArgs(argv);
|
|
} catch (error) {
|
|
writeLine(stderr, error instanceof Error ? error.message : "Invalid arguments.");
|
|
return 1;
|
|
}
|
|
|
|
if (options.dryRun) {
|
|
writeLine(
|
|
stdout,
|
|
JSON.stringify({
|
|
mode: "dry-run",
|
|
batchSize: options.batchSize,
|
|
scanned: 0,
|
|
updated: 0,
|
|
skipped: 0,
|
|
failed: 0,
|
|
message: "Dry run only; no database writes performed. Use --execute to apply the backfill.",
|
|
}),
|
|
);
|
|
return 0;
|
|
}
|
|
|
|
try {
|
|
const result = await migrate({ batchSize: options.batchSize, hashSecret: resolveHashSecret(env) });
|
|
writeLine(stdout, JSON.stringify({ mode: "execute", batchSize: options.batchSize, ...summarizeResult(result) }));
|
|
return result.failed > 0 ? 1 : 0;
|
|
} catch (error) {
|
|
writeLine(
|
|
stderr,
|
|
JSON.stringify({ mode: "execute", batchSize: options.batchSize, scanned: 0, updated: 0, skipped: 0, failed: 1, error: redactCliError(error) }),
|
|
);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
runApiKeyMigrationCli()
|
|
.then((exitCode) => {
|
|
process.exitCode = exitCode;
|
|
})
|
|
.catch(() => {
|
|
process.exitCode = 1;
|
|
});
|
|
}
|