forerunnerdb
Version:
A NoSQL document store database for browsers and Node.js.
397 lines (344 loc) • 11.8 kB
JavaScript
"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;