import type { UniqueID } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; import type { Sequelize, Transaction } from "sequelize"; import { QueryTypes, UniqueConstraintError } from "sequelize"; import { z } from "zod/v4"; import { ResourceReferenceSchema } from "../../../common/inventory"; import type { IDirectoryContextWriter } from "../../application/directory-context-writer"; import type { IResourceRepository, InventoryContext } from "../../application/resource.repository"; import type { IResourceAccessWriter } from "../../application/resource-access-writer"; import { ResourceCatalogError } from "../../domain/resource-definition"; import { projectDirectoryDomain, readDirectoryDomains } from "./directory-domain-projection"; import { networkShareResponse, projectNetworkShare, readNetworkShares, } from "./network-share-projection"; import { prepareSmbAccess } from "./prepare-smb-access"; import { removeResource } from "./remove-resource"; import { ResourceAvailabilityModel, ResourceBindingModel, ResourceCatalogModel, } from "./resource-catalog.model"; import { ResourceCatalogPersistenceMapper } from "./resource-catalog-persistence.mapper"; import { readDefinitions } from "./resource-catalog-relational"; import { projectResource } from "./resource-compatibility"; import { readInventory, writeInventory } from "./resource-inventory-relational"; import { ResourcePersistenceMapper } from "./resource-persistence.mapper"; import { ResourceScopeSchemaReader } from "./resource-scope-schema"; export class SequelizeResourceRepository implements IResourceRepository { private readonly mapper = new ResourcePersistenceMapper(); private readonly schema: ResourceScopeSchemaReader; constructor( private readonly database: Sequelize, private readonly directoryContexts?: (transaction: Transaction) => IDirectoryContextWriter, private readonly accessWriters?: (transaction: Transaction) => IResourceAccessWriter, private readonly readTransaction?: Transaction ) { this.schema = new ResourceScopeSchemaReader(database); } remove(companyId: UniqueID, id: UniqueID) { return this.database.transaction(async (transaction) => { const loaded = await this.load(companyId, transaction, true); if (!loaded.isSuccess) { return Result.fail(loaded.error); } const resource = loaded.data.resources.find((item) => item.id.equals(id)); if (!resource) { return Result.fail(new ResourceCatalogError("RESOURCE_NOT_FOUND")); } return removeResource(this.database, resource, transaction); }); } private async load( companyId: UniqueID, transaction: Transaction, write: boolean ): Promise> { const mode = await this.schema.read(); // 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."); } if (write && (this.directoryContexts || this.accessWriters)) { // Orden: catálogo de recursos, catálogo de accesos, compañía y workspace de accesos. await this.database.query( "SELECT id FROM access_workspaces WHERE scopeKey='global' FOR UPDATE", { transaction, type: QueryTypes.SELECT } ); } 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 readDefinitions(this.database, 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, revisions } = await readInventory( this.database, companyId.toPrimitive(), transaction ); const resources = rows.map((row) => { const history = revisions.filter((revision) => revision.resourceId === row.id); const resource = mode === "scoped" ? this.mapper.toScopedDomain(row, history) : this.mapper.toDomain(row, history); if (!resource.belongsToCompany(companyId)) { throw new ResourceCatalogError("INVALID_RESOURCE_SCOPE"); } 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>( "SELECT i.id,v.kind,v.name,v.identifier,v.active,v.systemId FROM access_items i JOIN access_workspaces w ON w.id=i.workspaceId JOIN access_current_values v ON v.ownerId=i.id AND v.kind=i.kind 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().nullish(), identifier: z.string().nullish(), active: z.union([z.boolean(), z.literal(0), z.literal(1)]).transform(Boolean), systemId: z.uuid().nullish(), }) .parse(row); return ResourceReferenceSchema.parse({ id: row.id, kind: data.kind, name: data.name ?? data.identifier ?? "", active: data.active, systemId: data.systemId ?? null, }); }); const shareRows = await readNetworkShares(this.database, companyId.toPrimitive(), transaction); const networkShares = new Map( shareRows.map((row) => { const resource = resources.find((item) => item.id.toPrimitive() === row.resourceId); if (!resource) { throw new Error("Recurso SMB fuera de su compañía."); } return [row.resourceId, networkShareResponse(resource, catalog, row.normalizedAddress)]; }) ); return Result.ok({ networkShares, directoryDomains: await readDirectoryDomains( this.database, companyId.toPrimitive(), transaction ), references, catalog, resources, actorNames: new Map(actors.map((actor) => [actor.id, actor.name])), }); } private persistence( resource: Parameters[0], mode: "legacy" | "scoped" ) { return mode === "scoped" ? this.mapper.toScopedPersistence(resource) : this.mapper.toPersistence(resource); } get(companyId: UniqueID) { return this.readTransaction ? this.load(companyId, this.readTransaction, false) : this.database.transaction((transaction) => this.load(companyId, transaction, false)); } async save( companyId: UniqueID, action: Parameters[1], creation?: Parameters[2] ) { try { return await this.database.transaction(async (transaction) => { const loaded = await this.load(companyId, transaction, true); if (!loaded.isSuccess) { return loaded; } const accessWriter = this.accessWriters?.(transaction); const context = await prepareSmbAccess(loaded.data, companyId, creation, accessWriter); const changed = action(context); if (!changed.isSuccess) { throw changed.error; } if (!changed.data.belongsToCompany(companyId)) { throw new ResourceCatalogError("RESOURCE_NOT_FOUND"); } const mode = await this.schema.read(); const persistence = this.persistence(changed.data, mode); await writeInventory( this.database, persistence, new Set( loaded.data.resources .find((item) => item.id.equals(changed.data.id)) ?.history.map((revision) => revision.id.toPrimitive()) ?? [] ), transaction ); const directory = await projectDirectoryDomain( this.database, changed.data, loaded.data.catalog, loaded.data.directoryDomains?.get(changed.data.id.toPrimitive()), this.directoryContexts?.(transaction), transaction ); if (!directory.isSuccess) { // Lanzar dentro del callback gestionado garantiza rollback de Resource y Companies. throw directory.error; } const networkShare = await projectNetworkShare( this.database, changed.data, loaded.data.catalog, loaded.data.resources.find((item) => item.id.equals(changed.data.id)), loaded.data.networkShares?.has(changed.data.id.toPrimitive()) ?? false, transaction ); await projectResource(this.database, changed.data, transaction, accessWriter); return Result.ok({ ...(networkShare ? { networkShare } : {}), resource: changed.data, actorNames: loaded.data.actorNames, ...(directory.data ? { directory: directory.data } : {}), }); }); } catch (error) { if ( error instanceof UniqueConstraintError && error.errors.some( (item) => item.path === "network_share_active_address" || item.message.includes("network_share_active_address") ) ) { return Result.fail(new ResourceCatalogError("SMB_ADDRESS_EXISTS")); } if (error instanceof ResourceCatalogError) { return Result.fail(error); } throw error; } } }