libflitter/routing/ResponseSystemMiddleware.js

/**
 * @module libflitter/routing/ResponseSystemMiddleware
 */

const { Injectable } = require('flitter-di')
const statuses = require('http-status')

/**
 * System Middleware is an abstract Flitter construct. System middleware is applied to
 * every request as it is processed by Flitter. It's different to traditional app-space
 * middleware in that it is inherently global and is applied to each transaction without
 * discrimination. The point of this type of middleware is to modify objects like Express
 * requests and responses to add Flitter-specific helper methods. ResponseSystemMiddleware
 * specifically adds helpers to the Express response.
 * @extends module:flitter-di/src/Injectable~Injectable
 */
class ResponseSystemMiddleware extends Injectable {
    /**
     * Defines the services required by this unit.
     * @returns {Array<string>}
     */
    static get services() {
        return [...super.services, 'app', 'views', 'configs', 'output']
    }

    /**
     * Instantiate the middleware. Bootstrap the response with methods.
     * @param {module:libflitter/app/FlitterApp~FlitterApp} app - the Flitter app
     * @param {express/response} response - the Express response
     * @param {express/request} request - the Express request
     */
    constructor(app, response, request){
        super()

        this.response = response
        this.request = request
        this.old_status = response.status.bind(response)

        if ( app.di().has('views') && app.di().has('express') ) {
            response.view = this.view.bind(this)
            response.error = this.error.bind(this)
            response.page = this.page.bind(this)
            response.api = this.api.bind(this)
            response.message = this.message.bind(this)
            response.status = this.status.bind(this)
        } else {
            this.output.warn('Unable to bind views and express services. The response was not modified.')
        }
    }

    /**
     * Sets the response message, if used.
     * @param msg
     * @returns {express|response}
     */
    message(msg = 'OK') {
        this._message = msg
        return this.response
    }

    /**
     * Sets the response status code.
     * @param code
     * @returns {express|response}
     */
    status(code = 200) {
        this._status = code
        this.old_status(code)
        return this.response
    }

    /**
     * Sends an JSON-formatted response in a standard API format containing the HTTP status
     * code, some message, and a data key with the passed in data.
     * @param data
     * @returns {Promise<*>}
     */
    async api(data) {
        const status_code = this._status ? this._status : 200
        const message = this._message ? this._message : statuses[status_code]

        this.old_status(status_code)
        this.output.info(`API Response (HTTP ${status_code}) - ${this.request.ip}${this.request.xhr ? ' (XHR)' : ''} - ${this.request.method} ${this.request.path}`)

        return this.response.send({
            status: status_code,
            message,
            data,
        })
    }

    /**
     * Renders a view for the user. Wraps the {module:libflitter/views/ViewEngineUnit~ViewEngineUnit#view} method.
     * @param {string} view_name - the Flitter canonical name of the view to be rendered
     * @param {Object} args - variables to be passed to the view
     * @param {module:libflitter/views/ViewEngineUnit~ViewEngineUnit~ResourceSpec[]} resource_list - an array of resource specifications to be passed to the view
     * @returns {Promise<*>} - returns the output of the wrapped function
     */
    async view(view_name, args = {}, resource_list = {}){
        this.output.info(`View Render Response (HTTP ${this._status ? this._status : 200}) - ${this.request.ip}${this.request.xhr ? ' (XHR)' : ''} - ${this.request.method} ${this.request.path}`)
        return await this.views.view(this.response, view_name, args, resource_list)
    }

    /**
     * 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.
     * Wraps the {module:libflitter/views/ViewEngineUnit~ViewEngineUnit#error} method.
     * @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(status, params){
        this.output.info(`Error View Response (HTTP ${status}) - ${this.request.ip}${this.request.xhr ? ' (XHR)' : ''} - ${this.request.method} ${this.request.path}`)
        return await this.views.error(this.response, status, params)
    }

    /**
     * Page-specific data made available as a utility when rendering a page.
     *
     * @typedef {Object} ResponseSystemMiddleware~PageAppData
     * @property {string} name - the configured "app.name"
     * @property {string} url - the configured "app.url"
     * @property {Object} user - the currently authenticated user, IF one exists. Expects the user to exist in request.session.auth.user.
     */

    /**
     * Renders a view for the user. Wraps the {module:libflitter/views/ViewEngineUnit~ViewEngineUnit#view} method.
     * Additionally, adds the _app variable to the view, which contains page-specific information like the title.
     * See {module:libflitter/routing/ResponseSystemMiddleware~ResponseSystemMiddleware~PageAppData} for more info.
     * @param {string} view_name - the Flitter canonical name of the view to be rendered
     * @param {Object} args - variables to be passed to the view
     * @param {module:libflitter/views/ViewEngineUnit~ViewEngineUnit~ResourceSpec[]} resource_list - an array of resource specifications to be passed to the view
     * @returns {Promise<*>} - returns the output of the wrapped function
     */
    async page(view_name, args = {}, resource_list = {}){
        // Automatically include some useful data.
        const _app = {
            name: this.configs.get('app.name'),
            url: this.configs.get('app.url'),
        }
        
        // Get the user, if they exist:
        if ( this.request.session.auth && this.request.session.auth.user ){
            _app.user = this.request.session.auth.user
        }

        this.output.info(`Page Render Response (HTTP ${this._status ? this._status : 200}) - ${this.request.ip}${this.request.xhr ? ' (XHR)' : ''} - ${this.request.method} ${this.request.path}`)
        return await this.views.view(this.response, view_name, {...{_app}, ...args}, resource_list)
    }
}

module.exports = exports = ResponseSystemMiddleware