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')
const error_context = require('../errors/error_context.fn')

/**
 * 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 = {}

                try {
                    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)
                    }
                } catch (e) {
                    throw error_context(e, {
                        middleware_name: mw_val,
                        middleware_args: mw_args,
                    })
                }
            }
        }

        /*
         * 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] ) {
                    try {
                        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)
                    } catch (e) {
                        throw error_context(e, {
                            route_verb_type: type,
                            route_path: path,
                        })
                    }
                }
            }
        })

        /*
         * 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)
                }

                e = error_context(e, {
                    request_path: req.path,
                    request_query: req.query,
                    request_body: req.body,
                    request_params: req.params,
                    request_method: req.method,
                })

                this.output.error(`Error encountered while handling request (${req.method} ${req.path}): ${e.message}`)
                return next(e)
            }
        }
    }
}

module.exports = exports = RoutingUnit