137 lines
4.9 KiB
TypeScript
137 lines
4.9 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 { IResourceRepository, InventoryContext } from "../../application/resource.repository";
|
||
|
|
import { ResourceCatalogError } from "../../domain/resource-definition";
|
||
|
|
|
||
|
|
import { ResourceModel, ResourceRevisionModel } from "./resource.model";
|
||
|
|
import {
|
||
|
|
ResourceAvailabilityModel,
|
||
|
|
ResourceBindingModel,
|
||
|
|
ResourceCatalogModel,
|
||
|
|
ResourceDefinitionModel,
|
||
|
|
} from "./resource-catalog.model";
|
||
|
|
import { ResourceCatalogPersistenceMapper } from "./resource-catalog-persistence.mapper";
|
||
|
|
import { ResourcePersistenceMapper } from "./resource-persistence.mapper";
|
||
|
|
|
||
|
|
export class SequelizeResourceRepository implements IResourceRepository {
|
||
|
|
private readonly mapper = new ResourcePersistenceMapper();
|
||
|
|
constructor(private readonly database: Sequelize) {}
|
||
|
|
private async load(
|
||
|
|
companyId: UniqueID,
|
||
|
|
transaction: Transaction,
|
||
|
|
write: boolean
|
||
|
|
): Promise<Result<InventoryContext, ResourceCatalogError>> {
|
||
|
|
// Orden compartido con las escrituras del catálogo para evitar interbloqueos.
|
||
|
|
const root = await ResourceCatalogModel.findOne({
|
||
|
|
where: { catalogKey: "global" },
|
||
|
|
transaction,
|
||
|
|
...(write ? { lock: transaction.LOCK.UPDATE } : {}),
|
||
|
|
});
|
||
|
|
if (!root) {
|
||
|
|
throw new Error("Catálogo de recursos no inicializado.");
|
||
|
|
}
|
||
|
|
const companies = await this.database.query<{ id: string }>(
|
||
|
|
`SELECT id FROM companies WHERE id=:id${write ? " FOR UPDATE" : ""}`,
|
||
|
|
{ replacements: { id: companyId.toPrimitive() }, type: QueryTypes.SELECT, transaction }
|
||
|
|
);
|
||
|
|
if (!companies.length) {
|
||
|
|
return Result.fail(new ResourceCatalogError("COMPANY_NOT_FOUND"));
|
||
|
|
}
|
||
|
|
const definitions = await ResourceDefinitionModel.findAll({
|
||
|
|
where: { catalogId: root.id },
|
||
|
|
transaction,
|
||
|
|
});
|
||
|
|
const bindings = definitions.length
|
||
|
|
? await ResourceBindingModel.findAll({
|
||
|
|
where: { templateId: definitions.map((item) => item.id) },
|
||
|
|
transaction,
|
||
|
|
})
|
||
|
|
: [];
|
||
|
|
const overrides = await ResourceAvailabilityModel.findAll({
|
||
|
|
where: { catalogId: root.id },
|
||
|
|
transaction,
|
||
|
|
});
|
||
|
|
const catalog = new ResourceCatalogPersistenceMapper().toDomain(
|
||
|
|
root,
|
||
|
|
definitions,
|
||
|
|
bindings,
|
||
|
|
overrides
|
||
|
|
);
|
||
|
|
const rows = await ResourceModel.findAll({
|
||
|
|
where: { companyId: companyId.toPrimitive() },
|
||
|
|
transaction,
|
||
|
|
});
|
||
|
|
const revisions = rows.length
|
||
|
|
? await ResourceRevisionModel.findAll({
|
||
|
|
where: { resourceId: rows.map((item) => item.id) },
|
||
|
|
transaction,
|
||
|
|
})
|
||
|
|
: [];
|
||
|
|
const resources = rows.map((row) => {
|
||
|
|
const resource = this.mapper.toDomain(
|
||
|
|
row,
|
||
|
|
revisions.filter((revision) => revision.resourceId === row.id)
|
||
|
|
);
|
||
|
|
const spec = resource.specification.toPrimitive();
|
||
|
|
const template = catalog
|
||
|
|
.visible(Maybe.some(companyId))
|
||
|
|
.find((item) => item.id.toPrimitive() === spec.templateId);
|
||
|
|
if (
|
||
|
|
!template ||
|
||
|
|
template.values.data.kind !== "template" ||
|
||
|
|
spec.fields.some(
|
||
|
|
(field) =>
|
||
|
|
!catalog
|
||
|
|
.visible(Maybe.some(companyId))
|
||
|
|
.some(
|
||
|
|
(item) =>
|
||
|
|
item.id.toPrimitive() === field.fieldId && item.values.data.kind === "field"
|
||
|
|
)
|
||
|
|
)
|
||
|
|
) {
|
||
|
|
throw new Error("Especificación de recurso fuera de su catálogo o compañía.");
|
||
|
|
}
|
||
|
|
return resource;
|
||
|
|
});
|
||
|
|
const actors = await this.database.query<{ id: string; name: string }>(
|
||
|
|
"SELECT id, name FROM users",
|
||
|
|
{ type: QueryTypes.SELECT, transaction }
|
||
|
|
);
|
||
|
|
return Result.ok({
|
||
|
|
catalog,
|
||
|
|
resources,
|
||
|
|
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<IResourceRepository["save"]>[1]) {
|
||
|
|
return this.database.transaction(async (transaction) => {
|
||
|
|
const loaded = await this.load(companyId, transaction, true);
|
||
|
|
if (!loaded.isSuccess) {
|
||
|
|
return loaded;
|
||
|
|
}
|
||
|
|
const changed = action(loaded.data);
|
||
|
|
if (!changed.isSuccess) {
|
||
|
|
return changed;
|
||
|
|
}
|
||
|
|
const persistence = this.mapper.toPersistence(changed.data);
|
||
|
|
await ResourceModel.upsert(persistence.resource, { transaction });
|
||
|
|
const existing = await ResourceRevisionModel.findAll({
|
||
|
|
where: { resourceId: persistence.resource.id },
|
||
|
|
transaction,
|
||
|
|
});
|
||
|
|
for (const revision of persistence.revisions) {
|
||
|
|
if (!existing.some((item) => item.id === revision.id)) {
|
||
|
|
await ResourceRevisionModel.create(revision, { transaction });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return Result.ok({ resource: changed.data, actorNames: loaded.data.actorNames });
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|