import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { QueryTypes, type Sequelize, type Transaction } from "sequelize"; import type { IMailConfigurationRepository, IMailInventoryService, MailContext, } from "../../application/mail-configuration.repository"; import { AccessDefinitionSchema } from "../../domain/access/access-definition"; import { MailConfigurationError } from "../../domain/mail-configuration-values"; import { MailConfigurationPersistenceMapper, type MailRevisionRow, type MailRow, mailIdentity, persistedJson, } from "./mail-configuration-persistence.mapper"; export class SequelizeMailConfigurationRepository implements IMailConfigurationRepository { private readonly mapper = new MailConfigurationPersistenceMapper(); constructor( private readonly database: Sequelize, private readonly inventory: IMailInventoryService ) {} private async load( company: UniqueID, transaction: Transaction, write: boolean ): Promise> { const companyId = company.toPrimitive(); // El bloqueo de compañía precede a las lecturas: los escritores de personas, cuentas y activos usan el mismo bloqueo. const companies = await this.database.query<{ id: string }>( `SELECT id FROM companies WHERE id=:companyId${write ? " FOR UPDATE" : ""}`, { replacements: { companyId }, transaction, type: QueryTypes.SELECT } ); if (!companies.length) { return Result.fail(new MailConfigurationError("COMPANY_NOT_FOUND")); } const rows = await this.database.query( "SELECT * FROM mail_configurations WHERE companyId=:companyId", { replacements: { companyId }, transaction, type: QueryTypes.SELECT } ); const revisions = await this.database.query( "SELECT r.* FROM mail_configuration_revisions r JOIN mail_configurations c ON c.id=r.configurationId WHERE c.companyId=:companyId", { replacements: { companyId }, transaction, type: QueryTypes.SELECT } ); const people = await this.database.query<{ id: string; active: boolean }>( "SELECT id,active FROM company_contacts WHERE companyId=:companyId", { replacements: { companyId }, transaction, 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", { replacements: { companyId }, transaction, type: QueryTypes.SELECT } ); const actors = await this.database.query<{ id: string; name: string }>( "SELECT id,name FROM users", { transaction, type: QueryTypes.SELECT } ); const context: MailContext = { configurations: rows.map((row) => this.mapper.toDomain( row, revisions.filter((revision) => revision.configurationId === row.id) ) ), people: people.map((person) => ({ id: mailIdentity(person.id), active: Boolean(person.active), })), items: items.map((item) => ({ id: mailIdentity(item.id), values: AccessDefinitionSchema.parse(persistedJson(item.values)), })), 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.configurations.some( (item) => !( context.people.some((person) => person.id.equals(item.values.personId)) && context.items.some( (account) => account.id.equals(item.values.accountId) && account.values.kind === "account" ) && context.assets.some((asset) => asset.id.equals(item.values.assetId)) ) ) ) { throw new Error("Pertenencia de correo persistida inconsistente."); } return Result.ok(context); } 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 context = await this.load(companyId, transaction, true); if (!context.isSuccess) { return context; } const candidate = action(context.data); if (!candidate.isSuccess) { return candidate; } const previous = context.data.configurations.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 mail_configurations (id,companyId,personId,accountId,assetId,`values`,currentKey) VALUES (:id,:companyId,:personId,:accountId,:assetId,:values,:currentKey) ON DUPLICATE KEY UPDATE `values`=VALUES(`values`),currentKey=VALUES(currentKey)", { replacements: record.root, transaction } ); const stored = new Set( previous?.history.map((revision) => revision.id.toPrimitive()) ?? [] ); for (const revision of record.revisions.filter((item) => !stored.has(item.id))) { await this.database.query( "INSERT INTO mail_configuration_revisions (id,configurationId,position,actorId,recordedAt,`values`) VALUES (:id,:configurationId,:position,:actorId,:recordedAt,:values)", { replacements: revision, transaction } ); } } return Result.ok({ configuration: candidate.data, context: context.data }); }); } }