Files
Epicnabbo-Catalogus-Updated…/src/features/housekeeping/foundation/commands/registry.ts
T

79 lines
2.1 KiB
TypeScript

import type { z } from "zod";
import {
HOUSEKEEPING_DOMAIN_IDS,
type HousekeepingDomainId,
} from "../../migration/types";
import type {
CapabilityRequirement,
HousekeepingCapabilityContext,
HousekeepingResult,
} from "../contracts";
export interface HousekeepingCommandContext {
capability: HousekeepingCapabilityContext;
correlationId: string;
ipAddress: string;
}
export interface HousekeepingCommand<I, O> {
id: string;
owner: HousekeepingDomainId;
risk: "safe" | "sensitive";
capability: CapabilityRequirement;
input: z.ZodType<I>;
requiresReason: boolean;
rateLimit: { attempts: number; windowMs: number };
execute(
context: HousekeepingCommandContext,
input: I,
): Promise<HousekeepingResult<O>>;
}
type RegisteredHousekeepingCommand = HousekeepingCommand<unknown, unknown>;
const approvedOwners = new Set<string>(HOUSEKEEPING_DOMAIN_IDS);
const commands = new Map<string, RegisteredHousekeepingCommand>();
export function registerHousekeepingCommand<I, O>(
command: HousekeepingCommand<I, O>,
): void {
validateCommand(command);
if (commands.has(command.id)) {
throw new Error(`duplicate command id: ${command.id}`);
}
commands.set(command.id, command as RegisteredHousekeepingCommand);
}
export function getHousekeepingCommand(
id: string,
): RegisteredHousekeepingCommand | undefined {
return commands.get(id);
}
function validateCommand<I, O>(command: HousekeepingCommand<I, O>): void {
if (typeof command.id !== "string" || !command.id.trim()) {
throw new Error("empty command id");
}
if (!approvedOwners.has(command.owner)) {
throw new Error(`unknown command owner: ${command.owner}`);
}
if (!command.id.startsWith(`${command.owner}.`)) {
throw new Error(`command owner mismatch: ${command.id}`);
}
if (
!command.input ||
typeof (command.input as { safeParse?: unknown }).safeParse !== "function"
) {
throw new Error(`command input schema missing: ${command.id}`);
}
if (
!Number.isInteger(command.rateLimit.attempts) ||
command.rateLimit.attempts <= 0 ||
!Number.isInteger(command.rateLimit.windowMs) ||
command.rateLimit.windowMs <= 0
) {
throw new Error(`invalid command rate limit: ${command.id}`);
}
}