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; 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 { 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(); 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 { 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")); }