Skip to content

Commit 49b0339

Browse files
authored
Merge pull request Automattic#14915 from Automattic/8.7
8.7
2 parents c4d96ea + a53f430 commit 49b0339

21 files changed

+629
-21
lines changed

docs/populate.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,33 @@ story.author = author;
119119
console.log(story.author.name); // prints "Ian Fleming"
120120
```
121121

122+
You can also push documents or POJOs onto a populated array, and Mongoose will add those documents if their `ref` matches.
123+
124+
```javascript
125+
const fan1 = await Person.create({ name: 'Sean' });
126+
await Story.updateOne({ title: 'Casino Royale' }, { $push: { fans: { $each: [fan1._id] } } });
127+
128+
const story = await Story.findOne({ title: 'Casino Royale' }).populate('fans');
129+
story.fans[0].name; // 'Sean'
130+
131+
const fan2 = await Person.create({ name: 'George' });
132+
story.fans.push(fan2);
133+
story.fans[1].name; // 'George'
134+
135+
story.fans.push({ name: 'Roger' });
136+
story.fans[2].name; // 'Roger'
137+
```
138+
139+
If you push a non-POJO and non-document value, like an ObjectId, Mongoose `>= 8.7.0` will depopulate the entire array.
140+
141+
```javascript
142+
const fan4 = await Person.create({ name: 'Timothy' });
143+
story.fans.push(fan4._id); // Push the `_id`, not the full document
144+
145+
story.fans[0].name; // undefined, `fans[0]` is now an ObjectId
146+
story.fans[0].toString() === fan1._id.toString(); // true
147+
```
148+
122149
## Checking Whether a Field is Populated {#checking-populated}
123150

124151
You can call the `populated()` function to check whether a field is populated.

lib/connection.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,15 @@ Object.setPrototypeOf(Connection.prototype, EventEmitter.prototype);
106106

107107
Object.defineProperty(Connection.prototype, 'readyState', {
108108
get: function() {
109+
// If connection thinks it is connected, but we haven't received a heartbeat in 2 heartbeat intervals,
110+
// that likely means the connection is stale (potentially due to frozen AWS Lambda container)
111+
if (
112+
this._readyState === STATES.connected &&
113+
this._lastHeartbeatAt != null &&
114+
typeof this.client?.topology?.s?.description?.heartbeatFrequencyMS === 'number' &&
115+
Date.now() - this._lastHeartbeatAt >= this.client.topology.s.description.heartbeatFrequencyMS * 2) {
116+
return STATES.disconnected;
117+
}
109118
return this._readyState;
110119
},
111120
set: function(val) {

lib/drivers/node-mongodb-native/connection.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ const utils = require('../../utils');
2323
function NativeConnection() {
2424
MongooseConnection.apply(this, arguments);
2525
this._listening = false;
26+
// Tracks the last time (as unix timestamp) the connection received a
27+
// serverHeartbeatSucceeded or serverHeartbeatFailed event from the underlying MongoClient.
28+
// If we haven't received one in a while (like due to a frozen AWS Lambda container) then
29+
// `readyState` is likely stale.
30+
this._lastHeartbeatAt = null;
2631
}
2732

2833
/**
@@ -106,6 +111,7 @@ NativeConnection.prototype.useDb = function(name, options) {
106111
_opts.noListener = options.noListener;
107112
}
108113
newConn.db = _this.client.db(name, _opts);
114+
newConn._lastHeartbeatAt = _this._lastHeartbeatAt;
109115
newConn.onOpen();
110116
}
111117

@@ -409,6 +415,9 @@ function _setClient(conn, client, options, dbName) {
409415
}
410416
});
411417
}
418+
client.on('serverHeartbeatSucceeded', () => {
419+
conn._lastHeartbeatAt = Date.now();
420+
});
412421

413422
if (options.monitorCommands) {
414423
client.on('commandStarted', (data) => conn.emit('commandStarted', data));
@@ -417,6 +426,9 @@ function _setClient(conn, client, options, dbName) {
417426
}
418427

419428
conn.onOpen();
429+
if (client.topology?.s?.state === 'connected') {
430+
conn._lastHeartbeatAt = Date.now();
431+
}
420432

421433
for (const i in conn.collections) {
422434
if (utils.object.hasOwnProperty(conn.collections, i)) {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*!
2+
* Module dependencies.
3+
*/
4+
5+
'use strict';
6+
7+
const MongooseError = require('./mongooseError');
8+
9+
10+
/**
11+
* If the underwriting `bulkWrite()` for `bulkSave()` succeeded, but wasn't able to update or
12+
* insert all documents, we throw this error.
13+
*
14+
* @api private
15+
*/
16+
17+
class MongooseBulkSaveIncompleteError extends MongooseError {
18+
constructor(modelName, documents, bulkWriteResult) {
19+
const matchedCount = bulkWriteResult?.matchedCount ?? 0;
20+
const insertedCount = bulkWriteResult?.insertedCount ?? 0;
21+
let preview = documents.map(doc => doc._id).join(', ');
22+
if (preview.length > 100) {
23+
preview = preview.slice(0, 100) + '...';
24+
}
25+
26+
const numDocumentsNotUpdated = documents.length - matchedCount - insertedCount;
27+
super(`${modelName}.bulkSave() was not able to update ${numDocumentsNotUpdated} of the given documents due to incorrect version or optimistic concurrency, document ids: ${preview}`);
28+
29+
this.modelName = modelName;
30+
this.documents = documents;
31+
this.bulkWriteResult = bulkWriteResult;
32+
this.numDocumentsNotUpdated = numDocumentsNotUpdated;
33+
}
34+
}
35+
36+
Object.defineProperty(MongooseBulkSaveIncompleteError.prototype, 'name', {
37+
value: 'MongooseBulkSaveIncompleteError'
38+
});
39+
40+
/*!
41+
* exports
42+
*/
43+
44+
module.exports = MongooseBulkSaveIncompleteError;
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
'use strict';
2+
3+
const mpath = require('mpath');
4+
5+
module.exports = applyVirtuals;
6+
7+
/**
8+
* Apply a given schema's virtuals to a given POJO
9+
*
10+
* @param {Schema} schema
11+
* @param {Object} obj
12+
* @param {Array<string>} [virtuals] optional whitelist of virtuals to apply
13+
* @returns
14+
*/
15+
16+
function applyVirtuals(schema, obj, virtuals) {
17+
if (obj == null) {
18+
return obj;
19+
}
20+
21+
let virtualsForChildren = virtuals;
22+
let toApply = null;
23+
24+
if (Array.isArray(virtuals)) {
25+
virtualsForChildren = [];
26+
toApply = [];
27+
for (const virtual of virtuals) {
28+
if (virtual.length === 1) {
29+
toApply.push(virtual[0]);
30+
} else {
31+
virtualsForChildren.push(virtual);
32+
}
33+
}
34+
}
35+
36+
applyVirtualsToChildren(schema, obj, virtualsForChildren);
37+
return applyVirtualsToDoc(schema, obj, toApply);
38+
}
39+
40+
/**
41+
* Apply virtuals to any subdocuments
42+
*
43+
* @param {Schema} schema subdocument schema
44+
* @param {Object} res subdocument
45+
* @param {Array<String>} [virtuals] optional whitelist of virtuals to apply
46+
*/
47+
48+
function applyVirtualsToChildren(schema, res, virtuals) {
49+
let attachedVirtuals = false;
50+
for (const childSchema of schema.childSchemas) {
51+
const _path = childSchema.model.path;
52+
const _schema = childSchema.schema;
53+
if (!_path) {
54+
continue;
55+
}
56+
const _obj = mpath.get(_path, res);
57+
if (_obj == null || (Array.isArray(_obj) && _obj.flat(Infinity).length === 0)) {
58+
continue;
59+
}
60+
61+
let virtualsForChild = null;
62+
if (Array.isArray(virtuals)) {
63+
virtualsForChild = [];
64+
for (const virtual of virtuals) {
65+
if (virtual[0] == _path) {
66+
virtualsForChild.push(virtual.slice(1));
67+
}
68+
}
69+
70+
if (virtualsForChild.length === 0) {
71+
continue;
72+
}
73+
}
74+
75+
applyVirtuals(_schema, _obj, virtualsForChild);
76+
attachedVirtuals = true;
77+
}
78+
79+
if (virtuals && virtuals.length && !attachedVirtuals) {
80+
applyVirtualsToDoc(schema, res, virtuals);
81+
}
82+
}
83+
84+
/**
85+
* Apply virtuals to a given document. Does not apply virtuals to subdocuments: use `applyVirtualsToChildren` instead
86+
*
87+
* @param {Schema} schema
88+
* @param {Object} doc
89+
* @param {Array<String>} [virtuals] optional whitelist of virtuals to apply
90+
* @returns
91+
*/
92+
93+
function applyVirtualsToDoc(schema, obj, virtuals) {
94+
if (obj == null || typeof obj !== 'object') {
95+
return;
96+
}
97+
if (Array.isArray(obj)) {
98+
for (const el of obj) {
99+
applyVirtualsToDoc(schema, el, virtuals);
100+
}
101+
return;
102+
}
103+
104+
if (schema.discriminators && Object.keys(schema.discriminators).length > 0) {
105+
for (const discriminatorKey of Object.keys(schema.discriminators)) {
106+
const discriminator = schema.discriminators[discriminatorKey];
107+
const key = discriminator.discriminatorMapping.key;
108+
const value = discriminator.discriminatorMapping.value;
109+
if (obj[key] == value) {
110+
schema = discriminator;
111+
break;
112+
}
113+
}
114+
}
115+
116+
if (virtuals == null) {
117+
virtuals = Object.keys(schema.virtuals);
118+
}
119+
for (const virtual of virtuals) {
120+
if (schema.virtuals[virtual] == null) {
121+
continue;
122+
}
123+
const virtualType = schema.virtuals[virtual];
124+
const sp = Array.isArray(virtual)
125+
? virtual
126+
: virtual.indexOf('.') === -1
127+
? [virtual]
128+
: virtual.split('.');
129+
let cur = obj;
130+
for (let i = 0; i < sp.length - 1; ++i) {
131+
cur[sp[i]] = sp[i] in cur ? cur[sp[i]] : {};
132+
cur = cur[sp[i]];
133+
}
134+
let val = virtualType.applyGetters(cur[sp[sp.length - 1]], obj);
135+
const isPopulateVirtual =
136+
virtualType.options && (virtualType.options.ref || virtualType.options.refPath);
137+
if (isPopulateVirtual && val === undefined) {
138+
if (virtualType.options.justOne) {
139+
val = null;
140+
} else {
141+
val = [];
142+
}
143+
}
144+
cur[sp[sp.length - 1]] = val;
145+
}
146+
}

lib/helpers/query/castUpdate.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const CastError = require('../../error/cast');
44
const MongooseError = require('../../error/mongooseError');
5+
const SchemaString = require('../../schema/string');
56
const StrictModeError = require('../../error/strict');
67
const ValidationError = require('../../error/validation');
78
const castNumber = require('../../cast/number');
@@ -307,6 +308,20 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
307308
continue;
308309
}
309310

311+
hasKeys = true;
312+
} else if (op === '$rename') {
313+
const schematype = new SchemaString(`${prefix}${key}.$rename`);
314+
try {
315+
obj[key] = castUpdateVal(schematype, val, op, key, context, prefix + key);
316+
} catch (error) {
317+
aggregatedError = _appendError(error, context, key, aggregatedError);
318+
}
319+
320+
if (obj[key] === void 0) {
321+
delete obj[key];
322+
continue;
323+
}
324+
310325
hasKeys = true;
311326
} else {
312327
const pathToCheck = (prefix + key);
@@ -372,10 +387,12 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
372387
delete obj[key];
373388
}
374389
} else {
375-
// gh-1845 temporary fix: ignore $rename. See gh-3027 for tracking
376-
// improving this.
377390
if (op === '$rename') {
378-
hasKeys = true;
391+
if (obj[key] == null) {
392+
throw new CastError('String', obj[key], `${prefix}${key}.$rename`);
393+
}
394+
const schematype = new SchemaString(`${prefix}${key}.$rename`);
395+
obj[key] = schematype.castForQuery(null, obj[key], context);
379396
continue;
380397
}
381398

0 commit comments

Comments
 (0)