import type { UniqueID } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; import type { Sequelize, Transaction } from "sequelize"; import { QueryTypes } from "sequelize"; import { z } from "zod/v4"; import { ResourceReferenceSchema } from "../../../common/inventory"; import type { IDirectoryContextWriter } from "../../application/directory-context-writer"; import type { IResourceRepository, InventoryContext } from "../../application/resource.repository"; import { ResourceCatalogError } from "../../domain/resource-definition"; import { projectDirectoryDomain, readDirectoryDomains } from "./directory-domain-projection"; import { removeResource } from "./remove-resource"; import { ResourceAvailabilityModel, ResourceBindingModel, ResourceCatalogModel, } from "./resource-catalog.model"; import { ResourceCatalogPersistenceMapper } from "./resource-catalog-persistence.mapper"; import { readDefinitions } from "./resource-catalog-relational"; import { projectResource } from "./resource-compatibility"; import { readInventory, writeInventory } from "./resource-inventory-relational"; import { ResourcePersistenceMapper } from "./resource-persistence.mapper"; export class SequelizeResourceRepository implements IResourceRepository { private readonly mapper = new ResourcePersistenceMapper(); constructor( private readonly database: Sequelize, private readonly directoryContexts?: (transaction: Transaction) => IDirectoryContextWriter ) {} remove(companyId: UniqueID, id: UniqueID) { return this.database.transaction(async (transaction) => { const loaded = await this.load(companyId, transaction, true); if (!loaded.isSuccess) { return Result.fail(loaded.error); } const resource = loaded.data.resources.find((item) => item.id.equals(id)); if (!resource) { return Result.fail(new ResourceCatalogError("RESOURCE_NOT_FOUND")); } return removeResource(this.database, resource, transaction); }); } private async load( companyId: UniqueID, transaction: Transaction, write: boolean ): Promise> { // Orden compartido con las escrituras del catálogo para evitar interbloqueos. const root = await ResourceCatalogModel.findOne({ where: { catalogKey: "global" }, transaction, ...(write ? { lock: transaction.LOCK.UPDATE } : {}), }); if (!root) { throw new Error("Catálogo de recursos no inicializado."); } if (write && this.directoryContexts) { // Orden: catálogo de recursos, catálogo de accesos, compañía y workspace de accesos. await this.database.query( "SELECT id FROM access_workspaces WHERE scopeKey='global' FOR UPDATE", { transaction, type: QueryTypes.SELECT } ); } const companies = await this.database.query<{ id: string }>( `SELECT id FROM companies WHERE id=:id${write ? " FOR UPDATE" : ""}`, { replacements: { id: companyId.toPrimitive() }, type: QueryTypes.SELECT, transaction } ); if (!companies.length) { return Result.fail(new ResourceCatalogError("COMPANY_NOT_FOUND")); } const definitions = await readDefinitions(this.database, root.id, transaction); const bindings = definitions.length ? await ResourceBindingModel.findAll({ where: { templateId: definitions.map((item) => item.id) }, transaction, }) : []; const overrides = await ResourceAvailabilityModel.findAll({ where: { catalogId: root.id }, transaction, }); const catalog = new ResourceCatalogPersistenceMapper().toDomain( root, definitions, bindings, overrides ); const { rows, revisions } = await readInventory( this.database, companyId.toPrimitive(), transaction ); const resources = rows.map((row) => { const resource = this.mapper.toDomain( row, revisions.filter((revision) => revision.resourceId === row.id) ); const spec = resource.specification.toPrimitive(); const template = catalog .visible(Maybe.some(companyId)) .find((item) => item.id.toPrimitive() === spec.templateId); if ( !template || template.values.data.kind !== "template" || spec.fields.some( (field) => !catalog .visible(Maybe.some(companyId)) .some( (item) => item.id.toPrimitive() === field.fieldId && item.values.data.kind === "field" ) ) ) { throw new Error("Especificación de recurso fuera de su catálogo o compañía."); } return resource; }); const actors = await this.database.query<{ id: string; name: string }>( "SELECT id, name FROM users", { type: QueryTypes.SELECT, transaction } ); const rawReferences = await this.database.query>( "SELECT i.id,v.kind,v.name,v.identifier,v.active,v.systemId FROM access_items i JOIN access_workspaces w ON w.id=i.workspaceId JOIN access_current_values v ON v.ownerId=i.id AND v.kind=i.kind WHERE (w.companyId=:companyId OR w.companyId IS NULL) AND i.kind IN ('account','system','environment','resource_type')", { replacements: { companyId: companyId.toPrimitive() }, transaction, type: QueryTypes.SELECT } ); const references = rawReferences.map((row) => { const data = z .object({ kind: z.enum(["account", "system", "environment", "resource_type"]), name: z.string().nullish(), identifier: z.string().nullish(), active: z.union([z.boolean(), z.literal(0), z.literal(1)]).transform(Boolean), systemId: z.uuid().nullish(), }) .parse(row); return ResourceReferenceSchema.parse({ id: row.id, kind: data.kind, name: data.name ?? data.identifier ?? "", active: data.active, systemId: data.systemId ?? null, }); }); return Result.ok({ directoryDomains: await readDirectoryDomains( this.database, companyId.toPrimitive(), transaction ), references, catalog, resources, actorNames: new Map(actors.map((actor) => [actor.id, actor.name])), }); } get(companyId: UniqueID) { return this.database.transaction((transaction) => this.load(companyId, transaction, false)); } async save(companyId: UniqueID, action: Parameters[1]) { try { return await this.database.transaction(async (transaction) => { const loaded = await this.load(companyId, transaction, true); if (!loaded.isSuccess) { return loaded; } const changed = action(loaded.data); if (!changed.isSuccess) { return changed; } const persistence = this.mapper.toPersistence(changed.data); await projectResource(this.database, changed.data, transaction); await writeInventory( this.database, persistence, new Set( loaded.data.resources .find((item) => item.id.equals(changed.data.id)) ?.history.map((revision) => revision.id.toPrimitive()) ?? [] ), transaction ); const directory = await projectDirectoryDomain( this.database, changed.data, loaded.data.catalog, loaded.data.directoryDomains?.get(changed.data.id.toPrimitive()), this.directoryContexts?.(transaction), transaction ); if (!directory.isSuccess) { // Lanzar dentro del callback gestionado garantiza rollback de Resource y Companies. throw directory.error; } return Result.ok({ resource: changed.data, actorNames: loaded.data.actorNames, ...(directory.data ? { directory: directory.data } : {}), }); }); } catch (error) { if (error instanceof ResourceCatalogError) { return Result.fail(error); } throw error; } } }