- Asociar entornos locales con activos y conservar su trazabilidad. - Registrar la cuenta de acceso del mapeo con historial de cambios. - Calcular cobertura documental específica sin alterar la cobertura general. - Añadir migraciones, validaciones, endpoints protegidos, pruebas y documentación.
13 KiB
Publicación de recursos compartidos SMB v1
Alcance y propiedad
Assets es propietario de assets:network-share-publication: Asset → publica → Resource con capacidad network:smb-share. Resources conserva el UUID, la UNC, la proyección SMB y su integración con AccessWorkspace. La publicación es un hecho documental registrado por un operador; no verifica disponibilidad, conectividad, permisos ni almacenamiento físico. Un alias UNC no identifica necesariamente al equipo publicador.
Un activo puede publicar múltiples carpetas. Cada recurso tiene cero o un publicador vigente y varios históricos. Un publicador desconocido se representa por ausencia de publicación vigente, sin activos ficticios. Nunca se infiere el publicador del hostname de la UNC.
No existe una clasificación de Assets que certifique capacidad real de servir SMB. Por ello se permite seleccionar explícitamente cualquier activo existente de la compañía con estado operational, available o in_use, siguiendo la política operativa existente. Esto admite Windows Server, NAS, equipos de sobremesa, portátiles y Linux con Samba, sin exigir sistema operativo, dominio, rol server/storage ni coincidencia entre nombre y UNC. La selección declara la relación; no descubre servicios.
Modelo e integridad
La migración explícita migrateNetworkSharePublications crea asset_network_share_publications; no convierte recursos históricos ni crea publicaciones automáticamente. Se ejecuta en la composición de Assets cuando se inyecta el lector público SMB de Resources.
Cada periodo es un NetworkSharePublication, con UUID v7 propio, compañía, activo, recurso, actor y fecha de inicio; al finalizar conserva su identidad y añade actor y fecha de cierre. El traslado crea otro periodo con predecessorId, sin cambiar el UUID del Resource. El contrato de historial es lifecycle: los periodos y su enlace de sucesión conservan el alta, cierre y traslado; no se introduce un historial universal ni una tabla paralela de revisiones.
MariaDB refuerza:
- FK
(companyId,assetId)hacia Assets y(resourceId,companyId)haciaresource_network_shares: exige destino SMB estructurado y evita referencias entre compañías. UNIQUE(activeResourceId): solo el periodo vigente guarda ahí suresourceId; los finalizados tienen NULL.- CHECK de coherencia entre
endedAt,endedByyactiveResourceId, y cierre posterior o igual al inicio. UNIQUE(predecessorId)y FK compuesta al periodo anterior de la misma compañía y recurso: un periodo tiene como máximo un sucesor de traslado.- FK de actores a Identity, índices
(companyId,assetId,id)y(companyId,resourceId,id), sin cascadas destructivas.
No hay comando público para borrar ni reescribir periodos históricos. Los mecanismos existentes de protección de borrado inspeccionan las FK externas de Assets y de la proyección SMB de Resources. Bloquean el borrado físico incluso cuando solo quedan periodos finalizados (ASSET_IN_USE / RESOURCE_IN_USE). No es necesaria una dependencia inversa Resources → Assets.
Comandos, concurrencia e idempotencia
NetworkSharePublicationUseCases valida entradas estrictas y delega la transacción al repositorio especializado de Assets. Consume únicamente INetworkShareReader, mediante la factoría pública de Resources ligada a la transacción.
| Acción | Entrada adicional | Resultado |
|---|---|---|
publish |
assetId, resourceId |
Crea un periodo si no hay publicador; devuelve el mismo periodo si ya publica ese activo; otro publicador produce PUBLICATION_CONFLICT. |
end |
publicationId |
Cierra ese periodo. Si ya estaba cerrado devuelve su cierre original sin afectar a un sucesor vigente. |
transfer |
publicationId, assetId nuevo |
Cierra el periodo indicado y crea su sucesor en una transacción. Exige que esté vigente y que cambie el activo. |
El reintento de un traslado devuelve el sucesor persistido si coinciden predecesor y nuevo activo, incluso si ese sucesor ya terminó o cambió de estado su activo. Un destino diferente para el mismo predecesor produce conflicto. Tras finalizar sin sucesor se utiliza publish para comenzar un nuevo periodo. El alta repetida es idempotente respecto de la publicación vigente, no una clave global de idempotencia para todas las altas históricas.
Las mutaciones bloquean la compañía antes de leer periodos y comprobar cardinalidad, compartiendo la serialización por compañía de las escrituras de inventario. Después bloquean el activo destinatario y leen/bloquean el recurso SMB a través de su lector público. El cierre se persiste antes que el sucesor, dentro de la misma transacción. Una excepción revierte ambas escrituras; la unicidad SQL es la garantía final y sus conflictos se traducen a PUBLICATION_CONFLICT.
No se permiten nuevas publicaciones ni traslados hacia activos no operativos o recursos inactivos (ASSET_INACTIVE, SMB_RESOURCE_INACTIVE). La desactivación concurrente queda serializada: o se registra antes de la publicación y esta se rechaza, o se registra después y conserva la publicación. Nunca se cierran periodos automáticamente por desactivación. Finalizar y recuperar un resultado idempotente siguen permitidos.
Consultas y Typed Relationships
NetworkSharePublicationQuery es el segundo RelationshipQueryService real. La composición estática mantiene assets:directory-membership, añade la definición de publicación y registra network:smb-share; los proveedores siguen siendo assets y resources.
La definición es dirigida, record, lifecycle, owner-command, con múltiples destinos por activo y máximo un origen vigente por recurso. Las vistas incluyen typeId, referencias de origen y destino, origin: { kind: "record", id: publicationId } y estado active / ended. No tienen roleCode ni atributos arbitrarios.
- Consulta directa: ancla
assets:asset, direcciónoutgoing. - Consulta inversa: ancla
resources:resource, direcciónincoming. includeEnded: falseselecciona solo publicaciones vigentes;trueañade las finalizadas. La orientación de las referencias permanece activo → recurso.- Anclas inexistentes, ajenas o no SMB se consideran no disponibles. El contexto procede de la autorización existente, sin aceptar contextos fabricados por consumidores.
La proyección compartida readPublicationPage filtra compañía, ancla y estado en SQL, ordena por UUID y consulta LIMIT pageSize+1, con id > afterId. Solo retiene el centinela adicional para detectar siguiente página. No hidrata agregados completos ni sus revisiones y no pagina un inventario en memoria. pageSize admite 1–100.
SignedQueryCursor reutiliza el codec HMAC del proveedor Directory mediante su adaptador existente. Cada instancia tiene clave efímera propia; el ámbito incluye compañía, operador y consulta normalizada (ancla, dirección, estado y tamaño). Publicaciones especializadas, publicaciones tipadas y Directory usan ámbitos/instancias separados: sus cursores son incompatibles. Cambiar filtros, compañía u operador invalida el cursor. El identificador de petición no forma parte del ámbito. Reiniciar o cambiar de instancia de servidor requiere comenzar la paginación de nuevo, igual que en el patrón previo; no se persisten claves nuevas ni datos de negocio en JSON.
Se reutiliza AssetSummaryResolver. Resources aporta NetworkShareSummaryResolver, exclusivo de proyecciones SMB: una consulta por lote de hasta 100 referencias, deduplicada y acotada por compañía. Conserva orden y repetidos y devuelve unavailable sin filtrar nombres para recursos inexistentes, ajenos, ordinarios o dominios Windows. Los resúmenes de recursos inactivos mantienen su estado real.
HTTP y autorización
Rutas especializadas autenticadas:
POST /api/asset-network-share-publications/:companyId: comando estricto conactiony los campos de la tabla anterior. Respuesta{ publication }.GET /api/asset-network-share-publications/:companyId: colección conassetId?,resourceId?,includeEnded=true|false(por defecto false),pageSize(por defecto 50) ycursor?. Respuesta{ publications, nextCursor }.GET /api/asset-network-share-publications/:companyId/relationships: adaptador tipado acotado compartido con el paso 2.4. ParámetrostypeId,anchorType,anchorId,direction,includeEnded,pageSizeycursor?; respuesta{ page, summaries }. Admite únicamente las dos definiciones registradas en este adaptador, sin explorador universal.
La ruta anterior /api/asset-directory-memberships/:companyId/relationships conserva el mismo adaptador y sus consultas Directory existentes. Las opciones de dominios y elegibilidad no cambian.
Para obtener el publicador vigente se filtra por resourceId con includeEnded=false; habrá cero o un elemento. Para el historial se usa el mismo filtro con includeEnded=true, siguiendo nextCursor. Para las carpetas de un equipo se filtra por assetId.
El actor se obtiene de la sesión autenticada, nunca de operatorId enviado por el navegador. Las escrituras usan el control global de origen y CSRF de Identity; compañía e identidades se validan en la frontera y en las consultas. Se mantiene la política actual de operadores autenticados de Metis Desk y el contexto de lectura autorizado ya existente, sin añadir un segundo sistema de permisos. Entrada inválida devuelve 400, ausencia funcional 404 y conflicto 409; la lectura tipada mantiene sus errores 400/403/404. Los errores inesperados siguen el mecanismo HTTP centralizado.
AccessWorkspace y límites
Publicar, finalizar o trasladar no modifica Resource, AccessItem(resource), concesiones, necesidades, cobertura, WorkProfile ni FolderMapping. Tampoco asigna dominios, cuentas locales ni grupos. La integración automática del paso 3.2 permanece en Resources/Companies y no interviene en estos comandos.
No se añade UI. Los contratos, identificadores de periodo, lectura paginada, resúmenes y comandos dejan preparada la presentación del paso 3.5. Local Identities y ampliaciones de FolderMapping pertenecen al paso 3.4. No se documentan puertos de switch, disponibilidad técnica ni topología de almacenamiento.
Validación
La suite apps/server/src/network-share-publications.integration.test.ts exige NODE_ENV=test y DB_NAME=metis_desk_test; genera y elimina exclusivamente sus compañías y actores de fixture. Cubre periodos, variantes de activo, cardinalidad SQL, compañía, borrado histórico, rollback por fallo de inserción, altas/traslados/cierres concurrentes, desactivación concurrente, cursores, SQL keyset, resolución por lote, accesos sin modificaciones y HTTP con sesión/origen/CSRF. Las pruebas de dominio viven en modules/assets/src/api/domain/network-share-publication.test.ts.
Comando específico: pnpm --filter @metis/server test:network-share-publications. Deben ejecutarse también las regresiones de SMB, accesos y dominios, las suites de Assets/Resources/relationship-contracts, los typechecks afectados, Biome, ESLint, build del servidor y git diff --check.
Fallos previos conocidos ajenos a esta implementación: dos expectativas de texto en Companies y TS2375 en packages/shadcn-ui/src/components/calendar.tsx. No se cambia vendor ni se ajustan esas pruebas.
Resultado de la verificación del paso 3.3
- Typechecks de
@metis/assets,@metis/resources,@metis/relationship-contracts,@metis/servery@metis/web: correctos (pnpm --filter … typecheck). - Suites de Assets: 131/131; Resources: 80/80; contratos tipados: 13/13. Se ejecutaron los scripts con
--test-isolation=nonepara comprobar todos los casos. - Integración MariaDB de publicaciones, SMB, accesos y Directory: 45/45. Regresiones adicionales de inventario, infraestructura, sedes y propagación de plantillas: 4/4. La suite nueva de publicaciones (10 casos incluidos en los 45) también se repitió con esas regresiones.
biome checksobre los 65 ficheros TypeScript/TSX/JSON modificados o nuevos del trabajo acumulado: correcto, sin avisos;eslintsobre los 58 ficheros TS/TSX: correcto.pnpm --filter @metis/server build, revisión del grafo de dependencias de producción de 13 paquetes ygit diff --check: correctos, sin ciclos.- Companies: 171/173, con las dos expectativas previas en
companies-page.test.tsx:180y:210. Typecheck directo del paquete vendor: reproduce TS2375 encalendar.tsx:165; no se modifica.
Todas las integraciones se ejecutaron con DB_NAME=metis_desk_test NODE_ENV=test node --env-file=../../.env --import tsx --test --test-isolation=none --test-concurrency=1 … desde apps/server, sin intervenir en datos de compañías reales.
Identidades locales y cuenta del mapeo (paso 3.4)
La clasificación local de entornos y la cuenta documental de cada FolderMapping se describen en identidades locales y cuentas de carpetas. La publicación sigue sin determinar quién accede a la carpeta ni qué cuenta utiliza un perfil.