fatmatto/express-toolkit

View on GitHub
src/router.js

Summary

Maintainability
C
1 day
Test Coverage
A
100%
const express = require('express')
const { asyncMiddleware } = require('./utils')
const Controller = require('./controller')
/**
 *
 * @param {Controller} resource
 * @param {String} eventName
 */
function runHooks (resource, eventName) {
  const hooks = resource.getHooks(eventName)
  if (!hooks) {
    return (req, res, next) => {
      next()
    }
  } else {
    return hooks
  }
}

/**
 * Flushes data to the http client
 * @param {*} req
 * @param {*} res
 * @param {function} next
 */
function finalize (req, res, next) {
  return res.send({ status: true, data: req.toSend })
}

/**
 * Endpoints enabled by default
 */
const defaultEndpoints = {
  find: true,
  findById: true,
  create: true,
  updateById: true,
  updateByQuery: true,
  deleteById: true,
  deleteByQuery: true,
  count: true,
  patchById: true,
  replaceById: true
}

/**
 * Builds an expressjs router instance
 * @param {Object} config
 * @param {Object} config.controller
 * @param {Object} [config.endpoints] Map<String,Boolean>
 * @param {String} [config.id] The named route parameter to use as id. Defaults to "id"
 * @param {Object} [config.options] Express router options. As described in https://expressjs.com/en/api.html#express.router
 */
function buildRouter (config) {
  config.options = config.options || {}
  config.id = config.id || 'id'
  const router = express.Router(config.options)
  if (!(config.controller instanceof Controller)) {
    throw new Error('config.controller must be an instance of Controller')
  }

  if (Object.prototype.hasOwnProperty.call(config, 'endpoints') && typeof config.endpoints !== 'object') {
    throw new Error('config.endpoints must be an object')
  }

  const endpointsMap = Object.assign({}, defaultEndpoints, config.endpoints)

  const findByIdMiddleware = asyncMiddleware(async (req, res, next) => {
    const resource = await config.controller.findById(req.params[config.id], req.query)
    req.toSend = resource
    return next()
  })

  const createMiddleware = asyncMiddleware(async (req, res, next) => {
    const resource = await config.controller.create(req.body)
    req.toSend = resource
    return next()
  })

  const updateByIdMiddleware = asyncMiddleware(async (req, res, next) => {
    const resource = await config.controller.updateById(req.params[config.id], req.body, req.query)
    req.toSend = resource
    return next()
  })

  const updatebyQueryMiddleware = asyncMiddleware(async (req, res, next) => {
    const resource = await config.controller.updateByQuery(req.query, req.body)
    req.toSend = resource
    return next()
  })

  const deleteByIdMiddleware = asyncMiddleware(async (req, res, next) => {
    await config.controller.deleteById(req.params[config.id])
    req.toSend = null
    return next()
  })

  const deleteByQueryMiddleware = asyncMiddleware(async (req, res, next) => {
    await config.controller.deleteByQuery(req.query)
    req.toSend = null
    return next()
  })

  const countMiddleware = asyncMiddleware(async (req, res, next) => {
    const query = req.query
    const count = await config.controller.count(query)
    req.toSend = { count: count }
    return next()
  })

  const findMiddleware = asyncMiddleware(async (req, res, next) => {
    const query = req.query
    const resources = await config.controller.find(query)
    req.toSend = resources
    return next()
  })

  const patchByIdMiddleware = asyncMiddleware(async (req, res, next) => {
    const resource = await config.controller.patchById(req.params[config.id], req.body)
    req.toSend = resource
    return next()
  })

  const replaceByIdMiddleware = asyncMiddleware(async (req, res, next) => {
    const resource = await config.controller.replaceById(req.params[config.id], req.body)
    req.toSend = resource
    return next()
  })

  const endpoints = {
    count: {
      method: 'get',
      path: '/count',
      middleware: countMiddleware
    },
    find: {
      method: 'get',
      path: '/',
      middleware: findMiddleware
    },
    findById: {
      method: 'get',
      path: `/:${config.id}`,
      middleware: findByIdMiddleware
    },
    create: {
      method: 'post',
      path: '/',
      middleware: createMiddleware
    },
    updateById: {
      method: 'put',
      path: `/:${config.id}`,
      middleware: updateByIdMiddleware
    },
    updateByQuery: {
      method: 'put',
      path: '/',
      middleware: updatebyQueryMiddleware
    },
    deleteById: {
      method: 'delete',
      path: `/:${config.id}`,
      middleware: deleteByIdMiddleware
    },
    deleteByQuery: {
      method: 'delete',
      path: '/',
      middleware: deleteByQueryMiddleware
    },
    patchById: {
      method: 'patch',
      path: `/:${config.id}`,
      middleware: patchByIdMiddleware
    },
    replaceById: {
      method: 'put',
      path: `/:${config.id}/replace`,
      middleware: replaceByIdMiddleware
    }

  }

  for (let endpointName in endpoints) {
    /**
     * This is for reto-compatibility reasons, some hooks (due to a bug or a bad design decision)
     * did not match the middleware name, for example the pre:update hook is run before the updateByQuery
     * middleware. for this reason we must remap those hooks
     */
    let hookName = endpointName
    if (hookName === 'updateByQuery') {
      hookName = 'update'
    }
    if (hookName === 'deleteByQuery') {
      hookName = 'delete'
    }
    const endpoint = endpoints[endpointName]
    if (endpointsMap[endpointName]) {
      router[endpoint.method](
        endpoint.path,
        runHooks(config.controller, 'pre:*'),
        runHooks(config.controller, `pre:${hookName}`),
        endpoint.middleware,
        runHooks(config.controller, `post:${hookName}`),
        runHooks(config.controller, 'post:*'),
        runHooks(config.controller, 'pre:finalize'),
        finalize
      )
    }
  }

  return router
}

module.exports = buildRouter