@carlosbajo/roket-micro
Version:
framework para microservicios con google/pubsub
285 lines (272 loc) • 8.99 kB
JavaScript
const Boom = require('boom');
const path = require('path');
const includeAll = require('include-all');
const _ = require('lodash');
const mongoose = require('mongoose');
const Router = require('koa-router');
const Jwt = require('jsonwebtoken');
const loadClass = require('mongoose-class-wrapper');
const access = require('./access');
const gLogger = require('./google-loggin');
const { name } = require(`${process.cwd()}/package.json`); // eslint-disable-line
const NotImplemented = Boom.notImplemented;
const MethodNotAllowed = Boom.methodNotAllowed;
const init = {
controllers: {},
routes: {},
modules: {},
services: {},
models: {}
};
const schemaOptions = {
toObject: {
virtuals: true
},
toJSON: {
virtuals: true
},
timestamps: true
};
const controllers = () =>
new Promise((resolve, reject) => {
includeAll.optional({
dirname: path.resolve(`${process.cwd()}/api/controllers`),
filter: /(.+)\.js$/,
}, (err, response) => {
if (err) return reject(err);
return resolve(response);
});
});
const config = () =>
new Promise((resolve, reject) => {
includeAll.optional({
dirname: path.resolve(`${process.cwd()}/api/config`),
filter: /(.+)\.js$/,
identity: false,
useGlobalIdForKeyName: false
}, (err, response) => {
if (err) return reject(err);
return resolve(response);
});
});
const modelsSchemas = () =>
new Promise((resolve, reject) => {
includeAll.optional({
dirname: path.resolve(`${process.cwd()}/api/schemas`),
filter: /(.+)\.js/,
identity: false,
useGlobalIdForKeyName: false
}, (err, response) => {
if (err) return reject(err);
return resolve(response);
});
});
const modelsClass = () =>
new Promise((resolve, reject) => {
includeAll.optional({
dirname: path.resolve(`${process.cwd()}/api/models`),
filter: /(.+)\.js$/,
identity: false,
useGlobalIdForKeyName: false
}, (err, response) => {
if (err) return reject(err);
return resolve(response);
});
});
const validatePolices = async (ctx, plcs) => {
for (let index = 0; index < plcs.length; index += 1) {
let fnName = plcs[index];
fnName = fnName.replace('fw.', '');
if (config.polices[fnName]) {
const response = await config.polices[fnName](ctx); // eslint-disable-line
if (!response) return response;
}
}
return true;
};
const generateModels = (mdls, classes, datasources) => {
const response = {};
Object.keys(mdls).forEach(name => { // eslint-disable-line
const model = mdls[name];
const modelName = _.camelCase(name);
const modelSchema = new mongoose.Schema(model.properties, schemaOptions);
if (classes[modelName]) {
modelSchema.plugin(loadClass, classes[modelName]);
}
response[modelName] = datasources(model.datasource).model(modelName, modelSchema);
if (model.softDelete) {
const modelDepName = `${modelName}DLT`;
const modelDepSchema = model.properties;
Object.keys(modelDepSchema).forEach(obj => {
if (modelDepSchema[obj].unique) delete modelDepSchema[obj].unique;
});
response[modelDepName] = datasources(model.datasource).model(modelDepName, modelDepSchema);
}
});
return response;
};
const getRootRoutes = () => {
const router = new Router();
router.get('/', async (ctx) => {
ctx.status = 404;
return ctx.status;
});
router.get('/healthz', async (ctx) => {
ctx.status = 200;
return ctx.status;
});
router.get('/_ah/health', async (ctx) => {
ctx.body = { res: 'OK' };
ctx.status = 200;
return ctx.status;
});
return router;
};
const generateRoutes = (routes, ctrls, logger) => {
const router = new Router();
Object.keys(routes).forEach(route => {
const [verb, uri] = route.split(' ');
const definition = routes[route];
if (!ctrls[definition.controller]) {
logger.error(`Controlador ${definition.controller} no encontrado`);
return;
}
const action = ctrls[definition.controller][definition.action];
router[_.toLower(verb)](uri, async (ctx) => {
try {
if (definition.polices) await validatePolices(ctx, definition.polices);
return await action(ctx);
} catch (ex) {
logger.error(ex);
ctx.body = Boom.wrap(ex);
ctx.status = ctx.body.output.statusCode;
if (ctx.body.output.statusCode == 500) ctx.errors.report(); // eslint-disable-line
return ctx;
}
});
});
return router;
};
const consumers = () => new Promise((resolve, reject) => {
includeAll.optional({
dirname: path.resolve(`${process.cwd()}/api/consumer`),
filter: /(.+)\.js$/,
identity: false,
useGlobalIdForKeyName: false
}, (err, response) => {
if (err) return reject(err);
return resolve(response);
});
});
const generateRoutesTemp = (consum, logger, pubsub) => {
const router = new Router();
const format = {};
if (!pubsub) return;
Object.keys(pubsub).forEach(async topic => {
const [type, module, method] = topic.split('.');
const rename = `POST /${type}/${module}/${method}`;
format[rename] = {
controller: pubsub[topic].consumer,
action: pubsub[topic].action,
access: pubsub[topic].access
};
});
const log = gLogger(logger);
Object.keys(format).forEach(route => {
const [verb, uri] = route.split(' ');
const definition = format[route];
if (!consum[definition.controller]) {
logger.error(`Controlador ${definition.controller} no encontrado`);
return;
}
const action = consum[definition.controller][definition.action];
router[_.toLower(verb)](uri, async (ctx) => {
try {
if (definition.polices) await validatePolices(ctx, definition.polices);
let val = ctx.request.body;
if (!_.isObject(ctx.request.body)) val = JSON.parse(val);
if (val.jwt) {
val.usr = Jwt.verify(val.jwt, process.env.JWTSECRET);
}
if (format[route].access) {
const validAccess = access(val.usr, format[route].access);
if (!validAccess) {
throw Boom.unauthorized('Acceso no autorizado');
}
}
log(route, val.usr.id, val);
return await action(ctx);
} catch (ex) {
logger.error(ex);
ctx.body = Boom.wrap(ex);
ctx.status = ctx.body.output.statusCode;
if (ctx.body.output.statusCode == 500) ctx.errors.report(); // eslint-disable-line
return ctx;
}
});
});
return router; // eslint-disable-line
};
const loadConsumers = async (consumer, pubsub, logger, gPubsub) => { // eslint-disable-line
if (!pubsub) {
logger.warn('No pubsub found');
return;
}
const log = gLogger(logger);
try {
Object.keys(pubsub).forEach(async topic => {
const googleTools = await gPubsub.loadSubscription(pubsub, topic);
const responseStream = googleTools.consumerClient;
const onMessage = message => {
message.ack();
let val = message.data;
if (!_.isObject(message.data)) val = JSON.parse(val);
if (val.jwt) {
val.usr = Jwt.verify(val.jwt, process.env.JWTSECRET);
}
if (pubsub[topic].access) {
const validAccess = access(val.usr, pubsub[topic].access);
if (!validAccess) {
const err = Boom.unauthorized('Acceso no autorizado');
googleTools.topic.publish(JSON.stringify(err)).then(res => logger.error(res));
}
}
log(topic, val.usr.id, val);
consumer[pubsub[topic].consumer][pubsub[topic].action](val);
};
const onError = error => logger.error('error on stream consumer', error);
responseStream.on('message', onMessage);
responseStream.on('error', onError);
});
} catch (e) {
throw Boom.conflict('Error', e);
}
};
module.exports = async (app) => {
try {
app = _.merge(app, init);
app.controllers = await controllers();
app.modelsClass = await modelsClass();
const cfg = await config();
app.routes = cfg.routes;
app.modelsSchemas = await modelsSchemas();
app.consumers = await consumers();
// setTimeout(() => loadConsumers(app.consumers, cfg.pubsub, app.logger, app.pubsub), 2000);
app.models = generateModels(app.modelsSchemas, app.modelsClass, app.datasources);
const rootRoutes = getRootRoutes();
const router = generateRoutes(app.routes, app.controllers, app.logger);
const routerConsumer = generateRoutesTemp(app.consumers, app.logger, cfg.pubsub);
app.use(rootRoutes.routes());
app.use(router.routes());
if (routerConsumer) app.use(routerConsumer.routes());
app.use(router.allowedMethods({
throw: true,
notImplemented: () => new NotImplemented(),
methodNotAllowed: () => new MethodNotAllowed()
}));
app.types = mongoose.Types;
} catch (exp) {
app.logger.warn(exp);
}
return app;
};