#!/usr/bin/env tsx import "dotenv/config"; import { randomUUID } from "node:crypto"; import { readFile, stat, writeFile } from "node:fs/promises"; import { createPrismaPlatformLeadService } from "@hivegrid/db/platform-leads-admin.server"; import { disconnectDbClients } from "@hivegrid/db/client.server"; import { runWithRlsContext } from "@hivegrid/db/rls-context.server"; import { closeSharedStores } from "../../apps/web/app/shared-store.server.js"; import { userHasPermission } from "../../apps/web/app/utils/db/permissions/userRoles.db.server.js"; import { isPlatformLeadLegacyStore, isSuccessfulPlatformLeadBackfillManifest, PLATFORM_LEAD_SOURCE_KEY_BY_LEGACY_STORE, PlatformLeadBackfillInputError, runPlatformLeadBackfill, type PlatformLeadBackfillInputFormat, type PlatformLeadBackfillMode, type PlatformLeadLegacyStore, } from "./backfill.js"; const MAX_INPUT_BYTES = 100 * 1024 * 1024; const SHA256_PATTERN = /^[a-f0-9]{64}$/u; const IDENTIFIER_PATTERN = /^[A-Za-z0-9_-]+$/u; export type PlatformLeadBackfillCliOptions = { mode: PlatformLeadBackfillMode; legacyStore: PlatformLeadLegacyStore; inputPath: string; manifestPath: string; format: PlatformLeadBackfillInputFormat; runId: string; actorAdminUserId?: string; expectedInputSha256?: string; expectedAccepted?: number; }; export function parsePlatformLeadBackfillCliArgs(argv: readonly string[]): PlatformLeadBackfillCliOptions { const values = parseNamedArguments(argv); rejectUnknownKeys(values, ["mode", "store", "input", "manifest", "format", "run-id", "actor-admin-user-id", "expected-input-sha256", "expected-accepted"]); const mode = requireOneOf(values.get("mode"), ["dry-run", "apply", "replay"] as const); const storeValue = requireArgument(values.get("store")); if (!isPlatformLeadLegacyStore(storeValue)) throw new PlatformLeadBackfillInputError("INVALID_CONFIGURATION"); const inputPath = requireArgument(values.get("input")); const manifestPath = requireArgument(values.get("manifest")); const format = requireOneOf(values.get("format") ?? "jsonl", ["jsonl"] as const); const runId = values.get("run-id")?.trim() || randomUUID(); if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,190}$/u.test(runId)) throw new PlatformLeadBackfillInputError("INVALID_CONFIGURATION"); const options: PlatformLeadBackfillCliOptions = { mode, legacyStore: storeValue, inputPath, manifestPath, format, runId, }; if (mode !== "dry-run") { options.actorAdminUserId = requireIdentifier(values.get("actor-admin-user-id")); options.expectedInputSha256 = requireSha256(values.get("expected-input-sha256")); options.expectedAccepted = requireNonNegativeInteger(values.get("expected-accepted")); } else if (values.has("actor-admin-user-id") || values.has("expected-input-sha256") || values.has("expected-accepted")) { throw new PlatformLeadBackfillInputError("INVALID_CONFIGURATION"); } return options; } export async function runPlatformLeadBackfillCli(argv: readonly string[] = process.argv.slice(2)): Promise { let options: PlatformLeadBackfillCliOptions; try { options = parsePlatformLeadBackfillCliArgs(argv); } catch (error) { writeSafeError(error); return 1; } try { const inputStat = await stat(options.inputPath); if (!inputStat.isFile() || inputStat.size > MAX_INPUT_BYTES) throw new PlatformLeadBackfillInputError("INVALID_INPUT"); const bytes = await readFile(options.inputPath); const common = { mode: options.mode, legacyStore: options.legacyStore, format: options.format, bytes, runId: options.runId, expectedInputSha256: options.expectedInputSha256, expectedAccepted: options.expectedAccepted, } as const; const manifest = options.mode === "dry-run" ? await runPlatformLeadBackfill(common) : await runWithRlsContext({ userId: options.actorAdminUserId! }, async () => { if (!(await userHasPermission(options.actorAdminUserId!, null, "admin.leads.admin"))) { throw new PlatformLeadBackfillInputError("INVALID_CONFIGURATION"); } const service = createPrismaPlatformLeadService(); const expectedSourceKey = PLATFORM_LEAD_SOURCE_KEY_BY_LEGACY_STORE[options.legacyStore]; const source = (await service.listSources()).find(({ key }) => key === expectedSourceKey); if (!source) throw new PlatformLeadBackfillInputError("INVALID_CONFIGURATION"); return runPlatformLeadBackfill({ ...common, sourceId: source.id, actorAdminUserId: options.actorAdminUserId, importer: (input) => service.importLead(input), }); }); const serialized = `${JSON.stringify(manifest, null, 2)}\n`; await writeFile(options.manifestPath, serialized, { encoding: "utf8", flag: "wx", mode: 0o600 }); process.stdout.write(serialized); return isSuccessfulPlatformLeadBackfillManifest(manifest) ? 0 : 2; } catch (error) { writeSafeError(error); return 1; } finally { await Promise.allSettled([closeSharedStores(), disconnectDbClients()]); } } function parseNamedArguments(argv: readonly string[]): Map { const values = new Map(); for (let index = 0; index < argv.length; index += 1) { const argument = argv[index]!; if (argument === "--" && index === 0) continue; if (!argument.startsWith("--")) throw new PlatformLeadBackfillInputError("INVALID_CONFIGURATION"); const equalsIndex = argument.indexOf("="); const key = argument.slice(2, equalsIndex === -1 ? undefined : equalsIndex); const value = equalsIndex === -1 ? argv[++index] : argument.slice(equalsIndex + 1); if (!key || !value || value.startsWith("--") || values.has(key)) { throw new PlatformLeadBackfillInputError("INVALID_CONFIGURATION"); } values.set(key, value); } return values; } function rejectUnknownKeys(values: ReadonlyMap, allowedKeys: readonly string[]): void { const allowed = new Set(allowedKeys); if ([...values.keys()].some((key) => !allowed.has(key))) throw new PlatformLeadBackfillInputError("INVALID_CONFIGURATION"); } function requireArgument(value: string | undefined): string { const normalized = value?.trim(); if (!normalized || /[\u0000-\u001f\u007f]/u.test(normalized)) throw new PlatformLeadBackfillInputError("INVALID_CONFIGURATION"); return normalized; } function requireIdentifier(value: string | undefined): string { const normalized = requireArgument(value); if (normalized.length > 191 || !IDENTIFIER_PATTERN.test(normalized)) throw new PlatformLeadBackfillInputError("INVALID_CONFIGURATION"); return normalized; } function requireSha256(value: string | undefined): string { const normalized = requireArgument(value).toLowerCase(); if (!SHA256_PATTERN.test(normalized)) throw new PlatformLeadBackfillInputError("INVALID_CONFIGURATION"); return normalized; } function requireNonNegativeInteger(value: string | undefined): number { const normalized = requireArgument(value); if (!/^\d+$/u.test(normalized)) throw new PlatformLeadBackfillInputError("INVALID_CONFIGURATION"); const parsed = Number(normalized); if (!Number.isSafeInteger(parsed) || parsed < 0) throw new PlatformLeadBackfillInputError("INVALID_CONFIGURATION"); return parsed; } function requireOneOf(value: string | undefined, allowed: T): T[number] { const normalized = requireArgument(value); if (!(allowed as readonly string[]).includes(normalized)) throw new PlatformLeadBackfillInputError("INVALID_CONFIGURATION"); return normalized as T[number]; } function writeSafeError(error: unknown): void { const code = error instanceof PlatformLeadBackfillInputError ? error.code : "OPERATION_FAILED"; process.stderr.write(`${JSON.stringify({ ok: false, code })}\n`); } if (import.meta.url === `file://${process.argv[1]}`) { runPlatformLeadBackfillCli() .then((exitCode) => { process.exitCode = exitCode; }) .catch(() => { process.exitCode = 1; }); }