UNPKG

node-querybuilder

Version:

Modeled after Codeigniter's QueryBuilder. Build and execute queries in a safe and database-agnostic way.

1,386 lines (1,154 loc) 55.9 kB
class GenericQueryBuilder { constructor() { this.reserved_identifiers = ['*']; this.where_array = []; this.where_in_array = []; this.from_array = []; this.join_array = []; this.select_array = []; this.set_array = []; // has to be array to work as reference this.order_by_array = []; this.group_by_array = []; this.having_array = []; this.limit_to = []; // has to be array to work as reference this.offset_val = []; // has to be array to work as reference this.join_clause = []; this.last_query_string = []; // has to be array to work as reference this.distinct_clause = []; // has to be array to work as reference this.aliased_tables = []; this.returning_ids = []; } // ------------------------------ GENERIC METHODS ------------------------------// _array_values(item) { const keys = Object.keys(item); const length = keys.length; const values = Array(length); for (let i = 0; i < length; i++) { values[i] = item[keys[i]]; } return values; } // Simply setting all properties to [] causes reference issues in the parent class. _clear_array(a, debug) { if (debug === true) { console.log("DEBUG before (" + Object.prototype.toString.call(a) + "):"); console.dir(a); } if (Object.prototype.toString.call(a) === Object.prototype.toString.call({})) { for (let key in a) { if (a.hasOwnProperty(key)) { delete a[key]; } } } else if (Array.isArray(a)) { while (a.length > 0) { a.pop(); } } if (debug === true) { console.log("DEBUG after (" + Object.prototype.toString.call(a) + "):"); console.dir(a); } } _extract_having_parts(key, key_array) { let m; key = key.trim().replace(/\s+/g,' '); const str_condition = /^([^\s]+\s(<=|>=|<>|>|<|!=|=))+\s"([^"]+)"$/; const sstr_condition = /^([^\s]+\s(<=|>=|<>|>|<|!=|=))+\s'([^']+)'$/; const num_condition = /^([^\s]+\s(<=|>=|<>|>|<|!=|=))+\s((?=.)([+-]?([0-9]*)(\.([0-9]+))?))$/; const bool_condition = /^([^\s]+\s(<=|>=|<>|>|<|!=|=))+\s((true|false)+)$/; if (m = str_condition.exec(key)) { key_array[m[1]] = m[3]; key = key_array; } else if (m = sstr_condition.exec(key)) { //console.log("Key has sstring value"); key_array[m[1]] = m[3]; key = key_array; } else if (m = num_condition.exec(key)) { //console.log("Key has numeric value"); key_array[m[1]] = m[3]; key = key_array; } else if (m = bool_condition.exec(key)) { //console.log("Key has boolean value"); key_array[m[1]] = m[3]; key = key_array; } else { throw new Error("An invalid condition was supplied (" + key + ") in your having statement!"); } return key_array; } _prepare_for_limit_and_offset(item, type = 'limit') { type = type.toLowerCase(); if (!/^(string|number)$/.test(typeof item)) { throw new Error("Only integers or integers in the form of a string are allowed"); } if (typeof item === 'string') { item = item.trim(); if (!/^\d+$/.test(item)) { throw new Error(`The string you provided to ${type} by contains non-integer values--this isn't allowed.`); } // Force to an integer item = parseInt(item); } // Make sure the number is a good one if (typeof item === 'number') { // Check for NaN and Infinity if (item !== +item || item === Infinity) { throw new Error(`You have not provided a valid number to ${type} by!`); } // Make sure it's positive if (item < 0) { throw new Error(`Only positive integers are allowed when ${(type == 'offset' ? 'offset' : 'limit')}ing SQL results!`); } // Only allow integers if (item % 1 !== 0) { throw new Error(`You cannot ${type} a SQL resultset with a floating point value!`); } } else { throw new Error(`There was an unrecoverable error while parsing the value provided in your ${type} statement.`); } return item; } // ---------------------------------------- SQL ESCAPE FUNCTIONS ------------------------ // /** * Escape the SQL Identifiers * * This function escapes column and table names * * @param mixed item Identifier to escape * @param bool split Whether to split identifiers when a dot is encountered * @return mixed */ _escape_identifiers(item='*', split=true) { if (item === '*') return item; // If object is supplied, escape the value of each key if (Object.prototype.toString.call(item) === Object.prototype.toString.call({})) { for (let i in item) { item[i] = this._escape_identifiers(item[i]); } return item; } // Avoid breaking functions and literal values inside queries else if ((typeof item === 'string' && /^\d+$/.test(item)) || item[0] === "'" || item.indexOf('(') !== -1) { return item; } let str; let escape_chars = []; if (Array.isArray(this.escape_char)) { escape_chars = this.escape_char; } else { escape_chars[0] = escape_chars[1] = this.escape_char; } this.reserved_identifiers.forEach(v => { if (item.indexOf(v) === -1) { return item.replace(RegExp(`\\${escape_chars[0]}?([^\\${escape_chars[1]}\.]+)\\${escape_chars[1]}?\.`, 'ig'), `${escape_chars[0]}$1${escape_chars[1]}.`); } }); const dot = (split !== false ? '\.' : ''); return item.replace(RegExp(`\\${escape_chars[0]}?([^\\${escape_chars[1]}${dot}]+)\\${escape_chars[1]}?(\.)?`, 'ig'), `${escape_chars[0]}$1${escape_chars[1]}$2`); } /** * Protect Identifiers * * Takes a column or table name (optionally with an alias) and inserts * the table prefix onto it. Some logic is necessary in order to deal with * column names that include the path. Consider a query like this: * * SELECT hostname.database.table.column AS c FROM hostname.database.table * * Or a query with aliasing: * * SELECT m.member_id, m.member_name FROM members AS m * * Since the column name can include up to four segments (host, DB, table, column) * or also have an alias prefix, we need to do a bit of work to figure this out and * insert the table prefix (if it exists) in the proper position, and escape only * the correct identifiers. * * @param string item * @param bool prefix_single * @param mixed protect_identifiers * @param bool field_exists * @return string */ _protect_identifiers(item, prefix_single=false, protect_identifiers=null, field_exists=true) { if (item === '') return item; protect_identifiers = (typeof protect_identifiers === 'boolean' ? protect_identifiers : true); if (Object.prototype.toString.call(item) === Object.prototype.toString.call({})) { const escaped_array = {}; for (let k in item) { const v = item[k]; escaped_array[this._protect_identifiers(k)] = this._protect_identifiers(v); } return escaped_array; } // Make sure item is a string... if (typeof item !== 'string') throw new Error("Invalid item passed to _protect_identifiers:" + typeof item); // Convert tabs or multiple spaces into single spaces item = item.trim().replace(/\s+/g, ' '); let alias = ''; // This is basically a bug fix for queries that use MAX, MIN, subqueries, etc. // If a parenthesis is found we know that we do not need to // escape the data or add a prefix. const match = /[\(\)\']{1}/.exec(item) if (match && match.index !== item.length) { const has_alias = item.lastIndexOf(')'); if (has_alias >= 0) { alias = item.substr(has_alias + 1).replace(/\sAS\s/i,'').trim(); alias = this._escape_identifiers(alias); if (alias != '') alias = ' AS ' + alias; item = item.substr(0, has_alias + 1); } else { alias = ''; } return item + alias; } // If the item has an alias declaration we remove it and set it aside. // Basically we remove everything to the right of the first space if (/\sAS\s/ig.test(item)) { const offset = item.indexOf(item.match(/\sAS\s/ig)[0]); alias = (protect_identifiers ? item.substr(offset, 4) + this._escape_identifiers(item.slice(offset + 4), false) : item.substr(offset)); item = item.substr(0, offset); } else if (item.indexOf(' ') !== -1) { const alias_index = item.indexOf(' '); alias = (protect_identifiers && ! this._has_operator(item.substr(alias_index + 1)) ? ' ' + this._escape_identifiers(item.substr(alias_index + 1)) : item.substr(alias_index)); item = item.substr(0, alias_index); } // Break the string apart if it contains periods, then insert the table prefix // in the correct location, assuming the period doesn't indicate that we're dealing // with an alias. While we're at it, we will escape the components if (item.indexOf('.') !== -1) { let parts = item.split('.'); const first_seg = parts[0].trim();//.replace(/`/g,''); // Does the first segment of the exploded item match // one of the aliases previously identified? If so, // we have nothing more to do other than escape the item if (this.aliased_tables.indexOf(first_seg) !== -1) { if (protect_identifiers === true) { parts = parts.map((v,i) => { if (!this.reserved_identifiers.includes(v)) { return this._escape_identifiers(v); } else { return v; } }); item = parts.join('.'); } return item + alias; } if (protect_identifiers === true) { item = this._escape_identifiers(item); } return item + alias; } if (protect_identifiers === true) { item = this._escape_identifiers(item); } return (item + alias).trim(); } _track_aliases(table) { if (Object.prototype.toString.call(table) === Object.prototype.toString.call({})) { for (let i in table) { const t = table[i]; this._track_aliases(t); } return; } // Does the string contain a comma? If so, we need to separate // the string into discreet statements if (table.indexOf(',') !== -1) { return this._track_aliases(table.split(',')); } // if a table alias is used we can recognize it by a space if (table.indexOf(' ') !== -1) { // if the alias is written with the AS keyword, remove it table = table.replace(/\s+AS\s+/gi, ' '); // Grab the alias const alias = table.slice(table.lastIndexOf(' ')).trim().replace(/`/g,'').replace(/\[([^\]]+)\]/, "$1"); // Store the alias, if it doesn't already exist if (this.aliased_tables.indexOf(alias) == -1) { this.aliased_tables.push(alias); } } } _create_aliases_from_table(item) { if (item.indexOf('.') !== -1) { return item.split('.').reverse()[0]; } return item; } _has_operator(str) { if (typeof str === 'string' && str.length > 0) { const match = /(<|>|!|=|\sIS NULL|\sIS NOT NULL|\sEXISTS|\sBETWEEN|\sLIKE|\sCASE|\sTHEN|\sWHEN|\sIN\s*\(|\s)/i.test(str.trim()); if (!match) { return false; } } return true; }; // ---------------------------- SQL BUILD TOOLS ----------------------------// _build_where_clause() { if (this.where_array.length === 0) return ''; return `WHERE ${this.where_array.join(" ")}`; }; _build_from_clause() { let sql = ''; if (this.from_array.length === 0) { throw new Error("You have not provided any tables, views, or store procedures for this query!!"); } sql = `FROM ${this.from_array.join(', ')}`; return sql.trim(); }; _build_join_string() { if (this.join_array.length <= 0) return ''; return this.join_array.join(' '); }; _build_group_by_clause() { if (this.group_by_array.length <= 0) return ''; return `GROUP BY ${this.group_by_array.join(', ')}`; }; _build_having_clause() { if (this.having_array.length <= 0) return ''; return `HAVING ${this.having_array.join(' ')}`; }; _build_order_by_clause() { if (this.order_by_array.length <= 0) return ''; return `ORDER BY ${this.order_by_array.join(', ')}`; }; reset_query(new_last_query, debug=false) { this._clear_array(this.where_array, debug); this._clear_array(this.where_in_array); this._clear_array(this.from_array); this._clear_array(this.join_array); this._clear_array(this.select_array); this._clear_array(this.set_array); this._clear_array(this.order_by_array); this._clear_array(this.group_by_array); this._clear_array(this.having_array); this._clear_array(this.limit_to); this._clear_array(this.offset_val); this._clear_array(this.join_clause); this._clear_array(this.distinct_clause); this._clear_array(this.aliased_tables); this._clear_array(this.returning_ids); this._clear_array(this.last_query_string); if (typeof new_last_query === 'string') { this.last_query_string.push(new_last_query); } } where(key, value=null, escape=true) { if (Object.prototype.toString.call(key) === Object.prototype.toString.call({}) && typeof value === 'boolean') { escape = value; } escape = (typeof escape === 'boolean' ? escape : true); if (typeof key === 'string' && Array.isArray(value) && value.length > 0) { return this._where_in(key, value, false, 'AND '); } return this._where(key, value, 'AND ', escape); } or_where(key, value=null, escape=true) { escape = (typeof escape === 'boolean' ? escape : true); if (typeof key === 'string' && typeof value === 'object' && Array.isArray(value) && value.length > 0) { return this._where_in(key, value, false, 'OR '); } return this._where(key, value, 'OR ', escape); } _where(key, value=null, type='AND ', escape=true) { escape = (typeof escape === 'boolean' ? escape : true); // If key is not an object.... if (Object.prototype.toString.call(key) !== Object.prototype.toString.call({})) { // If it's not an object, it must be a string if (typeof key !== 'string') { throw new Error(`where(): If first parameter is not an object, it must be a string. ${typeof key} provided.`); } else { // If it is a string, it can't be an empty one if (key.length == 0) { throw new Error("where(): No field name or query provided!"); } } // If it's a actual where clause string (with no parantheses), // not just a field name, split it into individual parts to escape it properly if (/(<=|>=|<>|>|<|!=|=)/.test(key) && key.indexOf('(') === -1 && escape === true) { const filters = key.split(/\s+(AND|OR)\s+/i); if (filters.length > 1) { const self = this; const parse_statement = (statement, joiner) => { const parsed = statement.match(/^([^<>=!]+)(<=|>=|<>|>|<|!=|=)(.*)$/); if (parsed.length >= 4) { const key = parsed[1].trim() + (parsed[2].trim() !== '=' ? ' ' + parsed[2].trim() : ''); const value = parsed[3].trim().replace(/^((?:'|"){1})(.*)/, "$2").replace(/'$/,''); if (joiner === null || /AND/i.test(joiner)) { self.where(key, value, true); } else { self.or_where(key, value, true); } } }; parse_statement(filters.shift(), null); while (filters.length > 0) { const joiner = filters.shift(); const statement = filters.shift(); parse_statement(statement, joiner); } return this; } } const key_array = {}; key_array[key] = value; key = key_array; } // Fail if its an empty object if (Object.keys(key).length === 0) { throw new Error("where(): You haven't provided any key value pairs to limit the resultset by."); } // If an object is supplied... for (let k in key) { let v = key[k]; if (Array.isArray(v) && v.length > 0) { this._where_in(k, v, false, type, escape); continue; } const prefix = (this.where_array.length == 0 ? '' : type); if (v === null && !this._has_operator(k)) { k += ' IS NULL'; } if (v !== null) { if (escape === true) { k = this._protect_identifiers(k, false, escape); v = ' ' + this._qb_escape(v); } if (escape !== true && Object.prototype.toString.call(key) === Object.prototype.toString.call({})) { v = ' ' + this._qb_escape(v); } if (!this._has_operator(k)) { k += ' ='; } } else { k = this._protect_identifiers(k, false, escape); } if (v) { this.where_array.push(prefix+k+v); } else { this.where_array.push(prefix+k); } } return this; } where_in(key, values, escape=true) { return this._where_in(key, values, false, 'AND ', escape); } or_where_in(key, values, escape=true) { return this._where_in(key, values, false, 'OR ', escape); } where_not_in(key, values, escape=true) { return this._where_in(key, values, true, 'AND ', escape); } or_where_not_in(key, values, escape=true) { return this._where_in(key, values, true, 'OR ', escape); } _where_in(key='', values=[], not, type='AND ', escape=true) { not = (not ? ' NOT' : ''); escape = (typeof escape === 'boolean' ? escape : true); if (typeof key !== 'string' || (typeof key === 'string' && key.length == 0)) { throw new Error("where_" + (not === '' ? '' : not.toLowerCase() + '_') + "in(): Invalid field name provided."); } // `values` must be an array... if (!Array.isArray(values)) { throw new Error("where_" + (not === '' ? '' : not.toLowerCase() + '_') + "in(): Invalid second parameter provided--it must be an array of scalar values or an empty array."); } // If array is empty, ignore this request else if (values.length === 0) return; for (let i in values) { this.where_in_array.push(this._qb_escape(values[i])); } const prefix = (this.where_array.length === 0 ? '' : type); const where_in = prefix + this._protect_identifiers(key, false, escape) + not + " IN (" + this.where_in_array.join(', ') + ")"; this.where_array.push(where_in); // reset the array for multiple calls this._clear_array(this.where_in_array); return this; } like(field, match, side) { return this._like(field, match, 'AND ', side, ''); } not_like(field, match, side) { return this._like(field, match, 'AND ', side, ' NOT'); } or_like(field, match, side) { return this._like(field, match, 'OR ', side, ''); } or_not_like(field, match, side) { return this._like(field, match, 'OR ', side, ' NOT'); } _like(field, match, type='AND ', side='both', not='') { match = (/^(string|number|boolean)$/.test(typeof match) ? match : null); if (typeof field === 'string' && field.length == 0) { throw new Error("like(): The field you provided is empty."); } else if (typeof field === 'object' && (field.length == 0 || Object.keys(field).length === 0)) { throw new Error("like(): The object you provided is empty."); } else if (!/^(string|object)$/.test(typeof field)) { throw new Error("like(): You have provided an invalid value as the first parameter. Only valid strings and objects are allowed."); } if (Object.prototype.toString.call(field) !== Object.prototype.toString.call({})) { if (match === null) { throw new Error("like(): Since your first parameter is a string, your second param must a valid number, boolean, or string."); } const field_array = {}; field_array[field] = match; field = field_array; } for (let k in field) { let like_statement; let v = field[k]; k = this._protect_identifiers(k.trim()); // Make sure value is only string, number, or boolean if (!/^(string|number|boolean)$/.test(typeof v)) { throw new Error("like(): You have provided an invalid value as the second parameter. Only valid strings, numbers, and booleans are allowed."); } // If number, don't allow Infinity or NaN else if (typeof v === 'number' && (v === Infinity || (v !== +v))) { throw new Error("like(): You have provided an invalid number value as the second parameter. Only valid strings, numbers, and booleans are allowed."); } // Make sure to escape the value... v = this._qb_escape(v); if (side === 'none') { like_statement = k + not + ` LIKE ${v}`; } else if (side === 'before' || side === 'left') { if (typeof v === 'string') { like_statement = k + not + ` LIKE ${v.substr(0, 1)}%${v.substr(1)}`; } else { like_statement = k + not + ` LIKE %${v}`; } } else if (side === 'after' || side === 'right') { if (typeof v === 'string') { like_statement = k + not + ` LIKE ${v.substr(0, v.length -1)}%${v.slice(-1)}`; } else { like_statement = k + not + ` LIKE ${v}%`; } } else if (side === 'both') { if (typeof v === 'string') { like_statement = k + not + ` LIKE ${v.substr(0, 1)}%${v.substr(1, v.length -2)}%${v.slice(-1)}`; } else { like_statement = k + not + ` LIKE %${v}%`; } } else { throw new Error("like(): Invalid direction provided!"); } this._where(like_statement, null, type, false); } return this; } from(from_param) { if (!Array.isArray(from_param)) { from_param = [from_param]; } for (let i in from_param) { let val = from_param[i]; if (typeof val !== 'string' || val.trim() === '') continue; if (val.indexOf(',') !== -1) { const objects = val.split(','); for (let j in objects) { const v = objects[j].trim(); this._track_aliases(v); this.from_array.push(this._protect_identifiers(v, false, true)); } } else { val = val.trim(); // Extract any aliases that might exist. We use this information // in the protect_identifiers function to know whether to add a table prefix this._track_aliases(val); this.from_array.push(this._protect_identifiers(val, false, true)); } } return this; } select(select, escape=true) { // First param must be a non-empty string or array if (typeof select === 'string') { select = select.trim(); if (select.length == 0) { throw new Error("Your select string is empty!"); } } else if (Array.isArray(select)) { if (select.length == 0) { throw new Error("Your select array is empty!"); } } else { throw new Error("Select method requires a string or array to be passed in the first parameter!"); } if (typeof escape !== 'boolean') escape = true; // Split statements out into individual ones by comma (unless there is a function or subquery with commas in it) if (typeof select === 'string') { if (select.indexOf(')') === -1) { select = select.split(','); } else { if (escape === true) { // Prevent it from trying to parse select statements with functions and if statements if (/\w\s?\(/.test(select)) throw new Error("Select statements with subqueries or functions cannot be escaped! Please escape manually and pass FALSE as the second paramter to the select method."); // Identify individual statements within select string let m, open_paren_index, inner_parenthesis; const reg = /\)/g; while ((m = reg.exec(select) !== null)) { open_paren_index = m.input.substring(0, m.index).lastIndexOf('('); if (open_paren_index !== -1) { inner_parenthesis = m.input.substring((open_paren_index + 1), m.index); if (inner.parenthesis.indexOf(',') !== -1) { throw new Error("Select statements with subqueries or functions cannot be escaped! Please escape manually and pass FALSE as the second paramter to the select method."); break; } } } } else { select = [select]; } } } for (let i in select) { const val = select[i].trim(); if (val !== '') { this.select_array.push(this._protect_identifiers(val, false, escape)); } } return this; } select_min(select, alias) { return this._min_max_avg_sum(select, alias, 'MIN'); } select_max(select, alias) { return this._min_max_avg_sum(select, alias, 'MAX'); } select_avg(select, alias) { return this._min_max_avg_sum(select, alias, 'AVG'); } select_sum(select, alias) { return this._min_max_avg_sum(select, alias, 'SUM'); } _min_max_avg_sum(select='', alias='', type='MAX') { if (typeof select !== 'string' || select === '') { throw Error("Invalid query!"); return this; } type = type.toUpperCase(); if (['MAX', 'MIN', 'AVG', 'SUM'].indexOf(type) === -1) { throw Error("Invalid function type!"); return this; } if (alias == '') { alias = this._create_aliases_from_table(select.trim()); } const sql = `${type}(${this._protect_identifiers(select.trim())}) AS ${alias}`; this.select_array.push(sql); return this; } distinct(do_distinct) { do_distinct = (typeof do_distinct !== 'boolean' ? true : do_distinct); if (do_distinct) { this._clear_array(this.distinct_clause); this.distinct_clause.push('DISTINCT '); } else { this._clear_array(this.distinct_clause); } return this; } group_by(by) { if (typeof by === 'string') { by = by.trim(); if (by.length <= 0) { throw new Error("You haven't provided any fields to group by!"); } by = by.split(','); } if (!Array.isArray(by)) { throw new Error("You have provided an invalid value to the group_by() method. Only strings and arrays of strings are allowed."); } if (by.length <= 0) { throw new Error("You haven't provided any fields to group by!"); } for (let key in by) { if (typeof by[key] !== 'string') { throw new Error("You have provided an invalid value to the group_by() method. Only strings and arrays of strings are allowed!"); } const val = by[key].trim(); if (val !== '') { this.group_by_array.push(this._protect_identifiers(val)); } } return this; } having(key, value, escape=true) { escape = (typeof escape !== 'boolean' ? true : escape); return this._having(key, value, 'AND ', escape); } or_having(key, value, escape=true) { escape = (typeof escape !== 'boolean' ? true : escape); return this._having(key, value, 'OR ', escape); } _having(key, value, type='AND ', escape=true) { let m; let key_array = {}; const key_is_object = Object.prototype.toString.call(key) === Object.prototype.toString.call({}); const key_is_array = Array.isArray(key); if (/^(string|number|boolean)$/.test(typeof value)) { // if the value is a string, number, or boolean... if (typeof key !== 'string' || /^\W+$/i.test(key)) { // if the key is not a string... throw new Error("having(): The value you provided when calling having() will be ignored since the first parameter is not a single field provided in string form."); } key_array[key] = value; key = key_array; } else if (typeof value === 'undefined' || value === null) { if (key_is_object === false) { if (typeof key === 'string') { if (value === null) { key_array[key] = null; key = key_array; } else { key = this._extract_having_parts(key, key_array); } } else if (key_is_array === true) { //console.log("Key is NOT a string"); for (let i in key) { if (typeof key[i] !== 'string') { throw new Error("having(): You've provided an unparseable format to the having() method.."); } else { key_array = this._extract_having_parts(key[i], key_array); } } key = key_array; } } } else { throw new Error(`Improper value supplied as the second parameter (${typeof value}) of the having() method.`); } for (let k in key) { let v = key[k]; const prefix = (this.having_array.length == 0 ? '' : type); if (escape === true) { k = this._protect_identifiers(k); } if (v === null) { k += ' IS'; } else if (!this._has_operator(k)) { k += ' ='; } if (v != '') { v = ' ' + this._qb_escape(v); } this.having_array.push(prefix + k + v); } return this; } join(table='', relation='', direction='', escape=true) { if (typeof table !== 'string' || (typeof table === 'string' && table.trim().length === 0)) { throw new Error("You must provide a table, view, or stored procedure to join to!"); } relation = (typeof relation === 'string' && relation.trim().length !== 0 ? relation.trim() : ''); direction = (typeof direction === 'string' && direction.trim().length !== 0 ? direction.trim() : ''); escape = (typeof escape === 'boolean' ? escape : true); const valid_directions = ['LEFT', 'RIGHT', 'OUTER', 'INNER', 'LEFT OUTER', 'RIGHT OUTER']; if (direction) { direction = direction.toUpperCase().trim(); if (!valid_directions.includes(direction)) { throw new Error("Invalid join direction provided as third parameter."); } if (!relation) { throw new Error("You must provide a valid condition to join on when providing a join direction."); } } // Keep track of the table alias (if one is provided) this._track_aliases(table); // How to split a condition (foo=bar) into its consituent parts // Find all the conditions and protect their identifiers if (escape === true && this.multi_condition_rgx.test(relation)) { const new_relation = relation.split(this.multi_condition_rgx).map((v,i) => { if (i % 2 !== 0) return v.trim(); const match = v.match(this.condition_rgx); if (!match) return v.trim(); return `${this._protect_identifiers(match[1].trim(), false, escape)} ${match[2].trim()} ${this._protect_identifiers(match[3].trim(), false, escape)}`; }).join(' '); relation = `ON ${new_relation}`; } // Split apart the condition and protect the identifiers else if (relation && escape !== false && this.condition_rgx.test(relation)) { const match = relation.match(this.condition_rgx); relation = `ON ${this._protect_identifiers(match[1].trim(), false, escape)} ${match[2].trim()} ${this._protect_identifiers(match[3].trim(), false, escape)}`; } else if (relation && !this._has_operator(relation)) { relation = `USING (${(escape ? this._escape_identifiers(relation) : relation)})`; } else if (relation && escape === false) { relation = `ON ${relation}`; } else { relation = ''; } // Do we want to escape the table name? if (escape === true) { table = this._protect_identifiers(table, false, true); } this.join_array.push(`${direction} JOIN ${table} ${relation}`.trim()); return this; } order_by(orderby, direction) { let m; direction = (typeof direction === 'string' ? direction.toLowerCase().trim() : ''); // Don't need to do anything below if the direction provided is random if ((direction === 'random' || direction === 'rand' || direction === 'rand()')) { this.order_by_array.push(this.rand_word); return this; } // Normalize orderby to be an array of items if (!Array.isArray(orderby)) { if (typeof orderby === 'string') { orderby = orderby.trim(); if (orderby.length == 0) { throw new Error("You haven't provided any fields to order by!!"); } orderby = orderby.split(','); } else if (!orderby && (/(newid|random|RAND|RAND\(\))/i.test(direction))) { this.order_by_array.push(this.rand_word); return this; } else { throw new Error("No field provided to order by!"); } } if (orderby.length === 0) { throw new Error("You haven't provided any fields to order by!"); } for (let i in orderby) { orderby[i] = orderby[i].replace(/\s+/g, ' '); if (m = orderby[i].match(/([^\s]+)\s+(ASC|DESC|RAND\(\)|NEWID\(\))/i)) { if (m[2].trim() === this.rand_word) { this.order_by_array.push(this.rand_word); return this; } orderby[i] = {field: this._protect_identifiers(m[1].trim()), direction: m[2].trim().toUpperCase()}; } else { if (/^(ASC|DESC)$/i.test(direction) || direction === '') { orderby[i] = {field: this._protect_identifiers(orderby[i].trim()), direction: (direction !== '' ? direction.toUpperCase() : 'ASC')}; } else { throw new Error("Invalid direction provided in order_by method! Only 'ASC', 'DESC', and 'RAND' are allowed!"); } } this.order_by_array.push(orderby[i].field + ' ' + orderby[i].direction); } return this; } limit(limit, offset) { this._clear_array(this.limit_to); this.limit_to.push(this._prepare_for_limit_and_offset(limit, 'limit')); if (offset !== undefined) { return this.offset(offset); } return this; } offset(offset) { this._clear_array(this.offset_val); this.offset_val.push(this._prepare_for_limit_and_offset(offset, 'offset')); return this; } returning(ids) { // By default, this will do nothing. Specific drivers will override as needed. return this; } set(key, value, escape=true) { escape = (typeof escape === 'boolean' ? escape : true); if (typeof key === 'string') { // Convert key and value params to {key: value} key = key.trim(); if (key.length == 0) throw new Error("set(): Invalid field name provided!"); if (typeof value === 'undefined') throw new Error("set(): First param was string but no value (second param) provided to set!"); const key_array = {}; key_array[key] = value; key = key_array; } else if (Object.prototype.toString.call(key) === Object.prototype.toString.call({})) { if (Object.keys(key).length === 0) { throw new Error("set(): The object you provided is empty."); } if (typeof value !== 'undefined' & value !== null) { throw new Error("set(): The value you provided in the second parameter will be ignored since you passed an object as the first parameter."); } } else { throw new Error("set(): First parameter must be a non-empty string or non-empty object! " + typeof key + " provided."); } // Add each key:value pair to the set_array for (let i in key) { let v = key[i]; if (typeof v === 'undefined') continue; if (v instanceof Date) v = v.toString(); if (!/^(number|string|boolean)$/.test(typeof v) && v !== null) { throw new Error("set(): Invalid value provided! (provided: " + v + " (type: " + (typeof v) + ")"); } else if (typeof v === 'number' && (v === Infinity || v !== +v)) { throw new Error("set(): Infinity and NaN are not valid values in MySQL!"); } // Escape the key to be DRY const escaped_key = this._protect_identifiers(i, false, escape); // Build a temporary object with escaped key and val const temp = {}; if (escape === false) { temp[escaped_key] = v; } else { temp[escaped_key] = this._qb_escape(v); } // Determine if this key has already been set let found_index = null; for (let j in this.set_array) { if (this.set_array[j].hasOwnProperty(escaped_key)) { found_index = j; break; } } // Update value if key already set or add if not found if (found_index !== null) { this.set_array[found_index] = temp; } else { this.set_array.push(temp); } } //console.dir(this.set_array); return this; } // ---------------------------- SQL EXEC TOOLS ----------------------------// insert(table='', set='', ignore=false, suffix='') { return this._insert(table, set, ignore, suffix); } _insert(table='', set='', ignore=false, suffix='') { table = table || ''; // force falsy values to be an empty string ignore = (typeof ignore !== 'boolean' ? false : ignore); suffix = (typeof suffix !== 'string' ? '' : suffix); if (/^(number|boolean)$/.test(typeof set) || (typeof set == 'string' && set !== '') || Object.prototype.toString.call(set) === Object.prototype.toString.call(/test/)) { throw new Error("insert(): Invalid data provided to insert into database!"); } if (Array.isArray(set)) { return this._insert_batch(table, set, ignore, suffix); } if (set !== null) { if (Object.prototype.toString.call(set) === Object.prototype.toString.call({}) && Object.keys(set).length > 0) { this.set(set); } } if (typeof table !== 'string') { throw new Error("insert(): Table parameter must be a string!"); } table = table.trim(); if (table !== '' && !(/^[a-zA-Z0-9\$_]+(\.[a-zA-Z0-9\$_]+)?$/).test(table)) { throw new Error("insert(): Invalid table name ('" + table + "') provided!"); } if (table === '') { if (this.from_array.length == 0) throw new Error('insert(): No tables set to insert into!'); table = this.from_array[0]; } else { this._clear_array(this.from_array); this.from(table); } return this._compile_insert(ignore, suffix); } insert_ignore(table, set, suffix) { return this._insert_ignore(table, set, suffix); } _insert_ignore(table, set, suffix) { return this.insert(table, set, true, suffix); } insert_batch(table, set=null, ignore, suffix) { return this._insert_batch(table, set, ignore, suffix); } get(table) { return this._get(table); } _get(table) { if (typeof table === 'string' || Array.isArray(table)) { this.from(table); } else { if (this.from_array.length === 0) { throw new Error('You have not specified any tables to select from!'); } } return this._compile_select(); } get_where(table=null, where=null) { return this._get_where(table, where); } _get_where(table=null, where=null) { // Check if table is either a string or array if (typeof table !== 'string' && !Array.isArray(table)) throw new Error('You must specify a table or array of tables in the first parameter of get_where()'); // If table is a string, make sure it's not empty if (typeof table === 'string' && table.trim().length <= 0) throw new Error("Invalid table string specified!"); // If table is array, make sure there are only strings in there and that they are non-empty strings if (Array.isArray(table)) { for (let v in table) { if (typeof v !== 'string' || (typeof v === 'string' && v.trim().length <= 0)) { throw new Error("Invalid table string specified in array of tables!"); break; } } } this.from(table); if (where === null || typeof where !== 'object' || Object.keys(where).length === 0) throw new Error('You must supply an object of field:value pairs in the second parameter of get_where()'); this.where(where); return this._compile_select(); } count(table) { return this._count(table); } update(table, set, where=null) { return this._update(table, set, where); } _update(table='', set=null, where=null) { table = table || ''; set = set || null; // Send to batch_update if the data param is an array if (Array.isArray(set)) { let index = null; if (set.length > 0) { if (Object.prototype.toString.call(set[0]) === Object.prototype.toString.call({})) { index = Object.keys(set[0])[0]; } } if (index) { return this.update_batch(table, set, index, where); } else { throw new Error("update(): update_batch attempted but could not ascertain a valid index to use from the dataset provided."); } } // If set is a number, boolean, a non-empty string, or regex, fail if (/^(number|boolean)$/.test(typeof set) || (typeof set == 'string' && set !== '') || Object.prototype.toString.call(set) === Object.prototype.toString.call(/test/)) { throw new Error("update(): Invalid data provided to update database!"); } // If data object was provided, set it if (set !== null) { if (Object.prototype.toString.call(set) === Object.prototype.toString.call({}) && Object.keys(set).length > 0) { this.set(set); } else { throw new Error("update(): Empty data object provided. This is not allowed."); } } // Fail if, at this point, nothing has been set if (this.set_array.length == 0) { throw new Error("update(): You must set some field value pairs to update using the set method or via an object passed to the second parameter of the update method!"); } // NOTE: If falsy table provided, table will have been converted to an empty string... if (typeof table !== 'string') { throw new Error("update(): Table parameter must be a string!"); } table = table.trim(); // Table name must be in a legitimate format if (table !== '' && !/^[a-zA-Z0-9\$_]+(\.[a-zA-Z0-9\$_]+)?$/.test(table)) { throw new Error("update(): You have not set any tables to update!"); } // If table not supplied, it must have been supplied already if (table == '') { if (this.from_array.length == 0) { throw new Error('update(): No tables set to update!'); } table = this.from_array[0]; } else { this._clear_array(this.from_array); this.from(table); } // Set where array if a where statement was provided if (where !== null) { this.where(where); } return this._compile_update(); } update_batch(table='', set=null, index=null, where=null) { return this._update_batch(table, set, index, where); } delete(table, where) { return this._delete(table, where); } _delete(table, where) { if (typeof table == 'string' && table.trim().length > 0) { this._clear_array(this.from_array); this.from(table); } if (Object.prototype.toString.call(where) === Object.prototype.toString.call({}) && where !== null) { if (Object.keys(where).length === 0) { throw new Error("where(): The object you provided to limit the deletion of rows is empty. Provide NULL if you need to an empty value."); } else { this.where(where); } } return this._compile_delete(); } get_compiled_select(table) { if (typeof table !== 'undefined') { this._track_aliases(table); this.from(table); } else { if (this.from_array.length == 0) { throw new Error('You have not specified any tables to build a select statement with!'); return this; } } return this._compile_select(); } get_compiled_delete(table) { if (typeof table !== 'function') { this._track_aliases(table); this.from(table); } return this._compile_delete(); } get_compiled_update(table) { if (typeof table !== 'function') { this._track_aliases(table); this.from(table); } return this._compile_update(); } get_compiled_insert(table) { if (typeof table !== 'function') { this._track_aliases(table); this.from(table); } return this._compile_insert(); } compile_select(table) { return this.get_compiled_select(table); } compile_delete(table) { return this.get_compiled_delete(table); } compile_update(table) { return this.get_compiled_update(table); } compile_insert(table) { return this.get_compiled_insert(ta