metis-desk/modules/resources/src/api/infrastructure/persistence/sequelize-global-resource.repository.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

442 lines
15 KiB
TypeScript

import type { UniqueID } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import type { Sequelize, Transaction } from "sequelize";
import { QueryTypes } from "sequelize";
import { z } from "zod/v4";
import type { ListGlobalApplicationHistoryRequestDTO } from "../../../common";
import {
GlobalResourceAdminSchema,
ListGlobalApplicationHistoryResponseSchema,
ResourceOwnershipSchema,
} from "../../../common";
import type { IGlobalResourceRepository } from "../../application/global-resource.repository";
import { captureGlobalResourceSpecification } from "../../application/global-resource-specification";
import {
readOperatingSystemIdentity,
validateOperatingSystemIdentity,
} from "../../application/operating-system-identity-policy";
import {
completeOperatingSystemIdentity,
sameOperatingSystemIdentity,
} from "../../domain/operating-system-identity";
import type { Resource } from "../../domain/resource.aggregate";
import type { ResourceCatalog } from "../../domain/resource-catalog.aggregate";
import { ResourceCatalogError } from "../../domain/resource-definition";
import type { ResourceInput } from "../../domain/resource-values";
import { ResourceBindingModel, ResourceCatalogModel } from "./resource-catalog.model";
import { ResourceCatalogPersistenceMapper } from "./resource-catalog-persistence.mapper";
import { readDefinitions } from "./resource-catalog-relational";
import { readInventoryByIds, writeGlobalInventory } from "./resource-inventory-relational";
import { ResourcePersistenceMapper } from "./resource-persistence.mapper";
import { readResourceReferenceMetadata } from "./resource-reference-metadata";
import { ResourceScopeSchemaReader } from "./resource-scope-schema";
const HistoryCursorSchema = z.strictObject({
version: z.literal(1),
resourceId: z.uuid(),
position: z.number().int().nonnegative(),
});
const HistoryIdentitySchema = z.object({ id: z.uuid(), position: z.number().int().nonnegative() });
export class SequelizeGlobalResourceRepository implements IGlobalResourceRepository {
private readonly schema: ResourceScopeSchemaReader;
private readonly mapper = new ResourcePersistenceMapper();
constructor(
private readonly database: Sequelize,
private readonly kind: "application" | "os" = "application"
) {
this.schema = new ResourceScopeSchemaReader(database);
}
private async catalog(transaction: Transaction, write: boolean) {
const root = await ResourceCatalogModel.findOne({
where: { catalogKey: "global" },
transaction,
...(write ? { lock: transaction.LOCK.UPDATE } : {}),
});
if (!root) {
throw new Error("Catálogo de recursos no inicializado.");
}
const definitions = await readDefinitions(this.database, root.id, transaction);
const bindings = await ResourceBindingModel.findAll({
where: { templateId: definitions.map((item) => item.id) },
transaction,
});
return new ResourceCatalogPersistenceMapper().toDomain(root, definitions, bindings, []);
}
template(options = { existing: true, kind: this.kind }) {
return this.database.transaction(async (transaction) => {
const catalog = await this.catalog(transaction, false);
const template = catalog.definitions.find(
(item) => item.stockKey.getOrUndefined() === `template:${this.kind}`
);
return template
? this.specificationResponse(
captureGlobalResourceSpecification(catalog, template.id.toPrimitive(), {
...options,
kind: this.kind,
})
)
: Result.fail(new ResourceCatalogError("TEMPLATE_UNAVAILABLE"));
});
}
private specificationResponse(result: ReturnType<typeof captureGlobalResourceSpecification>) {
return result.isSuccess ? Result.ok(result.data.toPrimitive()) : result;
}
private async isGlobal(id: UniqueID, transaction: Transaction) {
const rows = await this.database.query<{
companyId: unknown;
scope?: unknown;
templateId: string;
}>("SELECT * FROM resources WHERE id=:id", {
replacements: { id: id.toPrimitive() },
transaction,
type: QueryTypes.SELECT,
});
const root = rows[0];
if (!root) {
return false;
}
const mode = await this.schema.read();
const ownership = ResourceOwnershipSchema.parse({
scope: mode === "legacy" ? "company" : root.scope,
companyId: root.companyId,
});
if (ownership.scope !== "global") {
return false;
}
const metadata = await readResourceReferenceMetadata(this.database, transaction, mode, {
globalOnly: true,
ids: [id.toPrimitive()],
});
return metadata[0]?.reference?.kind === this.kind;
}
admin(id: UniqueID) {
return this.database.transaction(async (transaction) => {
if (!(await this.isGlobal(id, transaction))) {
return Result.fail(new ResourceCatalogError("RESOURCE_NOT_FOUND"));
}
const catalog = await this.catalog(transaction, false);
const latest = await this.database.query<{ id: string }>(
"SELECT id FROM resource_revisions WHERE resourceId=:id ORDER BY position DESC LIMIT 1",
{ replacements: { id: id.toPrimitive() }, transaction, type: QueryTypes.SELECT }
);
const loaded = await readInventoryByIds(
this.database,
[id.toPrimitive()],
transaction,
latest.map((item) => item.id)
);
const row = loaded.rows[0];
if (!(row && latest[0])) {
throw new Error("Raíz global sin revisión actual.");
}
const eligible = captureGlobalResourceSpecification(catalog, row.templateId, {
existing: true,
kind: this.kind,
});
if (!eligible.isSuccess) {
throw new Error("Plantilla global persistida incompatible.");
}
return Result.ok(
GlobalResourceAdminSchema.parse({
id: row.id,
scope: "global",
companyId: null,
revisionId: latest[0].id,
values: row.values,
specification: row.specification,
})
);
});
}
private async writeContext(id: UniqueID, transaction: Transaction) {
const mode = await this.schema.read();
const catalog = await this.catalog(transaction, true);
const roots = await this.database.query<{ companyId: unknown; scope?: unknown }>(
"SELECT * FROM resources WHERE id=:id FOR UPDATE",
{ replacements: { id: id.toPrimitive() }, transaction, type: QueryTypes.SELECT }
);
const root = roots[0];
const ownership = root
? ResourceOwnershipSchema.parse({
scope: mode === "legacy" ? "company" : root.scope,
companyId: root.companyId,
})
: undefined;
const loaded =
ownership?.scope === "global"
? await readInventoryByIds(this.database, [id.toPrimitive()], transaction)
: undefined;
const row = loaded?.rows[0];
const expectedTemplate = catalog.definitions.find(
(item) => item.stockKey.getOrUndefined() === `template:${this.kind}`
);
const previous =
row && loaded && row.templateId === expectedTemplate?.id.toPrimitive()
? this.mapper.toScopedDomain(row, loaded.revisions)
: undefined;
if (previous) {
await readResourceReferenceMetadata(this.database, transaction, mode, {
globalOnly: true,
ids: [id.toPrimitive()],
});
}
if (
previous &&
!captureGlobalResourceSpecification(catalog, previous.values.toPrimitive().templateId, {
existing: true,
kind: this.kind,
}).isSuccess
) {
throw new Error("Plantilla global persistida incompatible.");
}
return {
mode,
catalog,
root,
previous,
persistedRevisionIds: new Set(previous?.history.map((item) => item.id.toPrimitive()) ?? []),
};
}
async write(id: UniqueID, action: Parameters<IGlobalResourceRepository["write"]>[1]) {
try {
return await this.database.transaction(async (transaction) => {
const { mode, catalog, root, previous, persistedRevisionIds } = await this.writeContext(
id,
transaction
);
const changed = action({
catalog,
resource: previous ? Maybe.some(previous) : Maybe.none(),
occupied: Boolean(root),
});
if (!changed.isSuccess) {
return changed;
}
const validation = await this.validateWrite(changed.data, id, catalog, mode, transaction);
if (!validation.isSuccess) {
return validation;
}
if (changed.data !== previous) {
await writeGlobalInventory(
this.database,
this.mapper.toGlobalPersistence(changed.data),
persistedRevisionIds,
transaction
);
}
const revision = changed.data.history.at(-1);
if (!revision) {
throw new Error("Recurso global sin revisión.");
}
return Result.ok({
created: !root,
resource: GlobalResourceAdminSchema.parse({
id: changed.data.id.toPrimitive(),
scope: "global",
companyId: null,
revisionId: revision.id.toPrimitive(),
values: changed.data.values.toPrimitive(),
specification: changed.data.specification.toPrimitive(),
}),
});
});
} catch (error) {
if (error instanceof ResourceCatalogError) {
return Result.fail(error);
}
throw error;
}
}
private async validateWrite(
resource: Resource,
id: UniqueID,
catalog: ResourceCatalog,
mode: "legacy" | "scoped",
transaction: Transaction
) {
if (mode !== "scoped") {
return Result.fail(new ResourceCatalogError("RESOURCE_GLOBAL_FEATURE_DISABLED"));
}
if (!resource.id.equals(id) || resource.scope !== "global") {
throw new ResourceCatalogError("RESOURCE_OWNERSHIP_FIXED");
}
const eligible = captureGlobalResourceSpecification(
catalog,
resource.values.toPrimitive().templateId,
{ existing: true, kind: this.kind }
);
if (!eligible.isSuccess) {
return eligible;
}
const identityError = validateOperatingSystemIdentity(resource, catalog);
if (identityError) {
return Result.fail(new ResourceCatalogError(identityError));
}
if (this.kind === "os" && (await this.duplicate(resource, catalog, transaction))) {
return Result.fail(new ResourceCatalogError("OS_DUPLICATE"));
}
return Result.ok(undefined);
}
/** El lock exclusivo del catálogo serializa esta comprobación con altas y propagaciones. */
private async duplicate(resource: Resource, catalog: ResourceCatalog, transaction: Transaction) {
const identity = readOperatingSystemIdentity(catalog, resource.values.toPrimitive());
if (!completeOperatingSystemIdentity(identity)) {
return false;
}
const rows = await readResourceReferenceMetadata(this.database, transaction, "scoped", {
globalOnly: true,
globalKind: "os",
currentRead: true,
});
return rows.some(
({ reference }) =>
reference?.scope === "global" &&
reference.kind === "os" &&
reference.id !== resource.id.toPrimitive() &&
sameOperatingSystemIdentity(identity, reference)
);
}
review(values: ResourceInput) {
return this.database.transaction(async (transaction) => {
const catalog = await this.catalog(transaction, false);
const identity = readOperatingSystemIdentity(catalog, values);
const rows = await readResourceReferenceMetadata(
this.database,
transaction,
await this.schema.read(),
{ globalOnly: true }
);
return {
identity,
duplicates: rows.flatMap(({ reference }) =>
reference?.scope === "global" &&
reference.kind === "os" &&
sameOperatingSystemIdentity(identity, reference)
? [reference]
: []
),
};
});
}
history(id: UniqueID, query: ListGlobalApplicationHistoryRequestDTO) {
return this.database.transaction(async (transaction) => {
if (!(await this.isGlobal(id, transaction))) {
return Result.fail(new ResourceCatalogError("RESOURCE_NOT_FOUND"));
}
let position: number | null = null;
if (query.cursor) {
try {
const cursor = HistoryCursorSchema.parse(
JSON.parse(Buffer.from(query.cursor, "base64url").toString("utf8"))
);
if (cursor.resourceId !== id.toPrimitive()) {
return Result.fail(new ResourceCatalogError("INVALID_REQUEST"));
}
position = cursor.position;
} catch {
return Result.fail(new ResourceCatalogError("INVALID_REQUEST"));
}
}
const identities = z.array(HistoryIdentitySchema).parse(
await this.database.query(
"SELECT id,position FROM resource_revisions WHERE resourceId=:id AND (:position IS NULL OR position < :position) ORDER BY position DESC LIMIT :limit",
{
replacements: { id: id.toPrimitive(), position, limit: query.pageSize + 1 },
transaction,
type: QueryTypes.SELECT,
}
)
);
const page = identities.slice(0, query.pageSize);
const loaded = await readInventoryByIds(
this.database,
[id.toPrimitive()],
transaction,
page.map((item) => item.id)
);
const actors = page.length
? await this.database.query<{ id: string; name: string }>(
"SELECT u.id,u.name FROM users u JOIN resource_revisions r ON r.actorId=u.id WHERE r.id IN (:ids)",
{
replacements: { ids: page.map((item) => item.id) },
transaction,
type: QueryTypes.SELECT,
}
)
: [];
const last = page.at(-1);
return Result.ok(
ListGlobalApplicationHistoryResponseSchema.parse({
items: page.map((item) => {
const revision = loaded.revisions.find((entry) => entry.id === item.id);
if (!revision) {
throw new Error("Revisión global ausente.");
}
return {
id: revision.id,
values: revision.values,
specification: revision.specification,
actorId: revision.actorId,
actorName:
actors.find((actor) => actor.id === revision.actorId)?.name ?? revision.actorId,
recordedAt: revision.recordedAt,
};
}),
nextCursor:
last && identities.length > page.length
? Buffer.from(
JSON.stringify({
version: 1,
resourceId: id.toPrimitive(),
position: last.position,
})
).toString("base64url")
: null,
})
);
});
}
}