metis-desk/modules/resources/src/api/infrastructure/persistence/prepare-resource-template-updates.ts
David d2bf856c97 feat(global-operating-systems): implement global operating systems management features
- Add softwareReferenceHref helper function for generating resource links.
- Create useGlobalResourceEditor hook for managing global application resources.
- Implement useGlobalResourceForm hook for handling global resource forms.
- Introduce GlobalSoftwareCatalogProvider for context management of global applications.
- Develop GlobalOperatingSystemsPage to render global operating systems using the existing GlobalApplicationsPage.
- Add operating system form identity helpers for managing OS-specific fields.
- Create operating system specification labels for better localization.
- Implement OperatingSystemIdentityReview component for reviewing OS identities.
- Add OperatingSystemProductFields component for displaying OS product details.
- Write tests for resource characteristic control to ensure proper translation and value handling.
- Create a Python script for auditing global operating systems UI interactions.
- Add tests for expanded workstation profiles to ensure proper functionality and responsiveness.
2026-10-25 20:39:06 +01:00

213 lines
7.0 KiB
TypeScript

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());
}