UNPKG

forerunnerdb

Version:

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

827 lines (701 loc) 27.5 kB
<!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' &amp;&amp; source === null) { sourceType = 'null'; } if (testType === 'object' &amp;&amp; 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') &amp;&amp; (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') &amp;&amp; (testType === 'object' &amp;&amp; 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 &amp;&amp; test[i] instanceof RegExp) { operation = true; if (sourceType === 'object' &amp;&amp; source[i] !== undefined &amp;&amp; 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 &amp;&amp; !(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 &lt; 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) &amp;&amp; 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 &lt; 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] &amp;&amp; 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 &amp;&amp; source[i] === test[i]) { if (opToApply === 'or') { return true; } } else if (source &amp;&amp; source[i] &amp;&amp; source[i] instanceof Array &amp;&amp; test[i] &amp;&amp; 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 &lt; 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' &amp;&amp; !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 &lt; test; case '$lte': // Less than or equal return source &lt;= 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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' &amp;&amp; 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 &amp;&amp; options.$parent.parent &amp;&amp; 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 &amp;&amp; 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 &lt; 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] &amp;&amp; typeof this._db[joinSourceType] === 'function') { joinSourceInstance = this._db[joinSourceType](joinSourceKey); } } // Loop our result data array for (resultIndex = 0; resultIndex &lt; 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 &amp;&amp; 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) &amp;&amp; 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>