metis-desk/modules/companies/src/api/infrastructure/persistence/sequelize-mailbox.repository.ts
David 6375fa1b32 feat(mailbox): implement mailbox and mailbox use management features
- Add mailbox persistence mapper for domain object mapping.
- Create Sequelize repository for mailbox data access.
- Define common mailbox schemas and types for validation.
- Implement mailbox API for frontend communication.
- Develop mailbox form component for creating and editing mailboxes.
- Add internationalization support for mailbox labels and messages.
- Create mailbox use form for managing mailbox usage periods.
- Build mailboxes page to display and manage mailboxes and their usage.
2026-10-05 10:45:02 +02:00

196 lines
7.8 KiB
TypeScript

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<Result<MailboxContext, MailboxError>> {
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<MailboxRow>(
"SELECT * FROM mailboxes WHERE companyId=:companyId",
{ replacements, transaction, type: QueryTypes.SELECT }
);
const revisions = await this.database.query<MailboxRevisionRow>(
"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<MailboxUseRow>(
"SELECT * FROM mailbox_uses WHERE companyId=:companyId",
{ replacements, transaction, type: QueryTypes.SELECT }
);
const useRevisions = await this.database.query<MailboxRevisionRow>(
"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<IMailboxRepository["save"]>[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],
},
});
});
}
}