UNPKG

@elsikora/nestjs-crud-automator

Version:

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

241 lines (237 loc) 15.3 kB
'use strict'; var node_crypto = require('node:crypto'); require('../../../constant/decorator/api/function.constant.js'); var propertyDescribe_constant = require('../../../constant/decorator/api/property-describe.constant.js'); var safeObjectPropertyNames_constant = require('../../../constant/safe-object-property-names.constant.js'); require('../../../enum/decorator/api/action.enum.js'); require('../../../enum/decorator/api/authentication-type.enum.js'); require('../../../enum/decorator/api/controller/get-list/query/filter/missing-behavior.enum.js'); require('../../../enum/decorator/api/controller/get-list/query/pagination-mode.enum.js'); require('../../../enum/decorator/api/controller/get-list/query/unlisted-fields.enum.js'); require('../../../enum/decorator/api/controller/relation-reference-shape.enum.js'); var target_enum = require('../../../enum/decorator/api/controller/request/target.enum.js'); require('../../../enum/decorator/api/controller/request/transformer-type.enum.js'); require('../../../enum/decorator/api/controller/response-target.enum.js'); var dtoType_enum = require('../../../enum/decorator/api/dto-type.enum.js'); require('../../../enum/decorator/api/function/context-storage-kind.enum.js'); require('../../../enum/decorator/api/function/subscriber-transaction-expectation.enum.js'); require('../../../enum/decorator/api/function/transaction/event-status.enum.js'); require('../../../enum/decorator/api/function/transaction/failure-stage.enum.js'); require('../../../enum/decorator/api/function/transaction/mode.enum.js'); require('../../../enum/decorator/api/function/transaction/outcome.enum.js'); require('../../../enum/decorator/api/function/transaction/owner-kind.enum.js'); require('../../../enum/decorator/api/function/transaction/trace-type.enum.js'); require('../../../enum/decorator/api/function/type.enum.js'); require('../../../enum/decorator/api/on-type.enum.js'); require('../../../enum/decorator/api/property/data-type.enum.js'); require('../../../enum/decorator/api/property/date/identifier.enum.js'); require('../../../enum/decorator/api/property/date/type.enum.js'); var desribeType_enum = require('../../../enum/decorator/api/property/desribe-type.enum.js'); require('../../../enum/decorator/api/property/number-type.enum.js'); require('../../../enum/decorator/api/property/string-type.enum.js'); require('../../../enum/decorator/api/route/subscriber-authorization-expectation.enum.js'); var type_enum = require('../../../enum/decorator/api/route/type.enum.js'); var buildDecorator_utility = require('../../../utility/dto/build-decorator.utility.js'); var exception_utility = require('../../../utility/error/exception.utility.js'); var pathToRegexp = require('path-to-regexp'); class ApiControllerReadPlanCompiler { static compile(controller, controllerPath, entityMetadata, method, routeConfig) { const readDescriptor = Object.getOwnPropertyDescriptor(routeConfig, "read"); const requestConfig = routeConfig.request; if (!readDescriptor) { if (Reflect.has(routeConfig, "read")) { throw exception_utility.ErrorException("Generated read must be an own property on the route configuration"); } if (method === type_enum.EApiRouteType.GET_LIST && requestConfig?.[target_enum.EApiControllerRequestTarget.PARAMETERS] !== undefined) { throw exception_utility.ErrorException("GET_LIST PARAMETERS request configuration requires generated read scope"); } return undefined; } const readConfig = this.readTopLevelRead(routeConfig, readDescriptor); if (method !== type_enum.EApiRouteType.GET && method !== type_enum.EApiRouteType.GET_LIST) { throw exception_utility.ErrorException("Generated read configuration is supported only for GET and GET_LIST routes"); } if (routeConfig.dto?.[dtoType_enum.EApiDtoType.PARAMETERS]) { throw exception_utility.ErrorException("Generated read scope parameters cannot be combined with a manual PARAMETERS DTO"); } const rawReadConfig = this.requireRecord(readConfig, "Generated read configuration"); this.requireExactKeys(rawReadConfig, ["scope"], "Generated read configuration"); const rawScope = this.requireRecord(this.readDataProperty(rawReadConfig, "scope", "Generated read configuration"), "Generated read scope"); this.requireExactKeys(rawScope, ["parameters"], "Generated read scope"); const rawParameters = this.readDenseArray(this.readDataProperty(rawScope, "parameters", "Generated read scope"), "Generated read scope parameters"); const inheritedParameters = this.extractPathParameters(controllerPath ?? ""); const inheritedParameterSet = new Set(inheritedParameters); const mappedParameters = new Set(); const mappedFields = new Set(); const compiledByParameter = new Map(); const primaryIdentityParameter = entityMetadata.primaryKey ? String(entityMetadata.primaryKey.name) : undefined; const currentGuard = routeConfig.security?.authentication?.guard; if (method === type_enum.EApiRouteType.GET && primaryIdentityParameter && inheritedParameterSet.has(primaryIdentityParameter)) { throw exception_utility.ErrorException(`Inherited controller path parameter "${primaryIdentityParameter}" conflicts with the generated GET primary identity parameter`); } for (const [index, rawMapping] of rawParameters.entries()) { const mapping = this.requireRecord(rawMapping, `Generated read scope parameters[${index}]`); this.requireExactKeys(mapping, ["field", "parameter"], `Generated read scope parameters[${index}]`); const rawParameter = this.readDataProperty(mapping, "parameter", `Generated read scope parameters[${index}]`); const rawField = this.readDataProperty(mapping, "field", `Generated read scope parameters[${index}]`); if (typeof rawParameter !== "string" || rawParameter.length === 0) { throw exception_utility.ErrorException(`Generated read scope parameters[${index}] must declare a path parameter`); } const parameter = rawParameter; this.requireSafeScopePropertyName(parameter, "path parameter"); if (!inheritedParameterSet.has(parameter)) { throw exception_utility.ErrorException(`Generated read scope parameter "${parameter}" is not declared by the controller path`); } if (mappedParameters.has(parameter)) { throw exception_utility.ErrorException(`Generated read scope parameter "${parameter}" is mapped more than once`); } if (typeof rawField !== "string" || rawField.length === 0) { throw exception_utility.ErrorException(`Generated read scope parameter "${parameter}" must map to a direct scalar entity field`); } const field = rawField; this.requireSafeScopePropertyName(field, "entity field"); const column = entityMetadata.columns.find((candidate) => String(candidate.name) === field); const metadata = column?.metadata?.[propertyDescribe_constant.PROPERTY_DESCRIBE_DECORATOR_API_CONSTANT.METADATA_KEY]; if (!column || column.relation || !metadata || metadata.type === desribeType_enum.EApiPropertyDescribeType.OBJECT || metadata.type === desribeType_enum.EApiPropertyDescribeType.RELATION) { throw exception_utility.ErrorException(`Generated read scope parameter "${parameter}" must map to a described direct scalar entity field`); } if (!buildDecorator_utility.DtoBuildDecorator(method, metadata, entityMetadata, dtoType_enum.EApiDtoType.PARAMETERS, parameter, currentGuard)) { throw exception_utility.ErrorException(`Generated read scope parameter "${parameter}" maps to an entity field unavailable for the route PARAMETERS DTO`); } if (mappedFields.has(field)) { throw exception_utility.ErrorException(`Generated read scope field "${field}" is mapped more than once`); } mappedParameters.add(parameter); mappedFields.add(field); compiledByParameter.set(parameter, Object.freeze({ field, parameter })); } const unmappedParameter = inheritedParameters.find((parameter) => !mappedParameters.has(parameter)); if (unmappedParameter) { throw exception_utility.ErrorException(`Inherited controller path parameter "${unmappedParameter}" is not mapped by the generated read scope`); } const parameters = Object.freeze(inheritedParameters.flatMap((parameter) => { const compiled = compiledByParameter.get(parameter); return compiled ? [compiled] : []; })); const normalizedPlan = { method, parameters }; const signature = node_crypto.createHash("sha256").update(JSON.stringify(normalizedPlan)).digest("hex"); const controllerName = controller.name || "AnonymousController"; return Object.freeze({ controllerName, parameters, schemaName: `${controllerName}${entityMetadata.name ?? "UnknownResource"}${method === type_enum.EApiRouteType.GET ? "Get" : "GetList"}Parameters${signature}DTO`, signature, }); } static collectPathParameters(pathParts, parameters, isOptionalGroup = false) { for (const pathPart of pathParts) { if (pathPart.type === "text") { continue; } if (pathPart.type === "group") { this.collectPathParameters(pathPart.tokens, parameters, true); continue; } this.requireSafeScopePropertyName(pathPart.name, "controller path parameter"); if (pathPart.type === "wildcard") { throw exception_utility.ErrorException(`Controller path wildcard parameter "${pathPart.name}" cannot be used by generated read scope`); } if (isOptionalGroup) { throw exception_utility.ErrorException(`Controller path optional parameter "${pathPart.name}" cannot be used by generated read scope`); } if (parameters.includes(pathPart.name)) { throw exception_utility.ErrorException(`Controller path parameter "${pathPart.name}" is declared more than once`); } parameters.push(pathPart.name); } } static extractPathParameters(path) { const parameters = []; let pathParts; try { pathParts = pathToRegexp.parse(path).tokens; } catch { throw exception_utility.ErrorException(`Controller path "${path}" is not valid route syntax`); } this.collectPathParameters(pathParts, parameters); return parameters; } static readDataProperty(value, key, context) { const descriptor = Object.getOwnPropertyDescriptor(value, key); if (!descriptor?.enumerable || !("value" in descriptor)) { throw exception_utility.ErrorException(`${context} property "${key}" must be an enumerable data property`); } return descriptor.value; } static readDenseArray(value, context) { if (!Array.isArray(value)) { throw exception_utility.ErrorException(`${context} must be a non-empty array`); } const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length"); const length = lengthDescriptor && "value" in lengthDescriptor ? lengthDescriptor.value : undefined; if (length === 0) { throw exception_utility.ErrorException(`${context} must be a non-empty array`); } if (!Number.isSafeInteger(length) || length < 0 || Reflect.ownKeys(value).length !== length + 1) { throw exception_utility.ErrorException(`${context} must be a non-empty dense array of data properties`); } const items = []; for (let index = 0; index < length; index++) { const descriptor = Object.getOwnPropertyDescriptor(value, String(index)); if (!descriptor?.enumerable || !("value" in descriptor)) { throw exception_utility.ErrorException(`${context} must be a non-empty dense array of data properties`); } items.push(descriptor.value); } return items; } static readTopLevelRead(routeConfig, descriptor) { const prototype = Object.getPrototypeOf(routeConfig); if (prototype !== null && prototype !== Object.prototype) { throw exception_utility.ErrorException("Generated read route configuration must be a plain object"); } if (Reflect.ownKeys(routeConfig).some((key) => typeof key === "symbol")) { throw exception_utility.ErrorException("Generated read route configuration must not contain symbol keys"); } if (!descriptor.enumerable || !("value" in descriptor)) { throw exception_utility.ErrorException("Generated read must be an enumerable data property on the route configuration"); } return descriptor.value; } static requireExactKeys(value, expectedKeys, context) { const actualKeys = Reflect.ownKeys(value) .filter((key) => typeof key === "string") .toSorted((left, right) => left.localeCompare(right)); const normalizedExpectedKeys = expectedKeys.toSorted((left, right) => left.localeCompare(right)); if (actualKeys.length !== normalizedExpectedKeys.length || actualKeys.some((key, index) => key !== normalizedExpectedKeys[index])) { throw exception_utility.ErrorException(`${context} must contain exactly ${normalizedExpectedKeys.join(", ")}`); } } static requireRecord(value, context) { if (!value || typeof value !== "object" || Array.isArray(value)) { throw exception_utility.ErrorException(`${context} must be an object`); } const prototype = Object.getPrototypeOf(value); if (prototype !== null && prototype !== Object.prototype) { throw exception_utility.ErrorException(`${context} must be a plain object`); } for (const key of Reflect.ownKeys(value)) { if (typeof key !== "string") { throw exception_utility.ErrorException(`${context} must contain string keys only`); } const descriptor = Object.getOwnPropertyDescriptor(value, key); if (!descriptor?.enumerable || !("value" in descriptor)) { throw exception_utility.ErrorException(`${context} must contain enumerable data properties only`); } } return value; } static requireSafeScopePropertyName(name, context) { if (safeObjectPropertyNames_constant.UNSAFE_OBJECT_PROPERTY_NAMES_CONSTANT.has(name)) { throw exception_utility.ErrorException(`Generated read scope ${context} "${name}" is not a safe property name`); } } } exports.ApiControllerReadPlanCompiler = ApiControllerReadPlanCompiler; //# sourceMappingURL=read-plan-compiler.class.js.map