126 lines
4.2 KiB
TypeScript
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"));
|
|
}
|