UNPKG

@elsikora/nestjs-crud-automator

Version:

A library for automating the creation of CRUD operations in NestJS.

149 lines (146 loc) 8.67 kB
import { createHash } from 'node:crypto'; import '../../../constant/decorator/api/function.constant.js'; import { PROPERTY_DESCRIBE_DECORATOR_API_CONSTANT } from '../../../constant/decorator/api/property-describe.constant.js'; import { UNSAFE_OBJECT_PROPERTY_NAMES_CONSTANT } from '../../../constant/safe-object-property-names.constant.js'; import '../../../enum/decorator/api/action.enum.js'; import '../../../enum/decorator/api/authentication-type.enum.js'; import '../../../enum/decorator/api/controller/get-list/query/filter/missing-behavior.enum.js'; import '../../../enum/decorator/api/controller/get-list/query/pagination-mode.enum.js'; import '../../../enum/decorator/api/controller/get-list/query/unlisted-fields.enum.js'; import '../../../enum/decorator/api/controller/relation-reference-shape.enum.js'; import '../../../enum/decorator/api/controller/request/target.enum.js'; import '../../../enum/decorator/api/controller/request/transformer-type.enum.js'; import '../../../enum/decorator/api/controller/response-target.enum.js'; import { EApiDtoType } from '../../../enum/decorator/api/dto-type.enum.js'; import '../../../enum/decorator/api/function/context-storage-kind.enum.js'; import '../../../enum/decorator/api/function/subscriber-transaction-expectation.enum.js'; import '../../../enum/decorator/api/function/transaction/event-status.enum.js'; import '../../../enum/decorator/api/function/transaction/failure-stage.enum.js'; import '../../../enum/decorator/api/function/transaction/mode.enum.js'; import '../../../enum/decorator/api/function/transaction/outcome.enum.js'; import '../../../enum/decorator/api/function/transaction/owner-kind.enum.js'; import '../../../enum/decorator/api/function/transaction/trace-type.enum.js'; import '../../../enum/decorator/api/function/type.enum.js'; import '../../../enum/decorator/api/on-type.enum.js'; import '../../../enum/decorator/api/property/data-type.enum.js'; import '../../../enum/decorator/api/property/date/identifier.enum.js'; import '../../../enum/decorator/api/property/date/type.enum.js'; import { EApiPropertyDescribeType } from '../../../enum/decorator/api/property/desribe-type.enum.js'; import '../../../enum/decorator/api/property/number-type.enum.js'; import '../../../enum/decorator/api/property/string-type.enum.js'; import '../../../enum/decorator/api/route/subscriber-authorization-expectation.enum.js'; import { EApiRouteType } from '../../../enum/decorator/api/route/type.enum.js'; import { DtoBuildDecorator } from '../../../utility/dto/build-decorator.utility.js'; import { ErrorException } from '../../../utility/error/exception.utility.js'; import { parse } from 'path-to-regexp'; const IDENTITY_PARAMETER_PATTERN = /^[A-Za-z_$][\w$]*$/u; class ApiControllerIdentityPlanCompiler { static compile(controller, controllerPath, entityMetadata, method, routeConfig, readPlan) { const identityDescriptor = Object.getOwnPropertyDescriptor(routeConfig, "identity"); if (!identityDescriptor) { if (Reflect.has(routeConfig, "identity")) { throw ErrorException("Generated identity must be an own property on the route configuration"); } return undefined; } const rawIdentity = this.readTopLevelIdentity(routeConfig, identityDescriptor); if (method !== EApiRouteType.GET) { throw ErrorException("Generated identity configuration is supported only for GET routes"); } if (routeConfig.dto?.[EApiDtoType.PARAMETERS]) { throw ErrorException("Generated identity cannot be combined with a manual PARAMETERS DTO"); } const parameter = this.readParameter(rawIdentity); const primaryColumn = entityMetadata.primaryKey; const primaryMetadata = primaryColumn?.metadata?.[PROPERTY_DESCRIBE_DECORATOR_API_CONSTANT.METADATA_KEY]; if (!primaryColumn || primaryColumn.relation || !primaryMetadata || primaryMetadata.type === EApiPropertyDescribeType.OBJECT || primaryMetadata.type === EApiPropertyDescribeType.RELATION) { throw ErrorException("Generated identity requires a described direct scalar primary entity field"); } const field = String(primaryColumn.name); const inheritedParameters = this.extractPathParameters(controllerPath ?? ""); if (inheritedParameters.size > 0 && !readPlan) { throw ErrorException("Generated identity on a controller path with inherited parameters requires generated read scope mappings"); } if (inheritedParameters.has(parameter)) { throw ErrorException(`Generated identity parameter "${parameter}" conflicts with an inherited controller path parameter`); } if (parameter !== field && inheritedParameters.has(field)) { throw ErrorException(`Inherited controller path parameter "${field}" is ambiguous with generated identity parameter "${parameter}"`); } const currentGuard = routeConfig.security?.authentication?.guard; if (!DtoBuildDecorator(method, primaryMetadata, entityMetadata, EApiDtoType.PARAMETERS, parameter, currentGuard)) { throw ErrorException(`Generated identity parameter "${parameter}" maps to a primary entity field unavailable for the route PARAMETERS DTO`); } const normalizedPlan = { field, parameter }; const signature = createHash("sha256").update(JSON.stringify(normalizedPlan)).digest("hex"); const controllerName = controller.name || "AnonymousController"; return Object.freeze({ controllerName, field, parameter, schemaName: `${controllerName}${entityMetadata.name ?? "UnknownResource"}GetIdentity${signature}DTO`, signature, }); } static collectPathParameters(pathParts, parameters) { for (const pathPart of pathParts) { if (pathPart.type === "text") { continue; } if (pathPart.type === "group") { this.collectPathParameters(pathPart.tokens, parameters); continue; } parameters.add(pathPart.name); } } static extractPathParameters(path) { const parameters = new Set(); let pathParts; try { pathParts = parse(path).tokens; } catch { throw ErrorException(`Controller path "${path}" is not valid route syntax`); } this.collectPathParameters(pathParts, parameters); return parameters; } static readParameter(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { throw ErrorException("Generated identity configuration must be a plain object"); } const prototype = Object.getPrototypeOf(value); if (prototype !== null && prototype !== Object.prototype) { throw ErrorException("Generated identity configuration must be a plain object"); } const keys = Reflect.ownKeys(value); if (keys.length !== 1 || keys[0] !== "parameter") { throw ErrorException("Generated identity configuration must contain exactly one string key: parameter"); } const descriptor = Object.getOwnPropertyDescriptor(value, "parameter"); if (!descriptor?.enumerable || !("value" in descriptor)) { throw ErrorException("Generated identity parameter must be an enumerable data property"); } const parameter = descriptor.value; if (typeof parameter !== "string" || !IDENTITY_PARAMETER_PATTERN.test(parameter) || UNSAFE_OBJECT_PROPERTY_NAMES_CONSTANT.has(parameter)) { throw ErrorException("Generated identity parameter must be a safe simple identifier"); } return parameter; } static readTopLevelIdentity(routeConfig, descriptor) { const prototype = Object.getPrototypeOf(routeConfig); if (prototype !== null && prototype !== Object.prototype) { throw ErrorException("Generated identity route configuration must be a plain object"); } if (Reflect.ownKeys(routeConfig).some((key) => typeof key === "symbol")) { throw ErrorException("Generated identity route configuration must not contain symbol keys"); } if (!descriptor.enumerable || !("value" in descriptor)) { throw ErrorException("Generated identity must be an enumerable data property on the route configuration"); } return descriptor.value; } } export { ApiControllerIdentityPlanCompiler }; //# sourceMappingURL=identity-plan-compiler.class.js.map