Skip to content

Commit de49562

Browse files
committed
Merge branch 'master' of github.com:Automattic/mongoose
2 parents 3ee657d + 50da8e4 commit de49562

27 files changed

+448
-56
lines changed

.github/workflows/test.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767

6868
- name: Load MongoDB binary cache
6969
id: cache-mongodb-binaries
70-
uses: actions/cache@v3
70+
uses: actions/cache@v4
7171
with:
7272
path: ~/.cache/mongodb-binaries
7373
key: ${{ matrix.os }}-${{ matrix.mongodb }}
@@ -101,7 +101,7 @@ jobs:
101101
node-version: 16
102102
- name: Load MongoDB binary cache
103103
id: cache-mongodb-binaries
104-
uses: actions/cache@v3
104+
uses: actions/cache@v4
105105
with:
106106
path: ~/.cache/mongodb-binaries
107107
key: deno-${{ env.MONGOMS_VERSION }}
@@ -141,4 +141,4 @@ jobs:
141141
- name: Check out repo
142142
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
143143
- name: Dependency review
144-
uses: actions/dependency-review-action@v3
144+
uses: actions/dependency-review-action@v4

CHANGELOG.md

+18
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
8.1.2 / 2024-02-08
2+
==================
3+
* fix: include virtuals in document array toString() output if toObject.virtuals set #14335 #14315
4+
* fix(document): handle setting nested path to spread doc with extra properties #14287 #14269
5+
* fix(populate): call setter on virtual populated path with populated doc instead of undefined #14314
6+
* fix(QueryCursor): remove callback parameter of AggregationCursor and QueryCursor #14299 [DevooKim](https://github.com/DevooKim)
7+
* types: add typescript support for arbitrary fields for the options parameter of Model functions which are of type MongooseQueryOptions #14342 #14341 [FaizBShah](https://github.com/FaizBShah)
8+
* types(model): correct return type for findOneAndUpdate with includeResultMetadata and lean set #14336 #14303
9+
* types(connection): add type definition for `createCollections()` #14295 #14279
10+
* docs(timestamps): clarify that replaceOne() and findOneAndReplace() overwrite timestamps #14337 #14309
11+
12+
8.1.1 / 2024-01-24
13+
==================
14+
* fix(model): throw readable error when calling Model() with a string instead of model() #14288 #14281
15+
* fix(document): handle setting nested path to spread doc with extra properties #14287 #14269
16+
* types(query): add back context and setDefaultsOnInsert as Mongoose-specific query options #14284 #14282
17+
* types(query): add missing runValidators back to MongooseQueryOptions #14278 #14275
18+
119
8.1.0 / 2024-01-16
220
==================
321
* feat: upgrade MongoDB driver -> 6.3.0 #14241 #14189 #14108 #14104

docs/guide.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1471,7 +1471,7 @@ schema.searchIndex({
14711471
});
14721472
// Will automatically attempt to create the `my-index` search index.
14731473
const Test = mongoose.model('Test', schema);
1474-
``
1474+
```
14751475

14761476
<h2 id="es6-classes"><a href="#es6-classes">With ES6 Classes</a></h2>
14771477

docs/migrating_to_7.md

+3
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ They always return promises.
8787
* `Aggregate.prototype.exec`
8888
* `Aggregate.prototype.explain`
8989
* `AggregationCursor.prototype.close`
90+
* `AggregationCursor.prototype.next`
91+
* `AggregationCursor.prototype.eachAsync`
9092
* `Connection.prototype.startSession`
9193
* `Connection.prototype.dropCollection`
9294
* `Connection.prototype.createCollection`
@@ -138,6 +140,7 @@ They always return promises.
138140
* `Query.prototype.exec`
139141
* `QueryCursor.prototype.close`
140142
* `QueryCursor.prototype.next`
143+
* `QueryCursor.prototype.eachAsync`
141144

142145
If you are using the above functions with callbacks, we recommend switching to async/await, or promises if async functions don't work for you.
143146
If you need help refactoring a legacy codebase, [this tool from Mastering JS callbacks to async await](https://masteringjs.io/tutorials/tools/callback-to-async-await) using ChatGPT.

docs/timestamps.md

+30
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ console.log(doc.updatedAt); // 2022-02-26T17:08:13.991Z
4747

4848
// Mongoose also blocks changing `createdAt` and sets its own `updatedAt`
4949
// on `findOneAndUpdate()`, `updateMany()`, and other query operations
50+
// **except** `replaceOne()` and `findOneAndReplace()`.
5051
doc = await User.findOneAndUpdate(
5152
{ _id: doc._id },
5253
{ name: 'test3', createdAt: new Date(0), updatedAt: new Date(0) },
@@ -56,6 +57,35 @@ console.log(doc.createdAt); // 2022-02-26T17:08:13.930Z
5657
console.log(doc.updatedAt); // 2022-02-26T17:08:14.008Z
5758
```
5859

60+
Keep in mind that `replaceOne()` and `findOneAndReplace()` overwrite all non-`_id` properties, **including** immutable properties like `createdAt`.
61+
Calling `replaceOne()` or `findOneAndReplace()` will update the `createdAt` timestamp as shown below.
62+
63+
```javascript
64+
// `findOneAndReplace()` and `replaceOne()` without timestamps specified in `replacement`
65+
// sets `createdAt` and `updatedAt` to current time.
66+
doc = await User.findOneAndReplace(
67+
{ _id: doc._id },
68+
{ name: 'test3' },
69+
{ new: true }
70+
);
71+
console.log(doc.createdAt); // 2022-02-26T17:08:14.008Z
72+
console.log(doc.updatedAt); // 2022-02-26T17:08:14.008Z
73+
74+
// `findOneAndReplace()` and `replaceOne()` with timestamps specified in `replacement`
75+
// sets `createdAt` and `updatedAt` to the values in `replacement`.
76+
doc = await User.findOneAndReplace(
77+
{ _id: doc._id },
78+
{
79+
name: 'test3',
80+
createdAt: new Date('2022-06-01'),
81+
updatedAt: new Date('2022-06-01')
82+
},
83+
{ new: true }
84+
);
85+
console.log(doc.createdAt); // 2022-06-01T00:00:00.000Z
86+
console.log(doc.updatedAt); // 2022-06-01T00:00:00.000Z
87+
```
88+
5989
## Alternate Property Names
6090

6191
For the purposes of these docs, we'll always refer to `createdAt` and `updatedAt`.

lib/cursor/aggregationCursor.js

+15-4
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,20 @@ util.inherits(AggregationCursor, Readable);
5757
function _init(model, c, agg) {
5858
if (!model.collection.buffer) {
5959
model.hooks.execPre('aggregate', agg, function() {
60+
if (typeof agg.options?.cursor?.transform === 'function') {
61+
c._transforms.push(agg.options.cursor.transform);
62+
}
63+
6064
c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {});
6165
c.emit('cursor', c.cursor);
6266
});
6367
} else {
6468
model.collection.emitter.once('queue', function() {
6569
model.hooks.execPre('aggregate', agg, function() {
70+
if (typeof agg.options?.cursor?.transform === 'function') {
71+
c._transforms.push(agg.options.cursor.transform);
72+
}
73+
6674
c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {});
6775
c.emit('cursor', c.cursor);
6876
});
@@ -219,21 +227,24 @@ AggregationCursor.prototype.next = async function next() {
219227
* @param {Function} fn
220228
* @param {Object} [options]
221229
* @param {Number} [options.parallel] the number of promises to execute in parallel. Defaults to 1.
222-
* @param {Function} [callback] executed when all docs have been processed
230+
* @param {Number} [options.batchSize=null] if set, Mongoose will call `fn` with an array of at most `batchSize` documents, instead of a single document
231+
* @param {Boolean} [options.continueOnError=false] if true, `eachAsync()` iterates through all docs even if `fn` throws an error. If false, `eachAsync()` throws an error immediately if the given function `fn()` throws an error.
223232
* @return {Promise}
224233
* @api public
225234
* @method eachAsync
226235
*/
227236

228-
AggregationCursor.prototype.eachAsync = function(fn, opts, callback) {
237+
AggregationCursor.prototype.eachAsync = function(fn, opts) {
238+
if (typeof arguments[2] === 'function') {
239+
throw new MongooseError('AggregationCursor.prototype.eachAsync() no longer accepts a callback');
240+
}
229241
const _this = this;
230242
if (typeof opts === 'function') {
231-
callback = opts;
232243
opts = {};
233244
}
234245
opts = opts || {};
235246

236-
return eachAsync(function(cb) { return _next(_this, cb); }, fn, opts, callback);
247+
return eachAsync(function(cb) { return _next(_this, cb); }, fn, opts);
237248
};
238249

239250
/**

lib/cursor/queryCursor.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ QueryCursor.prototype.rewind = function() {
243243
*/
244244

245245
QueryCursor.prototype.next = async function next() {
246-
if (arguments[0] === 'function') {
246+
if (typeof arguments[0] === 'function') {
247247
throw new MongooseError('QueryCursor.prototype.next() no longer accepts a callback');
248248
}
249249
return new Promise((resolve, reject) => {
@@ -277,20 +277,21 @@ QueryCursor.prototype.next = async function next() {
277277
* @param {Number} [options.parallel] the number of promises to execute in parallel. Defaults to 1.
278278
* @param {Number} [options.batchSize] if set, will call `fn()` with arrays of documents with length at most `batchSize`
279279
* @param {Boolean} [options.continueOnError=false] if true, `eachAsync()` iterates through all docs even if `fn` throws an error. If false, `eachAsync()` throws an error immediately if the given function `fn()` throws an error.
280-
* @param {Function} [callback] executed when all docs have been processed
281280
* @return {Promise}
282281
* @api public
283282
* @method eachAsync
284283
*/
285284

286-
QueryCursor.prototype.eachAsync = function(fn, opts, callback) {
285+
QueryCursor.prototype.eachAsync = function(fn, opts) {
286+
if (typeof arguments[2] === 'function') {
287+
throw new MongooseError('QueryCursor.prototype.eachAsync() no longer accepts a callback');
288+
}
287289
if (typeof opts === 'function') {
288-
callback = opts;
289290
opts = {};
290291
}
291292
opts = opts || {};
292293

293-
return eachAsync((cb) => _next(this, cb), fn, opts, callback);
294+
return eachAsync((cb) => _next(this, cb), fn, opts);
294295
};
295296

296297
/**

lib/document.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1162,7 +1162,7 @@ Document.prototype.$set = function $set(path, val, type, options) {
11621162

11631163
// Assume this is a Mongoose document that was copied into a POJO using
11641164
// `Object.assign()` or `{...doc}`
1165-
val = handleSpreadDoc(val);
1165+
val = handleSpreadDoc(val, true);
11661166

11671167
// if this doc is being constructed we should not trigger getters
11681168
const priorVal = (() => {
@@ -4297,7 +4297,8 @@ Document.prototype.inspect = function(options) {
42974297
opts = options;
42984298
opts.minimize = false;
42994299
}
4300-
const ret = this.toObject(opts);
4300+
4301+
const ret = arguments.length > 0 ? this.toObject(opts) : this.toObject();
43014302

43024303
if (ret == null) {
43034304
// If `toObject()` returns null, `this` is still an object, so if `inspect()`

lib/helpers/populate/getModelsMapForPopulate.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
494494

495495
let k = modelNames.length;
496496
while (k--) {
497-
const modelName = modelNames[k];
497+
let modelName = modelNames[k];
498498
if (modelName == null) {
499499
continue;
500500
}
@@ -504,6 +504,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
504504
Model = options.model;
505505
} else if (modelName[modelSymbol]) {
506506
Model = modelName;
507+
modelName = Model.modelName;
507508
} else {
508509
try {
509510
Model = _getModelFromConn(connection, modelName);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict';
2+
3+
/**
4+
* Set a populated virtual value on a document's `$$populatedVirtuals` value
5+
*
6+
* @param {*} populatedVirtuals A document's `$$populatedVirtuals`
7+
* @param {*} name The virtual name
8+
* @param {*} v The result of the populate query
9+
* @param {*} options The populate options. This function handles `justOne` and `count` options.
10+
* @returns {Array<Document>|Document|Object|Array<Object>} the populated virtual value that was set
11+
*/
12+
13+
module.exports = function setPopulatedVirtualValue(populatedVirtuals, name, v, options) {
14+
if (options.justOne || options.count) {
15+
populatedVirtuals[name] = Array.isArray(v) ?
16+
v[0] :
17+
v;
18+
19+
if (typeof populatedVirtuals[name] !== 'object') {
20+
populatedVirtuals[name] = options.count ? v : null;
21+
}
22+
} else {
23+
populatedVirtuals[name] = Array.isArray(v) ?
24+
v :
25+
v == null ? [] : [v];
26+
27+
populatedVirtuals[name] = populatedVirtuals[name].filter(function(doc) {
28+
return doc && typeof doc === 'object';
29+
});
30+
}
31+
32+
return populatedVirtuals[name];
33+
};

lib/model.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const EventEmitter = require('events').EventEmitter;
1313
const Kareem = require('kareem');
1414
const MongooseBuffer = require('./types/buffer');
1515
const MongooseError = require('./error/index');
16+
const ObjectParameterError = require('./error/objectParameter');
1617
const OverwriteModelError = require('./error/overwriteModel');
1718
const Query = require('./query');
1819
const SaveOptions = require('./options/saveOptions');
@@ -118,10 +119,15 @@ const saveToObjectOptions = Object.assign({}, internalToObjectOptions, {
118119

119120
function Model(doc, fields, skipId) {
120121
if (fields instanceof Schema) {
121-
throw new TypeError('2nd argument to `Model` must be a POJO or string, ' +
122+
throw new TypeError('2nd argument to `Model` constructor must be a POJO or string, ' +
122123
'**not** a schema. Make sure you\'re calling `mongoose.model()`, not ' +
123124
'`mongoose.Model()`.');
124125
}
126+
if (typeof doc === 'string') {
127+
throw new TypeError('First argument to `Model` constructor must be an object, ' +
128+
'**not** a string. Make sure you\'re calling `mongoose.model()`, not ' +
129+
'`mongoose.Model()`.');
130+
}
125131
Document.call(this, doc, fields, skipId);
126132
}
127133

@@ -3099,6 +3105,9 @@ Model.$__insertMany = function(arr, options, callback) {
30993105
const toExecute = arr.map((doc, index) =>
31003106
callback => {
31013107
if (!(doc instanceof _this)) {
3108+
if (doc != null && typeof doc !== 'object') {
3109+
return callback(new ObjectParameterError(doc, 'arr.' + index, 'insertMany'));
3110+
}
31023111
try {
31033112
doc = new _this(doc);
31043113
} catch (err) {

lib/plugins/trackTransaction.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function mergeAtomics(destination, source) {
8585
destination.$addToSet = (destination.$addToSet || []).concat(source.$addToSet);
8686
}
8787
if (source.$set != null) {
88-
destination.$set = Object.assign(destination.$set || {}, source.$set);
88+
destination.$set = Array.isArray(source.$set) ? [...source.$set] : Object.assign({}, source.$set);
8989
}
9090

9191
return destination;

lib/schema.js

+20-18
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const handleReadPreferenceAliases = require('./helpers/query/handleReadPreferenc
2020
const idGetter = require('./helpers/schema/idGetter');
2121
const merge = require('./helpers/schema/merge');
2222
const mpath = require('mpath');
23+
const setPopulatedVirtualValue = require('./helpers/populate/setPopulatedVirtualValue');
2324
const setupTimestamps = require('./helpers/timestamps/setupTimestamps');
2425
const utils = require('./utils');
2526
const validateRef = require('./helpers/populate/validateRef');
@@ -2143,6 +2144,18 @@ Schema.prototype.set = function(key, value, tags) {
21432144
if (key === 'strictQuery') {
21442145
_propagateOptionsToImplicitlyCreatedSchemas(this, { strictQuery: value });
21452146
}
2147+
if (key === 'toObject') {
2148+
value = { ...value };
2149+
// Avoid propagating transform to implicitly created schemas re: gh-3279
2150+
delete value.transform;
2151+
_propagateOptionsToImplicitlyCreatedSchemas(this, { toObject: value });
2152+
}
2153+
if (key === 'toJSON') {
2154+
value = { ...value };
2155+
// Avoid propagating transform to implicitly created schemas re: gh-3279
2156+
delete value.transform;
2157+
_propagateOptionsToImplicitlyCreatedSchemas(this, { toJSON: value });
2158+
}
21462159

21472160
return this;
21482161
};
@@ -2288,28 +2301,17 @@ Schema.prototype.virtual = function(name, options) {
22882301
virtual.options = options;
22892302

22902303
virtual.
2291-
set(function(_v) {
2304+
set(function(v) {
22922305
if (!this.$$populatedVirtuals) {
22932306
this.$$populatedVirtuals = {};
22942307
}
22952308

2296-
if (options.justOne || options.count) {
2297-
this.$$populatedVirtuals[name] = Array.isArray(_v) ?
2298-
_v[0] :
2299-
_v;
2300-
2301-
if (typeof this.$$populatedVirtuals[name] !== 'object') {
2302-
this.$$populatedVirtuals[name] = options.count ? _v : null;
2303-
}
2304-
} else {
2305-
this.$$populatedVirtuals[name] = Array.isArray(_v) ?
2306-
_v :
2307-
_v == null ? [] : [_v];
2308-
2309-
this.$$populatedVirtuals[name] = this.$$populatedVirtuals[name].filter(function(doc) {
2310-
return doc && typeof doc === 'object';
2311-
});
2312-
}
2309+
return setPopulatedVirtualValue(
2310+
this.$$populatedVirtuals,
2311+
name,
2312+
v,
2313+
options
2314+
);
23132315
});
23142316

23152317
if (typeof options.get === 'function') {

0 commit comments

Comments
 (0)