/**
* @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