UNPKG

foxhound

Version:

A Database Query generation library.

1,333 lines (1,268 loc) 121 kB
(function (f) { if (typeof exports === "object" && typeof module !== "undefined") { module.exports = f(); } else if (typeof define === "function" && define.amd) { define([], f); } else { var g; if (typeof window !== "undefined") { g = window; } else if (typeof global !== "undefined") { g = global; } else if (typeof self !== "undefined") { g = self; } else { g = this; } g.Foxhound = f(); } })(function () { var define, module, exports; return function () { function r(e, n, t) { function o(i, f) { if (!n[i]) { if (!e[i]) { var c = "function" == typeof require && require; if (!f && c) return c(i, !0); if (u) return u(i, !0); var a = new Error("Cannot find module '" + i + "'"); throw a.code = "MODULE_NOT_FOUND", a; } var p = n[i] = { exports: {} }; e[i][0].call(p.exports, function (r) { var n = e[i][1][r]; return o(n || r); }, p, p.exports, r, e, n, t); } return n[i].exports; } for (var u = "function" == typeof require && require, i = 0; i < t.length; i++) o(t[i]); return o; } return r; }()({ 1: [function (require, module, exports) { /** * Simple browser shim loader - assign the npm module to a window global automatically * * @license MIT * @author <steven@velozo.com> */ var libNPMModuleWrapper = require('./Foxhound.js'); if (typeof window === 'object' && !window.hasOwnProperty('Foxhound')) { window.Foxhound = libNPMModuleWrapper; } module.exports = libNPMModuleWrapper; }, { "./Foxhound.js": 3 }], 2: [function (require, module, exports) { getDialects = () => { let tmpDialects = {}; tmpDialects.ALASQL = require('./dialects/ALASQL/FoxHound-Dialect-ALASQL.js'); tmpDialects.English = require('./dialects/English/FoxHound-Dialect-English.js'); tmpDialects.MeadowEndpoints = require('./dialects/MeadowEndpoints/FoxHound-Dialect-MeadowEndpoints.js'); tmpDialects.MySQL = require('./dialects/MySQL/FoxHound-Dialect-MySQL.js'); tmpDialects.default = tmpDialects.English; return tmpDialects; }; module.exports = getDialects(); }, { "./dialects/ALASQL/FoxHound-Dialect-ALASQL.js": 5, "./dialects/English/FoxHound-Dialect-English.js": 6, "./dialects/MeadowEndpoints/FoxHound-Dialect-MeadowEndpoints.js": 7, "./dialects/MySQL/FoxHound-Dialect-MySQL.js": 8 }], 3: [function (require, module, exports) { /** * 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 }); /** * 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(); }, { "./Foxhound-Dialects.js": 2, "./Parameters.js": 4 }], 4: [function (require, module, exports) { /** * Query Parameters Object * * @class FoxHoundQueryParameters * @constructor */ var FoxHoundQueryParameters = { scope: false, // STR: The scope of the data // TSQL: the "Table" or "View" // MongoDB: the "Collection" dataElements: false, // ARR of STR: The data elements to return // TSQL: the "Columns" // MongoDB: the "Fields" begin: false, // INT: Record index to start at // TSQL: n in LIMIT 1,n // MongoDB: n in Skip(n) cap: false, // INT: Maximum number of records to return // TSQL: n in LIMIT n // MongoDB: n in limit(n) // Serialization example for a query: // Take the filter and return an array of filter instructions // Basic instruction anatomy: // INSTRUCTION~FIELD~OPERATOR~VALUE // FOP - Filter Open Paren // FOP~~(~ // FCP - Filter Close Paren // FCP~~)~ // FBV - Filter By Value // FBV~Category~EQ~Books // Possible comparisons: // * EQ - Equals To (=) // * NE - Not Equals To (!=) // * GT - Greater Than (>) // * GE - Greater Than or Equals To (>=) // * LT - Less Than (<) // * LE - Less Than or Equals To (<=) // * LK - Like (Like) // FBL - Filter By List (value list, separated by commas) // FBL~Category~EQ~Books,Movies // FSF - Filter Sort Field // FSF~Category~ASC~0 // FSF~Category~DESC~0 // FCC - Filter Constraint Cap (the limit of what is returned) // FCC~~10~ // FCB - Filter Constraint Begin (the zero-based start index of what is returned) // FCB~~10~ // // This means: FBV~Category~EQ~Books~FBV~PublishedYear~GT~2000~FSF~PublishedYear~DESC~0 // Filters down to ALL BOOKS PUBLISHED AFTER 2000 IN DESCENDING ORDER filter: false, // ARR of OBJ: Data filter expression list {Column:'Name', Operator:'EQ', Value:'John', Connector:'And', Parameter:'Name'} // TSQL: the WHERE clause // MongoDB: a find() expression sort: false, // ARR of OBJ: The sort order {Column:'Birthday', Direction:'Ascending'} // TSQL: ORDER BY // MongoDB: sort() join: false, // ARR of OBJ: The join tables {Type:'INNER JOIN', Table:'test', From: 'Test.ID', To: 'Scope.IDItem' } // TSQL: JOIN // Force a specific query to run regardless of above ... this is used to override the query generator. queryOverride: false, // Where the generated query goes query: false, /* { body: false, schema: false, // The schema to intersect with our records IDUser: 0, // The User ID to stamp into records UUID: A_UUID, // Some globally unique record id, different per cloned query. records: false, // The records to be created or changed parameters: {} } */ // Who is making the query userID: 0, // Where the query results are stuck result: false /* { executed: false, // True once we've run a query. value: false, // The return value of the last query run error: false // The error message of the last run query } */ }; module.exports = FoxHoundQueryParameters; }, {}], 5: [function (require, module, exports) { /** * FoxHound ALASQL Dialect * * @license MIT * * For an ALASQL query override: // An underscore template with the following values: // <%= DataElements %> = Field1, Field2, Field3, Field4 // <%= Begin %> = 0 // <%= Cap %> = 10 // <%= Filter %> = WHERE StartDate > :MyStartDate // <%= Sort %> = ORDER BY Field1 // The values are empty strings if they aren't set. * * @author Steven Velozo <steven@velozo.com> * @class FoxHoundDialectALASQL */ var FoxHoundDialectALASQL = function (pFable) { //Request time from SQL server with microseconds resolution const SQL_NOW = "NOW(3)"; _Fable = pFable; /** * Generate a table name from the scope. * * Because ALASQL is all in-memory, and can be run in two modes (anonymous * working on arrays or table-based) we are going to make this a programmable * value. Then we can share the code across both providers. * * @method: generateTableName * @param: {Object} pParameters SQL Query Parameters * @return: {String} Returns the table name clause */ var generateTableName = function (pParameters) { return ' ' + pParameters.scope; }; /** * Escape columns, because ALASQL has more reserved KWs than most SQL dialects */ var escapeColumn = (pColumn, pParameters) => { if (pColumn.indexOf('.') < 0) { return '`' + pColumn + '`'; } else { // This could suck if the scope is not the same var tmpTableName = pParameters.scope; if (pColumn.indexOf(tmpTableName + '.') > -1) { return '`' + pColumn.replace(tmpTableName + '.', '') + '`'; } else { // This doesn't work well but we'll try it. return '`' + pColumn + '`'; } } }; /** * Generate a field list from the array of dataElements * * Each entry in the dataElements is a simple string * * @method: generateFieldList * @param: {Object} pParameters SQL Query Parameters * @param {Boolean} pIsForCountClause (optional) If true, generate fields for use within a count clause. * @return: {String} Returns the field list clause, or empty string if explicit fields are requested but cannot be fulfilled * due to missing schema. */ var generateFieldList = function (pParameters, pIsForCountClause) { var tmpDataElements = pParameters.dataElements; if (!Array.isArray(tmpDataElements) || tmpDataElements.length < 1) { if (!pIsForCountClause) { return ' *'; } // we need to list all of the table fields explicitly; get them from the schema const tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : []; if (tmpSchema.length < 1) { // this means we have no schema; returning an empty string here signals the calling code to handle this case return ''; } const idColumn = tmpSchema.find(entry => entry.Type === 'AutoIdentity'); if (!idColumn) { // this means there is no autoincrementing unique ID column; treat as above return ''; } return ` ${idColumn.Column}`; } var tmpFieldList = ' '; for (var i = 0; i < tmpDataElements.length; i++) { if (i > 0) { tmpFieldList += ', '; } tmpFieldList += escapeColumn(tmpDataElements[i], pParameters); } return tmpFieldList; }; /** * Generate a query from the array of where clauses * * Each clause is an object like: { Column:'Name', Operator:'EQ', Value:'John', Connector:'And', Parameter:'Name' } * * @method: generateWhere * @param: {Object} pParameters SQL Query Parameters * @return: {String} Returns the WHERE clause prefixed with WHERE, or an empty string if unnecessary */ var generateWhere = function (pParameters) { var tmpFilter = Array.isArray(pParameters.filter) ? pParameters.filter : []; var tmpTableName = generateTableName(pParameters).trim(); if (!pParameters.query.disableDeleteTracking) { // Check if there is a Deleted column on the Schema. If so, we add this to the filters automatically (if not already present) var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : []; for (var i = 0; i < tmpSchema.length; i++) { // There is a schema entry for it. Process it accordingly. var tmpSchemaEntry = tmpSchema[i]; if (tmpSchemaEntry.Type === 'Deleted') { var tmpHasDeletedParameter = false; //first, check to see if filters are already looking for Deleted column if (tmpFilter.length > 0) { for (var x = 0; x < tmpFilter.length; x++) { if (tmpFilter[x].Column === tmpSchemaEntry.Column) { tmpHasDeletedParameter = true; break; } } } if (!tmpHasDeletedParameter) { //if not, we need to add it tmpFilter.push({ Column: tmpTableName + '.' + tmpSchemaEntry.Column, Operator: '=', Value: 0, Connector: 'AND', Parameter: 'Deleted' }); } break; } } } if (tmpFilter.length < 1) { return ''; } var tmpWhere = ' WHERE'; // This is used to disable the connectors for subsequent queries. // Only the open parenthesis operator uses this, currently. var tmpLastOperatorNoConnector = false; for (var i = 0; i < tmpFilter.length; i++) { if (tmpFilter[i].Connector != 'NONE' && tmpFilter[i].Operator != ')' && tmpWhere != ' WHERE' && tmpLastOperatorNoConnector == false) { tmpWhere += ' ' + tmpFilter[i].Connector; } tmpLastOperatorNoConnector = false; var tmpColumnParameter; if (tmpFilter[i].Operator === '(') { // Open a logical grouping tmpWhere += ' ('; tmpLastOperatorNoConnector = true; } else if (tmpFilter[i].Operator === ')') { // Close a logical grouping tmpWhere += ' )'; } else if (tmpFilter[i].Operator === 'IN') { tmpColumnParameter = tmpFilter[i].Parameter + '_w' + i; // Add the column name, operator and parameter name to the list of where value parenthetical tmpWhere += ' ' + escapeColumn(tmpFilter[i].Column, pParameters) + ' ' + tmpFilter[i].Operator + ' ( :' + tmpColumnParameter + ' )'; pParameters.query.parameters[tmpColumnParameter] = tmpFilter[i].Value; } else if (tmpFilter[i].Operator === 'IS NOT NULL') { // IS NOT NULL is a special operator which doesn't require a value, or parameter tmpWhere += ' ' + escapeColumn(tmpFilter[i].Column, pParameters) + ' ' + tmpFilter[i].Operator; } else { tmpColumnParameter = tmpFilter[i].Parameter + '_w' + i; // Add the column name, operator and parameter name to the list of where value parenthetical tmpWhere += ' ' + escapeColumn(tmpFilter[i].Column, pParameters) + ' ' + tmpFilter[i].Operator + ' :' + tmpColumnParameter; pParameters.query.parameters[tmpColumnParameter] = tmpFilter[i].Value; } } return tmpWhere; }; /** * Generate an ORDER BY clause from the sort array * * Each entry in the sort is an object like: * {Column:'Color',Direction:'Descending'} * * @method: generateOrderBy * @param: {Object} pParameters SQL Query Parameters * @return: {String} Returns the field list clause */ var generateOrderBy = function (pParameters) { var tmpOrderBy = pParameters.sort; if (!Array.isArray(tmpOrderBy) || tmpOrderBy.length < 1) { return ''; } var tmpOrderClause = ' ORDER BY'; for (var i = 0; i < tmpOrderBy.length; i++) { if (i > 0) { tmpOrderClause += ','; } tmpOrderClause += ' ' + escapeColumn(tmpOrderBy[i].Column, pParameters); if (tmpOrderBy[i].Direction == 'Descending') { tmpOrderClause += ' DESC'; } } return tmpOrderClause; }; /** * Generate the limit clause * * @method: generateLimit * @param: {Object} pParameters SQL Query Parameters * @return: {String} Returns the table name clause */ var generateLimit = function (pParameters) { if (!pParameters.cap) { return ''; } var tmpLimit = ' LIMIT'; // Cap is required for a limit clause. tmpLimit += ' ' + pParameters.cap; // If there is a begin record, we'll pass that in as well. if (pParameters.begin !== false) { tmpLimit += ' FETCH ' + pParameters.begin; } return tmpLimit; }; /** * Generate the update SET clause * * @method: generateUpdateSetters * @param: {Object} pParameters SQL Query Parameters * @return: {String} Returns the table name clause */ var generateUpdateSetters = function (pParameters) { var tmpRecords = pParameters.query.records; // We need to tell the query not to generate improperly if there are no values to set. if (!Array.isArray(tmpRecords) || tmpRecords.length < 1) { return false; } // Check if there is a schema. If so, we will use it to decide if these are parameterized or not. var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : []; var tmpUpdate = ''; // If there is more than one record in records, we are going to ignore them for now. var tmpCurrentColumn = 0; for (var tmpColumn in tmpRecords[0]) { // No hash table yet, so, we will just linear search it for now. // This uses the schema to decide if we want to treat a column differently on insert var tmpSchemaEntry = { Column: tmpColumn, Type: 'Default' }; for (var i = 0; i < tmpSchema.length; i++) { if (tmpColumn == tmpSchema[i].Column) { // There is a schema entry for it. Process it accordingly. tmpSchemaEntry = tmpSchema[i]; break; } } if (pParameters.query.disableAutoDateStamp && tmpSchemaEntry.Type === 'UpdateDate') { // This is ignored if flag is set continue; } if (pParameters.query.disableAutoUserStamp && tmpSchemaEntry.Type === 'UpdateIDUser') { // This is ignored if flag is set continue; } switch (tmpSchemaEntry.Type) { case 'AutoIdentity': case 'CreateDate': case 'CreateIDUser': case 'DeleteDate': case 'DeleteIDUser': // These are all ignored on update continue; } if (tmpCurrentColumn > 0) { tmpUpdate += ','; } switch (tmpSchemaEntry.Type) { case 'UpdateDate': // This is an autoidentity, so we don't parameterize it and just pass in NULL tmpUpdate += ' ' + escapeColumn(tmpColumn, pParameters) + ' = NOW()'; break; case 'UpdateIDUser': // This is the user ID, which we hope is in the query. // This is how to deal with a normal column var tmpColumnParameter = tmpColumn + '_' + tmpCurrentColumn; tmpUpdate += ' ' + escapeColumn(tmpColumn, pParameters) + ' = :' + tmpColumnParameter; // Set the query parameter pParameters.query.parameters[tmpColumnParameter] = pParameters.query.IDUser; break; default: var tmpColumnDefaultParameter = tmpColumn + '_' + tmpCurrentColumn; tmpUpdate += ' ' + escapeColumn(tmpColumn, pParameters) + ' = :' + tmpColumnDefaultParameter; // Set the query parameter pParameters.query.parameters[tmpColumnDefaultParameter] = tmpRecords[0][tmpColumn]; b