Browse Source

Add updateBatch method to query builder

keep-around/4064a99419461bccaf062953d2fcfb38503fbc65
Timothy Warren 4 years ago
parent
commit
4064a99419
  1. 68
      .eslintrc
  2. 96
      lib/Driver.js
  3. 15
      lib/Helpers.js
  4. 11
      lib/NodeQuery.js
  5. 34
      lib/QueryBuilder.js
  6. 152
      lib/QueryBuilderBase.js
  7. 36
      lib/QueryParser.js
  8. 2
      lib/adapters/MSSQLServer/index.js
  9. 2
      lib/adapters/Mysql/mysql2.js
  10. 51
      lib/adapters/Pg/Pg.js
  11. 25
      lib/adapters/Pg/PgNative.js
  12. 8
      lib/adapters/Pg/index.js
  13. 4
      lib/adapters/Sqlite/dblite.js
  14. 4
      lib/adapters/Sqlite/sqlite3.js
  15. 3
      lib/drivers/MSSQLDriver.js
  16. 4
      lib/drivers/Pg.js
  17. 14
      lib/drivers/Sqlite.js
  18. 30
      package.json
  19. 11
      test/adapters/__snapshots__/dblite_test.js.snap
  20. 11
      test/adapters/__snapshots__/mysql2_test.js.snap
  21. 11
      test/adapters/__snapshots__/pg_test.js.snap
  22. 11
      test/adapters/__snapshots__/sqlite3_test.js.snap
  23. 22
      test/adapters/dblite_test.js
  24. 27
      test/adapters/mysql2_test.js
  25. 28
      test/adapters/pg_test.js
  26. 22
      test/adapters/sqlite3_test.js
  27. 5
      test/base.js
  28. 116
      test/base/adapterPromiseTestRunner.js
  29. 21
      test/config-ci.json

68
.eslintrc

@ -1,39 +1,43 @@
{
"env": {
"node": true,
"es6": true
"commonjs": true,
"es6": true,
"jest": true,
"node": true
},
"extends": ["eslint:recommended", "happiness"],
"rules": {
"arrow-parens": [2, "as-needed"],
"no-console": [1],
"no-constant-condition": [1],
"no-extra-semi": [1],
"no-func-assign": [1],
"no-obj-calls": [2],
"no-unexpected-multiline" : [2],
"no-unneeded-ternary": [2],
"radix": [2],
"no-with": [2],
"no-eval": [2],
"no-unreachable": [1],
"no-irregular-whitespace": [1],
"no-new-wrappers": [2],
"no-new-func": [2],
"curly" : [2, "multi-line"],
"no-implied-eval": [2],
"no-invalid-this": [2],
"constructor-super": [2],
"no-dupe-args": [2],
"no-dupe-keys": [2],
"no-dupe-class-members": [2],
"no-this-before-super": [2],
"prefer-arrow-callback": [1],
"no-var": [2],
"valid-jsdoc": [1],
"strict": [2, "global"],
"callback-return": [1],
"object-shorthand": [1, "methods"],
"prefer-template": [1]
"arrow-parens": ["error", "as-needed"],
"callback-return": ["warn"],
"constructor-super": ["error"],
"curly" : ["error", "multi-line"],
"no-case-declarations": "off",
"no-console": ["warn"],
"no-constant-condition": ["warn"],
"no-dupe-args": ["error"],
"no-dupe-class-members": ["error"],
"no-dupe-keys": ["error"],
"no-eval": ["error"],
"no-extra-semi": ["warn"],
"no-func-assign": ["warn"],
"no-implied-eval": ["error"],
"no-invalid-this": ["error"],
"no-irregular-whitespace": ["warn"],
"no-new-func": ["error"],
"no-new-wrappers": ["error"],
"no-obj-calls": ["error"],
"no-this-before-super": ["error"],
"no-unexpected-multiline" : ["error"],
"no-unneeded-ternary": ["error"],
"no-unreachable": ["warn"],
"no-var": ["error"],
"no-with": ["error"],
"object-shorthand": ["warn", "methods"],
"prefer-arrow-callback": ["warn"],
"prefer-template": ["warn"],
"radix": ["error"],
"strict": ["error", "global"],
"valid-jsdoc": ["warn"]
},
"parser": "babel-eslint"
}

96
lib/Driver.js

@ -1,3 +1,4 @@
const array = require('locutus/php/array');
const Helpers = require('./Helpers');
/**
@ -64,36 +65,38 @@ const Driver = {
* @return {String|Array} - Quoted identifier(s)
*/
quoteIdentifiers (str) {
let hiers, raw;
let pattern = new RegExp(
const pattern = new RegExp(
`${Driver.identifierStartChar}(` +
'([a-zA-Z0-9_]+)' + '(((.*?)))' +
`)${Driver.identifierEndChar}`, 'ig');
// Recurse for arrays of identifiiers
// Recurse for arrays of identifiers
if (Array.isArray(str)) {
return str.map(Driver.quoteIdentifiers);
}
// cast to string so that you don't have undefined method errors with junk data
str = String(str);
// Handle commas
if (str.includes(',')) {
let parts = str.split(',').map(Helpers.stringTrim);
const parts = str.split(',').map(Helpers.stringTrim);
str = parts.map(Driver.quoteIdentifiers).join(',');
}
// Split identifiers by period
hiers = str.split('.').map(Driver._quote);
raw = hiers.join('.');
const hierarchies = str.split('.').map(Driver._quote);
let raw = hierarchies.join('.');
// Fix functions
if (raw.includes('(') && raw.includes(')')) {
let funcs = pattern.exec(raw);
const functionCalls = pattern.exec(raw);
// Unquote the function
raw = raw.replace(funcs[0], funcs[1]);
raw = raw.replace(functionCalls[0], functionCalls[1]);
// Quote the identifiers inside of the parens
let inParens = funcs[3].substring(1, funcs[3].length - 1);
const inParens = functionCalls[3].substring(1, functionCalls[3].length - 1);
raw = raw.replace(inParens, Driver.quoteIdentifiers(inParens));
}
@ -126,7 +129,7 @@ const Driver = {
* @return {String} - Query and data to insert
*/
insertBatch (table, data) {
const vals = [];
const values = [];
const fields = Object.keys(data[0]);
let sql = '';
@ -134,7 +137,7 @@ const Driver = {
// be parameterized
data.forEach(obj => {
Object.keys(obj).forEach(key => {
vals.push(obj[key]);
values.push(obj[key]);
});
});
@ -145,16 +148,79 @@ const Driver = {
sql += `INSERT INTO ${table} (${Driver.quoteIdentifiers(fields).join(',')}) VALUES `;
// Create placeholder groups
let params = Array(fields.length).fill('?');
let paramString = `(${params.join(',')})`;
let paramList = Array(data.length).fill(paramString);
const params = Array(fields.length).fill('?');
const paramString = `(${params.join(',')})`;
const paramList = Array(data.length).fill(paramString);
sql += paramList.join(',');
return {
sql: sql,
values: vals
values: values
};
},
/**
* Creates a batch update sql statement
*
* @private
* @param {String} table
* @param {Array<Object>} data
* @param {String} updateKey
* @return {Array<String,Object,Number>} array
*/
updateBatch (table, data, updateKey) {
let affectedRows = 0;
let insertData = [];
const fieldLines = [];
let sql = `UPDATE ${Driver.quoteTable(table)} SET `;
// get the keys of the current set of data, except the one used to
// set the update condition
const fields = data.reduce((previous, current) => {
affectedRows++;
const keys = Object.keys(current).filter(key => {
return key !== updateKey && !previous.includes(key);
});
return previous.concat(keys);
}, []);
// Create the CASE blocks for each data set
fields.forEach(field => {
let line = `${Driver.quoteIdentifiers(field)} = CASE\n`;
const cases = [];
data.forEach(currentCase => {
insertData.push(currentCase[updateKey]);
insertData.push(currentCase[field]);
const newCase = `WHEN ${Driver.quoteIdentifiers(updateKey)} =? THEN ? `;
cases.push(newCase);
});
line += `${cases.join('\n')}\n`;
line += `ELSE ${Driver.quoteIdentifiers(field)} END`;
fieldLines.push(line);
});
sql += `${fieldLines.join(',\n')}\n`;
const whereValues = [];
data.forEach(entry => {
const insertValue = entry[updateKey];
whereValues.push(insertValue);
insertData.push(insertValue);
});
// Create the placeholders for the WHERE IN clause
const placeholders = Array(whereValues.length);
placeholders.fill('?');
sql += `WHERE ${Driver.quoteIdentifiers(updateKey)} IN `;
sql += `( ${placeholders.join(',')} )`;
return [sql, insertData, affectedRows];
}
};

15
lib/Helpers.js

@ -42,7 +42,7 @@ class Helpers {
* @return {String} - Type of the object
*/
static type (o) {
let type = Object.prototype.toString.call(o).slice(8, -1).toLowerCase();
const type = Object.prototype.toString.call(o).slice(8, -1).toLowerCase();
// handle NaN and Infinity
if (type === 'number') {
@ -62,11 +62,10 @@ class Helpers {
* Determine whether an object is scalar
*
* @param {mixed} obj - Object to test
* @return {bool} - Is object scalar
* @return {boolean} - Is object scalar
*/
static isScalar (obj) {
let scalar = ['string', 'number', 'boolean'];
return scalar.indexOf(Helpers.type(obj)) !== -1;
return ['string', 'number', 'boolean'].includes(Helpers.type(obj));
}
/**
@ -77,7 +76,7 @@ class Helpers {
* @return {Array} - The new array of plucked values
*/
static arrayPluck (arr, key) {
let output = [];
const output = [];
// Empty case
if (arr.length === 0) {
@ -119,20 +118,20 @@ class Helpers {
}
/**
* Make the first letter of the string uppercase
* Make the first constter of the string uppercase
*
* @param {String} str - The string to modify
* @return {String} - The modified string
*/
static upperCaseFirst (str) {
str += '';
let first = str.charAt(0).toUpperCase();
const first = str.charAt(0).toUpperCase();
return first + str.substr(1);
}
}
// Define an 'is' method for each type
let types = [
const types = [
'Null',
'Undefined',
'Object',

11
lib/NodeQuery.js

@ -50,17 +50,16 @@ class NodeQuery {
this.instance = null;
if (config !== undefined) {
let drivername = dbDriverMap.get(config.driver);
const driverName = dbDriverMap.get(config.driver);
if (!drivername) {
if (!driverName) {
throw new Error(`Selected driver (${config.driver}) does not exist!`);
}
const driver = require(`./drivers/${drivername}`);
const Adapter = require(`./adapters/${drivername}`);
const driver = require(`./drivers/${driverName}`);
const Adapter = require(`./adapters/${driverName}`);
let adapter = Adapter(config);
this.instance = new QueryBuilder(driver, adapter);
this.instance = new QueryBuilder(driver, Adapter(config));
}
}

34
lib/QueryBuilder.js

@ -107,7 +107,7 @@ class QueryBuilder extends QueryBuilderBase {
}
});
let safeArray = this.driver.quoteIdentifiers(fields);
const safeArray = this.driver.quoteIdentifiers(fields);
// Join the strings back together
safeArray.forEach((field, index) => {
@ -378,8 +378,8 @@ class QueryBuilder extends QueryBuilderBase {
table = table.join(' ');
// Parse out the join condition
let parsedCondition = this.parser.compileJoin(cond);
let condition = `${table} ON ${parsedCondition}`;
const parsedCondition = this.parser.compileJoin(cond);
const condition = `${table} ON ${parsedCondition}`;
// Append the join condition to the query map
this._appendMap(`\n${type.toUpperCase()} JOIN `, condition, 'join');
@ -395,7 +395,7 @@ class QueryBuilder extends QueryBuilderBase {
*/
groupBy (field) {
if (!Helpers.isScalar(field)) {
let newGroupArray = field.map(this.driver.quoteIdentifiers);
const newGroupArray = field.map(this.driver.quoteIdentifiers);
this.state.groupArray = this.state.groupArray.concat(newGroupArray);
} else {
this.state.groupArray.push(this.driver.quoteIdentifiers(field));
@ -421,7 +421,7 @@ class QueryBuilder extends QueryBuilderBase {
this.state.orderArray[field] = type;
let orderClauses = [];
const orderClauses = [];
// Flatten key/val pairs into an array of space-separated pairs
Object.keys(this.state.orderArray).forEach(key => {
@ -454,7 +454,7 @@ class QueryBuilder extends QueryBuilderBase {
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
groupStart () {
let conj = (this.state.queryMap.length < 1) ? ' WHERE ' : ' AND ';
const conj = (this.state.queryMap.length < 1) ? ' WHERE ' : ' AND ';
this._appendMap(conj, '(', 'groupStart');
return this;
@ -549,7 +549,7 @@ class QueryBuilder extends QueryBuilderBase {
* @return {Promise<Result>} - Promise containing the result of the query
*/
insertBatch (table, data) {
let batch = this.driver.insertBatch(table, data);
const batch = this.driver.insertBatch(table, data);
// Run the query
return this.query(batch.sql, batch.values);
@ -571,6 +571,20 @@ class QueryBuilder extends QueryBuilderBase {
return this._run('update', this.driver.quoteTable(table));
}
/**
* Creates a batch update sql statement
*
* @param {String} table - The table to update
* @param {Object} data - Batch insert data
* @param {String} updateKey - The field in the table to compare against for updating
* @return {Number} Number of rows updated
*/
updateBatch (table, data, updateKey) {
const [sql, insertData, affectedRows] = this.driver.updateBatch(table, data, updateKey);
this._run('', table, sql, insertData);
return affectedRows;
}
/**
* Run the generated delete query
*
@ -613,7 +627,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 = true) {
return this._getCompile('insert', this.driver.quoteTable(table), reset);
}
@ -624,7 +638,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 = true) {
return this._getCompile('update', this.driver.quoteTable(table), reset);
}
@ -635,7 +649,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
*/
getCompiledDelete (table, reset) {
getCompiledDelete (table, reset = true) {
return this._getCompile('delete', this.driver.quoteTable(table), reset);
}
}

152
lib/QueryBuilderBase.js

@ -16,74 +16,6 @@ class QueryBuilderBase {
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);
@ -147,7 +79,7 @@ class QueryBuilderBase {
Object.keys(obj).forEach(k => {
// If a single value for the return
if (['key', 'value'].indexOf(valType) !== -1) {
let pushVal = (valType === 'key') ? k : obj[k];
const pushVal = (valType === 'key') ? k : obj[k];
this.state[letName].push(pushVal);
} else {
this.state[letName][k] = obj[k];
@ -166,8 +98,8 @@ class QueryBuilderBase {
}
_fixConjunction (conj) {
let lastItem = this.state.queryMap[this.state.queryMap.length - 1];
let conjunctionList = Helpers.arrayPluck(this.state.queryMap, 'conjunction');
const lastItem = this.state.queryMap[this.state.queryMap.length - 1];
const conjunctionList = Helpers.arrayPluck(this.state.queryMap, 'conjunction');
if (this.state.queryMap.length === 0 || (!Helpers.regexInArray(conjunctionList, /^ ?WHERE/i))) {
conj = ' WHERE ';
@ -189,7 +121,7 @@ class QueryBuilderBase {
this.state = this.parser.parseWhere(this.driver, this.state);
this.state.whereMap.forEach(clause => {
let conj = this._fixConjunction(defaultConj);
const conj = this._fixConjunction(defaultConj);
this._appendMap(conj, clause, 'where');
});
@ -198,7 +130,7 @@ class QueryBuilderBase {
_whereNull (field, stmt, conj) {
field = this.driver.quoteIdentifiers(field);
let item = `${field} ${stmt}`;
const item = `${field} ${stmt}`;
this._appendMap(this._fixConjunction(conj), item, 'whereNull');
}
@ -225,7 +157,7 @@ class QueryBuilderBase {
_whereIn (key, val, inClause, conj) {
key = this.driver.quoteIdentifiers(key);
let params = Array(val.length);
const params = Array(val.length);
params.fill('?');
val.forEach(value => {
@ -233,7 +165,7 @@ class QueryBuilderBase {
});
conj = (this.state.queryMap.length > 0) ? ` ${conj} ` : ' WHERE ';
let str = `${key} ${inClause} (${params.join(',')}) `;
const str = `${key} ${inClause} (${params.join(',')}) `;
this._appendMap(conj, str, 'whereIn');
}
@ -257,7 +189,7 @@ class QueryBuilderBase {
_getCompile (type, table, reset) {
reset = reset || false;
let sql = this._compile(type, table);
const sql = this._compile(type, table);
if (reset) {
this._resetState();
@ -266,6 +198,74 @@ class QueryBuilderBase {
return sql;
}
/**
* Complete the sql building based on the type provided
*
* @private
* @param {String} type - Type of SQL query
* @param {String} table - The table to run the query on
* @return {String} - The compiled sql
*/
_compile (type, table) {
// Put together the basic query
let sql = this._compileType(type, table);
// Set each subClause
['queryMap', 'groupString', 'orderString', 'havingMap'].forEach(clause => {
const param = this.state[clause];
if (!Helpers.isScalar(param)) {
Object.keys(param).forEach(part => {
sql += param[part].conjunction + param[part].string;
});
} else {
sql += param;
}
});
// Append the limit, if it exists
if (Helpers.isNumber(this.state.limit)) {
sql = this.driver.limit(sql, this.state.limit, this.state.offset);
}
return sql;
}
_compileType (type, table) {
let sql = '';
switch (type) {
case 'insert':
const params = Array(this.state.setArrayKeys.length).fill('?');
sql = `INSERT INTO ${table} (`;
sql += this.state.setArrayKeys.join(',');
sql += `) VALUES (${params.join(',')})`;
break;
case 'update':
sql = `UPDATE ${table} SET ${this.state.setString}`;
break;
case 'delete':
sql = `DELETE FROM ${table}`;
break;
default:
sql = `SELECT * FROM ${this.state.fromString}`;
// Set the select string
if (this.state.selectString.length > 0) {
// Replace the star with the selected fields
sql = sql.replace('*', this.state.selectString);
}
break;
}
return sql;
}
_resetState () {
this.state = new State();
}

36
lib/QueryParser.js

@ -54,7 +54,7 @@ class QueryParser {
* @return {Array|null} - Filtered set of possible matches
*/
filterMatches (array) {
let output = [];
const output = [];
// Return non-array matches
if (Helpers.isNull(array)) {
@ -85,8 +85,8 @@ class QueryParser {
* @return {Object} - Join condition components
*/
parseJoin (sql) {
let matches = {};
let output = {
const matches = {};
const output = {
functions: [],
identifiers: [],
operators: [],
@ -117,7 +117,7 @@ class QueryParser {
* @return {String} - The parsed/escaped join condition
*/
compileJoin (condition) {
let parts = this.parseJoin(condition);
const parts = this.parseJoin(condition);
// Quote the identifiers
parts.combined.forEach((part, i) => {
@ -137,11 +137,11 @@ class QueryParser {
* @return {String} - The parsed/escaped where condition
*/
parseWhere (driver, state) {
let whereMap = state.whereMap;
const whereMap = state.whereMap;
let whereValues = state.rawWhereValues;
let outputMap = [];
let outputValues = [];
const outputMap = [];
const outputValues = [];
Object.keys(whereMap).forEach(key => {
// Combine fields, operators, functions and values into a full clause
@ -158,16 +158,16 @@ class QueryParser {
}
// Separate the clause into separate pieces
let parts = this.parseJoin(fullClause);
const parts = this.parseJoin(fullClause);
// Filter explicit literals from lists of matches
if (whereValues.indexOf(whereMap[key]) !== -1) {
let value = whereMap[key];
let identIndex = parts.identifiers.indexOf(value);
let litIndex = (Helpers.isArray(parts.literals)) ? parts.literals.indexOf(value) : -1;
let combIndex = parts.combined.indexOf(value);
let funcIndex = (Helpers.isArray(parts.functions)) ? parts.functions.indexOf(value) : -1;
let inOutputArray = outputValues.indexOf(value) !== -1;
const value = whereMap[key];
const identIndex = parts.identifiers.indexOf(value);
const litIndex = (Helpers.isArray(parts.literals)) ? parts.literals.indexOf(value) : -1;
const combIndex = parts.combined.indexOf(value);
const funcIndex = (Helpers.isArray(parts.functions)) ? parts.functions.indexOf(value) : -1;
let inOutputArray = outputValues.includes(value);
// Remove the identifier in question,
// and add to the output values array
@ -201,8 +201,8 @@ class QueryParser {
// Filter false positive identifiers
parts.identifiers = parts.identifiers.filter(item => {
let isInCombinedMatches = parts.combined.indexOf(item) !== -1;
let isNotInBlackList = this.identifierBlacklist.indexOf(item.toLowerCase()) === -1;
const isInCombinedMatches = parts.combined.indexOf(item) !== -1;
const isNotInBlackList = this.identifierBlacklist.indexOf(item.toLowerCase()) === -1;
return isInCombinedMatches && isNotInBlackList;
}, this);
@ -210,7 +210,7 @@ class QueryParser {
// Quote identifiers
if (Helpers.isArray(parts.identifiers)) {
parts.identifiers.forEach(ident => {
let index = parts.combined.indexOf(ident);
const index = parts.combined.indexOf(ident);
if (index !== -1) {
parts.combined[index] = driver.quoteIdentifiers(ident);
}
@ -224,7 +224,7 @@ class QueryParser {
// a where condition,
if (Helpers.isArray(parts.literals)) {
parts.literals.forEach(lit => {
let litIndex = parts.combined.indexOf(lit);
const litIndex = parts.combined.indexOf(lit);
if (litIndex !== -1) {
parts.combined[litIndex] = '?';

2
lib/adapters/MSSQLServer/index.js

@ -1,5 +1,3 @@
'use strict';
module.exports = config => {
};

2
lib/adapters/Mysql/mysql2.js

@ -36,7 +36,7 @@ class Mysql extends Adapter {
* Run the sql query as a prepared statement
*
* @param {String} sql - The sql with placeholders
* @param {Array|undefined} params - The values to insert into the query
* @param {Array} params - The values to insert into the query
* @return {Promise} Result of query
*/
execute (sql, params) {

51
lib/adapters/Pg/Pg.js

@ -7,14 +7,38 @@ const url = require('url');
class Pg extends Adapter {
constructor (config) {
let instance = null;
let connectionString = Pg.formatConnectionString(config);
if (connectionString !== '') {
const conn = new pg.Client(connectionString);
conn.connect(err => {
if (err) {
throw new Error(err);
}
});
instance = Promise.resolve(conn);
}
super(instance);
}
/**
* Convert the connection object to a connection string
*
* @param {Object} config - the configuration object
* @return {String} - the connection string
*/
static formatConnectionString (config) {
let connectionString = '';
if (Helpers.isObject(config)) {
let host = config.host || 'localhost';
let user = config.user || 'postgres';
let password = `:${config.password}` || '';
let port = config.port || 5432;
const host = config.host || 'localhost';
const user = config.user || 'postgres';
const password = `:${config.password}` || '';
const port = config.port || 5432;
let conn = {
const conn = {
protocol: 'postgres',
slashes: true,
host: `${host}:${port}`,
@ -27,18 +51,7 @@ class Pg extends Adapter {
connectionString = config;
}
if (connectionString !== '') {
let conn = new pg.Client(connectionString);
conn.connect(err => {
if (err) {
throw new Error(err);
}
});
instance = Promise.resolve(conn);
}
super(instance);
return connectionString;
}
/**
@ -52,9 +65,9 @@ class Pg extends Adapter {
return new Result();
}
let cols = [];
const cols = [];
result.fields.forEach(field => {
cols = field.name;
cols.push(field.name);
});
return new Result(result.rows, cols);

25
lib/adapters/Pg/PgNative.js

@ -1,25 +0,0 @@
const Pg = require('./Pg');
const pg = require('pg').native;
class PgNative extends Pg {
constructor (config) {
super(config);
let instance = null;
let connectionString = Pg._formatConnectionString(config);
if (connectionString !== '') {
let conn = new pg.Client(connectionString);
conn.connect(err => {
if (err) {
throw new Error(err);
}
});
instance = Promise.resolve(conn);
}
super.instance = instance;
}
}
module.exports = PgNative;

8
lib/adapters/Pg/index.js

@ -1,8 +1,2 @@
const Pg = require('./Pg');
const PgNative = require('./PgNative');
module.exports = config => {
return (config.native)
? new PgNative(config.connection)
: new Pg(config.connection);
};
module.exports = config => new Pg(config.connection);

4
lib/adapters/Sqlite/dblite.js

@ -5,10 +5,10 @@ const dbliteAdapter = require('dblite');
class SqliteDblite extends Adapter {
constructor (config) {
let file = (Helpers.isString(config)) ? config : config.file;
const file = (Helpers.isString(config)) ? config : config.file;
const instance = new Promise((resolve, reject) => {
let conn = dbliteAdapter(file);
const conn = dbliteAdapter(file);
// Stop the stupid 'bye bye' message being output
conn.on('close', () => {});

4
lib/adapters/Sqlite/sqlite3.js

@ -5,10 +5,10 @@ const sqlite3 = require('sqlite3').verbose();
class SqliteSqlite3 extends Adapter {
constructor (config) {
let file = (Helpers.isString(config)) ? config : config.file;
const file = (Helpers.isString(config)) ? config : config.file;
const instance = new Promise((resolve, reject) => {
let conn = new sqlite3.Database(file, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, err => {
const conn = new sqlite3.Database(file, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, err => {
if (err) {
reject(err);
}

3
lib/drivers/MSSQLDriver.js

@ -1,5 +1,3 @@
'use strict';
/**
* Driver for Microsoft SQL Server databases
*
@ -8,7 +6,6 @@
module.exports = (() => {
delete require.cache[require.resolve('../Driver')];
const driver = require('../Driver');
const Helpers = require('../Helpers');
driver.identifierStartChar = '[';
driver.identifierEndChar = ']';

4
lib/drivers/Pg.js

@ -5,7 +5,5 @@
*/
module.exports = (() => {
delete require.cache[require.resolve('../Driver')];
let driver = require('../Driver');
return driver;
return require('../Driver');
})();

14
lib/drivers/Sqlite.js

@ -5,7 +5,7 @@
*/
module.exports = (() => {
delete require.cache[require.resolve('../Driver')];
let driver = require('../Driver');
const driver = require('../Driver');
// Sqlite doesn't have a truncate command
driver.hasTruncate = false;
@ -22,11 +22,11 @@ module.exports = (() => {
// Get the data values to insert, so they can
// be parameterized
let sql = '';
let first = data.shift();
const first = data.shift();
let vals = [];
const vals = [];
data.forEach(obj => {
let row = [];
const row = [];
Object.keys(obj).forEach(key => {
row.push(obj[key]);
});
@ -37,8 +37,8 @@ module.exports = (() => {
// Get the field names from the keys of the first
// object to be inserted
let fields = Object.keys(first);
let cols = [];
const fields = Object.keys(first);
const cols = [];
fields.forEach(key => {
cols.push(`'${driver._quote(first[key])}' AS ${driver.quoteIdentifiers(key)}`);
});
@ -46,7 +46,7 @@ module.exports = (() => {
sql += `SELECT ${cols.join(', ')}\n`;
vals.forEach(rowValues => {
let quoted = rowValues.map(value => String(value).replace('\'', '\'\''));
const quoted = rowValues.map(value => String(value).replace('\'', '\'\''));
sql += `UNION ALL SELECT '${quoted.join('\', \'')}'\n`;
});

30
package.json

@ -1,6 +1,6 @@
{
"name": "ci-node-query",
"version": "6.0.0",
"version": "5.0.0",
"description": "A query builder for node based on the one in CodeIgniter",
"author": "Timothy J Warren <tim@timshomepage.net>",
"engines": {
@ -42,27 +42,31 @@
"glob": "^7.0.3",
"mysql2": "^1.2.0",
"pg": "^7.4",
"require-reload": "~0.2.2",
"sqlite3": "^3.1.8",
"tedious": "^2.0.0",
"xregexp": "^4.0.0"
},
"devDependencies": {
"babel-eslint": "^8.2.1",
"chai": "^4.1",
"chai-as-promised": "^7.1",
"documentation": "latest",
"eslint": "^4.16.0",
"eslint-config-happiness": "^10.2.1",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-standard": "^3.0.1",
"globstar": "^1.0.0",
"happiness": "^10.0",
"jest": "^22.0.0",
"jsdoc": "^3.4.3",
"npm-run-all": "^4.0.2",
"nsp": "^3.1",
"pg-native": "^2.2"
"require-reload": "~0.2.2"
},
"license": "MIT",
"jest": {
"collectCoverageFrom": [
"lib/**/*.js"
],
"coverageDirectory": "coverage",
"coverageReporters": [
"html",
@ -82,21 +86,11 @@
"default": "npm-run-all --parallel audit lint:src lint:tests && npm run test",
"predocs": "globstar -- documentation build -f md -o API.md \"lib/*.js\"",
"docs": "globstar -- documentation build -f html -o docs \"lib/*.js\"",
"fix": "happiness --fix \"lib/**/*.js\" \"test/**/*.js\"",
"fix": "eslint --fix ./lib ./test",
"postdocs": "jsdoc lib -r -d documentation",
"happy": "happiness \"lib/**/*.js\" \"test/**/*.js\"",
"happy:src": "happiness \"lib/**/*.js\"",
"happy:tests": "happiness \"test/**/*.js\"",
"lint": "npm-run-all lint:tests lint:src happy",
"lint": "npm-run-all lint:src lint:tests",
"lint:src": "eslint ./lib",
"lint:tests": "eslint ./test",
"test": "jest"
},
"happiness": {
"env": {
"es6": true,
"jest": true
},
"parser": "babel-eslint"
}
}

11
test/adapters/__snapshots__/dblite_test.js.snap

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Dblite adapter tests - Get compiled - getCompiledDelete 1`] = `"DELETE FROM \\"create_test\\" WHERE \\"id\\" = ?"`;
exports[`Dblite adapter tests - Get compiled - getCompiledInsert 1`] = `"INSERT INTO \\"create_test\\" (\\"id\\",\\"key\\",\\"val\\") VALUES (?,?,?)"`;
exports[`Dblite adapter tests - Get compiled - getCompiledSelect 1`] = `"SELECT \\"id\\", \\"key\\" AS \\"k\\", \\"val\\" FROM \\"create_test\\" WHERE (\\"id\\" > ? AND \\"id\\" < ?) LIMIT 2 OFFSET 1"`;
exports[`Dblite adapter tests - Get compiled - getCompiledSelect 2 1`] = `"SELECT \\"id\\", \\"key\\" AS \\"k\\", \\"val\\" FROM \\"create_test\\" WHERE (\\"id\\" > ? AND \\"id\\" < ?) LIMIT 2 OFFSET 1"`;
exports[`Dblite adapter tests - Get compiled - getCompiledUpdate 1`] = `"UPDATE \\"create_test\\" SET \\"id\\"=?,\\"key\\"=?,\\"val\\"=? WHERE \\"id\\" = ?"`;

11
test/adapters/__snapshots__/mysql2_test.js.snap

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Mysql2 adapter tests - Get compiled - getCompiledDelete 1`] = `"DELETE FROM \`create_test\` WHERE \`id\` = ?"`;
exports[`Mysql2 adapter tests - Get compiled - getCompiledInsert 1`] = `"INSERT INTO \`create_test\` (\`id\`,\`key\`,\`val\`) VALUES (?,?,?)"`;
exports[`Mysql2 adapter tests - Get compiled - getCompiledSelect 1`] = `"SELECT \`id\`, \`key\` AS \`k\`, \`val\` FROM \`create_test\` WHERE (\`id\` > ? AND \`id\` < ?) LIMIT 1,2"`;
exports[`Mysql2 adapter tests - Get compiled - getCompiledSelect 2 1`] = `"SELECT \`id\`, \`key\` AS \`k\`, \`val\` FROM \`create_test\` WHERE (\`id\` > ? AND \`id\` < ?) LIMIT 1,2"`;
exports[`Mysql2 adapter tests - Get compiled - getCompiledUpdate 1`] = `"UPDATE \`create_test\` SET \`id\`=?,\`key\`=?,\`val\`=? WHERE \`id\` = ?"`;

11
test/adapters/__snapshots__/pg_test.js.snap

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Pg adapter tests - Get compiled - getCompiledDelete 1`] = `"DELETE FROM \\"create_test\\" WHERE \\"id\\" = ?"`;
exports[`Pg adapter tests - Get compiled - getCompiledInsert 1`] = `"INSERT INTO \\"create_test\\" (\\"id\\",\\"key\\",\\"val\\") VALUES (?,?,?)"`;
exports[`Pg adapter tests - Get compiled - getCompiledSelect 1`] = `"SELECT \\"id\\", \\"key\\" AS \\"k\\", \\"val\\" FROM \\"create_test\\" WHERE (\\"id\\" > ? AND \\"id\\" < ?) LIMIT 2 OFFSET 1"`;
exports[`Pg adapter tests - Get compiled - getCompiledSelect 2 1`] = `"SELECT \\"id\\", \\"key\\" AS \\"k\\", \\"val\\" FROM \\"create_test\\" WHERE (\\"id\\" > ? AND \\"id\\" < ?) LIMIT 2 OFFSET 1"`;
exports[`Pg adapter tests - Get compiled - getCompiledUpdate 1`] = `"UPDATE \\"create_test\\" SET \\"id\\"=?,\\"key\\"=?,\\"val\\"=? WHERE \\"id\\" = ?"`;

11
test/adapters/__snapshots__/sqlite3_test.js.snap

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Sqlite3 adapter tests - Get compiled - getCompiledDelete 1`] = `"DELETE FROM \\"create_test\\" WHERE \\"id\\" = ?"`;
exports[`Sqlite3 adapter tests - Get compiled - getCompiledInsert 1`] = `"INSERT INTO \\"create_test\\" (\\"id\\",\\"key\\",\\"val\\") VALUES (?,?,?)"`;
exports[`Sqlite3 adapter tests - Get compiled - getCompiledSelect 1`] = `"SELECT \\"id\\", \\"key\\" AS \\"k\\", \\"val\\" FROM \\"create_test\\" WHERE (\\"id\\" > ? AND \\"id\\" < ?) LIMIT 2 OFFSET 1"`;
exports[`Sqlite3 adapter tests - Get compiled - getCompiledSelect 2 1`] = `"SELECT \\"id\\", \\"key\\" AS \\"k\\", \\"val\\" FROM \\"create_test\\" WHERE (\\"id\\" > ? AND \\"id\\" < ?) LIMIT 2 OFFSET 1"`;
exports[`Sqlite3 adapter tests - Get compiled - getCompiledUpdate 1`] = `"UPDATE \\"create_test\\" SET \\"id\\"=?,\\"key\\"=?,\\"val\\"=? WHERE \\"id\\" = ?"`;

22
test/adapters/dblite_test.js

@ -19,7 +19,7 @@ describe('Dblite adapter tests -', () => {
});
testRunner(qb);
it('Promise - Select with function and argument in WHERE clause', async () => {
it('Select with function and argument in WHERE clause', async () => {
let promise = await qb.select('id')
.from('create_test')
.where('id', 'ABS(-88)')
@ -27,26 +27,6 @@ describe('Dblite adapter tests -', () => {
expect(promise).toEqual(expect.anything());
});
it('Promise - Test Insert Batch', async () => {
let data = [
{
id: 544,
key: 3,
val: Buffer.from('7')
}, {
id: 89,
key: 34,
val: Buffer.from('10 o\'clock')
}, {
id: 48,
key: 403,
val: Buffer.from('97')
}
];
let promise = await qb.insertBatch('create_test', data);
expect(promise).toEqual(expect.anything());
});
afterAll(() => {
qb.end();
});

27
test/adapters/mysql2_test.js

@ -24,7 +24,7 @@ describe('Mysql2 adapter tests -', () => {
});
testRunner(qb);
it('Promise - Select with function and argument in WHERE clause', async () => {
it('Select with function and argument in WHERE clause', async () => {
let promise = await qb.select('id')
.from('create_test')
.where('id', 'CEILING(SQRT(88))')
@ -36,28 +36,7 @@ describe('Mysql2 adapter tests -', () => {
let promise = await qb.truncate('create_test');
expect(promise).toEqual(expect.anything());
});
it('Test Insert Batch', async () => {
let data = [
{
id: 5442,
key: 4,
val: Buffer.from('7')
}, {
id: 892,
key: 35,
val: Buffer.from('10 o\'clock')
}, {
id: 482,
key: 404,
val: 97
}
];
const promise = await qb.insertBatch('create_test', data);
expect(promise).toEqual(expect.anything());
});
/* describeTeardown(() => {
afterAll(() => {
qb.end();
}); */
});
});

28
test/adapters/pg_test.js

@ -45,36 +45,16 @@ describe('Pg adapter tests -', () => {
});
testRunner(qb);
it('Promise - Select with function and argument in WHERE clause', async () => {
let promise = await qb.select('id')
it('Select with function and argument in WHERE clause', async () => {
const promise = await qb.select('id')
.from('create_test')
.where('id', 'CEILING(SQRT(88))')
.get();
expect(promise).toEqual(expect.anything());
});
it('Promise - Test Truncate', async () => {
let promise = await qb.truncate('create_test');
expect(promise).toEqual(expect.anything());
});
it('Promise - Test Insert Batch', async () => {
let data = [
{
id: 544,
key: 3,
val: Buffer.from('7')
}, {
id: 89,
key: 34,
val: Buffer.from('10 o\'clock')
}, {
id: 48,
key: 403,
val: Buffer.from('97')
}
];
let promise = await qb.insertBatch('create_test', data);
it('Test Truncate', async () => {
const promise = await qb.truncate('create_test');
expect(promise).toEqual(expect.anything());
});
afterAll(() => {

22
test/adapters/sqlite3_test.js

@ -20,7 +20,7 @@ describe('Sqlite3 adapter tests -', () => {
});
testRunner(qb);
it('Promise - Select with function and argument in WHERE clause', async () => {
it('Select with function and argument in WHERE clause', async () => {
let promise = await qb.select('id')
.from('create_test')
.where('id', 'ABS(-88)')
@ -28,26 +28,6 @@ describe('Sqlite3 adapter tests -', () => {
expect(promise).toEqual(expect.anything());
});
it('Promise - Test Insert Batch', async () => {
let data = [
{
id: 544,
key: 3,
val: Buffer.from('7')
}, {
id: 89,
key: 34,
val: Buffer.from('10 o\'clock')
}, {
id: 48,
key: 403,
val: Buffer.from('97')
}
];
let promise = await qb.insertBatch('create_test', data);
expect(promise).toEqual(expect.anything());
});
afterAll(() => {
qb.end();
});

5
test/base.js

@ -1,13 +1,8 @@
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
// Load the test config file
const configFile = './config.json';
module.exports = {
config: require(configFile),
expect: chai.expect,
tests: require('./base/tests'),
promiseTestRunner: require('./base/adapterPromiseTestRunner')
};

116
test/base/adapterPromiseTestRunner.js

@ -37,13 +37,14 @@ module.exports = function promiseTestRunner (qb) {
});
});
});
describe('DB update tests -', async () => {
describe('DB update tests -', () => {
beforeAll(done => {
let sql = qb.driver.truncate('create_test');
qb.query(sql).then(res => done())
qb.query(sql).then(() => done())
.catch(err => done(err));
});
it('Promise - Test Insert', async () => {
it('Test Insert', async () => {
const promise = await qb.set('id', 98)
.set('key', '84')
.set('val', Buffer.from('120'))
@ -51,7 +52,7 @@ module.exports = function promiseTestRunner (qb) {
expect(promise).toEqual(expect.anything());
});
it('Promise - Test Insert Object', async () => {
it('Test Insert Object', async () => {
const promise = await qb.insert('create_test', {
id: 587,
key: 1,
@ -60,7 +61,7 @@ module.exports = function promiseTestRunner (qb) {
expect(promise).toEqual(expect.anything());
});
it('Promise - Test Update', async () => {
it('Test Update', async () => {
const promise = await qb.where('id', 7)
.update('create_test', {
id: 7,
@ -70,7 +71,7 @@ module.exports = function promiseTestRunner (qb) {
expect(promise).toEqual(expect.anything());
});
it('Promise - Test set Array Update', async () => {
it('Test set Array Update', async () => {
let object = {
id: 22,
key: 'gogle',
@ -83,7 +84,7 @@ module.exports = function promiseTestRunner (qb) {
expect(promise).toEqual(expect.anything());
});
it('Promise - Test where set update', async () => {
it('Test where set update', async () => {
const promise = await qb.where('id', 36)
.set('id', 36)
.set('key', 'gogle')
@ -92,17 +93,17 @@ module.exports = function promiseTestRunner (qb) {
expect(promise).toEqual(expect.anything());
});
it('Promise - Test delete', async () => {
it('Test delete', async () => {
const promise = await qb.delete('create_test', {id: 5});
expect(promise).toEqual(expect.anything());
});
it('Promise - Delete with where', async () => {
it('Delete with where', async () => {
const promise = await qb.where('id', 5)
.delete('create_test');
expect(promise).toEqual(expect.anything());
});
it('Promise - Delete multiple where values', async () => {
it('Delete multiple where values', async () => {
const promise = await qb.delete('create_test', {
id: 5,
key: 'gogle'
@ -111,8 +112,46 @@ module.exports = function promiseTestRunner (qb) {
expect(promise).toEqual(expect.anything());
});
});
describe('Grouping tests -', async () => {
it('Promise - Using grouping method', async () => {
describe('Batch tests -', () => {
it('Test Insert Batch', async () => {
const data = [
{
id: 544,
key: 3,
val: Buffer.from('7')
}, {
id: 89,
key: 34,
val: Buffer.from('10 o\'clock')
}, {
id: 48,
key: 403,