actionhero
Version:
The reusable, scalable, and quick node.js API server for stateless and stateful applications
370 lines (369 loc) • 15.5 kB
JavaScript
"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;