15 KiB
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; decisiones en 11.
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 runtime: identity-runtime.ts:17.
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 repository: sequelize-resource.repository.ts:32.
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 services: services.ts:11 supportRead: sequelize-support.repository.ts:21.
Inventario de implementación
Los formularios, selectores y demás consumidores se detallan en 05 y 06.
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 input: resource-values.ts:40 spec: resource-values.ts:49 sql: resource.model.ts:19 typed: resource-relational-schema.ts:98.
| 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.
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 mapper: resource-persistence.mapper.ts:24 history: resource-revision-specifications.ts:39.
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
- Alta: el path aporta companyId; no se deduce de una compañía de la sesión. El actor sí procede de sesión.
ResourceUseCases.saveconvierte UUID, carga el contexto y usamapResourceInput. - 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. - Persistencia: el repositorio bloquea catálogo global, catálogo de accesos cuando procede y compañía;
writeInventoryescribe raíz, tablas actuales y revisiones nuevas en transacción, más proyecciones. No hay una transacción distribuida de plugins. - 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. - 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. - Estado:
setActiveconstruye valores nuevos y aplica update; conservar ID y primera revisión. La desactivación no retira instalaciones ni configuraciones automáticamente. - Borrado: físico y explícito, no paranoid/soft delete.
removeResourcebloquea raíces y descubre FKs entrantes mediante information_schema; rechazaRESOURCE_IN_USEsi 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 load: sequelize-resource.repository.ts:58 update: resource.aggregate.ts:100 remove: remove-resource.ts:37. 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.
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 applicationPolicy: application-configuration-policy.ts:5. Decisiones de autorización y consulta están en 04 y 07.