Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Regex functionality is not documented in README #25

Open
bean5 opened this issue Aug 22, 2019 · 0 comments
Open

Regex functionality is not documented in README #25

bean5 opened this issue Aug 22, 2019 · 0 comments

Comments

@bean5
Copy link

bean5 commented Aug 22, 2019

Well, there are a few references, but builder.js has some references in

/* Example datatable querystring ....

Regex wasn't working for me. We were using this for SQLite. I realized we'd need to filter based on regex after running the full query. I was going to submit a pull request, but it turns out I modified a previous version of the code. Here is what we use:

/*!
 * node-datatable
 * https://github.com/jpravetz/node-datatable
 * Copyright(c) 2012-2013 Jim Pravetz <[email protected]>
 * node-datatable may be freely distributed under the MIT license.
 * 
 * Note: We are not compatible with newer versions of this since 
 * the newer version doesn't seem to support RegEx. In face,
 * we modified the default regex support functionality such that it
 * was supported after the query ran. In other words, the filtering
 * occurs after the query runs.
 */

// var _u = require('underscore');

var DEFAULT_LIMIT = 5000;

/**
 * Constructor
 * @param options Refer to README.md for a list of properties
 * @return {Object}
 */
// module.exports = function (options) {
var QueryBuilder = function ( options ) {

    var self = {
        sTableName: options.sTableName,
        sCountTableName: options.sCountTableName,   // Name of table to use when counting total number of rows. Defaults to sCountColumnName
        sCountColumnName: options.sCountColumnName,   // Name of column to use when counting total number of rows. Defaults to options.sCountColumnName
        sDatabaseOrSchema: options.sDatabaseOrSchema,           // If set, then do "USE useDatabase;" prior to query
        aSearchColumns: options.aSearchColumns || [],     // Used to determine names of columns to search
        sSelectSql: options.sSelectSql,           // alternate select statement
        sFromSql: options.sFromSql,           // alternate select statement
        sWhereAndSql: options.sWhereAndSql,           // Custom caller SQL, added as AND where to add date range or other checks (caller must write the SQL)
        sGroupBySql: options.sGroupBySql || [],   // Custom caller SQL, added as GROUP BY statement
        sDateColumnName: options.sDateColumnName,   // If set then only get entries within the range (can use sWhereSql instead)
        dateFrom: options.dateFrom,                 // Only retrieve content from before this date. sDateColumnName must be set.
        dateTo: options.dateTo,                     // Only retrieve content from after this date. sDateColumnName must be set.
        oRequestQuery: options.oRequestQuery,           // Usually passed in with buildQuery
        sAjaxDataProp: 'data',           // The name of the data prop to set on the return value

        dbType: options.dbType,                     // "postgres" or "oracle", defaults to MySQL syntax

        buildQuery: buildQuery,
        parseResponse: parseResponse,
        filteredResult: filteredResult
    };

    /**
     * (private) Build an optional "USE sDatabaseOrSchema" for MySQL / Postgres or
     * "ALTER SESSION SET CURRENT_SCHEMA = sDatabaseOrSchema" statement for Oracle if sDatabaseOrSchema is set.
     * @return {string|undefined} The SQL statement or undefined
     */
    function buildSetDatabaseOrSchemaStatement() {
        if (self.sDatabaseOrSchema){
            if (self.dbType === 'oracle'){
                return 'ALTER SESSION SET CURRENT_SCHEMA = ' + self.sDatabaseOrSchema;
            }
            else{
                return "USE " + self.sDatabaseOrSchema;
            }
        }
    }

    /**
     * (private) Build the date partial that is used in a WHERE clause
     * @return {*}
     */
    function buildDatePartial() {
        if (self.sDateColumnName && self.dateFrom || self.dateTo) {
            // console.log( "DateFrom %s to %s", self.dateFrom, self.dateTo );
            if (self.dateFrom && self.dateTo) {
                return self.sDateColumnName + " BETWEEN '" + self.dateFrom.toISOString() + "' AND '" + self.dateTo.toISOString() + "'";
            } else if (self.dateFrom) {
                return self.sDateColumnName + " >= '" + self.dateFrom.toISOString() + "'";
            } else if (self.dateTo) {
                return self.sDateColumnName + " <= '" + self.dateTo.toISOString() + "'";
            }
        }
        return undefined;
    }

    /**
     * (private) Build a complete SELECT statement that counts the number of entries.
     * @param searchString If specified then produces a statement to count the filtered list of records.
     * Otherwise the statement counts the unfiltered list of records.
     * @return {String} A complete SELECT statement
     */
    function buildCountStatement(requestQuery, countType) {
        var dateSql = buildDatePartial();
        var result;
        var countTable;

        if(self.sCountTableName) {
            countTable = self.sCountTableName;
        } else {
            countTable = self.sTableName;
        }

        if(self.sGroupBySql.length) {
            result = "SELECT COUNT(*) " + countType + " FROM (";
            result += "SELECT COUNT(" + self.sGroupBySql[0] + ") as " + countType + " FROM ";
            result += self.sFromSql ? self.sFromSql : countTable;
            result += buildWherePartial(requestQuery);
            result += buildGroupByPartial();
            result += ") temp";
        } else {
            result = "SELECT COUNT(";
            result += self.sSelectSql ? "*" : (self.sCountColumnName ? self.sCountColumnName : "id");
            result += ") AS " + countType + " FROM ";
            result += self.sFromSql ? self.sFromSql : countTable;
            result += buildWherePartial(requestQuery);
        }
//        var sSearchQuery = buildSearchPartial( sSearchString );
//        var sWheres = sSearchQuery ? [ sSearchQuery ] : [];
//        if( self.sWhereAndSql )
//            sWheres.push( self.sWhereAndSql )
//        if( dateSql )
//            sWheres.push( dateSql );
//        if( sWheres.length )
//            result += " WHERE (" + sWheres.join( ") AND (" ) + ")";
        return result;
    }

    /**
     * (private) Build the WHERE clause
     * otherwise uses aoColumnDef mData property.
     * @param searchString
     * @return {String}
     */
    function buildWherePartial(requestQuery) {
        var sWheres = [];
        var searchQuery = buildSearchPartial(requestQuery);
        if (searchQuery)
            sWheres.push(searchQuery);
        if (self.sWhereAndSql)
            sWheres.push(self.sWhereAndSql);
        var dateSql = buildDatePartial();
        if (dateSql)
            sWheres.push(dateSql);
        if (sWheres.length)
            return " WHERE (" + sWheres.join(") AND (") + ")";
        return "";
    }

    /**
     * (private) Build the GROUP BY clause
     * @return {String}
     */
    function buildGroupByPartial() {
        if (self.sGroupBySql.length)
            return " GROUP BY " + self.sGroupBySql.join(',') + " ";
        return "";
    }

    /**
     * (private)  Builds the search portion of the WHERE clause using LIKE (or ILIKE for PostgreSQL).
     * @param {Object} requestQuery The datatable parameters that are generated by the client
     * @return {String} A portion of a WHERE clause that does a search on all searchable row entries.
     */
    function buildSearchPartial(requestQuery) {
        var query = [];
        var globalQuery = [];
        var queryString;
        var globalString = "";
        var returnValue;
        for (var sdx = 0; sdx < requestQuery.columns.length; ++sdx) {
            var globalSearch = null,
                columnSearch = null,
                column = requestQuery.columns[sdx],
                colName = self.aSearchColumns[sdx] || column.data;

            if (column.searchable === true){
                if (requestQuery.search.value){
                    globalSearch = self.dbType === 'postgres' ?
                                   buildILIKESearch(colName, requestQuery.search.value) :
                                   buildLIKESearch(colName, requestQuery.search.value);
                }

                if (column.search.value){
                    if( self.dbType === 'postgres' ) {
                       columnSearch = buildILIKESearch(colName, column.search.value);
                    } else {
                        if( column.search.regex ) {
                            // Regex is not cross-browser supported in SQLite, so it will be handled after the query runs
                            // columnSearch = buildREGEXPSearch(colName, column.search.value);
                        } else {
                            columnSearch = buildLIKESearch(colName, column.search.value);
                        }
                    }
                }

                if (globalSearch && columnSearch){
                    query.push(columnSearch);
                    globalQuery.push(globalSearch);
                } else if (globalSearch){
                    globalQuery.push(globalSearch);
                }
                else if (columnSearch){
                    query.push(columnSearch);
                }
            }
        }

        if( query.length ) {
            queryString = "(" + query.join(" AND ") + ")";
        }

        if( globalQuery.length ) {
            globalString = "(" + globalQuery.join(" OR ") + ")";
        }

        if(query.length && globalQuery.length) {
            returnValue = queryString + " AND " + globalString;
        } else if (query.length) {
            returnValue = queryString;
        } else if (globalString.length) {
            returnValue = globalString;
        } else {
            returnValue = undefined;
        }

        return returnValue;
    }

    /**
     * (private) Builds the search portion of the WHERE clause using ILIKE
     * @param {string} colName The column to search
     * @param {string} searchVal The value to search for
     * @returns {string} An ILIKE statement to be added to the where clause
     */
    function buildILIKESearch(colName, searchVal) {
        return "CAST(" + colName + " as text)" + " ILIKE '%" + searchVal + "%'";
    }

    /**
     * (private) Builds the search portion of the WHERE clause using LIKE
     * @param {string} colName The column to search
     * @param {string} searchVal The value to search for
     * @returns {string} A LIKE statement to be added to the where clause
     */
    function buildLIKESearch(colName, searchVal) {
        return colName + " LIKE '%" + searchVal + "%'";
    }

    /**
     * (private) Builds the search portion of the WHERE clause using LIKE
     * @param {string} colName The column to search
     * @param {string} searchVal The value to search for
     * @returns {string} A LIKE statement to be added to the where clause
     */
    function buildREGEXPSearch(colName, searchVal) {
        return colName + " REGEXP '" + searchVal + "'";
    }

    /**
     * (private) Adds an ORDER clause
     * @param requestQuery The Datatable query string (we look at sort direction and sort columns)
     * @return {String} The ORDER clause
     */
    function buildOrderingPartial(requestQuery) {
        var query = [];
        for (var fdx = 0; fdx < requestQuery.order.length; ++fdx) {
            var order = requestQuery.order[fdx],
                column = requestQuery.columns[order.column];

            if (column.orderable === true && column.data) {
                query.push(column.data + " " + order.dir);
            }
        }
        if (query.length)
            return " ORDER BY " + query.join(", ");
        return "";
    }

    /**
     * Build a LIMIT clause
     * @param requestQuery The Datatable query string (we look at length and start)
     * @return {String} The LIMIT clause
     */
    function buildLimitPartial(requestQuery) {
        var sLimit = "";
        if (requestQuery && requestQuery.start !== undefined && self.dbType !== 'oracle') {
            var start = parseInt(requestQuery.start, 10);
            if (start >= 0) {
                var len = parseInt(requestQuery.length, 10);
                sLimit = (self.dbType === 'postgres') ? " OFFSET " + String(start) + " LIMIT " : " LIMIT " + String(start) + ", ";
                sLimit += ( len > 0 ) ? String(len) : String(DEFAULT_LIMIT);
            }
        }
        return sLimit;
    }

    /**
     * Build the base SELECT statement.
     * @return {String} The SELECT partial
     */
    function buildSelectPartial() {
        var query = "SELECT ";
        query += self.sSelectSql ? self.sSelectSql : "*";
        query += " FROM ";
        query += self.sFromSql ? self.sFromSql : self.sTableName;
        return query;
    }

    /**
     * Build an array of query strings based on the Datatable parameters
     * @param requestQuery The datatable parameters that are generated by the client
     * @return {Object} An array of query strings, each including a terminating semicolon.
     */
    function buildQuery(requestQuery) {
        var queries = {};
        if (typeof requestQuery !== 'object')
            return queries;
        var searchString = sanitize(requestQuery.search.value);
        self.oRequestQuery = requestQuery;
        var useStmt = buildSetDatabaseOrSchemaStatement();
        if (useStmt) {
            queries.use = useStmt;
        }
        queries.recordsTotal = buildCountStatement(requestQuery, "recordsTotal");
        if (searchString) {
            queries.recordsFiltered = buildCountStatement(requestQuery, "recordsFiltered");
        }
        var query = buildSelectPartial();
        query += buildWherePartial(requestQuery);
        query += buildGroupByPartial();
        query += buildOrderingPartial(requestQuery);
        // Due to the fact that regex filtering must occur after the query has run, we can't paginate until after the regex has been applied (which means a potentially large query result set up until pagination)
        // query += buildLimitPartial(requestQuery);
        if (self.dbType === 'oracle'){
            var start = parseInt(requestQuery.start, 10);
            var len = parseInt(requestQuery.length, 10);
            if (len >= 0 && start >= 0) {
                query = 'SELECT * FROM (SELECT a.*, ROWNUM rnum FROM (' + query + ') ';
                query += 'a WHERE ROWNUM <= ' + (start + len) + ') WHERE rnum >= ' + start;
            }
        }
        queries.select = query;
        return queries;
    }

    /**
     * Parse the responses from the database and build a Datatable response object.
     * @param queryResult An array of SQL response objects, each of which must, in order, correspond with a query string
     * returned by buildQuery.
     * @return {Object} A Datatable reply that is suitable for sending in a response to the client.
     */
    function parseResponse(queryResult) {
        var oQuery = self.oRequestQuery;
        var result = { recordsFiltered: 0, recordsTotal: 0 };
        if (oQuery && typeof oQuery.draw === 'string') {
            // Cast for security reasons, as per http://datatables.net/usage/server-side
            result.draw = parseInt(oQuery.draw,10);
        } else {
            result.draw = 0;
        }
        if (_u.isObject(queryResult) && _u.keys(queryResult).length > 1) {
            result.recordsFiltered = result.recordsTotal = extractCount(queryResult.recordsTotal);
            if (queryResult.recordsFiltered) {
                result.recordsFiltered = extractCount(queryResult.recordsFiltered);
            }
            result.data = queryResult.select;
        }
        return result;
    }

    /**
     * (private)
     * @param obj
     * @return {*}
     */
    function extractCount(obj) {
        var values;
        if (obj && obj.length)
            values = _u.values(obj[0]);
        if (values && values.length)
            return values[0];
        return 0;
    }

    /**
     * Debug, reduced size object for display
     * @param obj
     * @return {*}
     */
    function filteredResult(obj, count) {
        if (obj) {
            var result = _u.omit(obj, self.sAjaxDataProp );
            result.aaLength = obj[self.sAjaxDataProp] ? obj[self.sAjaxDataProp].length : 0;
            result[self.sAjaxDataProp] = [];
            var count = count ? Math.min(count, result.aaLength) : result.aaLength;
            for (var idx = 0; idx < count; ++idx) {
                result[self.sAjaxDataProp].push(obj[self.sAjaxDataProp][idx]);
            }
            return result;
        }
        return null;
    }

    return self;
}

/**
 * Sanitize to prevent SQL injections.
 * @param str
 * @return {*}
 */
function sanitize(str, len) {
    len = len || 256;
    if (!str || typeof str === 'string' && str.length < 1)
        return str;
    if (typeof str !== 'string' || str.length > len)
        return null;
    return str.replace(/[\0\x08\x09\x1a\n\r"'\\\%]/g, function (char) {
        switch (char) {
            case "\0":
                return "\\0";
            case "\x08":
                return "\\b";
            case "\x09":
                return "\\t";
            case "\x1a":
                return "\\z";
            case "\n":
                return "\\n";
            case "\r":
                return "\\r";
            case "\"":
            case "'":
            case "\\":
            case "%":
                return "\\" + char; // prepends a backslash to backslash, percent,
            // and double/single quotes
        }
    });
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant