metis-desk/modules/companies/src/api/infrastructure/persistence/sequelize-mail-configuration.repository.ts

132 lines
5.6 KiB
TypeScript
Raw Normal View History

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<Result<MailContext, MailConfigurationError>> {
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<MailRow>(
"SELECT * FROM mail_configurations WHERE companyId=:companyId",
{ replacements: { companyId }, transaction, type: QueryTypes.SELECT }
);
const revisions = await this.database.query<MailRevisionRow>(
"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<IMailConfigurationRepository["save"]>[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 });
});
}
}