foxhound
Version:
A Database Query generation library.
937 lines (797 loc) • 23.1 kB
JavaScript
/**
* FoxHound Query Generation Library
* @license MIT
* @author Steven Velozo <steven@velozo.com>
*/
// Load our base parameters skeleton object
const baseParameters = require('./Parameters.js');
var FoxHound = function()
{
function createNew(pFable, pFromParameters)
{
// If a valid Fable object isn't passed in, return a constructor
if ((typeof(pFable) !== 'object') || !('fable' in pFable))
{
return {new: createNew};
}
var _Fable = pFable;
// The default parameters config object, used as a template for all new
// queries created from this query.
var _DefaultParameters = (typeof(pFromParameters) === 'undefined') ? {} : pFromParameters;
// The parameters config object for the current query. This is the only
// piece of internal state that is important to operation.
var _Parameters = false;
var _Dialects = require('./Foxhound-Dialects.js');
// The unique identifier for a query
var _UUID = _Fable.getUUID();
// The log level, for debugging chattiness.
var _LogLevel = 0;
// The dialect to use when generating queries
var _Dialect = false;
/**
* Clone the current FoxHound Query into a new Query object, copying all
* parameters as the new default. Clone also copies the log level.
*
* @method clone
* @return {Object} Returns a cloned Query. This is still chainable.
*/
var clone = function()
{
var tmpFoxHound = createNew(_Fable, baseParameters)
.setScope(_Parameters.scope)
.setBegin(_Parameters.begin)
.setCap(_Parameters.cap);
// Schema is the only part of a query that carries forward.
tmpFoxHound.query.schema = _Parameters.query.schema;
if (_Parameters.dataElements)
{
tmpFoxHound.parameters.dataElements = _Parameters.dataElements.slice(); // Copy the array of dataElements
}
if (_Parameters.sort)
{
tmpFoxHound.parameters.sort = _Parameters.sort.slice(); // Copy the sort array.
// TODO: Fix the side affect nature of these being objects in the array .. they are technically clones of the previous.
}
if (_Parameters.filter)
{
tmpFoxHound.parameters.filter = _Parameters.filter.slice(); // Copy the filter array.
// TODO: Fix the side affect nature of these being objects in the array .. they are technically clones of the previous.
}
return tmpFoxHound;
};
/**
* Reset the parameters of the FoxHound Query to the Default. Default
* parameters were set during object construction.
*
* @method resetParameters
* @return {Object} Returns the current Query for chaining.
*/
var resetParameters = function()
{
_Parameters = _Fable.Utility.extend({}, baseParameters, _DefaultParameters);
_Parameters.query = ({
disableAutoIdentity: false,
disableAutoDateStamp: false,
disableAutoUserStamp: false,
disableDeleteTracking: false,
body: false,
schema: false, // The schema to intersect with our records
IDUser: 0, // The user to stamp into records
UUID: _Fable.getUUID(), // A UUID for this record
records: false, // The records to be created or changed
parameters: {}
});
_Parameters.result = ({
executed: false, // True once we've run a query.
value: false, // The return value of the last query run
// Updated below due to changes in how Async.js responds to a false value here
error: undefined // The error message of the last run query
});
return this;
};
resetParameters();
/**
* Reset the parameters of the FoxHound Query to the Default. Default
* parameters were set during object construction.
*
* @method mergeParameters
* @param {Object} pFromParameters A Parameters Object to merge from
* @return {Object} Returns the current Query for chaining.
*/
var mergeParameters = function(pFromParameters)
{
_Parameters = _Fable.Utility.extend({}, _Parameters, pFromParameters);
return this;
};
/**
* Set the the Logging level.
*
* The log levels are:
* 0 - Don't log anything
* 1 - Log queries
* 2 - Log queries and non-parameterized queries
* 3 - Log everything
*
* @method setLogLevel
* @param {Number} pLogLevel The log level for our object
* @return {Object} Returns the current Query for chaining.
*/
var setLogLevel = function(pLogLevel)
{
var tmpLogLevel = 0;
if (typeof(pLogLevel) === 'number' && (pLogLevel % 1) === 0)
{
tmpLogLevel = pLogLevel;
}
_LogLevel = tmpLogLevel;
return this;
};
/**
* Set the Scope for the Query. *Scope* is the source for the data being
* pulled. In TSQL this would be the _table_, whereas in MongoDB this
* would be the _collection_.
*
* A scope can be either a string, or an array (for JOINs and such).
*
* @method setScope
* @param {String} pScope A Scope for the Query.
* @return {Object} Returns the current Query for chaining.
*/
var setScope = function(pScope)
{
var tmpScope = false;
if (typeof(pScope) === 'string')
{
tmpScope = pScope;
}
else if (pScope !== false)
{
_Fable.log.error('Scope set failed. You must pass in a string or array.', {queryUUID:_UUID, parameters:_Parameters, invalidScope:pScope});
}
_Parameters.scope = tmpScope;
if (_LogLevel > 2)
{
_Fable.log.info('Scope set: '+tmpScope, {queryUUID:_UUID, parameters:_Parameters});
}
return this;
};
/**
* Set whether the query returns DISTINCT results.
* For count queries, returns the distinct for the selected fields, or all fields in the base table by default.
*
* @method setDistinct
* @param {Boolean} pDistinct True if the query should be distinct.
* @return {Object} Returns the current Query for chaining.
*/
var setDistinct = function(pDistinct)
{
_Parameters.distinct = !!pDistinct;
if (_LogLevel > 2)
{
_Fable.log.info('Distinct set: '+_Parameters.distinct, {queryUUID:_UUID, parameters:_Parameters});
}
return this;
};
/**
* Set the Data Elements for the Query. *Data Elements* are the fields
* being pulled by the query. In TSQL this would be the _columns_,
* whereas in MongoDB this would be the _fields_.
*
* The passed values can be either a string, or an array.
*
* @method setDataElements
* @param {String} pDataElements The Data Element(s) for the Query.
* @return {Object} Returns the current Query for chaining.
*/
var setDataElements = function(pDataElements)
{
var tmpDataElements = false;
if (Array.isArray(pDataElements))
{
// TODO: Check each entry of the array are all strings
tmpDataElements = pDataElements;
}
if (typeof(pDataElements) === 'string')
{
tmpDataElements = [pDataElements];
}
_Parameters.dataElements = tmpDataElements;
if (_LogLevel > 2)
{
_Fable.log.info('Data Elements set', {queryUUID:_UUID, parameters:_Parameters});
}
return this;
};
/**
* Set the sort data element
*
* The passed values can be either a string, an object or an array of objects.
*
* The Sort object has two values:
* {Column:'Birthday', Direction:'Ascending'}
*
* @method setSort
* @param {String} pSort The sort criteria(s) for the Query.
* @return {Object} Returns the current Query for chaining.
*/
var setSort = function(pSort)
{
var tmpSort = false;
if (Array.isArray(pSort))
{
// TODO: Check each entry of the array are all conformant sort objects
tmpSort = pSort;
}
else if (typeof(pSort) === 'string')
{
// Default to ascending
tmpSort = [{Column:pSort, Direction:'Ascending'}];
}
else if (typeof(pSort) === 'object')
{
// TODO: Check that this sort entry conforms to a sort entry
tmpSort = [pSort];
}
_Parameters.sort = tmpSort;
if (_LogLevel > 2)
{
_Fable.log.info('Sort set', {queryUUID:_UUID, parameters:_Parameters});
}
return this;
};
/**
* Set the join data element
*
* The passed values can be either an object or an array of objects.
*
* The join object has four values:
* {Type:'INNER JOIN', Table:'Test', From:'Test.ID', To:'Scope.IDItem'}
*
* @method setJoin
* @param {Object} pJoin The join criteria(s) for the Query.
* @return {Object} Returns the current Query for chaining.
*/
var setJoin = function(pJoin)
{
_Parameters.join = [];
if (Array.isArray(pJoin))
{
pJoin.forEach(function(join)
{
addJoin(join.Table, join.From, join.To, join.Type);
});
}
else if (typeof(pJoin) === 'object')
{
addJoin(pJoin.Table, pJoin.From, pJoin.To, pJoin.Type);
}
return this;
};
/**
* Add a sort data element
*
* The passed values can be either a string, an object or an array of objects.
*
* The Sort object has two values:
* {Column:'Birthday', Direction:'Ascending'}
*
* @method setSort
* @param {String} pSort The sort criteria to add to the Query.
* @return {Object} Returns the current Query for chaining.
*/
var addSort = function(pSort)
{
var tmpSort = false;
if (typeof(pSort) === 'string')
{
// Default to ascending
tmpSort = {Column:pSort, Direction:'Ascending'};
}
if (typeof(pSort) === 'object')
{
// TODO: Check that this sort entry conforms to a sort entry
tmpSort = pSort;
}
if (!_Parameters.sort)
{
_Parameters.sort = [];
}
_Parameters.sort.push(tmpSort);
if (_LogLevel > 2)
{
_Fable.log.info('Sort set', {queryUUID:_UUID, parameters:_Parameters});
}
return this;
};
/**
* Set the the Begin index for the Query. *Begin* is the index at which
* a query should start returning rows. In TSQL this would be the n
* parameter of ```LIMIT 1,n```, whereas in MongoDB this would be the
* n in ```skip(n)```.
*
* The passed value must be an Integer >= 0.
*
* @method setBegin
* @param {Number} pBeginAmount The index to begin returning Query data.
* @return {Object} Returns the current Query for chaining.
*/
var setBegin = function(pBeginAmount)
{
var tmpBegin = false;
// Test if it is an integer > -1
// http://jsperf.com/numbers-and-integers
if (typeof(pBeginAmount) === 'number' && (pBeginAmount % 1) === 0 && pBeginAmount >= 0)
{
tmpBegin = pBeginAmount;
}
else if (pBeginAmount !== false)
{
_Fable.log.error('Begin set failed; non-positive or non-numeric argument.', {queryUUID:_UUID, parameters:_Parameters, invalidBeginAmount:pBeginAmount});
}
_Parameters.begin = tmpBegin;
if (_LogLevel > 2)
{
_Fable.log.info('Begin set: '+pBeginAmount, {queryUUID:_UUID, parameters:_Parameters});
}
return this;
};
/**
* Set the the Cap for the Query. *Cap* is the maximum number of records
* a Query should return in a set. In TSQL this would be the n
* parameter of ```LIMIT n```, whereas in MongoDB this would be the
* n in ```limit(n)```.
*
* The passed value must be an Integer >= 0.
*
* @method setCap
* @param {Number} pCapAmount The maximum records for the Query set.
* @return {Object} Returns the current Query for chaining.
*/
var setCap = function(pCapAmount)
{
var tmpCapAmount = false;
if (typeof(pCapAmount) === 'number' && (pCapAmount % 1) === 0 && pCapAmount >= 0)
{
tmpCapAmount = pCapAmount;
}
else if (pCapAmount !== false)
{
_Fable.log.error('Cap set failed; non-positive or non-numeric argument.', {queryUUID:_UUID, parameters:_Parameters, invalidCapAmount:pCapAmount});
}
_Parameters.cap = tmpCapAmount;
if (_LogLevel > 2)
{
_Fable.log.info('Cap set to: '+tmpCapAmount, {queryUUID:_UUID, parameters:_Parameters});
}
return this;
};
/**
* Set the filter expression
*
* The passed values can be either an object or an array of objects.
*
* The Filter object has a minimum of two values (which expands to the following):
* {Column:'Name', Value:'John'}
* {Column:'Name', Operator:'EQ', Value:'John', Connector:'And', Parameter:'Name'}
*
* @method setFilter
* @param {String} pFilter The filter(s) for the Query.
* @return {Object} Returns the current Query for chaining.
*/
var setFilter = function(pFilter)
{
var tmpFilter = false;
if (Array.isArray(pFilter))
{
// TODO: Check each entry of the array are all conformant Filter objects
tmpFilter = pFilter;
}
else if (typeof(pFilter) === 'object')
{
// TODO: Check that this Filter entry conforms to a Filter entry
tmpFilter = [pFilter];
}
_Parameters.filter = tmpFilter;
if (_LogLevel > 2)
{
_Fable.log.info('Filter set', {queryUUID:_UUID, parameters:_Parameters});
}
return this;
};
/**
* Add a filter expression
*
* {Column:'Name', Operator:'EQ', Value:'John', Connector:'And', Parameter:'Name'}
*
* @method addFilter
* @return {Object} Returns the current Query for chaining.
*/
var addFilter = function(pColumn, pValue, pOperator, pConnector, pParameter)
{
if (typeof(pColumn) !== 'string')
{
_Fable.log.warn('Tried to add an invalid query filter column', {queryUUID:_UUID, parameters:_Parameters});
return this;
}
if (typeof(pValue) === 'undefined')
{
_Fable.log.warn('Tried to add an invalid query filter value', {queryUUID:_UUID, parameters:_Parameters, invalidColumn:pColumn});
return this;
}
var tmpOperator = (typeof(pOperator) === 'undefined') ? '=' : pOperator;
var tmpConnector = (typeof(pConnector) === 'undefined') ? 'AND' : pConnector;
var tmpParameter = (typeof(pParameter) === 'undefined') ? pColumn : pParameter;
//support table.field notation (mysql2 requires this)
tmpParameter = tmpParameter.replace('.', '_');
var tmpFilter = (
{
Column: pColumn,
Operator: tmpOperator,
Value: pValue,
Connector: tmpConnector,
Parameter: tmpParameter
});
if (!Array.isArray(_Parameters.filter))
{
_Parameters.filter = [tmpFilter];
}
else
{
_Parameters.filter.push(tmpFilter);
}
if (_LogLevel > 2)
{
_Fable.log.info('Added a filter', {queryUUID:_UUID, parameters:_Parameters, newFilter:tmpFilter});
}
return this;
};
/**
* Add a join expression
*
* {Type:'INNER JOIN', Table:'Test', From:'Test.ID', To:'Scope.IDItem'}
*
* @method addJoin
* @return {Object} Returns the current Query for chaining.
*/
var addJoin = function(pTable, pFrom, pTo, pType)
{
if (typeof(pTable) !== 'string')
{
_Fable.log.warn('Tried to add an invalid query join table', {queryUUID:_UUID, parameters:_Parameters});
return this;
}
if (typeof(pFrom) === 'undefined' || typeof(pTo) === 'undefined')
{
_Fable.log.warn('Tried to add an invalid query join field', {queryUUID:_UUID, parameters:_Parameters});
return this;
}
//sanity check the join fields
if (pFrom.indexOf(pTable)!=0)
{
_Fable.log.warn('Tried to add an invalid query join field, join must come FROM the join table!', {queryUUID:_UUID, parameters:_Parameters, invalidField:pFrom});
return this;
}
if (pTo.indexOf('.')<=0)
{
_Fable.log.warn('Tried to add an invalid query join field, join must go TO a field on another table ([table].[field])!', {queryUUID:_UUID, parameters:_Parameters, invalidField:pTo});
return this;
}
var tmpType = (typeof(pType) === 'undefined') ? 'INNER JOIN' : pType;
var tmpJoin = (
{
Type: tmpType,
Table: pTable,
From: pFrom,
To: pTo
});
if (!Array.isArray(_Parameters.join))
{
_Parameters.join = [tmpJoin];
}
else
{
_Parameters.join.push(tmpJoin);
}
if (_LogLevel > 2)
{
_Fable.log.info('Added a join', {queryUUID:_UUID, parameters:_Parameters});
}
return this;
};
/**
* Add a record (for UPDATE and INSERT)
*
*
* @method addRecord
* @param {Object} pRecord The record to add.
* @return {Object} Returns the current Query for chaining.
*/
var addRecord = function(pRecord)
{
if (typeof(pRecord) !== 'object')
{
_Fable.log.warn('Tried to add an invalid record to the query -- records must be an object', {queryUUID:_UUID, parameters:_Parameters});
return this;
}
if (!Array.isArray(_Parameters.query.records))
{
_Parameters.query.records = [pRecord];
}
else
{
_Parameters.query.records.push(pRecord);
}
if (_LogLevel > 2)
{
_Fable.log.info('Added a record to the query', {queryUUID:_UUID, parameters:_Parameters, newRecord:pRecord});
}
return this;
};
/**
* Set the Dialect for Query generation.
*
* This function expects a string, case sensitive, which matches both the
* folder and filename
*
* @method setDialect
* @param {String} pDialectName The dialect for query generation.
* @return {Object} Returns the current Query for chaining.
*/
var setDialect = function(pDialectName)
{
if (typeof(pDialectName) !== 'string')
{
_Fable.log.warn('Dialect set to English - invalid name', {queryUUID:_UUID, parameters:_Parameters, invalidDialect:pDialectName});
return setDialect('English');
}
if (_Dialects.hasOwnProperty(pDialectName))
{
_Dialect = _Dialects[pDialectName](_Fable);
if (_LogLevel > 2)
{
_Fable.log.info('Dialog set to: '+pDialectName, {queryUUID:_UUID, parameters:_Parameters});
}
}
else
{
_Fable.log.error('Dialect not set - unknown dialect "'+pDialectName+"'", {queryUUID:_UUID, parameters:_Parameters, invalidDialect:pDialectName});
setDialect('English');
}
return this;
};
/**
* User to use for this query
*
* @method setIDUser
*/
var setIDUser = function(pIDUser)
{
var tmpUserID = 0;
if (typeof(pIDUser) === 'number' && (pIDUser % 1) === 0 && pIDUser >= 0)
{
tmpUserID = pIDUser;
}
else if (pIDUser !== false)
{
_Fable.log.error('User set failed; non-positive or non-numeric argument.', {queryUUID:_UUID, parameters:_Parameters, invalidIDUser:pIDUser});
}
_Parameters.userID = tmpUserID;
_Parameters.query.IDUser = tmpUserID;
if (_LogLevel > 2)
{
_Fable.log.info('IDUser set to: '+tmpUserID, {queryUUID:_UUID, parameters:_Parameters});
}
return this;
};
/**
* Flag to disable auto identity
*
* @method setDisableAutoIdentity
*/
var setDisableAutoIdentity = function(pFlag)
{
_Parameters.query.disableAutoIdentity = pFlag;
return this; //chainable
};
/**
* Flag to disable auto datestamp
*
* @method setDisableAutoDateStamp
*/
var setDisableAutoDateStamp = function(pFlag)
{
_Parameters.query.disableAutoDateStamp = pFlag;
return this; //chainable
};
/**
* Flag to disable auto userstamp
*
* @method setDisableAutoUserStamp
*/
var setDisableAutoUserStamp = function(pFlag)
{
_Parameters.query.disableAutoUserStamp = pFlag;
return this; //chainable
};
/**
* Flag to disable delete tracking
*
* @method setDisableDeleteTracking
*/
var setDisableDeleteTracking = function(pFlag)
{
_Parameters.query.disableDeleteTracking = pFlag;
return this; //chainable
};
/**
* Check that a valid Dialect has been set
*
* If there has not been a dialect set, it defaults to English.
* TODO: Have the json configuration define a "default" dialect.
*
* @method checkDialect
*/
var checkDialect = function()
{
if (_Dialect === false)
{
setDialect('English');
}
};
var buildCreateQuery = function()
{
checkDialect();
_Parameters.query.body = _Dialect.Create(_Parameters);
return this;
};
var buildReadQuery = function()
{
checkDialect();
_Parameters.query.body = _Dialect.Read(_Parameters);
return this;
};
var buildUpdateQuery = function()
{
checkDialect();
_Parameters.query.body = _Dialect.Update(_Parameters);
return this;
};
var buildDeleteQuery = function()
{
checkDialect();
_Parameters.query.body = _Dialect.Delete(_Parameters);
return this;
};
var buildUndeleteQuery = function()
{
checkDialect();
_Parameters.query.body = _Dialect.Undelete(_Parameters);
return this;
};
var buildCountQuery = function()
{
checkDialect();
_Parameters.query.body = _Dialect.Count(_Parameters);
return this;
};
/**
* Container Object for our Factory Pattern
*/
var tmpNewFoxHoundObject = (
{
resetParameters: resetParameters,
mergeParameters: mergeParameters,
setLogLevel: setLogLevel,
setScope: setScope,
setDistinct: setDistinct,
setIDUser: setIDUser,
setDataElements: setDataElements,
setBegin: setBegin,
setCap: setCap,
setFilter: setFilter,
addFilter: addFilter,
setSort: setSort,
addSort: addSort,
setJoin: setJoin,
addJoin: addJoin,
addRecord: addRecord,
setDisableAutoIdentity: setDisableAutoIdentity,
setDisableAutoDateStamp: setDisableAutoDateStamp,
setDisableAutoUserStamp: setDisableAutoUserStamp,
setDisableDeleteTracking: setDisableDeleteTracking,
setDialect: setDialect,
buildCreateQuery: buildCreateQuery,
buildReadQuery: buildReadQuery,
buildUpdateQuery: buildUpdateQuery,
buildDeleteQuery: buildDeleteQuery,
buildUndeleteQuery: buildUndeleteQuery,
buildCountQuery: buildCountQuery,
clone: clone,
new: createNew
});
/**
* Query
*
* @property query
* @type Object
*/
Object.defineProperty(tmpNewFoxHoundObject, 'query',
{
get: function() { return _Parameters.query; },
set: function(pQuery) { _Parameters.query = pQuery; },
enumerable: true
});
/**
* Query
*
* @property query
* @type Object
*/
Object.defineProperty(tmpNewFoxHoundObject, 'indexHints',
{
get: function() { return _Parameters.indexHints; },
set: function(pHints) { _Parameters.indexHints = pHints; },
enumerable: true,
});
/**
* Result
*
* @property result
* @type Object
*/
Object.defineProperty(tmpNewFoxHoundObject, 'result',
{
get: function() { return _Parameters.result; },
set: function(pResult) { _Parameters.result = pResult; },
enumerable: true
});
/**
* Query Parameters
*
* @property parameters
* @type Object
*/
Object.defineProperty(tmpNewFoxHoundObject, 'parameters',
{
get: function() { return _Parameters; },
set: function(pParameters) { _Parameters = pParameters; },
enumerable: true
});
/**
* Dialect
*
* @property dialect
* @type Object
*/
Object.defineProperty(tmpNewFoxHoundObject, 'dialect',
{
get: function() { return _Dialect; },
enumerable: true
});
/**
* Universally Unique Identifier
*
* @property uuid
* @type String
*/
Object.defineProperty(tmpNewFoxHoundObject, 'uuid',
{
get: function() { return _UUID; },
enumerable: true
});
/**
* Log Level
*
* @property logLevel
* @type Integer
*/
Object.defineProperty(tmpNewFoxHoundObject, 'logLevel',
{
get: function() { return _LogLevel; },
enumerable: true
});
return tmpNewFoxHoundObject;
}
return createNew();
};
module.exports = FoxHound();