ihub-framework-js
Version:
Legacy version of iHub Framework written in Javascript
367 lines (305 loc) • 12 kB
JavaScript
/* 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);
}
})
);