Skip to content

Commit

Permalink
Merge branch 'master' of github.com:Automattic/mongoose
Browse files Browse the repository at this point in the history
  • Loading branch information
vkarpov15 committed Feb 15, 2024
2 parents 3ee657d + 50da8e4 commit de49562
Show file tree
Hide file tree
Showing 27 changed files with 448 additions and 56 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:

- name: Load MongoDB binary cache
id: cache-mongodb-binaries
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/mongodb-binaries
key: ${{ matrix.os }}-${{ matrix.mongodb }}
Expand Down Expand Up @@ -101,7 +101,7 @@ jobs:
node-version: 16
- name: Load MongoDB binary cache
id: cache-mongodb-binaries
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/mongodb-binaries
key: deno-${{ env.MONGOMS_VERSION }}
Expand Down Expand Up @@ -141,4 +141,4 @@ jobs:
- name: Check out repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Dependency review
uses: actions/dependency-review-action@v3
uses: actions/dependency-review-action@v4
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
8.1.2 / 2024-02-08
==================
* fix: include virtuals in document array toString() output if toObject.virtuals set #14335 #14315
* fix(document): handle setting nested path to spread doc with extra properties #14287 #14269
* fix(populate): call setter on virtual populated path with populated doc instead of undefined #14314
* fix(QueryCursor): remove callback parameter of AggregationCursor and QueryCursor #14299 [DevooKim](https://github.com/DevooKim)
* 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)
* types(model): correct return type for findOneAndUpdate with includeResultMetadata and lean set #14336 #14303
* types(connection): add type definition for `createCollections()` #14295 #14279
* docs(timestamps): clarify that replaceOne() and findOneAndReplace() overwrite timestamps #14337 #14309

8.1.1 / 2024-01-24
==================
* fix(model): throw readable error when calling Model() with a string instead of model() #14288 #14281
* fix(document): handle setting nested path to spread doc with extra properties #14287 #14269
* types(query): add back context and setDefaultsOnInsert as Mongoose-specific query options #14284 #14282
* types(query): add missing runValidators back to MongooseQueryOptions #14278 #14275

8.1.0 / 2024-01-16
==================
* feat: upgrade MongoDB driver -> 6.3.0 #14241 #14189 #14108 #14104
Expand Down
2 changes: 1 addition & 1 deletion docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1471,7 +1471,7 @@ schema.searchIndex({
});
// Will automatically attempt to create the `my-index` search index.
const Test = mongoose.model('Test', schema);
``
```

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

Expand Down
3 changes: 3 additions & 0 deletions docs/migrating_to_7.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ They always return promises.
* `Aggregate.prototype.exec`
* `Aggregate.prototype.explain`
* `AggregationCursor.prototype.close`
* `AggregationCursor.prototype.next`
* `AggregationCursor.prototype.eachAsync`
* `Connection.prototype.startSession`
* `Connection.prototype.dropCollection`
* `Connection.prototype.createCollection`
Expand Down Expand Up @@ -138,6 +140,7 @@ They always return promises.
* `Query.prototype.exec`
* `QueryCursor.prototype.close`
* `QueryCursor.prototype.next`
* `QueryCursor.prototype.eachAsync`

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.
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.
Expand Down
30 changes: 30 additions & 0 deletions docs/timestamps.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ console.log(doc.updatedAt); // 2022-02-26T17:08:13.991Z

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

Keep in mind that `replaceOne()` and `findOneAndReplace()` overwrite all non-`_id` properties, **including** immutable properties like `createdAt`.
Calling `replaceOne()` or `findOneAndReplace()` will update the `createdAt` timestamp as shown below.

```javascript
// `findOneAndReplace()` and `replaceOne()` without timestamps specified in `replacement`
// sets `createdAt` and `updatedAt` to current time.
doc = await User.findOneAndReplace(
{ _id: doc._id },
{ name: 'test3' },
{ new: true }
);
console.log(doc.createdAt); // 2022-02-26T17:08:14.008Z
console.log(doc.updatedAt); // 2022-02-26T17:08:14.008Z

// `findOneAndReplace()` and `replaceOne()` with timestamps specified in `replacement`
// sets `createdAt` and `updatedAt` to the values in `replacement`.
doc = await User.findOneAndReplace(
{ _id: doc._id },
{
name: 'test3',
createdAt: new Date('2022-06-01'),
updatedAt: new Date('2022-06-01')
},
{ new: true }
);
console.log(doc.createdAt); // 2022-06-01T00:00:00.000Z
console.log(doc.updatedAt); // 2022-06-01T00:00:00.000Z
```

## Alternate Property Names

For the purposes of these docs, we'll always refer to `createdAt` and `updatedAt`.
Expand Down
19 changes: 15 additions & 4 deletions lib/cursor/aggregationCursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,20 @@ util.inherits(AggregationCursor, Readable);
function _init(model, c, agg) {
if (!model.collection.buffer) {
model.hooks.execPre('aggregate', agg, function() {
if (typeof agg.options?.cursor?.transform === 'function') {
c._transforms.push(agg.options.cursor.transform);
}

c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {});
c.emit('cursor', c.cursor);
});
} else {
model.collection.emitter.once('queue', function() {
model.hooks.execPre('aggregate', agg, function() {
if (typeof agg.options?.cursor?.transform === 'function') {
c._transforms.push(agg.options.cursor.transform);
}

c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {});
c.emit('cursor', c.cursor);
});
Expand Down Expand Up @@ -219,21 +227,24 @@ AggregationCursor.prototype.next = async function next() {
* @param {Function} fn
* @param {Object} [options]
* @param {Number} [options.parallel] the number of promises to execute in parallel. Defaults to 1.
* @param {Function} [callback] executed when all docs have been processed
* @param {Number} [options.batchSize=null] if set, Mongoose will call `fn` with an array of at most `batchSize` documents, instead of a single document
* @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.
* @return {Promise}
* @api public
* @method eachAsync
*/

AggregationCursor.prototype.eachAsync = function(fn, opts, callback) {
AggregationCursor.prototype.eachAsync = function(fn, opts) {
if (typeof arguments[2] === 'function') {
throw new MongooseError('AggregationCursor.prototype.eachAsync() no longer accepts a callback');
}
const _this = this;
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
opts = opts || {};

return eachAsync(function(cb) { return _next(_this, cb); }, fn, opts, callback);
return eachAsync(function(cb) { return _next(_this, cb); }, fn, opts);
};

/**
Expand Down
11 changes: 6 additions & 5 deletions lib/cursor/queryCursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ QueryCursor.prototype.rewind = function() {
*/

QueryCursor.prototype.next = async function next() {
if (arguments[0] === 'function') {
if (typeof arguments[0] === 'function') {
throw new MongooseError('QueryCursor.prototype.next() no longer accepts a callback');
}
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -277,20 +277,21 @@ QueryCursor.prototype.next = async function next() {
* @param {Number} [options.parallel] the number of promises to execute in parallel. Defaults to 1.
* @param {Number} [options.batchSize] if set, will call `fn()` with arrays of documents with length at most `batchSize`
* @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.
* @param {Function} [callback] executed when all docs have been processed
* @return {Promise}
* @api public
* @method eachAsync
*/

QueryCursor.prototype.eachAsync = function(fn, opts, callback) {
QueryCursor.prototype.eachAsync = function(fn, opts) {
if (typeof arguments[2] === 'function') {
throw new MongooseError('QueryCursor.prototype.eachAsync() no longer accepts a callback');
}
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
opts = opts || {};

return eachAsync((cb) => _next(this, cb), fn, opts, callback);
return eachAsync((cb) => _next(this, cb), fn, opts);
};

/**
Expand Down
5 changes: 3 additions & 2 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -1162,7 +1162,7 @@ Document.prototype.$set = function $set(path, val, type, options) {

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

// if this doc is being constructed we should not trigger getters
const priorVal = (() => {
Expand Down Expand Up @@ -4297,7 +4297,8 @@ Document.prototype.inspect = function(options) {
opts = options;
opts.minimize = false;
}
const ret = this.toObject(opts);

const ret = arguments.length > 0 ? this.toObject(opts) : this.toObject();

if (ret == null) {
// If `toObject()` returns null, `this` is still an object, so if `inspect()`
Expand Down
3 changes: 2 additions & 1 deletion lib/helpers/populate/getModelsMapForPopulate.js
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re

let k = modelNames.length;
while (k--) {
const modelName = modelNames[k];
let modelName = modelNames[k];
if (modelName == null) {
continue;
}
Expand All @@ -504,6 +504,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
Model = options.model;
} else if (modelName[modelSymbol]) {
Model = modelName;
modelName = Model.modelName;
} else {
try {
Model = _getModelFromConn(connection, modelName);
Expand Down
33 changes: 33 additions & 0 deletions lib/helpers/populate/setPopulatedVirtualValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';

/**
* Set a populated virtual value on a document's `$$populatedVirtuals` value
*
* @param {*} populatedVirtuals A document's `$$populatedVirtuals`
* @param {*} name The virtual name
* @param {*} v The result of the populate query
* @param {*} options The populate options. This function handles `justOne` and `count` options.
* @returns {Array<Document>|Document|Object|Array<Object>} the populated virtual value that was set
*/

module.exports = function setPopulatedVirtualValue(populatedVirtuals, name, v, options) {
if (options.justOne || options.count) {
populatedVirtuals[name] = Array.isArray(v) ?
v[0] :
v;

if (typeof populatedVirtuals[name] !== 'object') {
populatedVirtuals[name] = options.count ? v : null;
}
} else {
populatedVirtuals[name] = Array.isArray(v) ?
v :
v == null ? [] : [v];

populatedVirtuals[name] = populatedVirtuals[name].filter(function(doc) {
return doc && typeof doc === 'object';
});
}

return populatedVirtuals[name];
};
11 changes: 10 additions & 1 deletion lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const EventEmitter = require('events').EventEmitter;
const Kareem = require('kareem');
const MongooseBuffer = require('./types/buffer');
const MongooseError = require('./error/index');
const ObjectParameterError = require('./error/objectParameter');
const OverwriteModelError = require('./error/overwriteModel');
const Query = require('./query');
const SaveOptions = require('./options/saveOptions');
Expand Down Expand Up @@ -118,10 +119,15 @@ const saveToObjectOptions = Object.assign({}, internalToObjectOptions, {

function Model(doc, fields, skipId) {
if (fields instanceof Schema) {
throw new TypeError('2nd argument to `Model` must be a POJO or string, ' +
throw new TypeError('2nd argument to `Model` constructor must be a POJO or string, ' +
'**not** a schema. Make sure you\'re calling `mongoose.model()`, not ' +
'`mongoose.Model()`.');
}
if (typeof doc === 'string') {
throw new TypeError('First argument to `Model` constructor must be an object, ' +
'**not** a string. Make sure you\'re calling `mongoose.model()`, not ' +
'`mongoose.Model()`.');
}
Document.call(this, doc, fields, skipId);
}

Expand Down Expand Up @@ -3099,6 +3105,9 @@ Model.$__insertMany = function(arr, options, callback) {
const toExecute = arr.map((doc, index) =>
callback => {
if (!(doc instanceof _this)) {
if (doc != null && typeof doc !== 'object') {
return callback(new ObjectParameterError(doc, 'arr.' + index, 'insertMany'));
}
try {
doc = new _this(doc);
} catch (err) {
Expand Down
2 changes: 1 addition & 1 deletion lib/plugins/trackTransaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function mergeAtomics(destination, source) {
destination.$addToSet = (destination.$addToSet || []).concat(source.$addToSet);
}
if (source.$set != null) {
destination.$set = Object.assign(destination.$set || {}, source.$set);
destination.$set = Array.isArray(source.$set) ? [...source.$set] : Object.assign({}, source.$set);
}

return destination;
Expand Down
38 changes: 20 additions & 18 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const handleReadPreferenceAliases = require('./helpers/query/handleReadPreferenc
const idGetter = require('./helpers/schema/idGetter');
const merge = require('./helpers/schema/merge');
const mpath = require('mpath');
const setPopulatedVirtualValue = require('./helpers/populate/setPopulatedVirtualValue');
const setupTimestamps = require('./helpers/timestamps/setupTimestamps');
const utils = require('./utils');
const validateRef = require('./helpers/populate/validateRef');
Expand Down Expand Up @@ -2143,6 +2144,18 @@ Schema.prototype.set = function(key, value, tags) {
if (key === 'strictQuery') {
_propagateOptionsToImplicitlyCreatedSchemas(this, { strictQuery: value });
}
if (key === 'toObject') {
value = { ...value };
// Avoid propagating transform to implicitly created schemas re: gh-3279
delete value.transform;
_propagateOptionsToImplicitlyCreatedSchemas(this, { toObject: value });
}
if (key === 'toJSON') {
value = { ...value };
// Avoid propagating transform to implicitly created schemas re: gh-3279
delete value.transform;
_propagateOptionsToImplicitlyCreatedSchemas(this, { toJSON: value });
}

return this;
};
Expand Down Expand Up @@ -2288,28 +2301,17 @@ Schema.prototype.virtual = function(name, options) {
virtual.options = options;

virtual.
set(function(_v) {
set(function(v) {
if (!this.$$populatedVirtuals) {
this.$$populatedVirtuals = {};
}

if (options.justOne || options.count) {
this.$$populatedVirtuals[name] = Array.isArray(_v) ?
_v[0] :
_v;

if (typeof this.$$populatedVirtuals[name] !== 'object') {
this.$$populatedVirtuals[name] = options.count ? _v : null;
}
} else {
this.$$populatedVirtuals[name] = Array.isArray(_v) ?
_v :
_v == null ? [] : [_v];

this.$$populatedVirtuals[name] = this.$$populatedVirtuals[name].filter(function(doc) {
return doc && typeof doc === 'object';
});
}
return setPopulatedVirtualValue(
this.$$populatedVirtuals,
name,
v,
options
);
});

if (typeof options.get === 'function') {
Expand Down

0 comments on commit de49562

Please sign in to comment.