UNPKG

actionhero

Version:

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

428 lines (370 loc) 12.8 kB
import { Connection } from "./connection"; import { Action } from "./action"; import { config } from "./../modules/config"; import { log } from "../modules/log"; import { utils } from "../modules/utils"; import * as dotProp from "dot-prop"; import { api } from "../index"; export class ActionProcessor<ActionClass extends Action> { connection: Connection; action: ActionClass["name"]; toProcess: boolean; toRender: boolean; messageId: number | string; params: { action: string; apiVersion: string | number; [key: string]: any; }; // params: ActionClass["inputs"]; missingParams: Array<string>; validatorErrors: Array<string | Error>; actionStartTime: number; actionTemplate: ActionClass; working: boolean; response: { [key: string]: any; }; duration: number; actionStatus: string | Error; // allow for setting of any value via middleware session: any; constructor(connection: 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 = {}; } private incrementTotalActions(count = 1) { this.connection.totalActions = this.connection.totalActions + count; } private incrementPendingActions(count = 1) { this.connection.pendingActions = this.connection.pendingActions + count; if (this.connection.pendingActions < 0) { this.connection.pendingActions = 0; } } getPendingActionCount() { return this.connection.pendingActions; } private async completeAction(status?: string | Error) { let error = null; this.actionStatus = String(status); if (status instanceof Error) { error = typeof config.errors.genericError === "function" ? await config.errors.genericError(this, status) : status; } else if (status === "server_shutting_down") { error = await config.errors.serverShuttingDown(this); } else if (status === "too_many_requests") { error = await config.errors.tooManyPendingActions(this); } else if (status === "unknown_action") { error = await config.errors.unknownAction(this); } else if (status === "unsupported_server_type") { error = await config.errors.unsupportedServerType(this); } else if (status === "missing_params") { error = await config.errors.missingParams(this, this.missingParams); } else if (status === "validator_errors") { error = await config.errors.invalidParams(this, this.validatorErrors); } else if (status) { error = status; } if (error && 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)) { this.response = error.toString(); } else { this.response.error = error; } } this.incrementPendingActions(-1); this.duration = new Date().getTime() - this.actionStartTime; this.working = false; this.logAndReportAction(error); return this; } private logAndReportAction(error) { let logLevel = "info"; if (this.actionTemplate && this.actionTemplate.logLevel) { logLevel = this.actionTemplate.logLevel; } const filteredParams = utils.filterObjectForLogging(this.params); let logLine = { to: this.connection.remoteIP, action: this.action, params: JSON.stringify(filteredParams), duration: this.duration, error: "", response: undefined, }; let filteredResponse; if (config.general.enableResponseLogging) { filteredResponse = utils.filterResponseForLogging(this.response); logLine.response = JSON.stringify(filteredResponse); } if (error) { let errorFields; const formatErrorLogLine = config.errors.serializers.actionProcessor || this.applyDefaultErrorLogLineFormat; ({ logLevel = "error", errorFields } = formatErrorLogLine(error)); logLine = { ...logLine, ...errorFields }; } log(`[ action @ ${this.connection.type} ]`, logLevel, logLine); if (error) api.exceptionHandlers.action(error, logLine); } private applyDefaultErrorLogLineFormat(error) { const errorFields: { error: string } = { 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)) .forEach((prop) => (errorFields[prop] = error[prop])); } else { try { errorFields.error = JSON.stringify(error); } catch (e) { errorFields.error = String(error); } } return { errorFields }; } private async preProcessAction() { const processorNames = 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 api.actions.middleware[name].preProcessor === "function") { await api.actions.middleware[name].preProcessor(this); } } } private async postProcessAction() { const processorNames = 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 api.actions.middleware[name].postProcessor === "function") { await api.actions.middleware[name].postProcessor(this); } } } private reduceParams(schemaKey?: string) { 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.general.disableParamScrubbing !== true) { for (const p in params) { if ( api.params.globalSafeParams.indexOf(p) < 0 && inputNames.indexOf(p) < 0 ) { delete params[p]; } } } } private prepareStringMethod(method: string): Function { 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(api, cmdParts.join(".")); } private 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(api, params[key], this); } 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(api, params[key], this); } else { const method = this.prepareStringMethod(formatter); params[key] = await method.call(api, params[key], this); } } } // 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(api, params[key], this); } else { const method = this.prepareStringMethod(validator); validatorResponse = await method.call(api, params[key], this); } // 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.general.missingParamChecks.indexOf(params[key]) >= 0) { let missingKey = key; if (schemaKey) { missingKey = `${schemaKey}.${missingKey}`; } this.missingParams.push(missingKey); } } } private async validateParams(schemaKey?: string) { 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?: string, apiVersion = this.params.apiVersion ) { this.actionStartTime = new Date().getTime(); this.working = true; this.incrementTotalActions(); this.incrementPendingActions(); this.action = actionName || this.params.action; if (api.actions.versions[this.action]) { if (!apiVersion) { apiVersion = api.actions.versions[this.action][ api.actions.versions[this.action].length - 1 ]; } //@ts-ignore this.actionTemplate = 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 (api.running !== true) { return this.completeAction("server_shutting_down"); } if (this.getPendingActionCount() > config.general.simultaneousActions) { return this.completeAction("too_many_requests"); } if (!this.action || !this.actionTemplate) { return this.completeAction("unknown_action"); } if ( this.actionTemplate.blockedConnectionTypes && this.actionTemplate.blockedConnectionTypes.indexOf( this.connection.type ) >= 0 ) { return this.completeAction("unsupported_server_type"); } return this.runAction(); } private 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(error); } if (this.missingParams.length > 0) { return this.completeAction("missing_params"); } if (this.validatorErrors.length > 0) { return this.completeAction("validator_errors"); } 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(); } catch (error) { return this.completeAction(error); } } else { return this.completeAction(); } } }