@elsikora/nestjs-crud-automator
Version:
A library for automating the creation of CRUD operations in NestJS.
249 lines (245 loc) • 14.1 kB
JavaScript
;
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