forerunnerdb
Version:
A NoSQL document store database for browsers and Node.js.
827 lines (701 loc) • 27.5 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: Mixin.Matching.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: Mixin.Matching.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>"use strict";
/**
* Provides object matching algorithm methods.
* @mixin
*/
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;
if (sourceType === 'object' && source === null) {
sourceType = 'null';
}
if (testType === 'object' && test === null) {
testType = 'null';
}
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;
}
// Check if options currently holds a root source object
if (!options.$rootSource) {
// Root query not assigned, hold the root query
options.$rootSource = source;
}
// Assign current query data
options.$currentQuery = test;
options.$rootData = options.$rootData || {};
// Check if the comparison data are both strings or numbers
if ((sourceType === 'string' || sourceType === 'number' || sourceType === 'null') && (testType === 'string' || testType === 'number' || testType === 'null')) {
// 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' || sourceType === 'null' || testType === 'null') {
// Number or null 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 if ((sourceType === 'string' || sourceType === 'number') && (testType === 'object' && test instanceof RegExp)) {
if (!test.test(source)) {
matchedAll = false;
}
} else {
for (i in test) {
if (test.hasOwnProperty(i)) {
// Assign previous query data
options.$previousQuery = options.$parent;
// Assign parent query data
options.$parent = {
query: test[i],
key: i,
parent: options.$previousQuery
};
// Reset operation flag
operation = false;
// Grab first two chars of the key name to check for $
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} queryOptions The options the query was passed with.
* @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 '$eq': // Equals
return source == test; // jshint ignore:line
case '$eeq': // Equals equals
return source === test;
case '$ne': // Not equals
return source != test; // jshint ignore:line
case '$nee': // Not equals equals
return source !== test;
case '$not': // Not operator
return !this._match(source, test, queryOptions, 'and', options);
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 (this._match(source, inArr[inArrIndex], queryOptions, 'and', options)) {
return true;
}
}
return false;
} else if (typeof test === 'object') {
return this._match(source, test, queryOptions, 'and', options);
} else {
console.log(this.logIdentifier() + ' Cannot use an $in operator on a non-array key: ' + key, options.$rootQuery);
return false;
}
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 (this._match(source, notInArr[notInArrIndex], queryOptions, 'and', options)) {
return false;
}
}
return true;
} else if (typeof test === 'object') {
return this._match(source, test, queryOptions, 'and', options);
} else {
console.log(this.logIdentifier() + ' Cannot use a $nin operator on a non-array key: ' + key, options.$rootQuery);
return false;
}
break;
case '$fastIn':
if (test instanceof Array) {
// Source is a string or number, use indexOf to identify match in array
return test.indexOf(source) !== -1;
} else {
console.log(this.logIdentifier() + ' Cannot use an $fastIn operator on a non-array key: ' + key, options.$rootQuery);
return false;
}
break;
case '$distinct':
var lookupPath,
value,
finalDistinctProp;
// Ensure options holds a distinct lookup
options.$rootData['//distinctLookup'] = options.$rootData['//distinctLookup'] || {};
for (var distinctProp in test) {
if (test.hasOwnProperty(distinctProp)) {
if (typeof test[distinctProp] === 'object') {
// Get the path string from the object
lookupPath = this.sharedPathSolver.parse(test)[0].path;
// Use the path string to find the lookup value from the source data
value = this.sharedPathSolver.get(source, lookupPath);
finalDistinctProp = lookupPath;
} else {
value = source[distinctProp];
finalDistinctProp = distinctProp;
}
options.$rootData['//distinctLookup'][finalDistinctProp] = options.$rootData['//distinctLookup'][finalDistinctProp] || {};
// Check if the options distinct lookup has this field's value
if (options.$rootData['//distinctLookup'][finalDistinctProp][value]) {
// Value is already in use
return false;
} else {
// Set the value in the lookup
options.$rootData['//distinctLookup'][finalDistinctProp][value] = 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;
case '$find':
case '$findOne':
case '$findSub':
var fromType = 'collection',
findQuery,
findOptions,
subQuery,
subOptions,
subPath,
result,
operation = {};
// Check all parts of the $find operation exist
if (!test.$from) {
throw(key + ' missing $from property!');
}
if (test.$fromType) {
fromType = test.$fromType;
// Check the fromType exists as a method
if (!this.db()[fromType] || typeof this.db()[fromType] !== 'function') {
throw(key + ' cannot operate against $fromType "' + fromType + '" because the database does not recognise this type of object!');
}
}
// Perform the find operation
findQuery = test.$query || {};
findOptions = test.$options || {};
if (key === '$findSub') {
if (!test.$path) {
throw(key + ' missing $path property!');
}
subPath = test.$path;
subQuery = test.$subQuery || {};
subOptions = test.$subOptions || {};
if (options.$parent && options.$parent.parent && options.$parent.parent.key) {
result = this.db()[fromType](test.$from).findSub(findQuery, subPath, subQuery, subOptions);
} else {
// This is a root $find* query
// Test the source against the main findQuery
if (this._match(source, findQuery, {}, 'and', options)) {
result = this._findSub([source], subPath, subQuery, subOptions);
}
return result && result.length > 0;
}
} else {
result = this.db()[fromType](test.$from)[key.substr(1)](findQuery, findOptions);
}
operation[options.$parent.parent.key] = result;
return this._match(source, operation, queryOptions, 'and', options);
}
return -1;
},
/**
* Performs a join operation and returns the final joined data.
* @param {Array | Object} docArr An array of objects to run the join
* operation against or a single object.
* @param {Array} joinClause The join clause object array (the array in
* the $join key of a normal join options object).
* @param {Object} joinSource An object containing join source reference
* data or a blank object if you are doing a bespoke join operation.
* @param {Object} options An options object or blank object if no options.
* @returns {Array}
* @private
*/
applyJoin: function (docArr, joinClause, joinSource, options) {
var self = this,
joinSourceIndex,
joinSourceKey,
joinMatch,
joinSourceType,
joinSourceIdentifier,
resultKeyName,
joinSourceInstance,
resultIndex,
joinSearchQuery,
joinMulti,
joinRequire,
joinPrefix,
joinMatchIndex,
joinMatchData,
joinSearchOptions,
joinFindResults,
joinFindResult,
joinItem,
resultRemove = [],
l;
if (!(docArr instanceof Array)) {
// Turn the document into an array
docArr = [docArr];
}
for (joinSourceIndex = 0; joinSourceIndex < joinClause.length; joinSourceIndex++) {
for (joinSourceKey in joinClause[joinSourceIndex]) {
if (joinClause[joinSourceIndex].hasOwnProperty(joinSourceKey)) {
// Get the match data for the join
joinMatch = joinClause[joinSourceIndex][joinSourceKey];
// Check if the join is to a collection (default) or a specified source type
// e.g 'view' or 'collection'
joinSourceType = joinMatch.$sourceType || 'collection';
joinSourceIdentifier = '$' + joinSourceType + '.' + joinSourceKey;
// Set the key to store the join result in to the collection name by default
// can be overridden by the '$as' clause in the join object
resultKeyName = joinSourceKey;
// Get the join collection instance from the DB
if (joinSource[joinSourceIdentifier]) {
// We have a joinSource for this identifier already (given to us by
// an index when we analysed the query earlier on) and we can use
// that source instead.
joinSourceInstance = joinSource[joinSourceIdentifier];
} else {
// We do not already have a joinSource so grab the instance from the db
if (this._db[joinSourceType] && typeof this._db[joinSourceType] === 'function') {
joinSourceInstance = this._db[joinSourceType](joinSourceKey);
}
}
// Loop our result data array
for (resultIndex = 0; resultIndex < docArr.length; resultIndex++) {
// Loop the join conditions and build a search object from them
joinSearchQuery = {};
joinMulti = false;
joinRequire = false;
joinPrefix = '';
for (joinMatchIndex in joinMatch) {
if (joinMatch.hasOwnProperty(joinMatchIndex)) {
joinMatchData = joinMatch[joinMatchIndex];
// Check the join condition name for a special command operator
if (joinMatchIndex.substr(0, 1) === '$') {
// Special command
switch (joinMatchIndex) {
case '$where':
if (joinMatchData.$query || joinMatchData.$options) {
if (joinMatchData.$query) {
// Commented old code here, new one does dynamic reverse lookups
//joinSearchQuery = joinMatchData.query;
joinSearchQuery = self.resolveDynamicQuery(joinMatchData.$query, docArr[resultIndex]);
}
if (joinMatchData.$options) {
joinSearchOptions = joinMatchData.$options;
}
} else {
throw('$join $where clause requires "$query" and / or "$options" keys to work!');
}
break;
case '$as':
// Rename the collection when stored in the result document
resultKeyName = joinMatchData;
break;
case '$multi':
// Return an array of documents instead of a single matching document
joinMulti = joinMatchData;
break;
case '$require':
// Remove the result item if no matching join data is found
joinRequire = joinMatchData;
break;
case '$prefix':
// Add a prefix to properties mixed in
joinPrefix = joinMatchData;
break;
default:
break;
}
} else {
// Get the data to match against and store in the search object
// Resolve complex referenced query
joinSearchQuery[joinMatchIndex] = self.resolveDynamicQuery(joinMatchData, docArr[resultIndex]);
}
}
}
// Do a find on the target collection against the match data
joinFindResults = joinSourceInstance.find(joinSearchQuery, joinSearchOptions);
// Check if we require a joined row to allow the result item
if (!joinRequire || (joinRequire && joinFindResults[0])) {
// Join is not required or condition is met
if (resultKeyName === '$root') {
// The property name to store the join results in is $root
// which means we need to mixin the results but this only
// works if joinMulti is disabled
if (joinMulti !== false) {
// Throw an exception here as this join is not physically possible!
throw(this.logIdentifier() + ' Cannot combine [$as: "$root"] with [$multi: true] in $join clause!');
}
// Mixin the result
joinFindResult = joinFindResults[0];
joinItem = docArr[resultIndex];
for (l in joinFindResult) {
if (joinFindResult.hasOwnProperty(l) && joinItem[joinPrefix + l] === undefined) {
// Properties are only mixed in if they do not already exist
// in the target item (are undefined). Using a prefix denoted via
// $prefix is a good way to prevent property name conflicts
joinItem[joinPrefix + l] = joinFindResult[l];
}
}
} else {
docArr[resultIndex][resultKeyName] = joinMulti === false ? joinFindResults[0] : joinFindResults;
}
} else {
// Join required but condition not met, add item to removal queue
resultRemove.push(resultIndex);
}
}
}
}
}
return resultRemove;
},
/**
* Takes a query object with dynamic references and converts the references
* into actual values from the references source.
* @param {Object} query The query object with dynamic references.
* @param {Object} item The document to apply the references to.
* @returns {*}
* @private
*/
resolveDynamicQuery: function (query, item) {
var self = this,
newQuery,
propType,
propVal,
pathResult,
i;
// Check for early exit conditions
if (typeof query === 'string') {
// Check if the property name starts with a back-reference
if (query.substr(0, 3) === '$$.') {
// Fill the query with a back-referenced value
pathResult = this.sharedPathSolver.value(item, query.substr(3, query.length - 3));
} else {
pathResult = this.sharedPathSolver.value(item, query);
}
if (pathResult.length > 1) {
return {$in: pathResult};
} else {
return pathResult[0];
}
}
newQuery = {};
for (i in query) {
if (query.hasOwnProperty(i)) {
propType = typeof query[i];
propVal = query[i];
switch (propType) {
case 'string':
// Check if the property name starts with a back-reference
if (propVal.substr(0, 3) === '$$.') {
// Fill the query with a back-referenced value
newQuery[i] = this.sharedPathSolver.value(item, propVal.substr(3, propVal.length - 3))[0];
} else {
newQuery[i] = propVal;
}
break;
case 'object':
newQuery[i] = self.resolveDynamicQuery(propVal, item);
break;
default:
newQuery[i] = propVal;
break;
}
}
}
return newQuery;
},
spliceArrayByIndexList: function (arr, list) {
var i;
for (i = list.length - 1; i >= 0; i--) {
arr.splice(list[i], 1);
}
}
};
module.exports = Matching;</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="ActiveBucket.html">ActiveBucket</a></li><li><a href="Angular.html">Angular</a></li><li><a href="AutoBind.html">AutoBind</a></li><li><a href="Collection.html">Collection</a></li><li><a href="CollectionGroup.html">CollectionGroup</a></li><li><a href="Condition.html">Condition</a></li><li><a href="Core.html">Core</a></li><li><a href="Db.html">Db</a></li><li><a href="Document.html">Document</a></li><li><a href="Grid.html">Grid</a></li><li><a href="Highchart.html">Highchart</a></li><li><a href="Index2d.html">Index2d</a></li><li><a href="IndexBinaryTree.html">IndexBinaryTree</a></li><li><a href="IndexHashMap.html">IndexHashMap</a></li><li><a href="Infinilist.html">Infinilist</a></li><li><a href="KeyValueStore.html">KeyValueStore</a></li><li><a href="Metrics.html">Metrics</a></li><li><a href="MyModule.html">MyModule</a></li><li><a href="NodeApiClient.html">NodeApiClient</a></li><li><a href="NodeApiServer.html">NodeApiServer</a></li><li><a href="NodeRAS.html">NodeRAS</a></li><li><a href="Odm.html">Odm</a></li><li><a href="OldView.html">OldView</a></li><li><a href="Operation.html">Operation</a></li><li><a href="Overload.html">Overload</a></li><li><a href="Overview.html">Overview</a></li><li><a href="Overview_init.html">init</a></li><li><a href="Path.html">Path</a></li><li><a href="Persist.html">Persist</a></li><li><a href="Procedure.html">Procedure</a></li><li><a href="ReactorIO.html">ReactorIO</a></li><li><a href="Section.html">Section</a></li><li><a href="Serialiser.html">Serialiser</a></li><li><a href="Shared.overload.html">overload</a></li><li><a href="View.html">View</a></li></ul><h3>Mixins</h3><ul><li><a href="ChainReactor.html">ChainReactor</a></li><li><a href="Common.html">Common</a></li><li><a href="Constants.html">Constants</a></li><li><a href="Events.html">Events</a></li><li><a href="Matching.html">Matching</a></li><li><a href="Shared.html">Shared</a></li><li><a href="Sorting.html">Sorting</a></li><li><a href="Tags.html">Tags</a></li><li><a href="Triggers.html">Triggers</a></li><li><a href="Updating.html">Updating</a></li></ul><h3><a href="global.html">Global</a></h3>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.4.0</a> on Thu Mar 01 2018 11:34:22 GMT+0000 (GMT)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>