/**
* @module libflitter/views/ViewEngineUnit
*/
const path = require('path')
const Unit = require('../Unit')
const error_context = require('../errors/error_context.fn')
/**
* 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 ){
try {
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
} catch (e) {
throw error_context(e, {
view_name,
resource_find_key: find_key,
})
}
}
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