UNPKG

actionhero

Version:

The reusable, scalable, and quick node.js API server for stateless and stateful applications

370 lines (369 loc) 15.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ActionProcessor = exports.ActionsStatus = void 0; const dotProp = require("dot-prop"); const index_1 = require("../index"); const log_1 = require("../modules/log"); const utils_1 = require("../modules/utils"); const config_1 = require("./../modules/config"); var ActionsStatus; (function (ActionsStatus) { ActionsStatus[ActionsStatus["Complete"] = 0] = "Complete"; ActionsStatus[ActionsStatus["GenericError"] = 1] = "GenericError"; ActionsStatus[ActionsStatus["ServerShuttingDown"] = 2] = "ServerShuttingDown"; ActionsStatus[ActionsStatus["TooManyRequests"] = 3] = "TooManyRequests"; ActionsStatus[ActionsStatus["UnknownAction"] = 4] = "UnknownAction"; ActionsStatus[ActionsStatus["UnsupportedServerType"] = 5] = "UnsupportedServerType"; ActionsStatus[ActionsStatus["MissingParams"] = 6] = "MissingParams"; ActionsStatus[ActionsStatus["ValidatorErrors"] = 7] = "ValidatorErrors"; })(ActionsStatus || (exports.ActionsStatus = ActionsStatus = {})); class ActionProcessor { constructor(connection) { this.connection = connection; this.action = null; this.toProcess = true; this.toRender = true; this.messageId = connection.messageId || 0; this.params = Object.assign({ action: null, apiVersion: null }, connection.params); this.missingParams = []; this.validatorErrors = []; this.actionStartTime = null; this.actionTemplate = null; this.working = false; this.response = {}; this.duration = null; this.actionStatus = null; this.session = {}; } incrementTotalActions(count = 1) { this.connection.totalActions = this.connection.totalActions + count; } incrementPendingActions(count = 1) { this.connection.pendingActions = this.connection.pendingActions + count; if (this.connection.pendingActions < 0) { this.connection.pendingActions = 0; } } getPendingActionCount() { return this.connection.pendingActions; } async completeAction(status, _error) { let error = null; this.actionStatus = status; if (status === ActionsStatus.GenericError) { error = typeof config_1.config.errors.genericError === "function" ? await config_1.config.errors.genericError(this, _error) : _error; } else if (status === ActionsStatus.ServerShuttingDown) { error = await config_1.config.errors.serverShuttingDown(this); } else if (status === ActionsStatus.TooManyRequests) { error = await config_1.config.errors.tooManyPendingActions(this); } else if (status === ActionsStatus.UnknownAction) { error = await config_1.config.errors.unknownAction(this); } else if (status === ActionsStatus.UnsupportedServerType) { error = await config_1.config.errors.unsupportedServerType(this); } else if (status === ActionsStatus.MissingParams) { error = await config_1.config.errors.missingParams(this, this.missingParams); } else if (status === ActionsStatus.ValidatorErrors) { error = await config_1.config.errors.invalidParams(this, this.validatorErrors); } else if (status) { error = _error; } if (typeof error === "string") error = new Error(error); if (error && (typeof this.response === "string" || !this.response.error)) { if (typeof this.response === "string" || Array.isArray(this.response)) { //@ts-ignore this.response = error.toString(); } else { this.response.error = error; } } this.incrementPendingActions(-1); this.duration = new Date().getTime() - this.actionStartTime; this.working = false; this.logAndReportAction(status, error); return this; } logAndReportAction(status, error) { const { type, rawConnection } = this.connection; let logLevel = "info"; if (this.actionTemplate && this.actionTemplate.logLevel) { logLevel = this.actionTemplate.logLevel; } const filteredParams = utils_1.utils.filterObjectForLogging(this.params); let logLine = { to: this.connection.remoteIP, action: this.action, params: JSON.stringify(filteredParams), duration: this.duration, method: type === "web" ? rawConnection.method : undefined, pathname: type === "web" ? rawConnection.parsedURL.pathname : undefined, error: "", response: undefined, }; if (config_1.config.general.enableResponseLogging) { logLine.response = JSON.stringify(utils_1.utils.filterResponseForLogging(this.response)); } if (error) { let errorFields; const formatErrorLogLine = config_1.config.errors.serializers.actionProcessor || this.applyDefaultErrorLogLineFormat; ({ logLevel = "error", errorFields } = formatErrorLogLine(error)); logLine = { ...logLine, ...errorFields }; } (0, log_1.log)(`[ action @ ${this.connection.type} ]`, logLevel, logLine); if (error && (status !== ActionsStatus.UnknownAction || config_1.config.errors.reportUnknownActions)) { index_1.api.exceptionHandlers.action(error, logLine); } } applyDefaultErrorLogLineFormat(error) { const logLevel = "error"; const errorFields = { error: null }; if (error instanceof Error) { errorFields.error = error.toString(); Object.getOwnPropertyNames(error) .filter((prop) => prop !== "message") .sort((a, b) => (a === "stack" || b === "stack" ? -1 : 1)) //@ts-ignore .forEach((prop) => (errorFields[prop] = error[prop])); } else { try { errorFields.error = JSON.stringify(error); } catch (e) { errorFields.error = String(error); } } return { errorFields, logLevel }; } async preProcessAction() { const processorNames = index_1.api.actions.globalMiddleware.slice(0); if (this.actionTemplate.middleware) { this.actionTemplate.middleware.forEach(function (m) { processorNames.push(m); }); } for (const i in processorNames) { const name = processorNames[i]; if (typeof index_1.api.actions.middleware[name].preProcessor === "function") { await index_1.api.actions.middleware[name].preProcessor(this); } } } async postProcessAction() { const processorNames = index_1.api.actions.globalMiddleware.slice(0); if (this.actionTemplate.middleware) { this.actionTemplate.middleware.forEach((m) => { processorNames.push(m); }); } for (const i in processorNames) { const name = processorNames[i]; if (typeof index_1.api.actions.middleware[name].postProcessor === "function") { await index_1.api.actions.middleware[name].postProcessor(this); } } } reduceParams(schemaKey) { let inputs = this.actionTemplate.inputs || {}; let params = this.params; if (schemaKey) { inputs = this.actionTemplate.inputs[schemaKey].schema; params = this.params[schemaKey]; } const inputNames = Object.keys(inputs) || []; if (config_1.config.general.disableParamScrubbing !== true) { for (const p in params) { if (index_1.api.params.globalSafeParams.indexOf(p) < 0 && inputNames.indexOf(p) < 0) { delete params[p]; } } } } prepareStringMethod(method) { const cmdParts = method.split("."); const cmd = cmdParts.shift(); if (cmd !== "api") { throw new Error("cannot operate on a method outside of the api object"); } return dotProp.get(index_1.api, cmdParts.join(".")); } async validateParam(props, params, key, schemaKey) { // default if (params[key] === undefined && props.default !== undefined) { if (typeof props.default === "function") { params[key] = await props.default.call(this, params[key]); } else { params[key] = props.default; } } // formatter if (params[key] !== undefined && props.formatter !== undefined) { if (!Array.isArray(props.formatter)) { props.formatter = [props.formatter]; } for (const i in props.formatter) { const formatter = props.formatter[i]; if (typeof formatter === "function") { params[key] = await formatter.call(this, params[key], key); } else { const method = this.prepareStringMethod(formatter); params[key] = await method.call(this, params[key], key); } } } // validator if (params[key] !== undefined && props.validator !== undefined) { if (!Array.isArray(props.validator)) { props.validator = [props.validator]; } for (const j in props.validator) { const validator = props.validator[j]; let validatorResponse; try { if (typeof validator === "function") { validatorResponse = await validator.call(this, params[key], key); } else { const method = this.prepareStringMethod(validator); validatorResponse = await method.call(this, params[key], key); } // validator function returned nothing; assume param is OK if (validatorResponse === null || validatorResponse === undefined) { return; } // validator returned something that was not `true` if (validatorResponse !== true) { if (validatorResponse === false) { this.validatorErrors.push(new Error(`Input for parameter "${key}" failed validation!`)); } else { this.validatorErrors.push(validatorResponse); } } } catch (error) { // validator threw an error this.validatorErrors.push(error); } } } // required if (props.required === true) { if (config_1.config.general.missingParamChecks.indexOf(params[key]) >= 0) { let missingKey = key; if (schemaKey) { missingKey = `${schemaKey}.${missingKey}`; } this.missingParams.push(missingKey); } } } async validateParams(schemaKey) { let inputs = this.actionTemplate.inputs || {}; let params = this.params; if (schemaKey) { inputs = this.actionTemplate.inputs[schemaKey].schema; params = this.params[schemaKey]; } for (const key in inputs) { const props = inputs[key]; await this.validateParam(props, params, key, schemaKey); if (props.schema && params[key]) { this.reduceParams(key); await this.validateParams(key); } } } lockParams() { this.params = Object.freeze(this.params); } async processAction(actionName, apiVersion = this.params.apiVersion) { this.actionStartTime = new Date().getTime(); this.working = true; this.incrementTotalActions(); this.incrementPendingActions(); this.action = actionName || this.params.action; if (index_1.api.actions.versions[this.action]) { if (!apiVersion) { apiVersion = index_1.api.actions.versions[this.action][index_1.api.actions.versions[this.action].length - 1]; } //@ts-ignore this.actionTemplate = index_1.api.actions.actions[this.action][apiVersion]; // send back the version we use to send in the api response if (!this.params.apiVersion) this.params.apiVersion = apiVersion; } if (index_1.api.running !== true) { return this.completeAction(ActionsStatus.ServerShuttingDown); } if (this.getPendingActionCount() > config_1.config.general.simultaneousActions) { return this.completeAction(ActionsStatus.TooManyRequests); } if (!this.action || !this.actionTemplate) { return this.completeAction(ActionsStatus.UnknownAction); } if (this.actionTemplate.blockedConnectionTypes && this.actionTemplate.blockedConnectionTypes.indexOf(this.connection.type) >= 0) { return this.completeAction(ActionsStatus.UnsupportedServerType); } return this.runAction(); } async runAction() { try { const preProcessResponse = await this.preProcessAction(); if (preProcessResponse !== undefined && preProcessResponse !== null) { Object.assign(this.response, preProcessResponse); } await this.reduceParams(); await this.validateParams(); this.lockParams(); } catch (error) { return this.completeAction(ActionsStatus.GenericError, error); } if (this.missingParams.length > 0) { return this.completeAction(ActionsStatus.MissingParams); } if (this.validatorErrors.length > 0) { return this.completeAction(ActionsStatus.ValidatorErrors); } if (this.toProcess === true) { try { const actionResponse = await this.actionTemplate.run(this); if (actionResponse !== undefined && actionResponse !== null) { Object.assign(this.response, actionResponse); } const postProcessResponse = await this.postProcessAction(); if (postProcessResponse !== undefined && postProcessResponse !== null) { Object.assign(this.response, postProcessResponse); } return this.completeAction(ActionsStatus.Complete); } catch (error) { return this.completeAction(ActionsStatus.GenericError, error); } } else { return this.completeAction(ActionsStatus.Complete); } } } exports.ActionProcessor = ActionProcessor;