import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import type { Sequelize, Transaction } from "sequelize"; import { QueryTypes } from "sequelize"; import type { IAssetRepository, InventoryContext } from "../../application/asset.repository"; import { AssetCatalogError } from "../../domain/asset-definition"; import { AssetModel, AssetRevisionModel } from "./asset.model"; import { AvailabilityModel, BindingModel, CatalogModel, DefinitionModel, } from "./asset-catalog.model"; import { AssetCatalogPersistenceMapper } from "./asset-catalog-persistence.mapper"; import { AssetPersistenceMapper, inventoryIdentity } from "./asset-persistence.mapper"; export class SequelizeAssetRepository implements IAssetRepository { private readonly mapper = new AssetPersistenceMapper(); constructor(private readonly database: Sequelize) {} private async load( companyId: UniqueID, transaction: Transaction, write: boolean ): Promise> { // Orden fijo de bloqueos: catálogo antes de compañía, compartido con las escrituras del catálogo. const root = await CatalogModel.findOne({ where: { catalogKey: "global" }, transaction, ...(write ? { lock: transaction.LOCK.UPDATE } : {}), }); if (!root) { throw new Error("Catálogo no inicializado."); } const replacements = { id: companyId.toPrimitive() }; const companies = await this.database.query<{ id: string }>( `SELECT id FROM companies WHERE id=:id${write ? " FOR UPDATE" : ""}`, { replacements, type: QueryTypes.SELECT, transaction } ); if (!companies.length) { return Result.fail(new AssetCatalogError("COMPANY_NOT_FOUND")); } const definitions = await DefinitionModel.findAll({ where: { catalogId: root.id }, transaction, }); const bindings = definitions.length ? await BindingModel.findAll({ where: { templateId: definitions.map((item) => item.id) }, transaction, }) : []; const overrides = await AvailabilityModel.findAll({ where: { catalogId: root.id }, transaction, }); const catalog = new AssetCatalogPersistenceMapper().toDomain( root, definitions, bindings, overrides ); const rows = await AssetModel.findAll({ where: { companyId: companyId.toPrimitive() }, transaction, }); const revisions = rows.length ? await AssetRevisionModel.findAll({ where: { assetId: rows.map((item) => item.id) }, transaction, }) : []; const sites = await this.database.query<{ id: string }>( "SELECT id FROM company_sites WHERE companyId=:id", { replacements, type: QueryTypes.SELECT, transaction } ); const people = await this.database.query<{ id: string; active: boolean }>( "SELECT id, active FROM company_contacts WHERE companyId=:id", { replacements, type: QueryTypes.SELECT, transaction } ); const actors = await this.database.query<{ id: string; name: string }>( "SELECT id, name FROM users", { type: QueryTypes.SELECT, transaction } ); const assets = rows.map((row) => { const asset = this.mapper.toDomain( row, revisions.filter((revision) => revision.assetId === row.id) ); const spec = asset.specification.toPrimitive(); const template = definitions.find((item) => item.id === spec.templateId); if ( !template || (template.companyId !== null && template.companyId !== companyId.toPrimitive()) ) { throw new Error("Plantilla de activo fuera de su compañía."); } for (const revision of asset.history) { const values = revision.values.toPrimitive(); if ( (values.siteId && !sites.some((site) => site.id === values.siteId)) || (values.personId && !people.some((person) => person.id === values.personId)) ) { throw new Error("Asignación de inventario fuera de su compañía."); } } return asset; }); return Result.ok({ catalog, assets, sites: sites.map((site) => inventoryIdentity(site.id)), people: people.map((person) => ({ id: inventoryIdentity(person.id), active: Boolean(person.active), })), 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 Result.fail(loaded.error); } const changed = action(loaded.data); if (!changed.isSuccess) { return Result.fail(changed.error); } const previous = loaded.data.assets.find((asset) => asset.id.equals(changed.data.id)); if (changed.data !== previous) { const rows = this.mapper.toPersistence(changed.data); await AssetModel.upsert(rows.asset, { transaction }); const existing = new Set(previous?.history.map((revision) => revision.id.toPrimitive())); for (const revision of rows.revisions) { if (!existing.has(revision.id)) { await AssetRevisionModel.create(revision, { transaction }); } } } return Result.ok({ asset: changed.data, actorNames: loaded.data.actorNames }); }); } }