156 lines
5.9 KiB
TypeScript
156 lines
5.9 KiB
TypeScript
import { operationAcceptanceOverrides58e2204 } from './acceptance-58e2204.ts';
|
|
import {
|
|
operationMetadata58e2204,
|
|
type OperationCatalogMetadata,
|
|
} from './catalog-metadata-58e2204.ts';
|
|
import { upstream58e2204Operations } from './upstream-58e2204.ts';
|
|
|
|
export type RuntimeRegistryErrorCode = 'UNKNOWN_OPERATION' | 'DEDICATED_FLOW_REQUIRED';
|
|
|
|
export class RuntimeRegistryError extends Error {
|
|
readonly code: RuntimeRegistryErrorCode;
|
|
|
|
constructor(code: RuntimeRegistryErrorCode, message: string) {
|
|
super(message);
|
|
this.name = 'RuntimeRegistryError';
|
|
this.code = code;
|
|
}
|
|
}
|
|
|
|
export interface RuntimeOperationDescriptor {
|
|
readonly operationId: string;
|
|
readonly method: string;
|
|
readonly pathTemplate: string;
|
|
readonly handler: string;
|
|
readonly capability: string;
|
|
readonly riskLevel: 'R0' | 'R1' | 'R2' | 'R3';
|
|
readonly batchable: boolean;
|
|
readonly executionPolicy: string;
|
|
readonly upstreamDomain: string;
|
|
readonly module: string;
|
|
readonly uiStrategy: string;
|
|
readonly title: string;
|
|
readonly parameterSchemaId: string;
|
|
readonly [field: string]: unknown;
|
|
}
|
|
|
|
type AcceptanceOverride = Readonly<Record<string, unknown>> & { readonly uiStrategy?: unknown };
|
|
|
|
const OPERATION_ID_PATTERN = /^[A-Za-z][A-Za-z0-9]*$/;
|
|
const EXPECTED_OPERATION_COUNT = 117;
|
|
const PARAMETER_SCHEMA_ID_PATTERN = /^[A-Za-z0-9]+(?:[.-][A-Za-z0-9]+)+$/;
|
|
|
|
function deepFreeze<T>(value: T): T {
|
|
if (value === null || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
for (const child of Object.values(value)) deepFreeze(child);
|
|
return Object.freeze(value);
|
|
}
|
|
|
|
function unknownOperation(operationId: unknown): RuntimeRegistryError {
|
|
const rendered = typeof operationId === 'string' && operationId ? operationId : '<invalid>';
|
|
return new RuntimeRegistryError('UNKNOWN_OPERATION', `Unknown operation: ${rendered}`);
|
|
}
|
|
|
|
/** Runtime-only, operationId-keyed projection of the frozen audited registry. */
|
|
export class OperationRegistry {
|
|
readonly #byOperationId: ReadonlyMap<string, RuntimeOperationDescriptor>;
|
|
readonly #operations: readonly RuntimeOperationDescriptor[];
|
|
|
|
constructor(
|
|
operations: readonly Readonly<Record<string, unknown>>[],
|
|
acceptanceOverrides: Readonly<Record<string, AcceptanceOverride>>,
|
|
metadata: Readonly<Record<string, OperationCatalogMetadata>>,
|
|
) {
|
|
const sourceById = new Map<string, Readonly<Record<string, unknown>>>();
|
|
for (const operation of operations) {
|
|
const id = operation.operationId;
|
|
if (typeof id !== 'string' || !OPERATION_ID_PATTERN.test(id)) {
|
|
throw new Error(`invalid operationId in registry: ${String(id)}`);
|
|
}
|
|
if (sourceById.has(id)) throw new Error(`duplicate operationId: ${id}`);
|
|
sourceById.set(id, operation);
|
|
}
|
|
if (sourceById.size !== EXPECTED_OPERATION_COUNT) {
|
|
throw new Error(
|
|
`runtime registry must contain exactly ${EXPECTED_OPERATION_COUNT} operations`,
|
|
);
|
|
}
|
|
for (const id of Object.keys(metadata)) {
|
|
if (!sourceById.has(id)) throw new Error(`extra operation metadata: ${id}`);
|
|
}
|
|
|
|
const descriptors: RuntimeOperationDescriptor[] = [];
|
|
for (const id of [...sourceById.keys()].sort()) {
|
|
if (!Object.hasOwn(acceptanceOverrides, id)) {
|
|
throw new Error(`missing acceptance override: ${id}`);
|
|
}
|
|
const acceptance = acceptanceOverrides[id];
|
|
if (!acceptance || typeof acceptance.uiStrategy !== 'string' || !acceptance.uiStrategy) {
|
|
throw new Error(`missing acceptance override: ${id}`);
|
|
}
|
|
const operation = sourceById.get(id)!;
|
|
if (!Object.hasOwn(metadata, id)) throw new Error(`missing operation metadata: ${id}`);
|
|
const catalog = metadata[id]!;
|
|
if (typeof catalog.title !== 'string' || catalog.title.trim().length === 0) {
|
|
throw new Error(`blank operation title: ${id}`);
|
|
}
|
|
if (!PARAMETER_SCHEMA_ID_PATTERN.test(catalog.parameterSchemaId)) {
|
|
throw new Error(`invalid parameter schema id: ${id}`);
|
|
}
|
|
if (typeof operation.upstreamDomain !== 'string') {
|
|
throw new Error(`invalid upstream domain: ${id}`);
|
|
}
|
|
const descriptor = structuredClone({
|
|
...operation,
|
|
module: operation.upstreamDomain,
|
|
uiStrategy: acceptance.uiStrategy,
|
|
title: catalog.title,
|
|
parameterSchemaId: catalog.parameterSchemaId,
|
|
}) as RuntimeOperationDescriptor;
|
|
descriptors.push(deepFreeze(descriptor));
|
|
}
|
|
|
|
this.#operations = Object.freeze(descriptors);
|
|
this.#byOperationId = new Map(
|
|
descriptors.map((descriptor) => [descriptor.operationId, descriptor]),
|
|
);
|
|
Object.freeze(this);
|
|
}
|
|
|
|
listOperations(): readonly RuntimeOperationDescriptor[] {
|
|
return this.#operations;
|
|
}
|
|
|
|
requireOperation(operationId: string): RuntimeOperationDescriptor {
|
|
if (typeof operationId !== 'string' || !OPERATION_ID_PATTERN.test(operationId)) {
|
|
throw unknownOperation(operationId);
|
|
}
|
|
const operation = this.#byOperationId.get(operationId);
|
|
if (!operation) throw unknownOperation(operationId);
|
|
return operation;
|
|
}
|
|
|
|
requireExecutableOperation(operationId: string): RuntimeOperationDescriptor {
|
|
const descriptor = this.requireOperation(operationId);
|
|
if (descriptor.executionPolicy !== 'registeredOperation') {
|
|
throw new RuntimeRegistryError(
|
|
'DEDICATED_FLOW_REQUIRED',
|
|
`Operation is not executable through the registered-operation boundary: ${operationId}`,
|
|
);
|
|
}
|
|
return descriptor;
|
|
}
|
|
}
|
|
export const operationRegistry = new OperationRegistry(
|
|
upstream58e2204Operations,
|
|
operationAcceptanceOverrides58e2204,
|
|
operationMetadata58e2204,
|
|
);
|
|
|
|
export const listOperations = (): readonly RuntimeOperationDescriptor[] =>
|
|
operationRegistry.listOperations();
|
|
export const requireOperation = (operationId: string): RuntimeOperationDescriptor =>
|
|
operationRegistry.requireOperation(operationId);
|
|
export const requireExecutableOperation = (operationId: string): RuntimeOperationDescriptor =>
|
|
operationRegistry.requireExecutableOperation(operationId);
|