/** * @module Databases/TAbstractResponder * @desc Export the TAbstractResponder abstract class. * @exports TAbstractResponder * * @author [Tristan Valcke]{@link https://github.com/Itee} * @license [BSD-3-Clause]{@link https://opensource.org/licenses/BSD-3-Clause} */ import { TAbstractObject } from 'itee-core' import { isArray, isDefined, isFunction, isObject, isString } from 'itee-validators' import { UnknownError } from '../messages/http/UnknownError' /** * @class * @classdesc The TAbstractResponder is the base class for all derived database controller that require to send a response to client. * It allow to send preformatted response in function of database query result. */ class TAbstractResponder extends TAbstractObject { /** * Normalize errors that can be in different format like single string, object, array of string, or array of object. * * @example <caption>Normalized error are simple literal object like:</caption> * { * name: 'TypeError', * message: 'the error message' * } * * @param {String|Object|Array.<String>|Array.<Object>} error - The error object to normalize * @returns {Array.<Object>} * @private */ static _formatErrors ( errors = [] ) { const _errors = ( isArray( errors ) ) ? errors : [ errors ] let formattedErrors = [] for ( let i = 0, numberOfErrors = _errors.length ; i < numberOfErrors ; i++ ) { formattedErrors.push( TAbstractResponder._formatError( _errors[ i ] ) ) } return formattedErrors } /** * Normalize error that can be in different format like single string, object, array of string, or array of object. * * @example <caption>Normalized error are simple literal object like:</caption> * { * name: 'TypeError', * message: 'the error message' * } * * @param {String|Object|Error} error - The error object to normalize * @returns {AbstractHTTPError} * @private */ static _formatError ( error ) { let formattedError if ( error instanceof Error ) { formattedError = error formattedError.statusCode = 500 } else if ( isString( error ) ) { formattedError = new UnknownError( error ) } else if ( isObject( error ) ) { const name = error.name const message = error.message || 'Empty message...' formattedError = new UnknownError( message ) if ( name ) { formattedError.name = name } } else { formattedError = new UnknownError( error.toString() ) } return formattedError } /** * In case database call return nothing consider that is a not found. * If response parameter is a function consider this is a returnNotFound callback function to call, * else check if server response headers aren't send yet, and return response with status 204 * * @param response - The server response or returnNotFound callback * @returns {*} callback call or response with status 204 */ static returnNotFound ( response ) { if ( isFunction( response ) ) { return response() } if ( response.headersSent ) { return } response.status( 204 ).end() } /** * In case database call return an error. * If response parameter is a function consider this is a returnError callback function to call, * else check if server response headers aren't send yet, log and flush stack trace (if allowed) and return response with status 500 and * stringified error as content * * @param error - A server/database error * @param response - The server response or returnError callback * @returns {*} callback call or response with status 500 and associated error */ static returnError ( error, response ) { if ( isFunction( response ) ) { return response( error, null ) } if ( response.headersSent ) { return } const formatedError = TAbstractResponder._formatError( error ) response.format( { 'application/json': () => { response.status( formatedError.statusCode ).json( formatedError ) }, 'default': () => { response.status( 406 ).send( 'Not Acceptable' ) } } ) } /** * In case database call return some data. * If response parameter is a function consider this is a returnData callback function to call, * else check if server response headers aren't send yet, and return response with status 200 and * stringified data as content * * @param data - The server/database data * @param response - The server response or returnData callback * @returns {*} callback call or response with status 200 and associated data */ static returnData ( data, response ) { if ( isFunction( response ) ) { return response( null, data ) } if ( response.headersSent ) { return } const _data = isArray( data ) ? data : [ data ] response.format( { 'application/json': () => { response.status( 200 ).json( _data ) }, 'default': () => { response.status( 406 ).send( 'Not Acceptable' ) } } ) } /** * In case database call return some data AND error. * If response parameter is a function consider this is a returnErrorAndData callback function to call, * else check if server response headers aren't send yet, log and flush stack trace (if allowed) and * return response with status 406 with stringified data and error in a literal object as content * * @param error - A server/database error * @param data - The server/database data * @param response - The server response or returnErrorAndData callback * @returns {*} callback call or response with status 406, associated error and data */ static returnErrorAndData ( error, data, response ) { if ( isFunction( response ) ) { return response( error, data ) } if ( response.headersSent ) { return } const result = { errors: TAbstractResponder._formatErrors( error ), datas: data } response.format( { 'application/json': () => { response.status( 416 ).json( result ) }, 'default': () => { response.status( 416 ).send( 'Range Not Satisfiable' ) } } ) } static return ( response, callbacks = {} ) { const _callbacks = Object.assign( { immediate: null, beforeAll: null, beforeReturnErrorAndData: null, afterReturnErrorAndData: null, beforeReturnError: null, afterReturnError: null, beforeReturnData: null, afterReturnData: null, beforeReturnNotFound: null, afterReturnNotFound: null, afterAll: null }, callbacks, { returnErrorAndData: TAbstractResponder.returnErrorAndData.bind( this ), returnError: TAbstractResponder.returnError.bind( this ), returnData: TAbstractResponder.returnData.bind( this ), returnNotFound: TAbstractResponder.returnNotFound.bind( this ) } ) /** * The callback that will be used for parse database response */ function dispatchResult ( error = null, data = null ) { const haveData = isDefined( data ) const haveError = isDefined( error ) if ( _callbacks.beforeAll ) { _callbacks.beforeAll() } if ( haveData && haveError ) { if ( _callbacks.beforeReturnErrorAndData ) { _callbacks.beforeReturnErrorAndData( error, data ) } _callbacks.returnErrorAndData( error, data, response ) if ( _callbacks.afterReturnErrorAndData ) { _callbacks.afterReturnErrorAndData( error, data ) } } else if ( haveData && !haveError ) { if ( _callbacks.beforeReturnData ) { _callbacks.beforeReturnData( data ) } _callbacks.returnData( data, response ) if ( _callbacks.afterReturnData ) { _callbacks.afterReturnData( data ) } } else if ( !haveData && haveError ) { if ( _callbacks.beforeReturnError ) { _callbacks.beforeReturnError( error ) } _callbacks.returnError( error, response ) if ( _callbacks.afterReturnError ) { _callbacks.afterReturnError( error ) } } else if ( !haveData && !haveError ) { if ( _callbacks.beforeReturnNotFound ) { _callbacks.beforeReturnNotFound() } _callbacks.returnNotFound( response ) if ( _callbacks.afterReturnNotFound ) { _callbacks.afterReturnNotFound() } } if ( _callbacks.afterAll ) { _callbacks.afterAll() } } // An immediate callback hook ( for timing for example ) if ( _callbacks.immediate ) { _callbacks.immediate() } return dispatchResult } constructor ( parameters = {} ) { const _parameters = { ...{}, ...parameters } super( _parameters ) } } export { TAbstractResponder }