UNPKG

@vara/custom-logic-sdk

Version:

Server Side JavaScript SDK for Custom Business Logic

275 lines (222 loc) 10.3 kB
/** * 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;