UNPKG

@elsikora/nestjs-crud-automator

Version:

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

590 lines (587 loc) 42 kB
import './controller/generated/get-many-contract.class.js'; import { ApiControllerGeneratedReadScopeStorage } from './controller/generated/read-scope-storage.class.js'; import { ApiControllerGeneratedSecuritySnapshot } from './controller/generated/security-snapshot.class.js'; import { ApiControllerGeneratedFunctionCapability } from './controller/generated/function-capability.class.js'; import { ApiControllerGetListCursorDataSourceContract } from './controller/get-list/cursor/data-source-contract.class.js'; import { ApiControllerGetListCursorRuntime } from './controller/get-list/cursor/runtime.class.js'; import './controller/get-list/query/plan-compiler.class.js'; import { ApiControllerGetListQueryRuntime } from './controller/get-list/query/runtime.class.js'; import { ApiFunctionContextStorage } from './function/context-storage.class.js'; import { ApiFunctionTransactionRuntime } from './function/transaction/runtime.class.js'; import { ApiServiceBase } from './service-base.class.js'; import { ApiSubscriberExecutor } from './subscriber/executor.class.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/get-list/query/filter/missing-behavior.enum.js'; import { EApiControllerGetListQueryPaginationMode } from '../../enum/decorator/api/controller/get-list/query/pagination-mode.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 { EApiControllerRequestTarget } from '../../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 { EApiFunctionTransactionMode } from '../../enum/decorator/api/function/transaction/mode.enum.js'; import '../../enum/decorator/api/function/transaction/outcome.enum.js'; import { EApiFunctionTransactionOwnerKind } from '../../enum/decorator/api/function/transaction/owner-kind.enum.js'; import '../../enum/decorator/api/function/transaction/trace-type.enum.js'; import { EApiFunctionType } from '../../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 '../../enum/decorator/api/route/subscriber-authorization-expectation.enum.js'; import { EApiRouteType } from '../../enum/decorator/api/route/type.enum.js'; import { BadRequestException } from '@nestjs/common'; import '../../constant/filter/operator-registry.constant.js'; import '../../enum/filter/operand.enum.js'; import '../../enum/filter/operation.enum.js'; import '../../enum/filter/order-direction.enum.js'; import '@nestjs/swagger'; import { ErrorException } from '../../utility/error/exception.utility.js'; import { ApiControllerGetListQueryPlanGet } from '../../utility/api/controller/get-list/query/plan/get.utility.js'; import { ApiControllerGetListQueryGetPaginationMode } from '../../utility/api/controller/get-list/query/get-pagination-mode.utility.js'; import { ApiControllerGetListQueryResolveCursorOrderFields } from '../../utility/api/controller/get-list/query/resolve-cursor-order-fields.utility.js'; import { ApiControllerGetListTransformFilter } from '../../utility/api/controller/get-list/transform/filter.utility.js'; import { ApiControllerGetDtoWithReadPlan } from '../../utility/api/controller/get/dto.utility.js'; import { ApiControllerGetPrimaryColumn } from '../../utility/api/controller/get/primary-column.utility.js'; import { ApiControllerHandleRequestRelations } from '../../utility/api/controller/handle-request-relations.utility.js'; import { ApiControllerIdentityPlanGet } from '../../utility/api/controller/identity/plan/get.utility.js'; import { ApiControllerReadPlanGet } from '../../utility/api/controller/read/plan/get.utility.js'; import { ApiControllerReadScopeWhere } from '../../utility/api/controller/read/scope.utility.js'; import { ApiControllerSerializeRouteResponse } from '../../utility/api/controller/serialize-route-response.utility.js'; import { ApiControllerTransformData } from '../../utility/api/controller/transform-data.utility.js'; import { ApiControllerValidateRequest } from '../../utility/api/controller/validate-request.utility.js'; import '@nestjs/common/constants.js'; import '@nestjs/common/enums/route-paramtypes.enum.js'; import '@nestjs/swagger/dist/constants.js'; import { ApiRouteCreateDiscriminatedDtoValidationFailure } from '../../utility/api/route/discriminator/create-validation-failure.utility.js'; import { ApiRouteIsDiscriminatedDtoProperties } from '../../utility/api/route/discriminator/is-dto-properties.utility.js'; import { ApiRoutePrepareDiscriminatedDtoPayload } from '../../utility/api/route/discriminator/prepare-payload.utility.js'; import { ApiRouteResolveDiscriminatedDto } from '../../utility/api/route/discriminator/resolve-dto.utility.js'; import '../metadata-storage.class.js'; import 'typeorm'; import { ApiRouteProjectRelationResponse } from '../../utility/api/route/response/project-relation.utility.js'; import { ApiRouteSerializeResponse } from '../../utility/api/route/response/serialize.utility.js'; import { ApiRouteValidationFlattenErrors } from '../../utility/api/route/validation-flatten-errors.utility.js'; import { AuthorizationDecisionApplyResult } from '../../utility/authorization/decision/apply-result.utility.js'; import { AuthorizationDecisionAttachResource } from '../../utility/authorization/decision/attach-resource.utility.js'; import { AuthorizationDecisionResolveFromRequest } from '../../utility/authorization/decision/resolve-from-request.utility.js'; import { AuthorizationScopeMergeWhere } from '../../utility/authorization/scope-merge-where.utility.js'; import { plainToInstance, instanceToPlain } from 'class-transformer'; import { validate } from 'class-validator'; class ApiRouteRuntime { static async executeCustom(options) { const action = options.metadata.resource.action; const entityInstance = new options.metadata.resource.entity(); const request = options.executionContext.switchToHttp().getRequest(); const headers = {}; for (const [key, value] of Object.entries(request.headers ?? {})) { if (typeof value === "string") { headers[key] = value; } else if (Array.isArray(value) && typeof value[0] === "string") { headers[key] = value[0]; } } const contextData = { authenticationRequest: request, headers, ip: request.ip ?? "", metadata: options.metadata, runtimeProperties: options.runtimeProperties, }; const beforeContext = { action, DATA: contextData, ENTITY: entityInstance, result: { body: request.body, parameters: request.params, query: request.query, }, ROUTE_TYPE: options.metadata.route.type, }; try { const beforeResult = await ApiSubscriberExecutor.executeRouteSubscribers(options.executionContext.getClass(), entityInstance, options.metadata.route.type, EApiSubscriberOnType.BEFORE, beforeContext, action); if (beforeResult) { request.body = beforeResult.body; request.params = beforeResult.parameters; request.query = beforeResult.query; } await this.executeRequestTargets(options.metadata, options.runtimeProperties, request, contextData); } catch (error) { await this.executeCustomErrorSubscribers(options, entityInstance, contextData, EApiSubscriberOnType.BEFORE_ERROR, error); throw error; } try { const result = await options.operation(); const transformedResult = await this.executeCustomResponse(options, entityInstance, contextData, result); return transformedResult; } catch (error) { await this.executeCustomErrorSubscribers(options, entityInstance, contextData, EApiSubscriberOnType.AFTER_ERROR, error); throw error; } } static async executeCustomRequestRelations(controller, metadata, runtimeProperties, body) { if (!body) { return; } await ApiControllerHandleRequestRelations(controller, { entity: metadata.resource.entity}, runtimeProperties.relations?.request, body); } static async executeCustomResponseRelations(controller, runtimeProperties, response) { if (!this.hasResponseRelationInclude(runtimeProperties.relations?.response?.load?.include)) { return response; } if (!(controller.service instanceof ApiServiceBase)) { throw ErrorException("ApiRouteCustom response relation loading requires a controller service extending ApiServiceBase"); } if (Array.isArray(response)) { return (await Promise.all(response.map(async (item) => await this.loadCustomResponseRelations(controller.service, runtimeProperties, item)))); } const responseValue = response; if (responseValue !== null && typeof responseValue === "object" && "items" in responseValue && Array.isArray(responseValue.items)) { const responseObject = responseValue; responseObject.items = await Promise.all(responseObject.items.map(async (item) => await this.loadCustomResponseRelations(controller.service, runtimeProperties, item))); return response; } return (await this.loadCustomResponseRelations(controller.service, runtimeProperties, response)); } static async executeGenerated(options) { const entityInstance = new options.properties.entity(); const authorizationDecision = ApiControllerGeneratedSecuritySnapshot.create(AuthorizationDecisionResolveFromRequest(options.targets.authenticationRequest)); const beforeContext = { DATA: { authorizationDecision: ApiControllerGeneratedSecuritySnapshot.createSubscriberView(authorizationDecision), entityMetadata: options.entityMetadata, method: options.method, methodName: options.methodName, properties: options.properties }, ENTITY: entityInstance, result: options.targets, ROUTE_TYPE: options.method, }; let beforeResult; try { beforeResult = await ApiSubscriberExecutor.executeRouteSubscribers(options.controller.constructor, entityInstance, options.method, EApiSubscriberOnType.BEFORE, beforeContext); } catch (error) { await this.executeGeneratedErrorSubscribers(options, options.targets, entityInstance, beforeContext.DATA, authorizationDecision, EApiSubscriberOnType.BEFORE_ERROR, error); throw error; } const targets = beforeResult ?? options.targets; let errorBoundary = EApiSubscriberOnType.BEFORE_ERROR; try { const result = await this.executeGeneratedOperation(options, targets, authorizationDecision, () => { errorBoundary = EApiSubscriberOnType.AFTER_ERROR; }); return await this.executeGeneratedResponse(options, targets, entityInstance, beforeContext.DATA, authorizationDecision, result); } catch (error) { await this.executeGeneratedErrorSubscribers(options, targets, entityInstance, beforeContext.DATA, authorizationDecision, errorBoundary, error); throw error; } } static async executeCustomErrorSubscribers(options, entityInstance, contextData, onType, error) { const action = options.metadata.resource.action; const errorContext = { action, DATA: contextData, ENTITY: entityInstance, ROUTE_TYPE: options.metadata.route.type, }; await ApiSubscriberExecutor.executeRouteErrorSubscribers(options.executionContext.getClass(), entityInstance, options.metadata.route.type, onType, errorContext, error, action); } static async executeCustomResponse(options, entityInstance, contextData, result) { const action = options.metadata.resource.action; const responseTarget = { response: ApiRouteProjectRelationResponse(options.runtimeProperties.relations?.response, result) }; let authorizationDecision = AuthorizationDecisionResolveFromRequest(contextData.authenticationRequest); const controllerProperties = { entity: options.metadata.resource.entity}; authorizationDecision = AuthorizationDecisionAttachResource(authorizationDecision, responseTarget.response); ApiControllerTransformData(options.runtimeProperties.response, controllerProperties, responseTarget, contextData); const afterContext = { action, DATA: contextData, ENTITY: entityInstance, result: responseTarget.response, ROUTE_TYPE: options.metadata.route.type, }; const afterResult = await ApiSubscriberExecutor.executeRouteSubscribers(options.executionContext.getClass(), entityInstance, options.metadata.route.type, EApiSubscriberOnType.AFTER, afterContext, action); const finalResult = afterResult ?? responseTarget.response; const transformedResult = await AuthorizationDecisionApplyResult(AuthorizationDecisionAttachResource(authorizationDecision, finalResult), finalResult); return ApiRouteSerializeResponse(options.metadata, transformedResult); } static async executeDiscriminatedRequestBodyDto(runtimeProperties, request) { const bodyDto = runtimeProperties.dto?.[EApiDtoType.BODY]; if (!ApiRouteIsDiscriminatedDtoProperties(bodyDto)) { return; } let selectedDto; try { selectedDto = ApiRouteResolveDiscriminatedDto(bodyDto, request.body, "ApiRouteCustom body"); } catch { const validationError = ApiRouteCreateDiscriminatedDtoValidationFailure(bodyDto, request.body); const validationMessages = ApiRouteValidationFlattenErrors([validationError]); throw new BadRequestException(validationMessages); } const transformedBody = plainToInstance(selectedDto, ApiRoutePrepareDiscriminatedDtoPayload(bodyDto, request.body), bodyDto.transformOptions); const validationErrors = await validate(transformedBody, bodyDto.validatorOptions); if (validationErrors.length > 0) { const validationMessages = ApiRouteValidationFlattenErrors(validationErrors); throw new BadRequestException(validationMessages); } request.body = transformedBody; } static async executeGeneratedErrorSubscribers(options, targets, entityInstance, baseData, authorizationDecision, onType, error) { const errorContext = { DATA: { ...baseData, authenticationRequest: targets.authenticationRequest, authorizationDecision: ApiControllerGeneratedSecuritySnapshot.createSubscriberView(authorizationDecision), body: targets.body, headers: targets.headers, ip: targets.ip, parameters: targets.parameters, query: targets.query }, ENTITY: entityInstance, ROUTE_TYPE: options.method, }; await ApiSubscriberExecutor.executeRouteErrorSubscribers(options.controller.constructor, entityInstance, options.method, onType, errorContext, error); } static async executeGeneratedOperation(options, targets, authorizationDecision, markAfterBoundary) { const routeConfig = options.properties.routes[options.method] ?? {}; const method = options.method; if (method === EApiRouteType.PARTIAL_UPDATE || method === EApiRouteType.UPDATE) { await this.executeGeneratedRequestPipeline(options, targets, [EApiControllerRequestTarget.PARAMETERS, EApiControllerRequestTarget.BODY]); const service = options.controller.service; const updateFunction = ApiControllerGeneratedFunctionCapability.resolve(service, EApiFunctionType.UPDATE, EApiFunctionType.UPDATE, options.properties.entity); const responseLoad = routeConfig.relations?.response?.load; const getFunction = responseLoad && this.hasResponseRelationInclude(responseLoad.include) ? ApiControllerGeneratedFunctionCapability.resolve(service, EApiFunctionType.GET, EApiFunctionType.GET, options.properties.entity) : undefined; return await this.executeGeneratedTransaction(options, routeConfig, async () => await this.executeGeneratedUpdateOperation(options, targets, authorizationDecision, routeConfig, markAfterBoundary, service, updateFunction, getFunction)); } switch (method) { case EApiRouteType.CREATE: { await this.executeGeneratedRequestPipeline(options, targets, [EApiControllerRequestTarget.BODY]); const service = options.controller.service; const getFunction = ApiControllerGeneratedFunctionCapability.resolve(service, EApiFunctionType.GET, EApiFunctionType.GET, options.properties.entity); return await this.executeGeneratedTransaction(options, routeConfig, async () => { await ApiControllerHandleRequestRelations(options.controller, options.properties, routeConfig.relations?.request, targets.body ?? {}); const primaryKey = this.resolvePrimaryKey(targets.body, options.entityMetadata); markAfterBoundary(); const createResponse = await options.controller.service.create((targets.body ?? {})); const requestProperties = { relationLoadStrategy: routeConfig.relations?.response?.load?.relationLoadStrategy, relations: routeConfig.relations?.response?.load?.include, where: AuthorizationScopeMergeWhere({ [primaryKey.key]: createResponse[primaryKey.key] }, ApiControllerGeneratedSecuritySnapshot.createMutableScopeWhere(authorizationDecision)), }; return await ApiControllerGeneratedReadScopeStorage.run(EApiFunctionType.GET, requestProperties, requestProperties.where, async () => await Reflect.apply(getFunction, service, [requestProperties])); }); } case EApiRouteType.DELETE: { await this.executeGeneratedRequestPipeline(options, targets, [EApiControllerRequestTarget.PARAMETERS]); const service = options.controller.service; const deleteFunction = ApiControllerGeneratedFunctionCapability.resolve(service, EApiFunctionType.DELETE, EApiFunctionType.DELETE, options.properties.entity); await this.executeGeneratedTransaction(options, routeConfig, async () => { const primaryKey = this.resolvePrimaryKey(targets.parameters, options.entityMetadata); const requestCriteria = { [primaryKey.key]: primaryKey.value }; const scopedCriteria = AuthorizationScopeMergeWhere(requestCriteria, ApiControllerGeneratedSecuritySnapshot.createMutableScopeWhere(authorizationDecision)); const deleteCriteria = scopedCriteria ?? requestCriteria; markAfterBoundary(); await ApiControllerGeneratedReadScopeStorage.run(EApiFunctionType.DELETE, deleteCriteria, deleteCriteria, async () => { await Reflect.apply(deleteFunction, service, [deleteCriteria]); }); }); return undefined; } case EApiRouteType.GET: { await this.executeGeneratedRequestPipeline(options, targets, [EApiControllerRequestTarget.PARAMETERS]); const service = options.controller.service; const getFunction = ApiControllerGeneratedFunctionCapability.resolve(service, EApiFunctionType.GET, EApiFunctionType.GET, options.properties.entity); return await this.executeGeneratedTransaction(options, routeConfig, async () => { const readPlan = ApiControllerReadPlanGet(Object.getPrototypeOf(options.controller), options.methodName); const identityPlan = this.resolveIdentityPlan(options.controller, options.methodName); const primaryKey = this.resolveReadIdentity(targets.parameters, options.entityMetadata, identityPlan); const identityWhere = { [primaryKey.key]: primaryKey.value }; const routeScopedWhere = readPlan ? AuthorizationScopeMergeWhere(identityWhere, ApiControllerReadScopeWhere(targets.parameters, readPlan)) : identityWhere; const requestProperties = { relationLoadStrategy: routeConfig.relations?.response?.load?.relationLoadStrategy, relations: routeConfig.relations?.response?.load?.include, where: AuthorizationScopeMergeWhere(routeScopedWhere, ApiControllerGeneratedSecuritySnapshot.createMutableScopeWhere(authorizationDecision)), }; markAfterBoundary(); return await ApiControllerGeneratedReadScopeStorage.run(EApiFunctionType.GET, requestProperties, requestProperties.where, async () => await Reflect.apply(getFunction, service, [requestProperties])); }); } case EApiRouteType.GET_LIST: { const readPlan = ApiControllerReadPlanGet(Object.getPrototypeOf(options.controller), options.methodName); await this.executeGeneratedRequestPipeline(options, targets, readPlan ? [EApiControllerRequestTarget.PARAMETERS, EApiControllerRequestTarget.QUERY] : [EApiControllerRequestTarget.QUERY]); const query = targets.query; if (!query) { throw ErrorException("Query target is required for GET_LIST routes"); } const queryPlan = ApiControllerGetListQueryPlanGet(Object.getPrototypeOf(options.controller), options.methodName); let cursorPlan; if (queryPlan && ApiControllerGetListQueryGetPaginationMode(queryPlan) === EApiControllerGetListQueryPaginationMode.CURSOR) { const compiledPlan = queryPlan; if (!compiledPlan.order?.serverFields) { throw ErrorException("CURSOR GET_LIST compiled query plan is unavailable"); } cursorPlan = queryPlan; } const cursorRuntimeQuery = cursorPlan ? ApiControllerGetListQueryRuntime.parse(query, cursorPlan) : undefined; const pageRuntimeQuery = !cursorPlan && queryPlan ? ApiControllerGetListQueryRuntime.parse(query, queryPlan) : undefined; const runtimeQuery = cursorRuntimeQuery ?? pageRuntimeQuery; const service = options.controller.service; const getManyFunction = cursorRuntimeQuery ? ApiControllerGeneratedFunctionCapability.resolve(service, EApiFunctionType.GET_MANY, EApiFunctionType.GET_MANY, options.properties.entity) : undefined; const getListFunction = cursorRuntimeQuery ? undefined : ApiControllerGeneratedFunctionCapability.resolve(service, EApiFunctionType.GET_LIST, EApiFunctionType.GET_LIST, options.properties.entity); return await this.executeGeneratedTransaction(options, routeConfig, async (eventManager) => { const generatedQuery = query; const { limit, orderBy, orderDirection, page, ...getListQuery } = generatedQuery; const filter = runtimeQuery?.ast ? ApiControllerGetListQueryRuntime.compileWhere(runtimeQuery.ast) : ApiControllerGetListTransformFilter(runtimeQuery?.filterQuery ?? getListQuery, options.entityMetadata, routeConfig.security?.authentication?.guard); const routeScopedFilter = readPlan ? AuthorizationScopeMergeWhere(filter, ApiControllerReadScopeWhere(targets.parameters, readPlan)) : filter; const scopedFilter = AuthorizationScopeMergeWhere(routeScopedFilter, ApiControllerGeneratedSecuritySnapshot.createMutableScopeWhere(authorizationDecision)); const effectiveLimit = runtimeQuery?.limit ?? limit; const effectiveOrderBy = runtimeQuery?.orderBy ?? orderBy; const effectiveOrderDirection = runtimeQuery?.orderDirection ?? orderDirection; const effectivePage = pageRuntimeQuery?.page ?? page; if (cursorRuntimeQuery) { if (!cursorPlan || !cursorRuntimeQuery.ast || !cursorRuntimeQuery.order?.length) { throw ErrorException("Compiled CURSOR GET_LIST query plan is incomplete"); } const baseProperties = { relationLoadStrategy: routeConfig.relations?.response?.load?.relationLoadStrategy, relations: routeConfig.relations?.response?.load?.include, where: scopedFilter ?? filter, }; const routePath = options.properties.path ?? (options.properties.entity.name ? options.properties.entity.name.toLowerCase() : "UnknownResource"); const contextHash = ApiControllerGetListCursorRuntime.createContextHash(routePath, targets.parameters, cursorPlan, cursorRuntimeQuery.ast, cursorRuntimeQuery.order, readPlan?.signature); let direction; if (cursorRuntimeQuery.after) { direction = "after"; } else if (cursorRuntimeQuery.before) { direction = "before"; } const cursor = cursorRuntimeQuery.after ?? cursorRuntimeQuery.before; if (!getManyFunction) { throw ErrorException("Generated CURSOR GET_LIST function capability is not available"); } const serviceRepository = service.repository; if (!serviceRepository) { throw ErrorException("Repository is not available in this context"); } const activeRepository = eventManager ? eventManager.getRepository(serviceRepository.target) : serviceRepository; const validateStorageValue = ApiControllerGetListCursorDataSourceContract.createValueValidator(activeRepository, cursorPlan); return await ApiControllerGetListCursorRuntime.execute({ baseProperties, contextHash, cursor, direction, limit: effectiveLimit, onBeforeQuery: markAfterBoundary, order: cursorRuntimeQuery.order, plan: cursorPlan, run: async (properties) => await ApiControllerGeneratedReadScopeStorage.run(EApiFunctionType.GET_MANY, properties, properties.where, async () => await Reflect.apply(getManyFunction, service, [properties])), validateStorageValue, }); } if (effectivePage === undefined) { throw ErrorException("Compiled PAGE GET_LIST query plan is incomplete"); } const requestProperties = { relationLoadStrategy: routeConfig.relations?.response?.load?.relationLoadStrategy, relations: routeConfig.relations?.response?.load?.include, skip: effectiveLimit * (effectivePage - 1), take: effectiveLimit, where: scopedFilter ?? filter, }; if (effectiveOrderBy) { requestProperties.order = { [effectiveOrderBy]: effectiveOrderDirection ?? FUNCTION_API_DECORATOR_CONSTANT.DEFAULT_FILTER_ORDER_BY_DIRECTION }; } if (runtimeQuery?.order?.length) { requestProperties.order = Object.fromEntries(runtimeQuery.order.map((entry) => [entry.field, entry.direction])); } if (!getListFunction) { throw ErrorException("Generated PAGE GET_LIST function capability is not available"); } markAfterBoundary(); return await ApiControllerGeneratedReadScopeStorage.run(EApiFunctionType.GET_LIST, requestProperties, requestProperties.where, async () => await Reflect.apply(getListFunction, service, [requestProperties])); }); } } } static async executeGeneratedRequestPipeline(options, targets, targetOrder) { const routeConfig = options.properties.routes[options.method] ?? {}; const requestTargets = routeConfig.request; const transformTargets = routeConfig.request; for (const target of targetOrder) { if (target === EApiControllerRequestTarget.PARAMETERS) { ApiControllerTransformData(transformTargets, options.properties, { parameters: targets.parameters }, { authenticationRequest: targets.authenticationRequest, headers: targets.headers, ip: targets.ip }); await ApiControllerValidateRequest(requestTargets?.[EApiControllerRequestTarget.PARAMETERS], options.properties, targets.parameters ?? {}); } if (target === EApiControllerRequestTarget.QUERY) { ApiControllerTransformData(transformTargets, options.properties, { query: targets.query }, { authenticationRequest: targets.authenticationRequest, headers: targets.headers, ip: targets.ip }); await ApiControllerValidateRequest(requestTargets?.[EApiControllerRequestTarget.QUERY], options.properties, targets.query ?? {}); } if (target === EApiControllerRequestTarget.BODY) { ApiControllerTransformData(transformTargets, options.properties, { body: targets.body }, { authenticationRequest: targets.authenticationRequest, headers: targets.headers, ip: targets.ip }); await ApiControllerValidateRequest(requestTargets?.[EApiControllerRequestTarget.BODY], options.properties, (targets.body ?? {})); } } } static async executeGeneratedResponse(options, targets, entityInstance, baseData, authorizationDecision, result) { const routeConfig = options.properties.routes[options.method] ?? {}; const queryPlan = options.method === EApiRouteType.GET_LIST ? ApiControllerGetListQueryPlanGet(Object.getPrototypeOf(options.controller), options.methodName) : undefined; let cursorOrderFields; let cursorResponseSignature; if (ApiControllerGetListQueryGetPaginationMode(queryPlan) === EApiControllerGetListQueryPaginationMode.CURSOR && queryPlan) { const protectedFields = ApiControllerGetListQueryResolveCursorOrderFields(queryPlan.order); if (protectedFields.length === 0) { throw ErrorException("CURSOR GET_LIST response requires its effective order"); } cursorOrderFields = protectedFields; cursorResponseSignature = ApiControllerGetListCursorRuntime.createResponseInvariantSignature(result, protectedFields); } const assertCursorResponseInvariant = (value) => { if (cursorOrderFields && cursorResponseSignature) { ApiControllerGetListCursorRuntime.assertResponseInvariant(value, cursorOrderFields, cursorResponseSignature); } }; const responseTarget = { response: result }; const responseResource = responseTarget.response; ApiControllerTransformData(routeConfig.response, options.properties, responseTarget, { authenticationRequest: targets.authenticationRequest, headers: targets.headers, ip: targets.ip }); assertCursorResponseInvariant(responseTarget.response); const afterContext = { DATA: { ...baseData, authenticationRequest: targets.authenticationRequest, authorizationDecision: ApiControllerGeneratedSecuritySnapshot.createSubscriberView(authorizationDecision, responseResource), body: targets.body, headers: targets.headers, ip: targets.ip, parameters: targets.parameters, query: targets.query }, ENTITY: responseTarget.response, result: responseTarget.response, ROUTE_TYPE: options.method, }; const afterResult = await ApiSubscriberExecutor.executeRouteSubscribers(options.controller.constructor, (responseTarget.response ?? entityInstance), options.method, EApiSubscriberOnType.AFTER, afterContext); const finalResponse = afterResult ?? responseTarget.response; assertCursorResponseInvariant(finalResponse); const transformedResponse = await AuthorizationDecisionApplyResult(ApiControllerGeneratedSecuritySnapshot.withResource(authorizationDecision, finalResponse), finalResponse); assertCursorResponseInvariant(transformedResponse); const identityPlan = this.resolveIdentityPlan(options.controller, options.methodName); const dto = ApiControllerGetDtoWithReadPlan(options.properties, options.entityMetadata, options.method, EApiDtoType.RESPONSE, routeConfig, queryPlan, undefined, identityPlan); const projectedResponse = ApiRouteProjectRelationResponse(routeConfig.relations?.response, transformedResponse); assertCursorResponseInvariant(projectedResponse); const serializedResponse = ApiControllerSerializeRouteResponse(routeConfig, dto, projectedResponse); if (cursorOrderFields && cursorResponseSignature) { const wireResponse = instanceToPlain(serializedResponse); assertCursorResponseInvariant(wireResponse); return wireResponse; } return serializedResponse; } static async executeGeneratedTransaction(options, routeConfig, callback) { const mode = routeConfig.transaction?.mode ?? EApiFunctionTransactionMode.SUPPORTS; const activeEventManager = ApiFunctionContextStorage.getTransactionRegistry() ? ApiFunctionContextStorage.getEventManager() : undefined; if (mode === EApiFunctionTransactionMode.NONE && activeEventManager) { throw ErrorException("ApiRoute generated transaction mode NONE cannot run inside an active transaction"); } if (mode === EApiFunctionTransactionMode.MANDATORY && !activeEventManager) { throw ErrorException("ApiRoute generated transaction mode MANDATORY requires an active transaction"); } if (mode === EApiFunctionTransactionMode.REQUIRED && !activeEventManager) { const repository = options.controller.service.repository; if (!repository) { throw ErrorException("Repository is not available in this context"); } return await ApiFunctionTransactionRuntime.execute({ callback, dataSource: repository.manager.connection, owner: { entityName: options.properties.entity.name ?? options.entityMetadata.tableName, kind: EApiFunctionTransactionOwnerKind.ROUTE, methodName: options.methodName, routeType: options.method, }, }); } return await callback(activeEventManager); } static async executeGeneratedUpdateOperation(options, targets, authorizationDecision, routeConfig, markAfterBoundary, service, updateFunction, getFunction) { const responseLoad = routeConfig.relations?.response?.load; await ApiControllerHandleRequestRelations(options.controller, options.properties, routeConfig.relations?.request, targets.body ?? {}); const primaryKey = this.resolvePrimaryKey(targets.parameters, options.entityMetadata); const requestCriteria = { [primaryKey.key]: primaryKey.value }; const scopedCriteria = AuthorizationScopeMergeWhere(requestCriteria, ApiControllerGeneratedSecuritySnapshot.createMutableScopeWhere(authorizationDecision)); const updateCriteria = scopedCriteria ?? requestCriteria; markAfterBoundary(); const updateResponse = await ApiControllerGeneratedReadScopeStorage.run(EApiFunctionType.UPDATE, updateCriteria, updateCriteria, async () => await Reflect.apply(updateFunction, service, [updateCriteria, (targets.body ?? {})])); if (!responseLoad || !getFunction) { return updateResponse; } const requestProperties = { relationLoadStrategy: responseLoad.relationLoadStrategy, relations: responseLoad.include, where: AuthorizationScopeMergeWhere({ [primaryKey.key]: updateResponse[primaryKey.key] }, ApiControllerGeneratedSecuritySnapshot.createMutableScopeWhere(authorizationDecision)), }; return await ApiControllerGeneratedReadScopeStorage.run(EApiFunctionType.GET, requestProperties, requestProperties.where, async () => await Reflect.apply(getFunction, service, [requestProperties])); } static async executeRequestTargets(metadata, runtimeProperties, request, contextData) { const controllerProperties = { entity: metadata.resource.entity}; const requestTargets = runtimeProperties.request; ApiControllerTransformData(runtimeProperties.request, controllerProperties, { parameters: request.params }, contextData); await ApiControllerValidateRequest(requestTargets?.[EApiControllerRequestTarget.PARAMETERS], controllerProperties, request.params ?? {}); ApiControllerTransformData(runtimeProperties.request, controllerProperties, { query: request.query }, contextData); await ApiControllerValidateRequest(requestTargets?.[EApiControllerRequestTarget.QUERY], controllerProperties, request.query ?? {}); ApiControllerTransformData(runtimeProperties.request, controllerProperties, { body: request.body }, contextData); await ApiControllerValidateRequest(requestTargets?.[EApiControllerRequestTarget.BODY], controllerProperties, request.body ?? {}); await this.executeDiscriminatedRequestBodyDto(runtimeProperties, request); } /** * Checks whether response relation loading has at least one configured include key. * @param {unknown} include - Response include map. * @returns {boolean} Whether response relation loading should run. */ static hasResponseRelationInclude(include) { return include !== null && typeof include === "object" && Object.keys(include).length > 0; } static async loadCustomResponseRelations(service, runtimeProperties, response) { if (response === null || typeof response !== "object" || !("id" in response)) { return response; } const responseId = response.id; if (responseId === undefined || responseId === null) { return response; } return await service.get({ relationLoadStrategy: runtimeProperties.relations?.response?.load?.relationLoadStrategy, relations: runtimeProperties.relations?.response?.load?.include, where: { id: responseId, }, }); } static resolveIdentityPlan(controller, methodName) { const handler = Reflect.get(controller, methodName); return typeof handler === "function" ? ApiControllerIdentityPlanGet(handler) : undefined; } static resolvePrimaryKey(source, entityMetadata) { const primaryKey = ApiControllerGetPrimaryColumn(source ?? {}, entityMetadata); if (!primaryKey) { throw ErrorException("Primary key not found in entity columns"); } return primaryKey; } static resolveReadIdentity(source, entityMetadata, identityPlan) { if (!identityPlan) { return this.resolvePrimaryKey(source, entityMetadata); } const parameters = source; if (!parameters || !Object.hasOwn(parameters, identityPlan.parameter)) { throw new BadRequestException("INVALID_PARAMETERS"); } const value = parameters[identityPlan.parameter]; if (value === undefined || value === null) { throw new BadRequestException("INVALID_PARAMETERS"); } return { key: identityPlan.field, value: value, }; } } export { ApiRouteRuntime }; //# sourceMappingURL=route-runtime.class.js.map