src/app.js
/*
* Copyright (c) 2015 TechnologyAdvice
*/
// Deps
const express = require('express')
const bodyParser = require('body-parser')
const fork = require('child_process').fork
const path = require('path')
const _ = require('lodash')
// Setup logs
export const log = require('bristol')
log.addTarget('console').withFormatter('commonInfoModel')
log.addTransform((elem) => {
delete elem.file
delete elem.line
return elem
})
// Default path to lambda runner
let runner = path.resolve(__dirname, './runner')
// Import router
import { loadSchema, initRoutes } from './router'
import { parseRequestParams } from './util'
/**
* Allows overriding default runner script
* @param {String} runnerPath Path to the runner module
*/
export const setRunner = (runnerPath) => runner = path.resolve(runnerPath)
/**
* Default config object
* @property config
* @attribute {String} lambdas The path to the lambdas directory
* @attribute {String} schema The path to the Gateway YAML config
* @attribute {Number} port The port for the HTTP service
* @attribute {String} apiPath The request path for the api
* @attribute {Boolean} log Show or repress console output
*/
export let config = {
lambdas: './lambdas',
schema: './gateway.yml',
port: 8181,
apiPath: '/api',
log: true,
cors: {
origin: '*',
methods: 'GET,PUT,POST,DELETE,OPTIONS',
headers: 'Content-Type, Authorization, Content-Length, X-Requested-With'
}
}
// Express setup
export const service = express()
// CORS
export const setCORS = () => {
service.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', config.cors.origin)
res.header('Access-Control-Allow-Methods', config.cors.methods)
res.header('Access-Control-Allow-Headers', config.cors.headers)
if (req.method === 'OPTIONS') {
res.send(200)
} else {
next()
}
})
}
// Body parser
service.use(bodyParser.json())
/**
* Calls log output method
* @param {String} type Type of log message to write
* @param {String|Object} msg Message body of log
*/
export const procLog = (type, ...msg) => {
/* istanbul ignore if */
if (config.log) log[type](msg[0], msg[1])
}
/**
* Parses response body for error code
* @param {String} output Output from context.fail
*/
export const parseErrorCode = (output) => {
const code = output.toString().replace(/^Error: ([1-5]\d\d).+$/, (i, match) => match)
if (code > 100 && code < 600) {
// Return specific code with stripped message
return {
code: parseInt(code, 10),
output: output.replace(`Error: ${code}`, '').trim()
}
}
// Return generic 500 with original message
return {
code: 500,
output: code
}
}
/**
* Handles response from forked lambda runner procs
* @param {Object} msg Message object from child proc
* @param {Object} [res] Express response object
*/
export const procResponse = (msg, res) => {
switch (msg.type) {
case 'metric': procLog('info', 'Lambda Processed', msg.output); break
case 'debug': procLog('info', 'Lambda Debug', msg.output); break
case 'success': res.status(200).send(msg.output); break
case 'error':
const err = parseErrorCode(msg.output)
res.status(err.code).send(err.output)
break
default: procLog('error', 'Missing response type')
}
}
/**
* Parses the properties from the template and then calls `parseRequestParams`
* to align variable properties with their template keys
* @param {Object} req The request object
* @param {Object} template The gateway template
* @returns {Object} the full event to be passed to the Lambda
*/
export const parseRequest = (req, template) => {
let tmpBody = {}
for (let prop in template) {
/* istanbul ignore else */
if ({}.hasOwnProperty.call(template, prop)) {
tmpBody[prop] = parseRequestParams(template[prop], req)
}
}
return tmpBody
}
/**
* Builds the `event` payload with the request body and then forks a new
* runner process to the requested lambda. Awaits messaging from the lambda
* to return response payload and display log information
* @param {String} lambda The lambda to run
* @param {Object} template The gateway template
* @param {Object} req Express req object
* @param {Object} res Express res object
*/
export const runLambda = (lambda, template, req, res) => {
// Parse body against template
const body = parseRequest(req, template)
// Build event by extending body with params
const event = JSON.stringify(_.extend(body, req.params))
// Build env to match current envirnment, add lambdas and event
const env = _.extend({ lambdas: config.lambdas, event }, process.env)
// Execute lambda
fork(runner, [ lambda ], { env }).on('message', (msg) => procResponse(msg, res))
}
/**
* Combines the default config with any passed to init and overrides (lastly)
* if there are any environment variables set
* @param {Object} [cfg] The config passed through init
*/
export const buildConfig = (cfg) => {
// Against defaults
_.extend(config, cfg)
// Against env vars
for (let prop in config) {
/* istanbul ignore else */
if ({}.hasOwnProperty.call(config, prop)) {
let envVar = process.env['GL_' + prop.toUpperCase()]
if (envVar) config[prop] = envVar
}
}
// Apply config to CORS
setCORS()
}
/**
* Initialize the service by building the config, loading the (YAML) Gateway
* API configuration and then initializing routes on Express and finally
* starting the service.
* @param {Object} [config] The main service configuration
*/
export const init = (cfg) => {
// Setup config
buildConfig(cfg)
// Load schema into router
loadSchema(config.schema)
// Initialize all routes from gateway schema
initRoutes()
// Starts service
service.listen(config.port, () => {
procLog('info', 'Service running', { port: config.port })
})
}