UNPKG

forerunnerdb

Version:

A NoSQL document store database for browsers and Node.js.

397 lines (344 loc) 11.8 kB
"use strict"; var Matching = { /** * Internal method that checks a document against a test object. * @param {*} source The source object or value to test against. * @param {*} test The test object or value to test with. * @param {Object} queryOptions The options the query was passed with. * @param {String=} opToApply The special operation to apply to the test such * as 'and' or an 'or' operator. * @param {Object=} options An object containing options to apply to the * operation such as limiting the fields returned etc. * @returns {Boolean} True if the test was positive, false on negative. * @private */ _match: function (source, test, queryOptions, opToApply, options) { // TODO: This method is quite long, break into smaller pieces var operation, applyOp = opToApply, recurseVal, tmpIndex, sourceType = typeof source, testType = typeof test, matchedAll = true, opResult, substringCache, i; options = options || {}; queryOptions = queryOptions || {}; // Check if options currently holds a root query object if (!options.$rootQuery) { // Root query not assigned, hold the root query options.$rootQuery = test; } options.$rootData = options.$rootData || {}; // Check if the comparison data are both strings or numbers if ((sourceType === 'string' || sourceType === 'number') && (testType === 'string' || testType === 'number')) { // The source and test data are flat types that do not require recursive searches, // so just compare them and return the result if (sourceType === 'number') { // Number comparison if (source !== test) { matchedAll = false; } } else { // String comparison // TODO: We can probably use a queryOptions.$locale as a second parameter here // TODO: to satisfy https://github.com/Irrelon/ForerunnerDB/issues/35 if (source.localeCompare(test)) { matchedAll = false; } } } else { for (i in test) { if (test.hasOwnProperty(i)) { // Reset operation flag operation = false; substringCache = i.substr(0, 2); // Check if the property is a comment (ignorable) if (substringCache === '//') { // Skip this property continue; } // Check if the property starts with a dollar (function) if (substringCache.indexOf('$') === 0) { // Ask the _matchOp method to handle the operation opResult = this._matchOp(i, source, test[i], queryOptions, options); // Check the result of the matchOp operation // If the result is -1 then no operation took place, otherwise the result // will be a boolean denoting a match (true) or no match (false) if (opResult > -1) { if (opResult) { if (opToApply === 'or') { return true; } } else { // Set the matchedAll flag to the result of the operation // because the operation did not return true matchedAll = opResult; } // Record that an operation was handled operation = true; } } // Check for regex if (!operation && test[i] instanceof RegExp) { operation = true; if (sourceType === 'object' && source[i] !== undefined && test[i].test(source[i])) { if (opToApply === 'or') { return true; } } else { matchedAll = false; } } if (!operation) { // Check if our query is an object if (typeof(test[i]) === 'object') { // Because test[i] is an object, source must also be an object // Check if our source data we are checking the test query against // is an object or an array if (source[i] !== undefined) { if (source[i] instanceof Array && !(test[i] instanceof Array)) { // The source data is an array, so check each item until a // match is found recurseVal = false; for (tmpIndex = 0; tmpIndex < source[i].length; tmpIndex++) { recurseVal = this._match(source[i][tmpIndex], test[i], queryOptions, applyOp, options); if (recurseVal) { // One of the array items matched the query so we can // include this item in the results, so break now break; } } if (recurseVal) { if (opToApply === 'or') { return true; } } else { matchedAll = false; } } else if (!(source[i] instanceof Array) && test[i] instanceof Array) { // The test key data is an array and the source key data is not so check // each item in the test key data to see if the source item matches one // of them. This is effectively an $in search. recurseVal = false; for (tmpIndex = 0; tmpIndex < test[i].length; tmpIndex++) { recurseVal = this._match(source[i], test[i][tmpIndex], queryOptions, applyOp, options); if (recurseVal) { // One of the array items matched the query so we can // include this item in the results, so break now break; } } if (recurseVal) { if (opToApply === 'or') { return true; } } else { matchedAll = false; } } else if (typeof(source) === 'object') { // Recurse down the object tree recurseVal = this._match(source[i], test[i], queryOptions, applyOp, options); if (recurseVal) { if (opToApply === 'or') { return true; } } else { matchedAll = false; } } else { recurseVal = this._match(undefined, test[i], queryOptions, applyOp, options); if (recurseVal) { if (opToApply === 'or') { return true; } } else { matchedAll = false; } } } else { // First check if the test match is an $exists if (test[i] && test[i].$exists !== undefined) { // Push the item through another match recurse recurseVal = this._match(undefined, test[i], queryOptions, applyOp, options); if (recurseVal) { if (opToApply === 'or') { return true; } } else { matchedAll = false; } } else { matchedAll = false; } } } else { // Check if the prop matches our test value if (source && source[i] === test[i]) { if (opToApply === 'or') { return true; } } else if (source && source[i] && source[i] instanceof Array && test[i] && typeof(test[i]) !== "object") { // We are looking for a value inside an array // The source data is an array, so check each item until a // match is found recurseVal = false; for (tmpIndex = 0; tmpIndex < source[i].length; tmpIndex++) { recurseVal = this._match(source[i][tmpIndex], test[i], queryOptions, applyOp, options); if (recurseVal) { // One of the array items matched the query so we can // include this item in the results, so break now break; } } if (recurseVal) { if (opToApply === 'or') { return true; } } else { matchedAll = false; } } else { matchedAll = false; } } } if (opToApply === 'and' && !matchedAll) { return false; } } } } return matchedAll; }, /** * Internal method, performs a matching process against a query operator such as $gt or $nin. * @param {String} key The property name in the test that matches the operator to perform * matching against. * @param {*} source The source data to match the query against. * @param {*} test The query to match the source against. * @param {Object=} options An options object. * @returns {*} * @private */ _matchOp: function (key, source, test, queryOptions, options) { // Check for commands switch (key) { case '$gt': // Greater than return source > test; case '$gte': // Greater than or equal return source >= test; case '$lt': // Less than return source < test; case '$lte': // Less than or equal return source <= test; case '$exists': // Property exists return (source === undefined) !== test; case '$ne': // Not equals return source != test; // jshint ignore:line case '$nee': // Not equals equals return source !== test; case '$or': // Match true on ANY check to pass for (var orIndex = 0; orIndex < test.length; orIndex++) { if (this._match(source, test[orIndex], queryOptions, 'and', options)) { return true; } } return false; case '$and': // Match true on ALL checks to pass for (var andIndex = 0; andIndex < test.length; andIndex++) { if (!this._match(source, test[andIndex], queryOptions, 'and', options)) { return false; } } return true; case '$in': // In // Check that the in test is an array if (test instanceof Array) { var inArr = test, inArrCount = inArr.length, inArrIndex; for (inArrIndex = 0; inArrIndex < inArrCount; inArrIndex++) { if (inArr[inArrIndex] instanceof RegExp && inArr[inArrIndex].test(source)) { return true; } else if (inArr[inArrIndex] === source) { return true; } } return false; } else { throw(this.logIdentifier() + ' Cannot use an $in operator on a non-array key: ' + key); } break; case '$nin': // Not in // Check that the not-in test is an array if (test instanceof Array) { var notInArr = test, notInArrCount = notInArr.length, notInArrIndex; for (notInArrIndex = 0; notInArrIndex < notInArrCount; notInArrIndex++) { if (notInArr[notInArrIndex] === source) { return false; } } return true; } else { throw(this.logIdentifier() + ' Cannot use a $nin operator on a non-array key: ' + key); } break; case '$distinct': // Ensure options holds a distinct lookup options.$rootData['//distinctLookup'] = options.$rootData['//distinctLookup'] || {}; for (var distinctProp in test) { if (test.hasOwnProperty(distinctProp)) { options.$rootData['//distinctLookup'][distinctProp] = options.$rootData['//distinctLookup'][distinctProp] || {}; // Check if the options distinct lookup has this field's value if (options.$rootData['//distinctLookup'][distinctProp][source[distinctProp]]) { // Value is already in use return false; } else { // Set the value in the lookup options.$rootData['//distinctLookup'][distinctProp][source[distinctProp]] = true; // Allow the item in the results return true; } } } break; case '$count': var countKey, countArr, countVal; // Iterate the count object's keys for (countKey in test) { if (test.hasOwnProperty(countKey)) { // Check the property exists and is an array. If the property being counted is not // an array (or doesn't exist) then use a value of zero in any further count logic countArr = source[countKey]; if (typeof countArr === 'object' && countArr instanceof Array) { countVal = countArr.length; } else { countVal = 0; } // Now recurse down the query chain further to satisfy the query for this key (countKey) if (!this._match(countVal, test[countKey], queryOptions, 'and', options)) { return false; } } } // Allow the item in the results return true; } return -1; } }; module.exports = Matching;