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 { z } from "zod/v4"; import { ResourceReferenceSchema } from "../../../common/inventory"; 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 { projectResource } from "./resource-compatibility"; 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> { // 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 } ); const rawReferences = await this.database.query<{ id: string; values: unknown }>( "SELECT i.id,i.values FROM access_items i JOIN access_workspaces w ON w.id=i.workspaceId WHERE (w.companyId=:companyId OR w.companyId IS NULL) AND i.kind IN ('account','system','environment','resource_type')", { replacements: { companyId: companyId.toPrimitive() }, transaction, type: QueryTypes.SELECT } ); const references = rawReferences.map((row) => { const data = z .object({ kind: z.enum(["account", "system", "environment", "resource_type"]), name: z.string().optional(), identifier: z.string().optional(), active: z.boolean(), systemId: z.uuid().optional(), }) .parse(typeof row.values === "string" ? JSON.parse(row.values) : row.values); return ResourceReferenceSchema.parse({ id: row.id, kind: data.kind, name: data.name ?? data.identifier ?? "", active: data.active, systemId: data.systemId ?? null, }); }); return Result.ok({ references, 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[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 projectResource(this.database, changed.data, transaction); 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 }); }); } }