- Implement `current-operating-systems-ui.test.py` to validate the functionality of current operating systems in the UI, including tests for source and states, computer changes, responsive design, error handling, and version saving. - Create `workplace-add-asset.test.py` to test the addition of assets to workstations, covering scenarios such as conflict resolution, responsive design on mobile, error handling during asset creation, and HTTP guards for asset management.
296 lines
10 KiB
TypeScript
296 lines
10 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 type {
|
|
GlobalApplicationDetail,
|
|
ListApplicationCandidatesRequestDTO,
|
|
ListGlobalApplicationsRequestDTO,
|
|
ResolveApplicationReferencesRequestDTO,
|
|
} from "../../../common";
|
|
import {
|
|
CompanyApplicationSummarySchema,
|
|
GlobalApplicationDetailSchema,
|
|
GlobalApplicationSummarySchema,
|
|
ListApplicationCandidatesResponseSchema,
|
|
ListCompanyApplicationsResponseSchema,
|
|
ResolveApplicationReferencesRequestSchema,
|
|
ResolveApplicationReferencesResponseSchema,
|
|
ResourceCandidateSchema,
|
|
} from "../../../common";
|
|
import type { ResourceGlobalGates } from "../../application/resource-global-gates";
|
|
import {
|
|
globalResourcePurposeEnabled,
|
|
resourceGlobalGates,
|
|
} from "../../application/resource-global-gates";
|
|
import { paginateResourceMetadata } from "../../application/resource-reference-pagination";
|
|
import type {
|
|
IGlobalApplicationReader,
|
|
IResourceReferenceReader,
|
|
IResourceReferenceSession,
|
|
} from "../../application/resource-reference-service";
|
|
import { ResourceReferenceError } from "../../application/resource-reference-service";
|
|
|
|
import { readResourceReferenceMetadata } from "./resource-reference-metadata";
|
|
import { ResourceScopeSchemaReader } from "./resource-scope-schema";
|
|
|
|
export class ResourceReferenceReader implements IResourceReferenceReader, IGlobalApplicationReader {
|
|
private readonly schema: ResourceScopeSchemaReader;
|
|
|
|
constructor(
|
|
private readonly database: Sequelize,
|
|
private readonly gates: ResourceGlobalGates = resourceGlobalGates
|
|
) {
|
|
this.schema = new ResourceScopeSchemaReader(database);
|
|
}
|
|
|
|
/** Debe ser la primera operación del consumidor, antes de bloquear compañías/raíces propias. */
|
|
async forTransaction(transaction: Transaction, company: UniqueID, options = { write: true }) {
|
|
const mode = await this.schema.read();
|
|
|
|
if (options.write) {
|
|
const catalog = await this.database.query(
|
|
"SELECT id FROM resource_catalogs WHERE catalogKey='global' LOCK IN SHARE MODE",
|
|
{ transaction, type: QueryTypes.SELECT }
|
|
);
|
|
|
|
if (catalog.length !== 1) {
|
|
throw new Error("Catálogo de recursos no inicializado.");
|
|
}
|
|
}
|
|
const exists = await this.companyExists(transaction, company, options.write);
|
|
|
|
if (!exists) {
|
|
return Result.fail(new ResourceReferenceError("COMPANY_NOT_FOUND"));
|
|
}
|
|
|
|
return Result.ok<IResourceReferenceSession>({
|
|
resolve: (query) => this.resolveIn(transaction, company, query, mode, options.write),
|
|
validateNewAssociation: async (id, purpose) => {
|
|
if (!options.write) {
|
|
throw new Error("La validación de asociación requiere una sesión de escritura.");
|
|
}
|
|
const parsed = ResolveApplicationReferencesRequestSchema.safeParse({ purpose, ids: [id] });
|
|
|
|
if (!parsed.success) {
|
|
return Result.fail(new ResourceReferenceError("INVALID_REQUEST"));
|
|
}
|
|
const result = await this.resolveIn(transaction, company, parsed.data, mode, true);
|
|
const resolved = result.items[0];
|
|
|
|
if (!resolved || resolved.status === "unavailable") {
|
|
return Result.fail(new ResourceReferenceError("RESOURCE_REFERENCE_NOT_FOUND"));
|
|
}
|
|
if (!resolved.reference.active) {
|
|
return Result.fail(new ResourceReferenceError("RESOURCE_REFERENCE_UNAVAILABLE"));
|
|
}
|
|
if (
|
|
resolved.reference.scope === "global" &&
|
|
!globalResourcePurposeEnabled(purpose, this.gates)
|
|
) {
|
|
return Result.fail(new ResourceReferenceError("RESOURCE_GLOBAL_FEATURE_DISABLED"));
|
|
}
|
|
|
|
return Result.ok(resolved.reference);
|
|
},
|
|
});
|
|
}
|
|
|
|
private async companyExists(transaction: Transaction, company: UniqueID, write = false) {
|
|
const rows = await this.database.query(
|
|
`SELECT id FROM companies WHERE id=:id${write ? " FOR UPDATE" : ""}`,
|
|
{ replacements: { id: company.toPrimitive() }, transaction, type: QueryTypes.SELECT }
|
|
);
|
|
|
|
return rows.length === 1;
|
|
}
|
|
|
|
private async resolveIn(
|
|
transaction: Transaction,
|
|
company: UniqueID,
|
|
query: ResolveApplicationReferencesRequestDTO,
|
|
mode: "legacy" | "scoped",
|
|
currentRead = false
|
|
) {
|
|
const validated = ResolveApplicationReferencesRequestSchema.parse(query);
|
|
const rows = await readResourceReferenceMetadata(this.database, transaction, mode, {
|
|
companyId: company.toPrimitive(),
|
|
ids: validated.ids,
|
|
currentRead,
|
|
});
|
|
const references = new Map(
|
|
rows.flatMap(({ reference }) =>
|
|
reference &&
|
|
(validated.purpose === "software-installation" || reference.kind === "application")
|
|
? [[reference.id, reference] as const]
|
|
: []
|
|
)
|
|
);
|
|
|
|
return ResolveApplicationReferencesResponseSchema.parse({
|
|
purpose: validated.purpose,
|
|
items: validated.ids.map((id) => {
|
|
const reference = references.get(id.toLowerCase());
|
|
|
|
return reference ? { id, status: "resolved", reference } : { id, status: "unavailable" };
|
|
}),
|
|
});
|
|
}
|
|
|
|
resolve(company: UniqueID, query: ResolveApplicationReferencesRequestDTO) {
|
|
return this.database.transaction(async (transaction) => {
|
|
if (!(await this.companyExists(transaction, company))) {
|
|
return Result.fail(new ResourceReferenceError("COMPANY_NOT_FOUND"));
|
|
}
|
|
|
|
return Result.ok(await this.resolveIn(transaction, company, query, await this.schema.read()));
|
|
});
|
|
}
|
|
|
|
capabilities(company: UniqueID) {
|
|
return this.database.transaction(async (transaction) => {
|
|
if (!(await this.companyExists(transaction, company))) {
|
|
return Result.fail(new ResourceReferenceError("COMPANY_NOT_FOUND"));
|
|
}
|
|
|
|
return Result.ok({
|
|
softwareSelection: this.gates.catalog && this.gates.mixedSelection,
|
|
softwareAssociation: globalResourcePurposeEnabled("software-installation", this.gates),
|
|
profileSelection: globalResourcePurposeEnabled("application-configuration", this.gates),
|
|
profileAssociation: globalResourcePurposeEnabled("application-configuration", this.gates),
|
|
});
|
|
});
|
|
}
|
|
|
|
candidates(company: UniqueID, query: ListApplicationCandidatesRequestDTO) {
|
|
return this.database.transaction(async (transaction) => {
|
|
if (!(await this.companyExists(transaction, company))) {
|
|
return Result.fail(new ResourceReferenceError("COMPANY_NOT_FOUND"));
|
|
}
|
|
const metadata = await readResourceReferenceMetadata(
|
|
this.database,
|
|
transaction,
|
|
await this.schema.read(),
|
|
{ companyId: company.toPrimitive(), activeOnly: true }
|
|
);
|
|
const candidates = metadata.flatMap(({ reference, provider }) => {
|
|
if (
|
|
!(reference && reference.active) ||
|
|
(query.purpose === "application-configuration" && reference.kind !== "application") ||
|
|
(reference.scope === "global" &&
|
|
!(query.purpose === "software-installation"
|
|
? this.gates.catalog && this.gates.mixedSelection
|
|
: globalResourcePurposeEnabled(query.purpose, this.gates)))
|
|
) {
|
|
return [];
|
|
}
|
|
|
|
return [ResourceCandidateSchema.parse({ ...reference, provider })];
|
|
});
|
|
const page = paginateResourceMetadata(candidates, {
|
|
...query,
|
|
sort: "name",
|
|
direction: "asc",
|
|
context: JSON.stringify([
|
|
company.toPrimitive(),
|
|
query.purpose,
|
|
(
|
|
query.purpose === "software-installation"
|
|
? this.gates.catalog && this.gates.mixedSelection
|
|
: globalResourcePurposeEnabled(query.purpose, this.gates)
|
|
)
|
|
? query.purpose === "software-installation"
|
|
? "mixed-r24"
|
|
: "mixed-r25"
|
|
: "local-r22",
|
|
]),
|
|
});
|
|
|
|
return page.isSuccess
|
|
? Result.ok(
|
|
ListApplicationCandidatesResponseSchema.parse({ purpose: query.purpose, ...page.data })
|
|
)
|
|
: page;
|
|
});
|
|
}
|
|
|
|
applications(company: UniqueID, query: ListGlobalApplicationsRequestDTO) {
|
|
return this.database.transaction(async (transaction) => {
|
|
if (!(await this.companyExists(transaction, company))) {
|
|
return Result.fail(new ResourceReferenceError("COMPANY_NOT_FOUND"));
|
|
}
|
|
const metadata = await readResourceReferenceMetadata(
|
|
this.database,
|
|
transaction,
|
|
await this.schema.read(),
|
|
{ companyId: company.toPrimitive() }
|
|
);
|
|
const items = metadata.flatMap(({ reference, provider }) =>
|
|
reference?.kind === "application" &&
|
|
(query.active === undefined || reference.active === query.active)
|
|
? [CompanyApplicationSummarySchema.parse({ ...reference, provider })]
|
|
: []
|
|
);
|
|
const page = paginateResourceMetadata(items, {
|
|
...query,
|
|
context: JSON.stringify([
|
|
"company-applications",
|
|
company.toPrimitive(),
|
|
query.active ?? null,
|
|
]),
|
|
});
|
|
|
|
return page.isSuccess
|
|
? Result.ok(ListCompanyApplicationsResponseSchema.parse(page.data))
|
|
: page;
|
|
});
|
|
}
|
|
|
|
list(query: ListGlobalApplicationsRequestDTO) {
|
|
return this.database.transaction(async (transaction) => {
|
|
const rows = await readResourceReferenceMetadata(
|
|
this.database,
|
|
transaction,
|
|
await this.schema.read(),
|
|
{ globalOnly: true }
|
|
);
|
|
const items = rows.flatMap(({ reference, provider }) =>
|
|
reference &&
|
|
reference.scope === "global" &&
|
|
(query.active === undefined || query.active === reference.active)
|
|
? [GlobalApplicationSummarySchema.parse({ ...reference, provider })]
|
|
: []
|
|
);
|
|
|
|
return paginateResourceMetadata(items, {
|
|
...query,
|
|
context: JSON.stringify(["global-catalog", query.active ?? null]),
|
|
});
|
|
});
|
|
}
|
|
|
|
detail(id: string) {
|
|
return this.database.transaction(async (transaction) => {
|
|
const rows = await readResourceReferenceMetadata(
|
|
this.database,
|
|
transaction,
|
|
await this.schema.read(),
|
|
{ globalOnly: true, ids: [id] }
|
|
);
|
|
const row = rows[0];
|
|
|
|
return row?.reference
|
|
? Maybe.some(
|
|
GlobalApplicationDetailSchema.parse({
|
|
...row.reference,
|
|
provider: row.provider,
|
|
url: row.url,
|
|
description: row.description,
|
|
})
|
|
)
|
|
: Maybe.none<GlobalApplicationDetail>();
|
|
});
|
|
}
|
|
}
|