/**
* @author [Tristan Valcke]{@link https://github.com/Itee}
* @license [BSD-3-Clause]{@link https://opensource.org/licenses/BSD-3-Clause}
*
* @file Todo
*
* @example Todo
*
*/
import express from 'express'
import http from 'http'
import https from 'https'
import {
DefaultLogger,
TAbstractObject
} from 'itee-core'
//todo: import Databases from 'itee-database'
import {
isArray,
isBlankString,
isDefined,
isEmptyString,
isNotArray,
isNotString,
isNull,
isUndefined
} from 'itee-validators'
import path from 'path'
class TBackendManager extends TAbstractObject {
constructor ( parameters = {} ) {
const _parameters = {
...{
logger: DefaultLogger,
rootPath: __dirname,
applications: [],
databases: [],
servers: []
},
...parameters
}
super( _parameters )
this.logger = _parameters.logger
this.rootPath = _parameters.rootPath
this.applications = express()
this.router = express.Router
this.databases = new Map()
this.servers = new Map()
this.connections = []
this._initApplications( _parameters.applications )
this._initDatabases( _parameters.databases )
this._initServers( _parameters.servers )
}
get applications () {
return this._applications
}
set applications ( value ) {
this._applications = value
}
get router () {
return this._router
}
// Todo remove middleware
set router ( value ) {
this._router = value
}
get databases () {
return this._databases
}
set databases ( value ) {
this._databases = value
}
get servers () {
return this._servers
}
set servers ( value ) {
this._servers = value
}
get rootPath () {
return this._rootPath
}
set rootPath ( value ) {
if ( isNull( value ) ) { throw new TypeError( 'Root path cannot be null ! Expect a non empty string.' ) }
if ( isUndefined( value ) ) { throw new TypeError( 'Root path cannot be undefined ! Expect a non empty string.' ) }
if ( isNotString( value ) ) { throw new TypeError( `Root path cannot be an instance of ${ value.constructor.name } ! Expect a non empty string.` ) }
if ( isEmptyString( value ) ) { throw new TypeError( 'Root path cannot be empty ! Expect a non empty string.' ) }
if ( isBlankString( value ) ) { throw new TypeError( 'Root path cannot contain only whitespace ! Expect a non empty string.' ) }
this._rootPath = value
}
setApplications ( value ) {
this.applications = value
return this
}
addMiddleware ( middleware ) {
this.applications.use( middleware )
return this
}
setRouter ( value ) {
this.router = value
return this
}
setDatabases ( value ) {
this.databases = value
return this
}
addDatabase ( databaseName, database ) {
this._databases.set( databaseName, database )
return this
}
setServers ( value ) {
this.servers = value
return this
}
setRootPath ( value ) {
this.rootPath = value
return this
}
_initApplications ( config ) {
if ( config.case_sensitive_routing ) { this.applications.set( 'case sensitive routing', config.case_sensitive_routing ) }
if ( config.env ) { this.applications.set( 'env', config.env ) }
if ( config.etag ) { this.applications.set( 'etag', config.etag ) }
if ( config.jsonp_callback_name ) { this.applications.set( 'jsonp callback name', config.jsonp_callback_name ) }
if ( config.jsonp_escape ) { this.applications.set( 'json escape', config.jsonp_escape ) }
if ( config.jsonp_replacer ) { this.applications.set( 'json replacer', config.jsonp_replacer ) }
if ( config.jsonp_spaces ) { this.applications.set( 'json spaces', config.jsonp_spaces ) }
if ( config.query_parser ) { this.applications.set( 'query parser', config.query_parser ) }
if ( config.strict_routing ) { this.applications.set( 'strict routing', config.strict_routing ) }
if ( config.subdomain_offset ) { this.applications.set( 'subdomain offset', config.subdomain_offset ) }
if ( config.trust_proxy ) { this.applications.set( 'trust proxy', config.trust_proxy ) }
if ( config.views ) { this.applications.set( 'views', config.views ) }
if ( config.view_cache ) { this.applications.set( 'view cache', config.view_cache ) }
if ( config.view_engine ) { this.applications.set( 'view engine', config.view_engine ) }
if ( config.x_powered_by ) { this.applications.set( 'x-powered-by', config.x_powered_by ) }
this._initMiddlewares( config.middlewares )
this._initRouters( config.routers )
}
_initMiddlewares ( middlewaresConfig ) {
for ( let [ name, config ] of Object.entries( middlewaresConfig ) ) {
if ( isNotArray( config ) ) {
throw new TypeError( `Invalid middlware configuration for ${ name }, expecting an array of arguments to spread to middleware module, got ${ config.constructor.name }` )
}
if ( this._initPackageMiddleware( name, config ) ) {
this.logger.log( `Use ${ name } middleware from node_modules` )
} else if ( this._initLocalMiddleware( name, config ) ) {
this.logger.log( `Use ${ name } middleware from local folder` )
} else {
this.logger.error( `Unable to register the middleware ${ name } the package and/or local file doesn't seem to exist ! Skip it.` )
}
}
}
_initPackageMiddleware ( name, config ) {
let success = false
try {
this.applications.use( require( name )( ...config ) )
success = true
} catch ( error ) {
if ( !error.code || error.code !== 'MODULE_NOT_FOUND' ) {
this.logger.error( `The middleware "${ name }" seems to encounter internal error.` )
this.logger.error( error )
}
}
return success
}
_initLocalMiddleware ( name, config ) {
let success = false
try {
const localMiddlewaresPath = path.join( this.rootPath, 'middlewares', name )
this.applications.use( require( localMiddlewaresPath )( ...config ) )
success = true
} catch ( error ) {
this.logger.error( error )
}
return success
}
_initRouters ( routers ) {
for ( let [ baseRoute, routerPath ] of Object.entries( routers ) ) {
if ( this._initPackageRouter( baseRoute, routerPath ) ) {
this.logger.log( `Use ${ routerPath } router from node_modules over base route: ${ baseRoute }` )
} else if ( this._initLocalRouter( baseRoute, routerPath ) ) {
this.logger.log( `Use ${ routerPath } router from local folder over base route: ${ baseRoute }` )
} else {
this.logger.error( `Unable to register the router ${ routerPath } the package and/or local file doesn't seem to exist ! Skip it.` )
}
}
}
_initPackageRouter ( baseRoute, routerPath ) {
let success = false
try {
this.applications.use( baseRoute, require( routerPath ) )
success = true
} catch ( error ) {
if ( !error.code || error.code !== 'MODULE_NOT_FOUND' ) {
this.logger.error( `The router "${ name }" seems to encounter internal error.` )
this.logger.error( error )
}
}
return success
}
_initLocalRouter ( baseRoute, routerPath ) {
let success = false
try {
const localRoutersPath = path.join( this.rootPath, 'routers', routerPath )
this.applications.use( baseRoute, require( localRoutersPath ) )
success = true
} catch ( error ) {
if ( error instanceof TypeError && error.message === 'Found non-callable @@iterator' ) {
this.logger.error( `The router "${ name }" seems to encounter error ! Are you using an object instead an array for router configuration ?` )
}
this.logger.error( error )
}
return success
}
_initDatabases ( config ) {
for ( let configIndex = 0, numberOfDatabasesConfigs = config.length ; configIndex < numberOfDatabasesConfigs ; configIndex++ ) {
const databaseConfig = config[ configIndex ]
const dbType = databaseConfig.type
const dbFrom = databaseConfig.from
const dbName = `${ ( databaseConfig.name ) ? databaseConfig.name : `${ dbType }_${ configIndex }` }`
try {
let database = null
if ( isDefined( dbFrom ) ) {
// In case user specify a package where take the database of type...
const databasePackage = require( dbFrom )
database = new databasePackage[ dbType ]( {
...{
application: this.applications,
router: this.router
},
...databaseConfig
} )
} else {
// // Else try to use auto registered database
// database = new Databases[ dbType ]( {
// ...{
// application: this.applications,
// router: this.router
// },
// ...databaseConfig
// } )
}
// Todo move in start
database.connect()
this.databases.set( dbName, database )
} catch ( error ) {
this.logger.error( `Unable to create database of type ${ dbType } due to ${ error.name }` )
this.logger.error( error.message )
this.logger.error( error.stack )
}
}
}
_initServers ( config ) {
const _config = ( isArray( config ) ) ? config : [ config ]
for ( let configId = 0, numberOfConfigs = _config.length ; configId < numberOfConfigs ; configId++ ) {
let configElement = _config[ configId ]
let server = null
if ( configElement.type === 'https' ) {
const options = {
pfx: configElement.pfx,
passphrase: configElement.passphrase
}
server = https.createServer( options, this.applications )
} else {
server = http.createServer( this.applications )
}
server.name = configElement.name || `${ ( configElement.name ) ? configElement.name : `Server_${ configId }` }`
server.maxHeadersCount = configElement.max_headers_count
server.timeout = configElement.timeout
server.type = configElement.type
server.host = configElement.host
server.port = configElement.port
server.env = configElement.env
server.listen( configElement.port, configElement.host, () => {
this.logger.log( `${ server.name } start listening on ${ server.type }://${ server.host }:${ server.port } at ${ new Date() } under ${ server.env } environment.` )
} )
server.on( 'connection', connection => {
this.connections.push( connection )
connection.on( 'close', () => {
this.connections = this.connections.filter( curr => curr !== connection )
} )
} )
this.servers.set( server.name, server )
}
}
/**
*
* @param databaseKey
* @param eventName
* @param callback
*/
databaseOn ( databaseKey, eventName, callback ) {} // eslint-disable-line no-unused-vars
serverOn ( serverName, eventName, callback ) {
this.servers[ serverName ].on( eventName, callback )
}
serversOn ( serverKey, eventName, callback ) {
//TODO: filter availaible events
// [ 'request', 'connection', 'close', 'timeout', 'checkContinue', 'connect', 'upgrade', 'clientError' ]
for ( let serverKey in this.servers ) {
this.serverOn( serverKey, eventName, callback )
}
}
start () {
}
stop ( callback ) {
const numberOfServers = this.servers.size
const numberOfDatabases = this.databases.size
let shutDownServers = 0
let closedDatabases = 0
if ( allClosed() ) { return }
for ( const [ databaseName, database ] of this.databases ) {
database.close( () => {
closedDatabases++
this.logger.log( `Connection to ${ databaseName } is closed.` )
allClosed()
} )
}
for ( let connection of this.connections ) {
connection.end()
}
for ( const [ serverName, server ] of this.servers ) {
server.close( () => {
shutDownServers++
this.logger.log( `The ${ serverName } listening on ${ server.type }://${ server.host }:${ server.port } is shutted down.` )
allClosed()
} )
}
function allClosed () {
if ( shutDownServers < numberOfServers ) {
return false
}
if ( closedDatabases < numberOfDatabases ) {
return false
}
if ( callback ) { callback() }
}
}
closeServers () {
}
}
export { TBackendManager }