Merge branch 'devel' into release-1.3.3

This commit is contained in:
Ben Newman
2016-05-19 18:34:20 -04:00
15 changed files with 227 additions and 33 deletions

View File

@@ -16,6 +16,11 @@
* Allow using authType in Facebook login [PR #5694](https://github.com/meteor/meteor/pull/5694)
* Adds flush() method to Tracker to force recomputation [PR #4710](https://github.com/meteor/meteor/pull/4710)
* Adds `defineMutationMethods` option (default: true) to `new Mongo.Collection` to override default behavior that sets up mutation methods (/collection/[insert|update...]) [PR #5778](https://github.com/meteor/meteor/pull/5778)
* Allow overridding the default warehouse url by specifying `METEOR_WAREHOUSE_URLBASE` [PR #7054](https://github.com/meteor/meteor/pull/7054)
* Allow `_id` in `$setOnInsert` in Minimongo: https://github.com/meteor/meteor/pull/7066
* Added support for `$eq` to Minimongo: https://github.com/meteor/meteor/pull/4235
* Insert a `Date` header into emails by default: https://github.com/meteor/meteor/pull/6916/files
* DDP callbacks are now batched on the client side. This means that after a DDP message arrives, the local DDP client will batch changes for a minimum of 5ms (configurable via `bufferedWritesInterval`) and a maximum of 500ms (configurable via `bufferedWritesMaxAge`) before calling any callbacks (such as cursor observe callbacks).
@@ -71,16 +76,14 @@
* The `npm-bcrypt` package has been upgraded to use the latest version
(0.8.5) of the `bcrypt` npm package.
* `Match.Optional` only passes if the value is `null` or the specified
type, whereas previously it accepted `undefined`. Use `Match.Maybe` to
allow `undefined`. #6735
* Compiler plugins can call `addJavaScript({ path })` multiple times with
different paths for the same source file, and `module.id` will reflect
this `path` instead of the source path, if they are different. #6806
* Fixed bugs: https://github.com/meteor/meteor/milestones/Release%201.3.2
* Fixed unintended change to `Match.Optional` which caused it to behave the same as the new `Match.Maybe` and incorrectly matching `null` where it previously would not have allowed it. #6735
## v1.3.1
* Long isopacket node_modules paths have been shortened, fixing upgrade
@@ -303,6 +306,10 @@
* Improve automatic blocking of URLs in attribute values to also
include `vbscript:` URLs.
### Check
* Introduced new matcher `Match.Maybe(type)` which will also match (permit) `null` in addition to `undefined`. This is a suggested replacement (where appropriate) for `Match.Optional` which did not permit `null`. This prevents the need to use `Match.OneOf(null, undefined, type)`. #6220
### Testing
* Packages can now be marked as `testOnly` to only run as part of app

View File

@@ -70,8 +70,9 @@ CollectionPrototype.deny = function(options) {
addValidator(this, 'deny', options);
};
CollectionPrototype._defineMutationMethods = function() {
CollectionPrototype._defineMutationMethods = function(options) {
const self = this;
options = options || {};
// set to true once we call any allow or deny methods. If true, use
// allow/deny semantics. If false, use insecure mode semantics.
@@ -99,12 +100,26 @@ CollectionPrototype._defineMutationMethods = function() {
// "Meteor:Mongo:insert/NAME"?
self._prefix = '/' + self._name + '/';
// mutation methods
if (self._connection) {
// Mutation Methods
// Minimongo on the server gets no stubs; instead, by default
// it wait()s until its result is ready, yielding.
// This matches the behavior of macromongo on the server better.
// XXX see #MeteorServerNull
if (self._connection && (self._connection === Meteor.server || Meteor.isClient)) {
const m = {};
_.each(['insert', 'update', 'remove'], function (method) {
m[self._prefix + method] = function (/* ... */) {
const methodName = self._prefix + method;
if (options.useExisting) {
const handlerPropName = Meteor.isClient ? '_methodHandlers' : 'method_handlers';
// Do not try to create additional methods if this has already been called.
// (Otherwise the .methods() call below will throw an error.)
if (self._connection[handlerPropName] &&
typeof self._connection[handlerPropName][methodName] === 'function') return;
}
m[methodName] = function (/* ... */) {
// All the methods do their own validation, instead of using check().
check(arguments, [Match.Any]);
const args = _.toArray(arguments);
@@ -183,12 +198,8 @@ CollectionPrototype._defineMutationMethods = function() {
}
};
});
// Minimongo on the server gets no stubs; instead, by default
// it wait()s until its result is ready, yielding.
// This matches the behavior of macromongo on the server better.
// XXX see #MeteorServerNull
if (Meteor.isClient || self._connection === Meteor.server)
self._connection.methods(m);
self._connection.methods(m);
}
};

View File

@@ -178,6 +178,10 @@ Email.send = function (options) {
mc.addHeader(name, value);
});
if (!options.headers.hasOwnProperty('Date')) {
mc.addHeader('Date', new Date().toUTCString().replace(/GMT/, '+0000'));
}
_.each(options.attachments, function(attachment){
mc.addAttachment(attachment);
});

View File

@@ -13,7 +13,10 @@ Tinytest.add("email - dev mode smoke test", function (test) {
cc: ["friends@example.com", "enemies@example.com"],
subject: "This is the subject",
text: "This is the body\nof the message\nFrom us.",
headers: {'X-Meteor-Test': 'a custom header'}
headers: {
'X-Meteor-Test': 'a custom header',
'Date': 'dummy',
},
});
// XXX brittle if mailcomposer changes header order, etc
test.equal(stream.getContentsAsString("utf8"),
@@ -22,6 +25,7 @@ Tinytest.add("email - dev mode smoke test", function (test) {
"environment variable.)\n" +
"MIME-Version: 1.0\r\n" +
"X-Meteor-Test: a custom header\r\n" +
"Date: dummy\r\n" +
"From: foo@example.com\r\n" +
"To: bar@example.com\r\n" +
"Cc: friends@example.com, enemies@example.com\r\n" +
@@ -52,6 +56,20 @@ Tinytest.add("email - dev mode smoke test", function (test) {
"\r\n" +
"body\r\n" +
"====== END MAIL #1 ======\n");
// Test if date header is automaticall generated, if not specified
Email.send({
from: "foo@example.com",
to: "bar@example.com",
subject: "This is the subject",
text: "This is the body\nof the message\nFrom us.",
headers: {
'X-Meteor-Test': 'a custom header',
},
});
test.matches(stream.getContentsAsString("utf8"),
/^Date: .+$/m);
} finally {
EmailTest.restoreOutputStream();
}

View File

@@ -527,6 +527,10 @@ Tinytest.add("minimongo - sorter and projection combination", function (test) {
// XXX this test should be F, but since it is so hard to be precise in
// floating point math, the current implementation falls back to T
T({ a: { $gt: 9.999999999999999, $lt: 10 }, x: 1 }, { $set: { x: 1 } }, "very close $gt and $lt");
T({ a: { $eq: 5 } }, { $set: { a: 5 } }, "set of $eq");
T({ a: { $eq: 5 }, b: { $eq: 7 } }, { $set: { a: 5 } }, "set of $eq with other $eq");
F({ a: { $eq: 5 } }, { $set: { a: 4 } }, "set below of $eq");
F({ a: { $eq: 5 } }, { $set: { a: 6 } }, "set above of $eq");
T({ a: { $ne: 5 } }, { $unset: { a: 1 } }, "unset of $ne");
T({ a: { $ne: 5 } }, { $set: { a: 1 } }, "set of $ne");
T({ a: { $ne: "some string" }, x: 1 }, { $set: { x: 1 } }, "$ne dummy");
@@ -549,6 +553,11 @@ Tinytest.add("minimongo - sorter and projection combination", function (test) {
Tinytest.add("minimongo - can selector become true by modifier - $-nonscalar selectors and simple tests", function (t) {
test = t;
T({ a: { $eq: { x: 5 } } }, { $set: { 'a.x': 5 } }, "set of $eq");
// XXX this test should be F, but it is not implemented yet
T({ a: { $eq: { x: 5 } } }, { $set: { 'a.x': 4 } }, "set of $eq");
// XXX this test should be F, but it is not implemented yet
T({ a: { $eq: { x: 5 } } }, { $set: { 'a.y': 4 } }, "set of $eq");
T({ a: { $ne: { x: 5 } } }, { $set: { 'a.x': 3 } }, "set of $ne");
// XXX this test should be F, but it is not implemented yet
T({ a: { $ne: { x: 5 } } }, { $set: { 'a.x': 5 } }, "set of $ne");
@@ -560,4 +569,3 @@ Tinytest.add("minimongo - sorter and projection combination", function (test) {
T({ a: { $ne: { a: 2 } } }, { $set: { a: { a: 2 } } }, "$ne object");
});
})();

View File

@@ -510,6 +510,23 @@ Tinytest.add("minimongo - selector_compiler", function (test) {
});
});
// $eq
nomatch({a: {$eq: 1}}, {a: 2});
match({a: {$eq: 2}}, {a: 2});
nomatch({a: {$eq: [1]}}, {a: [2]});
match({a: {$eq: [1, 2]}}, {a: [1, 2]});
match({a: {$eq: 1}}, {a: [1, 2]});
match({a: {$eq: 2}}, {a: [1, 2]});
nomatch({a: {$eq: 3}}, {a: [1, 2]});
match({'a.b': {$eq: 1}}, {a: [{b: 1}, {b: 2}]});
match({'a.b': {$eq: 2}}, {a: [{b: 1}, {b: 2}]});
nomatch({'a.b': {$eq: 3}}, {a: [{b: 1}, {b: 2}]});
match({a: {$eq: {x: 1}}}, {a: {x: 1}});
nomatch({a: {$eq: {x: 1}}}, {a: {x: 2}});
nomatch({a: {$eq: {x: 1}}}, {a: {x: 1, y: 2}});
// $ne
match({a: {$ne: 1}}, {a: 2});
nomatch({a: {$ne: 2}}, {a: 2});
@@ -2085,6 +2102,23 @@ Tinytest.add("minimongo - modify", function (test) {
exceptionWithQuery(doc, {}, mod);
};
var upsert = function (query, mod, expected) {
var coll = new LocalCollection;
var result = coll.upsert(query, mod);
var actual = coll.findOne();
if (expected._id) {
test.equal(result.insertedId, expected._id);
}
else {
delete actual._id;
}
test.equal(actual, expected);
};
// document replacement
modify({}, {}, {});
modify({a: 12}, {}, {}); // tested against mongodb
@@ -2406,6 +2440,13 @@ Tinytest.add("minimongo - modify", function (test) {
exception({}, {$rename: {'a.b': 'a.b'}});
modify({a: 12, b: 13}, {$rename: {a: 'b'}}, {b: 12});
// $setOnInsert
modify({a: 0}, {$setOnInsert: {a: 12}}, {a: 0});
upsert({a: 12}, {$setOnInsert: {b: 12}}, {a: 12, b: 12});
upsert({a: 12}, {$setOnInsert: {_id: 'test'}}, {_id: 'test', a: 12});
exception({}, {$set: {_id: 'bad'}});
// $bit
// unimplemented

View File

@@ -49,7 +49,7 @@ LocalCollection._modify = function (doc, mod, options) {
throw MinimongoError("An empty update path is not valid.");
}
if (keypath === '_id') {
if (keypath === '_id' && op !== '$setOnInsert') {
throw MinimongoError("Mod on _id not allowed");
}

View File

@@ -255,15 +255,13 @@ var operatorBranchedMatcher = function (valueSelector, matcher, isRoot) {
var operatorMatchers = [];
_.each(valueSelector, function (operand, operator) {
// XXX we should actually implement $eq, which is new in 2.6
var simpleRange = _.contains(['$lt', '$lte', '$gt', '$gte'], operator) &&
_.isNumber(operand);
var simpleInequality = operator === '$ne' && !_.isObject(operand);
var simpleEquality = _.contains(['$ne', '$eq'], operator) && !_.isObject(operand);
var simpleInclusion = _.contains(['$in', '$nin'], operator) &&
_.isArray(operand) && !_.any(operand, _.isObject);
if (! (operator === '$eq' || simpleRange ||
simpleInclusion || simpleInequality)) {
if (! (simpleRange || simpleInclusion || simpleEquality)) {
matcher._isSimple = false;
}
@@ -380,6 +378,10 @@ var invertBranchedMatcher = function (branchedMatcher) {
// "match each branched value independently and combine with
// convertElementMatcherToBranchedMatcher".
var VALUE_OPERATORS = {
$eq: function (operand) {
return convertElementMatcherToBranchedMatcher(
equalityElementMatcher(operand));
},
$not: function (operand, valueSelector, matcher) {
return invertBranchedMatcher(compileValueSelector(operand, matcher));
},

View File

@@ -141,7 +141,9 @@ Minimongo.Matcher.prototype.matchingDocument = function () {
// if there is a strict equality, there is a good
// chance we can use one of those as "matching"
// dummy value
if (valueSelector.$in) {
if (valueSelector.$eq) {
return valueSelector.$eq;
} else if (valueSelector.$in) {
var matcher = new Minimongo.Matcher({ placeholder: valueSelector });
// Return anything from $in that matches the whole selector for this
@@ -168,7 +170,7 @@ Minimongo.Matcher.prototype.matchingDocument = function () {
fallback = true;
return middle;
} else if (onlyContainsKeys(valueSelector, ['$nin',' $ne'])) {
} else if (onlyContainsKeys(valueSelector, ['$nin', '$ne'])) {
// Since self._isSimple makes sure $nin and $ne are not combined with
// objects or arrays, we can confidently return an empty object as it
// never matches any scalar.
@@ -217,4 +219,3 @@ var startsWith = function(str, starts) {
return str.length >= starts.length &&
str.substring(0, starts.length) === starts;
};

View File

@@ -22,6 +22,7 @@ Mongo = {};
The default id generation technique is `'STRING'`.
* @param {Function} options.transform An optional transformation function. Documents will be passed through this function before being returned from `fetch` or `findOne`, and before being passed to callbacks of `observe`, `map`, `forEach`, `allow`, and `deny`. Transforms are *not* applied for the callbacks of `observeChanges` or to cursors returned from publish functions.
* @param {Boolean} options.defineMutationMethods Set to `false` to skip setting up the mutation methods that enable insert/update/remove from client code. Default `true`.
*/
Mongo.Collection = function (name, options) {
var self = this;
@@ -209,21 +210,41 @@ Mongo.Collection = function (name, options) {
getDoc: function(id) {
return self.findOne(id);
},
// To be able to get back to the collection from the store.
_getCollection: function () {
return self;
}
});
if (!ok)
throw new Error("There is already a collection named '" + name + "'");
if (!ok) {
const message = `There is already a collection named "${name}"`;
if (options._suppressSameNameError === true) {
// XXX In theory we do not have to throw when `ok` is falsy. The store is already defined
// for this collection name, but this will simply be another reference to it and everything
// should work. However, we have historically thrown an error here, so for now we will
// skip the error only when `_suppressSameNameError` is `true`, allowing people to opt in
// and give this some real world testing.
console.warn ? console.warn(message) : console.log(message);
} else {
throw new Error(message);
}
}
}
// XXX don't define these until allow or deny is actually used for this
// collection. Could be hard if the security rules are only defined on the
// server.
self._defineMutationMethods();
if (options.defineMutationMethods !== false) {
try {
self._defineMutationMethods({ useExisting: (options._suppressSameNameError === true) });
} catch (error) {
// Throw a more understandable error on the server for same collection name
if (error.message === `A method named '/${name}/insert' is already defined`)
throw new Error(`There is already a collection named "${name}"`);
throw error;
}
}
// autopublish
if (Package.autopublish && !options._preventAutopublish && self._connection

View File

@@ -9,3 +9,46 @@ Tinytest.add(
);
}
);
Tinytest.add('collection - call new Mongo.Collection multiple times',
function (test) {
var collectionName = 'multiple_times_1_' + test.id;
new Mongo.Collection(collectionName);
test.throws(
function () {
new Mongo.Collection(collectionName);
},
/There is already a collection named/
);
}
);
Tinytest.add('collection - call new Mongo.Collection multiple times with _suppressSameNameError=true',
function (test) {
var collectionName = 'multiple_times_2_' + test.id;
new Mongo.Collection(collectionName);
try {
new Mongo.Collection(collectionName, {_suppressSameNameError: true});
test.ok();
} catch (error) {
console.log(error);
test.fail('Expected new Mongo.Collection not to throw an error when called twice with the same name');
}
}
);
Tinytest.add('collection - call new Mongo.Collection with defineMutationMethods=false',
function (test) {
var handlerPropName = Meteor.isClient ? '_methodHandlers' : 'method_handlers';
var methodCollectionName = 'hasmethods' + test.id;
var hasmethods = new Mongo.Collection(methodCollectionName);
test.equal(typeof hasmethods._connection[handlerPropName]['/' + methodCollectionName + '/insert'], 'function');
var noMethodCollectionName = 'nomethods' + test.id;
var nomethods = new Mongo.Collection(noMethodCollectionName, {defineMutationMethods: false});
test.equal(nomethods._connection[handlerPropName]['/' + noMethodCollectionName + '/insert'], undefined);
}
);

View File

@@ -16,6 +16,18 @@ Tinytest.add('ReactiveDict - setDefault', function (test) {
dict.setDefault('D', undefined);
test.equal(dict.all(), {A: 'blah', B: undefined,
C: 'default', D: undefined});
dict = new ReactiveDict;
dict.set('A', 'blah');
dict.set('B', undefined);
dict.setDefault({
A: 'default',
B: 'defualt',
C: 'default',
D: undefined
});
test.equal(dict.all(), {A: 'blah', B: undefined,
C: 'default', D: undefined});
});
Tinytest.add('ReactiveDict - all() works', function (test) {

View File

@@ -79,8 +79,18 @@ _.extend(ReactiveDict.prototype, {
}
},
setDefault: function (key, value) {
setDefault: function (keyOrObject, value) {
var self = this;
if ((typeof keyOrObject === 'object') && (value === undefined)) {
// Called as `dict.setDefault({...})`
self._setDefaultObject(keyOrObject);
return;
}
// the input isn't an object, so it must be a key
// and we resume with the rest of the function
var key = keyOrObject;
if (! _.has(self.keys, key)) {
self.set(key, value);
}
@@ -198,6 +208,14 @@ _.extend(ReactiveDict.prototype, {
});
},
_setDefaultObject: function (object) {
var self = this;
_.each(object, function (value, key){
self.setDefault(key, value);
});
},
_ensureKey: function (key) {
var self = this;
if (!(key in self.keyDeps)) {

View File

@@ -43,7 +43,9 @@ var files = require('../fs/files.js');
var httpHelpers = require('../utils/http-helpers.js');
var fiberHelpers = require('../utils/fiber-helpers.js');
var WAREHOUSE_URLBASE = 'https://warehouse.meteor.com';
// Use `METEOR_WAREHOUSE_URLBASE` to override the default warehouse
// url base.
var WAREHOUSE_URLBASE = process.env.METEOR_WAREHOUSE_URLBASE || 'https://warehouse.meteor.com';
var warehouse = exports;
_.extend(warehouse, {

View File

@@ -13,6 +13,7 @@ var archinfo = require('../utils/archinfo.js');
var config = require('../meteor-services/config.js');
var buildmessage = require('../utils/buildmessage.js');
var execFileSync = require('../utils/processes.js').execFileSync;
var Builder = require('../isobuild/builder.js').default;
var catalog = require('../packaging/catalog/catalog.js');
var catalogRemote = require('../packaging/catalog/catalog-remote.js');
@@ -836,9 +837,14 @@ _.extend(Sandbox.prototype, {
var serverUrl = self.env.METEOR_PACKAGE_SERVER_URL;
var packagesDirectoryName = config.getPackagesDirectoryName(serverUrl);
files.cp_r(files.pathJoin(builtPackageTropohouseDir, 'packages'),
files.pathJoin(self.warehouse, packagesDirectoryName),
{ preserveSymlinks: true });
var builder = new Builder({outputPath: self.warehouse});
builder.copyDirectory({
from: files.pathJoin(builtPackageTropohouseDir, 'packages'),
to: packagesDirectoryName,
symlink: true
});
builder.complete();
var stubCatalog = {
syncToken: {},