- 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.
102 lines
13 KiB
Markdown
102 lines
13 KiB
Markdown
# 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)` hacia `resource_network_shares`: exige destino SMB estructurado y evita referencias entre compañías.
|
||
- `UNIQUE(activeResourceId)`: solo el periodo vigente guarda ahí su `resourceId`; los finalizados tienen NULL.
|
||
- CHECK de coherencia entre `endedAt`, `endedBy` y `activeResourceId`, 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ón `outgoing`.
|
||
- Consulta inversa: ancla `resources:resource`, dirección `incoming`.
|
||
- `includeEnded: false` selecciona solo publicaciones vigentes; `true` añ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 con `action` y los campos de la tabla anterior. Respuesta `{ publication }`.
|
||
- `GET /api/asset-network-share-publications/:companyId`: colección con `assetId?`, `resourceId?`, `includeEnded=true|false` (por defecto false), `pageSize` (por defecto 50) y `cursor?`. Respuesta `{ publications, nextCursor }`.
|
||
- `GET /api/asset-network-share-publications/:companyId/relationships`: adaptador tipado acotado compartido con el paso 2.4. Parámetros `typeId`, `anchorType`, `anchorId`, `direction`, `includeEnded`, `pageSize` y `cursor?`; 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/server` y `@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=none` para 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 check` sobre los 65 ficheros TypeScript/TSX/JSON modificados o nuevos del trabajo acumulado: correcto, sin avisos; `eslint` sobre los 58 ficheros TS/TSX: correcto.
|
||
- `pnpm --filter @metis/server build`, revisión del grafo de dependencias de producción de 13 paquetes y `git diff --check`: correctos, sin ciclos.
|
||
- Companies: 171/173, con las dos expectativas previas en `companies-page.test.tsx:180` y `:210`. Typecheck directo del paquete vendor: reproduce TS2375 en `calendar.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](identidades-locales-y-cuentas-de-carpetas.md). La publicación sigue sin determinar quién accede a la carpeta ni qué cuenta utiliza un perfil.
|