libflitter/database/DatabaseUnit.js

/**
 * @module libflitter/database/DatabaseUnit
 */

const path = require('path')
const mongoose = require('mongoose')
const rra = require('recursive-readdir-async')
const Unit = require('../Unit')

/**
 * The database unit loads the model schemas from the specified directory
 * and creates Mongoose models from them. Then, using the specified values
 * it connects to the MongoDB database and continues initializing the stack
 * from within the context of a guaranteed connection to the database.
 * 
 * @extends module:libflitter/Unit~Unit
 */
class DatabaseUnit extends Unit {

    /**
     * Instantiate the unit. Resolve the path to the directory with the model definition schemata.
     * @param {string} models_directory - path to the directory with the model definition schemata
     */
    constructor(models_directory = './app/models'){
        super()

        /**
         * The fully-qualified path to the directory with the model definition schemata.
         * @type {string}
         * @name DatabaseUnit#directory
         */
        this.directory = path.resolve(models_directory)
    }

    /**
     * Loads the unit. Loads the model definition schemata from {@link module:libflitter/database/DatabaseUnit~DatabaseUnit#directory}
     * and creates Mongoose models from them. Then, it binds the models, schemata, and helper functions to the appropriate
     * contexts. Opens the database connection using the values configured in the 'database' configuration.
     * @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, context){

        /*
         * Get the models by file.
         */
        const files = await rra.list(this.directory)
        const models = {}
        const schemas = {}

        /*
         * Parse the name of the model based on its file
         * and folder name.
         * e.g. "auth/User.model.js" becomes "auth:User"
         */
        for ( let key in files ){
            if ( files[key].fullname.endsWith('.model.js') ) {
                let name = files[key].fullname.replace(this.directory, '')
                    .replace(/.model.js/g, '')
                    .replace(/\//g, ':').substring(1)
                app.d.utility.log('Model name: ' + name, 3)

                /*
                 * Store the schema and the Mongoose model
                 * using the parsed name.
                 */
                app.d.utility.log('Model schema file: ' + files[key].fullname, 3)
                app.d.utility.log(require(files[key].fullname), 4)
                schemas[name] = require(files[key].fullname)

                app.d.utility.log("Loaded model schemas.", 2)
                app.d.utility.log(schemas, 4)

                models[name] = mongoose.model(name, schemas[name])
                app.d.utility.log("Loaded models.", 2)
            }
        }
        
        context.bind('models', models)
        context.bind('schemas', schemas)
        context.bind('model', this.model)
        context.bind('schema', this.schema)

        app.global.bind('model', this.model)
        app.global.bind('schema', this.schema)

        /*
         * Get the database information from the config.
         */
        const db_host = app.d.config.get('database.host')
        const db_port = app.d.config.get('database.port')
        const db_name = app.d.config.get('database.name')

        /*
         * Form the connect string, which is the address of
         * the MongoDB server including the name of the
         * database and authentication, if it has been specified.
         */
        let connect_string = ""
        if ( app.d.config.get('database.auth.require') === 'true' ){
            const db_user = app.d.config.get('database.auth.username')
            const db_pass = app.d.config.get('database.auth.password')
            connect_string = `mongodb://${db_user}:${db_pass}@${db_host}:${db_port}/${db_name}`
        }
        else {
            connect_string = `mongodb://${db_host}:${db_port}/${db_name}`
        }
        app.d.utility.log("Database Connect String: "+connect_string, 4)
        
        /*
         * Save the connection string.
         */
        context.bind('string', connect_string)

        /*
         * Attempt to connect to the database.
         */
        await mongoose.connect(connect_string, { useNewUrlParser: true })
        const db = mongoose.connection
        context.bind('connection', db)

        /*
         * If the connection fails, print an error.
         */
        db.on('error', console.error.bind(console, 'MongoDB Connection Error: '))
    }

    /**
     * Cleans up the unit's resources before Flitter exits. Closes the MongoDB connection cleanly.
     * @param {module:libflitter/app/FlitterApp~FlitterApp} app - the Flitter app
     * @returns {Promise<void>}
     */
    async cleanup(app){
        await mongoose.disconnect()
    }

    /**
     * Helper function to get the Mongoose model instance by its Flitter canonical name.
     * This is usually bound to the relevant {@link module:libflitter/app/FlitterApp~FlitterApp} instance.
     * @param {string} name - the Flitter canonical name of the model
     * @returns {Mongoose/Model}
     */
    model(name){
        return this.d.database.models[name]
    }

    /**
     * Helper function to get the model definition schema instance by its Flitter canonical name.
     * This is usually bound to the relevant {@link module:libflitter/app/FlitterApp~FlitterApp} instance.
     * @param {string} name - the Flitter canonical name of the model definition schema
     * @returns {Object}
     */
    schema(name){
        return this.d.database.schemas[name]
    }

    /**
     * Get the name of the unit.
     * @returns {string} "database"
     */
    name(){
        return "database"
    }

    /**
     * Get the directories provided by this unit.
     * Currently, {@link module:libflitter/database/DatabaseUnit~DatabaseUnit#directory}
     * @returns {{models: string}}
     */
    directories() {
        return {
            models: this.directory
        }
    }

    /**
     * Get the templates provided by the unit.
     * Currently, "model" provided by {@link module:libflitter/templates/model}
     * @returns {{model: {template: (model|(function(string): string)|*), extension: string, directory: string}}}
     */
    templates(){
        return {
            model: {
                template: require('libflitter/templates/model'),
                directory: this.directory,
                extension: '.model.js'
            }
        }
    }
}

module.exports = DatabaseUnit