- 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.
442 lines
15 KiB
TypeScript
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,
|
|
})
|
|
);
|
|
});
|
|
}
|
|
}
|