libflitter/utility/UtilityUnit.js

/**
 * @module libflitter/utility/UtilityUnit
 */

const Unit = require('../Unit')
const path = require('path')

const Output = require('./services/Output.service')

/**
 * The utility unit contains various utility functions and tools that
 * are made available to almost all other units. Currently, a function
 * for restricting console.log calls by logging level is the one method
 * included. Also provides the path to the root folder of the app.
 * 
 * @extends module:libflitter/Unit~Unit
 */
class UtilityUnit extends Unit {
    /**
     * Defines the services required by this unit.
     * @returns {Array<string>}
     */
    static get services() {
        return [...super.services, 'configs']
    }

    /**
     * Array of additional service names provided by this unit.
     * @returns {Array<string>}
     */
    static get provides() {
        return ['output']
    }

    /**
     * Gets the name of the service provided by this unit: 'utility'
     * @returns {string} - 'utility'
     */
    static get name() {
        return 'utility'
    }

    /**
     * Instantiate the unit.
     * @param {string} [app_root = './app'] - path to the 'app' folder
     */
    constructor(app_root = './app'){
        super()

        /**
         * Fully-qualified path to the 'app' folder.
         * @type {Promise<void> | Promise<string>}
         */
        this.directory = path.resolve(app_root)

        /**
         * Fully-qualified path to the 'app/services' folder.
         * @type {Promise<void> | Promise<string>}
         */
        this.services_dir = path.resolve(app_root, 'services')
    }

    /**
     * Loads the unit. Binds the application root and global logging function to the appropriate contexts.
     * @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){
        super.go(app)

        const output = new Output()
        output.level = this.configs.get('server.logging.level')
        output.timestamp = this.configs.get('server.logging.include_timestamp')

        // Replace the output service instantiated by FlitterApp
        app.output = output
        this.output = output

        app.di().container.register_singleton(Output.name, output)
    }

    /**
     * Get the fully-qualified path to the root folder of the Flitter application.
     * @returns {string}
     */
    root(){
        if ( require && require.main && require.main.filename ) return require.main.filename
        else if ( process && process.mainModule && process.mainModule.filename ) return process.mainModule.filename
        else return process.cwd()
    }

    /**
     * Returns true if the application is running on Windows.
     * @returns {boolean}
     */
    is_windows() {
        return process.platform === 'win32'
    }

    /**
     * Returns true if the application is running on Linux.
     * @returns {boolean}
     */
    is_linux() {
        return process.platform === 'linux'
    }

    /**
     * Returns true if the application is running on macOS.
     * @returns {boolean}
     */
    is_mac() {
        return process.platform === 'darwin'
    }

    /**
     * Get the directives provided by this unit.
     * @returns {Object}
     */
    directives(){
        return {
            // ServerDirective
        }
    }

    /**
     * Get the directories managed by this unit. Includes 'root' for the application root.
     * @returns {{root: string}}
     */
    directories() {
        return {
            root: this.root()
        }
    }

    /**
     * Get the unit's migrations.
     * @returns {string}
     */
    migrations(){
        return path.resolve(__dirname, 'migrations')
    }

    /**
     * Make a deep copy of some item. If item is primitive, return it.
     * It item is an array, create a new array with deep copies of each array item.
     * If item is an object, create a new object with key-values such that the keys are the
     * same, but the values are deep copies of the keys.
     *
     * Assumes items can eventually be resolved to primitive types, and that they
     * contain no circular structure.
     * @param item
     * @returns {Object}
     */
    deep_copy(item){
        if ( Array.isArray(item) ){
            const ret = []
            item.forEach(e => {
                ret.push(this.deep_copy(e))
            })
            return ret
        } else if ( typeof item === 'object' ){
            const ret = {}
            Object.keys(item).forEach(key => {
                ret[key] = this.deep_copy(item[key])
            })
            return ret
        } else return item
    }

    /* Useful functions */

    /**
     * Deep merges object 2 into object 1, recursively.
     * This mutates object 1.
     * @param obj1
     * @param obj2
     * @returns {*}
     */
    deep_merge(obj1, obj2) {
        for ( let key in obj2 ){
            if ( !Object.keys(obj1).includes(key) ) obj1[key] = obj2[key]
            else if ( typeof obj1[key] !== 'object' ) obj1[key] = obj2[key]
            else if ( typeof obj2[key] !== 'object' ) obj1[key] = obj2[key]
            else obj1[key] = this.deep_merge(obj1[key], obj2[key])
        }

        return obj1
    }

    /**
     * Grabs an environment variable by name and tries to infer its type.
     * @param {string} name
     * @param default_value
     * @return {string|null|boolean|number}
     */
    env(name, default_value){
        const val = process.env[name]
        if ( !val ) return typeof default_value !== "undefined" ? default_value : null
        else return this.infer(val)
    }

    /**
     * Attempt to infer the variable type of a string's data.
     * @param {string} val
     * @return {boolean|null|*|number|undefined}
     */
    infer(val){
        if ( !val ) return null
        else if ( val.toLowerCase() === 'true' ) return true
        else if ( val.toLowerCase() === 'false' ) return false
        else if ( !isNaN(val) ) return +val
        else if ( this.is_json(val) ) return JSON.parse(val)
        else if ( val.toLowerCase() === 'null' ) return null
        else if ( val.toLowerCase() === 'undefined' ) return undefined
        else return val
    }

    /**
     * Checks if a string is valid JSON.
     * @param string
     * @return {boolean} - true if the string is valid JSON
     */
    is_json(string){
        try {
            JSON.parse(string)
            return true
        }
        catch (e) {
            return false
        }
    }
}

module.exports = UtilityUnit