Source: sources/TBackendManager.js

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