- 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.
187 lines
6.5 KiB
TypeScript
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
|
|
);
|
|
}
|