lsm/micromono

View on GitHub
lib/web/framework/express.js

Summary

Maintainability
B
5 hrs
Test Coverage
var path = require('path')
var jDad = require('jdad')
var logger = require('../../logger')('micromono:web:express')
var express = require('express')
var difference = require('lodash.difference')
var isPlainObject = require('lodash.isplainobject')

/**
 * ExpressAdapter constructor
 */
var ExpressAdapter = module.exports = function() {
  this.app = express()
  this.mapp = express()
  this.aapp = express()
}

ExpressAdapter.prototype.type = 'express'

ExpressAdapter.prototype.startHttpServer = function(port, host, serviceName, callback) {
  logger.debug('Starting http server', {
    host: host,
    port: port,
    service: serviceName
  })

  // Attach internal asset app
  this.app.use(this.aapp)
  // Attach internal middleware app
  this.app.use(this.mapp)
  // Create and listen http requests
  var server = this.app.listen(port, host, function() {
    var address = server.address()
    logger.info('Http server started', {
      service: serviceName,
      address: address
    })
    callback({
      httpHost: address.address,
      httpPort: address.port,
      httpServer: server
    })
  })
}

ExpressAdapter.prototype.attachHttpServer = function(httpServer, setHttpRequestHandler) {
  // Attach internal asset app
  this.app.use(this.aapp)
  // Attach internal middleware app
  this.app.use(this.mapp)
  setHttpRequestHandler(this.app)
}

ExpressAdapter.prototype.proxyWebsocket = function(upgradeUrl, wsProxyHandler) {
  upgradeUrl = path.join('/', upgradeUrl, '*')
  this.app.get(upgradeUrl, wsProxyHandler)
  this.app.post(upgradeUrl, wsProxyHandler)
}

/**
 * Attach a single route to express app.
 *
 * @param  {Object} route     The route definition object which has following format:
 *
 * ```javascript
 * {
 *   name: 'get::/user/:name',
 *   method: 'get',
 *   path: '/user/:name',
 *   handler: [Function],
 *   args: ['req', 'res'],
 *   middleware: null
 * }
 * ```
 *
 * @param  {Router}   router  The instance of Router.
 * @param  {Service}  service The service instance.
 * @return {ExpressAdapter}   The instance of this adpater.
 */
ExpressAdapter.prototype.attachRoutes = function(routes, service) {
  var app = this.app

  Object.keys(routes).forEach(function(routeName) {
    var route = routes[routeName]
    var method = route.method.toLowerCase()
    var routePath = route.path

    if ('param' === method)
      routePath = route.name.split('::')[1]
    logger.debug('Attach route handler', {
      service: service.name,
      method: method,
      routePath: routePath
    })

    var handler = route.handler.bind(service)

    var middlewares = []
    if (Array.isArray(route.middleware)) {
      // Only allow functions
      route.middleware.forEach(function(m) {
        if ('function' === typeof m)
          middlewares.push(m.bind(service))
      })
    }

    if (middlewares.length > 0)
      app[method](routePath, middlewares, handler)
    else
      app[method](routePath, handler)
  })

  return this
}

/**
 * Serve the static asset files accroding to service settings.
 *
 * @param  {Asset}    asset     The instance of asset.
 * @param  {Router}   [router]  The instance of Router.
 * @param  {Service}  [service] The service instance.
 * @return {ExpressAdapter}   The instance of this adpater.
 */
ExpressAdapter.prototype.serveLocalAsset = function(publicURL, publicPath, serviceName) {
  var assetApp = this.aapp

  if (!publicURL)
    throw new Error('Asset has no publicURL configured.')

  if (!publicPath)
    throw new Error('Asset has no publicPath configured.')

  publicURL.forEach(function(url) {
    logger.debug('Serve local static asset', {
      service: serviceName,
      publicPath: publicPath,
      url: url
    })
    assetApp.use(url, express.static(publicPath))
  })

  return this
}

ExpressAdapter.prototype.proxyAsset = function(assetInfo, proxyHandler, serviceName) {
  var assetApp = this.aapp
  var publicURL = assetInfo.publicURL

  publicURL.forEach(function(url) {
    logger.debug('Proxy static asset to remote', {
      service: serviceName,
      url: url
    })
    var assetUrl = path.join(url, '*')
    assetApp.get(assetUrl, proxyHandler)
  })
}

ExpressAdapter.prototype.injectAssetInfo = function(assetInfo) {
  this.app.use(function(req, res, next) {
    res.locals.asset = assetInfo
    next()
  })
}

/**
 * Use a middleware directly with framework without any modifications.
 *
 * @param  {String} url       The url which the middleware will be applied to.
 * @param  {Any} middleware   The middleware object accepts by the framework.
 * @return {ExpressAdapter}   The instance of this adpater.
 */
ExpressAdapter.prototype.useMiddleware = function(url, middleware, routes, service) {
  if (!Array.isArray(url))
    url = [url]

  var app = this.app
  var _middleware = middleware(app)

  url.forEach(function(link) {
    var method = link.method
    var mounted = false

    if ('default' === method && routes) {
      Object.keys(routes).forEach(function(routeName) {
        var _route = routes[routeName]
        // It's a router based middleware if we have exactly same url defined in route
        if (_route.path === link.path) {
          logger.debug('Attach router level middleware directly', {
            path: link.path,
            method: _route.method,
            service: service.name
          })
          app[_route.method](link.path, _middleware)
          mounted = true
        }
      })
    }

    if (false === mounted) {
      method = 'default' === method ? 'use' : method
      logger.debug('Use app level middleware directly', {
        path: link.path,
        method: method,
        service: service.name
      })
      app[method](link.path, _middleware)
    }
  })
}

/**
 * Attach a single middleware to express app.
 *
 * @param  {Object} middleware The middleware definition object which has following format:
 *
 * ```javascript
 * {
 *   // the name of the middleware
 *   name: 'auth',
 *   // relative path to the middleware
 *   path: '/account/middleware/auth',
 *   // the function for generating handler function
 *   handler: function() {
 *     ...
 *     return function(req, res, next) {...}
 *   }
 * }
 *
 * ```
 *
 * @param  {Router}   router  The instance of Router.
 * @param  {Service}  service The service instance.
 * @return {ExpressAdapter}   The instance of this adpater.
 */


ExpressAdapter.prototype.attachLocalMiddlewares = function(middlewares, service) {
  var self = this
  Object.keys(middlewares).forEach(function(mName) {
    var middleware = middlewares[mName]
    //
    self._attachLocalMiddleware(middleware, service)
  })
}

// Private function for attaching local middlewares
ExpressAdapter.prototype._attachLocalMiddleware = function(middleware, service) {
  var app = this.mapp

  middleware.handler = middleware.handler.bind(service)
  var handlerFn = middleware.handler()

  logger.debug('Attach local middleware', {
    path: middleware.path,
    service: service.name,
    middleware: middleware.name
  })

  app.use(middleware.path, function(req, res, next) {
    var semi = true

    // find out if the middleware wants to alter response
    var _writeHead = req.writeHead
    var _write = req.write
    var _end = req.end

    // record changes of `req` and `req.headers`
    var reqKeys = Object.keys(req)
    var headerKeys = Object.keys(req.headers)

    handlerFn(req, res, function(err) {
      semi = _writeHead === req.writeHead && _write === req.write && _end === req.end

      if (err) {
        logger.warn('Middleware error', {
          error: err
        })
        res.writeHead && res.writeHead(500, 'MicroMono middleware error.')
        res.end && res.end()
        return
      }

      if (semi) {
        // using a non-exists status code to indicate that the middleware
        // does not need to change the response
        res.statusCode = 103

        // we only care about properties which have been added to the `req`
        // object
        var changedReqKeys = difference(Object.keys(req), reqKeys)
        var changedHeaderKeys = difference(Object.keys(req.headers), headerKeys)

        var _req = {}
        var _headers = {}

        // Only allow value type `string`, `array` and `plain object`. 
        // But, properties or members of object and array are not checked.
        // This should be able to handle most of the cases. 
        changedReqKeys.forEach(function(key) {
          var value = req[key]
          if ('string' === typeof value || Array.isArray(value) || isPlainObject(value))
            _req[key] = value
        })

        changedHeaderKeys.forEach(function(key) {
          _headers[key] = req.headers[key]
        })

        if (Object.keys(_headers).length > 0)
          _req.headers = _headers

        if (Object.keys(_req).length > 0) {
          res.setHeader('X-MicroMono-Req', jDad.stringify(_req, {
            cycle: true
          }))
        }

        res.end()
      } else {
        // let the request go if this is a fully-remote middleware
        next()
      }
    })
  })
}