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);
-};