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 type { IResourceRepository, InventoryContext } from "../../application/resource.repository"; import { ResourceCatalogError } from "../../domain/resource-definition"; import { ResourceModel, ResourceRevisionModel } from "./resource.model"; import { ResourceAvailabilityModel, ResourceBindingModel, ResourceCatalogModel, ResourceDefinitionModel, } from "./resource-catalog.model"; import { ResourceCatalogPersistenceMapper } from "./resource-catalog-persistence.mapper"; import { ResourcePersistenceMapper } from "./resource-persistence.mapper"; export class SequelizeResourceRepository implements IResourceRepository { private readonly mapper = new ResourcePersistenceMapper(); constructor(private readonly database: Sequelize) {} 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."); } 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 ResourceDefinitionModel.findAll({ where: { catalogId: 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 = await ResourceModel.findAll({ where: { companyId: companyId.toPrimitive() }, transaction, }); const revisions = rows.length ? await ResourceRevisionModel.findAll({ where: { resourceId: rows.map((item) => item.id) }, 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 } ); return Result.ok({ 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)); } save(companyId: UniqueID, action: Parameters[1]) { return 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 ResourceModel.upsert(persistence.resource, { transaction }); const existing = await ResourceRevisionModel.findAll({ where: { resourceId: persistence.resource.id }, transaction, }); for (const revision of persistence.revisions) { if (!existing.some((item) => item.id === revision.id)) { await ResourceRevisionModel.create(revision, { transaction }); } } return Result.ok({ resource: changed.data, actorNames: loaded.data.actorNames }); }); } }