Skip to content

Commit

Permalink
Merge branch 'master' into gh-pages
Browse files Browse the repository at this point in the history
  • Loading branch information
vkarpov15 committed Feb 8, 2024
2 parents 80ec48a + c9877b6 commit a8b9eec
Show file tree
Hide file tree
Showing 19 changed files with 244 additions and 49 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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
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
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
11 changes: 7 additions & 4 deletions lib/cursor/aggregationCursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,21 +219,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
3 changes: 2 additions & 1 deletion lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -4288,7 +4288,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
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];
};
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
11 changes: 11 additions & 0 deletions lib/types/documentArray/methods/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const arrayPathSymbol = require('../../../helpers/symbols').arrayPathSymbol;
const arraySchemaSymbol = require('../../../helpers/symbols').arraySchemaSymbol;
const documentArrayParent = require('../../../helpers/symbols').documentArrayParent;

const _baseToString = Array.prototype.toString;

const methods = {
/*!
* ignore
Expand All @@ -22,6 +24,15 @@ const methods = {
return this.toObject(internalToObjectOptions);
},

toString() {
return _baseToString.call(this.__array.map(subdoc => {
if (subdoc != null && subdoc.$__ != null) {
return subdoc.toString();
}
return subdoc;
}));
},

/*!
* ignore
*/
Expand Down
6 changes: 1 addition & 5 deletions lib/types/subdocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,11 +400,7 @@ Subdocument.prototype.populate = function() {
*/

Subdocument.prototype.inspect = function() {
return this.toObject({
transform: false,
virtuals: false,
flattenDecimals: false
});
return this.toObject();
};

if (util.inspect.custom) {
Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mongoose",
"description": "Mongoose MongoDB ODM",
"version": "8.1.1",
"version": "8.1.2",
"author": "Guillermo Rauch <guillermo@learnboost.com>",
"keywords": [
"mongodb",
Expand All @@ -28,8 +28,8 @@
"sift": "16.0.1"
},
"devDependencies": {
"@babel/core": "7.23.7",
"@babel/preset-env": "7.23.7",
"@babel/core": "7.23.9",
"@babel/preset-env": "7.23.9",
"@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^6.2.1",
"acquit": "1.3.0",
Expand All @@ -43,7 +43,7 @@
"buffer": "^5.6.0",
"cheerio": "1.0.0-rc.12",
"crypto-browserify": "3.12.0",
"dotenv": "16.3.1",
"dotenv": "16.4.1",
"dox": "1.0.0",
"eslint": "8.56.0",
"eslint-plugin-markdown": "^3.0.1",
Expand All @@ -53,7 +53,7 @@
"highlight.js": "11.8.0",
"lodash.isequal": "4.5.0",
"lodash.isequalwith": "4.4.0",
"markdownlint-cli2": "^0.11.0",
"markdownlint-cli2": "^0.12.1",
"marked": "4.3.0",
"mkdirp": "^3.0.1",
"mocha": "10.2.0",
Expand All @@ -65,10 +65,10 @@
"q": "1.5.1",
"sinon": "17.0.1",
"stream-browserify": "3.0.0",
"tsd": "0.30.3",
"tsd": "0.30.4",
"typescript": "5.3.3",
"uuid": "9.0.1",
"webpack": "5.89.0"
"webpack": "5.90.1"
},
"directories": {
"lib": "./lib/mongoose"
Expand Down
19 changes: 19 additions & 0 deletions test/document.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12953,6 +12953,25 @@ describe('document', function() {
};
assert.equal(person.address.zip, 54321);
});

it('includes virtuals in doc array toString() output if virtuals enabled on toObject (gh-14315)', function() {
const schema = new Schema({
docArr: [{ childId: mongoose.ObjectId }]
});
schema.virtual('docArr.child', { ref: 'Child', localField: 'docArr.childId', foreignField: '_id' });
schema.set('toObject', { virtuals: true });
schema.set('toJSON', { virtuals: true });
const Test = db.model('Test', schema);
const Child = db.model('Child', new Schema({
name: String
}));

const child = new Child({ name: 'test child' });
const doc = new Test({ docArr: [{ childId: child._id }] });
doc.docArr[0].child = child;
assert.ok(doc.docArr.toString().includes('child'), doc.docArr.toString());
assert.ok(doc.docArr.toString().includes('test child'), doc.docArr.toString());
});
});

describe('Check if instance function that is supplied in schema option is availabe', function() {
Expand Down
Loading

0 comments on commit a8b9eec

Please sign in to comment.