2026-10-05 15:01:32 +00:00
|
|
|
import { UniqueID } from "@repo/rdx-ddd";
|
|
|
|
|
import type { Sequelize, Transaction } from "sequelize";
|
|
|
|
|
import { QueryTypes } from "sequelize";
|
2026-10-10 14:40:51 +00:00
|
|
|
import { z } from "zod/v4";
|
2026-10-05 15:01:32 +00:00
|
|
|
|
|
|
|
|
import type { Resource } from "../../domain/resource.aggregate";
|
|
|
|
|
import type { ResourceInput } from "../../domain/resource-values";
|
|
|
|
|
|
2026-10-10 14:40:51 +00:00
|
|
|
import { writeResourceRow } from "./resource-relational-sql";
|
|
|
|
|
|
2026-10-05 15:01:32 +00:00
|
|
|
/** Proyecciones para los contratos de accesos y perfiles: la ficha editable pertenece a resources. */
|
|
|
|
|
export async function projectResource(
|
|
|
|
|
database: Sequelize,
|
|
|
|
|
resource: Resource,
|
|
|
|
|
transaction: Transaction
|
|
|
|
|
) {
|
|
|
|
|
const current = resource.values.toPrimitive();
|
|
|
|
|
const integration = current.integration;
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 15:01:32 +00:00
|
|
|
if (!integration) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const companyId = resource.companyId.toPrimitive();
|
|
|
|
|
const id = resource.id.toPrimitive();
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 15:01:32 +00:00
|
|
|
if (integration.kind === "mailbox") {
|
|
|
|
|
const values = mailboxProjection(current);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-10 14:40:51 +00:00
|
|
|
const columns = scalarProjection(values);
|
|
|
|
|
|
|
|
|
|
await writeResourceRow(
|
|
|
|
|
database,
|
|
|
|
|
"mailboxes",
|
|
|
|
|
{ id, companyId, addressKey: `${companyId}:${values.address}`, ...columns },
|
|
|
|
|
transaction
|
2026-10-05 15:01:32 +00:00
|
|
|
);
|
|
|
|
|
for (const [position, revision] of resource.history.entries()) {
|
2026-10-10 14:40:51 +00:00
|
|
|
const revisionId = revision.id.toPrimitive();
|
|
|
|
|
|
|
|
|
|
if (!(await projectionExists(database, "mailbox_revisions", revisionId, transaction))) {
|
|
|
|
|
await writeResourceRow(
|
|
|
|
|
database,
|
|
|
|
|
"mailbox_revisions",
|
|
|
|
|
{
|
|
|
|
|
id: revisionId,
|
2026-10-05 15:01:32 +00:00
|
|
|
itemId: id,
|
|
|
|
|
position,
|
|
|
|
|
actorId: revision.actorId.toPrimitive(),
|
|
|
|
|
recordedAt: revision.recordedAt,
|
2026-10-10 14:40:51 +00:00
|
|
|
...scalarProjection(mailboxProjection(revision.values.toPrimitive())),
|
2026-10-05 15:01:32 +00:00
|
|
|
},
|
2026-10-10 14:40:51 +00:00
|
|
|
transaction
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-10-05 15:01:32 +00:00
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 15:01:32 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const roots = await database.query<{ id: string }>(
|
|
|
|
|
"SELECT id FROM access_workspaces WHERE companyId=:companyId",
|
|
|
|
|
{ replacements: { companyId }, transaction, type: QueryTypes.SELECT }
|
|
|
|
|
);
|
|
|
|
|
let workspaceId = roots[0]?.id;
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 15:01:32 +00:00
|
|
|
if (!workspaceId) {
|
|
|
|
|
workspaceId = UniqueID.generateNewID().toPrimitive();
|
|
|
|
|
await database.query(
|
|
|
|
|
"INSERT INTO access_workspaces (id,companyId,scopeKey) VALUES (:id,:companyId,:companyId)",
|
|
|
|
|
{ replacements: { id: workspaceId, companyId }, transaction }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const values = accessProjection(current);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-10 14:40:51 +00:00
|
|
|
await writeResourceRow(
|
|
|
|
|
database,
|
|
|
|
|
"access_items",
|
|
|
|
|
{ id, workspaceId, kind: "resource" },
|
|
|
|
|
transaction
|
|
|
|
|
);
|
|
|
|
|
const { kind: _kind, ...columns } = scalarProjection(values);
|
|
|
|
|
|
|
|
|
|
await writeResourceRow(
|
|
|
|
|
database,
|
|
|
|
|
"access_resource_values",
|
|
|
|
|
{ ownerId: id, ...columns },
|
|
|
|
|
transaction
|
2026-10-05 15:01:32 +00:00
|
|
|
);
|
|
|
|
|
for (const [sequence, revision] of resource.history.entries()) {
|
2026-10-10 14:40:51 +00:00
|
|
|
const revisionId = revision.id.toPrimitive();
|
|
|
|
|
|
|
|
|
|
if (!(await projectionExists(database, "access_revisions", revisionId, transaction))) {
|
|
|
|
|
await writeResourceRow(
|
|
|
|
|
database,
|
|
|
|
|
"access_revisions",
|
|
|
|
|
{
|
|
|
|
|
id: revisionId,
|
2026-10-05 15:01:32 +00:00
|
|
|
itemId: id,
|
|
|
|
|
sequence,
|
|
|
|
|
actorId: revision.actorId.toPrimitive(),
|
|
|
|
|
recordedAt: revision.recordedAt,
|
|
|
|
|
},
|
2026-10-10 14:40:51 +00:00
|
|
|
transaction
|
|
|
|
|
);
|
|
|
|
|
const { kind: _revisionKind, ...frozen } = scalarProjection(
|
|
|
|
|
accessProjection(revision.values.toPrimitive())
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await writeResourceRow(
|
|
|
|
|
database,
|
|
|
|
|
"access_resource_revision_values",
|
|
|
|
|
{ ownerId: revisionId, ...frozen },
|
|
|
|
|
transaction
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-10-05 15:01:32 +00:00
|
|
|
}
|
|
|
|
|
// El contexto es fijo; estas referencias preservan las FK utilizadas por permisos y necesidades.
|
|
|
|
|
for (const targetId of [
|
|
|
|
|
integration.resourceTypeId,
|
|
|
|
|
integration.systemId,
|
|
|
|
|
integration.environmentId,
|
|
|
|
|
].filter(Boolean)) {
|
|
|
|
|
const existing = await database.query<{ itemId: string }>(
|
|
|
|
|
"SELECT itemId FROM access_references WHERE itemId=:id AND targetId=:targetId",
|
|
|
|
|
{ replacements: { id, targetId }, transaction, type: QueryTypes.SELECT }
|
|
|
|
|
);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 15:01:32 +00:00
|
|
|
if (!existing.length) {
|
|
|
|
|
await database.query(
|
|
|
|
|
"INSERT INTO access_references (itemId,targetId) VALUES (:id,:targetId)",
|
|
|
|
|
{ replacements: { id, targetId }, transaction }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 15:01:32 +00:00
|
|
|
function mailboxProjection(input: ResourceInput) {
|
|
|
|
|
const integration = input.integration;
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 15:01:32 +00:00
|
|
|
if (integration?.kind !== "mailbox") {
|
|
|
|
|
throw new Error("Recurso sin contexto de buzón.");
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 15:01:32 +00:00
|
|
|
return {
|
|
|
|
|
address: input.characteristics[integration.addressFieldId],
|
|
|
|
|
kind: integration.mailboxKind,
|
|
|
|
|
context: input.characteristics[integration.contextFieldId] || null,
|
|
|
|
|
accountId: integration.accountId,
|
|
|
|
|
active: input.active,
|
|
|
|
|
notes: input.notes || null,
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 15:01:32 +00:00
|
|
|
function accessProjection(input: ResourceInput) {
|
|
|
|
|
const integration = input.integration;
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 15:01:32 +00:00
|
|
|
if (integration?.kind !== "access") {
|
|
|
|
|
throw new Error("Recurso sin contexto de acceso.");
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 15:01:32 +00:00
|
|
|
return {
|
|
|
|
|
kind: "resource",
|
|
|
|
|
name: input.name,
|
|
|
|
|
active: input.active,
|
|
|
|
|
resourceTypeId: integration.resourceTypeId,
|
|
|
|
|
systemId: integration.systemId,
|
|
|
|
|
environmentId: integration.environmentId,
|
|
|
|
|
reference: input.characteristics[integration.referenceFieldId] || null,
|
|
|
|
|
notes: input.notes || null,
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-10-10 14:40:51 +00:00
|
|
|
|
|
|
|
|
function scalarProjection(values: unknown) {
|
|
|
|
|
return z
|
|
|
|
|
.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()]))
|
|
|
|
|
.parse(values);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function projectionExists(
|
|
|
|
|
database: Sequelize,
|
|
|
|
|
table: "mailbox_revisions" | "access_revisions",
|
|
|
|
|
id: string,
|
|
|
|
|
transaction: Transaction
|
|
|
|
|
) {
|
|
|
|
|
const rows = await database.query(`SELECT id FROM ${table} WHERE id=:id`, {
|
|
|
|
|
replacements: { id },
|
|
|
|
|
transaction,
|
|
|
|
|
type: QueryTypes.SELECT,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return rows.length > 0;
|
|
|
|
|
}
|