UNPKG

@elsikora/nestjs-crud-automator

Version:

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

247 lines (244 loc) 13.6 kB
import { ApiFunctionContextStorage } from '../../../class/api/function/context-storage.class.js'; import { ApiServiceBase } from '../../../class/api/service-base.class.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 { EApiControllerRelationReferenceShape } from '../../../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 '../../../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 '../../../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 '../../../enum/decorator/api/route/type.enum.js'; import { EErrorStringAction } from '../../../enum/utility/error-string/action.enum.js'; import '../../../enum/utility/error-string/composite-action.enum.js'; import '../../../enum/utility/exception-details-type.enum.js'; import '../../../enum/utility/get-default-string-format-properties-bigint-string-sign.enum.js'; import '../../../enum/utility/manual-dto-property-metadata-decorator.enum.js'; import { BadRequestException, HttpStatus } from '@nestjs/common'; import { ErrorException } from '../../error/exception.utility.js'; import { ErrorString } from '../../error/string.utility.js'; import { GetEntityColumns } from '../../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 ErrorException("Request relation reference config is required when relation loading is configured"); } validateReferenceConfig(referenceConfig); const relationNames = new Set(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 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 ErrorException(`Request relation lock ${propertyName} requires a matching enabled include`); } if (relationLock === null || typeof relationLock !== "object" || Array.isArray(relationLock)) { throw ErrorException(`Request relation lock ${propertyName} must be an object`); } const relationLockMode = relationLock.mode; if (relationLockMode !== "pessimistic_read" && relationLockMode !== "pessimistic_write") { throw ErrorException(`Request relation lock ${propertyName} mode must be pessimistic_read or pessimistic_write`); } const includeValue = includeRecord[propertyName]; if (includeValue !== true && loadConfig.relationLoadStrategy !== "query") { throw ErrorException(`Request relation lock ${propertyName} with nested relations requires relationLoadStrategy query`); } } if (relationLockEntries.length > 0 && (!ApiFunctionContextStorage.getTransactionRegistry() || !ApiFunctionContextStorage.getEventManager())) { throw 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 ErrorException(`Service ${serviceName} not found in controller`); } if (!(service instanceof ApiServiceBase)) { throw 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 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 BadRequestException({ details: { expectedShape, propertyName, referenceKey, }, error: "Bad Request", message: ErrorString({ entity, type: EErrorStringAction.INVALID_REFERENCE }), statusCode: 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 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 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 === 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 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 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(EApiControllerRelationReferenceShape).includes(referenceConfig.shape)) { throw ErrorException("Request relation reference shape must be OBJECT or SCALAR"); } if (referenceConfig.key?.length === 0) { throw ErrorException("Request relation reference key must not be empty"); } } export { ApiControllerHandleRequestRelations }; //# sourceMappingURL=handle-request-relations.utility.js.map