10 KiB
Contratos de relaciones tipadas v1
@metis/relationship-contracts, en packages/relationship-contracts, define una
superficie común de lectura. No sustituye los agregados, relaciones, servicios,
comandos ni tablas actuales. El paso 2.3 registra el primer proveedor real: assets:directory-membership,
compuesto en el servidor con resolutores de Assets y Resources. Véase
proveedor de relaciones tipadas para su uso,
autorización, SQL, pruebas y límites. No se añaden endpoints nuevos.
Dependencias y entrada pública
El paquete exporta contratos y schemas desde su única entrada pública:
import type {
EntityReference,
RelationshipQueryService,
EntitySummaryResolver,
} from "@metis/relationship-contracts";
import { createRelationshipRegistry } from "@metis/relationship-contracts";
Un consumidor deberá declarar @metis/relationship-contracts: workspace:* en sus
propias dependencias cuando exista. Assets, Resources y el servidor consumen ya este paquete. Las pruebas del paquete consumen esa misma entrada pública.
Las únicas dependencias de ejecución son @repo/rdx-utils (el Result existente)
y Zod 4.3.6 (ya utilizado en el repositorio). Configuración TypeScript y tipos de
Node son dependencias de desarrollo. No hay nueva biblioteca externa ni dependencia
de React, HTTP, ORM, base de datos o implementaciones de módulos.
Referencias e identificadores
EntityReference contiene providerId, entityType, id y companyId. Los dos
últimos son cadenas UUID de transporte: el schema aplica z.uuid(), como los DTO
actuales y UniqueID.create, y normaliza a minúsculas sin cambiar la identidad.
No se generan identidades nuevas aquí. Los futuros propietarios seguirán utilizando
UniqueID en dominio y UUID v7 para nuevas entidades.
Los identificadores de proveedor y los segmentos namespaced usan minúsculas,
números y guiones, comienzan con letra y no admiten espacios ni guiones terminales.
Ejemplos de forma: assets:asset, resources:resource, assets:member-of-domain.
Son ejemplos conceptuales, no tipos registrados. El prefijo de entityType debe
coincidir con providerId; el de typeId, con ownerProviderId. Los nombres
visibles, hostname, dirección IP y nombres de tabla no sustituyen UUID ni IDs de
contrato. El ámbito v1 es exclusivamente una compañía; un catálogo global conserva
sus contratos propios y no usa una compañía ficticia.
Los schemas son estrictos e infieren los tipos públicos mediante Zod. Referencias, definiciones, páginas y resúmenes validados son inmutables, también en sus objetos y arrays internos. Los valores construidos directamente en TypeScript deben seguir validándose al cruzar una frontera externa.
Definiciones y registro
EndpointRule declara tipo de entidad y capacidad opcional. Una capacidad es una
condición funcional del extremo, no un permiso del operador. El registro
comprueba su namespace de proveedor o su declaración explícita en el catálogo de
capacidades del tercer argumento; comprobar que una entidad ofrece esa capacidad corresponde
al propietario.
RelationshipDefinition declara propietario, extremos, etiquetas traducibles,
dirección, cardinalidad activa, representación, historial y modo de escritura:
directed: origen y destino tienen papeles distintos. La consultaoutgoingancla el origen,incomingel destino yeithercualquiera de ambos.symmetric: ambos extremos tienen la misma regla y los mismos límites. Todos los sentidos de consulta buscan las relaciones incidentes al ancla. La vista mantiene sus extremos estables; no se invierten al consultar desde el destino.- La cardinalidad admite enteros positivos o
null(sin límite). Es metadato declarativo: no crea restricciones SQL ni valida concurrencia o datos existentes. recordreferencia una relación persistida con UUID;propertyidentifica entidad propietaria y propiedad;derivedusa una clave estable del proveedor. Una derivación solo admiteread-only. Una propiedad puede modificarse mediante un comando específico del propietario, nunca por un CRUD genérico.historydeclaranone,lifecycleorevisions; no implementa un historial.stateCodepertenece al vocabulario del propietario, no a un enum global.roleCodees un código semántico opcional; su vocabulario se valida en el propietario. No es un contenedor arbitrario de metadatos.
createRelationshipRegistry(providers, definitions, capabilities?) devuelve el Result habitual.
Construye una copia profundamente inmutable con contractVersion: 1, listas de
proveedores, capacidades y definiciones. Puede construirse vacío. Rechaza definiciones inválidas,
propietarios incompatibles con su ID, IDs duplicados, proveedores duplicados y
namespaces no registrados de propietarios, extremos o capacidades.
Los errores de construcción son RelationshipRegistryError, subclase de Error,
con códigos INVALID_REGISTRY, DUPLICATE_PROVIDER, DUPLICATE_RELATIONSHIP y
UNREGISTERED_PROVIDER. Una definición inválida se rechaza antes de comprobar el
resto del catálogo. No se publica el detalle de datos inválidos en el error.
El composition root aporta explícitamente proveedores y definiciones al arrancar. El registro solo contiene metadatos: no contiene instancias, repositorios, callbacks, resolutores ni descubrimiento de plugins. Los servicios se inyectarán directamente, según el patrón de composición.
Consultas y resolución por lotes
RelationshipQueryService.list(query, context) devuelve
Promise<Result<RelationshipPage, RelationshipReadError>>. La consulta exige tipo,
ancla, sentido, includeEnded y tamaño entre 1 y 100. Cursor opcional opaco, no
vacío y de hasta 2048 caracteres; nextCursor: null señala fin de recorrido.
El proveedor limita la página al tamaño solicitado, mantiene orden estable y evita
duplicar una relación simétrica o autorreferente en either. El cursor debe estar
vinculado a la consulta, compañía y autorización aplicables; el schema valida solo
su forma, no su autenticidad ni significado.
validateRelationshipQuery(input, context, registry) valida la estructura, el
ámbito de compañía, que el tipo esté registrado y que el ancla sea compatible con
el sentido solicitado. Devuelve INVALID_QUERY o FORBIDDEN sin detalles de
entidades. No consulta datos, capacidades ni autorizaciones.
RelationshipOrigin, RelationshipView y RelationshipPage representan resultados
sin nombres embebidos en las relaciones. El schema de vista impide extremos de
compañías diferentes y propietarios de propiedad de otra compañía. Los proveedores
siguen siendo responsables de comprobar definición, representación, identidad de la
propiedad, estado y ámbito de todas las filas respecto al contexto y la consulta.
El schema de página no sustituye esa validación contextual.
EntitySummaryResolver.resolveMany(references, context) resuelve un lote por
proveedor. Devuelve un resultado por referencia, en el mismo orden, conservando las
repeticiones. Debe evitar consultas por fila y validar ámbito y tipos soportados.
EntitySummary contiene referencia, nombre, subtítulo opcional y estado opcional
(código y clave de traducción). No incorpora React, iconos ni rutas del navegador.
SummaryResolution distingue resolved de unavailable. Esta última variante
solo contiene la referencia: no revela nombre ni distingue entidad inexistente de
entidad inaccesible. La falta de acceso a una entidad no debe provocar un resumen
parcial con información privada. Un fallo del lote completo puede devolver
UNAVAILABLE; los adaptadores deben gestionar y registrar su causa internamente.
RelationshipReadError extiende Error porque el Result del repositorio exige
E extends Error. Expone INVALID_QUERY, FORBIDDEN o UNAVAILABLE, sin mensajes
libres ni detalles internos. No se introduce otro formato de Result.
Seguridad y límites
RelationshipContext contiene operatorId, companyId y requestId. Solo lo
construye el servidor a partir de una sesión autenticada y del ámbito autorizado.
No se acepta como parte del body del navegador. Su schema valida estructura, no
prueba autenticación, pertenencia o permisos; el tipado no es una credencial.
El proveedor debe autorizar la lectura de ambos extremos antes de exponer una relación y filtrar todas las consultas por la compañía del contexto. Poder leer el origen no autoriza leer el destino. El resolutor aplica además la autorización de cada resumen; nunca utiliza la relación como permiso implícito. Las referencias de otra compañía se rechazan, sin devolver relaciones ni resúmenes de esa compañía. Los identificadores de cliente atendido no son el tenant del prestador de soporte.
Este paquete no añade ACL. El runtime del paso 2.3 valida la sesión vigente mediante Identity y la compañía mediante Companies, antes de cada lectura. Es un acceso general de operadores autenticados a compañías atendidas, sin ACL granular. La fábrica interna de contextos y la autorización se describen en el documento del proveedor; el schema por sí solo sigue sin ser una credencial. Los grupos de un directorio externo no confieren autorizaciones internas de Metis Desk.
Comprobación y siguiente paso
Las pruebas de contrato verifican referencias, namespaces, inmutabilidad,
definiciones, duplicados, proveedores, cardinalidades simétricas, orígenes,
compañías, sentidos, paginación, errores Result, lotes y entrada pública. Las
implementaciones dentro de pruebas son fixtures identificadas; no se exportan como
proveedores reales. También se comprueban los grafos de dependencias locales e importaciones internas.
El script usa el runner nativo de Node con tsx y --test-isolation=none; las
pruebas no alteran estado global y se ejecutan en el mismo proceso. En este entorno,
el modo con subprocesos solo informó el fichero como una prueba, sin enumerar los
casos; se verificó expresamente la ejecución de cada caso sin ese aislamiento.
Los pasos 2.1 y 2.2 incorporan dominios Windows y pertenencias, respectivamente. El paso 2.3 adapta sus datos existentes al contrato común con paginación SQL y resolución por lotes. La presentación y los adaptadores HTTP del siguiente consumidor permanecen fuera de este paso. SMB podrá utilizar los contratos mediante sus propios tipos y políticas, sin compartir tablas ni comandos de escritura por obligación.