node-query/docs/query-builder.js.html

1002 lines
24 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>DocStrap Source: query-builder.js</title>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/sunlight.default.css">
<link type="text/css" rel="stylesheet" href="styles/site.cosmo.css">
</head>
<body>
<div class="container-fluid">
<div class="navbar navbar-fixed-top navbar-inverse">
<div class="navbar-inner">
<a class="brand" href="index.html">DocStrap</a>
<ul class="nav">
<li class="dropdown">
<a href="modules.list.html" class="dropdown-toggle" data-toggle="dropdown">Modules<b
class="caret"></b></a>
<ul class="dropdown-menu ">
<li>
<a href="module-adapter.html">adapter</a>
</li>
<li>
<a href="module-driver.html">driver</a>
</li>
<li>
<a href="module-helpers.html">helpers</a>
</li>
<li>
<a href="module-node-query.html">node-query</a>
</li>
<li>
<a href="module-query-builder.html">query-builder</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
<div class="row-fluid">
<div class="span12">
<div id="main">
<h1 class="page-title">Source: query-builder.js</h1>
<section>
<article>
<pre
class="sunlight-highlight-javascript linenums">'use strict';
/** @module query-builder */
var getArgs = require('getargs'),
helpers = require('./helpers');
/**
* Variables controlling the sql building
*
* @private
*/
var state = {};
/*
* SQL generation object
*
* @param {driver} - The syntax driver for the database
* @param {adapter} - The database module adapter for running queries
* @constructor
*/
var QueryBuilder = function(driver, adapter) {
// That 'new' keyword is annoying
if ( ! (this instanceof QueryBuilder)) return new QueryBuilder(driver, adapter);
// Keep these properties as object members so they can be mocked/substituted
this.driver = driver;
this.adapter = adapter;
/**
* "Private" methods
*
* @private
*/
var _p = {
/**
* Complete the sql building based on the type provided
*
* @param {String} type
* @param {String} table
* @private
* @return {String}
*/
compile: function (type, table) {
// Put together the basic query
var sql = _p.compileType(type, table);
// Set each subClause
['queryMap', 'groupString', 'orderString', 'havingMap'].forEach(function(clause) {
var param = state[clause];
if ( ! helpers.isScalar(param))
{
Object.keys(param).forEach(function(part) {
sql += param[part].conjunction + param[part].string;
});
}
else
{
sql += param;
}
});
// Append the limit, if it exists
if (helpers.isNumber(state.limit))
{
sql = this.driver.limit(sql, state.limit, state.offset);
}
return sql;
},
compileType: function (type, table) {
var sql = '';
switch(type) {
case "insert":
var paramCount = state.setArrayKeys.length;
var params = [];
params.fill('?', 0, paramCount);
sql = "INSERT INTO " + table + " (";
sql += state.setArrayKeys.join(',');
sql += ") VALUES (";
sql += params.join(',') + ')';
break;
case "update":
sql = "UPDATE " + table + " SET " + state.setString;
break;
case "delete":
sql = "DELETE FROM " + table;
break;
default:
sql = "SELECT * FROM " + state.fromString;
// Set the select string
if (state.selectString.length > 0)
{
// Replace the star with the selected fields
sql = sql.replace('*', state.selectString);
}
break;
}
return sql;
},
like: function (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 = (state.queryMap.length &lt; 1) ? ' WHERE ' : ' ' + conj + '';
_p.appendMap(conj, like, 'like');
state.whereValues.push(val);
},
/**
* Append a clause to the query map
*
* @param {String} conjunction
* @param {String} string
* @param {String} type
* @return void
*/
appendMap: function(conjunction, string, type) {
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
*/
mixedSet: function(/* varName, key, [val], valType */) {
var args = getArgs('varName:string, key:string|array, [val]:string, valType:string', arguments);
var obj = {};
if (helpers.isString(args.key) &amp;&amp; helpers.isString(args.val))
{
obj[args.key] = args.val;
}
else
{
obj = args.key;
}
Object.keys(obj).forEach(function(k) {
if (args.valType != 'both')
{
state[args.varName].push(
(args.valType === 'key')? k: obj[k]
);
}
else
{
state[args.varName][k] = obj[k];
}
});
return state[args.varName];
},
whereMixedSet: function(key, val) {
_p.mixedSet('whereValues', key, val, 'value');
_p.mixedSet('whereMap', key, val, 'both');
},
where: function(key, val, conj) {
conj = conj || 'AND';
// Get an object to iterate over
_p.whereMixedSet(key, val);
Object.keys(state.whereMap).forEach(function(field) {
// Split each key by spaces, in case there
// is an operator such as >, &lt;, !=, etc.
var fieldArray = field.split(' ');
var item = this.driver.quoteIdentifiers(fieldArray[0]);
// Simple key value, or an operator?
item += (fieldArray.length === 1) ? '=?' : " " + fieldArray[1] + " ?";
var firstItem = state.queryMap[0],
lastItem = state.queryMap[state.queryMap.length - 1];
// Determine the correct conjunction
if (state.queryMap.length &lt; 1 || firstItem.contains('JOIN'))
{
conj = " WHERE ";
}
else if (lastItem.type === 'groupStart')
{
conj = '';
}
else
{
conj = ' ' + conj + ' ';
}
_p.appendMap(conj, item, 'where');
});
},
having: function(key, val, conj) {
conj = conj || 'AND';
_p.whereMixedSet(key, val);
Object.keys(state.whereMap).forEach(function(field) {
// Split each key by spaces, in case there
// is an operator such as >, &lt;, !=, etc.
var fieldArray = field.split(' ').map(helpers.stringTrim);
var item = this.driver.quoteIdentifiers(fieldArray[0]);
// Simple key value, or an operator?
item += (fieldArray.length === 1) ? '=?' : " " + fieldArray[1] + " ?";
// Put in the having map
state.havingMap.push({
conjunction: (state.havingMap.length > 0) ? " " + conj + " " : ' HAVING ',
string: item
});
});
},
whereIn: function(key, val, inClause, conj) {
key = this.driver.quoteIdentifiers(key);
var params = [];
params.fill('?', 0, val.length);
val.forEach(function(value) {
state.whereValues.push(value);
});
conj = (state.queryMap.length > 0) ? " " + conj + " " : ' WHERE ';
var str = key + " " + inClause + " (" + params.join(',') + ") ";
_p.appendMap(conj, str, 'whereIn');
},
run: function(type, table, callback, sql, vals) {
if ( ! sql)
{
sql = _p.compile(type, table);
}
if ( ! vals)
{
vals = state.values.concat(state.whereValues);
}
// Reset the state so another query can be built
_p.resetState();
// Pass the sql and values to the adapter to run on the database
adapter.execute(sql, vals, callback);
},
getCompile: function(type, table, reset) {
reset = reset || false;
var sql = _p.compile(type, table);
if (reset) _p.resetState();
return sql;
},
resetState: function() {
state = {
// Arrays/Maps
queryMap: {},
values: [],
whereValues: [],
setArrayKeys: [],
orderArray: [],
groupArray: [],
havingMap: [],
whereMap: [],
// Partials
selectString: '',
fromString: '',
setString: '',
orderString: '',
groupString: '',
// Other various values
limit: null,
offset: null
};
}
};
// ----------------------------------------------------------------------------
// ! Miscellaneous Methods
// ----------------------------------------------------------------------------
/**
* Reset the object state for a new query
*
* @memberOf query-builder
* @return void
*/
this.resetQuery = function() {
_p.resetState();
};
/**
* Returns the current class state for testing or other purposes
*
* @private
* @return {Object}
*/
this.getState = function() {
return state;
};
// ------------------------------------------------------------------------
// Set up state object
this.resetQuery();
// ------------------------------------------------------------------------
// ! Query Builder Methods
// ------------------------------------------------------------------------
/**
* Specify rows to select in the query
*
* @param {String|Array} fields - The fields to select from the current table
* @return this
*/
this.select = function(fields) {
// Split/trim fields by comma
fields = (Array.isArray(fields)) ? fields : fields.split(",").map(helpers.stringTrim);
// Split on 'As'
fields.forEach(function (field, index) {
if (field.match(/as/i))
{
fields[index] = field.split(/ as /i).map(helpers.stringTrim);
}
});
var safeArray = this.driver.quoteIdentifiers(fields);
// Join the strings back together
safeArray.forEach(function (field, index) {
if (Array.isArray(field))
{
safeArray[index] = safeArray[index].join(' AS ');
}
});
state.selectString += safeArray.join(', ');
return this;
};
/**
* Specify the database table to select from
*
* @param {String} tableName - The table to use for the current query
* @return this
*/
this.from = function(tableName) {
// Split identifiers on spaces
var identArray = tableName.trim().split(' ').map(helpers.stringTrim);
// Quote/prefix identifiers
identArray[0] = this.driver.quoteTable(identArray[0]);
identArray = this.driver.quoteIdentifiers(identArray);
// Put it back together
state.fromString = identArray.join(' ');
return this;
};
/**
* Add a 'like/ and like' clause to the query
*
* @param {String} field - The name of the field to compare to
* @param {String} val - The value to compare to
* @param {String} [pos=both] - The placement of the wildcard character(s): before, after, or both
* @return this
*/
this.like = function(field, val, pos) {
_p.like(field, val, pos, ' LIKE ', 'AND');
return this;
};
/**
* Add a 'not like/ and not like' clause to the query
*
* @param {String} field - The name of the field to compare to
* @param {String} val - The value to compare to
* @param {String} [pos=both] - The placement of the wildcard character(s): before, after, or both
* @return this
*/
this.notLike = function(field, val, pos) {
_p.like(field, val, pos, ' NOT LIKE ', 'AND');
return this;
};
/**
* Add an 'or like' clause to the query
*
* @param {String} field - The name of the field to compare to
* @param {String} val - The value to compare to
* @param {String} [pos=both] - The placement of the wildcard character(s): before, after, or both
* @return this
*/
this.orLike = function(field, val, pos) {
_p.like(field, val, pos, ' LIKE ', 'OR');
return this;
};
/**
* Add an 'or not like' clause to the query
*
* @param {String} field - The name of the field to compare to
* @param {String} val - The value to compare to
* @param {String} [pos=both] - The placement of the wildcard character(s): before, after, or both
* @return this
*/
this.orNotLike = function(field, val, pos) {
_p.like(field, val, pos, ' NOT LIKE ', 'OR');
return this;
};
/**
* Add a 'having' clause
*
* @param {String|Object} key - The name of the field and the comparision operator, or an object
* @param {String|Number} [val] - The value to compare if the value of key is a string
* @return this
*/
this.having = function(key, val) {
_p.having(key, val, 'AND');
return this;
};
/**
* Add an 'or having' clause
*
* @param {String|Object} key - The name of the field and the comparision operator, or an object
* @param {String|Number} [val] - The value to compare if the value of key is a string
* @return this
*/
this.orHaving = function(key, val) {
_p.having(key, val, 'OR');
return this;
};
/**
* Set a 'where' clause
*
* @param {String|Object} key - The name of the field and the comparision operator, or an object
* @param {String|Number} [val] - The value to compare if the value of key is a string
* @return this
*/
this.where = function(key, val) {
_p.where(key, val, 'AND');
return this;
};
/**
* Set a 'or where' clause
*
* @param {String|Object} key - The name of the field and the comparision operator, or an object
* @param {String|Number} [val] - The value to compare if the value of key is a string
* @return this
*/
this.orWhere = function(key, val) {
_p.where(key, val, 'OR');
return this;
};
/**
* Set a 'where in' clause
*
* @param {String} key - the field to search
* @param {Array} val - the array of items to search in
* @return this
*/
this.whereIn = function(key, val) {
_p.whereIn(key, val, 'IN', 'AND');
return this;
};
/**
* Set a 'or where in' clause
*
* @param {String} key - the field to search
* @param {Array} val - the array of items to search in
* @return this
*/
this.orWhereIn = function(key, val) {
_p.whereIn(key, val, 'IN', 'OR');
return this;
};
/**
* Set a 'where not in' clause
*
* @param {String} key - the field to search
* @param {Array} val - the array of items to search in
* @return this
*/
this.whereNotIn = function(key, val) {
_p.whereIn(key, val, 'NOT IN', 'AND');
return this;
};
/**
* Set a 'or where not in' clause
*
* @param {String} key - the field to search
* @param {Array} val - the array of items to search in
* @return this
*/
this.orWhereNotIn = function(key, val) {
_p.whereIn(key, val, 'NOT IN', 'OR');
return this;
};
/**
* Set values for insertion or updating
*
* @param {String|Object} key - The key or object to use
* @param {String} [val] - The value if using a scalar key
* @return this
*/
this.set = function(/* key, [val] */) {
var args = getArgs('key:string|object, [val]:string', arguments);
// Set the appropriate state variables
_p.mixedSet('setArrayKeys', args.key, args.val, 'key');
_p.mixedSet('values', args.key, args.val, 'value');
// Use the keys of the array to make the insert/update string
// and escape the field names
state.setArrayKeys = state.setArrayKeys.map(this.driver._quote);
// Generate the "set" string
state.setString = state.setArrayKeys.join('=?,');
state.setString += '=?';
return this;
};
/**
* Add a join clause to the query
*
* @param {String} joinOn - The table you are joining
* @param {String} [cond='='] - The join condition, eg. =,&lt;,>,&lt;>,!=,etc.
* @param {String} joinTo - The value of the condition you are joining on, whether another table's field, or a literal value
* @param {String} [type='inner'] - The type of join, which defaults to inner
* @return this
*/
this.join = function(/* joinOn, [cond='='], joinTo, [type='inner']*/) {
var args = getArgs('joinOn:string, [cond]:string, joinTo:string, [type]:string', arguments);
args.cond = args.cond || '=';
args.type = args.type || "inner";
return this;
};
/**
* Group the results by the selected field(s)
*
* @param {String|Array} field
* @return this
*/
this.groupBy = function(field) {
if (Array.isArray(field))
{
var newGroupArray = field.map(this.driver.quoteIdentifiers);
state.groupArray.concat(newGroupArray);
}
else
{
state.groupArray.push(this.driver.quoteIdentifiers(field));
}
state.groupString = ' GROUP BY ' + state.groupArray.join(',');
return this;
};
/**
* Order the results by the selected field(s)
*
* @param {String} field - The field to order by
* @param {String} [type='ASC'] - The order direction, ASC or DESC
* @return this
*/
this.orderBy = function(field, type) {
type = type || 'ASC';
// Set the fields for later manipulation
field = this.driver.quoteIdentifiers(field);
state.orderArray[field] = type;
var orderClauses = [];
// Flatten key/val pairs into an array of space-separated pairs
Object.keys(state.orderArray).forEach(function(key) {
orderClauses.push(key + ' ' + state.orderArray[key].toUpperCase());
});
// Set the final string
state.orderString = ' ORDER BY ' + orderClauses.join(', ');
return this;
};
/**
* Put a limit on the query
*
* @param {Number} limit - The maximum number of rows to fetch
* @param {Number} [offset] - The row number to start from
* @return this
*/
this.limit = function(limit, offset) {
state.limit = limit;
state.offset = offset || null;
return this;
};
/**
* Adds an open paren to the current query for logical grouping
*
* @return this
*/
this.groupStart = function() {
var conj = (state.queryMap.length &lt; 1) ? ' WHERE ' : ' ';
_p.appendMap(conj, '(', 'groupStart');
return this;
};
/**
* Adds an open paren to the current query for logical grouping,
* prefixed with 'OR'
*
* @return this
*/
this.orGroupStart = function() {
_p.appendMap('', ' OR (', 'groupStart');
return this;
};
/**
* Adds an open paren to the current query for logical grouping,
* prefixed with 'OR NOT'
*
* @return this
*/
this.orNotGroupStart = function() {
_p.appendMap('', ' OR NOT (', 'groupStart');
return this;
};
/**
* Ends a logical grouping started with one of the groupStart methods
*
* @return this
*/
this.groupEnd = function() {
_p.appendMap('', ')', 'groupEnd');
return this;
};
// ------------------------------------------------------------------------
// ! Result Methods
// ------------------------------------------------------------------------
/**
* Get the results of the compiled query
*
* @param {String} [table] - The table to select from
* @param {Number} [limit] - A limit for the query
* @param {Number} [offset] - An offset for the query
* @param {Function} callback - A callback for receiving the result
* @return void
*/
this.get = function(/* [table], [limit], [offset], callback */) {
var args = getArgs('[table]:string, [limit]:number, [offset]:number, callback:function', arguments);
if (args.table) {
this.from(args.table);
}
if (args.limit) {
this.limit(args.limit, args.offset);
}
// Run the query
_p.run('get', args.table, args.callback);
};
/**
* Run the generated insert query
*
* @param {String} table - The table to insert into
* @param {Object} [data] - Data to insert, if not already added with the 'set' method
* @param {Function} callback - Callback for handling response from the database
* @return void
*/
this.insert = function(table, data, callback) {
if (data) {
this.set(data);
}
// Run the query
_p.run('insert', table, callback);
};
/**
* Run the generated update query
*
* @param {String} table - The table to insert into
* @param {Object} [data] - Data to insert, if not already added with the 'set' method
* @param {Function} callback - Callback for handling response from the database
* @return void
*/
this.update = function(table, data, callback) {
if (data) {
this.set(data);
}
// Run the query
_p.run('update', table, callback);
};
/**
* Run the generated delete query
*
* @param {String} table - The table to insert into
* @param {Function} callback - Callback for handling response from the database
* @return void
*/
this['delete'] = function (table, callback) {
// Run the query
_p.run('delete', table, callback);
};
// ------------------------------------------------------------------------
// ! Methods returning SQL
// ------------------------------------------------------------------------
/**
* Return generated select query SQL
*
* @param {String} [table] - the name of the table to retrieve from
* @param {Boolean} [reset=true] - Whether to reset the query builder so another query can be built
* @return String
*/
this.getCompiledSelect = function(table, reset) {
if (table)
{
this.from(table);
}
return _p.getCompile('get', table, reset);
};
/**
* Return generated insert query SQL
*
* @param {String} table - the name of the table to insert into
* @param {Boolean} [reset=true] - Whether to reset the query builder so another query can be built
* @return {String}
*/
this.getCompiledInsert = function(table, reset) {
return _p.getCompile('insert', table, reset);
};
/**
* Return generated update query SQL
*
* @param {String} table - the name of the table to update
* @param {Boolean} [reset=true] - Whether to reset the query builder so another query can be built
* @return {String}
*/
this.getCompiledUpdate = function(table, reset) {
return _p.getCompile('update', table, reset);
};
/**
* Return generated delete query SQL
*
* @param {String} table - the name of the table to delete from
* @param {Boolean} [reset=true] - Whether to reset the query builder so another query can be built
* @return {String}
*/
this.getCompiledDelete = function(table, reset) {
return _p.getCompile('delete', table, reset);
};
return this;
};
module.exports = QueryBuilder;</pre>
</article>
</section>
</div>
<div class="clearfix"></div>
<footer>
<span class="copyright">
DocStrap Copyright © 2012-2014 The contributors to the JSDoc3 and DocStrap projects.
</span>
<br />
<span class="jsdoc-message">
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.3.0-alpha9</a>
on Thu Oct 23rd 2014 using the <a
href="https://github.com/terryweiss/docstrap">DocStrap template</a>.
</span>
</footer>
</div>
<br clear="both">
</div>
</div>
<!--<script src="scripts/sunlight.js"></script>-->
<script src="scripts/docstrap.lib.js"></script>
<script src="scripts/bootstrap-dropdown.js"></script>
<script src="scripts/toc.js"></script>
<script>
$( function () {
$( "[id*='$']" ).each( function () {
var $this = $( this );
$this.attr( "id", $this.attr( "id" ).replace( "$", "__" ) );
} );
$( "#toc" ).toc( {
anchorName : function ( i, heading, prefix ) {
return $( heading ).attr( "id" ) || ( prefix + i );
},
selectors : "h1,h2,h3,h4",
showAndHide : false,
scrollTo : "100px"
} );
$( "#toc>ul" ).addClass( "nav nav-pills nav-stacked" );
$( "#main span[id^='toc']" ).addClass( "toc-shim" );
$( '.dropdown-toggle' ).dropdown();
// $( ".tutorial-section pre, .readme-section pre" ).addClass( "sunlight-highlight-javascript" ).addClass( "linenums" );
$( ".tutorial-section pre, .readme-section pre" ).each( function () {
var $this = $( this );
var example = $this.find( "code" );
exampleText = example.html();
var lang = /{@lang (.*?)}/.exec( exampleText );
if ( lang && lang[1] ) {
exampleText = exampleText.replace( lang[0], "" );
example.html( exampleText );
lang = lang[1];
} else {
lang = "javascript";
}
if ( lang ) {
$this
.addClass( "sunlight-highlight-" + lang )
.addClass( "linenums" )
.html( example.html() );
}
} );
Sunlight.highlightAll( {
lineNumbers : true,
showMenu : true,
enableDoclinks : true
} );
} );
</script>
<!--Navigation and Symbol Display-->
<!--Google Analytics-->
</body>
</html>