A no-frills chainable interface for constructing mutable MySQL queries procedurally in Node.js
npm install queryize View on GitHub Report an Issue
var insert = queryize.insert;
var q = insert()
.into('users', 'u')
.set({name: 'John Doe', email: 'user@example.com'})
.compile();
//q.query contains INSERT INTO users SET u.name = ?, u.email = ?
//q.data contains ['John Doe', 'user@example.com']
Supports extension for node-mysql and node-mysql2 for inline evaluation with promises.
var queryize = require('queryize');
const mysql = require('mysql2/promise');
const pool = mysql.createPool({database: test});
queryize.evalFunction = (sql, data) => pool.query(sql, data);
queryize()
.from('employees').select()
.where({city: ['Chicago', 'Miami', 'San Francisco']})
.orderBy('lastname', 'firstname')
.then((results) => {
console.log(results);
})
.catch((err) => {
console.error(err);
});
Creates a query
object which encapsulates the state of the query to be generated
and provides the methods to manipulate that query.
An existing query object to duplicate.
var queryize = require('queryize');
var query = queryize();
var queryize = function (original) {
var query = {};
// bind all mutators directly to query
for (const [ key, fn ] of Object.entries(mutators)) {
query[key] = (typeof fn === 'function' ? fn.bind(query) : fn);
}
query._constructor = queryize;
query._isQueryizeObject = true;
query._evalFunction = queryize.evalFunction;
query._attributes = {
debugEnabled: false,
database: false,
tableName: false,
fromSubquery: false,
alias: false,
dataBindings: {},
useBoundParameters: queryize.useBoundParameters,
where: [],
whereBoolean: null,
set: [],
setKeys: {},
columns: [ '*' ],
joins: [],
orderBy: false,
groupBy: false,
distinct: false,
limit: false,
asName: false,
builder: false,
};
if (original) {
if (original._isQueryizeObject) {
Object.assign(query._attributes, original.export());
} else if (typeof original === 'object') {
Object.assign(query._attributes, original);
}
}
return query;
};
Shortcut for creating a new select query
See query.select()
for details.
queryize.select = function (...args) {
var q = queryize();
q.select(...args);
return q;
};
Shortcut for creating a new update query
See query.update()
for details.
queryize.update = function (...args) {
var q = queryize();
q.update(...args);
return q;
};
Shortcut for creating a new insert query
See query.insert()
for details.
queryize.insert = function (...args) {
var q = queryize();
q.insert(...args);
return q;
};
Shortcut for creating a new replace query
See query.replace()
for details.
queryize.replace = function (...args) {
var q = queryize();
q.replace(...args);
return q;
};
Shortcut for creating a new delete query
See query.select()
for details.
queryize.delete = function (...args) {
var q = queryize();
q.deleteFrom(...args);
return q;
};
queryize.deleteFrom = queryize.delete;
Globally controls if new queries will use bound params by default.
//disable bound params. This is not recommended
queryize.useBoundParameters = false;
Globally defines the function to evoke when calling query.then() or query.eval()
The function on this property will be copied to query._evalFunction at instantiation.
Receives two parameters, the query string and the data array, plus anything passed to query.eval()
The return value will be passed on the promise produced by query.then() and query.eval()
Public access point for adding functions to query objects
queryize.fn.someNewFunction = function () {
//`this` is the current query, use `this._attributes` to store query parameters
};
If passed a truthy value, query.eval()
will output the compiled query to the console.
query.debug = function debug (enable) {
this._attributes.debugEnabled = isDefined(enable) ? enable : true;
return this;
};
Stores the passed value
under a data binding with the key
name.
This allows for explicit usage of bindings within query strings.
The binding name to store the value under
The data to be stored
query.insertBinding = function insertBinding (key, value) {
if (typeof value === 'object') {
// if we've got a date, convert it into a mysql ready datestamp
if (value instanceof Date) {
value = mysqlDate(value);
} else {
throw new TypeError('Received unparsable object as field value.');
}
}
if (key.substr(0, 2) !== '{{') key = '{{' + key + '}}';
this._attributes.dataBindings[key] = value;
return this;
};
Stores the passed data
into the data bindings collection and returns a
unique string representation of that data for use in the query.
Data bindings in queryize are represented by a key name surrounded by
double brackets. These values are then converted to question marks after
the query is compiled, with the values appended to the data array.
If a modifier
function name is provided, the returned binding will be wrapped with that MySQL function.
The data to be stored
A MySQL function to wrap the binding in when it is inserted into SQL.
query.createBinding('name@example.com');
// => "{{binding0d}}"
query.createBinding('2013-06-18', 'DATE');
// => "DATE({{binding4f}})"
query.createBinding = function createBinding (value, modifier) {
if (value === true) return 'TRUE';
if (value === false) return 'FALSE';
if (value === null) return 'NULL';
var key = '{{' + _uniqueId('binding') + '}}';
this.insertBinding(key, value);
return modifier ? [ modifier, '(', key, ')' ].join('') : key;
};
Marks the query as being a SELECT statement.
One or more columns or an array of columns to select from may be passed
in as arguments. See query.columns()
for more details.
All arguments received are passed to a query.columns()
call
var q = queryize()
.select('name')
.from('users')
.where('user.id', 128)
.compile()
query.select = function select (...args) {
this._attributes.builder = 'select';
if (arguments.length) {
this.columns(args);
}
return this;
};
Marks the query as being a DELETE statement
Supports passing the target table and alias as syntatic sugar. See query.from()
for more details.
Table to delete from. If an array is passed, defines the tables that will be deleted from in a multi-table delete.
An alias to use for the table
var q = queryize()
.delete('users')
.where({'u.id':1})
.compile();
var q = queryize()
.from('logs')
.whereInRange('dts', null, '2014-06-01')
.deleteFrom()
.compile()
exports.delete = query.deleteFrom = function deleteFrom (tablename, alias) {
this._attributes.builder = 'delete';
if (isArray(tablename)) {
if (alias) {
this.from(tablename.shift(), alias);
tablename.unshift(alias);
}
this.columns(tablename);
} else if (tablename) {
this.from(tablename, alias);
}
return this;
};
Marks the query as being an INSERT statement
All arguments received are passed to a query.set()
call
queryize()
.insert({name: 'joe'})
.into('users')
query.insert = function insert (...args) {
this._attributes.builder = 'insert';
this._attributes.insertMode = 'INSERT';
if (arguments.length) {
this.set(...args);
}
return this;
};
Marks the query as being an INSERT IGNORE statement
All arguments received are passed to a query.set()
call
queryize()
.insertIgnore({name: 'joe'})
.into('users')
query.insertIgnore = function replace (...args) {
this._attributes.builder = 'insert';
this._attributes.insertMode = 'INSERT IGNORE';
if (arguments.length) {
this.set(...args);
}
return this;
};
Marks the query as being a REPLACE statement
All arguments received are passed to a query.set()
call
queryize()
.replace({name: 'joe'})
.into('users')
query.replace = function replace (...args) {
this._attributes.builder = 'insert';
this._attributes.insertMode = 'REPLACE';
if (arguments.length) {
this.set(...args);
}
return this;
};
Marks the query as being an UPDATE statement
Table to update
An alias to use for the table
queryize()
.update('users')
.set('name', 'bob')
.where('id', 234)
query.update = function update (tablename, alias) {
this._attributes.builder = 'update';
if (tablename) {
this.table(tablename, alias);
}
return this;
};
Defines the table that the query should be performed on.
Can be called directly but is better called via the syntactically correct aliases.
An alias to use for the table
queryize.update()
.table('users')
.set('name', 'bob')
.where('id', 234)
var q = queryize.select()
.from('users')
.compile()
queryize.insert()
.into('orders')
.set(order)
A query object to use as a subquery
Name to use for the subquery (overrides query.as() value)
queryize()
.select('AVG(total_per_user)')
.from(queryize()
.select('SUM(total_invoice) AS total_per_user')
.from('orders')
.groupBy('userid')
.as('order_totals');
query.table = exports.into = exports.from = function table (tablename, alias) {
if (isQueryizeObject(tablename)) {
if (isDefined(alias)) {
tablename = tablename.clone().as(alias);
}
this._attributes.fromSubquery = this._mergeSubquery(tablename);
return this;
}
this._attributes.tableName = tablename;
if (isDefined(alias)) {
this._attributes.alias = alias;
}
return this;
};
Defines what database that the query should be performed on.
This is only necessary if your connection has not defined a database
to use, or the query needs to act upon a database other than the one
currently in use.
An alias to use for the table
query.database = exports.intoDatabase = exports.fromDatabase = function database (dbname, tablename, alias) {
this._attributes.database = dbname;
if (tablename) {
this._attributes.tableName = tablename;
}
if (alias) {
this._attributes.alias = alias;
}
return this;
};
Defines what columns a SELECT statement should return, or what tables
a DELETE statement should delete from.
Accepts either an array of columns
, or multiple column
arguments.
Column names can be in any format allowed in a MySQL statement.
Calling multiple times will replace the previous columns with the new set.
By default, all queries have *
for SELECTs and nothing for DELETEs
var query = queryize.select()
.columns('users.*', 'passwords.hash as password_hash')
.from('users')
.join('passwords', {'users.id':'passwords.user_id'})
query.columns(['username', 'firstname', 'lastname']);
query.columns(
'users.id',
queryize()
.select('SUM(total_invoice)')
.from('orders')
.groupBy('userid')
.as('total_invoiced')
);
query.columns = function columns (...args) {
var self = this;
this._attributes.columns = null;
flatten(args).forEach((column) => {
self.addColumn(column);
});
return this;
};
Adds a single column to the columns list.
Column to add.
If truthy, duplicate columns will be skipped. This value is ignored for non-string columns.
query.addColumn = function addColumn (column, avoidDuplicates) {
// if the current columns list is only a '*' wildcard, reset the array.
// The expected use case in this scenario is that a new queryize object was made and they want to define columns individually.
if (this._attributes.columns && this._attributes.columns.length === 1 && this._attributes.columns[0] === '*') {
this._attributes.columns = null;
}
if (typeof column === 'number' || column instanceof Date) {
avoidDuplicates = false;
column = this.createBinding(column);
} else if (isQueryizeObject(column)) {
avoidDuplicates = false;
if (column._attributes.columns.length > 1 || column._attributes.columns[0] === '*') {
throw new Error('Column level subqueries can only return a single column.');
}
column = this._mergeSubquery(column);
} else if (typeof column === 'object' && isDefined(column.data)) {
avoidDuplicates = false;
column = this.createBinding(column.data, column.modifier);
}
if (typeof column !== 'string') {
throw new TypeError('Unknown column type: ' + JSON.stringify(column));
}
if (Array.isArray(this._attributes.columns)) {
if (!avoidDuplicates || this._attributes.columns.indexOf(column) === -1) {
this._attributes.columns.push(column);
}
} else {
this._attributes.columns = [ column ];
}
};
Controls the boolean operator used to combine multiple WHERE clauses
By default, Queryize will combine all top level WHERE clauses with AND operators.
"AND" or "OR"
query.comparisonMethod = function comparisonMethod (condition) {
if (typeof condition === 'boolean') {
condition = condition ? 'and' : 'or';
}
switch (String(condition).trim()) {
case 'and':
case 'AND':
case 'yes':
this._attributes.whereBoolean = 'AND'; break;
case 'or':
case 'OR':
case 'no':
this._attributes.whereBoolean = 'OR'; break;
// no default
}
return this;
};
Adds one or more WHERE clauses to the query.
Calling multiple times will append more clauses onto the stack.
Calling without any arguments will empty the stack.
If an operator
is provided, it will be used for all comparisons derived from this call.
If a modifier
function name is provided, the returned binding will be wrapped with that MySQL function.
//removes all existing where clauses
query.where()
A pre-written WHERE statement for direct insertion into the query
query.where('password IS NOT NULL')
// WHERE password IS NOT NULL
Multiple pre-written WHERE statements for direct insertion into the query as OR conditions unless otherwise defined.
query.where(['account.balance > 0', 'account.gratis IS TRUE'])
// WHERE account.balance > 0 OR account.gratis IS TRUE
query.where(['AND', 'client.active IS TRUE', 'client.paidthru < NOW()'])
// WHERE client.active IS TRUE AND client.paidthru < NOW()
The table field(s) to match against
The value(s) to match with (if more than one, performs an OR comparison of each)
The operator to use when performing the comparison (e.g. =, !=, >, LIKE, IS NOT, etc)
A MySQL function to wrap the binding in when it is inserted into SQL.
query.where('age', 21, '<')
// WHERE age < ?
// Data: [21]
query.where('created', new Date('2014-01-01'), '>=', 'DATE')
// WHERE created >= DATE(?)
// Data: ['2014-01-01 00:00:00']
Collection of field/value pairs to match against
The operator to use when performing the comparison (e.g. =, !=, >, LIKE, IS NOT, etc)
A MySQL function to wrap the binding in when it is inserted into SQL.
query.where({studio:'Paramount', franchise: 'Star Trek' });
// WHERE studio = ? AND franchise = ?
// Data: ['Paramount', 'Star Trek']
query.where({'property.ownership': ['Own', 'Rent']})
// WHERE property.ownership IN (?, ?)
// Data: ['Own', 'Rent']
query.where({'user.gender':'M, not: true, 'profile.spouse': null})
// WHERE user.gender = ? AND profile.spouse != NULL
// Data: ['M']
A subquery to be used as a compound where clause.
query
.where('type', [4,5,6])
.where(queryize()
.whereLike('address', '%' + search + '%')
.whereLike('city', '%' + search + '%')
)
// WHERE type IN (?,?,?) AND (address LIKE ? OR city LIKE ?)
// Data: [4, 5, 6, '%Foo%', '%Foo%']
query.where = function where (clause, value, operator, modifier) {
if (!isDefined(clause)) {
this._attributes.where = [];
return this;
}
var self = this;
if (isQueryizeObject(clause)) {
clause = this._mergeCompoundWhere(clause);
}
// if a value is defined, then we're performing a field > value comparison
// and must parse that first.
if (value !== undefined && (typeof clause === 'string' || isArray(clause))) {
clause = this._processWhereCondition(clause, value, operator, modifier);
// if there was no value, check to see if we got an object based where definition
} else if (typeof clause === 'object' && !isArray(clause)) {
modifier = operator;
operator = value;
clause = this._processWhereObject(clause, operator, modifier);
}
// if we've got an array at this point, then we should parse it as if it were
// a collection of possible conditions in an OR
if (isArray(clause)) {
clause = flatten(clause).map((c) => {
switch (typeof c) {
case 'string': return c;
case 'object': return self._processWhereObject(c, operator, modifier);
default:
throw new TypeError('Where clause could not be processed. Found ' + (typeof c) + ' instead.');
}
});
var l = clause.length;
var subBoolean = ' OR ';
if (l === 1) {
clause = clause[0];
} else if (l > 1) {
// if the first value in the array is "AND", reverse our typical boolean.
if (clause[0] === 'AND') {
subBoolean = ' AND ';
clause.shift();
l--;
}
if (l === 1) {
clause = clause[0];
} else {
clause = '(' + clause.join(subBoolean) + ')';
}
} else {
clause = null;
}
}
// by now the clause should be a string. if it isn't, then someone gave us an unusable clause
if (typeof clause === 'string') {
this._attributes.where.push(clause);
} else {
throw new TypeError('Where clause could not be processed. Found ' + (typeof clause) + ' instead.');
}
return this;
};
Handles basic (field, value) where clauses
query._processWhereCondition = function _processWhereCondition (field, value, operator, modifier) {
var self = this;
if (!operator) operator = '=';
if (isArray(field)) {
return field.map((f) => self._processWhereCondition(f, value, operator, modifier));
}
// if value is an array, then we need to compare against multiple values
if (isArray(value)) {
// if the operator is a plain equals, we should perform an IN() instead of multiple ORs
if (operator === '=') {
// process the values into bindings, and join the bindings inside an IN() clause
return field + ' IN (' + value.map((v) => self.createBinding(v, modifier)).join(',') + ')';
} else if (operator === '!=') {
// process the values into bindings, and join the bindings inside an IN() clause
return field + ' NOT IN (' + value.map((v) => self.createBinding(v, modifier)).join(',') + ')';
}
// process each value individually as a single condition and join the values in an OR
return value.map((v) => self._processWhereCondition(field, v, operator, modifier));
}
return [ field, operator, self.createBinding(value, modifier) ].join(' ');
};
Handles object based where clauses
query._processWhereObject = function _processWhereObject (clause, operator, modifier) {
if (!operator) operator = '=';
var self = this;
clause = Object.keys(clause).map((field) => {
// if the object contains a 'not' key, all subsequent keys parsed will be negations.
if (field === 'not' && clause[field] === true) {
if (operator === '=') operator = '!=';
return undefined;
}
return self._processWhereCondition(field, clause[field], operator, modifier);
});
clause = flatten(clause).filter((d) => d);
if (clause.length === 1) {
return clause[0];
} else if (clause.length > 1) {
return '(' + clause.join(' AND ') + ')';
}
return undefined;
};
Adds a where condition for a field between two values.
query.whereBetween('profile.income', 18000, 60000)
// Where profile.income BETWEEN ? AND ?
// Data: [18000, 60000]
query.whereBetween = function whereBetween (field, from, to, modifier) {
this.where([ field, 'BETWEEN', this.createBinding(from, modifier), 'AND', this.createBinding(to, modifier) ].join(' '));
return this;
};
Shortcut for performing a LIKE comparison on a field and value
queryize.select()
.from('users')
.whereLike('email', '%gmail.com')
query.whereLike = function whereLike (field, value, modifier) {
if (typeof field === 'object') {
this.where(field, 'LIKE', modifier);
} else {
this.where(field, value, 'LIKE', modifier);
}
return this;
};
Shortcut for performing an != comparisoon on a field and value
queryize.select()
.from('users')
.whereNot('type', 'Admin')
query.whereNot = function whereNot (field, value, modifier) {
if (typeof field === 'object') {
this.where(field, '!=', modifier);
} else {
this.where(field, value, '!=', modifier);
}
return this;
};
Shortcut for performing a NOT LIKE comparison on a field and value
[description]
[description]
[description]
query.whereNotLike = function whereNotLike (field, value, modifier) {
if (typeof field === 'object') {
this.where(field, 'NOT LIKE', modifier);
} else {
this.where(field, value, 'NOT LIKE', modifier);
}
return this;
};
Creates a where condition for if a field fit within a boundry of values.
Omitting/passing null to the from
or to
arguments will make the range boundless on that side.
query.whereInRange('profile.income', 18000, 60000)
// Where profile.income BETWEEN ? AND ?
// Data: [18000, 60000]
query.whereInRange('age', 21)
// WHERE age >= ?
// Data: [21]
query.whereInRange('product.cost', null, 100)
// WHERE product.cost <= ?
// Data: [100]
query.whereInRange = function whereInRange (field, from, to, modifier) {
if (isDefined(from) && isDefined(to)) {
this.whereBetween(field, from, to, modifier);
} else if (isDefined(from)) {
this.where(field, from, '>=', modifier);
} else if (isDefined(to)) {
this.where(field, to, '<=', modifier);
}
return this;
};
Defines what columns a select statement should use to sort the results.
Accepts either an array of columns
, or multiple column
arguments.
Calling multiple times will replace the previous sort order with the new values.
Column names can be in any format allowed in a MySQL statement.
query.orderBy('category', 'DATE(date_posted) DESC');
query.orderBy = function orderBy (...args) {
this._attributes.orderBy = flatten(args);
return this;
};
Defines what columns and conditions a select statement should group the results under.
Accepts either an array of columns
, or multiple column
arguments.
Calling multiple times will replace the previous grouping rules with the new values.
Column names can be in any format allowed in a MySQL statement.
query.groupBy('user.id', 'DATE(dts)');
query.groupBy = function groupBy (...args) {
this._attributes.groupBy = flatten(args);
return this;
};
Defines if a SELECT statement should return distinct results only
query.distinct = function distinct (enable) {
this._attributes.distinct = isDefined(enable) ? enable : true;
return this;
};
Defines the maximum results the query should return, and the starting offset of the first row within a set.
Total results to return
Starting offset of first row within the results
query.limit = function limit (max, offset) {
if (!isDefined(max)) max = 0;
if (!isDefined(offset)) offset = 0;
if (max) {
if (offset) {
this._attributes.limit = 'LIMIT ' + Number(offset) + ', ' + Number(max);
} else {
this._attributes.limit = 'LIMIT ' + Number(max);
}
} else {
this._attributes.limit = false;
}
return this;
};
Defines what data to set the specified columns to during INSERT and UPDATE queries
Any data provided via query.set
is ignored in Multi-Insert Mode. See query.addRow
for more details.
A fully written set condition
A plain object collection of column/value pairs
[description]
query.set('user.lastlogin = NOW()')
// SET user.lastlogin = NOW()
query.set('address', '9 Pseudopolis Yard')
// SET address = ?
// Data: ['9 Pseudopolis Yard']
query.set({firstname: 'Susan', lastname: 'Sto Helet'})
// SET firstname = ?, lastname = ?
// DATA: ['Susan', 'Sto Helet']
query.set('lastLogin', {raw:'NOW()'});
// SET lastLogin = NOW()
query.set = function set (clause, value, modifier) {
var self = this;
if (!isDefined(clause)) throw new Error('You must define a field to set.');
if (typeof clause === 'object') {
Object.keys(clause).forEach((field) => {
self.set(field, clause[field], value);
});
return this;
}
// if we received a value, create a set clause
if (typeof value === 'undefined') {
this._attributes.set.push(clause);
} else {
var column = clause;
if (value && typeof value === 'object' && typeof value.raw === 'string') {
clause = clause + ' = ' + value.raw;
} else {
if (!isValidPrimative(value)) {
throw new TypeError('Unknown data type in set clause');
}
clause = clause + ' = ' + this.createBinding(value, modifier);
}
// if we've previously created a clause for this column, overwrite it.
var key = this._attributes.setKeys[column];
if (isDefined(key)) {
this._attributes.set[key] = clause;
} else {
this._attributes.setKeys[column] = this._attributes.set.length;
this._attributes.set.push(clause);
}
}
return this;
};
Adds a row of data for a multi-row insert. Activates Multi-Insert Mode for the query.
About Multi-Insert Mode:
Single row inserts are performed using INSERT SET queries, where each column value is defined one at a time.
Example: INSERT INTO user SET firstname=?, lastname=?
This is more efficient for query generation and less prone to mistakes, but can only insert a single row at a time. When in Multi-Insert Mode, Queryize will construct an INSERT VALUES query where all columns are defined up front and preceded by multiple data sets.
Example: INSERT INTO user (firstname, lastname) VALUES (?,?), (?,?)
This format is more performant for inserting multiple uniform data sets, but is less flexible as all the columns must be known upfront. If row contents are provided as arrays, the column order must be defined before calling query.addRow
. If keyed objects are provided then Queryize will use the keys to identify the column names to use.
Any rows with missing columns will receive NULL
for the column's contents.
May be an object where the keys represent all columns, or an array that matches the number of columns already defined.
queryize
.insert()
.into('users')
.columns('name', 'email')
.addRow(['John Smith', 'jsmith@example.com'])
.addRow(['Jane Doe', 'jdoe@example.com'])
.compile()
// INSERT INTO users (name, email) VALUES (?, ?), (?, ?)
// DATA: ['John Smith', 'jsmith@example.com', 'Jane Doe', 'jdoe@example.com']
queryize
.insert()
.into('users')
.addRow({name: 'John Smith', email: 'jsmith@example.com'})
.addRow({name: 'Jane Doe', email: 'jdoe@example.com', date_created: {raw:'NOW()'}})
.compile()
// INSERT INTO users (name, email, date_created) VALUES (?, ?, NULL), (?, ?, NOW())
// DATA: ['John Smith', 'jsmith@example.com', 'Jane Doe', 'jdoe@example.com']
query.addRow = function addRow (data) {
var row, columns;
var self = this;
// make sure the rows property exists
if (!this._attributes.rows) {
this._attributes.rows = [];
}
if (Array.isArray(data)) {
columns = this._attributes.columns || [];
// if the current columns list is only a '*' wildcard, then the developer has not defined any columns yet and we can't take array data.
if (!this._attributes.columns || this._attributes.columns.length === 1 && this._attributes.columns[0] === '*') {
throw new Error('You must define the insert columns first in order to pass rows as array.');
}
if (columns.length !== data.length) {
throw new Error('Row does not contain the same number of entries (' + data.length + ') as the number of defined columns (' + columns.length + ').');
}
row = {};
columns.forEach((column, i) => {
row[column] = data[i];
});
this._attributes.rows.push(row);
} else if (typeof data === 'object') {
columns = Object.keys(data);
columns.forEach((column) => {
self.addColumn(column, true);
});
this._attributes.rows.push(data);
} else {
throw new TypeError('Row is not of the expected data type.');
}
return this;
};
Adds a table join to the query.
Queryize will append any missing JOIN command at the beginning of a statement
Fully formed join statement
query.join('orders o ON o.client_id = c.id')
query.join('JOIN orders o ON o.client_id = c.id')
query.join('LEFT JOIN orders o ON o.client_id = c.id')
Plain object containing options for the join
query.join('orders', {alias: 'o', on: 'o.client_id = c.id'})
query.join('orders', {alias: 'o', on: {'o.client_id':'c.id'})
// JOIN orders o ON o.client_id = c.id
query.join('orders', {on: {'o.client_id':'c.id', not:true, o.status: [4,5]})
// JOIN orders ON o.client_id = c.id AND o.status NOT IN (?,?)
// Data: [4, 5]
query.join('orders', {on: {'o.client_id':'c.id', not:true, o.condition: {data: 'A'}})
// JOIN orders ON o.client_id = c.id AND o.status != ?
// Data: ['A']
query.join('orders', {alias: 'o', type: 'LEFT', on: 'o.client_id = c.id'})
// LEFT JOIN orders o ON o.client_id = c.id
Plain object containing options for the join
query.join({table: 'orders', alias: 'o', on: {'o.client_id':'c.id'})
query.join({table: 'orders', alias: 'o', type: 'LEFT' on: {'o.client_id':'c.id'})
// LEFT JOIN orders o ON o.client_id = c.id
query.join('orders', {on: {'o.client_id':'c.id', not:true, o.condition: {data: 'A'}})
// JOIN orders ON o.client_id = c.id AND o.status != ?
// Data: ['A']
A query object to use as a subquery
Plain object containing options for the join
queryize()
.select('AVG(total_per_user)')
.from(queryize()
.select('SUM(total_invoice) AS total_per_user')
.from('orders')
.groupBy('userid')
.as('order_totals');
query.join = function join (clause, options) {
if (!isDefined(clause)) throw new Error('You must define a table to join against.');
if (isQueryizeObject(clause)) {
if (typeof options !== 'object') throw new Error('You must define join options when joining against a queryize subquery.');
options = Object.create(options);
options.table = clause;
clause = options;
} else if (typeof clause === 'object') {
if (isArray(clause)) throw new TypeError('Join clauses can only be strings or object definitions');
if (!clause.table) throw new Error('You must define a table to join against');
} else if (typeof options === 'object') {
options = Object.create(options);
options.table = clause;
clause = options;
} else if (typeof clause === 'string' && clause.search(joinTest) < 0) {
clause = 'JOIN ' + clause;
}
if (typeof clause === 'object') {
var stack = [];
if (clause.type) stack.push(clause.type);
stack.push('JOIN');
if (isQueryizeObject(clause.table)) {
if (clause.alias) {
clause.table = clause.table.clone().as(clause.alias);
}
stack.push(this._mergeSubquery(clause.table));
} else if (typeof clause.table === 'string') {
stack.push(clause.table);
if (clause.alias) stack.push(clause.alias);
} else {
throw new TypeError('The join table name is not a string.');
}
if (clause.using) {
stack.push('USING (' + (isArray(clause.using) ? clause.using.join(', ') : clause.using) + ')');
} else if (clause.on) {
stack.push('ON (' + this._processJoinOns(clause.on, clause.onBoolean || 'AND') + ')');
}
clause = stack.join(' ');
}
this._attributes.joins.push(clause);
return this;
};
Generates the closures for innerJoin, leftJoin and rightJoin
Join type
function _makeJoiner (type) {
return function (clause, options) {
if (isQueryizeObject(clause)) {
if (typeof options !== 'object') throw new Error('You must define join options when joining against a queryize subquery.');
options = Object.create(options);
options.table = clause;
clause = options;
} else if (typeof clause === 'object') {
if (isArray(clause)) throw new TypeError('Join clauses can only be strings or object definitions');
if (!clause.table) throw new Error('You must define a table to join against');
} else if (typeof options === 'object') {
options = Object.create(options);
options.table = clause;
clause = options;
}
if (typeof clause === 'object') {
clause.type = type;
} else {
clause = clause.search(joinTest) > -1 ? clause.replace(joinTest, type + ' JOIN ') : type + ' JOIN ' + clause;
}
this.join(clause);
return this;
};
}
Shortcut for creating an INNER JOIN
See query.join()
for argument details
query.innerJoin = _makeJoiner('INNER');
Shortcut for creating a LEFT JOIN
See query.join()
for argument details
query.leftJoin = _makeJoiner('LEFT');
Shortcut for creating a RIGHT JOIN
See query.join()
for argument details
query.rightJoin = _makeJoiner('RIGHT');
Processes join conditions into a standardized format that's uniformly parsable
query._processJoinOns = function _processJoinOns (ons, onBoolean) {
if (typeof ons === 'string') {
return ons;
}
var self = this;
if (isArray(ons)) {
ons = ons.map((d) => self._processJoinOns(d));
}
if (typeof ons === 'object' && !isArray(ons)) {
var not = false;
ons = Object.keys(ons).map((field) => {
var value = ons[field];
// if the object contains a 'not' key, all subsequent keys parsed will be negations.
if (field === 'not' && value === true) {
not = true;
return undefined;
}
// if value is an array, perform an IN() on the values
if (isArray(value)) {
value = value.map((d) => self.createBinding(d));
return [ field, not ? 'NOT IN' : 'IN', '(', value.join(', '), ')' ].join(' ');
// if value is a string or a number, process as if a normal pairing of columns
} else if (typeof value === 'string') {
return [ field, not ? '!=' : '=', value ].join(' ');
// finally, process the value as if it were an actual value for binding
} else if (isValidPrimative(value)) {
return [ field, not ? '!=' : '=', self.createBinding(value) ].join(' ');
// if value is an object, verify if it's a data object, and if so create a binding for the value
} else if (typeof value === 'object' && value.data !== undefined) {
return [ field, not ? '!=' : '=', self.createBinding(value.data, value.modifier) ].join(' ');
}
// we don't know how to handle the value
throw new Error('Encountered unexpected value while parsing a JOIN ON condition for ' + field);
});
}
// remove any undefined or empty string values
ons = ons.filter((d) => d);
return ons.join(' ' + onBoolean + ' ');
};
Defines the name to be used to identify the results of a subquery
query.as = function asNamed (name) {
this._attributes.asName = name;
return this;
};
Compiles this query for use as a subquery in another query object.
query._buildSubquery = function _buildSubquery () {
if (!this._attributes.tableName && !this._attributes.fromSubquery) throw new Error('No table name has been defined for a sub-query.');
// I couldn't find a single instance of a non-select subquery, so I'm just assuming it here
var queryString = builders.select.call(this);
var queryData = this._collectNamedParameters(queryString);
queryString = '(' + queryString + ') as `' + (this._attributes.asName || _uniqueId('subquery')) + '`';
return {
query: queryString,
data: queryData,
};
};
Compiles this query for use as a where clause in another query.
query._buildCompoundWhere = function _buildCompoundWhere () {
var wheres = this._attributes.where.concat();
// if the whereBoolean is defined and contains AND, the compound condition was explicitly defined as an AND.
if (this._attributes.whereBoolean === 'AND') {
// an array of wheres is normally processed as an OR condition.
// prefixing it with 'AND' changes it to an AND condition
wheres.unshift('AND');
}
return {
where: wheres,
data: this._collectNamedParameters(wheres.join(' ')),
};
};
Extracts an external query object as a subquery and merges the bound data values into this query
query._mergeSubquery = function _mergeSubquery (subquery) {
if (!isQueryizeObject(subquery)) throw new TypeError('Provided subquery is not a queryize object.');
var q = subquery._buildSubquery();
Object.assign(this._attributes.dataBindings, q.data);
return q.query;
};
Extracts the where clauses of an external query object and merges the bound data values into this query
query._mergeCompoundWhere = function _mergeCompoundWhere (subquery) {
if (!isQueryizeObject(subquery)) throw new TypeError('Provided compound where is not a queryize object.');
var q = subquery._buildCompoundWhere();
Object.assign(this._attributes.dataBindings, q.data);
return q.where;
};
Causes queryize to insert escaped data directly into the query instead of using data bound placeholders.
This is not recommended and should only be used for debugging purposes.
You can also set queryize.useBoundParameters = false
to disable databinding for all queries
query.disableBoundParameters = function disableBoundParameters (bool) {
this._attributes.useBoundParameters = isDefined(bool) ? !bool : false;
return this;
};
query.useBoundParameters = function (bool) {
this.disableBoundParameters(isDefined(bool) ? !bool : true);
return this;
};
Constructs the table name to use in queries, with backticks
query._buildTableName = function _buildTableName () {
if (this._attributes.fromSubquery) {
return this._attributes.fromSubquery;
}
var q = [];
if (this._attributes.database) {
q.push('`' + this._attributes.database + '`.');
}
q.push('`' + this._attributes.tableName + '`');
if (this._attributes.alias) {
q.push(' ' + this._attributes.alias);
}
return q.join('');
};
Parses the comparisonMethod value and returns a string suitable as a delimiter.
query._buildWhereBoolean = function _buildWhereBoolean () {
return ' ' + (this._attributes.whereBoolean && this._attributes.whereBoolean.trim() || 'AND') + ' ';
};
query._builders = builders;
Compiles the final MySQL query
query.compile = function compile (useBoundParameters) {
if (!this._attributes.builder || !builders[this._attributes.builder]) throw new Error('Query operation undefined, must identify if performing a select/update/insert/delete query.');
if (!this._attributes.tableName && !this._attributes.fromSubquery) throw new Error('No table name has been defined');
if (this._attributes.builder === 'insert' && this._attributes.rows) {
this._attributes.builder = 'insertMultiple';
}
if (typeof useBoundParameters === 'undefined') useBoundParameters = this._attributes.useBoundParameters;
var q = builders[this._attributes.builder].call(this);
if (typeof q === 'string') {
return this._convertNamedParameters(q, useBoundParameters);
}
if (typeof q !== 'object' || !q.query) throw new Error('INTERNAL ERROR: Query builder returned unrecognized output.');
return q;
};
Processes the freshly compiled query string, replacing all data bindings with placeholders and constructs the data array
If useBoundParameters is turned off, it replaces the placeholders with their escaped values.
The query string to be processed
query._convertNamedParameters = function _convertNamedParameters (queryString, useBoundParameters) {
var data = [];
var self = this;
queryString = queryString.replace(/({{\w*}})/g, (match, name) => {
if (self._attributes.dataBindings[name] === undefined) throw new Error('The data binding ' + name + ' could not be found.');
data.push(self._attributes.dataBindings[name]);
return '?';
});
if (!useBoundParameters) {
queryString = SQLString.format(queryString, data);
data = [];
}
Processes the freshly compiled query string, finding all data bindings placeholders and collecting the values for those bindings.
The query string to be processed
query._collectNamedParameters = function _collectNamedParameters (queryString) {
var data = {};
var self = this;
(queryString.match(/({{\w*}})/g) || []).forEach((name) => {
// Subqueries might reference data bound to the parent query, so it's ok if a value doesn't exist
if (self._attributes.dataBindings[name] !== undefined) {
data[name] = self._attributes.dataBindings[name];
}
});
return data;
};
Compiles the MySQL query and executes it using queryize.evalFunction
, returning a promise.
If no eval function has been defined, the promise resolves with a tuple of the query string and data array.
Any options to be passed to the evalFunction
// pool is a mysql2/promise connection pool
queryize.evalFunction = (q, d, options) => pool.query(q, d, options);
const query = queryize()
.select()
.from('employees')
.orderBy('lastname', 'firstname');
query.eval(options).then((result) => console.log(result))
// using with async/await
queryize.evalFunction = (q, d, options) => pool.query(q, d, options);
const result = await queryize()
.select()
.from('employees')
.orderBy('lastname', 'firstname')
.eval(options);
query.eval = async function evaluate (...args) {
const { query: q, data } = this.compile();
if (this._attributes.debugEnabled) console.log({ query: q, data }); // eslint-disable-line no-console
if (typeof this._evalFunction !== 'function') return [ q, data ];
return await this._evalFunction(q, data, ...args);
};
Compiles the MySQL query and executes it using queryize.evalFunction
, returning a promise.
If no eval function has been defined, the promise resolves with a tuple of the query string and data array.
Function to execute when the evaluation completes
Function to execute if the evaluation fails
// pool is a mysql2/promise connection pool
queryize.evalFunction = (q, d) => pool.query(q, d);
const query = queryize()
.select()
.from('employees')
.orderBy('lastname', 'firstname');
query.then((result) => console.log(result))
// using with async/await
queryize.evalFunction = (q, d) => pool.query(q, d);
const result = await queryize()
.select()
.from('employees')
.orderBy('lastname', 'firstname');
query.then = function then (...args) {
const p = this.eval();
if (args.length) return p.then(...args);
return p;
};
Copies the attribute data for the query object and returns the copy
query.export = function exportAttributes () {
return clonedeep(this._attributes, true);
};
Generates a duplicate of the current query
var findCompleted = queryize()
.from('orders')
.where('status', 'completed')
.select();
var archiveCompleted = a.clone()
.update()
.set('status', 'archived');
query.clone = function clone () {
return this._constructor(this);
};
Compiles the query without bound parameters, escaping data in-line with the query.
var q = queryize()
.select()
.from('employees')
.where({ type: 'fulltime'});
console.log('' + q); // SELECT * FROM `employees` WHERE (type = "fulltime") ORDER BY lastname, firstname
query.toString = function toString () {
return this.compile(false).query;
};
creates a string that is unique to this runtime session
function _uniqueId (prefix) {
var id = (++idCounter).toString(16);
return prefix ? prefix + id : id;
}
Test if a value is defined and not null
function isDefined (value) {
return value !== undefined && value !== null;
}
Tests if a value is a valid type for storing in mysql
function isValidPrimative (value) {
return typeof value === 'string' ||
typeof value === 'boolean' ||
typeof value === 'number' ||
value === null ||
(value instanceof Date);
}
Tests if the provided value is a queryize query object
function isQueryizeObject (value) {
return typeof value === 'object' && value._isQueryizeObject;
}
BREAKING CHANGES:
query.exec
has now been removed my default and is no longer supported.New: Added query.eval([options])
and query.then()
, which executes the query using a method defined on queryize.evalFunction
and returns a promise
Dependencies: Bluebird, proxmis and Lodash have been removed.
insertIgnore
function.BREAKING CHANGES:
query.emitted()
query.run()
alias for query.exec()
query.exec()
now always uses the connection.query
function instead of connection.execute
.New: Casting a query object to a string now produces a query with escaped values.
Fixed: query.clone()
will now properly clone all attributes.
Dependencies:
lodash.assign
with ES6 Object.assign
lodash.clone
with lodash.clonedeep
.lodash.mapvalues
to 4.5.0proxmis
to 1.0.0sqlstring
package to handle data escapementInternal changes:
INSERT INTO table (columnA, columnB) VALUES (valueA, valueB)
, supporting multiple rows of data to be inserted.query.addRow
is used.query.replace()
as an alternative to query.insert()
query.set()
values
query.addColumn
to append extra columns outside of the query.column
full set.query.set()
not properly overwriting a previously defined key/value pair.query.emitted()
as an alternative to query.exec()
.
query.as(name)
to define the name for the subquery. If omitted, queryize will create a random name.query.debug()
that prevented it from enabling without passing true.query.exec()
this._attributes
instead of in a local variable.queryize.fn
now contains the prototype of the queryize query object. See lib/mutators.js for the contents.query.clone()
to duplicate a query state inlinequery.deleet()
to query.delete()
query.disableBoundParameters
and deprecated query.useBoundParameters
query.limit()
and query.distinct()
query.where()
connection.execute()
functions. Prefers over connection.query()
query.exec()
now extends the node-mysql(2) query emitter object with .then()
and .catch()
, allowing use as a promise.query.exec()
and deprecated query.run()
to be more consistent with other database libraries (eg, Mongoose)query.run()