UNPKG

@elsikora/nestjs-crud-automator

Version:

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

428 lines (424 loc) 29.3 kB
import { ApiSubscriberExecutor } from '../../class/api/subscriber/executor.class.js'; import { CONTROLLER_API_DECORATOR_CONSTANT } from '../../constant/decorator/api/controller.constant.js'; import { FUNCTION_API_DECORATOR_CONSTANT } from '../../constant/decorator/api/function.constant.js'; import '../../enum/decorator/api/action.enum.js'; import '../../enum/decorator/api/authentication-type.enum.js'; import '../../enum/decorator/api/controller/load-relations-strategy.enum.js'; import '../../enum/decorator/api/controller/request-transformer-type.enum.js'; import { EApiDtoType } from '../../enum/decorator/api/dto-type.enum.js'; import '../../enum/decorator/api/function/type.enum.js'; import { EApiSubscriberOnType } from '../../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 '../../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 { EApiRouteType } from '../../enum/decorator/api/route-type.enum.js'; import { Controller } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ApiControllerApplyDecorators } from '../../utility/api/controller/apply-decorators.utility.js'; import { ApiControllerApplyMetadata } from '../../utility/api/controller/apply-metadata.utility.js'; import { ApiControllerGetListTransformFilter } from '../../utility/api/controller/get-list/transform-filter.utility.js'; import '../../enum/filter-operation.enum.js'; import 'typeorm/index'; import { ApiControllerGetPrimaryColumn } from '../../utility/api/controller/get-primary-column.utility.js'; import { ApiControllerHandleRequestRelations } from '../../utility/api/controller/handle-request-relations.utility.js'; import { ApiControllerTransformData } from '../../utility/api/controller/transform-data.utility.js'; import { ApiControllerValidateRequest } from '../../utility/api/controller/validate-request.utility.js'; import { ApiControllerWriteDtoSwagger } from '../../utility/api/controller/write-dto-swagger.utility.js'; import { ApiControllerWriteMethod } from '../../utility/api/controller/write-method.utility.js'; import 'typeorm'; import 'reflect-metadata'; import { analyzeEntityMetadata } from '../../utility/dto/analize.utility.js'; import '../../constant/utility/dto/constant.js'; import { ErrorException } from '../../utility/error-exception.utility.js'; import '../../external/tslib/tslib.es6.js'; import { plainToInstance } from 'class-transformer'; import 'class-validator'; import 'lodash/random'; import '../../validator/is-regular-expression.validator.js'; import 'node:crypto'; import '../../validator/must-match-one-of-schemas.validator.js'; import { DtoGenerate } from '../../utility/dto/generate.utility.js'; import '../../enum/filter-order-direction.enum.js'; import '../../validator/all-or-none-of-listed-properties.validator.js'; import { GenerateEntityInformation } from '../../utility/generate-entity-information.utility.js'; class ApiControllerFactory { target; properties; get targetPrototype() { return this.target.prototype; } ENTITY; constructor(target, properties) { this.target = target; this.properties = properties; this.ENTITY = GenerateEntityInformation(properties.entity); analyzeEntityMetadata(this.properties.entity); if (!this.ENTITY.primaryKey) { throw ErrorException(`Primary key for entity ${this.properties.entity.name ?? "UnknownResource"} not found`); } Controller(this.properties.path ?? (this.properties.entity.name ? this.properties.entity.name.toLowerCase() : "UnknownResource"))(this.target); ApiTags(this.properties.name ?? this.properties.entity.name ?? "UnknownResource")(this.target); } createMethod(method) { if (!(method in this.properties.routes) || this.properties.routes[method]?.isEnabled !== false) { const routeConfig = this.properties.routes[method] ?? {}; const routeDecorators = routeConfig.decorators ?? []; const methodName = `${CONTROLLER_API_DECORATOR_CONSTANT.RESERVED_METHOD_PREFIX}${method}`; ApiControllerWriteMethod(this, this.targetPrototype, method, this.properties, this.ENTITY); const targetMethod = this.targetPrototype[methodName]; ApiControllerApplyMetadata(this.target, this.targetPrototype, this.ENTITY, this.properties, method, methodName, routeConfig); if (this.properties.routes[method]?.shouldWriteToController !== false) { ApiControllerApplyDecorators(targetMethod, this.ENTITY, this.properties, method, methodName, routeConfig, routeDecorators); } ApiControllerWriteDtoSwagger(this.target, this.ENTITY, this.properties, method, routeConfig, this.ENTITY); // Reflect.defineMetadata(PARAMTYPES_METADATA, [routeConfig.dto.request], this.target, methodNameOnController); // Reflect.defineMetadata(PATH_METADATA, `:${this.ENTITY.name}`, this.target, methodNameOnController); /* const dtoProperties: Record<string, unknown> = (Reflect.getMetadata(DECORATORS.API_MODEL_PROPERTIES, routeConfig.dto.request) || {}) as Record<string, unknown>; const dtoPropertiesArray: Array<unknown> = (Reflect.getMetadata(DECORATORS.API_MODEL_PROPERTIES_ARRAY, routeConfig.dto.request) || []) as Array<unknown>; Reflect.defineMetadata(DECORATORS.API_MODEL_PROPERTIES, dtoProperties, routeConfig.dto.request); Reflect.defineMetadata(DECORATORS.API_MODEL_PROPERTIES_ARRAY, dtoPropertiesArray, routeConfig.dto.request); */ } } init() { for (const method of Object.values(EApiRouteType)) { this.createMethod(method); } } [EApiRouteType.CREATE](method, methodName, properties, entityMetadata) { this.targetPrototype[methodName] = Object.defineProperty(async function (body, headers, ip, authenticationRequest) { const entityInstance = new properties.entity(); const beforeExecutionContext = { DATA: { entityMetadata, method, methodName, properties }, ENTITY: entityInstance, result: { authenticationRequest, body, headers, ip }, ROUTE_TYPE: EApiRouteType.CREATE, }; const beforeResult = await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.CREATE, EApiSubscriberOnType.BEFORE, beforeExecutionContext); if (beforeResult) { body = beforeResult.body; headers = beforeResult.headers; ip = beforeResult.ip; authenticationRequest = beforeResult.authenticationRequest; } try { const primaryKey = ApiControllerGetPrimaryColumn(body, entityMetadata); if (!primaryKey) { await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.CREATE, EApiSubscriberOnType.BEFORE_ERROR, { ...beforeExecutionContext, result: undefined }, new Error("Primary key not found in entity columns")); throw ErrorException("Primary key not found in entity columns"); } ApiControllerTransformData(properties.routes[method]?.request?.transformers, properties, { body }, { authenticationRequest, headers, ip }); await ApiControllerValidateRequest(properties.routes[method]?.request?.validators, properties, body); await ApiControllerHandleRequestRelations(this, properties, properties.routes[method]?.request?.relations, body); const createResponse = await this.service.create(body); const dto = DtoGenerate(properties.entity, entityMetadata, method, EApiDtoType.RESPONSE, properties.routes[method]?.autoDto?.[EApiDtoType.RESPONSE], properties.routes[method]?.authentication?.guard); const requestProperties = { relations: properties.routes[method]?.response?.relations, where: { [primaryKey.key]: createResponse[primaryKey.key] }, }; const response = await this.service.get(requestProperties); ApiControllerTransformData(properties.routes[method]?.response?.transformers, properties, { response }, { authenticationRequest, headers, ip }); const afterExecutionContext = { DATA: { ...beforeExecutionContext.DATA, authenticationRequest, body, headers, ip }, ENTITY: response, result: response, ROUTE_TYPE: EApiRouteType.CREATE, }; const afterResult = await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, response, EApiRouteType.CREATE, EApiSubscriberOnType.AFTER, afterExecutionContext); const finalResponse = afterResult ?? response; return plainToInstance(dto, finalResponse, { /* eslint-disable-next-line @elsikora/typescript/naming-convention */ excludeExtraneousValues: true, strategy: "excludeAll", }); } catch (error) { const errorExecutionContext = { DATA: { ...beforeExecutionContext.DATA, authenticationRequest, body, headers, ip }, ENTITY: entityInstance, ROUTE_TYPE: EApiRouteType.CREATE, }; await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.CREATE, EApiSubscriberOnType.AFTER_ERROR, errorExecutionContext, error); throw error; } }, "name", { value: methodName }); } [EApiRouteType.DELETE](method, methodName, properties, entityMetadata) { this.targetPrototype[methodName] = Object.defineProperty(async function (parameters, headers, ip, authenticationRequest) { const entityInstance = new properties.entity(); const beforeExecutionContext = { DATA: { entityMetadata, method, methodName, properties }, ENTITY: entityInstance, result: { authenticationRequest, headers, ip, parameters }, ROUTE_TYPE: EApiRouteType.DELETE, }; const beforeResult = await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.DELETE, EApiSubscriberOnType.BEFORE, beforeExecutionContext); if (beforeResult) { parameters = beforeResult.parameters; headers = beforeResult.headers; ip = beforeResult.ip; authenticationRequest = beforeResult.authenticationRequest; } try { const primaryKey = ApiControllerGetPrimaryColumn(parameters, entityMetadata); if (!primaryKey) { await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.DELETE, EApiSubscriberOnType.BEFORE_ERROR, { ...beforeExecutionContext, result: undefined }, new Error("Primary key not found in entity columns")); throw ErrorException("Primary key not found in entity columns"); } ApiControllerTransformData(properties.routes[method]?.request?.transformers, properties, { parameters }, { authenticationRequest, headers, ip }); await ApiControllerValidateRequest(properties.routes[method]?.request?.validators, properties, parameters); await ApiControllerHandleRequestRelations(this, properties, properties.routes[method]?.request?.relations, parameters); const requestCriteria = { [primaryKey.key]: primaryKey.value, }; await this.service.delete(requestCriteria); const afterExecutionContext = { DATA: { ...beforeExecutionContext.DATA, authenticationRequest, headers, ip, parameters }, ENTITY: entityInstance, result: parameters, ROUTE_TYPE: EApiRouteType.DELETE, }; await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.DELETE, EApiSubscriberOnType.AFTER, afterExecutionContext); } catch (error) { const errorExecutionContext = { DATA: { ...beforeExecutionContext.DATA, authenticationRequest, headers, ip, parameters }, ENTITY: entityInstance, ROUTE_TYPE: EApiRouteType.DELETE, }; await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.DELETE, EApiSubscriberOnType.AFTER_ERROR, errorExecutionContext, error); throw error; } }, "name", { value: methodName }); } [EApiRouteType.GET](method, methodName, properties, entityMetadata) { this.targetPrototype[methodName] = Object.defineProperty(async function (parameters, headers, ip, authenticationRequest) { const entityInstance = new properties.entity(); const beforeExecutionContext = { DATA: { entityMetadata, method, methodName, properties }, ENTITY: entityInstance, result: { authenticationRequest, headers, ip, parameters }, ROUTE_TYPE: EApiRouteType.GET, }; const beforeResult = await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.GET, EApiSubscriberOnType.BEFORE, beforeExecutionContext); if (beforeResult) { parameters = beforeResult.parameters; headers = beforeResult.headers; ip = beforeResult.ip; authenticationRequest = beforeResult.authenticationRequest; } try { const primaryKey = ApiControllerGetPrimaryColumn(parameters, entityMetadata); if (!primaryKey) { await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.GET, EApiSubscriberOnType.BEFORE_ERROR, { ...beforeExecutionContext, result: undefined }, new Error("Primary key not found in entity columns")); throw ErrorException("Primary key not found in entity columns"); } ApiControllerTransformData(properties.routes[method]?.request?.transformers, properties, { parameters }, { authenticationRequest, headers, ip }); await ApiControllerValidateRequest(properties.routes[method]?.request?.validators, properties, parameters); await ApiControllerHandleRequestRelations(this, properties, properties.routes[method]?.request?.relations, parameters); const requestProperties = { relations: properties.routes[method]?.response?.relations, where: { [primaryKey.key]: primaryKey.value }, }; const response = await this.service.get(requestProperties); ApiControllerTransformData(properties.routes[method]?.response?.transformers, properties, { response }, { authenticationRequest, headers, ip }); const afterExecutionContext = { DATA: { ...beforeExecutionContext.DATA, authenticationRequest, headers, ip, parameters }, ENTITY: response, result: response, ROUTE_TYPE: EApiRouteType.GET, }; const afterResult = await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, response, EApiRouteType.GET, EApiSubscriberOnType.AFTER, afterExecutionContext); const finalResponse = afterResult ?? response; const dto = DtoGenerate(properties.entity, entityMetadata, method, EApiDtoType.RESPONSE, properties.routes[method]?.autoDto?.[EApiDtoType.RESPONSE], properties.routes[method]?.authentication?.guard); return plainToInstance(dto, finalResponse, { /* eslint-disable-next-line @elsikora/typescript/naming-convention */ excludeExtraneousValues: true, }); } catch (error) { const errorExecutionContext = { DATA: { ...beforeExecutionContext.DATA, authenticationRequest, headers, ip, parameters }, ENTITY: entityInstance, ROUTE_TYPE: EApiRouteType.GET, }; await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.GET, EApiSubscriberOnType.AFTER_ERROR, errorExecutionContext, error); throw error; } }, "name", { value: methodName }); } [EApiRouteType.GET_LIST](method, methodName, properties, entityMetadata) { this.targetPrototype[methodName] = Object.defineProperty(async function (query, headers, ip, authenticationRequest) { const entityInstance = new properties.entity(); const beforeExecutionContext = { DATA: { entityMetadata, method, methodName, properties }, ENTITY: entityInstance, result: { authenticationRequest, headers, ip, query }, ROUTE_TYPE: EApiRouteType.GET_LIST, }; const beforeResult = await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.GET_LIST, EApiSubscriberOnType.BEFORE, beforeExecutionContext); if (beforeResult) { query = beforeResult.query; headers = beforeResult.headers; ip = beforeResult.ip; authenticationRequest = beforeResult.authenticationRequest; } try { ApiControllerTransformData(properties.routes[method]?.request?.transformers, properties, { query }, { authenticationRequest, headers, ip }); await ApiControllerValidateRequest(properties.routes[method]?.request?.validators, properties, query); await ApiControllerHandleRequestRelations(this, properties, properties.routes[method]?.request?.relations, query); const { limit, orderBy, orderDirection, page, ...getListQuery } = query; const filter = ApiControllerGetListTransformFilter(getListQuery, entityMetadata); const requestProperties = { relations: properties.routes[method]?.response?.relations, skip: query.limit * (query.page - 1), take: query.limit, where: filter, }; if (orderBy) { requestProperties.order = { [orderBy]: orderDirection ?? FUNCTION_API_DECORATOR_CONSTANT.DEFAULT_FILTER_ORDER_BY_DIRECTION }; } const response = await this.service.getList(requestProperties); ApiControllerTransformData(properties.routes[method]?.request?.transformers, properties, { response }, { authenticationRequest, headers, ip }); const afterExecutionContext = { DATA: { ...beforeExecutionContext.DATA, authenticationRequest, headers, ip, query }, ENTITY: entityInstance, result: response, ROUTE_TYPE: EApiRouteType.GET_LIST, }; const afterResult = await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.GET_LIST, EApiSubscriberOnType.AFTER, afterExecutionContext); const finalResponse = afterResult ?? response; const dto = DtoGenerate(properties.entity, entityMetadata, method, EApiDtoType.RESPONSE, properties.routes[method]?.autoDto?.[EApiDtoType.RESPONSE], properties.routes[method]?.authentication?.guard); return plainToInstance(dto, finalResponse, { // eslint-disable-next-line @elsikora/typescript/naming-convention excludeExtraneousValues: true, }); } catch (error) { const errorExecutionContext = { DATA: { ...beforeExecutionContext.DATA, authenticationRequest, headers, ip, query }, ENTITY: entityInstance, ROUTE_TYPE: EApiRouteType.GET_LIST, }; await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.GET_LIST, EApiSubscriberOnType.AFTER_ERROR, errorExecutionContext, error); throw error; } }, "name", { value: methodName }); } [EApiRouteType.PARTIAL_UPDATE](method, methodName, properties, entityMetadata) { this.targetPrototype[methodName] = Object.defineProperty(async function (parameters, body, headers, ip, authenticationRequest) { const entityInstance = new properties.entity(); const beforeExecutionContext = { DATA: { entityMetadata, method, methodName, properties }, ENTITY: entityInstance, result: { authenticationRequest, body, headers, ip, parameters }, ROUTE_TYPE: EApiRouteType.PARTIAL_UPDATE, }; const beforeResult = await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.PARTIAL_UPDATE, EApiSubscriberOnType.BEFORE, beforeExecutionContext); if (beforeResult) { parameters = beforeResult.parameters; body = beforeResult.body; headers = beforeResult.headers; ip = beforeResult.ip; authenticationRequest = beforeResult.authenticationRequest; } try { const primaryKey = ApiControllerGetPrimaryColumn(parameters, entityMetadata); if (!primaryKey) { await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.PARTIAL_UPDATE, EApiSubscriberOnType.BEFORE_ERROR, { ...beforeExecutionContext, result: undefined }, new Error("Primary key not found in entity columns")); throw ErrorException("Primary key not found in entity columns"); } ApiControllerTransformData(properties.routes[method]?.request?.transformers, properties, { body, parameters }, { authenticationRequest, headers, ip }); await ApiControllerValidateRequest(properties.routes[method]?.request?.validators, properties, parameters); await ApiControllerHandleRequestRelations(this, properties, properties.routes[method]?.request?.relations, parameters); const requestCriteria = { [primaryKey.key]: primaryKey.value, }; const response = await this.service.update(requestCriteria, body); ApiControllerTransformData(properties.routes[method]?.response?.transformers, properties, { response }, { authenticationRequest, headers, ip }); const afterExecutionContext = { DATA: { ...beforeExecutionContext.DATA, authenticationRequest, body, headers, ip, parameters }, ENTITY: response, result: response, ROUTE_TYPE: EApiRouteType.PARTIAL_UPDATE, }; const afterResult = await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, response, EApiRouteType.PARTIAL_UPDATE, EApiSubscriberOnType.AFTER, afterExecutionContext); const finalResponse = afterResult ?? response; const dto = DtoGenerate(properties.entity, entityMetadata, method, EApiDtoType.RESPONSE, properties.routes[method]?.autoDto?.[EApiDtoType.RESPONSE], properties.routes[method]?.authentication?.guard); return plainToInstance(dto, finalResponse, { // eslint-disable-next-line @elsikora/typescript/naming-convention excludeExtraneousValues: true, }); } catch (error) { const errorExecutionContext = { DATA: { ...beforeExecutionContext.DATA, authenticationRequest, body, headers, ip, parameters }, ENTITY: entityInstance, ROUTE_TYPE: EApiRouteType.PARTIAL_UPDATE, }; await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.PARTIAL_UPDATE, EApiSubscriberOnType.AFTER_ERROR, errorExecutionContext, error); throw error; } }, "name", { value: methodName }); } [EApiRouteType.UPDATE](method, methodName, properties, entityMetadata) { this.targetPrototype[methodName] = Object.defineProperty(async function (parameters, body, headers, ip, authenticationRequest) { const entityInstance = new properties.entity(); const beforeExecutionContext = { DATA: { entityMetadata, method, methodName, properties }, ENTITY: entityInstance, result: { authenticationRequest, body, headers, ip, parameters }, ROUTE_TYPE: EApiRouteType.UPDATE, }; const beforeResult = await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.UPDATE, EApiSubscriberOnType.BEFORE, beforeExecutionContext); if (beforeResult) { parameters = beforeResult.parameters; body = beforeResult.body; headers = beforeResult.headers; ip = beforeResult.ip; authenticationRequest = beforeResult.authenticationRequest; } try { const primaryKey = ApiControllerGetPrimaryColumn(parameters, entityMetadata); if (!primaryKey) { await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.UPDATE, EApiSubscriberOnType.BEFORE_ERROR, { ...beforeExecutionContext, result: undefined }, new Error("Primary key not found in entity columns")); throw ErrorException("Primary key not found in entity columns"); } ApiControllerTransformData(properties.routes[method]?.request?.transformers, properties, { body, parameters }, { authenticationRequest, headers, ip }); await ApiControllerValidateRequest(properties.routes[method]?.request?.validators, properties, parameters); await ApiControllerHandleRequestRelations(this, properties, properties.routes[method]?.request?.relations, parameters); const requestCriteria = { [primaryKey.key]: primaryKey.value, }; const response = await this.service.update(requestCriteria, body); ApiControllerTransformData(properties.routes[method]?.response?.transformers, properties, { response }, { authenticationRequest, headers, ip }); const afterExecutionContext = { DATA: { ...beforeExecutionContext.DATA, authenticationRequest, body, headers, ip, parameters }, ENTITY: response, result: response, ROUTE_TYPE: EApiRouteType.UPDATE, }; const afterResult = await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, response, EApiRouteType.UPDATE, EApiSubscriberOnType.AFTER, afterExecutionContext); const finalResponse = afterResult ?? response; const dto = DtoGenerate(properties.entity, entityMetadata, method, EApiDtoType.RESPONSE, properties.routes[method]?.autoDto?.[EApiDtoType.RESPONSE], properties.routes[method]?.authentication?.guard); return plainToInstance(dto, finalResponse, { // eslint-disable-next-line @elsikora/typescript/naming-convention excludeExtraneousValues: true, }); } catch (error) { const errorExecutionContext = { DATA: { ...beforeExecutionContext.DATA, authenticationRequest, body, headers, ip, parameters }, ENTITY: entityInstance, ROUTE_TYPE: EApiRouteType.UPDATE, }; await ApiSubscriberExecutor.executeRouteSubscribers(this.constructor, entityInstance, EApiRouteType.UPDATE, EApiSubscriberOnType.AFTER_ERROR, errorExecutionContext, error); throw error; } }, "name", { value: methodName }); } } export { ApiControllerFactory }; //# sourceMappingURL=controller.factory.js.map