Home Reference Source Repository

src/util/PDOX.js

var Q = require("q");
var mysql = require("mysql");

/**
 * Routines to handle connection chores.
 * https://www.npmjs.com/package/mysql
 */
class PDOX {

    /**
     * Create the database conection pools.
     * @param {Config} Configuration object
     */
    constructor(CFG) {
        /**
         * A reference to the configuration object
         *
         * @private
         */
        this._CFG = CFG;  // Retain for leter

        /**
         * The database pool (private)
         * https://www.npmjs.com/package/mysql#pooling-connections
         *
         * @private
         */
        this._pool = null;

        // Create a MySql pool
        var pool  = mysql.createPool({
            host     : CFG.dbhost,
            port     : CFG.dbport,
            database : CFG.dbname,
            // TODO: Pool parameters
            user     : CFG.dbuser,
            password : CFG.dbpass
        });

        // Test the pool (async - will fail later)
/*
        this._testPool(pool).then(function (message) {
            console.log(message);
        });
*/
        this._pool = pool;
    }

    /**
     * Replace {p} with the configured database table prefix.
     */
    setPrefix(sql) {
        return sql.replace(/{p}/g, this._CFG.dbprefix);
    }

    /**
     * Dp field Substitution
     */
    static substituteFields(conn, sql, data) {
        let retval = sql.replace(/\:(\w+)/g, function (txt, key) {
            if (data.hasOwnProperty(key)) {
                if ( typeof data[key] == 'undefined' || data[key] === null ) {
                    console.log("NULL at",key);
                    return 'NULL';
                }
                return conn.escape(data[key]);
            }
            console.log("Warning: substitution key not found:",key);
            return txt;
        });
        return retval;
    }

    /**
     * Make the PDO-style format substitutions work
     *
     * https://www.npmjs.com/package/mysql#preparing-queries
     *
     *     connection.query("UPDATE posts SET title = :title",
     *         { title: "Hello MySQL" });
     *
     * @private
     */
    _setupFormat(connection) {
        connection.config.queryFormat = function (query, values) {
            if (!values) return query;
            return query.replace(/\:(\w+)/g, function (txt, key) {
                if (values.hasOwnProperty(key)) {
                    if ( typeof values[key] == 'undefined' || values[key] === null ) {
                        // console.log("NULL at",key);
                        return 'NULL';
                    }
                    return this.escape(values[key]);
                }
                console.log("Warning: substitution key not found:",key);
                return txt;
            }.bind(this));
        };
     }

    /**
     * Test the pool
     *
     * @private
     */
    _testPool(pool) {
        var deferred = Q.defer();
        pool.getConnection(function(err, conn){
            conn.query("SELECT 1 + 1 AS solution", function(err, rows) {
                if (err) {
                    console.log("Unable to make database pool");
                    console.log("This could be incorrect configuration or missing database");
                    console.log("Here are the rough instructions to make the database tables:");
                    console.log("   CREATE DATABASE tsugi DEFAULT CHARACTER SET utf8;");
                    console.log("   GRANT ALL ON tsugi.* TO 'ltiuser'@'localhost' IDENTIFIED BY 'ltipassword';");
                    console.log("   GRANT ALL ON tsugi.* TO 'ltiuser'@'127.0.0.1' IDENTIFIED BY 'ltipassword';");
                    conn.release();
                    deferred.reject(err);
                }
                conn.release();
                deferred.resolve('Pool test success');
            })
        });
        return deferred.promise;
    }

    /**
     * Get a connection promise from the pool
     * Make sure to do a cop.release()
     *
     *     var thekey = '12345';
     *     pdox.cop.then( function(connection) {
     *         let sql = 'SELECT * FROM lti_key WHERE key_key = :key';
     *         connection.query(sql, { key: thekey }, function(err, rows, fields) {
     *             if (err) {
     *                 console.log('Could not load data query');
     *             } else {
     *                 console.log("Rows:",rows.length);
     *             }
     *             connection.release();
     *         });
     *     });
     */
    cop() {
        var deferred = Q.defer();
        var setupFormat = this._setupFormat;
        this._pool.getConnection(function(err, connection) {
            if(err) {
                deferred.reject(err);
            } else {
                setupFormat(connection);
                deferred.resolve(connection);
            }
        });
        return deferred.promise;
    }

    /**
     * Run a query and return all the rows from the query and throw any error.
     *
     *     let sql = 'SELECT * FROM {p}lti_key WHERE key_key = :key_key';
     *     pdox.allRowsDie(sql,{ key_key: thekey }).then(
     *          function(rows) {
     *              console.log("Rows:",rows.length);
     *          },
     *          function(reason) {
     *               console.log("Bummer",reason);
     *          }
     *     );
     *
     * @param {string} sql The SQL to use - it is ok to use {p} for
     * the database prefix.
     * @param {object} data The key-value pairs for substitution
     */
    allRowsDie(sql, data=null) {
        return this.allRows(sql,data,true);
    }

    /**
     * Run a query and return all the rows or an error from the query.
     *
     *     let sql = 'SELECT * FROM lti_key WHERE key_key = :key_key';
     *     pdox.allRows(sql,{ key_key: thekey }, false).then(
     *          function(rows) {
     *              console.log("Rows:",rows.length);
     *          },
     *          function(reason) {
     *               console.log("Bummer",reason);
     *          }
     *     );
     *
     * @param {string} sql The SQL to use - it is ok to use {p} for
     * the database prefix - must be SELECT
     * @param {object} data The key-value pairs for substitution
     * @param {boolean} dothrow Whether to throw or return the error
     */
    allRows(sql, data=null, dothrow=false) {
        // console.log("allRowsDie",sql); console.log("   ",data);
        var deferred = Q.defer();
        sql = this.setPrefix(sql);
        this.cop().then( function(connection) {
            sql = PDOX.substituteFields(connection, sql, data);

            connection.query(sql, data, function(err, rows, fields) {
                if (err) {
                    console.log('Could not load data query');
                    console.log(sql);
                    console.log(data);
                    connection.release();
                    let myerror = 'Could not load data query '+sql;
                    if ( dothrow )  {
                      deferred.reject(myerror);
                    } else {
                      deferred.resolve(myerror);
                    }

                } else {
                    // console.log('query die returning rows:', rows.length);
                    connection.release();
                    deferred.resolve(rows);
                }
            });
        }).catch (function (connectionError) {
            deferred.reject (connectionError);
        });
        return deferred.promise;
    }

    /**
     * Run a query and return the number of affected rows, throw on error
     *
     *     sql = "DELETE FROM {p}lti_unit_test WHERE name='tsugi'";
     *     pdox.query(sql).then( function(retval) {
     *          console.log("DELETE retval:",retval);
     *     });
     *
     * @param {string} sql The SQL to use - it is ok to use {p} for
     * the database prefix - must not be a SELECT
     * @param {object} data The key-value pairs for substitution (optional)
     */
    query(sql, data=null) {
        return this.queryFull(sql, data, 0, true);
    }

    /**
     * Run a query and return the number of changed rows, throw on error
     *
     *     sql = "UPDATE {p}lti_unit_test SET email=:new WHERE name='tsugi'";
     *     pdox.queryChanged(sql, {new:'tsugi@fred.com'}).then(
     *         function(retval) {
     *             console.log("UPDATE retval:",retval);
     *         }
     *     );
     *
     * @param {string} sql The SQL to use - it is ok to use {p} for
     * the database prefix - must not be a SELECT
     * @param {object} data The key-value pairs for substitution (optional)
     */
    queryChanged(sql, data=null) {
        return this.queryFull(sql, data, 1, true);
    }

    /**
     * Run an INSERT and return the generated key, throw on error
     *
     *     sql = "INSERT INTO {p}lti_unit_test (name,email)
     *            VALUES ('tsugi', 'tsugi@zap.com')";
     *     pdox.insertKey(sql).then( function(retval) {
     *          console.log("INSERT retval:",retval);
     *     });
     *
     * @param {string} sql The SQL to use - it is ok to use {p} for
     * the database prefix - must be an INSERT to a table with
     * an auto-increment field.
     * @param {object} data The key-value pairs for substitution (optional)
     */
    insertKey(sql, data=null) {
        return this.queryFull(sql, data, 2, true);
    }

    /**
     * Run a query and return the appropriate result for the query
     *
     * This has more parameters and is typically used by methods with
     * simpler signatures.
     *
     * @param {string} sql The SQL to use - it is ok to use {p} for
     * the database prefix - must not be SELECT.
     * @param {object} data The key-value pairs for substitution
     * @param {number} returnval What to return from the function.
     * 0=rows affected, 1=rows changed, 2=last insert id
     * @param {boolean} dothrow Whether to throw or return the error
     */
    queryFull(sql, data=null, returnval=0, dothrow=false) {
        // console.log("queryFull",sql); console.log("   ",data);
        var deferred = Q.defer();
        sql = this.setPrefix(sql);
        this.cop().then( function(connection) {
            sql = PDOX.substituteFields(connection, sql, data);
            connection.query(sql, data, function(err, result) {
                if (err) {
                    console.log('Could not execute query');
                    console.log(sql);
                    console.log(data);
                    connection.release();
                    let myerror = 'Could not execute query '+sql;
                    if ( dothrow )  {
                      deferred.reject(myerror);
                    } else {
                      deferred.resolve(myerror);
                    }
                } else {
                    // console.log('query die returning rows:', rows.length);
                    connection.release();
                    if ( returnval == 2 ) {
                        deferred.resolve(result.insertId);
                    } else if ( returnval == 1 ) {
                        deferred.resolve(result.changedRows);
                    } else {
                        deferred.resolve(result.affectedRows);
                    }
                }
            });
        }).catch (function (connectionError) {
            deferred.reject (connectionError);
        });
        return deferred.promise;
    }
}

module.exports = PDOX;