metis-desk/packages/relationship-contracts/src/relationship-registry.ts
2026-10-20 13:20:22 +02:00

126 lines
4.2 KiB
TypeScript

import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4";
import { RelationshipReadError } from "./relationship-read";
import type { RelationshipContext, RelationshipQuery } from "./relationship-schemas";
import {
NamespacedIdSchema,
ProviderIdSchema,
RelationshipDefinitionSchema,
RelationshipQuerySchema,
} from "./relationship-schemas";
const RegistrySchema = z
.strictObject({
contractVersion: z.literal(1),
providers: z.array(ProviderIdSchema).readonly(),
definitions: z.array(RelationshipDefinitionSchema).readonly(),
capabilities: z.array(NamespacedIdSchema).readonly(),
})
.readonly();
export type RelationshipRegistry = z.infer<typeof RegistrySchema>;
export type RelationshipRegistryErrorCode =
| "INVALID_REGISTRY"
| "DUPLICATE_PROVIDER"
| "DUPLICATE_RELATIONSHIP"
| "UNREGISTERED_PROVIDER";
export class RelationshipRegistryError extends Error {
constructor(readonly code: RelationshipRegistryErrorCode) {
super(code);
this.name = "RelationshipRegistryError";
}
}
/** Catálogo de metadatos por composición explícita; no almacena ni resuelve servicios. */
export function createRelationshipRegistry(
providers: readonly string[],
definitions: readonly unknown[],
capabilities: readonly string[] = []
): Result<RelationshipRegistry, RelationshipRegistryError> {
const parsed = RegistrySchema.safeParse({
contractVersion: 1,
providers,
definitions,
capabilities,
});
if (!parsed.success) {
return Result.fail(new RelationshipRegistryError("INVALID_REGISTRY"));
}
const registry = parsed.data;
if (new Set(registry.capabilities).size !== registry.capabilities.length) {
return Result.fail(new RelationshipRegistryError("INVALID_REGISTRY"));
}
const knownProviders = new Set(registry.providers);
if (knownProviders.size !== registry.providers.length) {
return Result.fail(new RelationshipRegistryError("DUPLICATE_PROVIDER"));
}
const knownTypes = new Set<string>();
for (const definition of registry.definitions) {
if (knownTypes.has(definition.typeId)) {
return Result.fail(new RelationshipRegistryError("DUPLICATE_RELATIONSHIP"));
}
knownTypes.add(definition.typeId);
for (const endpoint of [definition.source, definition.target]) {
if (
endpoint.requiredCapability &&
!registry.capabilities.includes(endpoint.requiredCapability) &&
!knownProviders.has(endpoint.requiredCapability.split(":")[0] ?? "")
) {
return Result.fail(new RelationshipRegistryError("UNREGISTERED_PROVIDER"));
}
}
const namespaces = [
definition.ownerProviderId,
...[definition.source, definition.target].flatMap((endpoint) => [
endpoint.entityType.split(":")[0],
]),
];
if (namespaces.some((namespace) => namespace !== undefined && !knownProviders.has(namespace))) {
return Result.fail(new RelationshipRegistryError("UNREGISTERED_PROVIDER"));
}
}
return Result.ok(registry);
}
/** Validación estructural y de ámbito; no sustituye la autorización del propietario. */
export function validateRelationshipQuery(
input: unknown,
context: RelationshipContext,
registry: RelationshipRegistry
): Result<RelationshipQuery, RelationshipReadError> {
const parsed = RelationshipQuerySchema.safeParse(input);
if (!parsed.success) {
return Result.fail(new RelationshipReadError("INVALID_QUERY"));
}
const query = parsed.data;
if (query.anchor.companyId !== context.companyId) {
return Result.fail(new RelationshipReadError("FORBIDDEN"));
}
const definition = registry.definitions.find((entry) => entry.typeId === query.typeId);
if (!definition) {
return Result.fail(new RelationshipReadError("INVALID_QUERY"));
}
const matchesSource = query.anchor.entityType === definition.source.entityType;
const matchesTarget = query.anchor.entityType === definition.target.entityType;
const compatible =
definition.direction === "symmetric" || query.direction === "either"
? matchesSource || matchesTarget
: query.direction === "outgoing"
? matchesSource
: matchesTarget;
return compatible ? Result.ok(query) : Result.fail(new RelationshipReadError("INVALID_QUERY"));
}