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[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[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 ); }