diff --git a/docs/client/introduction.html b/docs/client/introduction.html index 33dd87b5b5..0c130eaacf 100644 --- a/docs/client/introduction.html +++ b/docs/client/introduction.html @@ -114,8 +114,8 @@ with the project!
Stack Overflow
-
The best place to ask (and answer!) technical questions is on [Stack - Overflow](http://stackoverflow.com/questions/tagged/meteor). Be sure to add +
The best place to ask (and answer!) technical questions is on Stack + Overflow. Be sure to add the meteor tag to your question.
diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index 0a19516159..2ae037b777 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -1,7 +1,7 @@ (function () { Accounts.oauth.registerService('twitter', 1, function(oauthBinding) { - var identity = oauthBinding.get('https://api.twitter.com/1/account/verify_credentials.json').data; + var identity = oauthBinding.get('https://api.twitter.com/1.1/account/verify_credentials.json').data; return { serviceData: { diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 5e0a6ddb74..7d14ac21dd 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -477,6 +477,10 @@ Tinytest.add("minimongo - selector_compiler", function (test) { nomatch({a: /a/}, {a: 'cut'}); nomatch({a: /a/}, {a: 'CAT'}); match({a: /a/i}, {a: 'CAT'}); + match({a: /a/}, {a: ['foo', 'bar']}); // search within array... + nomatch({a: /,/}, {a: ['foo', 'bar']}); // but not by stringifying + match({a: {$regex: 'a'}}, {a: ['foo', 'bar']}); + nomatch({a: {$regex: ','}}, {a: ['foo', 'bar']}); match({a: {$regex: /a/}}, {a: 'cat'}); nomatch({a: {$regex: /a/}}, {a: 'cut'}); nomatch({a: {$regex: /a/}}, {a: 'CAT'}); @@ -524,6 +528,9 @@ Tinytest.add("minimongo - selector_compiler", function (test) { nomatch({"a.b": [1,2,3]}, {a: {b: [4]}}); match({"a.b": /a/}, {a: {b: "cat"}}); nomatch({"a.b": /a/}, {a: {b: "dog"}}); + match({"a.b.c": null}, {}); + match({"a.b.c": null}, {a: 1}); + match({"a.b.c": null}, {a: {b: 4}}); // trying to access a dotted field that is undefined at some point // down the chain @@ -796,11 +803,28 @@ Tinytest.add("minimongo - selector_compiler", function (test) { match({$where: "_.isArray(this.a)"}, {a: []}); nomatch({$where: "_.isArray(this.a)"}, {a: 1}); + // reaching into array match({"dogs.0.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); match({"dogs.1.name": "Rex"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); nomatch({"dogs.1.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); match({"room.1b": "bla"}, {room: {"1b": "bla"}}); + // $elemMatch + match({dogs: {$elemMatch: {name: /e/}}}, + {dogs: [{name: "Fido"}, {name: "Rex"}]}); + nomatch({dogs: {$elemMatch: {name: /a/}}}, + {dogs: [{name: "Fido"}, {name: "Rex"}]}); + match({dogs: {$elemMatch: {age: {$gt: 4}}}}, + {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + match({dogs: {$elemMatch: {name: "Fido", age: {$gt: 4}}}}, + {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + nomatch({dogs: {$elemMatch: {name: "Fido", age: {$gt: 5}}}}, + {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + match({dogs: {$elemMatch: {name: /i/, age: {$gt: 4}}}}, + {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + nomatch({dogs: {$elemMatch: {name: /e/, age: 5}}}, + {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + // XXX still needs tests: // - $elemMatch // - non-scalar arguments to $gt, $lt, etc diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index 29700785f5..7b88f59b3c 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -12,7 +12,6 @@ Package.on_use(function (api, where) { api.add_files([ 'minimongo.js', 'selector.js', - 'sort.js', 'uuid.js', 'modify.js', 'diff.js', diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 19061d68af..0d402b2125 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -1,53 +1,296 @@ +(function(){ + +var _anyIfArray = function (x, f) { + if (_.isArray(x)) + return _.any(x, f); + return f(x); +}; + +var _anyIfArrayPlus = function (x, f) { + if (f(x)) + return true; + return _.isArray(x) && _.any(x, f); +}; + +var hasOperators = function(valueSelector) { + var theseAreOperators = undefined; + for (var selKey in valueSelector) { + var thisIsOperator = selKey.substr(0, 1) === '$'; + if (theseAreOperators === undefined) { + theseAreOperators = thisIsOperator; + } else if (theseAreOperators !== thisIsOperator) { + throw new Error("Inconsistent selector: " + valueSelector); + } + } + return !!theseAreOperators; // {} has no operators +}; + +var compileValueSelector = function (valueSelector) { + if (valueSelector == null) { // undefined or null + return function (value) { + return _anyIfArray(value, function (x) { + return x == null; // undefined or null + }); + }; + } + + // Selector is a non-null primitive (and not an array or RegExp either). + if (!_.isObject(valueSelector)) { + return function (value) { + return _anyIfArray(value, function (x) { + return x === valueSelector; + }); + }; + } + + if (valueSelector instanceof RegExp) { + return function (value) { + return _anyIfArray(value, function (x) { + return valueSelector.test(x); + }); + }; + } + + // Arrays match either identical arrays or arrays that contain it as a value. + if (_.isArray(valueSelector)) { + return function (value) { + if (!_.isArray(value)) + return false; + return _anyIfArrayPlus(value, function (x) { + return LocalCollection._f._equal(valueSelector, x); + }); + }; + } + + // It's an object, but not an array or regexp. + if (hasOperators(valueSelector)) { + var operatorFunctions = []; + _.each(valueSelector, function (operand, operator) { + if (!_.has(VALUE_OPERATORS, operator)) + throw new Error("Unrecognized operator: " + operator); + operatorFunctions.push(VALUE_OPERATORS[operator]( + operand, valueSelector.$options)); + }); + return function (value) { + return _.all(operatorFunctions, function (f) { + return f(value); + }); + }; + } + + // It's a literal; compare value (or element of value array) directly to the + // selector. + return function (value) { + return _anyIfArray(value, function (x) { + return LocalCollection._f._equal(valueSelector, x); + }); + }; +}; + +// XXX can factor out common logic below +var LOGICAL_OPERATORS = { + "$and": function(subSelector) { + if (!_.isArray(subSelector) || _.isEmpty(subSelector)) + throw Error("$and/$or/$nor must be nonempty array"); + var subSelectorFunctions = _.map( + subSelector, compileDocumentSelector); + return function (doc) { + return _.all(subSelectorFunctions, function (f) { + return f(doc); + }); + }; + }, + + "$or": function(subSelector) { + if (!_.isArray(subSelector) || _.isEmpty(subSelector)) + throw Error("$and/$or/$nor must be nonempty array"); + var subSelectorFunctions = _.map( + subSelector, compileDocumentSelector); + return function (doc) { + return _.any(subSelectorFunctions, function (f) { + return f(doc); + }); + }; + }, + + "$nor": function(subSelector) { + if (!_.isArray(subSelector) || _.isEmpty(subSelector)) + throw Error("$and/$or/$nor must be nonempty array"); + var subSelectorFunctions = _.map( + subSelector, compileDocumentSelector); + return function (doc) { + return _.all(subSelectorFunctions, function (f) { + return !f(doc); + }); + }; + }, + + "$where": function(selectorValue) { + if (!(selectorValue instanceof Function)) { + selectorValue = Function("return " + selectorValue); + } + return function (doc) { + return selectorValue.call(doc); + }; + } +}; + +var VALUE_OPERATORS = { + "$in": function (operand) { + if (!_.isArray(operand)) + throw new Error("Argument to $in must be array"); + return function (value) { + return _anyIfArrayPlus(value, function (x) { + return _.any(operand, function (operandElt) { + return LocalCollection._f._equal(operandElt, x); + }); + }); + }; + }, + + "$all": function (operand) { + if (!_.isArray(operand)) + throw new Error("Argument to $all must be array"); + return function (value) { + if (!_.isArray(value)) + return false; + return _.all(operand, function (operandElt) { + return _.any(value, function (valueElt) { + return LocalCollection._f._equal(operandElt, valueElt); + }); + }); + }; + }, + + "$lt": function (operand) { + return function (value) { + return _anyIfArray(value, function (x) { + return LocalCollection._f._cmp(x, operand) < 0; + }); + }; + }, + + "$lte": function (operand) { + return function (value) { + return _anyIfArray(value, function (x) { + return LocalCollection._f._cmp(x, operand) <= 0; + }); + }; + }, + + "$gt": function (operand) { + return function (value) { + return _anyIfArray(value, function (x) { + return LocalCollection._f._cmp(x, operand) > 0; + }); + }; + }, + + "$gte": function (operand) { + return function (value) { + return _anyIfArray(value, function (x) { + return LocalCollection._f._cmp(x, operand) >= 0; + }); + }; + }, + + "$ne": function (operand) { + return function (value) { + return ! _anyIfArrayPlus(value, function (x) { + return LocalCollection._f._equal(x, operand); + }); + }; + }, + + "$nin": function (operand) { + if (!_.isArray(operand)) + throw new Error("Argument to $nin must be array"); + var inFunction = VALUE_OPERATORS.$in(operand); + return function (value) { + // Field doesn't exist, so it's not-in operand + if (value === undefined) + return true; + return !inFunction(value); + }; + }, + + "$exists": function (operand) { + return function (value) { + return operand === (value !== undefined); + }; + }, + + "$mod": function (operand) { + var divisor = operand[0], + remainder = operand[1]; + return function (value) { + return _anyIfArray(value, function (x) { + return x % divisor === remainder; + }); + }; + }, + + "$size": function (operand) { + return function (value) { + return _.isArray(value) && operand === value.length; + }; + }, + + "$type": function (operand) { + return function (value) { + // Definitely not _anyIfArrayPlus: $type: 4 only matches arrays that have + // arrays as elements according to the Mongo docs. + return _anyIfArray(value, function (x) { + return LocalCollection._f._type(x) === operand; + }); + }; + }, + + "$regex": function (operand, options) { + if (options !== undefined) { + // Options passed in $options (even the empty string) always overrides + // options in the RegExp object itself. + var regexSource = operand instanceof RegExp ? operand.source : operand; + operand = new RegExp(regexSource, options); + } else if (!(operand instanceof RegExp)) { + operand = new RegExp(operand); + } + + return function (value) { + return _anyIfArray(value, function (x) { + return operand.test(x); + }); + }; + }, + + "$options": function (operand) { + // evaluation happens at the $regex function above + return function (value) { return true; }; + }, + + "$elemMatch": function (operand) { + var matcher = compileDocumentSelector(operand); + return function (value) { + if (!_.isArray(value)) + return false; + return _.any(value, function (x) { + return matcher(x); + }); + }; + }, + + "$not": function (operand) { + var matcher = compileValueSelector(operand); + return function (value) { + return !matcher(value); + }; + } +}; + // helpers used by compiled selector code LocalCollection._f = { // XXX for _all and _in, consider building 'inquery' at compile time.. - _all: function (x, qval) { - // $all is only meaningful on arrays - if (!(x instanceof Array)) { - return false; - } - // XXX should use a canonicalizing representation, so that we - // don't get screwed by key order - var parts = {}; - var remaining = 0; - _.each(qval, function (q) { - var hash = JSON.stringify(q); - if (!(hash in parts)) { - parts[hash] = true; - remaining++; - } - }); - - for (var i = 0; i < x.length; i++) { - var hash = JSON.stringify(x[i]); - if (parts[hash]) { - delete parts[hash]; - remaining--; - if (0 === remaining) - return true; - } - } - - return false; - }, - - _in: function (x, qval) { - if (typeof x !== "object") { - // optimization: use scalar equality (fast) - for (var i = 0; i < qval.length; i++) - if (x === qval[i]) - return true; - return false; - } else { - // nope, have to use deep equality - for (var i = 0; i < qval.length; i++) - if (LocalCollection._f._equal(x, qval[i])) - return true; - return false; - } - }, - _type: function (v) { if (typeof v === "number") return 1; @@ -83,36 +326,6 @@ LocalCollection._f = { return EJSON.equals(a, b, {keyOrderSensitive: true}); }, - // if x is not an array, true iff f(x) is true. if x is an array, - // true iff f(y) is true for any y in x. - // - // this is the way most mongo operators (like $gt, $mod, $type..) - // treat their arguments. - _matches: function (x, f) { - if (x instanceof Array) { - for (var i = 0; i < x.length; i++) - if (f(x[i])) - return true; - return false; - } - return f(x); - }, - - // like _matches, but if x is an array, it's true not only if f(y) - // is true for some y in x, but also if f(x) is true. - // - // this is the way mongo value comparisons usually work, like {x: - // 4}, {x: [4]}, or {x: {$in: [1,2,3]}}. - _matches_plus: function (x, f) { - if (x instanceof Array) { - for (var i = 0; i < x.length; i++) - if (f(x[i])) - return true; - // fall through! - } - return f(x); - }, - // maps a type code to a value that can be used to sort values of // different types _typeorder: function (t) { @@ -200,311 +413,129 @@ LocalCollection._matches = function (selector, doc) { return (LocalCollection._compileSelector(selector))(doc); }; +var makeLookupFunction = function (key) { + var dotLocation = key.indexOf('.'); + var first = dotLocation === -1 ? key : key.substr(0, dotLocation); + var lookupRest = dotLocation !== -1 && + makeLookupFunction(key.substr(dotLocation + 1)); + return function (doc) { + if (doc == null) // null or undefined + return undefined; + var firstLevel = doc[first]; + if (lookupRest) + return lookupRest(firstLevel); + return firstLevel; + }; +}; + +// The main compilation function for a given selector. +var compileDocumentSelector = function (docSelector) { + var perKeySelectors = []; + _.each(docSelector, function (subSelector, key) { + if (key.substr(0, 1) === '$') { + // Outer operators are either logical operators (they recurse back into + // this function), or $where. + if (!_.has(LOGICAL_OPERATORS, key)) + throw new Error("Unrecognized logical operator: " + key); + perKeySelectors.push(LOGICAL_OPERATORS[key](subSelector)); + } else { + var lookUpByIndex = makeLookupFunction(key); + var valueSelectorFunc = compileValueSelector(subSelector); + perKeySelectors.push(function (doc) { + return valueSelectorFunc(lookUpByIndex(doc)); + }); + } + }); + + + return function (doc) { + return _.all(perKeySelectors, function (f) { + return f(doc); + }); + }; +}; + // Given a selector, return a function that takes one argument, a // document, and returns true if the document matches the selector, // else false. LocalCollection._compileSelector = function (selector) { - var literals = []; // you can pass a literal function instead of a selector if (selector instanceof Function) return function (doc) {return selector.call(doc);}; // shorthand -- scalars match _id if (LocalCollection._selectorIsId(selector)) - selector = {_id: selector}; + return function (doc) { return 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. + // 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)) return function (doc) {return false;}; - // eval() does not return a value in IE8, nor does the spec say it - // should. Assign to a local to get the value, instead. - var _func; - eval("_func = (function(f,literals){return function(doc){return " + - LocalCollection._exprForSelector(selector, literals) + - ";};})"); - return _func(LocalCollection._f, literals); + // Top level can't be an array or true. + if (typeof(selector) === 'boolean' || _.isArray(selector)) + throw new Error("Invalid selector: " + selector); + + return compileDocumentSelector(selector); }; -// Given an arbitrary Mongo-style query selector, return an expression -// that evaluates to true if the document in 'doc' matches the -// selector, else false. -LocalCollection._exprForSelector = function (selector, literals) { - var clauses = []; - for (var key in selector) { - var value = selector[key]; +// Give a sort spec, which can be in any of these forms: +// {"key1": 1, "key2": -1} +// [["key1", "asc"], ["key2", "desc"]] +// ["key1", ["key2", "desc"]] +// +// (.. with the first form being dependent on the key enumeration +// behavior of your javascript VM, which usually does what you mean in +// this case if the key names don't look like integers ..) +// +// return a function that takes two objects, and returns -1 if the +// first object comes first in order, 1 if the second object comes +// first, or 0 if neither object comes before the other. - if (key.substr(0, 1) === '$') { // no indexing into strings on IE7 - // whole-document predicate like {$or: [{x: 12}, {y: 12}]} - clauses.push(LocalCollection._exprForDocumentPredicate(key, value, literals)); - } else { - // else, it's a constraint on a particular key (or dotted keypath) - clauses.push(LocalCollection._exprForKeypathPredicate(key, value, literals)); - } - }; +LocalCollection._compileSort = function (spec) { + var sortSpecParts = []; - if (clauses.length === 0) return 'true'; // selector === {} - return '(' + clauses.join('&&') +')'; -}; - -// 'op' is a top-level, whole-document predicate from a mongo -// selector, like '$or' in {$or: [{x: 12}, {y: 12}]}. 'value' is its -// value in the selector. Return an expression that evaluates to true -// if 'doc' matches this predicate, else false. -LocalCollection._exprForDocumentPredicate = function (op, value, literals) { - if (op === '$or' || op === '$and' || op === '$nor') { - if (_.isEmpty(value) || !_.isArray(value)) - throw Error("$and/$or/$nor must be a nonempty array"); - } - - var clauses; - if (op === '$or') { - clauses = _.map(value, function (c) { - return LocalCollection._exprForSelector(c, literals); - }); - return '(' + clauses.join('||') +')'; - } - - if (op === '$and') { - clauses = _.map(value, function (c) { - return LocalCollection._exprForSelector(c, literals); - }); - return '(' + clauses.join('&&') +')'; - } - - if (op === '$nor') { - clauses = _.map(value, function (c) { - return "!(" + LocalCollection._exprForSelector(c, literals) + ")"; - }); - return '(' + clauses.join('&&') +')'; - } - - if (op === '$where') { - if (value instanceof Function) { - literals.push(value); - return 'literals[' + (literals.length - 1) + '].call(doc)'; - } - return "(function(){return " + value + ";}).call(doc)"; - } - - throw Error("Unrecognized key in selector: ", op); -} - -// Given a single 'dotted.key.path: value' constraint from a Mongo -// query selector, return an expression that evaluates to true if the -// document in 'doc' matches the constraint, else false. -LocalCollection._exprForKeypathPredicate = function (keypath, value, literals) { - var keyparts = keypath.split('.'); - - // get the inner predicate expression - var predcode = ''; - if (value instanceof RegExp) { - predcode = LocalCollection._exprForOperatorTest(value, literals); - } else if ( !(typeof value === 'object') - || value === null - || value instanceof Array) { - // it's something like {x.y: 12} or {x.y: [12]} - predcode = LocalCollection._exprForValueTest(value, literals); - } else { - // is it a literal document or a bunch of $-expressions? - var is_literal = true; - for (var k in value) { - if (k.substr(0, 1) === '$') { // no indexing into strings on IE7 - is_literal = false; - break; - } - } - - if (is_literal) { - // it's a literal document, like {x.y: {a: 12}} - predcode = LocalCollection._exprForValueTest(value, literals); - } else { - predcode = LocalCollection._exprForOperatorTest(value, literals); - } - } - - // now, deal with the orthogonal concern of dotted.key.paths and the - // (potentially multi-level) array searching they require. - // while at it, make sure to not throw an exception if we hit undefined while - // drilling down through the dotted parts - var ret = ''; - var innermost = true; - var lastPartWasNumber = false; - while (keyparts.length) { - var part = keyparts.pop(); - var thisPartIsNumber = false; - if (/^\d+$/.test(part)) { - part = +part; - thisPartIsNumber = true; - } - var formal = keyparts.length ? "x" : "doc"; - if (innermost) { - ret = '(function(x){return ' + predcode + ';})(' + formal + '&&' + formal + '[' + - JSON.stringify(part) + '])'; - innermost = false; - } else if (lastPartWasNumber) { - // The last part was an array index, so if we find an array here we - // shouldn't search it! - ret = '(function(x){return ' + ret + ';})(' + formal + '&&' + formal + '[' + - JSON.stringify(part) + '])'; - } else { - // If the runtime type is an array, search it, unless we're already at the - // innermost bit, or if the next part is a number (ie, an array index). - ret = 'f._matches(' + formal + '&&' + formal + '[' + JSON.stringify(part) + - '], function(x){return ' + ret + ';})'; - } - lastPartWasNumber = thisPartIsNumber; - } - - return ret; -}; - -// Given a value, return an expression that evaluates to true if the -// value in 'x' matches the value, or else false. This includes -// searching 'x' if it is an array. This doesn't include regular -// expressions (that's because mongo's $not operator works with -// regular expressions but not other kinds of scalar tests.) -LocalCollection._exprForValueTest = function (value, literals) { - var expr; - - if (value === null) { - // null has special semantics - // http://www.mongodb.org/display/DOCS/Querying+and+nulls - expr = 'x===null||x===undefined'; - } else if (typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean') { - // literal scalar value - // XXX object ids, dates, timestamps? - expr = 'x===' + JSON.stringify(value); - } else if (typeof value === 'function') { - // note that typeof(/a/) === 'function' in javascript - // XXX improve error - throw Error("Bad value type in query"); - } else if (value.serializeForEval) { - expr = 'f._equal(x,' + value.serializeForEval() + ')'; - } else { - // array or literal document - expr = 'f._equal(x,' + JSON.stringify(value) + ')'; - } - - return 'f._matches_plus(x,function(x){return ' + expr + ';})'; -}; - -// In a selector like {x: {$gt: 4, $lt: 8}}, we're calling the {$gt: -// 4, $lt: 8} part an "operator." Given an operator, return an -// expression that evaluates to true if the value in 'x' matches the -// operator, or else false. This includes searching 'x' if necessary -// if it's an array. In {x: /a/}, we consider /a/ to be an operator. -LocalCollection._exprForOperatorTest = function (op, literals) { - if (op instanceof RegExp) { - return LocalCollection._exprForOperatorTest({$regex: op}, literals); - } else { - var clauses = []; - for (var type in op) - clauses.push(LocalCollection._exprForConstraint(type, op[type], - op, literals)); - if (clauses.length === 0) - return 'true'; - return '(' + clauses.join('&&') + ')'; - } -}; - -// In an operator like {$gt: 4, $lt: 8}, we call each key/value pair, -// such as $gt: 4, a constraint. Given a constraint and its arguments, -// return an expression that evaluates to true if the value in 'x' -// matches the constraint, or else false. This includes searching 'x' -// if it's an array (and it's appropriate to the constraint.) -LocalCollection._exprForConstraint = function (type, arg, others, - literals) { - var expr; - var search = '_matches'; - var negate = false; - - if (type === '$gt') { - expr = 'f._cmp(x,' + JSON.stringify(arg) + ')>0'; - } else if (type === '$lt') { - expr = 'f._cmp(x,' + JSON.stringify(arg) + ')<0'; - } else if (type === '$gte') { - expr = 'f._cmp(x,' + JSON.stringify(arg) + ')>=0'; - } else if (type === '$lte') { - expr = 'f._cmp(x,' + JSON.stringify(arg) + ')<=0'; - } else if (type === '$all') { - expr = 'f._all(x,' + JSON.stringify(arg) + ')'; - search = null; - } else if (type === '$exists') { - if (arg) - expr = 'x!==undefined'; - else - expr = 'x===undefined'; - search = null; - } else if (type === '$mod') { - expr = 'x%' + JSON.stringify(arg[0]) + '===' + - JSON.stringify(arg[1]); - } else if (type === '$ne') { - if (typeof arg !== "object") - expr = 'x===' + JSON.stringify(arg); - else - expr = 'f._equal(x,' + JSON.stringify(arg) + ')'; - search = '_matches_plus'; - negate = true; // tricky - } else if (type === '$in') { - expr = 'f._in(x,' + JSON.stringify(arg) + ')'; - search = '_matches_plus'; - } else if (type === '$nin') { - expr = 'f._in(x,' + JSON.stringify(arg) + ')'; - search = '_matches_plus'; - negate = true; - } else if (type === '$size') { - expr = '(x instanceof Array)&&x.length===' + arg; - search = null; - } else if (type === '$type') { - // $type: 1 is true for an array if any element in the array is of - // type 1. but an array doesn't have type array unless it contains - // an array.. - expr = 'f._type(x)===' + JSON.stringify(arg); - } else if (type === '$regex') { - // XXX mongo uses PCRE and supports some additional flags: 'x' and - // 's'. javascript doesn't support them. so this is a divergence - // between our behavior and mongo's behavior. ideally we would - // implement x and s by transforming the regexp, but not today.. - if ('$options' in others && /[^gim]/.test(others['$options'])) - throw Error("Only the i, m, and g regexp options are supported"); - expr = 'literals[' + literals.length + '].test(x)'; - if (arg instanceof RegExp) { - if ('$options' in others) { - literals.push(new RegExp(arg.source, others['$options'])); + if (spec instanceof Array) { + for (var i = 0; i < spec.length; i++) { + if (typeof spec[i] === "string") { + sortSpecParts.push({ + lookup: makeLookupFunction(spec[i]), + ascending: true + }); } else { - literals.push(arg); + sortSpecParts.push({ + lookup: makeLookupFunction(spec[i][0]), + ascending: spec[i][1] !== "desc" + }); } - } else { - literals.push(new RegExp(arg, others['$options'])); } - } else if (type === '$options') { - expr = 'true'; - search = null; - } else if (type === '$elemMatch') { - // XXX implement - throw Error("$elemMatch unimplemented"); - } else if (type === '$not') { - // mongo doesn't support $regex inside a $not for some reason. we - // do, because there's no reason not to that I can see.. but maybe - // we should follow mongo's behavior? - expr = '!' + LocalCollection._exprForOperatorTest(arg, literals); - search = null; + } else if (typeof spec === "object") { + for (var key in spec) { + sortSpecParts.push({ + lookup: makeLookupFunction(key), + ascending: spec[key] >= 0 + }); + } } else { - throw Error("Unrecognized key in selector: " + type); + throw Error("Bad sort specification: ", JSON.stringify(spec)); } - if (search) { - expr = 'f.' + search + '(x,function(x){return ' + - expr + ';})'; - } + if (sortSpecParts.length === 0) + return function () {return 0;}; - if (negate) - expr = '!' + expr; - - return expr; + return function (a, b) { + for (var i = 0; i < sortSpecParts.length; ++i) { + var specPart = sortSpecParts[i]; + var aValue = specPart.lookup(a); + var bValue = specPart.lookup(b); + var compare = LocalCollection._f._cmp(aValue, bValue); + if (compare !== 0) + return specPart.ascending ? compare : -compare; + }; + return 0; + }; }; + + +})(); diff --git a/packages/minimongo/sort.js b/packages/minimongo/sort.js deleted file mode 100644 index 94420cf237..0000000000 --- a/packages/minimongo/sort.js +++ /dev/null @@ -1,65 +0,0 @@ -// Give a sort spec, which can be in any of these forms: -// {"key1": 1, "key2": -1} -// [["key1", "asc"], ["key2", "desc"]] -// ["key1", ["key2", "desc"]] -// -// (.. with the first form being dependent on the key enumeration -// behavior of your javascript VM, which usually does what you mean in -// this case if the key names don't look like integers ..) -// -// return a function that takes two objects, and returns -1 if the -// first object comes first in order, 1 if the second object comes -// first, or 0 if neither object comes before the other. - -LocalCollection._compileSort = function (spec) { - var keys = []; - var asc = []; - - if (spec instanceof Array) { - for (var i = 0; i < spec.length; i++) { - if (typeof spec[i] === "string") { - keys.push(spec[i]); - asc.push(true); - } else { - keys.push(spec[i][0]); - asc.push(spec[i][1] !== "desc"); - } - } - } else if (typeof spec === "object") { - for (key in spec) { - keys.push(key); - asc.push(!(spec[key] < 0)); - } - } else { - throw Error("Bad sort specification: ", JSON.stringify(spec)); - } - - if (keys.length === 0) - return function () {return 0;}; - - // eval() does not return a value in IE8, nor does the spec say it - // should. Assign to a local to get the value, instead. - var _func; - var code = "_func = (function(c){return function(a,b){var x;"; - for (var i = 0; i < keys.length; i++) { - // handle dotted subpaths. Make sure to avoid dereferencing - // undefined if a subkey doesn't exist. - var splittedKeys = keys[i].split("."); - var keyString = ""; - var aCode = "a"; - var bCode = "b"; - for(var o = 0; o < splittedKeys.length; o++) { - keyString = keyString + "[" + JSON.stringify(splittedKeys[o]) + "]"; - aCode += '&&a' + keyString; - bCode += '&&b' + keyString; - } - if (i !== 0) { - code += "if(x!==0)return x;"; - } - code += "x=" + (asc[i] ? "" : "-") + - "c(" + aCode + "," + bCode + ");"; - } - code += "return x;};})"; - eval(code); - return _func(LocalCollection._f._cmp); -};