metis-desk/docs/auditorias/resources-global-scope/01-modelo-resource.md
2026-10-25 01:19:17 +02:00

88 lines
15 KiB
Markdown

# 01 — Modelo Resource actual
Fecha de corte: 24 de octubre de 2026. Investigación del árbol de trabajo, incluidos cambios locales previos, sin modificar aplicación ni datos. Las líneas enlazadas corresponden a ese árbol, no necesariamente a HEAD. Método, limitaciones y huellas en [12](12-matriz-trazabilidad.md); decisiones en [11](11-resumen-ejecutivo.md).
Etiquetas para toda la serie: **CONFIRMADO** (código/esquema del repositorio), **DERIVABLE** (consecuencia de contratos), **DOCUMENTADO** (texto sin ejecución verificada), **PROPUESTO** (diseño futuro), **NO CONFIRMADO** (evidencia insuficiente), **RIESGO** (requiere mitigación). «Esquema actual» significa esquema que construyen las migraciones del repositorio; no se inspeccionó ninguna BD desplegada.
## Arquitectura y mapa de dependencias
**CONFIRMADO.** TypeScript, React 19.2.5, TanStack Query 5.98.0, Zod 4.3.6 y Sequelize 6.37.8 figuran en los manifiestos. MariaDB se configura en `apps/server/src/config/database.ts` y en Docker Compose. Resources, Assets, Companies e Identity son módulos con composition roots explícitos. [exports: package.json:6](../../../modules/resources/package.json#L6) [runtime: identity-runtime.ts:17](../../../apps/server/src/infrastructure/http/identity-runtime.ts#L17).
```mermaid
flowchart TD
UI[ResourcesPage / ResourceEditor] --> API[inventoryApi + schemas Zod]
API --> HTTP[registerResourceRoutes]
HTTP --> UC[ResourceUseCases]
UC --> PORT[IResourceRepository]
PORT --> REPO[SequelizeResourceRepository]
REPO --> DOM[Resource + ResourceValues + ResourceSpecification + ResourceRevision]
REPO --> SQL[resources y tablas relacionales de valores/especificaciones/revisiones]
CAT[ResourceCatalog + ResourceDefinition] --> REPO
REPO --> PROJ[Proyecciones Mailbox / Access / Directorio / SMB]
COMP[Companies: instalaciones y configuraciones] --> SERVICE[IResourceInventoryService]
SERVICE --> PORT
```
Dependencias de compilación: Infrastructure → Application → Domain. El puerto se declara en Application; el adaptador lo implementa en Infrastructure; el diagrama representa llamadas, no una dependencia del puerto hacia Sequelize. [port: resource.repository.ts:21](../../../modules/resources/src/api/application/resource.repository.ts#L21) [repository: sequelize-resource.repository.ts:32](../../../modules/resources/src/api/infrastructure/persistence/sequelize-resource.repository.ts#L32).
Superficies públicas: `@metis/resources/common`, `/client`, `/module`, `/services`. `/services` exporta puertos de inventario, directorio, red y escritores de contexto. Companies consume esos servicios; Assets consume lectores de dominios/SMB. No importar `ResourcePersistenceMapper`, `ResourceCatalogModel` ni repositorios privados desde otros módulos. La búsqueda no localizó imports privados de Resources en consumidores de producción; sí imports relativos internos en pruebas de integración de apps/server (template-propagation, directory-domains, smb-access y fixtures), que no son APIs públicas de negocio. **CONFIRMADO:** existen consultas SQL propietarias de Resources en la migración y repositorio de asistencia de Assets; son acoplamiento transversal existente, no una API recomendada. **NO CONFIRMADO:** un framework dinámico «Plugin Architecture v1»; los registros de relaciones tipadas no lo constituyen. [module: module.ts:39](../../../modules/resources/src/api/module.ts#L39) [services: services.ts:11](../../../modules/resources/src/api/services.ts#L11) [supportRead: sequelize-support.repository.ts:21](../../../modules/assets/src/api/infrastructure/persistence/sequelize-support.repository.ts#L21).
## Inventario de implementación
| Capa | Entradas concretas | Responsabilidad |
| --- | --- | --- |
| Dominio | [aggregate: resource.aggregate.ts:17](../../../modules/resources/src/api/domain/resource.aggregate.ts#L17) [values: resource-values.vo.ts:123](../../../modules/resources/src/api/domain/value-objects/resource-values.vo.ts#L123) [definition: resource-definition.ts:23](../../../modules/resources/src/api/domain/resource-definition.ts#L23) | Identidad, valores, especificación y revisiones |
| Application | [save: resource.use-cases.ts:64](../../../modules/resources/src/api/application/resource.use-cases.ts#L64) [mapInput: resource-input.mapper.ts:13](../../../modules/resources/src/api/application/resource-input.mapper.ts#L13) [capture: resource-input.mapper.ts:69](../../../modules/resources/src/api/application/resource-input.mapper.ts#L69) | Alta/edición, unicidad visible y referencias |
| Puertos/adaptador | [port: resource.repository.ts:21](../../../modules/resources/src/api/application/resource.repository.ts#L21) [load: sequelize-resource.repository.ts:58](../../../modules/resources/src/api/infrastructure/persistence/sequelize-resource.repository.ts#L58) | Contexto por compañía, bloqueo y persistencia |
| Esquema/SQL | [sql: resource.model.ts:19](../../../modules/resources/src/api/infrastructure/persistence/resource.model.ts#L19) [columns: resource-relational-schema.ts:25](../../../modules/resources/src/api/infrastructure/persistence/resource-relational-schema.ts#L25) [read: resource-inventory-relational.ts:195](../../../modules/resources/src/api/infrastructure/persistence/resource-inventory-relational.ts#L195) [write: resource-inventory-relational.ts:315](../../../modules/resources/src/api/infrastructure/persistence/resource-inventory-relational.ts#L315) | Tablas normalizadas, sin documento JSON |
| HTTP/DTO | [routes: resource.routes.ts:54](../../../modules/resources/src/api/infrastructure/http/resource.routes.ts#L54) [dto: inventory.ts:35](../../../modules/resources/src/common/inventory.ts#L35) | Validación de params/body, traducción de resultados |
| Web | [inventoryAPI: inventory-api.ts:11](../../../modules/resources/src/web/resources/shared/api/inventory-api.ts#L11) [resourceController: use-resources-page-controller.ts:24](../../../modules/resources/src/web/resources/list/controllers/use-resources-page-controller.ts#L24) [resourceForm: use-resource-form-controller.ts:16](../../../modules/resources/src/web/resources/shared/controllers/use-resource-form-controller.ts#L16) [resourceDetail: resource-detail.tsx:11](../../../modules/resources/src/web/resources/detail/ui/resource-detail.tsx#L11) | Consultas, filtros, formulario y ficha/historial |
| i18n | `modules/resources/src/common/locales/es.json`, `en.json` | Etiquetas, ayuda y errores; traducciones operativas en Companies |
| Pruebas | [unitResource: resource.test.ts:38](../../../modules/resources/src/api/domain/resource.test.ts#L38) [unitLifecycle: resource-lifecycle.test.ts:94](../../../modules/resources/src/api/application/resource-lifecycle.test.ts#L94) [integrationResource: resources.integration.ts:19](../../../apps/server/src/resources.integration.ts#L19) [integrationLifecycle: resource-lifecycle.integration.ts:11](../../../apps/server/src/resource-lifecycle.integration.ts#L11) | Dominio, ciclo de vida y contratos HTTP/SQL |
Los formularios, selectores y demás consumidores se detallan en [05](05-consumidores-resource.md) y [06](06-software-configuraciones.md).
## Propiedades de raíz y transporte
**CONFIRMADO.** `ResourceProps` solo contiene compañía, especificación, valores e historial; `id` viene de AggregateRoot. `values`, `specification` e `history` son objetos reconstruidos, no columnas JSON. [owner: resource.aggregate.ts:11](../../../modules/resources/src/api/domain/resource.aggregate.ts#L11) [input: resource-values.ts:40](../../../modules/resources/src/api/domain/resource-values.ts#L40) [spec: resource-values.ts:49](../../../modules/resources/src/api/domain/resource-values.ts#L49) [sql: resource.model.ts:19](../../../modules/resources/src/api/infrastructure/persistence/resource.model.ts#L19) [typed: resource-relational-schema.ts:98](../../../modules/resources/src/api/infrastructure/persistence/resource-relational-schema.ts#L98).
| Campo | Tipo | Nullable | Obligatorio | Restricción | Uso |
| --- | --- | --- | --- | --- | --- |
| id | UniqueID / UUID | No | Sí; generado al crear | PK, UUID v7 nuevo; UUID válido al rehidratar | Identidad estable independiente del nombre |
| companyId | UniqueID / UUID | No | Sí | FK companies, RESTRICT | Propiedad y contexto de carga/escritura |
| templateId / values.templateId | UUID | No | Sí | FK resource_definitions; plantilla fija | Selección por ID, no por nombre |
| values.name | string(1..100) | No | Sí | Name normalizado; duplicado controlado en Application | Identificación visible |
| values.active | boolean | No | Sí | Sin default en entrada | Activar/desactivar sin borrar |
| values.notes | string ≤10000 / TEXT | No | Sí; admite vacío | Zod | Observaciones |
| values.characteristics | record UUID → string/number/boolean | No | Sí; admite objeto vacío si los campos lo permiten | Solo fields de specification, tipo y required | Valores dinámicos; fecha es string ISO |
| values.integration | mailbox/access discriminado | Omitible | Condicional | Campos y referencias validados; contexto fijo | Compatibilidad operativa |
| specification.templateId/templateName | UUID/string ≤100 | No | Sí | Captura de plantilla | Estructura actual |
| specification.fields | array ≥1 | No | Sí | fieldId único, tipos text/number/date/boolean/choice | Nombre, unidad, opciones, required, sectionId opcional |
| specification.sections | array de id/name/description | Omitible | No | Referencias coherentes | Organización visual |
| history | ResourceRevision[] ≥1 en DTO/mapper | No | Sí | Orden, fechas y última revisión coherentes | Auditoría |
| directory / networkShare | DTO derivado | Omitible | Solo capacidad aplicable | Proyección de dominio/SMB | No son propiedades de ResourceProps |
Integración persistida: `integrationKind` VARCHAR(10), `addressFieldId`, `contextFieldId`, `accountId`, `referenceFieldId`, `resourceTypeId`, `systemId`, `environmentId` UUID nullable; `mailboxKind` VARCHAR(10) nullable. Los UUID referencian definiciones o Access según columna. `templateName` VARCHAR(100) NOT NULL y `sectionsDefined` boolean NOT NULL describen la especificación actual. [columns: resource-relational-schema.ts:25](../../../modules/resources/src/api/infrastructure/persistence/resource-relational-schema.ts#L25).
Cada revisión persiste `id`, `resourceId`, `actorId`, `recordedAt` VARCHAR(30), `position`, `templateId`, nombre/actividad/notas e integración. Valores y especificación histórica están en tablas hijas. Actor tiene FK a users. `actorName` es resuelto al construir respuesta, no una instantánea del nombre del operador. [sql: resource.model.ts:19](../../../modules/resources/src/api/infrastructure/persistence/resource.model.ts#L19) [mapper: resource-persistence.mapper.ts:24](../../../modules/resources/src/api/infrastructure/persistence/resource-persistence.mapper.ts#L24) [history: resource-revision-specifications.ts:39](../../../modules/resources/src/api/infrastructure/persistence/resource-revision-specifications.ts#L39).
**CONFIRMADO por inventario estático:** no hay `scope`, `externalId`, `createdAt`, `updatedAt`, `deletedAt`, columna de versión optimista ni identificador canónico de producto. Una característica propia puede contener una referencia textual, sin unicidad ni semántica externa automática. No hay control optimista de ediciones con una versión enviada por cliente. Con integración, validateIntegration limita notes a 4000 y referencias documentales a 255; el máximo genérico de notes sigue siendo 10000. La concurrencia se ordena mediante transacciones/bloqueos y posición única del historial.
## Ciclo de vida y reglas
1. Alta: el path aporta companyId; no se deduce de una compañía de la sesión. El actor sí procede de sesión. `ResourceUseCases.save` convierte UUID, carga el contexto y usa `mapResourceInput`.
2. Validación: captura plantilla visible y activa; verifica integración/referencias y características con `ResourceValues.create`. Nombre repetido se rechaza por plantilla y contexto de acceso en la compañía; direcciones de buzón tienen control adicional.
3. Persistencia: el repositorio bloquea catálogo global, catálogo de accesos cuando procede y compañía; `writeInventory` escribe raíz, tablas actuales y revisiones nuevas en transacción, más proyecciones. No hay una transacción distribuida de plugins.
4. Consulta: `readInventory(companyId)` carga raíces de esa compañía y sus tablas hijas por IDs; mapper comprueba UUID, tipos, orden y última revisión. Repositorio comprueba plantilla/campos visibles para esa compañía.
5. Edición: plantilla fija (`TEMPLATE_FIXED`); varios atributos de integración son fijos (`CONTEXT_FIXED`). Igualdad semántica evita nueva revisión, aunque el adaptador de Resource puede reescribir tablas actuales.
6. Estado: `setActive` construye valores nuevos y aplica update; conservar ID y primera revisión. La desactivación no retira instalaciones ni configuraciones automáticamente.
7. Borrado: físico y explícito, no paranoid/soft delete. `removeResource` bloquea raíces y descubre FKs entrantes mediante information_schema; rechaza `RESOURCE_IN_USE` si hay una asociación externa actual o histórica; elimina datos propios y proyecciones admitidas cuando no los hay. La FK no protege referencias textuales sin FK.
Evidencia: [save: resource.use-cases.ts:64](../../../modules/resources/src/api/application/resource.use-cases.ts#L64) [load: sequelize-resource.repository.ts:58](../../../modules/resources/src/api/infrastructure/persistence/sequelize-resource.repository.ts#L58) [update: resource.aggregate.ts:100](../../../modules/resources/src/api/domain/resource.aggregate.ts#L100) [remove: remove-resource.ts:37](../../../modules/resources/src/api/infrastructure/persistence/remove-resource.ts#L37). No se encontraron recursos válidos sin compañía en este contrato; **NO CONFIRMADO** su ausencia física en una BD real, no consultada.
## Historial e impacto del ámbito global
La compañía no se repite como dato independiente en cada revisión de Resource: se hereda de la raíz. Cambiar el propietario cambiaría cómo se interpreta todo el historial; por ello se propone ámbito/propietario inmutables. Un ID global único permite conservar las FKs simples de software/perfil, pero los mappers y lectores actuales fallarían ante companyId NULL. No equivale a soportar globales. [mapper: resource-persistence.mapper.ts:24](../../../modules/resources/src/api/infrastructure/persistence/resource-persistence.mapper.ts#L24).
**DERIVABLE:** al desactivar un recurso referenciado, `softwareAvailable` y `applicationAvailable` pasan a false; sus builders marcan `needsReview` para registros abiertos. Las referencias históricas siguen resolviéndose si la lectura incluye inactivos. El futuro catálogo deberá mantener ese comportamiento para todas las compañías consumidoras. [softwareResponse: software-installation-response.builder.ts:7](../../../modules/companies/src/api/application/software-installation-response.builder.ts#L7) [applicationPolicy: application-configuration-policy.ts:5](../../../modules/companies/src/api/application/application-configuration-policy.ts#L5). Decisiones de autorización y consulta están en [04](04-aislamiento-autorizacion.md) y [07](07-contratos-api.md).