libflitter/routing/RoutingUnit.js

/**
 * @module libflitter/routing/RoutingUnit
 */

const rra = require('recursive-readdir-async')
const path = require('path')
const express = require('express')
const Unit = require('../Unit')
const ResponseSystemMiddleware = require('./ResponseSystemMiddleware')

/**
 * The routing unit loads route definitions from a specific directory and
 * registers them as Express routers with the underlying Express app. This
 * system requires a specific object structure in the route definition file
 * and currently supports route prefixing, GET routes, and POST routes.
 * 
 * @extends libflitter/Unit~Unit
 */
class RoutingUnit extends Unit {

    /**
     * Instantiate the unit. Resolves the path to the router directory.
     * @param {string} [router_directory = './app/routing/routers'] - the path to the router directory
     */
    constructor(router_directory = './app/routing/routers'){
        super()

        /**
         * Fully-qualified path to the router directory.
         * @name RoutingUnit#directory
         * @type {string}
         */
        this.directory = path.resolve(router_directory)
    }

    /**
     * Loads the unit. For each route definition file in {@link module:libflitter/routing/RoutingUnit~RoutingUnit#directory},
     * create an Express router and register it with the underlying Express app. Also, bind a helper function for redirecting
     * the user to the appropriate contexts. Router files are discovered recursively.
     * @param {module:libflitter/app/FlitterApp~FlitterApp} app - the Flitter app
     * @param {module:libflitter/Context~Context} context - the Unit's context
     * @returns {Promise<void>}
     */
    async go(app, context){
        
        context.bind('redirect', this.redirect)
        app.global.bind('redirect', this.redirect)

        /*
         * Get the route definitions NON-RECURSIVELY.
         */
        const files = await rra.list(this.directory)
        const definitions = {}
        const schemas = {}
        app.d.utility.log("Router directory: "+this.directory, 3)
        
        for ( let key in files ){
            if ( files[key].fullname.endsWith('.routes.js') ){
                let name = files[key].fullname.replace(this.directory, '')
                    .replace(/.routes.js/g, '')
                    .replace(/\//g, ':').substr(1)
                
                const routes = require(files[key].fullname)
                schemas[name] = Object.assign({}, routes)
                const router = express.Router()

                // TODO throw error if path is missing preceding slash

                /*
                 * Register router-level middleware.
                 */
                if ( 'middleware' in routes ){
                    for ( let i in routes.middleware ){
                        router.use( routes.middleware[i] )
                    }
                }

                /*
                 * Register GET method routes.
                 */
                if ('get' in routes) {
                    for (let path in routes.get) {

                        /*
                         * If the specified handler is an array, register multiple handlers
                         * as Express middleware.
                         */
                        if ( Array.isArray(routes.get[path]) ){
                            const handler = routes.get[path][routes.get[path].length-1]
                            routes.get[path].splice(-1,1)

                            router.get(path, routes.get[path].map(mw => {
                                return this.system_middleware(app, mw)
                            }), this.system_middleware(app, handler))
                        }

                        /*
                         * Otherwise, just register the specified handler.
                         */
                        else {
                            router.get(path, this.system_middleware(app, routes.get[path]))
                        }
                    }
                }

                /*
                 * Register POST method routes.
                 */
                if ('post' in routes) {
                    for (let path in routes.post) {

                        /*
                         * If the specified handler is an array, register multiple handlers
                         * as Express middleware.
                         */
                        if ( Array.isArray(routes.post[path]) ){
                            const handler = routes.post[path][routes.post[path].length-1]
                            routes.post[path].splice(-1,1)

                            router.post(path, routes.post[path].map(mw => {
                                return this.system_middleware(app, mw)
                            }), this.system_middleware(app, handler))
                        }

                        /*
                         * Otherwise, just register the specified handler.
                         */
                        else {
                            router.post(path, this.system_middleware(app, routes.post[path]))
                        }
                    }
                }

                /*
                 * If a prefix is specified, use that.
                 * Otherwise, assume the routes are in the root '/' space.
                 */
                let prefix = '/'
                if ('prefix' in routes) {
                    prefix = routes.prefix
                }
                
                definitions[prefix] = {...definitions[prefix], ...{name:router}}

                /*
                 * Register the created router with the underlying Express app.
                 */
                app.express.use(prefix, router)
                app.d.utility.log(`Registered route definition file: ${files[key].fullname}`, 2)
            }
        }
        
        context.bind('definitions', definitions)
        context.bind('schemas', schemas)
    }

    /**
     * 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) => {
            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){
        return async (req, res, next) => {
            const res_mw = new ResponseSystemMiddleware(app, res, req)
            return await handler(req, res, next)
        }
    }

    /**
     * Get the name of the unit.
     * @returns {string} "routing"
     */
    name(){
        return "routing"
    }

    /**
     * Get the directories provided by this unit.
     * {@link module:libflitter/routing/RoutingUnit~RoutingUnit#directory} as "routers"
     * @returns {{routers: string}}
     */
    directories() {
        return {
            routers: this.directory
        }
    }

    /**
     * Get the templates provided by this unit. 
     * Currently, this is the "router" template generated by {@link module:libflitter/templates/router}.
     * @returns {{router: {template: (router|(function(string): string)|*), extension: string, directory: string}}}
     */
    templates() {
        return {
            router: {
                template: require('libflitter/templates/router'),
                directory: this.directory,
                extension: '.routes.js'
            }
        }
    }
}

module.exports = RoutingUnit