UNPKG

@escueladigital/micro

Version:

Microservice manager for nodejs with kafka

540 lines (501 loc) 14.9 kB
/* eslint-disable no-unused-vars */ /* eslint-disable no-shadow */ const Boom = require('@hapi/boom') const path = require('path') const includeAll = require('include-all') const _ = require('lodash') const chalk = require('chalk') const mongoose = require('mongoose') const Router = require('@koa/router') const log = require('./logger').logger const Reporter = require('./logger').Reporter const { name } = require(`${process.cwd()}/package.json`) const logger = log() const upMachineDate = new Date() 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, } /** * controllers - Generate the controllers */ const controllers = () => new Promise((resolve, reject) => { includeAll.optional( { dirname: path.resolve(`${process.cwd()}/api/controllers`), filter: /(.+)\.js$/, identity: false, useGlobalIdForKeyName: false, }, (e, res) => { if (e) { Reporter.report(e) return reject(e) } return resolve(res) } ) }) /** * modelsSchemas - Generates the model schemas for the api. */ const modelsSchemas = () => new Promise((resolve, reject) => { includeAll.optional( { dirname: path.resolve(`${process.cwd()}/api/schemas`), filter: /(.+)\.js/, identity: false, useGlobalIdForKeyName: false, }, (e, res) => { if (e) { Reporter.report(e) return reject(e) } return resolve(res) } ) }) /** * modelsClass - Generate the models. */ const modelsClass = () => new Promise((resolve, reject) => { includeAll.optional( { dirname: path.resolve(`${process.cwd()}/api/models`), filter: /(.+)\.js$/, identity: false, useGlobalIdForKeyName: false, }, (e, res) => { if (e) { Reporter.report(e) return reject(e) } return resolve(res) } ) }) /** * * @param {Object} ctx * @param {Object} plcs */ 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); // if (!response) return response; // } } return true } /** * * @param {Models} mdls * @param {Classes} classes * @param {DataSources} datasources */ const generateModels = (mdls, classes, datasources) => { if (_.isEmpty(mdls)) { logger.warn('No hay esquemas') return } const response = {} Object.keys(mdls).forEach(nam => { const model = mdls[nam] const modelName = _.camelCase(nam) const modelSchema = new mongoose.Schema(model.properties, schemaOptions) response[modelName] = mongoose.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 } /** * getRootRoutes - Get the base routes of the API */ const getRootRoutes = () => { const router = new Router() router.get('/', async ctx => { ctx.status = 404 return ctx.status }) router.get('/api', async ctx => { ctx.body = { res: 'API running :D' } ctx.status = 200 return ctx.status }) return router } /** * * @param {*} routes * @param {*} ctrls */ const generateRoutes = (routes, ctrls) => { if (_.isEmpty(routes)) { logger.warn('No hay rutas') return } const router = new Router() Object.keys(routes).forEach(r => { const [verb, uri] = r.split(' ') const { controller, action: act, polices } = routes[r] if (r !== 'identity' && r !== 'globalId') { if (!ctrls[controller]) { logger.error('Controlador de ruta no encontrado') return } const action = ctrls[controller][act] router[_.toLower(verb)](uri, async ctx => { try { if (polices) await validatePolices(ctx, polices) return await action(ctx) } catch (ex) { ctx.body = new Boom(ex) ctx.status = ctx.body.output.statusCode if (ctx.body.output.statusCode === 500) logger.error(`Error ${ex}`) return ctx } }) } }) return router } const config = () => new Promise((resolve, reject) => { includeAll.optional( { dirname: path.resolve(`${process.cwd()}/api/config`), filter: /(.+)\.js$/, identity: false, useGlobalIdForKeyName: false, }, (e, res) => { if (e) { return reject(e) } return resolve(res) } ) }) /** * Cargar todos los parasitos para la API */ const parasites = () => new Promise((resolve, reject) => { includeAll.optional( { dirname: path.resolve(`${process.cwd()}/api/events`), filter: /(.+)\.js$/, identity: false, useGlobalIdForKeyName: false, }, (e, res) => { if (e) { Reporter.report(e) return reject(e) } return resolve(_.omit(res, ['helper'])) } ) }) /** * Carga todos los consumidores de la API */ const consumers = () => new Promise((resolve, reject) => { includeAll.optional( { dirname: path.resolve(`${process.cwd()}/api/consumer`), filter: /(.+)\.js$/, identity: false, useGlobalIdForKeyName: false, }, (e, res) => { if (e) { Reporter.report(e) return reject(e) } return resolve(_.omit(res, ['helper'])) } ) }) const makeConsumers = async consumers => { const cons = [] _.forOwn(consumers, (value, key) => { _.forOwn(value, (iv, action) => { const req = `${process.env.NODE_ENV}.req.${name}.${key}.${action}` const res = `${process.env.NODE_ENV}.res.${name}.${key}.${action}` const micro = `${process.env.NODE_ENV}.micro.${name}.${key}.${action}` cons.push({ req, res, micro, action, key, }) }) }) return cons } /** * * @param {*} topicList - Lista de topicos para hacer de parasito. */ const loadParasites = async (parasite, pubsub) => { if (_.isEmpty(parasite)) { logger.warn('No hay parasitos') return } const elements = Object.keys(parasite) // const topics = [] logger.info(chalk.bgRed(chalk.black('*** CREATING PARASITES ***'))) for (let i = 0; i < elements.length; i += 1) { const micro = elements[i] const consumers = Object.keys(parasite[micro]) for (let j = 0; j < consumers.length; j += 1) { const consumer = consumers[j] const methods = Object.keys(parasite[micro][consumer]) for (let z = 0; z < methods.length; z += 1) { const method = methods[z] const topic = `${process.env.NODE_ENV || 'local'}.res.${micro}.${consumer}.${method}` try { await pubsub.parasite.subscribe({ topic, }) logger.info(`${topic} - ${chalk.red('PARASITE')} - subscribed`) } catch (e) { logger.warn(`${topic} - ${chalk.red('PARASITE')} - already subscribed`) } } } } const accs = {} const messages = [] await pubsub.parasite.run({ eachBatchAutoResolve: false, eachBatch: async ({ batch, resolveOffset, heartbeat, isStale, isRunning }) => { try { const topicOffset = await pubsub.admin.fetchTopicOffsets(batch.topic) for (let i = 0; i < batch.messages.length; i++) { const message = batch.messages[i] if (!isRunning() || isStale()) break const val = JSON.parse(Buffer.from(message.value).toString()) const splited = batch.topic.split('.') const micro = splited[2] const consume = splited[3] const method = splited[4] await parasite[micro][consume][method]( val, ready => { if (!ready) return if (Number(message.offset) >= Number(topicOffset[0].offset) - 1) { ready(accs[batch.topic]) delete accs[batch.topic] } }, reduce => { if (!reduce) return reduce(val, accs[batch.topic] || 0, val => (accs[batch.topic] = val)) } ) await resolveOffset(message.offset) await heartbeat() } } catch (e) { Reporter.report(e) } }, }) // await pubsub.parasite.run({ // eachMessage: async ({ topic, message }) => { // const val = JSON.parse(Buffer.from(message.value).toString()) // const splited = topic.split('.') // const micro = splited[2] // const consume = splited[3] // const method = splited[4] // try { // await parasite[micro][consume][method](val, ready) // } catch (e) { // Reporter.report(e) // } // }, // }) logger.info(chalk.bgRed(chalk.black('*** CREATING PARASITES - END ***'))) } const loadConsumers = async (consumer, pubsub, gPubsub, app, communication) => { if (_.isEmpty(consumer)) { logger.warn('No hay consumidores') return } if (!pubsub) { logger.warn('No pubsub') return } try { console.log(communication) const microTopicsT = await gPubsub.getMicroTopics(communication) console.log(microTopicsT) const reqTopics = pubsub.map(topic => ({ topic: topic.req })) const microTopics = microTopicsT.map(topic => ({ topic: topic.name })) console.log(JSON.stringify(microTopics)) const microConsumer = gPubsub.kafka.consumer({ groupId: `${name}-consumer-m-Micro-${process.env.NODE_ENV}`, }) await microConsumer.connect() const cleanMicro = microTopics.filter(topic => topic.topic.includes(`${process.env.NODE_ENV}.`)) const cleanReq = reqTopics.filter(topic => topic.topic.includes(`${process.env.NODE_ENV}.`)) console.log(JSON.stringify(cleanMicro)) logger.info(`${chalk.bgCyan(chalk.black('*** NORMAL SUBSCRIPTIONS ***'))}`) for (let i = 0; i < cleanMicro.length; i += 1) { const topic = cleanMicro[i] try { await microConsumer.subscribe(topic) logger.info(`${topic.topic} - subscribed`) } catch (e) { logger.warn(`${topic.topic} - already subscribed`) } } for (let i = 0; i < cleanReq.length; i += 1) { const topic = cleanReq[i] try { await gPubsub.consumer.subscribe(topic) logger.info(`${topic.topic} - subscribed`) } catch (e) { logger.warn(`${topic.topic} - already subscribed`) } } logger.info(`${chalk.bgCyan(chalk.black('*** NORMAL SUBSCRIPTIONS - END ***'))}`) await microConsumer.run({ eachMessage: ({ message }) => { if (message.timestamp < upMachineDate) return const val = JSON.parse(Buffer.from(message.value).toString()) const promise = gPubsub.reqResources[val.uuid] if (promise) { if (!val.data) promise.resolve(val.data) else if (val.data.isBoom) { promise.reject(val.data.message) } else { try { promise.resolve(val.data) } catch (e) { promise.reject(e.toString()) } } delete gPubsub.reqResources[val.uuid] } else { logger.error('No subscription promise') delete gPubsub.reqResources[val.uuid] } }, }) await gPubsub.consumer.run({ eachMessage: async ({ topic, partition, message }) => { if (message.timestamp < upMachineDate) return const val = JSON.parse(Buffer.from(message.value).toString()) const responseKind = Buffer.from(message.headers.responseKind).toString() const splited = topic.split('.') const consume = splited[3] const method = splited[4] const res = `${responseKind}.${name}.${consume}.${method}` try { const data = await consumer[consume][method](val) gPubsub.writeMessageToTopic( JSON.stringify({ data, uuid: val.uuid, val: val }), res, responseKind === 'micro' ? 'resmicro' : 'res' ) } catch (e) { Reporter.report(e) gPubsub.writeMessageToTopic( JSON.stringify({ data: { isBoom: true, message: _.get(e, 'message', 'No hay descripción para este error'), }, uuid: val.uuid, }), res ) } }, }) } catch (e) { Reporter.report(e) throw Boom.conflict('Error', e) } } module.exports = async (app, port) => { try { app = _.merge(app, init) app.controllers = await controllers() app.modelsClass = await modelsClass() app.modelsSchemas = await modelsSchemas() app.consumers = await consumers() app.parasites = await parasites() if (_.isEmpty(app.consumers)) { app.PubSub.parasite.on('consumer.group_join', () => { app.listen(port, '0.0.0.0') app.logger.info(`Server running on port ${port}`) app.logger.info(`ENVIROMENT: ${process.env.NODE_ENV || 'development'}`) }) } else { app.PubSub.consumer.on('consumer.group_join', () => { app.listen(port, '0.0.0.0') app.logger.info(`Server running on port ${port}`) app.logger.info(`ENVIROMENT: ${process.env.NODE_ENV || 'development'}`) }) } const buildedConsumers = await makeConsumers(app.consumers) const { communication, routes } = await config() app.routes = routes await app.PubSub.setUpProject(buildedConsumers) await loadParasites(app.parasites, app.PubSub) await loadConsumers(app.consumers, buildedConsumers, app.PubSub, app, communication) app.models = generateModels(app.modelsSchemas, app.modelsClass, app.datasources) const rootRoutes = getRootRoutes() const router = generateRoutes(app.routes, app.controllers) app.use(rootRoutes.routes()) if (router) { app.use(router.routes()) app.use( router.allowedMethods({ throw: true, notImplemented: () => new NotImplemented(), methodNotAllowed: () => new MethodNotAllowed(), }) ) } app.types = mongoose.Types } catch (e) { Reporter.report(e) logger.warn(e) } }