UNPKG

@elsikora/nestjs-crud-automator

Version:

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

249 lines (245 loc) 14.1 kB
'use strict'; var contextStorage_class = require('../../../class/api/function/context-storage.class.js'); var serviceBase_class = require('../../../class/api/service-base.class.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/unlisted-fields.enum.js'); var relationReferenceShape_enum = require('../../../enum/decorator/api/controller/relation-reference-shape.enum.js'); 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'); 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'); 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'); require('../../../enum/decorator/api/route/type.enum.js'); var action_enum = require('../../../enum/utility/error-string/action.enum.js'); require('../../../enum/utility/error-string/composite-action.enum.js'); require('../../../enum/utility/exception-details-type.enum.js'); require('../../../enum/utility/get-default-string-format-properties-bigint-string-sign.enum.js'); require('../../../enum/utility/manual-dto-property-metadata-decorator.enum.js'); var common = require('@nestjs/common'); var exception_utility = require('../../error/exception.utility.js'); var string_utility = require('../../error/string.utility.js'); var entityColumns_utility = require('../../get/entity-columns.utility.js'); /** * Manages loading related entities when processing API requests. * Determines which relations to load based on request load include, * finds the appropriate service for each relation, and loads the related entities. * @param {TApiControllerMethod<E>} controllerMethod - The controller method with access to service instances * @param {IApiControllerProperties<E>} properties - Controller configuration properties * @param {IApiControllerPropertiesRouteBaseRelationsRequest<E> | undefined} relationConfig - Configuration for relation loading * @param {DeepPartial<E> | Partial<E> | TApiControllerGetListQuery<E>} parameters - The request parameters containing relation IDs * @returns {Promise<void>} A promise that resolves when all relations are loaded * @throws {BadRequestException} When the request relation reference shape is invalid * @throws {Error} When service configuration is invalid or services are not found * @template E - The entity type */ async function ApiControllerHandleRequestRelations(controllerMethod, properties, relationConfig, parameters) { const loadConfig = relationConfig?.load; const referenceConfig = relationConfig?.reference; if (!loadConfig) { return; } if (!referenceConfig) { throw exception_utility.ErrorException("Request relation reference config is required when relation loading is configured"); } validateReferenceConfig(referenceConfig); const relationNames = new Set(entityColumns_utility.GetEntityColumns({ entity: properties.entity, shouldTakeRelationsOnly: true }).filter((propertyName) => typeof propertyName === "string")); const parametersRecord = parameters; const includedRelationEntries = getIncludedRelationEntries(loadConfig.include); const relationLocks = loadConfig.locks; let relationLockRecord; if (relationLocks !== undefined) { if (relationLocks === null || typeof relationLocks !== "object" || Array.isArray(relationLocks)) { throw exception_utility.ErrorException("Request relation locks must be an object"); } relationLockRecord = relationLocks; const includeRecord = loadConfig.include; const relationLockEntries = Object.entries(relationLockRecord); for (const [propertyName, relationLock] of relationLockEntries) { validateIncludedRelationKey(propertyName, relationNames); if (!Object.prototype.hasOwnProperty.call(includeRecord, propertyName) || includeRecord[propertyName] === false) { throw exception_utility.ErrorException(`Request relation lock ${propertyName} requires a matching enabled include`); } if (relationLock === null || typeof relationLock !== "object" || Array.isArray(relationLock)) { throw exception_utility.ErrorException(`Request relation lock ${propertyName} must be an object`); } const relationLockMode = relationLock.mode; if (relationLockMode !== "pessimistic_read" && relationLockMode !== "pessimistic_write") { throw exception_utility.ErrorException(`Request relation lock ${propertyName} mode must be pessimistic_read or pessimistic_write`); } const includeValue = includeRecord[propertyName]; if (includeValue !== true && loadConfig.relationLoadStrategy !== "query") { throw exception_utility.ErrorException(`Request relation lock ${propertyName} with nested relations requires relationLoadStrategy query`); } } if (relationLockEntries.length > 0 && (!contextStorage_class.ApiFunctionContextStorage.getTransactionRegistry() || !contextStorage_class.ApiFunctionContextStorage.getEventManager())) { throw exception_utility.ErrorException("Request relation locks require an active transaction"); } } for (const [propertyName, includeValue] of includedRelationEntries) { validateIncludedRelationKey(propertyName, relationNames); if (!Object.prototype.hasOwnProperty.call(parametersRecord, propertyName) || parametersRecord[propertyName] === undefined || parametersRecord[propertyName] === null || includeValue === false) { continue; } const nestedInclude = resolveNestedInclude(propertyName, includeValue); const serviceName = resolveRelationServiceName(propertyName, loadConfig.services); const service = controllerMethod[serviceName]; if (!service) { throw exception_utility.ErrorException(`Service ${serviceName} not found in controller`); } if (!(service instanceof serviceBase_class.ApiServiceBase)) { throw exception_utility.ErrorException(`Service ${serviceName} is not an instance of ApiServiceBase`); } const referenceKey = referenceConfig.key ?? "id"; const referenceValue = resolveRelationReferenceValue(properties.entity, propertyName, parametersRecord[propertyName], referenceConfig.shape, referenceKey); const requestProperties = { where: { [referenceKey]: referenceValue, }, }; if (nestedInclude) { requestProperties.relations = nestedInclude; } if (loadConfig.relationLoadStrategy) { requestProperties.relationLoadStrategy = loadConfig.relationLoadStrategy; } const relationLock = relationLockRecord?.[propertyName]; if (relationLock) { requestProperties.lock = relationLock; requestProperties.loadEagerRelations = false; } const entity = await service.get(requestProperties); if (!entity) { throw exception_utility.ErrorException(`Service ${serviceName} returned an empty relation entity`); } parametersRecord[propertyName] = entity; } } /** * Builds a canonical bad request exception for invalid relation references. * @param {IApiBaseEntity} entity - Parent entity metadata. * @param {string} propertyName - Relation property name. * @param {EApiControllerRelationReferenceShape} expectedShape - Expected reference shape. * @param {string} referenceKey - Expected object reference key. * @returns {BadRequestException} Structured bad request exception. */ function buildRelationReferenceBadRequestException(entity, propertyName, expectedShape, referenceKey) { return new common.BadRequestException({ details: { expectedShape, propertyName, referenceKey, }, error: "Bad Request", message: string_utility.ErrorString({ entity, type: action_enum.EErrorStringAction.INVALID_REFERENCE }), statusCode: common.HttpStatus.BAD_REQUEST, }); } /** * Returns direct relation include entries from the TypeORM-shaped include map. * @param {unknown} include - Request include map. * @returns {Array<[string, unknown]>} Direct include entries. */ function getIncludedRelationEntries(include) { if (include === null || typeof include !== "object" || Array.isArray(include)) { throw exception_utility.ErrorException("Request relation load include must be an object"); } return Object.entries(include); } /** * Converts a direct include value into nested TypeORM relations for relation service get calls. * @param {string} propertyName - Relation property name. * @param {unknown} includeValue - Direct include value. * @returns {FindOptionsRelations<IApiBaseEntity> | undefined} Nested relation include map. */ function resolveNestedInclude(propertyName, includeValue) { if (includeValue === true || includeValue === false) { return undefined; } if (includeValue === null || typeof includeValue !== "object") { throw exception_utility.ErrorException(`Invalid include value for relation ${propertyName}`); } return includeValue; } /** * Resolves the relation reference value according to the configured request shape. * @param {IApiBaseEntity} entity - Parent entity metadata. * @param {string} propertyName - Relation property name used in validation details. * @param {unknown} value - Incoming scalar or object reference value. * @param {EApiControllerRelationReferenceShape} shape - Expected relation reference shape. * @param {string} referenceKey - Property key to read from object references. * @returns {unknown} Reference value used to load the related entity. */ function resolveRelationReferenceValue(entity, propertyName, value, shape, referenceKey) { if (shape === relationReferenceShape_enum.EApiControllerRelationReferenceShape.SCALAR) { if (value !== null && typeof value === "object") { throw buildRelationReferenceBadRequestException(entity, propertyName, shape, referenceKey); } return value; } if (value === null || typeof value !== "object" || !(referenceKey in value)) { throw buildRelationReferenceBadRequestException(entity, propertyName, shape, referenceKey); } const referenceValue = value[referenceKey]; if (referenceValue === null || referenceValue === undefined) { throw buildRelationReferenceBadRequestException(entity, propertyName, shape, referenceKey); } return referenceValue; } /** * Resolves the controller property name for a direct request relation service. * @param {string} propertyName - Relation property name. * @param {NonNullable<IApiControllerPropertiesRouteBaseRelationsRequest<E>["load"]>["services"]} services - Optional service override map. * @returns {keyof TApiServiceKeys<E>} Controller service property name. * @template E - Entity type. */ function resolveRelationServiceName(propertyName, services) { const serviceName = services?.[propertyName] ?? `${propertyName}Service`; if (!serviceName) { throw exception_utility.ErrorException(`Service name not specified for property ${propertyName}`); } return serviceName; } /** * Ensures a configured include key is a direct relation on the entity. * @param {string} propertyName - Include property name. * @param {Set<string>} relationNames - Direct entity relation names. * @returns {void} */ function validateIncludedRelationKey(propertyName, relationNames) { if (!relationNames.has(propertyName)) { throw exception_utility.ErrorException(`Relation ${propertyName} is not a direct relation on the entity`); } } /** * Ensures request relation reference settings are valid route configuration. * @param {IApiControllerPropertiesRouteBaseRelationsRequest<IApiBaseEntity>["reference"]} referenceConfig - Request relation reference config. * @returns {void} */ function validateReferenceConfig(referenceConfig) { if (!Object.values(relationReferenceShape_enum.EApiControllerRelationReferenceShape).includes(referenceConfig.shape)) { throw exception_utility.ErrorException("Request relation reference shape must be OBJECT or SCALAR"); } if (referenceConfig.key?.length === 0) { throw exception_utility.ErrorException("Request relation reference key must not be empty"); } } exports.ApiControllerHandleRequestRelations = ApiControllerHandleRequestRelations; //# sourceMappingURL=handle-request-relations.utility.js.map