@vara/custom-logic-sdk
Version:
Server Side JavaScript SDK for Custom Business Logic
275 lines (222 loc) • 10.3 kB
JavaScript
/**
* Created by stevenchin on 2/2/17.
*/
const _ = require('lodash');
const logSvc = require('../services/log-svc');
const protocolHandlerSvc = require('../services/protocol-handler');
const customError = require('../services/custom-error');
const errorFormatter = require('../services/error-formatter');
const ApplicationErrors = require('../constants/application-errors.json');
const { CUSTOM_LOGIC } = require('../constants/constants');
const apiFactory = require('../lib/etx-client');
const config = require('../../config/environment/env-config');
const { RESPONSE_MODES } = CUSTOM_LOGIC.FUNCTIONS;
const { PRE_HOOK, POST_HOOK } = CUSTOM_LOGIC.FUNCTIONS.TYPES;
const { InternalError, ResourceNotFoundError } = ApplicationErrors;
function setKeyToStringValue(object, key, value) {
if (_.isString(key) && _.isString(value)) {
_.set(object, key, value);
}
}
function setKeyToValue(object, key, value) {
if (_.isString(key)) {
_.set(object, key, value);
}
}
const fnExecutorSvc = {};
/**
* @throws ResourceNotFoundError - thrown when a protocol handler could not be found for the custom logic protocol specified
* Builds input parameters for custom logic functions
* @param protocolVersion {String} - custom logic protocol version to use for building the inputs for the custom logic function
* @param req {Object} - request object
* @param res {Object} - response object
* @param next {Function} - function to execute the next middleware/response handler
* @returns {{context, done}} - object containing the context object and done function
*/
fnExecutorSvc.buildClFnParams = function buildClFnParams(protocolVersion, req, res, next) {
const responseContent = {
/*
* statusCode: ..., // status code to send back to the platform
* headers: { ... }, // updated headers to send back to the platform
* query: { ... }. // updated query parameters to send back to the platform
* responseMode: '...', // directive to indicate what the platform should do its request/response body; can be set to merge or replace
* body: { ... }, // body to send back to the platform
* encoding: '...', // utf8 or base64, depending on body param
* stopProcessing: '...', // true/false - if true, return immediately to client
* customState: {...}, // keep track of custom data between hooks (pre and post)
*/
};
// get protocol handler to build the initial context object
const protocolHandler = protocolHandlerSvc.getHandler(protocolVersion);
if (!protocolHandler) {
throw customError(ResourceNotFoundError.name,
`A protocol handler for custom logic protocol version: ${protocolVersion} could not be found`);
}
const context = protocolHandler.buildClHandlerContext(req, res);
let modifiedBody = context.body;
context.log = logSvc;
context.setBody = function setBody(keyOrBody, value) {
const numOfArgs = arguments.length;
if (numOfArgs > 2) {
return this;
}
if (numOfArgs === 1) {
// key is an object, replace input with object
logSvc.info('>> updating input wholesale');
modifiedBody = keyOrBody;
} else if (numOfArgs === 2 && _.isString(keyOrBody) && keyOrBody.length > 0) {
modifiedBody = context.body || {};
// update that key/value pair in the input
logSvc.info(`>> updating input with k/v {${keyOrBody}: ${value}}`);
_.set(modifiedBody, keyOrBody, value);
}
responseContent.body = modifiedBody;
// always set the response mode flag to replace as merging is handled by updating the object in-place before returning it
responseContent.responseMode = RESPONSE_MODES.REPLACE;
return this;
};
// function to replace/update query params for original request
context.setQuery = function setQuery(keyOrQueryParams, value) {
if (_.isPlainObject(keyOrQueryParams) && !_.isEmpty(keyOrQueryParams)) {
_.set(responseContent, 'query', keyOrQueryParams);
} else if (_.isString(keyOrQueryParams) && keyOrQueryParams.length > 0) {
setKeyToValue(responseContent, `query.${keyOrQueryParams}`, value);
}
return this;
};
context.setHeader = function setHeaders(keyOrHeaders, value) {
if (_.isPlainObject(keyOrHeaders) && !_.isEmpty(keyOrHeaders)) {
_.set(responseContent, 'headers', keyOrHeaders);
} else if (_.isString(keyOrHeaders) && keyOrHeaders.length > 0) {
setKeyToStringValue(responseContent, `headers.${keyOrHeaders}`, value);
}
return this;
};
context.setHeaders = context.setHeader;
context.setStatus = function setStatus(status) {
if (_.isInteger(status) && status > 0) {
responseContent.statusCode = status;
// setting a status code for a pre-hook usually indicates the developer wants request processing on the platform to end
// however, if this was not intended, the behavior can be overridden via context.stopProcessing(false)
if (context.metadata.fnType === PRE_HOOK) {
context.stopProcessing();
}
}
return this;
};
context.stopProcessing = function stopProcessing(stop = true) {
responseContent.stopProcessing = !!stop;
return this;
};
context.getCustomState = function getCustomState() {
return context.requestContext.customState;
};
// state management; Setter is chainable and overrides existing state
context.setCustomState = function setCustomState(keyOrCustomState, value) {
const numOfArgs = arguments.length;
if (numOfArgs === 0 || numOfArgs > 2) {
return this;
}
if (numOfArgs === 1 && _.isPlainObject(keyOrCustomState)) {
context.requestContext.customState = keyOrCustomState;
} else if (_.isString(keyOrCustomState) && keyOrCustomState.length > 0) {
_.set(context.requestContext.customState, keyOrCustomState, value);
}
return this;
};
// chainable method that merges specified object with current state
context.mergeCustomState = function mergeCustomState(customState) {
Object.assign(context.requestContext.customState, customState || {});
return this;
};
context.api = apiFactory(config.TX_API_PUBLIC_URL, config.TX_API_INTERNAL_URL, config.TX_APP_ID, config.TX_CLIENT_KEY, config.TX_MASTER_KEY);
function done(err) {
const logPrefix = 'fnExecutorSvc#clFunction#done';
if (err instanceof Error) {
// when status code is not set by a cl function or the error object does not contain a status code, set it to the one used for internal errors
responseContent.statusCode = responseContent.statusCode || err.statusCode || InternalError.statusCode;
// ensure error is returned to client instead of original API response for lifecycle hooks
responseContent.responseMode = RESPONSE_MODES.REPLACE;
logSvc.logWarning(err, {}, logPrefix, `Error response received from custom logic handler: ${req.params.name};`);
responseContent.body = errorFormatter.formatError(err);
// if this is a pre-hook, ensure that we break out of execution
if (context.metadata.fnType === PRE_HOOK) {
context.stopProcessing();
}
return res.json(responseContent);
}
try {
// when status code is not set by a cl function, set it to a successful response
responseContent.statusCode = responseContent.statusCode || 200;
// make sure body is encoded properly
const { encoding, body } = protocolHandlerSvc.getEncodedBody(modifiedBody);
responseContent.encoding = encoding;
responseContent.body = body;
// pass through customState to response automatically for hooks (standalone functions don't have custom state)
const isHook = context.metadata.fnType === PRE_HOOK || context.metadata.fnType === POST_HOOK;
if (isHook) {
responseContent.customState = context.getCustomState();
}
// send response based on params updated by handler
return res.json(responseContent);
} catch (ex) {
const internalErr = customError(InternalError.name, null, { innerError: err });
logSvc.logError(internalErr, {}, logPrefix, `Failed to send response from custom logic handler: ${req.params.name};`);
return next(internalErr);
}
}
return { context, done };
};
/**
* Builds input parameters for custom logic actions
* @param req {Object} - request object
* @param res {Object} - response object
* @param next {Function} - function to execute the next middleware/response handler
* @returns {{context, done}} - object containing the context object and done function
*/
fnExecutorSvc.buildClActionParams = function buildClActionParams(req, res, next) {
const context = {
_req: req,
_res: res,
event: _.get(req, 'body.event'),
user: _.get(req, 'body.user'),
userRoles: _.get(req, 'body.userRoles'),
log: logSvc,
};
context.api = apiFactory(config.TX_API_PUBLIC_URL, config.TX_API_INTERNAL_URL, config.TX_APP_ID, config.TX_CLIENT_KEY, config.TX_MASTER_KEY);
function done(err) {
const logPrefix = 'fnExecutorSvc#clAction#done';
if (err instanceof Error) {
logSvc.logWarning(err, {}, logPrefix, `Error response received from custom logic action: ${req.params.name};`);
return next(err);
}
const successRes = {
message: `Custom logic action: ${req.params.name} successfully executed`,
};
return res.json(successRes);
}
return { context, done };
};
/**
* Executes a handler, transparently handling callback-style and promise-returning functions
* @param handler {Function} - handler function
* @param context {Object} - context object
* @param done {Function} - callback that handles sending back the actual HTTP response
* @returns {Promise} - A Promise object that resolves/rejects based on handler execution
*/
fnExecutorSvc.execHandler = function execHandler(handler, context, done) {
// wrap handler in a promise-returning function
const promise = new Promise((resolve, reject) => {
// allow non-async handler code to throw normally
const ret = handler(context, (err) => {
if (err) return reject(err);
return resolve();
});
if (ret && typeof ret.then === 'function') {
return ret.then(resolve, reject);
}
return ret;
});
return promise.then(done, done);
};
module.exports = fnExecutorSvc;