import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { QueryTypes, type Sequelize, type Transaction } from "sequelize"; import type { IMailboxRepository, MailboxContext } from "../../application/mailbox.repository"; import { AccessDefinitionSchema } from "../../domain/access/access-definition"; import { Mailbox } from "../../domain/mailbox.aggregate"; import type { MailboxUse } from "../../domain/mailbox-use.aggregate"; import { MailboxError } from "../../domain/mailbox-values"; import { MailboxPersistenceMapper, type MailboxRevisionRow, type MailboxRow, type MailboxUseRow, mailboxIdentity, } from "./mailbox-persistence.mapper"; export class SequelizeMailboxRepository implements IMailboxRepository { private readonly mapper = new MailboxPersistenceMapper(); constructor(private readonly database: Sequelize) {} async load( companyId: UniqueID, transaction: Transaction, write: boolean ): Promise> { const replacements = { companyId: companyId.toPrimitive() }; const companies = await this.database.query<{ id: string }>( `SELECT id FROM companies WHERE id=:companyId${write ? " FOR UPDATE" : ""}`, { replacements, transaction, type: QueryTypes.SELECT } ); if (!companies.length) { return Result.fail(new MailboxError("COMPANY_NOT_FOUND")); } const rows = await this.database.query( "SELECT * FROM mailboxes WHERE companyId=:companyId", { replacements, transaction, type: QueryTypes.SELECT } ); const revisions = await this.database.query( "SELECT r.* FROM mailbox_revisions r JOIN mailboxes m ON m.id=r.itemId WHERE m.companyId=:companyId", { replacements, transaction, type: QueryTypes.SELECT } ); const useRows = await this.database.query( "SELECT * FROM mailbox_uses WHERE companyId=:companyId", { replacements, transaction, type: QueryTypes.SELECT } ); const useRevisions = await this.database.query( "SELECT r.* FROM mailbox_use_revisions r JOIN mailbox_uses m ON m.id=r.itemId WHERE m.companyId=:companyId", { replacements, transaction, type: QueryTypes.SELECT } ); const people = await this.database.query<{ id: string; active: boolean }>( "SELECT id,active FROM company_contacts WHERE companyId=:companyId", { replacements, 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, 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: MailboxContext = { mailboxes: rows.map((row) => this.mapper.toDomain( row, revisions.filter((revision) => revision.itemId === row.id) ) ), uses: useRows.map((row) => this.mapper.toUseDomain( row, useRevisions.filter((revision) => revision.itemId === row.id) ) ), people: people.map((person) => ({ id: mailboxIdentity(person.id), active: Boolean(person.active), })), items: items.map((item) => ({ id: mailboxIdentity(item.id), values: AccessDefinitionSchema.parse( typeof item.values === "string" ? JSON.parse(item.values) : item.values ), })), actors: new Map(actors.map((actor) => [actor.id, actor.name])), today: new Date().toISOString().slice(0, 10), }; if ( context.mailboxes.some((mailbox) => mailbox.history.some((revision) => { const account = revision.values.accountId.getOrUndefined(); return ( account && !context.items.some((item) => item.id.equals(account) && item.values.kind === "account") ); }) ) || context.uses.some( (use) => !( context.mailboxes.some((item) => item.id.equals(use.values.mailboxId)) && context.people.some((item) => item.id.equals(use.values.personId)) ) ) ) { throw new Error("Ámbito de buzón persistido inconsistente."); } return Result.ok(context); } private async saveRevisions( item: Mailbox | MailboxUse, previous: Mailbox | MailboxUse | undefined, transaction: Transaction ) { const row = this.mapper.toPersistence(item); const existing = new Set(previous?.history.map((revision) => revision.id.toPrimitive())); for (const revision of row.revisions.filter((revision) => !existing.has(revision.id))) { const table = item instanceof Mailbox ? "mailbox_revisions" : "mailbox_use_revisions"; await this.database.query( `INSERT INTO ${table} (id,itemId,position,actorId,recordedAt,\`values\`) VALUES (:id,:itemId,:position,:actorId,:recordedAt,:values)`, { replacements: revision, transaction } ); } } 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 candidate = action(loaded.data); if (!candidate.isSuccess) { return candidate; } const item = candidate.data; const previous = item instanceof Mailbox ? loaded.data.mailboxes.find((old) => old.id.equals(item.id)) : loaded.data.uses.find((old) => old.id.equals(item.id)); if (previous !== item) { const row = this.mapper.toPersistence(item); if (item instanceof Mailbox) { const values = item.values.toPrimitive(); await this.database.query( "INSERT INTO mailboxes (id,companyId,accountId,addressKey,`values`) VALUES (:id,:companyId,:accountId,:addressKey,:values) ON DUPLICATE KEY UPDATE accountId=VALUES(accountId),addressKey=VALUES(addressKey),`values`=VALUES(`values`)", { replacements: { id: row.id, companyId: row.companyId, values: row.values, accountId: values.accountId, addressKey: `${row.companyId}:${values.address}`, }, transaction, } ); } else { const values = item.values.toPrimitive(); await this.database.query( "INSERT INTO mailbox_uses (id,companyId,mailboxId,personId,`values`) VALUES (:id,:companyId,:mailboxId,:personId,:values) ON DUPLICATE KEY UPDATE `values`=VALUES(`values`)", { replacements: { id: row.id, companyId: row.companyId, values: row.values, mailboxId: values.mailboxId, personId: values.personId, }, transaction, } ); } await this.saveRevisions(item, previous, transaction); } return Result.ok({ item, context: { ...loaded.data, mailboxes: item instanceof Mailbox ? [...loaded.data.mailboxes.filter((old) => !old.id.equals(item.id)), item] : loaded.data.mailboxes, uses: item instanceof Mailbox ? loaded.data.uses : [...loaded.data.uses.filter((old) => !old.id.equals(item.id)), item], }, }); }); } }