metis-desk/modules/companies/src/api/infrastructure/persistence/process-review-reader.ts
2026-10-10 17:57:05 +02:00

276 lines
9.5 KiB
TypeScript

import type { UniqueID } from "@repo/rdx-ddd";
import type { Sequelize, Transaction } from "sequelize";
import { QueryTypes } from "sequelize";
import type { IProcessAssetInventoryService } from "../../application/process.repository";
import type { ProcessReviewSources } from "../../application/process-review.builder";
import { buildProcessReview } from "../../application/process-review.builder";
import { accessReviewRelationships } from "../../application/process-review-access";
import { AccessDefinitionSchema } from "../../domain/access/access-definition";
import { ApplicationConfigurationInputSchema } from "../../domain/application-configuration-values";
import { CertificateConfigurationInputSchema } from "../../domain/certificate-configuration-values";
import { FolderMappingInputSchema } from "../../domain/folder-mapping-values";
import { MailSnapshotSchema } from "../../domain/mail-configuration-values";
import { MailboxUseInputSchema } from "../../domain/mailbox-values";
import { PrinterConfigurationInputSchema } from "../../domain/printer-configuration-values";
import { RemoteConnectionInputSchema } from "../../domain/remote-connection-values";
import { TeleworkAuthorizationInputSchema } from "../../domain/telework-authorization-values";
import type { WorkProfileInput } from "../../domain/work-profile-values";
import { WorkProfileInputSchema } from "../../domain/work-profile-values";
import { configurationSnapshots } from "./configuration-snapshots";
import { snapshotValues } from "./scalar-snapshot-reader";
import { profileIdentity } from "./work-profile-persistence.mapper";
function configurationDetail(
value: {
profileId: string;
path?: string;
location?: string | null;
version?: string | null;
resourceId?: string;
printerId?: string;
},
assetNames: ReadonlyMap<string, string>,
resourceNames: ReadonlyMap<string, string>
) {
const target = value.resourceId
? resourceNames.get(value.resourceId)
: value.printerId
? assetNames.get(value.printerId)
: undefined;
return [target, value.path ?? value.location ?? value.version].filter(Boolean).join(" · ");
}
type Profile = { id: string; values: WorkProfileInput };
import { readAccessItems } from "./access-relational";
type Row = { id: string; values: unknown };
type Table =
| "work_profiles"
| "mail_configurations"
| "folder_mappings"
| "printer_configurations"
| "application_configurations"
| "certificate_configurations"
| "remote_connections"
| "mailbox_uses"
| "telework_authorizations"
| "mailboxes";
function profileOf(profiles: readonly Profile[], id: string) {
const p = profiles.find((p) => p.id === id);
if (!p) {
throw new Error("Perfil de revisión inconsistente.");
}
return p;
}
export class ProcessReviewReader {
constructor(private readonly database: Sequelize) {}
private async rows(table: Table, companyId: string, transaction: Transaction) {
const rows = await this.database.query<Row>(
`SELECT * FROM ${table} WHERE companyId=:companyId`,
{ replacements: { companyId }, transaction, type: QueryTypes.SELECT }
);
const definition = configurationSnapshots.get(table);
return rows.map((row) => ({
...row,
values: definition ? snapshotValues(definition, row) : row.values,
}));
}
private mail(data: readonly Row[], profiles: readonly Profile[]) {
const configurations: ProcessReviewSources["configurations"][number][] = [];
for (const r of data) {
const v = MailSnapshotSchema.parse(r.values);
const personId =
"profileId" in v ? profileOf(profiles, v.profileId).values.personId : v.personId;
configurations.push({
id: profileIdentity(r.id).toPrimitive(),
personId,
kind: "mail",
label: v.address,
state: v.state,
});
}
return configurations;
}
private remote(
data: readonly Row[],
assetNames: ReadonlyMap<string, string>,
resourceNames: ReadonlyMap<string, string>
) {
const configurations: ProcessReviewSources["configurations"][number][] = [];
for (const r of data) {
const v = RemoteConnectionInputSchema.parse(r.values);
configurations.push({
id: profileIdentity(r.id).toPrimitive(),
personId: v.personId,
kind: "remote",
label: [
v.method,
v.sourceAssetId ? assetNames.get(v.sourceAssetId) : null,
v.targetKind === "asset"
? assetNames.get(v.targetId ?? "")
: v.targetKind === "resource"
? resourceNames.get(v.targetId ?? "")
: null,
]
.filter(Boolean)
.join(" · "),
sourceKind: v.sourceKind,
state: v.state,
});
}
return configurations;
}
private async profileConfigurations(
companyId: string,
transaction: Transaction,
profiles: readonly Profile[],
assetNames: ReadonlyMap<string, string>,
resourceNames: ReadonlyMap<string, string>
) {
const configurations: ProcessReviewSources["configurations"][number][] = [];
const tables = [
["folder_mappings", "folder", FolderMappingInputSchema],
["printer_configurations", "printer", PrinterConfigurationInputSchema],
["application_configurations", "application", ApplicationConfigurationInputSchema],
["certificate_configurations", "certificate", CertificateConfigurationInputSchema],
] as const;
for (const [table, kind, schema] of tables) {
for (const r of await this.rows(table, companyId, transaction)) {
const v = schema.parse(r.values);
const p = profileOf(profiles, v.profileId);
const detail = configurationDetail(v, assetNames, resourceNames);
configurations.push({
id: profileIdentity(r.id).toPrimitive(),
personId: p.values.personId,
kind,
label: detail ? `${p.values.name} · ${detail}` : p.values.name,
state: v.state,
});
}
}
return configurations;
}
async load(
companyId: string,
people: readonly { id: string; name: string }[],
transaction: Transaction,
assets: Awaited<ReturnType<IProcessAssetInventoryService["list"]>>,
resources: readonly { id: UniqueID; name: string }[]
) {
const assetNames = new Map(assets.map((a) => [a.id.toPrimitive(), a.name]));
const resourceNames = new Map(resources.map((r) => [r.id.toPrimitive(), r.name]));
const rows = (table: Table) => this.rows(table, companyId, transaction);
const profiles = (await rows("work_profiles")).map((r) => ({
id: profileIdentity(r.id).toPrimitive(),
values: WorkProfileInputSchema.parse(r.values),
}));
const configurations = [
...this.mail(await rows("mail_configurations"), profiles),
...(await this.profileConfigurations(
companyId,
transaction,
profiles,
assetNames,
resourceNames
)),
...this.remote(await rows("remote_connections"), assetNames, resourceNames),
];
const relationships: ProcessReviewSources["relationships"][number][] = [];
const workspaces = await this.database.query<{ id: string }>(
"SELECT id FROM access_workspaces WHERE companyId=:companyId",
{ replacements: { companyId }, transaction, type: QueryTypes.SELECT }
);
const rawAccess = (await readAccessItems(this.database, transaction)).filter((row) =>
workspaces.some((workspace) => workspace.id === row.workspaceId)
);
const access = rawAccess.map((r) => ({
id: profileIdentity(r.id).toPrimitive(),
values: AccessDefinitionSchema.parse(r.values),
}));
relationships.push(...accessReviewRelationships(access));
const mailboxes = await this.database.query<{ id: string; address: string }>(
"SELECT id,address FROM mailboxes WHERE companyId=:companyId",
{ replacements: { companyId }, transaction, type: QueryTypes.SELECT }
);
for (const r of await rows("mailbox_uses")) {
const v = MailboxUseInputSchema.parse(r.values);
relationships.push({
id: profileIdentity(r.id).toPrimitive(),
kind: "mailbox_use",
personId: v.personId,
label: mailboxes.find((m) => m.id === v.mailboxId)?.address ?? v.mailboxId,
endDate: v.endDate,
configuration: null,
});
}
const authorizations = (await rows("telework_authorizations")).map((r) => ({
id: profileIdentity(r.id).toPrimitive(),
...TeleworkAuthorizationInputSchema.parse(r.values),
}));
const sources = {
assets: assets.map((a) => ({ ...a, id: a.id.toPrimitive() })),
profiles: profiles.map((p) => ({ id: p.id, ...p.values })),
configurations,
relationships,
authorizations,
};
const knownPeople = new Set(people.map((p) => p.id));
const relatedPeople = [
...sources.profiles,
...sources.configurations,
...sources.relationships,
...sources.authorizations,
...sources.assets,
];
if (relatedPeople.some((item) => item.personId && !knownPeople.has(item.personId))) {
throw new Error("Ámbito de revisión inconsistente.");
}
const result = new Map(
people.map((person) => [
person.id,
buildProcessReview(person.id, sources, new Date().toISOString().slice(0, 10)).map((ref) =>
ref.kind === "telework" ? { ...ref, label: person.name } : ref
),
])
);
for (const refs of result.values()) {
if (new Set(refs.map((r) => `${r.kind}:${r.id}`)).size !== refs.length) {
throw new Error("Referencias de revisión duplicadas.");
}
}
return result;
}
}