metis-desk/docs/arquitectura/contratos-de-relaciones-tipadas.md
2026-10-20 13:20:22 +02:00

174 lines
10 KiB
Markdown

# 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](proveedor-de-relaciones-tipadas.md) 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:
```ts
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 consulta `outgoing`
ancla el origen, `incoming` el destino y `either` cualquiera 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.
- `record` referencia una relación persistida con UUID; `property` identifica
entidad propietaria y propiedad; `derived` usa una clave estable del proveedor.
Una derivación solo admite `read-only`. Una propiedad puede modificarse mediante
un comando específico del propietario, nunca por un CRUD genérico.
- `history` declara `none`, `lifecycle` o `revisions`; no implementa un historial.
`stateCode` pertenece al vocabulario del propietario, no a un enum global.
`roleCode` es 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](composicion-de-modulos.md).
## 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](proveedor-de-relaciones-tipadas.md) 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.