mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'release-3.0' into release-3.0-tools
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
2
packages/email/email.d.ts
vendored
2
packages/email/email.d.ts
vendored
@@ -17,7 +17,9 @@ export namespace Email {
|
||||
packageSettings?: unknown;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
function send(options: EmailOptions): void;
|
||||
function sendAsync(options: EmailOptions): Promise<void>;
|
||||
function hookSend(fn: (options: EmailOptions) => boolean): void;
|
||||
function customTransport(fn: (options: CustomEmailOptions) => void): void;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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: '<i>Cool</i>, 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: '<i>Cool</i>, 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: '<i>Cool</i>, 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',
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user