metis-desk/docs/auditorias/resources-global-scope/03-sequelize-mariadb.md

73 lines
13 KiB
Markdown
Raw Normal View History

2026-10-24 23:19:17 +00:00
# 03 — Sequelize y MariaDB
## Esquema verificado en fuentes
**CONFIRMADO:** `resource.model.ts` no declara un `ResourceModel extends Model`; `migrateInventory` crea tablas mediante QueryInterface y el adaptador usa SQL parametrizado. Sí hay modelos Sequelize para catálogo, bindings y disponibilidad. No inventar asociaciones `belongsTo`/`hasMany` que no están declaradas. Las relaciones importantes aquí son FKs SQL y reconstrucción manual. [sql: resource.model.ts:19](../../../modules/resources/src/api/infrastructure/persistence/resource.model.ts#L19) [catalogSQL: resource-catalog.model.ts:48](../../../modules/resources/src/api/infrastructure/persistence/resource-catalog.model.ts#L48) [read: resource-inventory-relational.ts:195](../../../modules/resources/src/api/infrastructure/persistence/resource-inventory-relational.ts#L195).
| Tabla | PK / índices / referencias | Consecuencia |
| --- | --- | --- |
| resources | id PK; companyId NOT NULL → companies; templateId NOT NULL → resource_definitions | Todo Resource válido tiene dueño |
| resources, añadidos posteriores | unique directory_resource_scope_template(id,companyId,templateId); unique resource_company_identity(companyId,id) | Soportan FKs compuestas de dominios/SMB/asistencia; no unicidad de nombre |
| resource_revisions | id PK; unique resource_revision_position_unique(resourceId,position); resourceId/actorId/templateId FK | Conserva referencias y orden |
| resource_characteristics | PK resourceId,fieldId; FK a resources/definitions | Valores tipados normalizados |
| resource_revision_characteristics | PK revisionId,fieldId; resourceId y revisionId FK | Valores históricos, sin companyId repetido |
| resource_specification_fields / sections / field_choices | PK compuestas con resourceId; FK de raíz | Nombres, tipos, orden, obligatoriedad, opciones |
| resource_revision_specifications | revisionId PK/FK; resourceId FK | Cabecera histórica templateId/templateName/sectionsDefined |
| resource_revision_specification_fields / sections / choices | PK compuestas con revisionId; resourceId FK | Instantáneas de estructura |
| resource_definitions | id PK; stockKey unique nullable; companyId nullable | Ámbito global ya representado por NULL para definiciones |
| resource_template_fields | id PK; unique templateId,fieldId; FK de sección compuesta | Vínculos de campos |
| resource_availability | id PK; unique companyId,definitionId | Override de definiciones, no de Resource |
| resource_directory_domains | resourceId PK, companyId NOT NULL; FK (resourceId,companyId,templateId); unique companyId,normalizedDns y environmentId | Operación local de directorio |
| resource_network_shares | resourceId PK, companyId NOT NULL; FK compuesta; unique companyId,activeAddress | Carpeta local; inactive libera dirección activa |
| software_installations / application_configurations | id PK; companyId FK NOT NULL; resourceId FK simple; currentKey unique nullable | Ámbito de relaciones validado en Application, no por FK compuesta a Resource |
Fuentes: [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) [history: resource-revision-specifications.ts:39](../../../modules/resources/src/api/infrastructure/persistence/resource-revision-specifications.ts#L39) [directorySQL: directory-domain-migration.ts:5](../../../modules/resources/src/api/infrastructure/persistence/directory-domain-migration.ts#L5) [shareSQL: network-share-migration.ts:4](../../../modules/resources/src/api/infrastructure/persistence/network-share-migration.ts#L4) [supportSQL: support-migration.ts:86](../../../modules/assets/src/api/infrastructure/persistence/support-migration.ts#L86) [softwareSQL: software-installation-migration.ts:6](../../../modules/companies/src/api/infrastructure/persistence/software-installation-migration.ts#L6) [applicationSQL: application-configuration-migration.ts:6](../../../modules/companies/src/api/infrastructure/persistence/application-configuration-migration.ts#L6).
`resource_revision_specifications.templateId` se declara UUID NOT NULL sin FK en esa cabecera; la revisión base sí tiene FK de templateId. No atribuir todas las restricciones de la raíz a todas las tablas históricas. Las columnas de integración tienen políticas RESTRICT/CASCADE distintas según tabla referenciada; ver `fk` de resource-relational-schema y el borrado explícito. [history: resource-revision-specifications.ts:39](../../../modules/resources/src/api/infrastructure/persistence/resource-revision-specifications.ts#L39) [columns: resource-relational-schema.ts:25](../../../modules/resources/src/api/infrastructure/persistence/resource-relational-schema.ts#L25).
No hay unique SQL `(companyId,templateId,name)` en Resource. La regla de nombres vive en ResourceUseCases sobre un contexto bloqueado; el caso access distingue sistema/entorno/tipo. No sustituirla por un unique simplista. [save: resource.use-cases.ts:64](../../../modules/resources/src/api/application/resource.use-cases.ts#L64).
## Qué rompe el diseño scope + nullable
**DERIVABLE:** cambian simultáneamente dominio UniqueID obligatorio, InventoryRow, mapper, contrato ResourceSchema, captura de plantilla, load(company), borrado por compañía, propagación y dashboard. El scope explícito requiere impedir pares contradictorios; una columna nullable no resuelve semántica ni seguridad. [owner: resource.aggregate.ts:11](../../../modules/resources/src/api/domain/resource.aggregate.ts#L11) [mapper: resource-persistence.mapper.ts:24](../../../modules/resources/src/api/infrastructure/persistence/resource-persistence.mapper.ts#L24) [dto: inventory.ts:35](../../../modules/resources/src/common/inventory.ts#L35) [propagation: prepare-resource-template-updates.ts:15](../../../modules/resources/src/api/infrastructure/persistence/prepare-resource-template-updates.ts#L15) [dashboard: resource-dashboard-reader.ts:15](../../../modules/resources/src/api/infrastructure/persistence/resource-dashboard-reader.ts#L15).
Las FKs locales con companyId NOT NULL deben conservarse. Una referencia global no debe convertirse en SMB/dominio/asistencia. No hacer nullable la compañía de los hijos operativos. Las FKs simples de instalaciones/perfiles ya permiten apuntar al mismo resources.id desde compañías distintas; eso preserva identidad, pero no valida autorización ni ámbito. [softwareFK: software-installation-snapshot.ts:7](../../../modules/companies/src/api/infrastructure/persistence/software-installation-snapshot.ts#L7) [applicationFK: configuration-snapshots.ts:57](../../../modules/companies/src/api/infrastructure/persistence/configuration-snapshots.ts#L57).
**DOCUMENTADO en MariaDB oficial:** UNIQUE admite varios NULL, y una FK compuesta con algún valor NULL en el hijo no exige coincidencia del padre. Por tanto, añadir `unique(scope,companyId,name)` no garantiza unicidad global, ni una FK nullable puede sustituir la regla «global permitido O propietario local». Mantener hijos operativos NOT NULL y resolver la regla en servicio propietario + Application. [ALTER TABLE](https://mariadb.com/docs/server/reference/sql-statements/data-definition/alter/alter-table), [Foreign Keys](https://mariadb.com/docs/server/ha-and-performance/optimization-and-tuning/optimization-and-indexes/foreign-keys).
**PROPUESTO A:** scope VARCHAR restringido y NOT NULL; companyId nullable FK, con CHECK explícito de par: global implica NULL; company implica NOT NULL. Añadir validación global de plantilla/campos e integración en Resources. El CHECK de fila no resuelve la clasificación de una plantilla en otra tabla: no prometerlo como constraint automática. Alternativa B elimina el discriminador SQL redundante, ver [09](09-alternativas-arquitectonicas.md).
## Consultas e índices futuros
Evaluación **teórica**, sin EXPLAIN, volumen medido, estadísticas ni BD conectada. No se fijan mejoras en milisegundos ni se propone cada índice como obligatorio.
| Consulta | Hoy | Adaptación propuesta | Índice candidato / precaución |
| --- | --- | --- | --- |
| Compañía | readInventory por companyId y carga completa | Mantener gestión local | companyId,id existente |
| Global | No Resource global | Endpoint de catálogo, solo application elegible | scope,active,templateId,id según selectividad |
| Mixto | No | Candidatos autorizados: globals permitidos UNION ALL locales propios | Dos ramas disjuntas; orden y paginación conjunta estable |
| Por plantilla | Filtrado frecuente en navegador | Predicado templateId opcional | companyId,templateId,active,id si EXPLAIN lo justifica |
| Por uso | Servicio clasifica en memoria | Resolver stockKey/usage vía definitions | Evitar duplicar usage mutable en Resource |
| Por nombre | Filtro local del inventario completo | Búsqueda servidor parametrizada y limitada | Índice B-tree no garantiza acelerar substring/LOCATE; definir collation y criterio |
| Por ID | Find en contexto completo / FKs simples | Resolver lote/ID con compañía autorizada y propósito | PK id + comprobar visibilidad, nunca findByPk sin policy |
| Activos | Boolean en valores y filtros UI | Candidatos activos; resolución de usados incluye inactivos | Medir selectividad de active antes de indexar |
| Historial | Carga todas revisiones por raíz | Fuera del DTO de candidatos | Índices resourceId y posición existentes |
| Dashboard | Count incluye globales con filtro opcional, filas inner join companies | Separar catálogo global de métricas operativas | Evitar total que cuente NULL pero filas que lo omitan |
[read: resource-inventory-relational.ts:195](../../../modules/resources/src/api/infrastructure/persistence/resource-inventory-relational.ts#L195) [usage: resource-inventory.service.ts:54](../../../modules/resources/src/api/application/resource-inventory.service.ts#L54) [shareOptions: network-share-options.ts:11](../../../modules/resources/src/api/infrastructure/persistence/network-share-options.ts#L11) [dashboard: resource-dashboard-reader.ts:15](../../../modules/resources/src/api/infrastructure/persistence/resource-dashboard-reader.ts#L15). No hay búsqueda Resource de servidor con paginación hoy; las options SMB sí tienen cursor y LIMIT 21 y no deben ensancharse.
## Migración futura segura — no ejecutada
1. Inventariar SHOW CREATE TABLE/FKs/índices y versión exacta de MariaDB sobre entorno de prueba, respaldar y ensayar recuperación; verificar diferencias respecto a migraciones idempotentes.
2. Preparar autorización/lectores que toleren scope sin producir globales. Feature flag de escritura global desactivada, almacenada en configuración validada existente si se aprueba.
3. Expansión: añadir scope con default company compatible con escritores antiguos y backfill explícito de filas existentes; permitir companyId NULL manteniendo FK. Si ALTER encuentra dependencia de FK/índice, retirarla/recrear solo en orden ensayado, sin desactivar foreign_key_checks como solución.
4. CHECK de combinación, índices justificados y validaciones de mappers/DTO. Verificar companyId de todas las filas legacy no nulo; no cambiar IDs ni revisiones.
5. Desplegar lectores completos (incluyendo propagación, historial, resolución de nombres y dashboard) a todas las instancias y clientes relevantes. No activar globales con lectores antiguos: NULL rompe mapper y Zod antiguos.
6. Habilitar escrituras globales restringidas; luego candidatos para software y perfiles. Mantener gestión local y licencias aisladas. Probar concurrencia desactivación/asociación y eliminación.
7. Activación gradual con verificación de invariantes, recuentos por ámbito y referencias cruzadas; no crear adopciones implícitas ni backfill de versiones/licencias.
8. Contracción: retirar defaults de compatibilidad cuando no queden escritores antiguos, si el contrato final exige scope explícito. Versionar migración aplicada y evitar regenerar catálogo/IDs.
**Rollback:** antes de crear globales, revertir aplicación y comprobar que no quedan NULL antes de restablecer NOT NULL. Tras crear globales/referencias, deshabilitar nuevas escrituras, mantener lectores compatibles y corregir hacia delante o restaurar una copia coherente. No reasignar compañía, borrar globales ni copiar recursos automáticamente para forzar downgrade.
**DOCUMENTADO:** ALTER TABLE y otros DDL provocan commits implícitos; una transaction Sequelize no ofrece rollback de toda la migración. Usar pasos idempotentes verificables y ensayo de interrupción por paso. [MariaDB, implicit commit](https://mariadb.com/docs/server/reference/sql-statements/transactions/sql-statements-that-cause-an-implicit-commit). No se ejecutó DDL ni se midió bloqueo.