Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/platform-leads/source-bootstrap-cli.ts
T

152 lines
6.4 KiB
TypeScript

#!/usr/bin/env tsx
import "dotenv/config";
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { createPrismaPlatformLeadService } from "@hivegrid/db/platform-leads-admin.server";
import { disconnectDbClients, getDbClient } 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 {
isSuccessfulPlatformLeadSourceBootstrapManifest,
PlatformLeadSourceBootstrapError,
runPlatformLeadSourceBootstrap,
type PlatformLeadSourceBootstrapMode,
} from "./source-bootstrap.js";
export type PlatformLeadSourceBootstrapCliOptions = {
mode: PlatformLeadSourceBootstrapMode;
actorAdminUserId: string;
routingAdminUserId: string;
manifestPath: string;
runId: string;
expectedPlanSha256?: string;
};
export function parsePlatformLeadSourceBootstrapCliArgs(argv: readonly string[]): PlatformLeadSourceBootstrapCliOptions {
const values = parseNamedArguments(argv);
const allowed = new Set(["mode", "actor-admin-user-id", "routing-admin-user-id", "manifest", "run-id", "expected-plan-sha256"]);
if ([...values.keys()].some((key) => !allowed.has(key))) throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
const modeValue = required(values.get("mode"));
if (modeValue !== "dry-run" && modeValue !== "apply") throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
const options: PlatformLeadSourceBootstrapCliOptions = {
mode: modeValue,
actorAdminUserId: identifier(values.get("actor-admin-user-id")),
routingAdminUserId: identifier(values.get("routing-admin-user-id")),
manifestPath: required(values.get("manifest")),
runId: values.get("run-id")?.trim() || randomUUID(),
};
if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,190}$/u.test(options.runId)) {
throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
}
if (modeValue === "apply") {
const expected = required(values.get("expected-plan-sha256")).toLowerCase();
if (!/^[a-f0-9]{64}$/u.test(expected)) throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
options.expectedPlanSha256 = expected;
} else if (values.has("expected-plan-sha256")) throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
return options;
}
export async function runPlatformLeadSourceBootstrapCli(argv: readonly string[] = process.argv.slice(2)): Promise<number> {
let options: PlatformLeadSourceBootstrapCliOptions;
try {
options = parsePlatformLeadSourceBootstrapCliArgs(argv);
} catch (error) {
writeSafeError(error);
return 1;
}
try {
const manifest = await runWithRlsContext({ userId: options.actorAdminUserId }, async () => {
if (!(await userHasPermission(options.actorAdminUserId, null, "admin.leads.admin"))) {
throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
}
const [canReadOwn, canWrite] = await Promise.all([
userHasPermission(options.routingAdminUserId, null, "admin.leads.read_own"),
userHasPermission(options.routingAdminUserId, null, "admin.leads.write"),
]);
if (!canReadOwn || !canWrite) {
throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
}
const db = getDbClient();
const routingOperator = await db.adminUser.findUnique({
where: { userId: options.routingAdminUserId },
select: { user: { select: { active: true, pendingDeletion: true, pseudonymized: true } } },
});
if (
!routingOperator?.user.active ||
routingOperator.user.pendingDeletion ||
routingOperator.user.pseudonymized
) {
throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
}
const service = createPrismaPlatformLeadService();
const existingSources = await service.listSources();
return runPlatformLeadSourceBootstrap({
mode: options.mode,
runId: options.runId,
actorAdminUserId: options.actorAdminUserId,
routingAdminUserId: options.routingAdminUserId,
existingSources,
expectedPlanSha256: options.expectedPlanSha256,
service,
});
});
const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
await writeFile(options.manifestPath, serialized, { encoding: "utf8", flag: "wx", mode: 0o600 });
process.stdout.write(serialized);
return isSuccessfulPlatformLeadSourceBootstrapManifest(manifest) ? 0 : 2;
} catch (error) {
writeSafeError(error);
return 1;
} finally {
await Promise.allSettled([closeSharedStores(), disconnectDbClients()]);
}
}
function parseNamedArguments(argv: readonly string[]): Map<string, string> {
const values = new Map<string, string>();
for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index]!;
if (argument === "--" && index === 0) continue;
if (!argument.startsWith("--")) throw new PlatformLeadSourceBootstrapError("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 PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
values.set(key, value);
}
return values;
}
function required(value: string | undefined): string {
const normalized = value?.trim();
if (!normalized || /[\u0000-\u001f\u007f]/u.test(normalized)) throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
return normalized;
}
function identifier(value: string | undefined): string {
const normalized = required(value);
if (normalized.length > 191 || !/^[A-Za-z0-9_-]+$/u.test(normalized)) throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
return normalized;
}
function writeSafeError(error: unknown): void {
const code = error instanceof PlatformLeadSourceBootstrapError ? error.code : "OPERATION_FAILED";
process.stderr.write(`${JSON.stringify({ ok: false, code })}\n`);
}
if (import.meta.url === `file://${process.argv[1]}`) {
runPlatformLeadSourceBootstrapCli()
.then((exitCode) => {
process.exitCode = exitCode;
})
.catch(() => {
process.exitCode = 1;
});
}