UNPKG

@elsikora/nestjs-crud-automator

Version:

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

250 lines (247 loc) 15.1 kB
import '../../../constant/decorator/api/function.constant.js'; import { PROPERTY_DESCRIBE_DECORATOR_API_CONSTANT } from '../../../constant/decorator/api/property-describe.constant.js'; import { FILTER_OPERATOR_REGISTRY_CONSTANT } from '../../../constant/filter/operator-registry.constant.js'; import { SWAGGER_METADATA_CONSTANT } from '../../../constant/swagger/metadata.constant.js'; import { DTO_GENERATE_CONSTANT } from '../../../constant/utility/dto/generate.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/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 { ApiExtraModels } from '@nestjs/swagger'; import { CamelCaseString } from '../../camel-case-string.utility.js'; import { DtoAutoContextPop } from '../auto/context/pop.utility.js'; import { DtoAutoContextPush } from '../auto/context/push.utility.js'; import { DtoBuildDecorator } from '../build-decorator.utility.js'; import { DtoGenerateCacheKey } from './cache-key.utility.js'; import { DtoGenerateDynamic } from './dynamic.utility.js'; import { DtoGenerateFilterDecorator } from './filter-decorator.utility.js'; import { DtoGenerateGetListQueryProperties } from './get-list-query-properties.utility.js'; import { DtoGenerateGetListResponse } from './get-list-response.utility.js'; import { DtoGetGetListQueryBaseClass } from '../get/get-list-query-base-class.utility.js'; import { DtoIsPropertyShouldBeMarked } from '../is/property/should-be-marked.utility.js'; import { DtoIsShouldBeGenerated } from '../is/should-be-generated.utility.js'; import { ErrorException } from '../../error/exception.utility.js'; import { HasPairedCustomSuffixesFieldsValidator } from '../../../validator/has/paired-custom-suffixes-fields.validator.js'; import { Transform } from 'class-transformer'; import { Validate } from 'class-validator'; const dtoGenerateCache = new Map(); /** * Core utility for DTO generation that determines which properties should be included in the DTO. * Builds decorators, handles special cases like filter queries, and generates the appropriate class * based on entity metadata, route type, and DTO type. * @param {ObjectLiteral} entity - The entity class or prototype * @param {IApiEntity<E>} entityMetadata - The entity metadata containing column information * @param {EApiRouteType} method - The API route type (CREATE, DELETE, GET, etc.) * @param {EApiDtoType} dtoType - The type of DTO (REQUEST, RESPONSE, etc.) * @param {IApiControllerPropertiesRouteAutoDtoConfig} [dtoConfig] - Optional configuration for automatic DTO generation * @param {Type<IAuthGuard>} [currentGuard] - Optional authentication guard for property visibility control * @param {IApiControllerGetListQueryPlan} [queryPlan] - Normalized route plan that narrows generated GET_LIST query fields. * @returns {Type<unknown> | undefined} The generated DTO class or undefined if no DTO should be generated * @throws {Error} When primary key metadata is missing * @template E - The entity type */ function DtoGenerate(entity, entityMetadata, method, dtoType, dtoConfig, currentGuard, queryPlan) { if (!DtoIsShouldBeGenerated(method, dtoType)) { return undefined; } const cacheKey = DtoGenerateCacheKey({ controllerName: queryPlan?.controllerName, dtoConfig, dtoType, entityName: String(entityMetadata.name), guardName: currentGuard?.name, method, queryPlanSignature: queryPlan?.signature, }); const cached = dtoGenerateCache.get(cacheKey); if (cached) { return cached; } if (!entityMetadata.primaryKey?.metadata?.[PROPERTY_DESCRIBE_DECORATOR_API_CONSTANT.METADATA_KEY]) { throw ErrorException(`Primary key for entity ${String(entityMetadata.name)} not found in metadata storage`); } // eslint-disable-next-line @elsikora/typescript/no-unsafe-function-type const extraModels = []; const markedProperties = []; for (const column of entityMetadata.columns) { if (column.metadata?.[PROPERTY_DESCRIBE_DECORATOR_API_CONSTANT.METADATA_KEY] && DtoIsPropertyShouldBeMarked(method, dtoType, column.name, column.metadata[PROPERTY_DESCRIBE_DECORATOR_API_CONSTANT.METADATA_KEY], column.isPrimary, currentGuard)) { markedProperties.push({ isPrimary: column.isPrimary, metadata: column.metadata[PROPERTY_DESCRIBE_DECORATOR_API_CONSTANT.METADATA_KEY], name: column.name, }); } } const queryFilterProperties = method === EApiRouteType.GET_LIST && dtoType === EApiDtoType.QUERY ? DtoGenerateGetListQueryProperties(entityMetadata, markedProperties, currentGuard, queryPlan) : []; const BaseClass = method === EApiRouteType.GET_LIST && dtoType === EApiDtoType.QUERY ? DtoGetGetListQueryBaseClass(entity, entityMetadata, method, dtoType, queryPlan) : class { }; const isTypedFilterPlan = method === EApiRouteType.GET_LIST && dtoType === EApiDtoType.QUERY && queryPlan !== undefined && !queryPlan.filter.isLegacy; class GeneratedDTO extends BaseClass { constructor() { super(); if (method === EApiRouteType.GET_LIST && dtoType === EApiDtoType.QUERY) { for (const property of queryFilterProperties) { Object.defineProperty(this, `${property.name}[value]`, { // eslint-disable-next-line @elsikora/typescript/naming-convention configurable: true, // eslint-disable-next-line @elsikora/typescript/naming-convention enumerable: true, value: undefined, // eslint-disable-next-line @elsikora/typescript/naming-convention writable: true, }); Object.defineProperty(this, `${property.name}[values]`, { // eslint-disable-next-line @elsikora/typescript/naming-convention configurable: true, // eslint-disable-next-line @elsikora/typescript/naming-convention enumerable: true, value: undefined, // eslint-disable-next-line @elsikora/typescript/naming-convention writable: true, }); Object.defineProperty(this, `${property.name}[operator]`, { // eslint-disable-next-line @elsikora/typescript/naming-convention configurable: true, // eslint-disable-next-line @elsikora/typescript/naming-convention enumerable: true, value: undefined, // eslint-disable-next-line @elsikora/typescript/naming-convention writable: true, }); } } else { for (const property of markedProperties) { Object.defineProperty(this, property.name, { // eslint-disable-next-line @elsikora/typescript/naming-convention configurable: true, // eslint-disable-next-line @elsikora/typescript/naming-convention enumerable: true, value: undefined, // eslint-disable-next-line @elsikora/typescript/naming-convention writable: true, }); } } } } Object.defineProperty(GeneratedDTO, "name", { value: queryPlan && method === EApiRouteType.GET_LIST && dtoType === EApiDtoType.QUERY ? queryPlan.schemaName : `${entityMetadata.name ?? "UnknownResource"}${CamelCaseString(method)}${CamelCaseString(dtoType)}DTO`, }); DtoAutoContextPush(GeneratedDTO.prototype, method, dtoType); try { if (method === EApiRouteType.GET_LIST && dtoType === EApiDtoType.QUERY) { for (const property of queryFilterProperties) { const decorators = DtoBuildDecorator(method, property.metadata, property.entityMetadata, dtoType, property.name, currentGuard); if (decorators) { for (const [, decorator] of decorators.entries()) { decorator(GeneratedDTO.prototype, `${property.name}[value]`); } } DtoGenerateFilterDecorator(property.metadata, property.entityMetadata, property.allowedOperations, `${queryPlan?.schemaName ?? entityMetadata.name ?? "UnknownResource"}${CamelCaseString(property.name)}FilterOperation`)(GeneratedDTO.prototype, `${property.name}[operator]`); const metadataArray = { ...property.metadata, isArray: true, isUniqueItems: false, maxItems: DTO_GENERATE_CONSTANT.MAXIMUM_FILTER_PROPERTIES, minItems: isTypedFilterPlan ? FILTER_OPERATOR_REGISTRY_CONSTANT.VALUES_MINIMUM_OPERAND_COUNT : DTO_GENERATE_CONSTANT.MINIMUM_FILTER_PROPERTIES, }; const decoratorsArray = DtoBuildDecorator(method, metadataArray, property.entityMetadata, dtoType, property.name, currentGuard); if (isTypedFilterPlan) { Transform(({ value }) => (value === undefined || Array.isArray(value) ? value : [value]), { toClassOnly: true })(GeneratedDTO.prototype, `${property.name}[values]`); } if (decoratorsArray) { for (const [, decorator] of decoratorsArray.entries()) { decorator(GeneratedDTO.prototype, `${property.name}[values]`); } } } } else { for (const property of markedProperties) { const generatedDTOs = DtoGenerateDynamic(method, property.metadata, entityMetadata, dtoType, property.name, currentGuard); const decorators = DtoBuildDecorator(method, property.metadata, entityMetadata, dtoType, property.name, currentGuard, generatedDTOs); if (decorators) { for (const [, decorator] of decorators.entries()) { decorator(GeneratedDTO.prototype, property.name); } } if (property.metadata.type === EApiPropertyDescribeType.OBJECT && Array.isArray(property.metadata.dataType)) { // @ts-ignore extraModels.push(...property.metadata.dataType); } if (generatedDTOs) { for (const [, value] of Object.entries(generatedDTOs)) { extraModels.push(value); } } } } } finally { DtoAutoContextPop(GeneratedDTO.prototype); } if (isTypedFilterPlan) { const hiddenSwaggerProperties = new Set(); for (const property of queryFilterProperties) { for (const suffix of ["operator", "value", "values"]) { const propertyName = `${property.name}[${suffix}]`; hiddenSwaggerProperties.add(`:${propertyName}`); Reflect.deleteMetadata(SWAGGER_METADATA_CONSTANT.MODEL_PROPERTIES, GeneratedDTO.prototype, propertyName); } } const swaggerProperties = Reflect.getMetadata(SWAGGER_METADATA_CONSTANT.MODEL_PROPERTIES_ARRAY, GeneratedDTO.prototype) ?? []; Reflect.defineMetadata(SWAGGER_METADATA_CONSTANT.MODEL_PROPERTIES_ARRAY, swaggerProperties.filter((propertyName) => !hiddenSwaggerProperties.has(propertyName)), GeneratedDTO.prototype); } if (dtoConfig?.validators) { for (const validator of dtoConfig.validators) { Validate(validator.constraintClass, validator.options)(GeneratedDTO.prototype, "object"); } } if (method === EApiRouteType.GET_LIST && dtoType === EApiDtoType.QUERY) { Object.defineProperty(GeneratedDTO.prototype, "object", { // eslint-disable-next-line @elsikora/typescript/naming-convention configurable: true, // eslint-disable-next-line @elsikora/typescript/naming-convention enumerable: true, value: function () { return this; }, // eslint-disable-next-line @elsikora/typescript/naming-convention writable: true, }); Validate(HasPairedCustomSuffixesFieldsValidator, ["operator", ["value", "values"]])(GeneratedDTO.prototype, "object"); } if (extraModels.length > 0) { ApiExtraModels(...extraModels)(GeneratedDTO); } // @ts-ignore const result = method === EApiRouteType.GET_LIST && dtoType === EApiDtoType.RESPONSE ? DtoGenerateGetListResponse(entity, GeneratedDTO, `${entityMetadata.name ?? "UnknownResource"}${CamelCaseString(method)}${CamelCaseString(dtoType)}ItemsDTO`) : GeneratedDTO; dtoGenerateCache.set(cacheKey, result); return result; } export { DtoGenerate }; //# sourceMappingURL=core.utility.js.map