import type { UniqueID } from "@repo/rdx-ddd"; import { Result, synchronizeTemplateStructure } from "@repo/rdx-utils"; import type { Sequelize, Transaction } from "sequelize"; import { QueryTypes } from "sequelize"; import { captureGlobalResourceSpecification } from "../../application/global-resource-specification"; import { validateOperatingSystemIdentity } from "../../application/operating-system-identity-policy"; import { captureResourceSpecification } from "../../application/resource-input.mapper"; import type { Resource } from "../../domain/resource.aggregate"; import type { ResourceCatalog } from "../../domain/resource-catalog.aggregate"; import { ResourceCatalogError } from "../../domain/resource-definition"; import { ResourceSpecification } from "../../domain/value-objects/resource-values.vo"; import { readInventory, readInventoryByIds } from "./resource-inventory-relational"; import { ResourcePersistenceMapper, inventoryIdentity } from "./resource-persistence.mapper"; import { ResourceScopeSchemaReader } from "./resource-scope-schema"; /** Prepara todas las compañías antes de escribir: un fallo de dominio no deja el catálogo a medio propagar. */ export async function prepareResourceTemplateUpdates( database: Sequelize, previous: ResourceCatalog, next: ResourceCatalog, actorId: UniqueID | undefined, transaction: Transaction ) { const updates: { previous: Resource; next: Resource }[] = []; if (!actorId) { return Result.ok(updates); } const templateIds = changedTemplateIds(previous, next); if (!templateIds.length) { return Result.ok(updates); } const mode = await new ResourceScopeSchemaReader(database).read(); const companies = await database.query<{ companyId: string | null }>( "SELECT DISTINCT companyId FROM resources WHERE templateId IN (:templateIds) ORDER BY companyId", { replacements: { templateIds }, type: QueryTypes.SELECT, transaction } ); const now = new Date().toISOString(); const mapper = new ResourcePersistenceMapper(); const owners = companies.map(({ companyId }) => companyId === null ? null : inventoryIdentity(companyId) ); const readResource = mode === "scoped" ? mapper.toScopedDomain.bind(mapper) : mapper.toDomain.bind(mapper); for (const owner of owners.filter((entry) => entry !== null)) { await database.query("SELECT id FROM companies WHERE id=:id FOR UPDATE", { replacements: { id: owner.toPrimitive() }, type: QueryTypes.SELECT, transaction, }); const loaded = await readInventory(database, owner.toPrimitive(), transaction); const resources = loaded.rows .filter((row) => templateIds.includes(row.templateId)) .map((row) => readResource( row, loaded.revisions.filter((revision) => revision.resourceId === row.id) ) ); if (resources.some((item) => !item.belongsToCompany(owner))) { throw new ResourceCatalogError("INVALID_RESOURCE_SCOPE"); } const prepared = prepareTemplateUpdates(resources, next, actorId, now); if (!prepared.isSuccess) { return prepared; } updates.push(...prepared.data); } if (owners.includes(null)) { const identities = await database.query<{ id: string }>( "SELECT id FROM resources WHERE companyId IS NULL AND templateId IN (:templateIds) ORDER BY id", { replacements: { templateIds }, transaction, type: QueryTypes.SELECT } ); const loaded = await readInventoryByIds( database, identities.map((row) => row.id), transaction ); const resources = loaded.rows.map((row) => mapper.toScopedDomain( row, loaded.revisions.filter((revision) => revision.resourceId === row.id) ) ); const prepared = prepareTemplateUpdates(resources, next, actorId, now); if (!prepared.isSuccess) { return prepared; } updates.push(...prepared.data); } return Result.ok(updates); } function prepareTemplateUpdates( resources: readonly Resource[], next: ResourceCatalog, actor: UniqueID, now: string ) { const updates: { previous: Resource; next: Resource }[] = []; for (const item of resources) { const changed = synchronizeResourceTemplate(item, next, actor, now); if (!changed.isSuccess) { return Result.fail(changed.error); } const error = validateOperatingSystemIdentity(changed.data, next); if (error) { return Result.fail(new ResourceCatalogError(error)); } if (changed.data !== item) { updates.push({ previous: item, next: changed.data }); } } return Result.ok(updates); } function synchronizeResourceTemplate( item: Resource, next: ResourceCatalog, actorId: UniqueID, now: string ) { const captured = item.scope === "global" ? captureGlobalResourceSpecification(next, item.values.toPrimitive().templateId, { existing: true, kind: next.definitions .find((definition) => definition.id.equals(item.specification.templateId)) ?.stockKey.getOrUndefined() === "template:os" ? "os" : "application", }) : captureResourceSpecification( { templateId: item.values.toPrimitive().templateId }, item.requireCompanyId(), { catalog: next }, { existing: true } ); if (!captured.isSuccess) { return Result.fail(captured.error); } const integration = item.values.toPrimitive().integration; const protectedFieldIds = integration?.kind === "mailbox" ? [integration.addressFieldId, integration.contextFieldId] : integration?.kind === "access" ? [integration.referenceFieldId] : []; const structure = synchronizeTemplateStructure({ previous: item.specification.toPrimitive(), next: captured.data.toPrimitive(), values: item.values.toPrimitive().characteristics, protectedFieldIds, }); const specification = ResourceSpecification.create({ ...captured.data.toPrimitive(), ...structure, }); if (!specification.isSuccess) { return Result.fail(specification.error); } return item.scope === "global" ? item.synchronizeGlobalSpecification(specification.data, actorId, now) : item.synchronizeSpecification(specification.data, actorId, now); } function changedTemplateIds(previous: ResourceCatalog, next: ResourceCatalog) { return next.definitions .filter((definition) => { if (definition.values.data.kind !== "template") { return false; } const old = previous.definitions.find((item) => item.id.equals(definition.id)); return ( old && (!old.values.equals(definition.values) || definition.values.data.fields.some((binding) => { const field = next.definitions.find( (item) => item.id.toPrimitive() === binding.fieldId ); const oldField = previous.definitions.find( (item) => item.id.toPrimitive() === binding.fieldId ); return field && oldField && !field.values.equals(oldField.values); })) ); }) .map((item) => item.id.toPrimitive()); }