/**
* @module libflitter/routing/RoutingUnit
*/
const CanonicalUnit = require('../canon/CanonicalUnit')
const ResponseSystemMiddleware = require('./ResponseSystemMiddleware')
const express = require('express')
const StopError = require('../errors/StopError')
const FatalError = require('../errors/FatalError')
const SoftError = require('../errors/SoftError')
/**
* Unit to load, parse, and manage router definitions.
* @extends module:libflitter/canon/CanonicalUnit~CanonicalUnit
*/
class RoutingUnit extends CanonicalUnit {
/**
* Defines the services required by this unit.
* @returns {Array<string>}
*/
static get services() {
return [...super.services, 'output', 'controllers', 'middlewares']
}
/**
* Gets the name of the service provided by this unit: 'routers'
* @returns {string} - 'routers'
*/
static get name() {
return 'routers'
}
/**
* Instantiate the unit.
* @param {string} [base_directory = './app/routing/routers']
*/
constructor(base_directory = './app/routing/routers') {
super(base_directory)
/**
* The canonical name of the item.
* @type {string} = 'router'
*/
this.canonical_item = 'router'
/**
* The file extension of the canonical item files.
* @type {string} - '.router.js'
*/
this.suffix = '.routes.js'
}
/**
* Prepare a single canonical router and return the value that should be given by the resolver.
* This creates a new Express.js router and applies the appropriate middlewares.
* @param {object} info
* @param {module:libflitter/app/FlitterApp} info.app
* @param {string} info.name - the unqualified canonical name
* @param {*} info.instance - router definition schema from the file
* @returns {Promise<*>}
*/
async init_canonical_file({app, name, instance}) {
const router = express.Router()
/*
* Register router-level middleware.
*/
if ( 'middleware' in instance ){
for ( let i in instance.middleware ){
let mw_val = instance.middleware[i]
let mw_args = {}
if ( Array.isArray(mw_val) && mw_val.length > 0 && typeof mw_val[0] === 'string' ) {
if ( mw_val.length > 1 ) {
mw_args = mw_val[1]
}
mw_val = mw_val[0]
}
if ( typeof mw_val === 'string' ) {
const mw = this.canon.get(`middleware::${mw_val}`)
router.use(this.system_middleware(app, mw, mw_args))
} else {
this.output.error(`Specifying middlewares as functions is no longer supported. Prefer specifying the canonical name of the middleware. (Router: ${name})`)
throw (new SoftError('Invalid route handler. Please specify the name of the canonical resource to inject in the route.')).unit(this.constructor.name)
}
}
}
/*
* Register routes for the types we know can be handled by Express.js
*/
const valid_route_types = ['get', 'post', 'put', 'delete', 'copy', 'patch']
valid_route_types.forEach(type => {
if ( type in instance ) {
for ( let path in instance[type] ) {
let handlers = instance[type][path]
if ( !Array.isArray(handlers) ) handlers = [handlers]
const express_handlers = []
handlers.forEach(h => {
let h_args = undefined
if ( Array.isArray(h) && h.length > 0 && typeof h[0] === 'string' ) {
if ( h.length > 1 ) {
h_args = h[1]
}
h = h[0]
}
if ( typeof h === 'string' ) {
const handler = this.canon.get(h)
express_handlers.push(this.system_middleware(app, handler, h_args))
} else {
this.output.error(`Specifying route handlers as functions is no longer supported. Prefer specifying the canonical name of the handlers. (Router: ${name}, Method: ${type}, Route: ${path})`)
throw (new SoftError('Invalid route handler. Please specify the name of the canonical resource to inject in the route.')).unit(this.constructor.name)
}
})
router[type](path.startsWith('/') ? path : `/${path}`, express_handlers)
}
}
})
/*
* If a prefix is specified, use that.
* Otherwise, assume the routes are in the root '/' space.
*/
let prefix = '/'
if ('prefix' in instance) {
prefix = instance.prefix
}
app.express.use(prefix, router)
return {
prefix,
router,
schema: instance,
}
}
/**
* A helper function that returns Express middleware to redirect the request to the specified destination.
* @param {string} to - destination route to which the request should be redirected
* @returns {Function} - Express middleware
*/
redirect(to){
return (req, res) => {
this.output.info(`HTTP Redirect - ${this.request.ip}${this.request.xhr ? ' (XHR)' : ''} - ${this.request.method} ${this.request.path} to ${to}`)
return res.redirect(to)
}
}
/**
* Helper function that wraps all request handlers with Flitter system middleware.
* Allows for things like adding custom methods to the Express request/response objects.
* @param {Function} handler - the handler to call with the modified request
* @returns {Function} - an Express-compatible handler
*/
system_middleware(app, handler, args){
return async (req, res, next) => {
const RSM = ResponseSystemMiddleware
app.di().inject(RSM)
const res_mw = new RSM(app, res, req)
try {
return await handler(req, res, next, args)
} catch (e) {
e.request = req
e.status = e.status ? e.status : (req.status ? req.status : 500)
if ( e instanceof StopError || e instanceof FatalError ) {
await app.app_error(e)
}
this.output.error(`Error encountered while handling request (${req.method} ${req.path}): ${e.message}`)
return next(e)
}
}
}
}
module.exports = exports = RoutingUnit