UNPKG

ihub-framework-js

Version:

Legacy version of iHub Framework written in Javascript

367 lines (305 loc) 12 kB
/* eslint-disable global-require */ /* eslint-disable import/no-dynamic-require */ /* eslint-disable no-await-in-loop */ const fs = require('fs'); const path = require('path'); const cluster = require('cluster'); const log = require('winston'); const jwt = require('jsonwebtoken'); const { intersection } = require('lodash'); const { apm } = require('../core'); // Components path, the default component path can be overwrite by the COMPONENTS_PATH environment variable const componentsPath = path.join(process.cwd(), process.env.COMPONENTS_PATH || 'components'); // Loads the components model, we are going to pass through all files inside the component model folder and require it const loadModels = require('../common/loadModels'); // Loads the components tasks and bind it to the ampqb/rabbit abstracted functions const loadTasks = (component, message) => ( new Promise((resolve, reject) => { /* Debug */ log.debug(`Loading tasks for component '${component}'`); const componentTasksPath = path.join(componentsPath, component, 'tasks.js'); if (fs.existsSync(componentTasksPath)) { const tasks = require(componentTasksPath); if (!Array.isArray(tasks)) { reject(new Error(`tasks.js file is not an array for component '${component}'`)); return; } // Load tasks tasks.forEach((task, index) => { log.debug(`Loading task '${index}:${task.queue}' for component '${component}'`); if (typeof task.queue !== 'string' || task.queue.length === 0) { reject(new Error(`Queue field for action '${index}' at component '${component}' must be a string`)); return; } if (typeof task.action !== 'function') { reject(new Error(`action field for action '${index}' at component '${component}' must be a function`)); return; } if (task.action.length !== 2) { reject(new Error(`action field for action function '${index}' at component '${component}' expects 2 arguments`)); return; } message.job(task.exchange, task.key, task.queue, task.prefetch, task.action, task.options); log.debug(`Tasks '${index}:${task.queue}' for component '${component}' loaded`); }); resolve(); } else { resolve(); } }) ); // Loads the components kafka queues and bind it to the kafka abstracted functions /** * * @param {string} component * @param {{ send: Function, sendTransational: Function, subscribe: Function, run: Function, kafka: object, createOrUpdateTopic: Function, consumer: object, }} message * @returns */ const loadKafkaTasks = (component, message) => ( new Promise(async (resolve, reject) => { /* Debug */ log.debug(`Loading tasks for component '${component}'`); const componentTasksPath = path.join(componentsPath, component, 'kafka.js'); if (fs.existsSync(componentTasksPath)) { const kafkaTopics = require(componentTasksPath); if (!Array.isArray(kafkaTopics)) { reject(new Error(`kafka.js file is not exporting an array for component '${component}'`)); return; } // Load kafka topics await Promise.all(kafkaTopics.map(async (kafkaTopic, index) => { log.debug(`Loading kafka topic '${index}:${kafkaTopic.topic}' for component '${component}'`); if (typeof kafkaTopic.topic !== 'string' || kafkaTopic.topic.length === 0) { reject(new Error(`Topic field for action '${index}' at component '${component}' must be a string`)); return; } if (typeof kafkaTopic.partitions !== 'number') { reject(new Error(`partitions field for action '${index}' at component '${component}' must be a number`)); return; } if (typeof kafkaTopic.handler !== 'function') { reject(new Error(`handler field for action '${index}' at component '${component}' must be a function`)); return; } await message.createOrUpdateTopic(kafkaTopic); await message.subscribe({ topic: kafkaTopic.topic, ...kafkaTopic.options, }); log.debug(`Topic '${index}:${kafkaTopic.topic}' for component '${component}' subscribed`); })); await message.run(kafkaTopics); log.debug(`Topics for component '${component}' are listening`); resolve(); } else { resolve(); } }) ); // Loads the components routines and bind it to the CronJob abstracted functions const loadRoutines = (component, cron) => ( new Promise((resolve, reject) => { /* Debug */ log.debug(`Loading routines for component '${component}'`); const componentTasksPath = path.join(componentsPath, component, 'routines.js'); if (fs.existsSync(componentTasksPath)) { const routines = require(componentTasksPath); if (!Array.isArray(routines)) { reject(new Error(`routines.js file is not an array for component '${component}'`)); return; } // Load routines routines.forEach((routine, index) => { // Skips routine loading for non primary ou non master worker if ((process.env.NODE_APP_INSTANCE !== '0' && process.env.NODE_APP_INSTANCE !== undefined) || (cluster.worker && cluster.worker.id !== 1)) { return; } log.debug(`Loading routine '${index}:${routine.id}' for component '${component}'`); if (typeof routine.cron !== 'string' || routine.cron.length === 0) { reject(new Error(`Cron field for action '${index}' at component '${component}' must be a string`)); return; } if (typeof routine.action !== 'function') { reject(new Error(`action field for action '${index}' at component '${component}' must be a function`)); return; } if (routine.action.length !== 1) { reject(new Error(`action field for action function '${index}' at component '${component}' expects one argument`)); return; } cron.job(routine.id, routine.action, routine.cron, routine.runOnInit); log.debug(`Routine '${index}:${routine.id}' for component '${component}' loaded`); }); resolve(); } else { resolve(); } }) ); // JWT Middleware const _403 = res => res.status(403).json({ error: { code: 403, message: 'Not authorized', }, }); // let logRequest = (req, res, next) => next(); // if (process.env.LOGS_ELASTICSEARCH_ENABLED) { // const Elasticsearch = require('elasticsearch'); // const configOpts = { // host: `${process.env.ELASTICSEARCH_HOST}:${process.env.ELASTICSEARCH_LOG_PORT}`, // log: 'error', // }; // if (process.env.ELASTICSEARCH_REQUEST_TIMEOUT) { // configOpts.requestTimeout = process.env.ELASTICSEARCH_REQUEST_TIMEOUT; // } // const client = new Elasticsearch.Client(configOpts); // logRequest = async (req, res, next) => { // if (req.method.toLowerCase() === 'get') return next(); // // TODO: Improve later // try { // await client.index({ // index: process.env.NODE_ENV === 'production' ? `request-${process.env.PROJECT_NAMESPACE}` : `request-${process.env.PROJECT_NAMESPACE}-${process.env.NODE_ENV || 'development'}`, // type: 'request', // body: { // method: req.method, // headers: req.headers, // path: req.path, // body: req.body, // user: req.user, // timestamp: new Date(), // }, // }); // } catch (e) { // log.error(e.message || e); // } // return next(); // }; // } const JWTMiddleware = (privateRoute, acl) => { if (!privateRoute) { return (req, res, next) => next(); } return (req, res, next) => { // Check if authorization is set and if it's Bearer const { authorization } = req.headers; if (!authorization || authorization.indexOf('Bearer ') === -1) return _403(res); const token = authorization.replace('Bearer ', ''); if (token.length === 0) return _403(res); return jwt.verify(token, process.env.JWT_SECRET, (error, decodedToken) => { if (error) { return _403(res); } // Check the ACL if (acl && Array.isArray(acl) && decodedToken.acl && Array.isArray(decodedToken.acl)) { // Check if valid acl object was passed // Check if the token has the prop setted as acl property if (!decodedToken.acl || decodedToken.acl.length === 0) { return _403(res); } // If the user is root, let him in! if (decodedToken.acl.includes('Root')) return next(); // Check if Root // Check the matched const matchedAcl = intersection(acl, decodedToken.acl); if (matchedAcl.length === 0) { return _403(res); } } // Register user context to APM if (apm) { apm.setUserContext({ id: decodedToken.id || null, username: decodedToken.username || decodedToken.email || null, email: decodedToken.email || null, }); } // Pass the token/user information in the request object req.user = decodedToken; return next(); }); }; }; // Load the components express routes and its controller const loadRoutes = (component, server) => ( new Promise((resolve, reject) => { const componentRoutesPath = path.join(componentsPath, component, 'routes.js'); // checks route.js file exists if (fs.existsSync(componentRoutesPath)) { const routes = require(componentRoutesPath); // checks route.js is an array if (!Array.isArray(routes)) { reject(new Error(`route.js file is not an array, component '${component}'`)); return; } /* Debug */ log.debug(`Loading routes for component '${component}'`); routes.forEach((route) => { /* Debug */ log.debug(`Loading route '${route.method}:${component}${route.path}'`); try { server[route.method](`/${component}${route.path.indexOf('/') === 0 ? route.path : `/${route.path}`}`, JWTMiddleware(route.private, route.acl), route.controller); /* Debug */ log.debug(`Route '${route.method}:${component}${route.path}' loaded'`); } catch (error) { reject(error); } }); resolve(); } }) ); module.exports = (database, tasks, routines, server, kafka) => ( new Promise(async (resolve, reject) => { try { if (!fs.existsSync(componentsPath)) { /* Debug */ log.debug('Component folder not found'); resolve(); return; } const components = fs.readdirSync(componentsPath); /* Debug */ log.debug('Loading components'); for (let i = 0; i < components.length; i += 1) { const component = components[i]; /* Debug */ log.debug(`Loading component '${component}'`); if (database) { const componentModelsPath = path.join(componentsPath, component, 'models'); loadModels(componentModelsPath); // Load component models } try { if (tasks) await loadTasks(component, tasks); // Load component tasks file } catch (error) { log.error(error.message || error); } try { if (kafka) await loadKafkaTasks(component, kafka); // Load component kafka file } catch (error) { log.error(error.message || error); } try { if (routines) await loadRoutines(component, routines); // Load component routines file } catch (error) { log.error(error.message || error); } if (server) await loadRoutes(component, server); // Load routes /* Debug */ log.debug(`Component '${component}' loaded`); } /* Debug */ log.debug(`${components.length} component(s) loaded`); resolve(); } catch (error) { reject(error); } }) );