UNPKG

@dreesq/serpent

Version:

An express wrapper for developing fast web applications

454 lines (362 loc) 10.6 kB
const {get, error, load, d, parseOptions} = require('../utils'); const {APP_PATH, MODULE_PATH, SOURCE_LOCAL, SOURCE_REMOTE} = require('../constants'); /** * Router vars * @type {{}} */ let app = {}; let actions = {}; let middleware = {}; let cache = null; let ctx = null; let plugins = {}; let build = {}; /** * Returns a list with available actions * @param req * @param res */ const list = (req, res) => { if (!cache) { cache = JSON.stringify(Object.keys(actions).reduce((all, key) => { let current = actions[key]; if (!current.visible) { return all; } return { ...all, [key]: typeof current.input === 'object' ? current.input : {} } }, {})); } res.end(cache); }; /** * Handler function * @param req * @param res */ const handle = async (req, res) => { const {i18n} = plugins; const config = ctx.get('config'); if (!Array.isArray(req.body)) { return res.status(400).json(error(i18n.translate('errors.invalidAction'))); } const runAction = async (currentAction = []) => { const [action, payload = {}] = currentAction; req.body = payload; if (!actions[action]) { return res.status(400).json(error(i18n.translate('errors.invalidAction'))); } const invokedAction = actions[action]; const middleware = invokedAction.middleware; req.name = action; let processMiddleware = (key = 0) => new Promise(resolve => { middleware[key](req, res, async error => { if (error) { return; } if (++key < middleware.length) { return processMiddleware(key); } const {handler, input, ...actionOptions} = invokedAction; await makeHandler({ handler, source: SOURCE_REMOTE, actionOptions, input })(req, res); resolve(); }); }); if (middleware.length) { return await processMiddleware(); } const { handler, input, ...actionOptions } = invokedAction; await makeHandler({ handler, input, source: SOURCE_REMOTE, actionOptions })(req, res); }; if (!config.actions.batch) { return await runAction(req.body); } const isBatchRequest = (req.body || []).every(i => Array.isArray(i)); if (!isBatchRequest) { return await runAction(req.body); } const results = []; res._json = res.json; res.status = () => { return res; }; const actionsList = req.body; await Promise.all(actionsList.map(action => new Promise(resolve => { res.json = function(result) { results.push({ [action[0]]: result }); }; req.body = action[1]; runAction(action).finally(resolve); }))); return res._json(results); }; /** * Returns an express compatible handler * @param handler * @param actionInput * @param source * @param actionOptions * @returns {Function} */ const makeHandler = ({ handler, input: actionInput, source = SOURCE_REMOTE, actionOptions = {} }) => { const isDev = process.env.NODE_ENV !== 'production'; const { input: inputPlugin, axios, db, mail, events, config, logger = console, redis, i18n, auth } = plugins; return async (req, res) => { const user = req.user ? req.user : false; const mergedInput = inputPlugin.merge(req); try { const {errors, input} = await inputPlugin.validate(mergedInput, actionInput, req.translate); if (errors) { return res.status(400).json({errors}); } if (typeof handler !== 'function') { return res.json(handler); } const makeCtx = async () => { const ctx = { req, res, input, axios, db, mail, events, config, session: req.session, t: req.translate, user, redis, i18n, logger, source, options: actionOptions, auth }; for (const name in build) { ctx[name] = await build[name](req, res, ctx); } return ctx; }; const result = await handler(await makeCtx()); if (typeof result === 'undefined') { return; } return res.json(result); } catch(e) { const status = e.status || 500; const info = { message: e.message ? e.message : e }; if (config.get('debug') || status === 500) { info.debug = { ...((e instanceof Error || e.message) ? { message: e.message, stack: e.stack } : { message: e }), source, user, input: mergedInput, }; logger.json({ ip: req && req.connection ? req.connection.remoteAddress : 'standalone', ...info.debug }, 'error'); } if (!isDev) { delete info.debug; } res.status(status).json(error(info)); } }; }; /** * Given an action name, * returns its registered handler * @param action */ exports.getAction = getAction = (action) => { return actions[action] || {}; }; /** * Fakes the request object and allows running * @param action * @param input * @param extra * @returns {Promise<*|*>} */ exports.runAction = (action, input = {}, extra = {}) => new Promise((resolve, reject) => { const {handler, ...actionOptions} = getAction(action); if (!Object.keys(action).length) { throw new Error(`Tried running unexisting action ${action}.`); } let func = makeHandler({ handler, input, source: SOURCE_LOCAL, actionOptions }); let fakeReq = { user: extra.user, translate: extra.translate, body: input }; let fakeRes = { status() { return fakeRes; }, json: resolve, end: resolve }; try { func(fakeReq, fakeRes); } catch(e) { reject(e); } }); /** * Registers an action * @param handler * @param opts */ exports.registerAction = registerAction = (handler, opts) => { let name = opts.name || (opts.route ? `${opts.route[0]}-${opts.route[1]}` : false); let enabled = typeof opts.enabled === 'undefined' ? true : opts.enabled; if (!enabled) { return; } if (!name) { throw new Error(`Action has no route nor method name.`); } /** * Replace string middleware with functions */ let middlewareList = opts.middleware || []; middlewareList.unshift('i18n'); for (const key in middlewareList) { if (!middlewareList.hasOwnProperty(key)) { continue; } const current = middlewareList[key]; if (typeof current === 'string') { let [handler, ...options] = current.split(':'); options = Array.isArray(options) ? options.join(':') : ''; if (!middleware[handler]) { continue; } if (options) { options = parseOptions(options); } middlewareList[key] = middleware[handler].length === 3 ? middleware[handler] : middleware[handler](options); } } d(`${actions[name] ? '~ action' : '+ action'} (${name})`); /** * Delete the actions cache * @type {null} */ cache = null; /** * Register route or action * @type {{handler: *, visible: boolean}} */ actions[name] = { handler, middleware: middlewareList, visible: !opts.route, ...opts }; if (opts.route) { const actionRunner = makeHandler({ handler, source: SOURCE_REMOTE, actionOptions: opts, ...(actions[name].input ? {input: actions[name].input} : {}) }); const method = opts.route[0].toLowerCase(); app && app[method].apply(app, [opts.route[1], ...middlewareList, actionRunner]); } }; /** * Initialize the router * @param context */ exports.init = async context => { ctx = context; const config = context.get('config'); app = context.get('app'); plugins = context.get('plugins'); build = context.get('build'); if (app && get(config, 'actions.handler')) { app.post(config.actions.handler, handle); } if (app && get(config, 'actions.list')) { app.get(config.actions.list, list); } /** * Load core middleware */ d('load (middlewares)'); await load(MODULE_PATH, 'middlewares', (name, handler) => { name = name.substring(0, name.lastIndexOf('.')); middleware[name] = handler; }); /** * Autoload middleware */ let middlewarePath = get(config, 'autoload.middlewares', false); middlewarePath = middlewarePath === true ? 'middlewares' : middlewarePath; if (middlewarePath) { await load(APP_PATH, middlewarePath, (name, handler) => { name = name.substring(0, name.lastIndexOf('.')); middleware[name] = handler; }); } /** * Load core actions */ await load(MODULE_PATH); /** * Autoload actions */ let actionsPath = get(config, 'autoload.actions', false); actionsPath = actionsPath === true ? 'actions' : actionsPath; if (actionsPath) { await load(APP_PATH, actionsPath); } };