diff --git a/docs/history.md b/docs/history.md index 2c41408d65..e68b7c6f98 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,3 +1,33 @@ +## v3.0, TBD + +### Highlights + +#### Breaking Changes + +* `email`: + `Email.send` is no longer available. Use `Email.sendAsync` instead. + +* `accounts-password`: + - `Accounts.sendResetPasswordEmail` is now async + - `Accounts.sendEnrollmentEmail` is now async + - `Accounts.sendVerificationEmail` is now async + +* `accounts-passwordless`: + - `Accounts.sendLoginTokenEmail` is now async + +#### Internal API changes + + +#### Migration Steps + +You can follow in [here](https://guide.meteor.com/3.0-migration.html). + +#### Meteor Version Release + +#### Special thanks to + +For making this great framework even better! + ## v2.9, 2022-12-12 ### Highlights diff --git a/docs/source/commandline.md b/docs/source/commandline.md index ebb9381d3a..1be73f5058 100644 --- a/docs/source/commandline.md +++ b/docs/source/commandline.md @@ -203,6 +203,10 @@ Create a basic [Solid](https://www.solidjs.com/) app. you what is the name of the model you want to generate, if you do want methods for your api and publications. It can be used as a command line only operation as well. +> _Important to note:_ +> By default, the generator will use JavaScript but if it detects that you have a +``tsconfig.json`` file in your project, it will use TypeScript instead. + running ```bash meteor generate customer diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 5127342209..6b6a8d59f6 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -368,7 +368,7 @@ const pluckAddresses = (emails = []) => emails.map(email => email.address); // Method called by a user to request a password reset email. This is // the start of the reset process. -Meteor.methods({forgotPassword: options => { +Meteor.methods({forgotPassword: async options => { check(options, {email: String}) const user = Accounts.findUserByEmail(options.email, { fields: { emails: 1 } }); @@ -382,7 +382,7 @@ Meteor.methods({forgotPassword: options => { email => email.toLowerCase() === options.email.toLowerCase() ); - Accounts.sendResetPasswordEmail(user._id, caseSensitiveEmail); + await Accounts.sendResetPasswordEmail(user._id, caseSensitiveEmail); }}); /** @@ -532,12 +532,12 @@ Accounts.generateVerificationToken = (userId, email, extraTokenData) => { * @returns {Object} Object with {email, user, token, url, options} values. * @importFromPackage accounts-base */ -Accounts.sendResetPasswordEmail = (userId, email, extraTokenData, extraParams) => { +Accounts.sendResetPasswordEmail = async (userId, email, extraTokenData, extraParams) => { const {email: realEmail, user, token} = Accounts.generateResetToken(userId, email, 'resetPassword', extraTokenData); const url = Accounts.urls.resetPassword(token, extraParams); const options = Accounts.generateOptionsForEmail(realEmail, user, url, 'resetPassword'); - Email.send(options); + await Email.sendAsync(options); if (Meteor.isDevelopment) { console.log(`\nReset password URL: ${url}`); } @@ -562,12 +562,12 @@ Accounts.sendResetPasswordEmail = (userId, email, extraTokenData, extraParams) = * @returns {Object} Object with {email, user, token, url, options} values. * @importFromPackage accounts-base */ -Accounts.sendEnrollmentEmail = (userId, email, extraTokenData, extraParams) => { +Accounts.sendEnrollmentEmail = async (userId, email, extraTokenData, extraParams) => { const {email: realEmail, user, token} = Accounts.generateResetToken(userId, email, 'enrollAccount', extraTokenData); const url = Accounts.urls.enrollAccount(token, extraParams); const options = Accounts.generateOptionsForEmail(realEmail, user, url, 'enrollAccount'); - Email.send(options); + await Email.sendAsync(options); if (Meteor.isDevelopment) { console.log(`\nEnrollment email URL: ${url}`); } @@ -711,7 +711,7 @@ Meteor.methods({resetPassword: async function (...args) { * @returns {Object} Object with {email, user, token, url, options} values. * @importFromPackage accounts-base */ -Accounts.sendVerificationEmail = (userId, email, extraTokenData, extraParams) => { +Accounts.sendVerificationEmail = async (userId, email, extraTokenData, extraParams) => { // XXX Also generate a link using which someone can delete this // account if they own said address but weren't those who created // this account. @@ -720,7 +720,7 @@ Accounts.sendVerificationEmail = (userId, email, extraTokenData, extraParams) => Accounts.generateVerificationToken(userId, email, extraTokenData); const url = Accounts.urls.verifyEmail(token, extraParams); const options = Accounts.generateOptionsForEmail(realEmail, user, url, 'verifyEmail'); - Email.send(options); + await Email.sendAsync(options); if (Meteor.isDevelopment) { console.log(`\nVerification email URL: ${url}`); } @@ -979,9 +979,9 @@ Accounts.createUserVerifyingEmail = async (options) => { // that address. if (options.email && Accounts._options.sendVerificationEmail) { if (options.password) { - Accounts.sendVerificationEmail(userId, options.email); + await Accounts.sendVerificationEmail(userId, options.email); } else { - Accounts.sendEnrollmentEmail(userId, options.email); + await Accounts.sendEnrollmentEmail(userId, options.email); } } diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index 382867926a..55f3f46222 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -110,7 +110,7 @@ function generateSequence() { } Meteor.methods({ - requestLoginTokenForUser: ({ selector, userData, options = {} }) => { + requestLoginTokenForUser: async ({ selector, userData, options = {} }) => { let user = Accounts._findUserByQuery(selector, { fields: { emails: 1 }, }); @@ -189,14 +189,15 @@ Meteor.methods({ : true; if (shouldSendLoginTokenEmail) { - tokens.forEach(({ email, sequence }) => { + const sendLogins = tokens.map(({ email, sequence }) => Accounts.sendLoginTokenEmail({ userId: user._id, sequence, email, ...(options.extra ? { extra: options.extra } : {}), - }); - }); + }) + ); + await Promise.all(sendLogins); } return result; @@ -213,7 +214,7 @@ Meteor.methods({ * @param {Object} options.extra Optional. Extra properties * @returns {Object} Object with {email, user, token, url, options} values. */ -Accounts.sendLoginTokenEmail = ({ userId, sequence, email, extra = {} }) => { +Accounts.sendLoginTokenEmail = async ({ userId, sequence, email, extra = {} }) => { const user = getUserById(userId); const url = Accounts.urls.loginToken(email, sequence); const options = Accounts.generateOptionsForEmail( @@ -223,7 +224,7 @@ Accounts.sendLoginTokenEmail = ({ userId, sequence, email, extra = {} }) => { 'sendLoginToken', { ...extra, sequence } ); - Email.send({ ...options, extra }); + await Email.sendAsync({ ...options, extra }); if (Meteor.isDevelopment) { console.log(`\nLogin Token url: ${url}`); } diff --git a/packages/deprecated/http/httpcall_server.js b/packages/deprecated/http/httpcall_server.js index 0413d0bdba..9ed4549c5f 100644 --- a/packages/deprecated/http/httpcall_server.js +++ b/packages/deprecated/http/httpcall_server.js @@ -161,4 +161,5 @@ function _call (method, url, options, callback) { .catch(err => callback(err)); } +// we are keeping wrapAsync here as this package is deprecated HTTP.call = Meteor.wrapAsync(_call); diff --git a/packages/email/email.d.ts b/packages/email/email.d.ts index 71380d328e..a34ce0a17d 100644 --- a/packages/email/email.d.ts +++ b/packages/email/email.d.ts @@ -17,7 +17,9 @@ export namespace Email { packageSettings?: unknown; } + /** @deprecated */ function send(options: EmailOptions): void; + function sendAsync(options: EmailOptions): Promise; function hookSend(fn: (options: EmailOptions) => boolean): void; function customTransport(fn: (options: CustomEmailOptions) => void): void; } diff --git a/packages/email/email.js b/packages/email/email.js index eed8dbd9b3..bbdf0dab39 100644 --- a/packages/email/email.js +++ b/packages/email/email.js @@ -24,7 +24,7 @@ export const EmailInternals = { const MailComposer = EmailInternals.NpmModules.mailcomposer.module; -const makeTransport = function (mailUrlString) { +const makeTransport = async function (mailUrlString) { const mailUrl = new URL(mailUrlString); if (mailUrl.protocol !== 'smtp:' && mailUrl.protocol !== 'smtps:') { @@ -52,10 +52,7 @@ const makeTransport = function (mailUrlString) { mailUrl.query.pool = 'true'; } - const transport = nodemailer.createTransport(url.format(mailUrl)); - - transport._syncSendMail = Meteor.wrapAsync(transport.sendMail, transport); - return transport; + return nodemailer.createTransport(url.format(mailUrl)); }; // More info: https://nodemailer.com/smtp/well-known/ @@ -96,20 +93,17 @@ const knownHostsTransport = function (settings = undefined, url = undefined) { ); } - const transport = nodemailer.createTransport({ + return nodemailer.createTransport({ service: settings?.service || service, auth: { user: settings?.user || user, pass: settings?.password || password, }, }); - - transport._syncSendMail = Meteor.wrapAsync(transport.sendMail, transport); - return transport; }; EmailTest.knowHostsTransport = knownHostsTransport; -const getTransport = function () { +const getTransport = async function () { const packageSettings = Meteor.settings.packages?.email || {}; // We delay this check until the first call to Email.send, in case someone // set process.env.MAIL_URL in startup code. Then we store in a cache until @@ -130,7 +124,7 @@ const getTransport = function () { this.cache = knownHostsTransport(packageSettings, url); } else { this.cacheKey = url; - this.cache = url ? makeTransport(url, packageSettings) : null; + this.cache = url ? await makeTransport(url, packageSettings) : null; } } return this.cache; @@ -170,10 +164,6 @@ const devModeSendAsync = function (mail, options) { }); }; -const smtpSend = function (transport, mail) { - transport._syncSendMail(mail); -}; - const sendHooks = new Hook(); /** @@ -198,58 +188,6 @@ Email.hookSend = function (f) { */ Email.customTransport = undefined; -/** - * @summary Send an email. Throws an `Error` on failure to contact mail server - * or if mail server returns an error. All fields should match - * [RFC5322](http://tools.ietf.org/html/rfc5322) specification. - * - * If the `MAIL_URL` environment variable is set, actually sends the email. - * Otherwise, prints the contents of the email to standard out. - * - * Note that this package is based on **nodemailer**, so make sure to refer to - * [the documentation](http://nodemailer.com/) - * when using the `attachments` or `mailComposer` options. - * - * @locus Server - * @param {Object} options - * @param {String} [options.from] "From:" address (required) - * @param {String|String[]} options.to,cc,bcc,replyTo - * "To:", "Cc:", "Bcc:", and "Reply-To:" addresses - * @param {String} [options.inReplyTo] Message-ID this message is replying to - * @param {String|String[]} [options.references] Array (or space-separated string) of Message-IDs to refer to - * @param {String} [options.messageId] Message-ID for this message; otherwise, will be set to a random value - * @param {String} [options.subject] "Subject:" line - * @param {String} [options.text|html] Mail body (in plain text and/or HTML) - * @param {String} [options.watchHtml] Mail body in HTML specific for Apple Watch - * @param {String} [options.icalEvent] iCalendar event attachment - * @param {Object} [options.headers] Dictionary of custom headers - e.g. `{ "header name": "header value" }`. To set an object under a header name, use `JSON.stringify` - e.g. `{ "header name": JSON.stringify({ tracking: { level: 'full' } }) }`. - * @param {Object[]} [options.attachments] Array of attachment objects, as - * described in the [nodemailer documentation](https://nodemailer.com/message/attachments/). - * @param {MailComposer} [options.mailComposer] A [MailComposer](https://nodemailer.com/extras/mailcomposer/#e-mail-message-fields) - * object representing the message to be sent. Overrides all other options. - * You can create a `MailComposer` object via - * `new EmailInternals.NpmModules.mailcomposer.module`. - */ -Email.send = function (options) { - if (Email.customTransport) { - // Preserve current behavior - const email = options.mailComposer ? options.mailComposer.mail : options; - let send = true; - sendHooks.forEach((hook) => { - send = hook(email); - return send; - }); - if (!send) { - return; - } - const packageSettings = Meteor.settings.packages?.email || {}; - Email.customTransport({ packageSettings, ...email }); - return; - } - // Using Fibers Promise.await - return Promise.await(Email.sendAsync(options)); -}; - /** * @summary Send an email with asyncronous method. Capture Throws an `Error` on failure to contact mail server * or if mail server returns an error. All fields should match @@ -284,12 +222,11 @@ Email.send = function (options) { * `new EmailInternals.NpmModules.mailcomposer.module`. */ Email.sendAsync = async function (options) { - const email = options.mailComposer ? options.mailComposer.mail : options; let send = true; - sendHooks.forEach((hook) => { - send = hook(email); + await sendHooks.forEachAsync(async (sendHook) => { + send = await sendHook(email); return send; }); if (!send) { @@ -313,9 +250,28 @@ Email.sendAsync = async function (options) { } if (mailUrlEnv || mailUrlSettings) { - const transport = getTransport(); - smtpSend(transport, email); + const transport = await getTransport(); + await transport.sendMail(email); return; } return devModeSendAsync(email, options); }; + +/** + * @deprecated use Email.sendAsync + * @param options + */ +Email.send = function(options) { + Email.sendAsync(options) + .then(() => + console.warn( + `Email.send is no longer recommended, you should use Email.sendAsync` + ) + ) + .catch(e => + console.error( + `Email.send is no longer recommended and an error happened`, + e + ) + ); +}; diff --git a/packages/email/email_tests.js b/packages/email/email_tests.js index 6f016f26b9..585d4b9990 100644 --- a/packages/email/email_tests.js +++ b/packages/email/email_tests.js @@ -10,22 +10,9 @@ const sleep = (ms) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; -// Create dynamic sync tests -TEST_CASES.forEach(({ title, options, testCalls }) => { - Tinytest.add(`[Sync] ${title}`, function (test) { - smokeEmailTest((stream) => { - Object.entries(options).forEach(([key, option]) => { - const testCall = testCalls[key]; - Email.send({ ...option, stream }); - testCall(test, stream); - }); - }); - }); -}); - // Create dynamic async tests TEST_CASES.forEach(({ title, options, testCalls }) => { - Tinytest.addAsync(`[Async] ${title}`, function (test, onComplete) { + Tinytest.addAsync(`${title}`, function (test, onComplete) { smokeEmailTest((stream) => { const allPromises = Object.entries(options).map(([key, option]) => { const testCall = testCalls[key]; @@ -38,82 +25,10 @@ TEST_CASES.forEach(({ title, options, testCalls }) => { }); }); -// Individual sync tests - -Tinytest.add( - '[Sync] email - alternate API is used for sending gets data', - function (test) { - smokeEmailTest(function (stream) { - Email.customTransport = (options) => { - test.equal(options.from, 'foo@example.com'); - }; - Email.send({ - from: 'foo@example.com', - to: 'bar@example.com', - text: '*Cool*, man', - html: 'Cool, man', - stream, - }); - test.equal(stream.getContentsAsString('utf8'), false); - }); - - smokeEmailTest(function (stream) { - Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS; - Email.customTransport = (options) => { - test.equal(options.from, 'foo@example.com'); - test.equal(options.packageSettings?.service, '1on1'); - }; - - Email.send({ - from: 'foo@example.com', - to: 'bar@example.com', - text: '*Cool*, man', - html: 'Cool, man', - stream, - }); - - test.equal(stream.getContentsAsString('utf8'), false); - }); - Email.customTransport = undefined; - Meteor.settings.packages = undefined; - } -); - -Tinytest.add('[Sync] email - hooks stop the sending', function (test) { - // Register hooks - const hook1 = Email.hookSend((options) => { - // Test that we get options through - test.equal(options.from, 'foo@example.com'); - console.log('EXECUTE'); - return true; - }); - const hook2 = Email.hookSend(() => { - console.log('STOP'); - return false; - }); - const hook3 = Email.hookSend(() => { - console.log('FAIL'); - }); - smokeEmailTest(function (stream) { - Email.send({ - from: 'foo@example.com', - to: 'bar@example.com', - text: '*Cool*, man', - html: 'Cool, man', - stream, - }); - - test.equal(stream.getContentsAsString('utf8'), false); - }); - hook1.stop(); - hook2.stop(); - hook3.stop(); -}); - // Individual Async tests Tinytest.addAsync( - '[Async] email - alternate API is used for sending gets data', + 'email - alternate API is used for sending gets data', function (test, onComplete) { const allPromises = []; smokeEmailTest((stream) => { @@ -161,7 +76,7 @@ Tinytest.addAsync( ); Tinytest.addAsync( - '[Async] email - hooks stop the sending', + 'email - hooks stop the sending', function (test, onComplete) { // Register hooks const hook1 = Email.hookSend((options) => { @@ -197,7 +112,7 @@ Tinytest.addAsync( // Another tests -Tinytest.add('[Sync] email - URL string for known hosts', function (test) { +Tinytest.add('email - URL string for known hosts', function (test) { const oneTransport = EmailTest.knowHostsTransport({ service: '1und1', user: 'test', @@ -254,7 +169,7 @@ Tinytest.add('[Sync] email - URL string for known hosts', function (test) { }); Tinytest.addAsync( - '[Async] email - with custom transport exception', + 'email - with custom transport exception', async function (test) { Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS; Email.customTransport = (options) => { @@ -274,7 +189,7 @@ Tinytest.addAsync( ); Tinytest.addAsync( - '[Async] email - with custom transport long time running', + 'email - with custom transport long time running', async function (test) { Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS; Email.customTransport = async (options) => { @@ -290,22 +205,3 @@ Tinytest.addAsync( Email.customTransport = undefined; } ); - -Tinytest.addAsync( - '[Sync] email - with custom transport long time running', - function (test, onComplete) { - Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS; - Email.customTransport = async (options) => { - await sleep(3000); - test.equal(options.from, 'foo@example.com'); - test.equal(options.packageSettings?.service, '1on1'); - Meteor.settings.packages = undefined; - Email.customTransport = undefined; - onComplete(); - }; - Email.send({ - from: 'foo@example.com', - to: 'bar@example.com', - }); - } -); diff --git a/packages/meteor/dynamics_nodejs.js b/packages/meteor/dynamics_nodejs.js index a2528efefa..e38d072f0a 100644 --- a/packages/meteor/dynamics_nodejs.js +++ b/packages/meteor/dynamics_nodejs.js @@ -222,6 +222,9 @@ const bindEnvironmentFibers = (func, onException, _this) => { }; }; +// This function has two reasons: +// 1. Return the function to be executed on the MeteorJS context, having it assinged in the async localstorage. +// 2. Better error handling, the error message will be more clear. const bindEnvironmentAsync = (func, onException, _this) => { const dynamics = Meteor._getValueFromAslStore(CURRENT_VALUE_KEY_NAME); const currentSlot = Meteor._getValueFromAslStore(SLOT_CALL_KEY); diff --git a/packages/mongo-async/collection.js b/packages/mongo-async/collection.js index 855b2f3c14..dc2d29c206 100644 --- a/packages/mongo-async/collection.js +++ b/packages/mongo-async/collection.js @@ -745,16 +745,29 @@ Object.assign(Mongo.Collection.prototype, { // We'll actually design an index API later. For now, we just pass through to // Mongo's, but make it synchronous. - _ensureIndex(index, options) { + /** + * @summary Creates the specified index on the collection. + * @locus server + * @method _ensureIndex + * @deprecated in 3.0 + * @memberof Mongo.Collection + * @instance + * @param {Object} index A document that contains the field and value pairs where the field is the index key and the value describes the type of index for that field. For an ascending index on a field, specify a value of `1`; for descending index, specify a value of `-1`. Use `text` for text indexes. + * @param {Object} [options] All options are listed in [MongoDB documentation](https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/#options) + * @param {String} options.name Name of the index + * @param {Boolean} options.unique Define that the index values must be unique, more at [MongoDB documentation](https://docs.mongodb.com/manual/core/index-unique/) + * @param {Boolean} options.sparse Define that the index is sparse, more at [MongoDB documentation](https://docs.mongodb.com/manual/core/index-sparse/) + */ + async _ensureIndex(index, options) { var self = this; if (!self._collection._ensureIndex || !self._collection.createIndex) throw new Error('Can only call createIndex on server collections'); if (self._collection.createIndex) { - self._collection.createIndex(index, options); + await self._collection.createIndex(index, options); } else { import { Log } from 'meteor/logging'; - Log.debug(`_ensureIndex has been deprecated, please use the new 'createIndex' instead${options?.name ? `, index name: ${options.name}` : `, index: ${JSON.stringify(index)}`}`) - self._collection._ensureIndex(index, options); + Log.debug(`_ensureIndex has been deprecated, please use the new 'createIndex' instead${ options?.name ? `, index name: ${ options.name }` : `, index: ${ JSON.stringify(index) }` }`) + await self._collection._ensureIndex(index, options); } }, @@ -770,37 +783,37 @@ Object.assign(Mongo.Collection.prototype, { * @param {Boolean} options.unique Define that the index values must be unique, more at [MongoDB documentation](https://docs.mongodb.com/manual/core/index-unique/) * @param {Boolean} options.sparse Define that the index is sparse, more at [MongoDB documentation](https://docs.mongodb.com/manual/core/index-sparse/) */ - createIndex(index, options) { + async createIndex(index, options) { var self = this; if (!self._collection.createIndex) throw new Error('Can only call createIndex on server collections'); try { - self._collection.createIndex(index, options); + await self._collection.createIndex(index, options); } catch (e) { if (e.message.includes('An equivalent index already exists with the same name but different options.') && Meteor.settings?.packages?.mongo?.reCreateIndexOnOptionMismatch) { import { Log } from 'meteor/logging'; - - Log.info(`Re-creating index ${index} for ${self._name} due to options mismatch.`); - self._collection._dropIndex(index); - self._collection.createIndex(index, options); + Log.info(`Re-creating index ${ index } for ${ self._name } due to options mismatch.`); + await self._collection._dropIndex(index); + await self._collection.createIndex(index, options); } else { - throw new Meteor.Error(`An error occurred when creating an index for collection "${self._name}: ${e.message}`); + console.error(e); + throw new Meteor.Error(`An error occurred when creating an index for collection "${ self._name }: ${ e.message }`); } } }, - _dropIndex(index) { + async _dropIndex(index) { var self = this; if (!self._collection._dropIndex) throw new Error('Can only call _dropIndex on server collections'); self._collection._dropIndex(index); }, - _dropCollection() { + async _dropCollection() { var self = this; if (!self._collection.dropCollection) throw new Error('Can only call _dropCollection on server collections'); - self._collection.dropCollection(); + await self._collection.dropCollection(); }, _createCappedCollection(byteSize, maxDocuments) { diff --git a/packages/mongo-async/collection_tests.js b/packages/mongo-async/collection_tests.js index c5d88db645..a6a1d79979 100644 --- a/packages/mongo-async/collection_tests.js +++ b/packages/mongo-async/collection_tests.js @@ -147,8 +147,8 @@ Tinytest.addAsync('collection - calling native find with good hint and maxTimeMs Promise.resolve( Meteor.isServer && collection.rawCollection().createIndex({ a: 1 }) - ).then(() => { - test.equal(collection.find({}, { + ).then(async () => { + test.equal(await collection.find({}, { hint: {a: 1}, maxTimeMs: 1000 }).count(), 1); diff --git a/packages/mongo-async/mongo_driver.js b/packages/mongo-async/mongo_driver.js index 44f399142c..0de93d0c5f 100644 --- a/packages/mongo-async/mongo_driver.js +++ b/packages/mongo-async/mongo_driver.js @@ -426,19 +426,25 @@ MongoConnection.prototype._remove = function (collection_name, selector, } }; -MongoConnection.prototype._dropCollection = function (collectionName, cb) { +MongoConnection.prototype._dropCollection = async function (collectionName, cb) { var self = this; var write = self._maybeBeginWrite(); var refresh = function () { - Meteor.refresh({collection: collectionName, id: null, - dropCollection: true}); + return Meteor.refresh({ + collection: collectionName, + id: null, + dropCollection: true + }); }; - cb = bindEnvironmentForWrite(writeCallback(write, refresh, cb)); + // TODO[FIBERS]: Check if this is correct after the DDP changes. + const fn = bindEnvironmentForWrite( + writeCallback(write, refresh, cb) + ); try { var collection = self.rawCollection(collectionName); - collection.drop(cb); + await Meteor.promisify(collection.drop)(fn); } catch (e) { write.committed(); throw e; @@ -447,17 +453,17 @@ MongoConnection.prototype._dropCollection = function (collectionName, cb) { // For testing only. Slightly better than `c.rawDatabase().dropDatabase()` // because it lets the test's fence wait for it to be complete. -MongoConnection.prototype._dropDatabase = function (cb) { +MongoConnection.prototype._dropDatabase = async function (cb) { var self = this; var write = self._maybeBeginWrite(); var refresh = function () { Meteor.refresh({ dropDatabase: true }); }; - cb = bindEnvironmentForWrite(writeCallback(write, refresh, cb)); + const fn = Meteor.bindEnvironment(writeCallback(write, refresh, cb)) try { - self.db.dropDatabase(cb); + await Meteor.promisify(self.db.dropDatabase)(fn); } catch (e) { write.committed(); throw e; @@ -838,8 +844,8 @@ MongoConnection.prototype.createIndex = async function (collectionName, index, // We expect this function to be called at startup, not from within a method, // so we don't interact with the write fence. - var collection = self.rawCollection(collectionName); - var indexName = await collection.createIndex(index, options); + var collection = self.rawCollection(collectionName) + var indexName = await collection.createIndex(index, options) }; MongoConnection.prototype._ensureIndex = MongoConnection.prototype.createIndex; @@ -850,7 +856,7 @@ MongoConnection.prototype._dropIndex = async function (collectionName, index) { // This function is only used by test code, not within a method, so we don't // interact with the write fence. var collection = self.rawCollection(collectionName); - var indexName = await collection.dropIndex(index); + var indexName = await collection.dropIndex(index) }; // CURSORS diff --git a/packages/mongo-async/oplog_tests.js b/packages/mongo-async/oplog_tests.js index e6fead6ba6..8861d9cf3f 100644 --- a/packages/mongo-async/oplog_tests.js +++ b/packages/mongo-async/oplog_tests.js @@ -2,11 +2,14 @@ var OplogCollection = new Mongo.Collection("oplog-" + Random.id()); Tinytest.addAsync("mongo-livedata - oplog - cursorSupported", async function (test) { var oplogEnabled = - !!MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle; + !!MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle; var supported = async function (expected, selector, options) { var cursor = OplogCollection.find(selector, options); - var handle = await cursor.observeChanges({added: function () {}}); + var handle = await cursor.observeChanges({ + added: function () { + } + }); // If there's no oplog at all, we shouldn't ever use it. if (!oplogEnabled) expected = false; @@ -18,152 +21,151 @@ Tinytest.addAsync("mongo-livedata - oplog - cursorSupported", async function (te await supported(true, 1234); await supported(true, new Mongo.ObjectID()); - await supported(true, {_id: "asdf"}); - await supported(true, {_id: 1234}); - await supported(true, {_id: new Mongo.ObjectID()}); + await supported(true, { _id: "asdf" }); + await supported(true, { _id: 1234 }); + await supported(true, { _id: new Mongo.ObjectID() }); - await supported(true, {foo: "asdf", - bar: 1234, - baz: new Mongo.ObjectID(), - eeney: true, - miney: false, - moe: null}); + await supported(true, { + foo: "asdf", + bar: 1234, + baz: new Mongo.ObjectID(), + eeney: true, + miney: false, + moe: null + }); await supported(true, {}); - await supported(true, {$and: [{foo: "asdf"}, {bar: "baz"}]}); - await supported(true, {foo: {x: 1}}); - await supported(true, {foo: {$gt: 1}}); - await supported(true, {foo: [1, 2, 3]}); + await supported(true, { $and: [{ foo: "asdf" }, { bar: "baz" }] }); + await supported(true, { foo: { x: 1 } }); + await supported(true, { foo: { $gt: 1 } }); + await supported(true, { foo: [1, 2, 3] }); // No $where. - await supported(false, {$where: "xxx"}); - await supported(false, {$and: [{foo: "adsf"}, {$where: "xxx"}]}); + await supported(false, { $where: "xxx" }); + await supported(false, { $and: [{ foo: "adsf" }, { $where: "xxx" }] }); // No geoqueries. - await supported(false, {x: {$near: [1,1]}}); + await supported(false, { x: { $near: [1, 1] } }); // Nothing Minimongo doesn't understand. (Minimongo happens to fail to // implement $elemMatch inside $all which MongoDB supports.) - await supported(false, {x: {$all: [{$elemMatch: {y: 2}}]}}); + await supported(false, { x: { $all: [{ $elemMatch: { y: 2 } }] } }); - await supported(true, {}, { sort: {x:1} }); - await supported(true, {}, { sort: {x:1}, limit: 5 }); - await supported(false, {}, { sort: {$natural:1}, limit: 5 }); + await supported(true, {}, { sort: { x: 1 } }); + await supported(true, {}, { sort: { x: 1 }, limit: 5 }); + await supported(false, {}, { sort: { $natural: 1 }, limit: 5 }); await supported(false, {}, { limit: 5 }); await supported(false, {}, { skip: 2, limit: 5 }); await supported(false, {}, { skip: 2 }); }); -// TODO -> Index here. -// process.env.MONGO_OPLOG_URL && testAsyncMulti( -// "mongo-livedata - oplog - entry skipping", [ -// function (test, expect) { -// var self = this; -// self.collectionName = Random.id(); -// self.collection = new Mongo.Collection(self.collectionName); -// self.collection.createIndex({species: 1}); -// -// // Fill collection with lots of irrelevant objects (red cats) and some -// // relevant ones (blue dogs). -// -// // After updating to mongo 3.2 with the 2.1.18 driver it was no longer -// // possible to make this test fail with TOO_FAR_BEHIND = 2000. -// // The documents waiting to be processed would hardly go beyond 1000 -// // using mongo 3.2 with WiredTiger -// MongoInternals.defaultRemoteCollectionDriver() -// .mongo._oplogHandle._defineTooFarBehind(500); -// -// self.IRRELEVANT_SIZE = 15000; -// self.RELEVANT_SIZE = 10; -// var docs = []; -// var i; -// for (i = 0; i < self.IRRELEVANT_SIZE; ++i) { -// docs.push({ -// name: "cat " + i, -// species: 'cat', -// color: 'red' -// }); -// } -// for (i = 0; i < self.RELEVANT_SIZE; ++i) { -// docs.push({ -// name: "dog " + i, -// species: 'dog', -// color: 'blue' -// }); -// } -// // XXX implement bulk insert #1255 -// var rawCollection = self.collection.rawCollection(); -// rawCollection.insertMany(docs, Meteor.bindEnvironment(expect(function (err) { -// test.isFalse(err); -// }))); -// }, -// -// function (test, expect) { -// var self = this; -// -// test.equal(self.collection.find().count(), -// self.IRRELEVANT_SIZE + self.RELEVANT_SIZE); -// -// var blueDog5Id = null; -// var gotSpot = false; -// -// // Watch for blue dogs. -// const gotSpotPromise = new Promise(resolve => { -// self.subHandle = self.collection.find({ -// species: 'dog', -// color: 'blue', -// }).observeChanges({ -// added(id, fields) { -// if (fields.name === 'dog 5') { -// blueDog5Id = id; -// } -// }, -// changed(id, fields) { -// if (EJSON.equals(id, blueDog5Id) && -// fields.name === 'spot') { -// gotSpot = true; -// resolve(); -// } -// }, -// }); -// }); -// -// test.isTrue(self.subHandle._multiplexer._observeDriver._usesOplog); -// test.isTrue(blueDog5Id); -// test.isFalse(gotSpot); -// -// self.skipped = false; -// self.skipHandle = MongoInternals.defaultRemoteCollectionDriver() -// .mongo._oplogHandle.onSkippedEntries(function () { -// self.skipped = true; -// }); -// -// // Dye all the cats blue. This adds lots of oplog mentries that look like -// // they might in theory be relevant (since they say "something you didn't -// // know about is now blue", and who knows, maybe it's a dog) which puts -// // the OplogObserveDriver into FETCHING mode, which performs poorly. -// self.collection.update({species: 'cat'}, -// {$set: {color: 'blue'}}, -// {multi: true}); -// self.collection.update(blueDog5Id, {$set: {name: 'spot'}}); -// -// // We ought to see the spot change soon! -// return gotSpotPromise; -// }, -// -// function (test, expect) { -// var self = this; -// test.isTrue(self.skipped); -// -// //This gets the TOO_FAR_BEHIND back to its initial value -// MongoInternals.defaultRemoteCollectionDriver() -// .mongo._oplogHandle._resetTooFarBehind(); -// -// self.skipHandle.stop(); -// self.subHandle.stop(); -// self.collection.remove({}); -// } -// ] -// ); +process.env.MONGO_OPLOG_URL && testAsyncMulti( + "mongo-livedata - oplog - entry skipping", [ + async function (test, expect) { + var self = this; + self.collectionName = Random.id(); + self.collection = new Mongo.Collection(self.collectionName); + await self.collection.createIndex({ species: 1 }); + + // Fill collection with lots of irrelevant objects (red cats) and some + // relevant ones (blue dogs). + + // After updating to mongo 3.2 with the 2.1.18 driver it was no longer + // possible to make this test fail with TOO_FAR_BEHIND = 2000. + // The documents waiting to be processed would hardly go beyond 1000 + // using mongo 3.2 with WiredTiger + MongoInternals.defaultRemoteCollectionDriver() + .mongo._oplogHandle._defineTooFarBehind(500); + + self.IRRELEVANT_SIZE = 15000; + self.RELEVANT_SIZE = 10; + var docs = []; + var i; + for (i = 0; i < self.IRRELEVANT_SIZE; ++i) { + docs.push({ + name: "cat " + i, + species: 'cat', + color: 'red' + }); + } + for (i = 0; i < self.RELEVANT_SIZE; ++i) { + docs.push({ + name: "dog " + i, + species: 'dog', + color: 'blue' + }); + } + // XXX implement bulk insert #1255 + var rawCollection = self.collection.rawCollection(); + rawCollection.insertMany(docs, Meteor.bindEnvironment(expect(function (err) { + test.isFalse(err); + }))); + }, + + async function (test, expect) { + var self = this; + + test.equal((await self.collection.find().count()), + self.IRRELEVANT_SIZE + self.RELEVANT_SIZE); + + var blueDog5Id = null; + var gotSpot = false; + let resolver; const gotSpotPromise = new Promise(resolve => resolver = resolve) + let resolver2; const gotSpotPromise2 = new Promise(resolve => resolver2 = resolve) + self.subHandle = await self.collection.find({ + species: 'dog', + color: 'blue', + }).observeChanges({ + added(id, fields) { + if (fields.name === 'dog 5') { + blueDog5Id = id + resolver2() + } + }, + changed(id, fields) { + if (EJSON.equals(id, blueDog5Id) && + fields.name === 'spot') { + gotSpot = true; + resolver(); + } + }, + }); + test.isTrue(self.subHandle._multiplexer._observeDriver._usesOplog); + self.skipped = false; + self.skipHandle = MongoInternals.defaultRemoteCollectionDriver() + .mongo._oplogHandle.onSkippedEntries(function () { + self.skipped = true; + }); + + // Dye all the cats blue. This adds lots of oplog mentries that look like + // they might in theory be relevant (since they say "something you didn't + // know about is now blue", and who knows, maybe it's a dog) which puts + // the OplogObserveDriver into FETCHING mode, which performs poorly. + await self.collection.update({ species: 'cat' }, + { $set: { color: 'blue' } }, + { multi: true }); + test.isTrue(blueDog5Id); + test.isFalse(gotSpot); + await self.collection.update(blueDog5Id, { $set: { name: 'spot' } }); + + + // We ought to see the spot change soon! + return Promise.all([gotSpotPromise, gotSpotPromise2]); + }, + + async function (test, expect) { + var self = this; + test.isTrue(self.skipped); + + //This gets the TOO_FAR_BEHIND back to its initial value + MongoInternals.defaultRemoteCollectionDriver() + .mongo._oplogHandle._resetTooFarBehind(); + + await self.skipHandle.stop(); + await self.subHandle.stop(); + await self.collection.remove({}); + } + ] +); Meteor.isServer && Tinytest.addAsync(