libflitter/routing/RoutingUnit.js

/**
 * @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