metis-desk/modules/assets/src/api/infrastructure/persistence/sequelize-asset.repository.ts

148 lines
5.5 KiB
TypeScript
Raw Normal View History

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<Result<InventoryContext, AssetCatalogError>> {
// 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<IAssetRepository["save"]>[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 });
});
}
}