Skip to content

Commit 473a636

Browse files
authored
Merge pull request #14831 from Automattic/8.6
8.6
2 parents fb0febb + 0a93334 commit 473a636

22 files changed

+309
-118
lines changed

lib/cursor/changeStream.js

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,18 @@ class ChangeStream extends EventEmitter {
3333
);
3434
}
3535

36-
// This wrapper is necessary because of buffering.
37-
changeStreamThunk((err, driverChangeStream) => {
38-
if (err != null) {
39-
this.emit('error', err);
40-
return;
41-
}
36+
this.$driverChangeStreamPromise = new Promise((resolve, reject) => {
37+
// This wrapper is necessary because of buffering.
38+
changeStreamThunk((err, driverChangeStream) => {
39+
if (err != null) {
40+
this.emit('error', err);
41+
return reject(err);
42+
}
4243

43-
this.driverChangeStream = driverChangeStream;
44-
this.emit('ready');
44+
this.driverChangeStream = driverChangeStream;
45+
this.emit('ready');
46+
resolve();
47+
});
4548
});
4649
}
4750

@@ -53,20 +56,23 @@ class ChangeStream extends EventEmitter {
5356
this.bindedEvents = true;
5457

5558
if (this.driverChangeStream == null) {
56-
this.once('ready', () => {
57-
this.driverChangeStream.on('close', () => {
58-
this.closed = true;
59-
});
59+
this.$driverChangeStreamPromise.then(
60+
() => {
61+
this.driverChangeStream.on('close', () => {
62+
this.closed = true;
63+
});
6064

61-
driverChangeStreamEvents.forEach(ev => {
62-
this.driverChangeStream.on(ev, data => {
63-
if (data != null && data.fullDocument != null && this.options && this.options.hydrate) {
64-
data.fullDocument = this.options.model.hydrate(data.fullDocument);
65-
}
66-
this.emit(ev, data);
65+
driverChangeStreamEvents.forEach(ev => {
66+
this.driverChangeStream.on(ev, data => {
67+
if (data != null && data.fullDocument != null && this.options && this.options.hydrate) {
68+
data.fullDocument = this.options.model.hydrate(data.fullDocument);
69+
}
70+
this.emit(ev, data);
71+
});
6772
});
68-
});
69-
});
73+
},
74+
() => {} // No need to register events if opening change stream failed
75+
);
7076

7177
return;
7278
}
@@ -142,8 +148,12 @@ class ChangeStream extends EventEmitter {
142148
this.closed = true;
143149
if (this.driverChangeStream) {
144150
return this.driverChangeStream.close();
151+
} else {
152+
return this.$driverChangeStreamPromise.then(
153+
() => this.driverChangeStream.close(),
154+
() => {} // No need to close if opening the change stream failed
155+
);
145156
}
146-
return Promise.resolve();
147157
}
148158
}
149159

lib/cursor/queryCursor.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const eachAsync = require('../helpers/cursor/eachAsync');
1010
const helpers = require('../queryHelpers');
1111
const kareem = require('kareem');
1212
const immediate = require('../helpers/immediate');
13+
const { once } = require('node:events');
1314
const util = require('util');
1415

1516
/**
@@ -42,6 +43,7 @@ function QueryCursor(query) {
4243
this.cursor = null;
4344
this.skipped = false;
4445
this.query = query;
46+
this._closed = false;
4547
const model = query.model;
4648
this._mongooseOptions = {};
4749
this._transforms = [];
@@ -135,6 +137,25 @@ QueryCursor.prototype._read = function() {
135137
});
136138
};
137139

140+
/**
141+
* Returns the underlying cursor from the MongoDB Node driver that this cursor uses.
142+
*
143+
* @method getDriverCursor
144+
* @memberOf QueryCursor
145+
* @returns {Cursor} MongoDB Node driver cursor instance
146+
* @instance
147+
* @api public
148+
*/
149+
150+
QueryCursor.prototype.getDriverCursor = async function getDriverCursor() {
151+
if (this.cursor) {
152+
return this.cursor;
153+
}
154+
155+
await once(this, 'cursor');
156+
return this.cursor;
157+
};
158+
138159
/**
139160
* Registers a transform function which subsequently maps documents retrieved
140161
* via the streams interface or `.next()`
@@ -209,6 +230,7 @@ QueryCursor.prototype.close = async function close() {
209230
}
210231
try {
211232
await this.cursor.close();
233+
this._closed = true;
212234
this.emit('close');
213235
} catch (error) {
214236
this.listeners('error').length > 0 && this.emit('error', error);
@@ -246,6 +268,9 @@ QueryCursor.prototype.next = async function next() {
246268
if (typeof arguments[0] === 'function') {
247269
throw new MongooseError('QueryCursor.prototype.next() no longer accepts a callback');
248270
}
271+
if (this._closed) {
272+
throw new MongooseError('Cannot call `next()` on a closed cursor');
273+
}
249274
return new Promise((resolve, reject) => {
250275
_next(this, function(error, doc) {
251276
if (error) {

lib/model.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2115,17 +2115,21 @@ Model.countDocuments = function countDocuments(conditions, options) {
21152115
*
21162116
* @param {String} field
21172117
* @param {Object} [conditions] optional
2118+
* @param {Object} [options] optional
21182119
* @return {Query}
21192120
* @api public
21202121
*/
21212122

2122-
Model.distinct = function distinct(field, conditions) {
2123+
Model.distinct = function distinct(field, conditions, options) {
21232124
_checkContext(this, 'distinct');
2124-
if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') {
2125+
if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') {
21252126
throw new MongooseError('Model.distinct() no longer accepts a callback');
21262127
}
21272128

21282129
const mq = new this.Query({}, {}, this, this.$__collection);
2130+
if (options != null) {
2131+
mq.setOptions(options);
2132+
}
21292133

21302134
return mq.distinct(field, conditions);
21312135
};

lib/query.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2777,7 +2777,7 @@ Query.prototype.estimatedDocumentCount = function(options) {
27772777
this.op = 'estimatedDocumentCount';
27782778
this._validateOp();
27792779

2780-
if (typeof options === 'object' && options != null) {
2780+
if (options != null) {
27812781
this.setOptions(options);
27822782
}
27832783

@@ -2836,7 +2836,7 @@ Query.prototype.countDocuments = function(conditions, options) {
28362836
this.merge(conditions);
28372837
}
28382838

2839-
if (typeof options === 'object' && options != null) {
2839+
if (options != null) {
28402840
this.setOptions(options);
28412841
}
28422842

@@ -2874,21 +2874,24 @@ Query.prototype.__distinct = async function __distinct() {
28742874
*
28752875
* #### Example:
28762876
*
2877+
* distinct(field, conditions, options)
28772878
* distinct(field, conditions)
28782879
* distinct(field)
28792880
* distinct()
28802881
*
28812882
* @param {String} [field]
28822883
* @param {Object|Query} [filter]
2884+
* @param {Object} [options]
28832885
* @return {Query} this
28842886
* @see distinct https://www.mongodb.com/docs/manual/reference/method/db.collection.distinct/
28852887
* @api public
28862888
*/
28872889

2888-
Query.prototype.distinct = function(field, conditions) {
2890+
Query.prototype.distinct = function(field, conditions, options) {
28892891
if (typeof field === 'function' ||
28902892
typeof conditions === 'function' ||
2891-
typeof arguments[2] === 'function') {
2893+
typeof options === 'function' ||
2894+
typeof arguments[3] === 'function') {
28922895
throw new MongooseError('Query.prototype.distinct() no longer accepts a callback');
28932896
}
28942897

@@ -2907,6 +2910,10 @@ Query.prototype.distinct = function(field, conditions) {
29072910
this._distinct = field;
29082911
}
29092912

2913+
if (options != null) {
2914+
this.setOptions(options);
2915+
}
2916+
29102917
return this;
29112918
};
29122919

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"dependencies": {
2222
"bson": "^6.7.0",
2323
"kareem": "2.6.3",
24-
"mongodb": "6.7.0",
24+
"mongodb": "6.8.0",
2525
"mpath": "0.9.0",
2626
"mquery": "5.0.0",
2727
"ms": "2.1.3",

test/connection.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,8 @@ describe('connections:', function() {
10321032
await nextChange;
10331033
assert.equal(changes.length, 1);
10341034
assert.equal(changes[0].operationType, 'insert');
1035+
1036+
await changeStream.close();
10351037
await conn.close();
10361038
});
10371039

test/docs/transactions.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,25 @@ describe('transactions', function() {
338338
assert.deepEqual(fromDb, { name: 'Tyrion Lannister' });
339339
});
340340

341+
it('distinct (gh-8006)', async function() {
342+
const Character = db.model('gh8006_Character', new Schema({ name: String, rank: String }, { versionKey: false }));
343+
344+
const session = await db.startSession();
345+
346+
session.startTransaction();
347+
await Character.create([{ name: 'Will Riker', rank: 'Commander' }, { name: 'Jean-Luc Picard', rank: 'Captain' }], { session });
348+
349+
let names = await Character.distinct('name', {}, { session });
350+
assert.deepStrictEqual(names.sort(), ['Jean-Luc Picard', 'Will Riker']);
351+
352+
names = await Character.distinct('name', { rank: 'Captain' }, { session });
353+
assert.deepStrictEqual(names.sort(), ['Jean-Luc Picard']);
354+
355+
// Undo both update and delete since doc should pull from `$session()`
356+
await session.abortTransaction();
357+
session.endSession();
358+
});
359+
341360
it('save() with no changes (gh-8571)', async function() {
342361
db.deleteModel(/Test/);
343362
const Test = db.model('Test', Schema({ name: String }));

test/model.test.js

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const sinon = require('sinon');
77
const start = require('./common');
88

99
const assert = require('assert');
10+
const { once } = require('events');
1011
const random = require('./util').random;
1112
const util = require('./util');
1213

@@ -3508,6 +3509,9 @@ describe('Model', function() {
35083509
}
35093510
changeStream.removeListener('change', listener);
35103511
listener = null;
3512+
// Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
3513+
// may still poll after close.
3514+
changeStream.on('error', () => {});
35113515
changeStream.close();
35123516
changeStream = null;
35133517
});
@@ -3560,14 +3564,21 @@ describe('Model', function() {
35603564
it('fullDocument (gh-11936)', async function() {
35613565
const MyModel = db.model('Test', new Schema({ name: String }));
35623566

3567+
const doc = await MyModel.create({ name: 'Ned Stark' });
35633568
const changeStream = await MyModel.watch([], {
35643569
fullDocument: 'updateLookup',
35653570
hydrate: true
35663571
});
3572+
await changeStream.$driverChangeStreamPromise;
35673573

3568-
const doc = await MyModel.create({ name: 'Ned Stark' });
3569-
3570-
const p = changeStream.next();
3574+
const p = new Promise((resolve) => {
3575+
changeStream.once('change', change => {
3576+
resolve(change);
3577+
});
3578+
});
3579+
// Need to wait for resume token to be set after the event listener,
3580+
// otherwise change stream might not pick up the update.
3581+
await once(changeStream.driverChangeStream, 'resumeTokenChanged');
35713582
await MyModel.updateOne({ _id: doc._id }, { name: 'Tony Stark' });
35723583

35733584
const changeData = await p;
@@ -3576,22 +3587,31 @@ describe('Model', function() {
35763587
doc._id.toHexString());
35773588
assert.ok(changeData.fullDocument.$__);
35783589
assert.equal(changeData.fullDocument.get('name'), 'Tony Stark');
3590+
3591+
await changeStream.close();
35793592
});
35803593

35813594
it('fullDocument with immediate watcher and hydrate (gh-14049)', async function() {
35823595
const MyModel = db.model('Test', new Schema({ name: String }));
35833596

35843597
const doc = await MyModel.create({ name: 'Ned Stark' });
35853598

3599+
let changeStream = null;
35863600
const p = new Promise((resolve) => {
3587-
MyModel.watch([], {
3601+
changeStream = MyModel.watch([], {
35883602
fullDocument: 'updateLookup',
35893603
hydrate: true
3590-
}).on('change', change => {
3604+
});
3605+
3606+
changeStream.on('change', change => {
35913607
resolve(change);
35923608
});
35933609
});
35943610

3611+
// Need to wait for cursor to be initialized and for resume token to
3612+
// be set, otherwise change stream might not pick up the update.
3613+
await changeStream.$driverChangeStreamPromise;
3614+
await once(changeStream.driverChangeStream, 'resumeTokenChanged');
35953615
await MyModel.updateOne({ _id: doc._id }, { name: 'Tony Stark' });
35963616

35973617
const changeData = await p;
@@ -3600,6 +3620,8 @@ describe('Model', function() {
36003620
doc._id.toHexString());
36013621
assert.ok(changeData.fullDocument.$__);
36023622
assert.equal(changeData.fullDocument.get('name'), 'Tony Stark');
3623+
3624+
await changeStream.close();
36033625
});
36043626

36053627
it('respects discriminators (gh-11007)', async function() {
@@ -3639,6 +3661,9 @@ describe('Model', function() {
36393661
assert.equal(changeData.operationType, 'insert');
36403662
assert.equal(changeData.fullDocument.name, 'Ned Stark');
36413663

3664+
// Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
3665+
// may still poll after close.
3666+
changeStream.on('error', () => {});
36423667
await changeStream.close();
36433668
await db.close();
36443669
});
@@ -3654,11 +3679,16 @@ describe('Model', function() {
36543679
setTimeout(resolve, 500, false);
36553680
});
36563681

3657-
changeStream.close();
3658-
await db;
3682+
// Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
3683+
// may still poll after close.
3684+
changeStream.on('error', () => {});
3685+
3686+
const close = changeStream.close();
3687+
await db.asPromise();
36593688
const readyCalled = await ready;
36603689
assert.strictEqual(readyCalled, false);
36613690

3691+
await close;
36623692
await db.close();
36633693
});
36643694

@@ -3675,6 +3705,10 @@ describe('Model', function() {
36753705

36763706
await MyModel.create({ name: 'Hodor' });
36773707

3708+
// Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
3709+
// may still poll after close.
3710+
changeStream.on('error', () => {});
3711+
36783712
changeStream.close();
36793713
const closedData = await closed;
36803714
assert.strictEqual(closedData, true);

0 commit comments

Comments
 (0)