timw4mail/node-query

View on GitHub
lib/QueryBuilderBase.js

Summary

Maintainability
A
0 mins
Test Coverage
const Helpers = require('./Helpers');
const QueryParser = require('./QueryParser');
const State = require('./State');

class QueryBuilderBase {
    /**
     * @private
     * @constructor
     * @param {Driver} Driver - The syntax driver for the database
     * @param {Adapter} Adapter - The database module adapter for running queries
     */
    constructor (Driver, Adapter) {
        this.driver = Driver;
        this.adapter = Adapter;
        this.parser = new QueryParser(this.driver);
        this.state = new State();
    }

    _like (field, val, pos, like, conj) {
        field = this.driver.quoteIdentifiers(field);

        like = `${field} ${like} ?`;

        if (pos === 'before') {
            val = `%${val}`;
        } else if (pos === 'after') {
            val = `${val}%`;
        } else {
            val = `%${val}%`;
        }

        conj = (this.state.queryMap.length < 1) ? ' WHERE ' : ` ${conj} `;
        this._appendMap(conj, like, 'like');

        this.state.whereValues.push(val);
    }

    /**
     * Append a clause to the query map
     *
     * @private
     * @param {String} conjunction - linking keyword for the clause
     * @param {String} string - pre-compiled sql fragment
     * @param {String} type - type of sql clause
     * @return {void}
     */
    _appendMap (conjunction, string, type) {
        this.state.queryMap.push({
            type: type,
            conjunction: conjunction,
            string: string
        });
    }

    /**
     * Handle key/value pairs in an object the same way as individual arguments,
     * when appending to state
     *
     * @private
     * @param {mixed} letName Lorem Ipsum
     * @param {mixed} valType Lorem Ipsum
     * @param {mixed} key Lorem Ipsum
     * @param {mixed} val Lorem Ipsum
     * @return {Array} - modified state array
     */
    _mixedSet (letName, valType, key, val) {
        let obj = {};

        if (Helpers.isScalar(key) && !Helpers.isUndefined(val)) {
            // Convert key/val pair to a simple object
            obj[key] = val;
        } else if (Helpers.isScalar(key) && Helpers.isUndefined(val)) {
            // If just a string for the key, and no value, create a simple object with duplicate key/val
            obj[key] = key;
        } else {
            obj = key;
        }

        Object.keys(obj).forEach(k => {
            // If a single value for the return
            if (['key', 'value'].indexOf(valType) !== -1) {
                const pushVal = (valType === 'key') ? k : obj[k];
                this.state[letName].push(pushVal);
            } else {
                this.state[letName][k] = obj[k];
            }
        });

        return this.state[letName];
    }

    _whereMixedSet (key, val) {
        this.state.whereMap = [];
        this.state.rawWhereValues = [];

        this._mixedSet('whereMap', 'both', key, val);
        this._mixedSet('rawWhereValues', 'value', key, val);
    }

    _fixConjunction (conj) {
        const lastItem = this.state.queryMap[this.state.queryMap.length - 1];
        const conjunctionList = Helpers.arrayPluck(this.state.queryMap, 'conjunction');

        if (this.state.queryMap.length === 0 || (!Helpers.regexInArray(conjunctionList, /^ ?WHERE/i))) {
            conj = ' WHERE ';
        } else if (lastItem.type === 'groupStart') {
            conj = '';
        } else {
            conj = ` ${conj} `;
        }

        return conj;
    }

    _where (key, val, defaultConj) {
        // Normalize key and value and insert into this.state.whereMap
        this._whereMixedSet(key, val);

        // Parse the where condition to account for operators,
        // functions, identifiers, and literal values
        this.state = this.parser.parseWhere(this.driver, this.state);

        this.state.whereMap.forEach(clause => {
            const conj = this._fixConjunction(defaultConj);
            this._appendMap(conj, clause, 'where');
        });

        this.state.whereMap = {};
    }

    _whereNull (field, stmt, conj) {
        field = this.driver.quoteIdentifiers(field);
        const item = `${field} ${stmt}`;

        this._appendMap(this._fixConjunction(conj), item, 'whereNull');
    }

    _having (key, val = null, conj = 'AND') {
        // Normalize key/val and put in state.whereMap
        this._whereMixedSet(key, val);

        // Parse the having condition to account for operators,
        // functions, identifiers, and literal values
        this.state = this.parser.parseWhere(this.driver, this.state);

        this.state.whereMap.forEach(clause => {
            // Put in the having map
            this.state.havingMap.push({
                conjunction: (this.state.havingMap.length > 0) ? ` ${conj} ` : ' HAVING ',
                string: clause
            });
        });

        // Clear the where Map
        this.state.whereMap = {};
    }

    _whereIn (key, val, inClause, conj) {
        key = this.driver.quoteIdentifiers(key);
        const params = Array(val.length);
        params.fill('?');

        val.forEach(value => {
            this.state.whereValues.push(value);
        });

        conj = (this.state.queryMap.length > 0) ? ` ${conj} ` : ' WHERE ';
        const str = `${key} ${inClause} (${params.join(',')}) `;

        this._appendMap(conj, str, 'whereIn');
    }

    _run (type, table, sql, vals) {
        if (!sql) {
            sql = this._compile(type, table);
        }

        if (!vals) {
            vals = this.state.values.concat(this.state.whereValues);
        }

        // Reset the state so another query can be built
        this._resetState();

        // Pass the sql and values to the adapter to run on the database
        return this.query(sql, vals);
    }

    _getCompile (type, table, reset) {
        reset = reset || false;

        const sql = this._compile(type, table);

        if (reset) {
            this._resetState();
        }

        return sql;
    }

    /**
     * Complete the sql building based on the type provided
     *
     * @private
     * @param {String} type - Type of SQL query
     * @param {String} table - The table to run the query on
     * @return {String} - The compiled sql
     */
    _compile (type, table) {
        // Put together the basic query
        let sql = this._compileType(type, table);

        // Set each subClause
        ['queryMap', 'groupString', 'orderString', 'havingMap'].forEach(clause => {
            const param = this.state[clause];

            if (!Helpers.isScalar(param)) {
                Object.keys(param).forEach(part => {
                    sql += param[part].conjunction + param[part].string;
                });
            } else {
                sql += param;
            }
        });

        // Append the limit, if it exists
        if (Helpers.isNumber(this.state.limit)) {
            sql = this.driver.limit(sql, this.state.limit, this.state.offset);
        }

        return sql;
    }

    _compileType (type, table) {
        let sql = '';

        switch (type) {
            case 'insert':
                const params = Array(this.state.setArrayKeys.length).fill('?');

                sql = `INSERT INTO ${table} (`;
                sql += this.state.setArrayKeys.join(',');
                sql += `) VALUES (${params.join(',')})`;
                break;

            case 'update':
                sql = `UPDATE ${table} SET ${this.state.setString}`;
                break;

            case 'delete':
                sql = `DELETE FROM ${table}`;
                break;

            default:
                sql = `SELECT * FROM ${this.state.fromString}`;

                // Set the select string
                if (this.state.selectString.length > 0) {
                    // Replace the star with the selected fields
                    sql = sql.replace('*', this.state.selectString);
                }

                break;
        }

        return sql;
    }

    _resetState () {
        this.state = new State();
    }
}

module.exports = QueryBuilderBase;