- 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.
205 lines
8.3 KiB
TypeScript
205 lines
8.3 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 {
|
|
IMailConfigurationRepository,
|
|
IMailInventoryService,
|
|
MailContext,
|
|
} from "../../application/mail-configuration.repository";
|
|
import type { MailboxContext } from "../../application/mailbox.repository";
|
|
import { MailConfigurationError } from "../../domain/mail-configuration-values";
|
|
import type { MailConfigurationValues } from "../../domain/value-objects/mail-configuration-values";
|
|
|
|
import {
|
|
MailConfigurationPersistenceMapper,
|
|
type MailRevisionRow,
|
|
type MailRow,
|
|
mailIdentity,
|
|
} from "./mail-configuration-persistence.mapper";
|
|
import type { SequelizeMailboxRepository } from "./sequelize-mailbox.repository";
|
|
import {
|
|
type ProfileRevisionRow,
|
|
type ProfileRow,
|
|
WorkProfilePersistenceMapper,
|
|
} from "./work-profile-persistence.mapper";
|
|
|
|
function validateLink(
|
|
values: MailConfigurationValues,
|
|
legacy: MailConfigurationValues["legacy"],
|
|
context: MailContext
|
|
) {
|
|
if (!legacy) {
|
|
return;
|
|
}
|
|
const profile = context.profiles.find((item) =>
|
|
item.id.equals(values.profileId.getOrUndefined())
|
|
);
|
|
if (
|
|
!profile ||
|
|
profile.values.personId.toPrimitive() !== legacy.personId ||
|
|
profile.values.assetId.toPrimitive() !== legacy.assetId ||
|
|
values.address.toPrimitive() !== legacy.address.toLowerCase()
|
|
) {
|
|
throw new Error("Vinculación histórica de correo inconsistente.");
|
|
}
|
|
}
|
|
function checkScope(
|
|
values: MailConfigurationValues,
|
|
context: MailContext,
|
|
accounts: MailboxContext["items"]
|
|
) {
|
|
const legacy = values.legacy;
|
|
if (legacy) {
|
|
return (
|
|
context.people.some((item) => item.id.toPrimitive() === legacy.personId) &&
|
|
context.assets.some((item) => item.id.toPrimitive() === legacy.assetId) &&
|
|
accounts.some(
|
|
(item) => item.id.toPrimitive() === legacy.accountId && item.values.kind === "account"
|
|
)
|
|
);
|
|
}
|
|
return (
|
|
context.profiles.some((item) => item.id.equals(values.profileId.getOrUndefined())) &&
|
|
context.mailboxes.some((item) => item.id.equals(values.mailboxId.getOrUndefined()))
|
|
);
|
|
}
|
|
function validateReferences(context: MailContext, accounts: MailboxContext["items"]) {
|
|
for (const profile of context.profiles) {
|
|
if (
|
|
!(
|
|
context.people.some((item) => item.id.equals(profile.values.personId)) &&
|
|
context.assets.some((item) => item.id.equals(profile.values.assetId))
|
|
)
|
|
) {
|
|
throw new Error("Pertenencia de perfil persistida inconsistente.");
|
|
}
|
|
}
|
|
for (const configuration of context.configurations) {
|
|
let previousLegacy: MailConfigurationValues["legacy"];
|
|
for (const revision of configuration.history) {
|
|
if (!revision.values.legacy) {
|
|
validateLink(revision.values, previousLegacy, context);
|
|
}
|
|
previousLegacy = revision.values.legacy;
|
|
if (!checkScope(revision.values, context, accounts)) {
|
|
throw new Error("Pertenencia de correo persistida inconsistente.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export class SequelizeMailConfigurationRepository implements IMailConfigurationRepository {
|
|
private readonly mapper = new MailConfigurationPersistenceMapper();
|
|
constructor(
|
|
private readonly database: Sequelize,
|
|
private readonly inventory: IMailInventoryService,
|
|
private readonly mailboxRepository: SequelizeMailboxRepository
|
|
) {}
|
|
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 mailboxContext = await this.mailboxRepository.load(company, transaction, false);
|
|
if (!mailboxContext.isSuccess) {
|
|
return Result.fail(new MailConfigurationError(mailboxContext.error.code));
|
|
}
|
|
const profiles = await this.database.query<ProfileRow>(
|
|
"SELECT * FROM work_profiles WHERE companyId=:companyId",
|
|
{ replacements: { companyId }, transaction, type: QueryTypes.SELECT }
|
|
);
|
|
const profileRevisions = await this.database.query<ProfileRevisionRow>(
|
|
"SELECT r.* FROM work_profile_revisions r JOIN work_profiles p ON p.id=r.profileId WHERE p.companyId=:companyId",
|
|
{ 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),
|
|
})),
|
|
mailboxes: mailboxContext.data.mailboxes,
|
|
uses: mailboxContext.data.uses,
|
|
profiles: profiles.map((row) =>
|
|
new WorkProfilePersistenceMapper().toDomain(
|
|
row,
|
|
profileRevisions.filter((revision) => revision.profileId === row.id)
|
|
)
|
|
),
|
|
assets: await this.inventory.list(company),
|
|
actors: new Map(actors.map((actor) => [actor.id, actor.name])),
|
|
today: new Date().toISOString().slice(0, 10),
|
|
};
|
|
|
|
validateReferences(context, mailboxContext.data.items);
|
|
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,profileId,mailboxId,`values`,currentKey) VALUES (:id,:companyId,:personId,:accountId,:assetId,:profileId,:mailboxId,:values,:currentKey) ON DUPLICATE KEY UPDATE `values`=VALUES(`values`),currentKey=VALUES(currentKey),personId=VALUES(personId),accountId=VALUES(accountId),assetId=VALUES(assetId),profileId=VALUES(profileId),mailboxId=VALUES(mailboxId)",
|
|
{ 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 });
|
|
});
|
|
}
|
|
}
|