metis-desk/modules/companies/src/api/infrastructure/persistence/resource-access-writer.ts
David fe6ec361b7 feat(companies): vincular identidades locales a activos y añadir cuenta de acceso a FolderMapping
- 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.
2026-10-21 11:25:23 +02:00

187 lines
6.5 KiB
TypeScript

import type { ResourceInput } from "@metis/resources/common";
import type { IResourceAccessWriter } from "@metis/resources/services";
import { UniqueID } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import type { Sequelize, Transaction } from "sequelize";
import { QueryTypes } from "sequelize";
import { AccessDefinitionSchema } from "../../domain/access/access-definition";
import { AccessError } from "../../domain/access/access-error";
import { AccessItem } from "../../domain/access/access-item.entity";
import { AccessValues } from "../../domain/access/access-values.vo";
import type { SequelizeAccessRepository } from "./access-persistence";
import { writeAccessRow } from "./access-relational";
const SMB_RESOURCE_TYPE_NAME = "Carpeta compartida SMB";
export class ResourceAccessWriter implements IResourceAccessWriter {
constructor(
private readonly database: Sequelize,
private readonly repository: SequelizeAccessRepository,
private readonly transaction: Transaction
) {}
async ensureSmbType(input: Parameters<IResourceAccessWriter["ensureSmbType"]>[0]) {
let typeId: string | undefined;
const result = await this.repository.updateInTransaction(
Maybe.some(input.companyId),
(snapshot) => {
const items = [...snapshot.context.catalog, ...snapshot.workspace.items];
const existing = input.preferredTypeId
? items.find((item) => item.id.toPrimitive() === input.preferredTypeId)
: items.find(
(item) =>
item.values.data.kind === "resource_type" &&
item.values.data.name.normalize("NFC").toLowerCase() ===
SMB_RESOURCE_TYPE_NAME.toLowerCase()
);
if (existing) {
if (existing.values.data.kind !== "resource_type" || !existing.values.data.active) {
return Result.fail(new AccessError("INACTIVE_TARGET"));
}
typeId = existing.id.toPrimitive();
return Result.ok(snapshot.workspace);
}
if (input.preferredTypeId) {
return Result.fail(new AccessError("INACTIVE_TARGET"));
}
const values = AccessValues.create({
kind: "resource_type",
name: SMB_RESOURCE_TYPE_NAME,
active: true,
});
if (!values.isSuccess) {
return values;
}
const item = AccessItem.create(Maybe.some(input.companyId), values.data, {
actorId: input.actorId,
recordedAt: input.recordedAt,
});
const saved = snapshot.workspace.save(item, snapshot.context);
if (saved.isSuccess) {
typeId = item.id.toPrimitive();
}
return saved;
},
this.transaction
);
if (!result.isSuccess) {
return Result.fail(result.error);
}
if (!typeId) {
throw new Error("Tipo de acceso SMB no construido.");
}
return Result.ok(typeId);
}
async project(input: Parameters<IResourceAccessWriter["project"]>[0]) {
const { companyId, resourceId: id } = input;
const integration = input.values.integration;
if (integration?.kind !== "access") {
throw new Error("Proyección de acceso incompatible.");
}
const roots = await this.database.query<{ id: string }>(
"SELECT id FROM access_workspaces WHERE companyId=:companyId",
{ replacements: { companyId }, transaction: this.transaction, type: QueryTypes.SELECT }
);
let workspaceId = roots[0]?.id;
if (!workspaceId) {
workspaceId = UniqueID.generateNewID().toPrimitive();
await this.database.query(
"INSERT INTO access_workspaces (id,companyId,scopeKey) VALUES (:id,:companyId,:companyId)",
{ replacements: { id: workspaceId, companyId }, transaction: this.transaction }
);
}
const existing = await this.database.query<{ workspaceId: string; kind: string }>(
"SELECT workspaceId,kind FROM access_items WHERE id=:id FOR UPDATE",
{ replacements: { id }, transaction: this.transaction, type: QueryTypes.SELECT }
);
if (existing.some((row) => row.workspaceId !== workspaceId || row.kind !== "resource")) {
throw new Error("Colisión de identidad en proyección de recurso.");
}
await writeAccessRow(
this.database,
{ id, workspaceId, kind: "resource", values: accessProjection(input.values) },
this.transaction,
existing.length ? "update" : "insert"
);
for (const [sequence, revision] of input.history.entries()) {
if (!(await projectionExists(this.database, revision.id, this.transaction))) {
await writeAccessRow(
this.database,
{
id: revision.id,
itemId: id,
sequence,
actorId: revision.actorId,
recordedAt: revision.recordedAt,
values: accessProjection(revision.values),
},
this.transaction,
"insert"
);
}
}
// El contexto es fijo; estas referencias preservan las FK utilizadas por permisos y necesidades.
for (const targetId of [
integration.resourceTypeId,
integration.systemId,
integration.environmentId,
].filter(Boolean)) {
const existing = await this.database.query<{ itemId: string }>(
"SELECT itemId FROM access_references WHERE itemId=:id AND targetId=:targetId",
{ replacements: { id, targetId }, transaction: this.transaction, type: QueryTypes.SELECT }
);
if (!existing.length) {
await this.database.query(
"INSERT INTO access_references (itemId,targetId) VALUES (:id,:targetId)",
{ replacements: { id, targetId }, transaction: this.transaction }
);
}
}
}
}
function accessProjection(input: ResourceInput) {
const integration = input.integration;
if (integration?.kind !== "access") {
throw new Error("Recurso sin contexto de acceso.");
}
return AccessDefinitionSchema.parse({
kind: "resource",
name: input.name,
active: input.active,
resourceTypeId: integration.resourceTypeId,
systemId: integration.systemId,
environmentId: integration.environmentId,
reference: input.characteristics[integration.referenceFieldId] || null,
notes: input.notes || null,
});
}
async function projectionExists(database: Sequelize, id: string, transaction: Transaction) {
return (
(
await database.query("SELECT id FROM access_revisions WHERE id=:id", {
replacements: { id },
transaction,
type: QueryTypes.SELECT,
})
).length > 0
);
}