import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import type { Sequelize, Transaction } from "sequelize"; import { QueryTypes } from "sequelize"; import type { FolderContext, IFolderMappingRepository, } from "../../application/folder-mapping.repository"; import { folderReferences } from "../../application/folder-mapping-policy"; import type { IWorkProfileInventoryService } from "../../application/work-profile.repository"; import { AccessDefinitionSchema } from "../../domain/access/access-definition"; import { AccessValues } from "../../domain/access/access-values.vo"; import { FolderMappingError } from "../../domain/folder-mapping-values"; import type { FolderRevisionRow, FolderRow } from "./folder-mapping-persistence.mapper"; import { FolderMappingPersistenceMapper } from "./folder-mapping-persistence.mapper"; import type { ProfileRevisionRow, ProfileRow } from "./work-profile-persistence.mapper"; import { WorkProfilePersistenceMapper, persistedJson, profileIdentity, } from "./work-profile-persistence.mapper"; export class SequelizeFolderMappingRepository implements IFolderMappingRepository { private readonly mapper = new FolderMappingPersistenceMapper(); constructor( private readonly database: Sequelize, private readonly inventory: IWorkProfileInventoryService ) {} private async load( company: UniqueID, transaction: Transaction, write: boolean ): Promise> { const replacements = { companyId: company.toPrimitive() }; const options = { replacements, transaction, type: QueryTypes.SELECT }; const companies = await this.database.query<{ id: string }>( `SELECT id FROM companies WHERE id=:companyId${write ? " FOR UPDATE" : ""}`, { ...options, type: QueryTypes.SELECT } ); if (!companies.length) { return Result.fail(new FolderMappingError("COMPANY_NOT_FOUND")); } const rows = await this.database.query( "SELECT * FROM folder_mappings WHERE companyId=:companyId", { ...options, type: QueryTypes.SELECT } ); const revisions = await this.database.query( "SELECT r.* FROM folder_mapping_revisions r JOIN folder_mappings m ON m.id=r.mappingId WHERE m.companyId=:companyId", { ...options, type: QueryTypes.SELECT } ); const profiles = await this.database.query( "SELECT * FROM work_profiles WHERE companyId=:companyId", { ...options, type: QueryTypes.SELECT } ); const profileRevisions = await this.database.query( "SELECT r.* FROM work_profile_revisions r JOIN work_profiles p ON p.id=r.profileId WHERE p.companyId=:companyId", { ...options, type: QueryTypes.SELECT } ); const items = 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", { ...options, type: QueryTypes.SELECT } ); const people = await this.database.query<{ id: string; active: boolean }>( "SELECT id,active FROM company_contacts WHERE companyId=:companyId", { ...options, type: QueryTypes.SELECT } ); const actors = await this.database.query<{ id: string; name: string }>( "SELECT id,name FROM users", { transaction, type: QueryTypes.SELECT } ); const context: FolderContext = { mappings: rows.map((row) => this.mapper.toDomain( row, revisions.filter((r) => r.mappingId === row.id) ) ), profiles: profiles.map((row) => new WorkProfilePersistenceMapper().toDomain( row, profileRevisions.filter((r) => r.profileId === row.id) ) ), items: items.map((item) => { const values = AccessValues.create( AccessDefinitionSchema.parse(persistedJson(item.values)) ); if (!values.isSuccess) { throw new Error("Recurso de carpeta persistido inconsistente."); } return { id: profileIdentity(item.id), values: values.data }; }), people: people.map((item) => ({ id: profileIdentity(item.id), active: Boolean(item.active), })), assets: await this.inventory.list(company), actors: new Map(actors.map((actor) => [actor.id, actor.name])), today: new Date().toISOString().slice(0, 10), }; if ( context.profiles.some( (profile) => !( context.people.some((person) => person.id.equals(profile.values.personId)) && context.assets.some((asset) => asset.id.equals(profile.values.assetId)) ) ) || context.mappings.some((mapping) => !folderReferences(mapping.values, context)) ) { throw new Error("Ámbito de carpeta persistido inconsistente."); } return Result.ok(context); } get(company: UniqueID) { return this.database.transaction((t) => this.load(company, t, false)); } remove(company: UniqueID, id: UniqueID): Promise> { return this.database.transaction(async (transaction) => { const replacements = { companyId: company.toPrimitive(), id: id.toPrimitive() }; await this.database.query("SELECT id FROM companies WHERE id=:companyId FOR UPDATE", { replacements, transaction, type: QueryTypes.SELECT, }); const rows = await this.database.query<{ id: string }>( "SELECT id FROM folder_mappings WHERE companyId=:companyId AND id=:id FOR UPDATE", { replacements, transaction, type: QueryTypes.SELECT } ); if (!rows.length) { return Result.fail(new FolderMappingError("FOLDER_MAPPING_NOT_FOUND")); } await this.database.query("DELETE FROM folder_mapping_revisions WHERE mappingId=:id", { replacements, transaction, }); await this.database.query( "DELETE FROM folder_mappings WHERE companyId=:companyId AND id=:id", { replacements, transaction } ); return Result.ok(undefined); }); } save(company: UniqueID, action: Parameters[1]) { return this.database.transaction(async (transaction) => { const loaded = await this.load(company, transaction, true); if (!loaded.isSuccess) { return loaded; } const candidate = action(loaded.data); if (!candidate.isSuccess) { return candidate; } const previous = loaded.data.mappings.find((item) => item.id.equals(candidate.data.id)); if (previous !== candidate.data) { const record = this.mapper.toPersistence(candidate.data); await this.database.query( "INSERT INTO folder_mappings (id,companyId,profileId,needId,path,driveLetter,state,notes,currentKey,driveKey) VALUES (:id,:companyId,:profileId,:needId,:path,:driveLetter,:state,:notes,:currentKey,:driveKey) ON DUPLICATE KEY UPDATE path=VALUES(path),driveLetter=VALUES(driveLetter),state=VALUES(state),notes=VALUES(notes),currentKey=VALUES(currentKey),driveKey=VALUES(driveKey)", { replacements: record.root, transaction } ); const stored = new Set(previous?.history.map((r) => r.id.toPrimitive())); for (const revision of record.revisions.filter((r) => !stored.has(r.id))) { await this.database.query( "INSERT INTO folder_mapping_revisions (id,mappingId,position,actorId,recordedAt,profileId,needId,path,driveLetter,state,notes) VALUES (:id,:mappingId,:position,:actorId,:recordedAt,:profileId,:needId,:path,:driveLetter,:state,:notes)", { replacements: revision, transaction } ); } } return Result.ok({ mapping: candidate.data, context: loaded.data }); }); } }