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