UNPKG

mobdb

Version:

MarsDB is a lightweight client-side MongoDB-like database, Promise based, written in ES6

1,065 lines (957 loc) 42.2 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.ELEMENT_OPERATORS = exports.DocumentMatcher = undefined; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); exports.regexpElementMatcher = regexpElementMatcher; exports.equalityElementMatcher = equalityElementMatcher; exports.makeLookupFunction = makeLookupFunction; exports.expandArraysInBranches = expandArraysInBranches; var _checkTypes = require('check-types'); var _checkTypes2 = _interopRequireDefault(_checkTypes); var _forEach = require('fast.js/forEach'); var _forEach2 = _interopRequireDefault(_forEach); var _keys2 = require('fast.js/object/keys'); var _keys3 = _interopRequireDefault(_keys2); var _map2 = require('fast.js/map'); var _map3 = _interopRequireDefault(_map2); var _some2 = require('fast.js/array/some'); var _some3 = _interopRequireDefault(_some2); var _every2 = require('fast.js/array/every'); var _every3 = _interopRequireDefault(_every2); var _indexOf2 = require('fast.js/array/indexOf'); var _indexOf3 = _interopRequireDefault(_indexOf2); var _geojsonUtils = require('geojson-utils'); var _geojsonUtils2 = _interopRequireDefault(_geojsonUtils); var _EJSON = require('./EJSON'); var _EJSON2 = _interopRequireDefault(_EJSON); var _Document = require('./Document'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } // The minimongo selector compiler! // Terminology: // - a 'selector' is the EJSON object representing a selector // - a 'matcher' is its compiled form (whether a full Minimongo.Matcher // object or one of the component lambdas that matches parts of it) // - a 'result object' is an object with a 'result' field and maybe // distance and arrayIndices. // - a 'branched value' is an object with a 'value' field and maybe // 'dontIterate' and 'arrayIndices'. // - a 'document' is a top-level object that can be stored in a collection. // - a 'lookup function' is a function that takes in a document and returns // an array of 'branched values'. // - a 'branched matcher' maps from an array of branched values to a result // object. // - an 'element matcher' maps from a single value to a bool. // Main entry point. // var matcher = new Minimongo.Matcher({a: {$gt: 5}}); // if (matcher.documentMatches({a: 7})) ... var DocumentMatcher = exports.DocumentMatcher = function () { function DocumentMatcher(selector) { _classCallCheck(this, DocumentMatcher); // A set (object mapping string -> *) of all of the document paths looked // at by the selector. Also includes the empty string if it may look at any // path (eg, $where). this._paths = {}; // Set to true if compilation finds a $near. this._hasGeoQuery = false; // Set to true if compilation finds a $where. this._hasWhere = false; // Set to false if compilation finds anything other than a simple equality or // one or more of '$gt', '$gte', '$lt', '$lte', '$ne', '$in', '$nin' used with // scalars as operands. this._isSimple = true; // Set to a dummy document which always matches this Matcher. Or set to null // if such document is too hard to find. this._matchingDocument = undefined; // A clone of the original selector. It may just be a function if the user // passed in a function; otherwise is definitely an object (eg, IDs are // translated into {_id: ID} first. Used by canBecomeTrueByModifier and // Sorter._useWithMatcher. this._selector = null; this._docMatcher = this._compileSelector(selector); } _createClass(DocumentMatcher, [{ key: 'documentMatches', value: function documentMatches(doc) { if (!doc || (typeof doc === 'undefined' ? 'undefined' : _typeof(doc)) !== 'object') { throw Error('documentMatches needs a document'); } return this._docMatcher(doc); } }, { key: '_compileSelector', // Given a selector, return a function that takes one argument, a // document. It returns a result object. value: function _compileSelector(selector) { // you can pass a literal function instead of a selector if (selector instanceof Function) { this._isSimple = false; this._selector = selector; this._recordPathUsed(''); return function (doc) { return { result: !!selector.call(doc) }; }; } // shorthand -- scalars match _id if ((0, _Document.selectorIsId)(selector)) { this._selector = { _id: selector }; this._recordPathUsed('_id'); return function (doc) { return { result: _EJSON2.default.equals(doc._id, selector) }; }; } // protect against dangerous selectors. falsey and {_id: falsey} are both // likely programmer error, and not what you want, particularly for // destructive operations. if (!selector || '_id' in selector && !selector._id) { this._isSimple = false; return nothingMatcher; } // Top level can't be an array or true or binary. if (typeof selector === 'boolean' || (0, _Document.isArray)(selector) || _EJSON2.default.isBinary(selector)) { throw new Error('Invalid selector: ' + selector); } this._selector = _EJSON2.default.clone(selector); return compileDocumentSelector(selector, this, { isRoot: true }); } }, { key: '_recordPathUsed', value: function _recordPathUsed(path) { this._paths[path] = true; } // Returns a list of key paths the given selector is looking for. It includes // the empty string if there is a $where. }, { key: '_getPaths', value: function _getPaths() { return _checkTypes2.default.object(this._paths) ? (0, _keys3.default)(this._paths) : null; } }, { key: 'hasGeoQuery', get: function get() { return this._hasGeoQuery; } }, { key: 'hasWhere', get: function get() { return this._hasWhere; } }, { key: 'isSimple', get: function get() { return this._isSimple; } }]); return DocumentMatcher; }(); exports.default = DocumentMatcher; // Takes in a selector that could match a full document (eg, the original // selector). Returns a function mapping document->result object. // // matcher is the Matcher object we are compiling. // // If this is the root document selector (ie, not wrapped in $and or the like), // then isRoot is true. (This is used by $near.) var compileDocumentSelector = function compileDocumentSelector(docSelector, matcher) { var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var docMatchers = []; (0, _forEach2.default)(docSelector, function (subSelector, key) { if (key.substr(0, 1) === '$') { // Outer operators are either logical operators (they recurse back into // this function), or $where. if (!LOGICAL_OPERATORS.hasOwnProperty(key)) { throw new Error('Unrecognized logical operator: ' + key); } matcher._isSimple = false; docMatchers.push(LOGICAL_OPERATORS[key](subSelector, matcher, options.inElemMatch)); } else { // Record this path, but only if we aren't in an elemMatcher, since in an // elemMatch this is a path inside an object in an array, not in the doc // root. if (!options.inElemMatch) { matcher._recordPathUsed(key); } var lookUpByIndex = makeLookupFunction(key); var valueMatcher = compileValueSelector(subSelector, matcher, options.isRoot); docMatchers.push(function (doc) { var branchValues = lookUpByIndex(doc); return valueMatcher(branchValues); }); } }); return andDocumentMatchers(docMatchers); }; // Takes in a selector that could match a key-indexed value in a document; eg, // {$gt: 5, $lt: 9}, or a regular expression, or any non-expression object (to // indicate equality). Returns a branched matcher: a function mapping // [branched value]->result object. var compileValueSelector = function compileValueSelector(valueSelector, matcher, isRoot) { if (valueSelector instanceof RegExp) { matcher._isSimple = false; return convertElementMatcherToBranchedMatcher(regexpElementMatcher(valueSelector)); } else if ((0, _Document.isOperatorObject)(valueSelector)) { return operatorBranchedMatcher(valueSelector, matcher, isRoot); } else { return convertElementMatcherToBranchedMatcher(equalityElementMatcher(valueSelector)); } }; // Given an element matcher (which evaluates a single value), returns a branched // value (which evaluates the element matcher on all the branches and returns a // more structured return value possibly including arrayIndices). var convertElementMatcherToBranchedMatcher = function convertElementMatcherToBranchedMatcher(elementMatcher) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; return function (branches) { var expanded = branches; if (!options.dontExpandLeafArrays) { expanded = expandArraysInBranches(branches, options.dontIncludeLeafArrays); } var ret = {}; ret.result = (0, _some3.default)(expanded, function (element) { var matched = elementMatcher(element.value); // Special case for $elemMatch: it means 'true, and use this as an array // index if I didn't already have one'. if (typeof matched === 'number') { // XXX This code dates from when we only stored a single array index // (for the outermost array). Should we be also including deeper array // indices from the $elemMatch match? if (!element.arrayIndices) { element.arrayIndices = [matched]; } matched = true; } // If some element matched, and it's tagged with array indices, include // those indices in our result object. if (matched && element.arrayIndices) { ret.arrayIndices = element.arrayIndices; } return matched; }); return ret; }; }; // Takes a RegExp object and returns an element matcher. function regexpElementMatcher(regexp) { return function (value) { if (value instanceof RegExp) { // Comparing two regexps means seeing if the regexps are identical // (really!). Underscore knows how. return String(value) === String(regexp); } // Regexps only work against strings. if (typeof value !== 'string') { return false; } // Reset regexp's state to avoid inconsistent matching for objects with the // same value on consecutive calls of regexp.test. This happens only if the // regexp has the 'g' flag. Also note that ES6 introduces a new flag 'y' for // which we should *not* change the lastIndex but MongoDB doesn't support // either of these flags. regexp.lastIndex = 0; return regexp.test(value); }; } // Takes something that is not an operator object and returns an element matcher // for equality with that thing. function equalityElementMatcher(elementSelector) { if ((0, _Document.isOperatorObject)(elementSelector)) { throw Error('Can\'t create equalityValueSelector for operator object'); } // Special-case: null and undefined are equal (if you got undefined in there // somewhere, or if you got it due to some branch being non-existent in the // weird special case), even though they aren't with EJSON.equals. if (elementSelector == null) { // undefined or null return function (value) { return value == null; // undefined or null }; } return function (value) { return _Document.MongoTypeComp._equal(elementSelector, value); }; } // Takes an operator object (an object with $ keys) and returns a branched // matcher for it. var operatorBranchedMatcher = function operatorBranchedMatcher(valueSelector, matcher, isRoot) { // Each valueSelector works separately on the various branches. So one // operator can match one branch and another can match another branch. This // is OK. var operatorMatchers = []; (0, _forEach2.default)(valueSelector, function (operand, operator) { // XXX we should actually implement $eq, which is new in 2.6 var simpleRange = (0, _indexOf3.default)(['$lt', '$lte', '$gt', '$gte'], operator) >= 0 && _checkTypes2.default.number(operand); var simpleInequality = operator === '$ne' && !_checkTypes2.default.object(operand); var simpleInclusion = (0, _indexOf3.default)(['$in', '$nin'], operator) >= 0 && _checkTypes2.default.array(operand) && !(0, _some3.default)(operand, _checkTypes2.default.object); if (!(operator === '$eq' || simpleRange || simpleInclusion || simpleInequality)) { matcher._isSimple = false; } if (VALUE_OPERATORS.hasOwnProperty(operator)) { operatorMatchers.push(VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot)); } else if (ELEMENT_OPERATORS.hasOwnProperty(operator)) { var options = ELEMENT_OPERATORS[operator]; operatorMatchers.push(convertElementMatcherToBranchedMatcher(options.compileElementSelector(operand, valueSelector, matcher), options)); } else { throw new Error('Unrecognized operator: ' + operator); } }); return andBranchedMatchers(operatorMatchers); }; var compileArrayOfDocumentSelectors = function compileArrayOfDocumentSelectors(selectors, matcher, inElemMatch) { if (!(0, _Document.isArray)(selectors) || _checkTypes2.default.emptyArray(selectors)) { throw Error('$and/$or/$nor must be nonempty array'); } return (0, _map3.default)(selectors, function (subSelector) { if (!(0, _Document.isPlainObject)(subSelector)) { throw Error('$or/$and/$nor entries need to be full objects'); } return compileDocumentSelector(subSelector, matcher, { inElemMatch: inElemMatch }); }); }; // Operators that appear at the top level of a document selector. var LOGICAL_OPERATORS = { $and: function $and(subSelector, matcher, inElemMatch) { var matchers = compileArrayOfDocumentSelectors(subSelector, matcher, inElemMatch); return andDocumentMatchers(matchers); }, $or: function $or(subSelector, matcher, inElemMatch) { var matchers = compileArrayOfDocumentSelectors(subSelector, matcher, inElemMatch); // Special case: if there is only one matcher, use it directly, *preserving* // any arrayIndices it returns. if (matchers.length === 1) { return matchers[0]; } return function (doc) { var result = (0, _some3.default)(matchers, function (f) { return f(doc).result; }); // $or does NOT set arrayIndices when it has multiple // sub-expressions. (Tested against MongoDB.) return { result: result }; }; }, $nor: function $nor(subSelector, matcher, inElemMatch) { var matchers = compileArrayOfDocumentSelectors(subSelector, matcher, inElemMatch); return function (doc) { var result = (0, _every3.default)(matchers, function (f) { return !f(doc).result; }); // Never set arrayIndices, because we only match if nothing in particular // 'matched' (and because this is consistent with MongoDB). return { result: result }; }; }, $where: function $where(selectorValue, matcher) { // Record that *any* path may be used. matcher._recordPathUsed(''); matcher._hasWhere = true; if (!(selectorValue instanceof Function)) { // XXX MongoDB seems to have more complex logic to decide where or or not // to add 'return'; not sure exactly what it is. selectorValue = Function('obj', 'return ' + selectorValue); //eslint-disable-line no-new-func } return function (doc) { // We make the document available as both `this` and `obj`. // XXX not sure what we should do if this throws return { result: selectorValue.call(doc, doc) }; }; }, // This is just used as a comment in the query (in MongoDB, it also ends up in // query logs); it has no effect on the actual selection. $comment: function $comment() { return function () { return { result: true }; }; } }; // Returns a branched matcher that matches iff the given matcher does not. // Note that this implicitly 'deMorganizes' the wrapped function. ie, it // means that ALL branch values need to fail to match innerBranchedMatcher. var invertBranchedMatcher = function invertBranchedMatcher(branchedMatcher) { return function (branchValues) { var invertMe = branchedMatcher(branchValues); // We explicitly choose to strip arrayIndices here: it doesn't make sense to // say 'update the array element that does not match something', at least // in mongo-land. return { result: !invertMe.result }; }; }; // Operators that (unlike LOGICAL_OPERATORS) pertain to individual paths in a // document, but (unlike ELEMENT_OPERATORS) do not have a simple definition as // 'match each branched value independently and combine with // convertElementMatcherToBranchedMatcher'. var VALUE_OPERATORS = { $not: function $not(operand, valueSelector, matcher) { return invertBranchedMatcher(compileValueSelector(operand, matcher)); }, $ne: function $ne(operand) { return invertBranchedMatcher(convertElementMatcherToBranchedMatcher(equalityElementMatcher(operand))); }, $nin: function $nin(operand) { return invertBranchedMatcher(convertElementMatcherToBranchedMatcher(ELEMENT_OPERATORS.$in.compileElementSelector(operand))); }, $exists: function $exists(operand) { var exists = convertElementMatcherToBranchedMatcher(function (value) { return value !== undefined; }); return operand ? exists : invertBranchedMatcher(exists); }, // $options just provides options for $regex; its logic is inside $regex $options: function $options(operand, valueSelector) { if (!_checkTypes2.default.object(valueSelector) || !valueSelector.hasOwnProperty('$regex')) { throw Error('$options needs a $regex'); } return everythingMatcher; }, // $maxDistance is basically an argument to $near $maxDistance: function $maxDistance(operand, valueSelector) { if (!valueSelector.$near) { throw Error('$maxDistance needs a $near'); } return everythingMatcher; }, $all: function $all(operand, valueSelector, matcher) { if (!(0, _Document.isArray)(operand)) { throw Error('$all requires array'); } // Not sure why, but this seems to be what MongoDB does. if (_checkTypes2.default.emptyArray(operand)) { return nothingMatcher; } var branchedMatchers = []; (0, _forEach2.default)(operand, function (criterion) { // XXX handle $all/$elemMatch combination if ((0, _Document.isOperatorObject)(criterion)) { throw Error('no $ expressions in $all'); } // This is always a regexp or equality selector. branchedMatchers.push(compileValueSelector(criterion, matcher)); }); // andBranchedMatchers does NOT require all selectors to return true on the // SAME branch. return andBranchedMatchers(branchedMatchers); }, $near: function $near(operand, valueSelector, matcher, isRoot) { if (!isRoot) { throw Error('$near can\'t be inside another $ operator'); } matcher._hasGeoQuery = true; // There are two kinds of geodata in MongoDB: coordinate pairs and // GeoJSON. They use different distance metrics, too. GeoJSON queries are // marked with a $geometry property. var maxDistance, point, distance; if ((0, _Document.isPlainObject)(operand) && operand.hasOwnProperty('$geometry')) { // GeoJSON '2dsphere' mode. maxDistance = operand.$maxDistance; point = operand.$geometry; distance = function distance(value) { // XXX: for now, we don't calculate the actual distance between, say, // polygon and circle. If people care about this use-case it will get // a priority. if (!value || !value.type) { return null; } if (value.type === 'Point') { return _geojsonUtils2.default.pointDistance(point, value); } else { return _geojsonUtils2.default.geometryWithinRadius(value, point, maxDistance) ? 0 : maxDistance + 1; } }; } else { maxDistance = valueSelector.$maxDistance; if (!(0, _Document.isArray)(operand) && !(0, _Document.isPlainObject)(operand)) { throw Error('$near argument must be coordinate pair or GeoJSON'); } point = pointToArray(operand); distance = function distance(value) { if (!(0, _Document.isArray)(value) && !(0, _Document.isPlainObject)(value)) { return null; } return distanceCoordinatePairs(point, value); }; } return function (branchedValues) { // There might be multiple points in the document that match the given // field. Only one of them needs to be within $maxDistance, but we need to // evaluate all of them and use the nearest one for the implicit sort // specifier. (That's why we can't just use ELEMENT_OPERATORS here.) // // Note: This differs from MongoDB's implementation, where a document will // actually show up *multiple times* in the result set, with one entry for // each within-$maxDistance branching point. branchedValues = expandArraysInBranches(branchedValues); var result = { result: false }; (0, _forEach2.default)(branchedValues, function (branch) { var curDistance = distance(branch.value); // Skip branches that aren't real points or are too far away. if (curDistance === null || curDistance > maxDistance) { return; } // Skip anything that's a tie. if (result.distance !== undefined && result.distance <= curDistance) { return; } result.result = true; result.distance = curDistance; if (!branch.arrayIndices) { delete result.arrayIndices; } else { result.arrayIndices = branch.arrayIndices; } }); return result; }; } }; // Helpers for $near. var distanceCoordinatePairs = function distanceCoordinatePairs(a, b) { a = pointToArray(a); b = pointToArray(b); var x = a[0] - b[0]; var y = a[1] - b[1]; if (!_checkTypes2.default.number(x) || !_checkTypes2.default.number(y)) { return null; } return Math.sqrt(x * x + y * y); }; // Makes sure we get 2 elements array and assume the first one to be x and // the second one to y no matter what user passes. // In case user passes { lon: x, lat: y } returns [x, y] var pointToArray = function pointToArray(point) { return (0, _map3.default)(point, function (x) { return x; }); }; // Helper for $lt/$gt/$lte/$gte. var makeInequality = function makeInequality(cmpValueComparator) { return { compileElementSelector: function compileElementSelector(operand) { // Arrays never compare false with non-arrays for any inequality. // XXX This was behavior we observed in pre-release MongoDB 2.5, but // it seems to have been reverted. // See https://jira.mongodb.org/browse/SERVER-11444 if ((0, _Document.isArray)(operand)) { return function () { return false; }; } // Special case: consider undefined and null the same (so true with // $gte/$lte). if (operand === undefined) { operand = null; } var operandType = _Document.MongoTypeComp._type(operand); return function (value) { if (value === undefined) { value = null; } // Comparisons are never true among things of different type (except // null vs undefined). if (_Document.MongoTypeComp._type(value) !== operandType) { return false; } return cmpValueComparator(_Document.MongoTypeComp._cmp(value, operand)); }; } }; }; // Each element selector contains: // - compileElementSelector, a function with args: // - operand - the 'right hand side' of the operator // - valueSelector - the 'context' for the operator (so that $regex can find // $options) // - matcher - the Matcher this is going into (so that $elemMatch can compile // more things) // returning a function mapping a single value to bool. // - dontExpandLeafArrays, a bool which prevents expandArraysInBranches from // being called // - dontIncludeLeafArrays, a bool which causes an argument to be passed to // expandArraysInBranches if it is called var ELEMENT_OPERATORS = exports.ELEMENT_OPERATORS = { $lt: makeInequality(function (cmpValue) { return cmpValue < 0; }), $gt: makeInequality(function (cmpValue) { return cmpValue > 0; }), $lte: makeInequality(function (cmpValue) { return cmpValue <= 0; }), $gte: makeInequality(function (cmpValue) { return cmpValue >= 0; }), $mod: { compileElementSelector: function compileElementSelector(operand) { if (!((0, _Document.isArray)(operand) && operand.length === 2 && typeof operand[0] === 'number' && typeof operand[1] === 'number')) { throw Error('argument to $mod must be an array of two numbers'); } // XXX could require to be ints or round or something var divisor = operand[0]; var remainder = operand[1]; return function (value) { return typeof value === 'number' && value % divisor === remainder; }; } }, $in: { compileElementSelector: function compileElementSelector(operand) { if (!(0, _Document.isArray)(operand)) { throw Error('$in needs an array'); } var elementMatchers = []; (0, _forEach2.default)(operand, function (option) { if (option instanceof RegExp) { elementMatchers.push(regexpElementMatcher(option)); } else if ((0, _Document.isOperatorObject)(option)) { throw Error('cannot nest $ under $in'); } else { elementMatchers.push(equalityElementMatcher(option)); } }); return function (value) { // Allow {a: {$in: [null]}} to match when 'a' does not exist. if (value === undefined) { value = null; } return (0, _some3.default)(elementMatchers, function (e) { return e(value); }); }; } }, $size: { // {a: [[5, 5]]} must match {a: {$size: 1}} but not {a: {$size: 2}}, so we // don't want to consider the element [5,5] in the leaf array [[5,5]] as a // possible value. dontExpandLeafArrays: true, compileElementSelector: function compileElementSelector(operand) { if (typeof operand === 'string') { // Don't ask me why, but by experimentation, this seems to be what Mongo // does. operand = 0; } else if (typeof operand !== 'number') { throw Error('$size needs a number'); } return function (value) { return (0, _Document.isArray)(value) && value.length === operand; }; } }, $type: { // {a: [5]} must not match {a: {$type: 4}} (4 means array), but it should // match {a: {$type: 1}} (1 means number), and {a: [[5]]} must match {$a: // {$type: 4}}. Thus, when we see a leaf array, we *should* expand it but // should *not* include it itself. dontIncludeLeafArrays: true, compileElementSelector: function compileElementSelector(operand) { if (typeof operand !== 'number') { throw Error('$type needs a number'); } return function (value) { return value !== undefined && _Document.MongoTypeComp._type(value) === operand; }; } }, $regex: { compileElementSelector: function compileElementSelector(operand, valueSelector) { if (!(typeof operand === 'string' || operand instanceof RegExp)) { throw Error('$regex has to be a string or RegExp'); } var regexp; if (valueSelector.$options !== undefined) { // Options passed in $options (even the empty string) always overrides // options in the RegExp object itself. (See also // Mongo.Collection._rewriteSelector.) // Be clear that we only support the JS-supported options, not extended // ones (eg, Mongo supports x and s). Ideally we would implement x and s // by transforming the regexp, but not today... if (/[^gim]/.test(valueSelector.$options)) { throw new Error('Only the i, m, and g regexp options are supported'); } var regexSource = operand instanceof RegExp ? operand.source : operand; regexp = new RegExp(regexSource, valueSelector.$options); } else if (operand instanceof RegExp) { regexp = operand; } else { regexp = new RegExp(operand); } return regexpElementMatcher(regexp); } }, $elemMatch: { dontExpandLeafArrays: true, compileElementSelector: function compileElementSelector(operand, valueSelector, matcher) { if (!(0, _Document.isPlainObject)(operand)) { throw Error('$elemMatch need an object'); } var subMatcher, isDocMatcher; if ((0, _Document.isOperatorObject)(operand, true)) { subMatcher = compileValueSelector(operand, matcher); isDocMatcher = false; } else { // This is NOT the same as compileValueSelector(operand), and not just // because of the slightly different calling convention. // {$elemMatch: {x: 3}} means 'an element has a field x:3', not // 'consists only of a field x:3'. Also, regexps and sub-$ are allowed. subMatcher = compileDocumentSelector(operand, matcher, { inElemMatch: true }); isDocMatcher = true; } return function (value) { if (!(0, _Document.isArray)(value)) { return false; } for (var i = 0; i < value.length; ++i) { var arrayElement = value[i]; var arg; if (isDocMatcher) { // We can only match {$elemMatch: {b: 3}} against objects. // (We can also match against arrays, if there's numeric indices, // eg {$elemMatch: {'0.b': 3}} or {$elemMatch: {0: 3}}.) if (!(0, _Document.isPlainObject)(arrayElement) && !(0, _Document.isArray)(arrayElement)) { return false; } arg = arrayElement; } else { // dontIterate ensures that {a: {$elemMatch: {$gt: 5}}} matches // {a: [8]} but not {a: [[8]]} arg = [{ value: arrayElement, dontIterate: true }]; } // XXX support $near in $elemMatch by propagating $distance? if (subMatcher(arg).result) { return i; // specially understood to mean 'use as arrayIndices' } } return false; }; } } }; // makeLookupFunction(key) returns a lookup function. // // A lookup function takes in a document and returns an array of matching // branches. If no arrays are found while looking up the key, this array will // have exactly one branches (possibly 'undefined', if some segment of the key // was not found). // // If arrays are found in the middle, this can have more than one element, since // we 'branch'. When we 'branch', if there are more key segments to look up, // then we only pursue branches that are plain objects (not arrays or scalars). // This means we can actually end up with no branches! // // We do *NOT* branch on arrays that are found at the end (ie, at the last // dotted member of the key). We just return that array; if you want to // effectively 'branch' over the array's values, post-process the lookup // function with expandArraysInBranches. // // Each branch is an object with keys: // - value: the value at the branch // - dontIterate: an optional bool; if true, it means that 'value' is an array // that expandArraysInBranches should NOT expand. This specifically happens // when there is a numeric index in the key, and ensures the // perhaps-surprising MongoDB behavior where {'a.0': 5} does NOT // match {a: [[5]]}. // - arrayIndices: if any array indexing was done during lookup (either due to // explicit numeric indices or implicit branching), this will be an array of // the array indices used, from outermost to innermost; it is falsey or // absent if no array index is used. If an explicit numeric index is used, // the index will be followed in arrayIndices by the string 'x'. // // Note: arrayIndices is used for two purposes. First, it is used to // implement the '$' modifier feature, which only ever looks at its first // element. // // Second, it is used for sort key generation, which needs to be able to tell // the difference between different paths. Moreover, it needs to // differentiate between explicit and implicit branching, which is why // there's the somewhat hacky 'x' entry: this means that explicit and // implicit array lookups will have different full arrayIndices paths. (That // code only requires that different paths have different arrayIndices; it // doesn't actually 'parse' arrayIndices. As an alternative, arrayIndices // could contain objects with flags like 'implicit', but I think that only // makes the code surrounding them more complex.) // // (By the way, this field ends up getting passed around a lot without // cloning, so never mutate any arrayIndices field/var in this package!) // // // At the top level, you may only pass in a plain object or array. // // See the test 'minimongo - lookup' for some examples of what lookup functions // return. function makeLookupFunction(key, options) { options = options || {}; var parts = key.split('.'); var firstPart = parts.length ? parts[0] : ''; var firstPartIsNumeric = (0, _Document.isNumericKey)(firstPart); var nextPartIsNumeric = parts.length >= 2 && (0, _Document.isNumericKey)(parts[1]); var lookupRest; if (parts.length > 1) { lookupRest = makeLookupFunction(parts.slice(1).join('.')); } var omitUnnecessaryFields = function omitUnnecessaryFields(retVal) { if (!retVal.dontIterate) { delete retVal.dontIterate; } if (retVal.arrayIndices && !retVal.arrayIndices.length) { delete retVal.arrayIndices; } return retVal; }; // Doc will always be a plain object or an array. // apply an explicit numeric index, an array. return function (doc, arrayIndices) { if (!arrayIndices) { arrayIndices = []; } if ((0, _Document.isArray)(doc)) { // If we're being asked to do an invalid lookup into an array (non-integer // or out-of-bounds), return no results (which is different from returning // a single undefined result, in that `null` equality checks won't match). if (!(firstPartIsNumeric && firstPart < doc.length)) { return []; } // Remember that we used this array index. Include an 'x' to indicate that // the previous index came from being considered as an explicit array // index (not branching). arrayIndices = arrayIndices.concat(+firstPart, 'x'); } // Do our first lookup. var firstLevel = doc[firstPart]; // If there is no deeper to dig, return what we found. // // If what we found is an array, most value selectors will choose to treat // the elements of the array as matchable values in their own right, but // that's done outside of the lookup function. (Exceptions to this are $size // and stuff relating to $elemMatch. eg, {a: {$size: 2}} does not match {a: // [[1, 2]]}.) // // That said, if we just did an *explicit* array lookup (on doc) to find // firstLevel, and firstLevel is an array too, we do NOT want value // selectors to iterate over it. eg, {'a.0': 5} does not match {a: [[5]]}. // So in that case, we mark the return value as 'don't iterate'. if (!lookupRest) { return [omitUnnecessaryFields({ value: firstLevel, dontIterate: (0, _Document.isArray)(doc) && (0, _Document.isArray)(firstLevel), arrayIndices: arrayIndices })]; } // We need to dig deeper. But if we can't, because what we've found is not // an array or plain object, we're done. If we just did a numeric index into // an array, we return nothing here (this is a change in Mongo 2.5 from // Mongo 2.4, where {'a.0.b': null} stopped matching {a: [5]}). Otherwise, // return a single `undefined` (which can, for example, match via equality // with `null`). if (!(0, _Document.isIndexable)(firstLevel)) { if ((0, _Document.isArray)(doc)) { return []; } return [omitUnnecessaryFields({ value: undefined, arrayIndices: arrayIndices })]; } var result = []; var appendToResult = function appendToResult(more) { Array.prototype.push.apply(result, more); }; // Dig deeper: look up the rest of the parts on whatever we've found. // (lookupRest is smart enough to not try to do invalid lookups into // firstLevel if it's an array.) appendToResult(lookupRest(firstLevel, arrayIndices)); // If we found an array, then in *addition* to potentially treating the next // part as a literal integer lookup, we should also 'branch': try to look up // the rest of the parts on each array element in parallel. // // In this case, we *only* dig deeper into array elements that are plain // objects. (Recall that we only got this far if we have further to dig.) // This makes sense: we certainly don't dig deeper into non-indexable // objects. And it would be weird to dig into an array: it's simpler to have // a rule that explicit integer indexes only apply to an outer array, not to // an array you find after a branching search. // // In the special case of a numeric part in a *sort selector* (not a query // selector), we skip the branching: we ONLY allow the numeric part to mean // 'look up this index' in that case, not 'also look up this index in all // the elements of the array'. if ((0, _Document.isArray)(firstLevel) && !(nextPartIsNumeric && options.forSort)) { (0, _forEach2.default)(firstLevel, function (branch, arrayIndex) { if ((0, _Document.isPlainObject)(branch)) { appendToResult(lookupRest(branch, arrayIndices.concat(arrayIndex))); } }); } return result; }; } function expandArraysInBranches(branches, skipTheArrays) { var branchesOut = []; (0, _forEach2.default)(branches, function (branch) { var thisIsArray = (0, _Document.isArray)(branch.value); // We include the branch itself, *UNLESS* we it's an array that we're going // to iterate and we're told to skip arrays. (That's right, we include some // arrays even skipTheArrays is true: these are arrays that were found via // explicit numerical indices.) if (!(skipTheArrays && thisIsArray && !branch.dontIterate)) { branchesOut.push({ value: branch.value, arrayIndices: branch.arrayIndices }); } if (thisIsArray && !branch.dontIterate) { (0, _forEach2.default)(branch.value, function (leaf, i) { branchesOut.push({ value: leaf, arrayIndices: (branch.arrayIndices || []).concat(i) }); }); } }); return branchesOut; } var nothingMatcher = function nothingMatcher(docOrBranchedValues) { return { result: false }; }; var everythingMatcher = function everythingMatcher(docOrBranchedValues) { return { result: true }; }; // NB: We are cheating and using this function to implement 'AND' for both // 'document matchers' and 'branched matchers'. They both return result objects // but the argument is different: for the former it's a whole doc, whereas for // the latter it's an array of 'branched values'. var andSomeMatchers = function andSomeMatchers(subMatchers) { if (subMatchers.length === 0) { return everythingMatcher; } if (subMatchers.length === 1) { return subMatchers[0]; } return function (docOrBranches) { var ret = {}; ret.result = (0, _every3.default)(subMatchers, function (f) { var subResult = f(docOrBranches); // Copy a 'distance' number out of the first sub-matcher that has // one. Yes, this means that if there are multiple $near fields in a // query, something arbitrary happens; this appears to be consistent with // Mongo. if (subResult.result && subResult.distance !== undefined && ret.distance === undefined) { ret.distance = subResult.distance; } // Similarly, propagate arrayIndices from sub-matchers... but to match // MongoDB behavior, this time the *last* sub-matcher with arrayIndices // wins. if (subResult.result && subResult.arrayIndices) { ret.arrayIndices = subResult.arrayIndices; } return subResult.result; }); // If we didn't actually match, forget any extra metadata we came up with. if (!ret.result) { delete ret.distance; delete ret.arrayIndices; } return ret; }; }; var andDocumentMatchers = andSomeMatchers; var andBranchedMatchers = andSomeMatchers;