- 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.
263 lines
10 KiB
TypeScript
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 });
|
|
});
|
|
}
|
|
}
|