diff --git a/History.md b/History.md index e526cb2bc4..3c9f001b04 100644 --- a/History.md +++ b/History.md @@ -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 diff --git a/packages/allow-deny/allow-deny.js b/packages/allow-deny/allow-deny.js index b8cf8159dc..819a4dfc59 100644 --- a/packages/allow-deny/allow-deny.js +++ b/packages/allow-deny/allow-deny.js @@ -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); } }; diff --git a/packages/email/email.js b/packages/email/email.js index d4eda67e75..82bd8d62a0 100644 --- a/packages/email/email.js +++ b/packages/email/email.js @@ -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); }); diff --git a/packages/email/email_tests.js b/packages/email/email_tests.js index 286440232c..c5faade643 100644 --- a/packages/email/email_tests.js +++ b/packages/email/email_tests.js @@ -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(); } diff --git a/packages/minimongo/minimongo_server_tests.js b/packages/minimongo/minimongo_server_tests.js index 5443659567..38cc1476da 100644 --- a/packages/minimongo/minimongo_server_tests.js +++ b/packages/minimongo/minimongo_server_tests.js @@ -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"); }); })(); - diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 4be1670fbb..5a77df7652 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -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 diff --git a/packages/minimongo/modify.js b/packages/minimongo/modify.js index 3b49ff0443..348e31a51e 100644 --- a/packages/minimongo/modify.js +++ b/packages/minimongo/modify.js @@ -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"); } diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 145487be82..4e80e57d32 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -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)); }, diff --git a/packages/minimongo/selector_modifier.js b/packages/minimongo/selector_modifier.js index 7ea8ed36c5..54b37df68b 100644 --- a/packages/minimongo/selector_modifier.js +++ b/packages/minimongo/selector_modifier.js @@ -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; }; - diff --git a/packages/mongo/collection.js b/packages/mongo/collection.js index 98d621a7e5..6a588ac64e 100644 --- a/packages/mongo/collection.js +++ b/packages/mongo/collection.js @@ -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 diff --git a/packages/mongo/collection_tests.js b/packages/mongo/collection_tests.js index 2e10e449d8..e7b709e861 100644 --- a/packages/mongo/collection_tests.js +++ b/packages/mongo/collection_tests.js @@ -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); + } +); diff --git a/packages/reactive-dict/reactive-dict-tests.js b/packages/reactive-dict/reactive-dict-tests.js index 7c845cd0f4..f67530d011 100644 --- a/packages/reactive-dict/reactive-dict-tests.js +++ b/packages/reactive-dict/reactive-dict-tests.js @@ -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) { diff --git a/packages/reactive-dict/reactive-dict.js b/packages/reactive-dict/reactive-dict.js index a807898c1b..a4d4592a54 100644 --- a/packages/reactive-dict/reactive-dict.js +++ b/packages/reactive-dict/reactive-dict.js @@ -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)) { diff --git a/tools/packaging/warehouse.js b/tools/packaging/warehouse.js index 585090d0d1..e17f33d4d7 100644 --- a/tools/packaging/warehouse.js +++ b/tools/packaging/warehouse.js @@ -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, { diff --git a/tools/tool-testing/selftest.js b/tools/tool-testing/selftest.js index 1f6b1d41ed..bad7286aa1 100644 --- a/tools/tool-testing/selftest.js +++ b/tools/tool-testing/selftest.js @@ -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: {},