libflitter/views/ViewEngineUnit.js

/**
 * @module libflitter/views/ViewEngineUnit
 */

const path = require('path')
const Unit = require('../Unit')

/**
 * The view engine unit is responsible for registering the view engine
 * with the underlying Express installation. It also defines a function
 * for instantly creating route handlers from view names. This unit sets
 * Pug as the view engine, though that would be relatively easy to alter.
 *
 * @extends module:libflitter/Unit~Unit
 */
class ViewEngineUnit extends Unit {
    /**
     * Defines the services required by this unit.
     * @returns {Array<string>}
     */
    static get services() {
        return [...super.services, 'output', 'configs', 'models']
    }

    /**
     * Get the name of the service provided by this unit: 'views'
     * @returns {string} - 'views'
     */
    static get name() {
        return 'views'
    }

    /**
     * Instantiate the unit class. Resolves the fully-qualified path to the views directory.
     * @param {string} [views_dir = './app/views'] - path to the views directory
     */
    constructor(views_dir = './app/views'){
        super()

        /**
         * The fully-qualified path to the views directory.
         * @type {string}
         */
        this.directory = path.resolve(views_dir)
    }

    /**
     * Function that modifies and returns resources passed to it.
     *
     * @typedef {Function} ViewEngineUnit~ResourceModifier
     * @function
     * @param {*} items - either a collection of Model instances, or a single Model instance to be modified
     * @returns {*} - some modified version of the items. This is usually stored to be passed to the view.
     */

    /**
     * A collection of resource specifications.
     * This is used by the {@link module:libflitter/views/ViewEngineUnit~ViewEngineUnit#view} method to retrieve resources and pass them to a view.
     *
     * @typedef {Object} ViewEngineUnit~ResourceSpec
     * @property {string} model - the Flitter canonical name of the model to retrieve
     * @property {"one" | "all"} fine - if "one", only the first instance of the model matching the criteria will be retrieved. If "all", then all instances matching the criteria will be retrieved.
     * @property {Object} criteria - Find criteria to be passed to the model's find() or findOne() functions. See the Mongoose docs for more info.
     * @property {string} name - accessor name for the resource. This will become the variable name used to access the resource from w/in the view.
     * @property {module:libflitter/views/ViewEngineUnit~ViewEngineUnit~ResourceModifier} modifier - allows the modification of the resource before it is passed to the view
     */

    /**
     * Render a view to the provided response. Passes through arguments and retrieves the specified resources for the view.
     * @param {Express/Response} response - the Express response to be served
     * @param {string} view_name - the Flitter canonical name of the view to be served
     * @param {Object} args - collection of arguments to be passed directly to the view
     * @param {module:libflitter/views/ViewEngineUnit~ViewEngineUnit~ResourceSpec[]} resource_list - array of resource specifications to be retrieved and passed to the view
     * @returns {Promise<*>}
     */
    async view(response, view_name, args = {}, resource_list = {}){
        view_name = view_name.replace(/:/g, '/')
        let resources = {}

        for ( let find_key in resource_list ){
            const find = resource_list[find_key]
            let returns;

            if ( find.find === 'one' ){
                returns = await this.models.get(find.model).findOne(find.criteria)
            }
            else {
                returns = await this.models.get(find.model).find(find.criteria)
            }
            
            if ( find.modifier ) returns = await find.modifier(returns)

            resources[find.name] = returns
        }

        args = {...args, ...resources}

        return response.render(view_name, args)
    }

    /**
     * Sends an HTTP error and renders the error view with the corresponding status code. If the status code cannot be
     * resolved to an integer, the request status will default to 400.
     * @param {Express/Response} response - the Express response to be served
     * @param {string|int} status - HTTP or error status code
     * @param {Object} params - collection of arguments to be passed to the view. The "code" key will be overwritten with the status.
     * @returns {Promise<*>}
     */
    async error(response, status, params){
        response.status((isNaN(parseInt(status)) ? 400 : parseInt(status)))
        if ( this.configs.get('server.environment') === "development" ){
            if ( !params.error ) {
                const error = new Error;
                error.message = params.message ? params.message : 'An unknown error occurred.'
                error.status = status ? status : 500
                params.error = error
            }
            return response.render('errors/development', {...params, ...{code: status}})
        }

        return response.render('errors/'+status, {...params, ...{code: status}})
    }

    /**
     * Loads the unit.
     * Binds the view helper function to the global context and configures Express to use the Pug view engine.
     * @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){

        /*
         * Set the underlying express view engine.
         */
        this.output.debug('Setting Express views directory: '+this.directory)
        const app_config = this.configs.guarantee('app', { view_engine: 'pug' })
        app.express.set('view engine', app_config.view_engine)
        app.express.set('views', this.directory)

    }

    /**
     * Get the directories provided by this unit.
     * Currently, "views" mapped to {@link module:libflitter/views/ViewEngineUnit~ViewEngineUnit#directory}.
     * @returns {{views: string}}
     */
    directories() {
        return {
            views: this.directory
        }
    }

    /**
     * Get the fully-qualified path to the migrations provided by this unit.
     * @returns {string}
     */
    migrations(){
        return path.resolve(__dirname, 'migrations')
    }
}

module.exports = ViewEngineUnit