Update dependencies and switch to Happiness code style

This commit is contained in:
Timothy Warren 2016-09-14 16:50:32 -04:00
parent d3e5da66c4
commit 8d7e4aaa8c
31 changed files with 674 additions and 656 deletions

View File

@ -1,7 +0,0 @@
{
"preset": "airbnb",
"validateIndentation": null,
"requireLineFeedAtFileEnd": null,
"disallowSpaceAfterPrefixUnaryOperators": null,
"disallowMultipleVarDecl": null
}

2
API.md
View File

@ -1,3 +1,5 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
# limit
Set the limit clause

View File

@ -95,3 +95,5 @@ As of version 2, `where` and `having` type methods parse the values passed to lo
* The `tests/adapters` folder contains examples of how to set up a connection for the appropriate database library
* The documentation generated for the latest dev build is also [Available](https://github.timshomepage.net/node-query/docs/index.html)
[![js-happiness-style](https://cdn.rawgit.com/JedWatson/happiness/master/badge.svg)](https://github.com/JedWatson/happiness)

View File

@ -14,7 +14,7 @@ class Adapter {
* @constructor
* @param {Object} instance - The connection object
*/
constructor(instance) {
constructor (instance) {
this.instance = instance;
}
@ -26,7 +26,7 @@ class Adapter {
* @param {Function} [callback] - Callback to run when a response is recieved
* @return {void|Promise} - returns a promise if no callback is passed
*/
execute(/*sql, params, callback*/) {
execute (/* sql, params, callback */) {
throw new Error('Correct adapter not defined for query execution');
}
@ -36,7 +36,7 @@ class Adapter {
* @param {*} originalResult - the original result object from the driver
* @return {Result} - the new result object
*/
transformResult(originalResult) {
transformResult (originalResult) {
throw new Error('Result transformer method not defined for current adapter');
}
@ -44,9 +44,9 @@ class Adapter {
* Close the current database connection
* @return {void}
*/
close() {
close () {
this.instance.end();
}
}
module.exports = Adapter;
module.exports = Adapter;

View File

@ -7,7 +7,7 @@ const helpers = require('./helpers');
*
* @private
*/
let Driver = {
const Driver = {
identifierStartChar: '"',
identifierEndChar: '"',
tablePrefix: null,
@ -20,9 +20,9 @@ let Driver = {
* @return {String} - The quoted sql fragment
* @private
*/
_quote(str) {
return (helpers.isString(str)
&& ! (str.startsWith(Driver.identifierStartChar) || str.endsWith(Driver.identifierEndChar))
_quote (str) {
return (helpers.isString(str) &&
!(str.startsWith(Driver.identifierStartChar) || str.endsWith(Driver.identifierEndChar))
)
? `${Driver.identifierStartChar}${str}${Driver.identifierEndChar}`
: str;
@ -36,11 +36,10 @@ let Driver = {
* @param {Number} [offset] - Number of rows to skip
* @return {String} - Modified SQL statement
*/
limit(sql, limit, offset) {
sql += ` LIMIT ${limit}`;
limit (sql, limit, offset) {
sql += ` LIMIT ${limit}`;
if (helpers.isNumber(offset))
{
if (helpers.isNumber(offset)) {
sql += ` OFFSET ${offset}`;
}
@ -53,7 +52,7 @@ let Driver = {
* @param {String} table - Table name to quote
* @return {String} - Quoted table name
*/
quoteTable(table) {
quoteTable (table) {
// Quote after prefix
return Driver.quoteIdentifiers(table);
},
@ -64,22 +63,20 @@ let Driver = {
* @param {String|Array} str - String or array of strings to quote identifiers
* @return {String|Array} - Quoted identifier(s)
*/
quoteIdentifiers(str) {
quoteIdentifiers (str) {
let hiers, raw;
let pattern = new RegExp(
`${Driver.identifierStartChar}(`
+ '([a-zA-Z0-9_]+)' + '(\((.*?)\))'
+ `)${Driver.identifierEndChar}`, 'ig');
`${Driver.identifierStartChar}(` +
'([a-zA-Z0-9_]+)' + '(((.*?)))' +
`)${Driver.identifierEndChar}`, 'ig');
// Recurse for arrays of identifiiers
if (Array.isArray(str))
{
if (Array.isArray(str)) {
return str.map(Driver.quoteIdentifiers);
}
// Handle commas
if (str.includes(','))
{
if (str.includes(',')) {
let parts = str.split(',').map(helpers.stringTrim);
str = parts.map(Driver.quoteIdentifiers).join(',');
}
@ -89,8 +86,7 @@ let Driver = {
raw = hiers.join('.');
// Fix functions
if (raw.includes('(') && raw.includes(')'))
{
if (raw.includes('(') && raw.includes(')')) {
let funcs = pattern.exec(raw);
// Unquote the function
@ -110,7 +106,7 @@ let Driver = {
* @param {String} table - Table to truncate
* @return {String} - Truncation SQL
*/
truncate(table) {
truncate (table) {
let sql = (Driver.hasTruncate)
? 'TRUNCATE '
: 'DELETE FROM ';
@ -127,13 +123,10 @@ let Driver = {
* @param {Array} [data] - The array of object containing data to insert
* @return {String} - Query and data to insert
*/
insertBatch(table, data) {
let vals = [],
fields = Object.keys(data[0]),
sql = '',
params = [],
paramString = '',
paramList = [];
insertBatch (table, data) {
const vals = [];
const fields = Object.keys(data[0]);
let sql = '';
// Get the data values to insert, so they can
// be parameterized
@ -150,17 +143,17 @@ let Driver = {
sql += `INSERT INTO ${table} (${Driver.quoteIdentifiers(fields).join(',')}) VALUES `;
// Create placeholder groups
params = Array(fields.length).fill('?');
paramString = `(${params.join(',')})`;
paramList = Array(data.length).fill(paramString);
let params = Array(fields.length).fill('?');
let paramString = `(${params.join(',')})`;
let paramList = Array(data.length).fill(paramString);
sql += paramList.join(',');
return {
sql: sql,
values: vals,
values: vals
};
},
}
};
module.exports = Driver;
module.exports = Driver;

View File

@ -1,6 +1,5 @@
'use strict';
const helpers = require('./helpers');
const QueryBuilder = require('./QueryBuilder');
// Map config driver name to code class name
@ -14,7 +13,7 @@ const dbDriverMap = new Map([
['postgres', 'Pg'],
['pg', 'Pg'],
['sqlite3', 'Sqlite'],
['sqlite', 'Sqlite'],
['sqlite', 'Sqlite']
]);
/**
@ -23,7 +22,6 @@ const dbDriverMap = new Map([
* @param {object} config - connection parameters
*/
class NodeQuery {
/**
* Constructor
*
@ -42,20 +40,20 @@ class NodeQuery {
* connection: ':memory:'
* });
*/
constructor(config) {
constructor (config) {
this.instance = null;
if (config != null) {
let drivername = dbDriverMap.get(config.driver);
if (! drivername) {
if (!drivername) {
throw new Error(`Selected driver (${config.driver}) does not exist!`);
}
let driver = require(`./drivers/${drivername}`);
let $adapter = require(`./adapters/${drivername}`);
const driver = require(`./drivers/${drivername}`);
const Adapter = require(`./adapters/${drivername}`);
let adapter = new $adapter(config.connection);
let adapter = new Adapter(config.connection);
this.instance = new QueryBuilder(driver, adapter);
}
}
@ -65,7 +63,7 @@ class NodeQuery {
*
* @return {QueryBuilder} - The Query Builder object
*/
getQuery() {
getQuery () {
if (this.instance == null) {
throw new Error('No Query Builder instance to return');
}
@ -74,4 +72,4 @@ class NodeQuery {
}
}
module.exports = (config => new NodeQuery(config));
module.exports = config => new NodeQuery(config);

View File

@ -2,308 +2,16 @@
const getArgs = require('getargs');
const helpers = require('./helpers');
const State = require('./State');
const QueryParser = require('./QueryParser');
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();
}
/**
* 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 => {
let 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':
let 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;
}
_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
* @return {Array} - modified state array
*/
_mixedSet(/* $letName, $valType, $key, [$val] */) {
const argPattern = '$letName:string, $valType:string, $key:object|string|number, [$val]';
let args = getArgs(argPattern, arguments);
let obj = {};
if (helpers.isScalar(args.$key) && !helpers.isUndefined(args.$val)) {
// Convert key/val pair to a simple object
obj[args.$key] = args.$val;
} else if (helpers.isScalar(args.$key) && helpers.isUndefined(args.$val)) {
// If just a string for the key, and no value, create a simple object with duplicate key/val
obj[args.$key] = args.$key;
} else {
obj = args.$key;
}
Object.keys(obj).forEach(k => {
// If a single value for the return
if (['key', 'value'].indexOf(args.$valType) !== -1) {
let pushVal = (args.$valType === 'key') ? k : obj[k];
this.state[args.$letName].push(pushVal);
} else {
this.state[args.$letName][k] = obj[k];
}
});
return this.state[args.$letName];
}
_whereMixedSet(/*key, val*/) {
let args = getArgs('key:string|object, [val]', arguments);
this.state.whereMap = [];
this.state.rawWhereValues = [];
this._mixedSet('whereMap', 'both', args.key, args.val);
this._mixedSet('rawWhereValues', 'value', args.key, args.val);
}
_fixConjunction(conj) {
let lastItem = this.state.queryMap[this.state.queryMap.length - 1];
let 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 => {
let conj = this._fixConjunction(defaultConj);
this._appendMap(conj, clause, 'where');
});
this.state.whereMap = {};
}
_whereNull(field, stmt, conj) {
field = this.driver.quoteIdentifiers(field);
let item = `${field} ${stmt}`;
this._appendMap(this._fixConjunction(conj), item, 'whereNull');
}
_having(/*key, val, conj*/) {
let args = getArgs('key:string|object, [val]:string|number, [conj]:string', arguments);
args.conj = args.conj || 'AND';
args.val = args.val || null;
// Normalize key/val and put in state.whereMap
this._whereMixedSet(args.key, args.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) ? ` ${args.conj} ` : ' HAVING ',
string: clause,
});
});
// Clear the where Map
this.state.whereMap = {};
}
_whereIn(/*key, val, inClause, conj*/) {
let args = getArgs('key:string, val:array, inClause:string, conj:string', arguments);
args.key = this.driver.quoteIdentifiers(args.key);
let params = Array(args.val.length);
params.fill('?');
args.val.forEach(value => {
this.state.whereValues.push(value);
});
args.conj = (this.state.queryMap.length > 0) ? ` ${args.conj} ` : ' WHERE ';
let str = `${args.key} ${args.inClause} (${params.join(',')}) `;
this._appendMap(args.conj, str, 'whereIn');
}
_run(type, table, callback, 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
if (callback) {
return this.query(sql, vals, callback);
} else {
return this.query(sql, vals);
}
}
_getCompile(type, table, reset) {
reset = reset || false;
let sql = this._compile(type, table);
if (reset) {
this._resetState();
}
return sql;
}
_resetState() {
this.state = new State();
}
}
const QueryBuilderBase = require('./QueryBuilderBase');
/**
* Main object that builds SQL queries.
*
* @param {Driver} Driver - The syntax driver for the database
* @param {Adapter} Adapter - The database module adapter for running queries
* @extends QueryBuilderBase
*/
class QueryBuilder extends 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) {
super(Driver, Adapter);
}
// ----------------------------------------------------------------------------
// ! Miscellaneous Methods
// ----------------------------------------------------------------------------
@ -316,7 +24,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {function} [callback] - Optional callback
* @return {void|Promise} - Returns a promise if no callback is supplied
*/
query(/*sql:string, [params]:array, [callback]:function*/) {
query (/* sql:string, [params]:array, [callback]:function */) {
return this.adapter.execute.apply(this.adapter, arguments);
}
@ -325,7 +33,7 @@ class QueryBuilder extends QueryBuilderBase {
*
* @return {void}
*/
resetQuery() {
resetQuery () {
this._resetState();
}
@ -335,7 +43,7 @@ class QueryBuilder extends QueryBuilderBase {
* @private
* @return {Object} - The State object
*/
getState() {
getState () {
return this.state;
}
@ -346,7 +54,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {function} [callback] - Optional callback
* @return {void|Promise} - Returns a promise if no callback is supplied
*/
truncate(/*table:string, [callback]:function*/) {
truncate (/* table:string, [callback]:function */) {
getArgs('table:string, [callback]:function', arguments);
let args = [].slice.apply(arguments);
let sql = this.driver.truncate(args.shift());
@ -359,7 +67,7 @@ class QueryBuilder extends QueryBuilderBase {
*
* @return {void}
*/
end() {
end () {
this.adapter.close();
}
@ -375,8 +83,7 @@ class QueryBuilder extends QueryBuilderBase {
* @example query.select(['foo', 'bar']); // Select multiple fileds with an array
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
select(fields) {
select (fields) {
// Split/trim fields by comma
fields = (Array.isArray(fields))
? fields
@ -411,7 +118,7 @@ class QueryBuilder extends QueryBuilderBase {
* @example query.from('tableName t'); // Select the table with an alias
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
from(tableName) {
from (tableName) {
// Split identifiers on spaces
let identArray = tableName.trim().split(' ').map(helpers.stringTrim);
@ -433,7 +140,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {String} [pos=both] - The placement of the wildcard character(s): before, after, or both
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
like(field, val, pos) {
like (field, val, pos) {
this._like(field, val, pos, ' LIKE ', 'AND');
return this;
}
@ -446,7 +153,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {String} [pos=both] - The placement of the wildcard character(s): before, after, or both
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
notLike(field, val, pos) {
notLike (field, val, pos) {
this._like(field, val, pos, ' NOT LIKE ', 'AND');
return this;
}
@ -459,7 +166,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {String} [pos=both] - The placement of the wildcard character(s): before, after, or both
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
orLike(field, val, pos) {
orLike (field, val, pos) {
this._like(field, val, pos, ' LIKE ', 'OR');
return this;
}
@ -472,7 +179,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {String} [pos=both] - The placement of the wildcard character(s): before, after, or both
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
orNotLike(field, val, pos) {
orNotLike (field, val, pos) {
this._like(field, val, pos, ' NOT LIKE ', 'OR');
return this;
}
@ -484,7 +191,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {String|Number} [val] - The value to compare if the value of key is a string
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
having(/*key, [val]*/) {
having (/* key, [val] */) {
let args = getArgs('key:string|object, [val]:string|number', arguments);
this._having(args.key, args.val, 'AND');
@ -498,7 +205,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {String|Number} [val] - The value to compare if the value of key is a string
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
orHaving(/*key, [val]*/) {
orHaving (/* key, [val] */) {
let args = getArgs('key:string|object, [val]:string|number', arguments);
this._having(args.key, args.val, 'OR');
@ -512,7 +219,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {String|Number} [val] - The value to compare if the value of key is a string
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
where(key, val) {
where (key, val) {
this._where(key, val, 'AND');
return this;
}
@ -524,7 +231,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {String|Number} [val] - The value to compare if the value of key is a string
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
orWhere(key, val) {
orWhere (key, val) {
this._where(key, val, 'OR');
return this;
}
@ -535,7 +242,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {String} field - The name of the field that has a NULL value
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
whereIsNull(field) {
whereIsNull (field) {
this._whereNull(field, 'IS NULL', 'AND');
return this;
}
@ -546,7 +253,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {String} field - The name so the field that is not to be null
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
whereIsNotNull(field) {
whereIsNotNull (field) {
this._whereNull(field, 'IS NOT NULL', 'AND');
return this;
}
@ -557,7 +264,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {String} field - The name of the field
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
orWhereIsNull(field) {
orWhereIsNull (field) {
this._whereNull(field, 'IS NULL', 'OR');
return this;
}
@ -568,7 +275,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {String} field - The name of the field
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
orWhereIsNotNull(field) {
orWhereIsNotNull (field) {
this._whereNull(field, 'IS NOT NULL', 'OR');
return this;
}
@ -580,7 +287,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {Array} values - the array of items to search in
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
whereIn(key, values) {
whereIn (key, values) {
this._whereIn(key, values, 'IN', 'AND');
return this;
}
@ -592,7 +299,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {Array} values - the array of items to search in
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
orWhereIn(key, values) {
orWhereIn (key, values) {
this._whereIn(key, values, 'IN', 'OR');
return this;
}
@ -604,7 +311,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {Array} values - the array of items to search in
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
whereNotIn(key, values) {
whereNotIn (key, values) {
this._whereIn(key, values, 'NOT IN', 'AND');
return this;
}
@ -616,7 +323,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {Array} values - the array of items to search in
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
orWhereNotIn(key, values) {
orWhereNotIn (key, values) {
this._whereIn(key, values, 'NOT IN', 'OR');
return this;
}
@ -630,7 +337,7 @@ class QueryBuilder extends QueryBuilderBase {
* @example query.set({foo:'bar'}); // Set with an object
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
set(/* $key, [$val] */) {
set (/* $key, [$val] */) {
let args = getArgs('$key, [$val]', arguments);
// Set the appropriate state variables
@ -656,7 +363,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {String} [type='inner'] - The type of join, which defaults to inner
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
join(table, cond, type) {
join (table, cond, type) {
type = type || 'inner';
// Prefix/quote table name
@ -681,8 +388,8 @@ class QueryBuilder extends QueryBuilderBase {
* @param {String|Array} field - The name of the field to group by
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
groupBy(field) {
if (! helpers.isScalar(field)) {
groupBy (field) {
if (!helpers.isScalar(field)) {
let newGroupArray = field.map(this.driver.quoteIdentifiers);
this.state.groupArray = this.state.groupArray.concat(newGroupArray);
} else {
@ -701,7 +408,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {String} [type='ASC'] - The order direction, ASC or DESC
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
orderBy(field, type) {
orderBy (field, type) {
type = type || 'ASC';
// Set the fields for later manipulation
@ -729,7 +436,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {Number} [offset] - The row number to start from
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
limit(limit, offset) {
limit (limit, offset) {
this.state.limit = limit;
this.state.offset = offset || null;
@ -741,7 +448,7 @@ class QueryBuilder extends QueryBuilderBase {
*
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
groupStart() {
groupStart () {
let conj = (this.state.queryMap.length < 1) ? ' WHERE ' : ' AND ';
this._appendMap(conj, '(', 'groupStart');
@ -754,7 +461,7 @@ class QueryBuilder extends QueryBuilderBase {
*
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
orGroupStart() {
orGroupStart () {
this._appendMap('', ' OR (', 'groupStart');
return this;
@ -766,7 +473,7 @@ class QueryBuilder extends QueryBuilderBase {
*
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
orNotGroupStart() {
orNotGroupStart () {
this._appendMap('', ' OR NOT (', 'groupStart');
return this;
@ -777,7 +484,7 @@ class QueryBuilder extends QueryBuilderBase {
*
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
groupEnd() {
groupEnd () {
this._appendMap('', ')', 'groupEnd');
return this;
@ -799,7 +506,7 @@ class QueryBuilder extends QueryBuilderBase {
* @example query.get(callback); // Get the results of a query generated with other methods
* @return {void|Promise} - If no callback is passed, a promise is returned
*/
get(/* [table], [limit], [offset], [callback] */) {
get (/* [table], [limit], [offset], [callback] */) {
const argPattern = '[table]:string, [limit]:number, [offset]:number, [callback]:function';
let args = getArgs(argPattern, arguments);
@ -823,7 +530,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {Function} [callback] - Callback for handling response from the database
* @return {void|Promise} - If no callback is passed, a promise is returned
*/
insert(/* table, data, callback */) {
insert (/* table, data, callback */) {
let args = getArgs('table:string, [data]:object, [callback]:function', arguments);
if (args.data) {
@ -845,7 +552,7 @@ class QueryBuilder extends QueryBuilderBase {
*.then(promiseCallback);
* @return {void|Promise} - If no callback is passed, a promise is returned
*/
insertBatch(/* table, data, callback */) {
insertBatch (/* table, data, callback */) {
let args = getArgs('table:string, data:array, [callback]:function', arguments);
let batch = this.driver.insertBatch(args.table, args.data);
@ -861,7 +568,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {Function} [callback] - Callback for handling response from the database
* @return {void|Promise} - If no callback is passed, a promise is returned
*/
update(/*table, data, callback*/) {
update (/* table, data, callback */) {
let args = getArgs('table:string, [data]:object, [callback]:function', arguments);
if (args.data) {
@ -880,7 +587,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {Function} [callback] - Callback for handling response from the database
* @return {void|Promise} - If no callback is passed, a promise is returned
*/
delete(/*table, [where], [callback]*/) {
delete (/* table, [where], [callback] */) {
let args = getArgs('table:string, [where]:object, [callback]:function', arguments);
if (args.where) {
@ -902,7 +609,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {Boolean} [reset=true] - Whether to reset the query builder so another query can be built
* @return {String} - The compiled sql statement
*/
getCompiledSelect(/*table, reset*/) {
getCompiledSelect (/* table, reset */) {
let args = getArgs('[table]:string, [reset]:boolean', arguments);
if (args.table) {
this.from(args.table);
@ -918,7 +625,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {Boolean} [reset=true] - Whether to reset the query builder so another query can be built
* @return {String} - The compiled sql statement
*/
getCompiledInsert(table, reset) {
getCompiledInsert (table, reset) {
return this._getCompile('insert', this.driver.quoteTable(table), reset);
}
@ -929,7 +636,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {Boolean} [reset=true] - Whether to reset the query builder so another query can be built
* @return {String} - The compiled sql statement
*/
getCompiledUpdate(table, reset) {
getCompiledUpdate (table, reset) {
return this._getCompile('update', this.driver.quoteTable(table), reset);
}
@ -940,9 +647,9 @@ class QueryBuilder extends QueryBuilderBase {
* @param {Boolean} [reset=true] - Whether to reset the query builder so another query can be built
* @return {String} - The compiled sql statement
*/
getCompiledDelete(table, reset) {
getCompiledDelete (table, reset) {
return this._getCompile('delete', this.driver.quoteTable(table), reset);
}
}
module.exports = QueryBuilder;
module.exports = QueryBuilder;

288
lib/QueryBuilderBase.js Normal file
View File

@ -0,0 +1,288 @@
'use strict';
const getArgs = require('getargs');
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();
}
/**
* 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 => {
let 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':
let 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;
}
_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
* @return {Array} - modified state array
*/
_mixedSet (/* $letName, $valType, $key, [$val] */) {
const argPattern = '$letName:string, $valType:string, $key:object|string|number, [$val]';
let args = getArgs(argPattern, arguments);
let obj = {};
if (helpers.isScalar(args.$key) && !helpers.isUndefined(args.$val)) {
// Convert key/val pair to a simple object
obj[args.$key] = args.$val;
} else if (helpers.isScalar(args.$key) && helpers.isUndefined(args.$val)) {
// If just a string for the key, and no value, create a simple object with duplicate key/val
obj[args.$key] = args.$key;
} else {
obj = args.$key;
}
Object.keys(obj).forEach(k => {
// If a single value for the return
if (['key', 'value'].indexOf(args.$valType) !== -1) {
let pushVal = (args.$valType === 'key') ? k : obj[k];
this.state[args.$letName].push(pushVal);
} else {
this.state[args.$letName][k] = obj[k];
}
});
return this.state[args.$letName];
}
_whereMixedSet (/* key, val */) {
let args = getArgs('key:string|object, [val]', arguments);
this.state.whereMap = [];
this.state.rawWhereValues = [];
this._mixedSet('whereMap', 'both', args.key, args.val);
this._mixedSet('rawWhereValues', 'value', args.key, args.val);
}
_fixConjunction (conj) {
let lastItem = this.state.queryMap[this.state.queryMap.length - 1];
let 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 => {
let conj = this._fixConjunction(defaultConj);
this._appendMap(conj, clause, 'where');
});
this.state.whereMap = {};
}
_whereNull (field, stmt, conj) {
field = this.driver.quoteIdentifiers(field);
let item = `${field} ${stmt}`;
this._appendMap(this._fixConjunction(conj), item, 'whereNull');
}
_having (/* key, val, conj */) {
let args = getArgs('key:string|object, [val]:string|number, [conj]:string', arguments);
args.conj = args.conj || 'AND';
args.val = args.val || null;
// Normalize key/val and put in state.whereMap
this._whereMixedSet(args.key, args.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) ? ` ${args.conj} ` : ' HAVING ',
string: clause
});
});
// Clear the where Map
this.state.whereMap = {};
}
_whereIn (/* key, val, inClause, conj */) {
let args = getArgs('key:string, val:array, inClause:string, conj:string', arguments);
args.key = this.driver.quoteIdentifiers(args.key);
let params = Array(args.val.length);
params.fill('?');
args.val.forEach(value => {
this.state.whereValues.push(value);
});
args.conj = (this.state.queryMap.length > 0) ? ` ${args.conj} ` : ' WHERE ';
let str = `${args.key} ${args.inClause} (${params.join(',')}) `;
this._appendMap(args.conj, str, 'whereIn');
}
_run (type, table, callback, 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
if (callback) {
return this.query(sql, vals, callback);
} else {
return this.query(sql, vals);
}
}
_getCompile (type, table, reset) {
reset = reset || false;
let sql = this._compile(type, table);
if (reset) {
this._resetState();
}
return sql;
}
_resetState () {
this.state = new State();
}
}
module.exports = QueryBuilderBase;

View File

@ -18,13 +18,13 @@ class QueryParser {
* @param {Driver} driver - The driver object for the database in use
* @return {void}
*/
constructor(driver) {
constructor (driver) {
this.driver = driver;
const matchPatterns = {
function: /([a-z0-9_]+\((.*)\))/i,
operator: /\!=?|\=|\+|&&?|~|\|\|?|\^|\/|<>|>=?|<=?|\-|%|OR|AND|NOT|XOR/ig,
literal: /([0-9]+)|'(.*?)'|true|false/ig,
operator: /!=?|=|\+|&&?|~|\|\|?|\^|\/|<>|>=?|<=?|-|%|OR|AND|NOT|XOR/ig,
literal: /([0-9]+)|'(.*?)'|true|false/ig
};
// Full pattern for identifiers
@ -55,7 +55,7 @@ class QueryParser {
* @param {Array} array - Set of possible matches
* @return {Array|null} - Filtered set of possible matches
*/
filterMatches(array) {
filterMatches (array) {
let output = [];
// Return non-array matches
@ -76,7 +76,7 @@ class QueryParser {
* @param {String} string - the string to check
* @return {Array|null} - List of operators
*/
hasOperator(string) {
hasOperator (string) {
return this.filterMatches(string.match(this.matchPatterns.operator));
}
@ -86,13 +86,13 @@ class QueryParser {
* @param {String} sql - Join sql to parse
* @return {Object} - Join condition components
*/
parseJoin(sql) {
parseJoin (sql) {
let matches = {};
let output = {
functions: [],
identifiers: [],
operators: [],
literals: [],
literals: []
};
// Get clause components
@ -118,12 +118,12 @@ class QueryParser {
* @param {String} condition - The join condition to evalate
* @return {String} - The parsed/escaped join condition
*/
compileJoin(condition) {
compileJoin (condition) {
let parts = this.parseJoin(condition);
// Quote the identifiers
parts.combined.forEach((part, i) => {
if (parts.identifiers.indexOf(part) !== -1 && ! helpers.isNumber(part)) {
if (parts.identifiers.indexOf(part) !== -1 && !helpers.isNumber(part)) {
parts.combined[i] = this.driver.quoteIdentifiers(part);
}
});
@ -138,7 +138,7 @@ class QueryParser {
* @param {State} state - Query Builder state object
* @return {String} - The parsed/escaped where condition
*/
parseWhere(driver, state) {
parseWhere (driver, state) {
let whereMap = state.whereMap;
let whereValues = state.rawWhereValues;
@ -151,7 +151,7 @@ class QueryParser {
let fullClause = '';