metis-desk/modules/assets/src/api/infrastructure/persistence/sequelize-asset.repository.ts
David 6c6eee8f93 feat: implement record deletion functionality across various configurations
- Added remove methods to repositories for application, certificate, folder, mail, and printer configurations.
- Implemented use cases for removing configurations, including validation for company and identity.
- Created HTTP routes for deleting configurations with appropriate request handling and error responses.
- Introduced a DeleteRecordDialog component for user confirmation before deletion.
- Updated UI pages to include delete actions for configurations with modal confirmation.
- Added integration tests to verify record deletion functionality and ensure data integrity.
- Documented the record deletion process and its implications in the project documentation.
2026-10-10 00:19:35 +02:00

263 lines
10 KiB
TypeScript

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<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;
});
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<IAssetRepository["saveWorkstation"]>[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<Result<void, AssetCatalogError>> {
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<IAssetRepository["saveRelationship"]>[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<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 });
});
}
}