libflitter/routing/ResponseSystemMiddleware.js

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

const { Injectable } = require('flitter-di')
const statuses = require('http-status')
const error_context = require('../errors/error_context.fn')

/**
 * 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', 'models']
    }

    /**
     * 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 APIRequest = this.models.get('models::apirequest')

        const status_code = this._status ? this._status : 200
        const message = this._message ? this._message : statuses[status_code]

        if ( this.configs.get('server.logging.api_logging') ) {
            await APIRequest.log(this.request, { status_code, message, data })
        }

        try {
            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,
            })
        } catch (e) {
            throw error_context(e, {
                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 = {}){
        try {
            // 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)
        } catch (e) {
            throw error_context(e, {
                view_name,
                view_args: args,
                view_resource_list: resource_list,
            })
        }
    }
}

module.exports = exports = ResponseSystemMiddleware