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

237 lines
9.0 KiB
TypeScript

import { createHash } from "node:crypto";
import type { CreatePlatformLeadSourceInput, PlatformLeadSourceAdminDto, UpdatePlatformLeadSourceInput } from "@hivegrid/db/platform-leads-admin.server";
export type PlatformLeadSourceBootstrapMode = "dry-run" | "apply";
export const PLATFORM_LEAD_INITIAL_SOURCE_CONFIGURATION = Object.freeze([
{ key: "hivegrid-website", displayName: "HiveGrid Website", currentWebsiteSource: true },
{ key: "hivegrid-legacy-cloud", displayName: "HiveGrid Cloud Legacy", currentWebsiteSource: false },
{ key: "lead-inbox-legacy", displayName: "Lead Inbox Legacy", currentWebsiteSource: false },
] as const);
type CreateAction = {
kind: "create";
key: string;
input: Omit<CreatePlatformLeadSourceInput, "actorAdminUserId" | "reason">;
};
type UpdateAction = {
kind: "update";
key: string;
input: Omit<UpdatePlatformLeadSourceInput, "actorAdminUserId" | "reason">;
};
type UnchangedAction = { kind: "unchanged"; key: string };
export type PlatformLeadSourceBootstrapAction = CreateAction | UpdateAction | UnchangedAction;
export type PlatformLeadSourceBootstrapPlan = {
hash: string;
actions: PlatformLeadSourceBootstrapAction[];
};
export type PlatformLeadSourceBootstrapService = {
createSource(input: CreatePlatformLeadSourceInput): Promise<PlatformLeadSourceAdminDto>;
updateSource(input: UpdatePlatformLeadSourceInput): Promise<PlatformLeadSourceAdminDto>;
};
export type PlatformLeadSourceBootstrapManifest = {
schemaVersion: 1;
runId: string;
mode: PlatformLeadSourceBootstrapMode;
startedAt: string;
finishedAt: string;
operatorBindingSha256: string;
planSha256: string;
expectedPlanSha256: string | null;
planVerified: boolean | null;
actions: Array<{ key: string; action: PlatformLeadSourceBootstrapAction["kind"] }>;
counts: { createPlanned: number; updatePlanned: number; unchanged: number; created: number; updated: number; failed: number };
outcome: "validated" | "applied" | "blocked" | "failed";
credentialsIssued: 0;
notificationsTriggered: 0;
};
export class PlatformLeadSourceBootstrapError extends Error {
public override readonly name = "PlatformLeadSourceBootstrapError";
public constructor(public readonly code: "INVALID_CONFIGURATION") {
super(code);
}
}
export function planPlatformLeadSourceBootstrap(
existingSources: readonly PlatformLeadSourceAdminDto[],
routingAdminUserId: string
): PlatformLeadSourceBootstrapPlan {
const routingUserId = requireIdentifier(routingAdminUserId);
const byKey = new Map<string, PlatformLeadSourceAdminDto>();
for (const source of existingSources) {
if (byKey.has(source.key)) throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
byKey.set(source.key, source);
}
const actions = PLATFORM_LEAD_INITIAL_SOURCE_CONFIGURATION.map<PlatformLeadSourceBootstrapAction>((desired) => {
const existing = byKey.get(desired.key);
if (!existing) {
return {
kind: "create",
key: desired.key,
input: {
key: desired.key,
displayName: desired.displayName,
enabled: desired.currentWebsiteSource,
defaultAssigneeAdminUserId: desired.currentWebsiteSource ? routingUserId : null,
notifyAdminUserIds: desired.currentWebsiteSource ? [routingUserId] : [],
rateLimitPerMinute: 60,
metadata: { managedBy: "R-LEADS" },
},
};
}
if (!desired.currentWebsiteSource) {
return existing.enabled
? { kind: "update", key: desired.key, input: { sourceId: existing.id, enabled: false } }
: { kind: "unchanged", key: desired.key };
}
const notifyAdminUserIds = [...new Set([...existing.notifyAdminUserIds, routingUserId])].sort();
const input: UpdateAction["input"] = { sourceId: existing.id };
if (!existing.enabled) input.enabled = true;
if (existing.defaultAssigneeAdminUserId !== routingUserId) input.defaultAssigneeAdminUserId = routingUserId;
if (!sameStringList(existing.notifyAdminUserIds, notifyAdminUserIds)) input.notifyAdminUserIds = notifyAdminUserIds;
if (Object.keys(input).length === 1) return { kind: "unchanged", key: desired.key };
return { kind: "update", key: desired.key, input };
});
return { hash: hashPlan(actions), actions };
}
export async function runPlatformLeadSourceBootstrap(input: {
mode: PlatformLeadSourceBootstrapMode;
runId: string;
actorAdminUserId: string;
routingAdminUserId: string;
existingSources: readonly PlatformLeadSourceAdminDto[];
expectedPlanSha256?: string;
service?: PlatformLeadSourceBootstrapService;
clock?: () => Date;
}): Promise<PlatformLeadSourceBootstrapManifest> {
const clock = input.clock ?? (() => new Date());
const startedAt = validDate(clock());
const actorAdminUserId = requireIdentifier(input.actorAdminUserId);
const routingAdminUserId = requireIdentifier(input.routingAdminUserId);
const runId = requireRunId(input.runId);
const plan = planPlatformLeadSourceBootstrap(input.existingSources, routingAdminUserId);
const expectedPlanSha256 = input.expectedPlanSha256?.trim().toLowerCase() ?? null;
const planVerified = expectedPlanSha256 === null ? null : plan.hash === expectedPlanSha256;
if (input.mode === "apply" && (!expectedPlanSha256 || !/^[a-f0-9]{64}$/u.test(expectedPlanSha256) || !input.service)) {
throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
}
let created = 0;
let updated = 0;
let failed = 0;
if (input.mode === "apply" && planVerified) {
for (const action of plan.actions) {
try {
if (action.kind === "create") {
await input.service!.createSource({
...action.input,
actorAdminUserId,
reason: "R-LEADS initial platform lead source bootstrap",
requestId: runId,
});
created += 1;
} else if (action.kind === "update") {
await input.service!.updateSource({
...action.input,
actorAdminUserId,
reason: "R-LEADS platform lead source routing bootstrap",
requestId: runId,
});
updated += 1;
}
} catch {
failed += 1;
}
}
}
const createPlanned = plan.actions.filter(({ kind }) => kind === "create").length;
const updatePlanned = plan.actions.filter(({ kind }) => kind === "update").length;
const unchanged = plan.actions.filter(({ kind }) => kind === "unchanged").length;
const outcome = input.mode === "dry-run" ? "validated" : planVerified === false ? "blocked" : failed > 0 ? "failed" : "applied";
return {
schemaVersion: 1,
runId,
mode: input.mode,
startedAt: startedAt.toISOString(),
finishedAt: validDate(clock()).toISOString(),
operatorBindingSha256: sha256(routingAdminUserId),
planSha256: plan.hash,
expectedPlanSha256,
planVerified,
actions: plan.actions.map(({ key, kind }) => ({ key, action: kind })),
counts: { createPlanned, updatePlanned, unchanged, created, updated, failed },
outcome,
credentialsIssued: 0,
notificationsTriggered: 0,
};
}
export function isSuccessfulPlatformLeadSourceBootstrapManifest(manifest: PlatformLeadSourceBootstrapManifest): boolean {
return manifest.outcome === "validated" || manifest.outcome === "applied";
}
function hashPlan(actions: readonly PlatformLeadSourceBootstrapAction[]): string {
return sha256(
JSON.stringify(
actions.map((action) =>
action.kind === "unchanged" ? action : { kind: action.kind, key: action.key, input: sortRecord(action.input as Record<string, unknown>) }
)
)
);
}
function sortRecord(record: Record<string, unknown>): Record<string, unknown> {
return Object.fromEntries(
Object.entries(record)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, value]) => [key, Array.isArray(value) ? [...value].sort() : value])
);
}
function sameStringList(left: readonly string[], right: readonly string[]): boolean {
if (left.length !== right.length) return false;
const sortedLeft = [...left].sort();
return sortedLeft.every((value, index) => value === right[index]);
}
function requireIdentifier(value: string): string {
if (typeof value !== "string") throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
const normalized = value.trim();
if (!normalized || normalized.length > 191 || !/^[A-Za-z0-9_-]+$/u.test(normalized)) {
throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
}
return normalized;
}
function requireRunId(value: string): string {
if (typeof value !== "string") throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
const normalized = value.trim();
if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,190}$/u.test(normalized)) {
throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
}
return normalized;
}
function validDate(value: Date): Date {
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
return value;
}
function sha256(value: string): string {
return createHash("sha256").update(value, "utf8").digest("hex");
}