'use strict';
/**
* @module lib/common/httpres
* @summary Whiteflag API common http response handler module
* @description Module with common http response functions
* @tutorial modules
* @tutorial openapi
*/
module.exports = {
// HTTP response functions
createBody,
getURL,
sendIndicative,
sendImperative
};
// Whiteflag common functions and classes //
const array = require('./arrays');
const { ProcessingError, ProtocolError } = require('./errors');
// Whiteflag modules //
const apiConfigData = require('../config').getConfig();
// MAIN MODULE FUNCTIONS //
/**
* Returns the URL without path of the request
* @function getURL
* @alias module:lib/common/httpres.getURL
* @param {Object} req the http request
* @returns {string} the url without path
*/
function getURL(req) {
const reqProtocol = req.protocol || apiConfigData.server.protocol;
const reqHost = req.hostname || apiConfigData.server.hostname || 'localhost';
return `${reqProtocol}://${reqHost}`;
}
/**
* Creates and returns response body object
* @function createBody
* @alias module:lib/common/httpres.createBody
* @param {Object} req the http request
* @param {string} operationId the operation id as defined in the openapi definition
* @returns {Object} the response body
*/
function createBody(req, operationId = null) {
let resBody = {};
resBody.meta = {};
if (operationId) resBody.meta.operationId = operationId;
if (apiConfigData.version) resBody.meta.version = apiConfigData.version;
resBody.meta.request = {};
resBody.meta.request.client = req.ip;
resBody.meta.request.method = req.method;
resBody.meta.request.url = getURL(req) + req.originalUrl;
return resBody;
}
/**
* Sends informative response based on available data and errors
* @function sendIndicative
* @alias module:lib/common/httpres.sendIndicative
* @param {Object} res the http response object
* @param {Error} err error object if any errors
* @param {Object} resBody the response body
* @param {Object} resData the response data
* @param {logEndpointEventCb} callback
*/
function sendIndicative(res, err, resBody, resData, callback) {
// Processing errors
if (err && err instanceof ProcessingError) {
if (err.causes) resBody.meta.errors = array.addArray(resBody.meta.errors, err.causes);
domainErrorResponse(res, err, resBody);
return callback(null, resBody.meta.request.client, err.code, `${err.message}: ` + JSON.stringify(resBody.meta));
}
// Generic errors
if (err && !(err instanceof ProtocolError)) {
if (err.causes) resBody.meta.errors = array.addArray(resBody.meta.errors, err.causes);
genericErrorResponse(res, err, resBody);
return callback(null, resBody.meta.request.client, 'ERROR', `${err.message}: ` + JSON.stringify(resBody.meta));
}
// Protocol errors: will cause a warning but request is considered successful
if (err && err instanceof ProtocolError) {
resBody.meta.warnings = array.addItem(resBody.meta.warnings, err.message);
if (err.causes) resBody.meta.warnings = array.addArray(resBody.meta.warnings, err.causes);
}
// Return successful response
if (Array.isArray(resData)) resBody.meta.info = array.addItem(resBody.meta.info, `Returning ${resData.length} items`);
successResponse(res, resData, resBody);
return callback(null, resBody.meta.request.client, 'SUCCESS', 'Processed request: ' + JSON.stringify(resBody.meta));
}
/**
* Sends imperative response based on available data and errors
* @function sendImperative
* @alias module:lib/common/httpres.sendImperative
* @param {Object} res the http response object
* @param {Error} err error object if any errors
* @param {Object} resBody the response body
* @param {Object} resData the response data
* @param {logEndpointEventCb} callback
*/
function sendImperative(res, err, resBody, resData, callback) {
// Check data
if (!err && typeof resData === 'undefined') err = new Error('Could not retrieve any data');
if (!err && !resData) err = new ProcessingError('No data available', null, 'WF_API_NO_DATA');
// Add underlying causes to response body
if (err && err.causes) resBody.meta.errors = array.addArray(resBody.meta.errors, err.causes);
// Processing errors
if (err && err instanceof ProcessingError) {
domainErrorResponse(res, err, resBody);
return callback(null, resBody.meta.request.client, err.code, `${err.message}: ` + JSON.stringify(resBody.meta));
}
// Protocol errors
if (err && err instanceof ProtocolError) {
domainErrorResponse(res, err, resBody);
return callback(null, resBody.meta.request.client, err.code, `${err.message}: ` + JSON.stringify(resBody.meta));
}
// Generic errors
if (err) {
genericErrorResponse(res, err, resBody);
return callback(null, resBody.meta.request.client, 'ERROR', `${err.message}: ` + JSON.stringify(resBody.meta));
}
// Return successful response
if (Array.isArray(resData)) resBody.meta.info = array.addItem(resBody.meta.info, `Returning ${resData.length} items`);
successResponse(res, resData, resBody);
return callback(null, resBody.meta.request.client, 'SUCCESS', 'Processed request: ' + JSON.stringify(resBody.meta));
}
// PRIVATE MODULE FUNCTIONS //
/**
* Sends successful http repsonse
* @private
* @param {Object} res the http response
* @param {Object} resData the response data
* @param {Object} resBody the response body
*/
function successResponse(res, resData, resBody) {
resBody.data = resData || {};
/* For operations that effectively created a new resource,
* the response code should be 201.
* Operations that are not directly completed, should return 202.
* Other succesfull operations return 200.
*/
switch (resBody.meta.operationId) {
case 'updateOriginator':
case 'deleteOriginator':
case 'storeMainPreSharedKey':
case 'deleteMainPreSharedKey':
case 'storePreSharedKey':
case 'deletePreSharedKey':
case 'storeAuthToken':
case 'deleteAuthToken': {
// Async operations: request accepted
if (resBody.meta.resource) res.set('Location', resBody.meta.resource);
return res.status(202).send(resBody);
}
case 'createAccount': {
// Resource created with pointer to the new resources
if (resBody.meta.resource) res.set('Location', resBody.meta.resource);
return res.status(201).send(resBody);
}
default: {
// Normal success
return res.status(200).send(resBody);
}
}
}
/**
* Sends domain error http response
* @private
* @param {Object} res the http response
* @param {Error} err the error
* @param {Object} resBody the response body
*/
function domainErrorResponse(res, err, resBody) {
// Sends domain error responses
// Not implemented error
if (err.code === 'WF_API_NOT_IMPLEMENTED') {
resBody.errors = array.addItem(resBody.errors, `${err.code}: ${err.message}`);
return res.status(501).send(resBody);
}
// Not available error
if (err.code === 'WF_API_NOT_AVAILABLE') {
resBody.errors = array.addItem(resBody.errors, `${err.code}: ${err.message}`);
return res.status(503).send(resBody);
}
// Not allowed error
if (err.code === 'WF_API_NOT_ALLOWED') {
resBody.errors = array.addItem(resBody.errors, `${err.code}: ${err.message}`);
return res.status(403).send(resBody);
}
// Resource not found or no data error
if (err.code === 'WF_API_NO_RESOURCE' || err.code === 'WF_API_NO_DATA') {
resBody.errors = array.addItem(resBody.errors, `${err.code}: ${err.message}`);
return res.status(404).send(resBody);
}
// Resource conflict
if (err.code === 'WF_API_RESOURCE_CONFLICT') {
resBody.errors = array.addItem(resBody.errors, `${err.code}: ${err.message}`);
if (resBody.meta.resource) res.set('Location', resBody.meta.resource);
return res.status(409).send(resBody);
}
// Other client request errors
resBody.errors = array.addItem(resBody.errors, `${err.code}: ${err.message}`);
return res.status(400).send(resBody);
}
/**
* Sends generic error http response
* @private
* @param {Object} res the http response
* @param {Error} err the error
* @param {Object} resBody the response body
*/
function genericErrorResponse(res, err, resBody) {
// Sends generic error response
resBody.errors = array.addItem(resBody.errors, `Internal server error: ${err.message}`);
return res.status(500).send(resBody);
}