13 KiB
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 catalogSQL: resource-catalog.model.ts:48 read: resource-inventory-relational.ts:195.
| 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 typed: resource-relational-schema.ts:98 history: resource-revision-specifications.ts:39 directorySQL: directory-domain-migration.ts:5 shareSQL: network-share-migration.ts:4 supportSQL: support-migration.ts:86 softwareSQL: software-installation-migration.ts:6 applicationSQL: application-configuration-migration.ts:6.
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 columns: resource-relational-schema.ts:25.
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.
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 mapper: resource-persistence.mapper.ts:24 dto: inventory.ts:35 propagation: prepare-resource-template-updates.ts:15 dashboard: resource-dashboard-reader.ts:15.
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 applicationFK: configuration-snapshots.ts:57.
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, 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.
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 usage: resource-inventory.service.ts:54 shareOptions: network-share-options.ts:11 dashboard: resource-dashboard-reader.ts:15. 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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- Activación gradual con verificación de invariantes, recuentos por ámbito y referencias cruzadas; no crear adopciones implícitas ni backfill de versiones/licencias.
- 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. No se ejecutó DDL ni se midió bloqueo.