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 { IAssetRepository, InventoryContext } from "../../application/asset.repository"; import { AssetCatalogError } from "../../domain/asset-definition"; import type { Workstation } from "../../domain/workstation.aggregate"; 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"; import { RelationshipModel } from "./asset-relationship.model"; import { AssetRelationshipPersistenceMapper } from "./asset-relationship-persistence.mapper"; import { loadWorkstations } from "./load-workstations"; import { readBrands } from "./sequelize-brand.repository"; import { WorkstationAssetModel, WorkstationModel, WorkstationRevisionModel, } from "./workstation.model"; import { WorkstationPersistenceMapper } from "./workstation-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; }); const relationships = ( await RelationshipModel.findAll({ where: { companyId: companyId.toPrimitive() }, transaction, }) ).map((row) => new AssetRelationshipPersistenceMapper().toDomain(row)); for (const relationship of relationships) { const values = relationship.values; const definition = catalog .visible(Maybe.some(companyId)) .find((item) => item.id.equals(values.typeId)); if ( !( definition && definition.values.data.kind === "relation" && definition.values.data.symmetric === values.symmetric && [values.sourceId, values.targetId].every((id) => assets.some((asset) => asset.id.equals(id)) ) ) ) { throw new Error("Relación de activos fuera de su compañía."); } } const workstations = await loadWorkstations(companyId, transaction, assets, sites, people); return Result.ok({ workstations, brands: await readBrands(this.database, transaction, write), relationships, 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])), }); } saveWorkstation(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.workstations.find((item) => item.id.equals(changed.data.id)); if (previous !== changed.data) { await this.persistWorkstation(changed.data, previous, transaction); } return Result.ok({ workstation: changed.data, actorNames: loaded.data.actorNames }); }); } private async persistWorkstation( item: Workstation, previous: Workstation | undefined, transaction: Transaction ) { const rows = new WorkstationPersistenceMapper().toPersistence(item); await WorkstationModel.upsert(rows.workstation, { transaction }); const existing = new Set(previous?.history.map((item) => item.id.toPrimitive())); for (const revision of rows.revisions) { if (!existing.has(revision.id)) { await WorkstationRevisionModel.create(revision, { transaction }); } } for (const reference of rows.references) { await WorkstationAssetModel.upsert(reference, { transaction }); } } removeRelationship(company: UniqueID, id: UniqueID): Promise> { return this.database.transaction(async (transaction) => { await this.database.query("SELECT id FROM companies WHERE id=:companyId FOR UPDATE", { replacements: { companyId: company.toPrimitive() }, transaction, type: QueryTypes.SELECT, }); const row = await RelationshipModel.findOne({ where: { companyId: company.toPrimitive(), id: id.toPrimitive() }, transaction, lock: transaction.LOCK.UPDATE, }); if (!row) { return Result.fail(new AssetCatalogError("RELATIONSHIP_NOT_FOUND")); } await row.destroy({ transaction }); return Result.ok(undefined); }); } saveRelationship( 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.relationships.find((item) => item.id.equals(changed.data.id)); if (previous !== changed.data) { await RelationshipModel.upsert( new AssetRelationshipPersistenceMapper().toPersistence(changed.data), { transaction } ); } return Result.ok({ relationship: changed.data, actorNames: loaded.data.actorNames }); }); } 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 }); }); } }