- 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.
213 lines
7.0 KiB
TypeScript
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());
|
|
}
|