Skip to content

Commit

Permalink
Merge branch 'master' into 7.2
Browse files Browse the repository at this point in the history
  • Loading branch information
vkarpov15 committed May 19, 2023
2 parents 8b41e8a + 06cd519 commit 3a333f8
Show file tree
Hide file tree
Showing 16 changed files with 221 additions and 16 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
7.1.2 / 2023-05-18
==================
* fix: set timestamps on single nested subdoc in insertMany() #13416 #13343
* fix: mention model name in missing virtual option in getModelsMapForPopulate #13408 #13406 [hasezoey](https://github.com/hasezoey)
* fix: custom debug function not processing all args #13418 #13364
* docs: add virtuals schema options #13407 [hasezoey](https://github.com/hasezoey)
* docs: clarify `JSON.stringify()` virtuals docs #13273 [iatenine](https://github.com/iatenine)

7.1.1 / 2023-05-10
==================
* fix(document): handle set() from top-level underneath a map of mixed #13386
Expand Down
33 changes: 28 additions & 5 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,11 +371,34 @@ Now, mongoose will call your getter function every time you access the
console.log(axl.fullName); // Axl Rose
```

If you use `toJSON()` or `toObject()` mongoose will *not* include virtuals
by default. This includes the output of calling [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)
on a Mongoose document, because [`JSON.stringify()` calls `toJSON()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description).
Pass `{ virtuals: true }` to either
[`toObject()`](api/document.html#document_Document-toObject) or [`toJSON()`](api/document.html#document_Document-toJSON).
If you use `toJSON()` or `toObject()` Mongoose will *not* include virtuals by default.
Pass `{ virtuals: true }` to [`toJSON()`](api/document.html#document_Document-toJSON) or `toObject()` to include virtuals.

```javascript
// Convert `doc` to a POJO, with virtuals attached
doc.toObject({ virtuals: true });

// Equivalent:
doc.toJSON({ virtuals: true });
```

The above caveat for `toJSON()` also includes the output of calling [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) on a Mongoose document, because [`JSON.stringify()` calls `toJSON()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description).
To include virtuals in `JSON.stringify()` output, you can either call `toObject({ virtuals: true })` on the document before calling `JSON.stringify()`, or set the `toJSON: { virtuals: true }` option on your schema.

```javascript
// Explicitly add virtuals to `JSON.stringify()` output
JSON.stringify(doc.toObject({ virtuals: true }));

// Or, to automatically attach virtuals to `JSON.stringify()` output:
const personSchema = new Schema({
name: {
first: String,
last: String
}
}, {
toJSON: { virtuals: true } // <-- include virtuals in `JSON.stringify()`
});
```

You can also add a custom setter to your virtual that will let you set both
first name and last name via the `fullName` virtual.
Expand Down
15 changes: 15 additions & 0 deletions docs/tutorials/virtuals.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Virtuals are typically used for computed properties on documents.
* [Virtuals with Lean](#virtuals-with-lean)
* [Limitations](#limitations)
* [Populate](#populate)
* [Virtuals via schema options](#virtuals-via-schema-options)
* [Further Reading](#further-reading)

## Your First Virtual
Expand Down Expand Up @@ -105,6 +106,20 @@ virtual, you need to specify:
[require:Virtuals.*populate]
```

## Virtuals via schema options

Virtuals can also be defined in the schema-options directly without having to use [`.virtual`](../api/schema.html#Schema.prototype.virtual):

```acquit
[require:Virtuals.*schema-options fullName]
```

The same also goes for virtual options, like virtual populate:

```acquit
[require:Virtuals.*schema-options populate]
```

## Further Reading

* [Virtuals in Mongoose Schemas](../guide.html#virtuals)
Expand Down
8 changes: 7 additions & 1 deletion lib/drivers/node-mongodb-native/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,14 @@ function iter(i) {

if (debug) {
if (typeof debug === 'function') {
let argsToAdd = null;
if (typeof args[args.length - 1] == 'function') {
argsToAdd = args.slice(0, args.length - 1);
} else {
argsToAdd = args;
}
debug.apply(_this,
[_this.name, i].concat(args.slice(0, args.length - 1)));
[_this.name, i].concat(argsToAdd));
} else if (debug instanceof stream.Writable) {
this.$printToStream(_this.name, i, args, debug);
} else {
Expand Down
3 changes: 1 addition & 2 deletions lib/helpers/populate/getModelsMapForPopulate.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,7 @@ function _virtualPopulate(model, docs, options, _virtualRes) {
let foreignField = virtual.options.foreignField;

if (!localField || !foreignField) {
return new MongooseError('If you are populating a virtual, you must set the ' +
'localField and foreignField options');
return new MongooseError(`Cannot populate virtual \`${options.path}\` on model \`${model.modelName}\`, because options \`localField\` and / or \`foreignField\` are missing`);
}

if (typeof localField === 'function') {
Expand Down
8 changes: 3 additions & 5 deletions lib/helpers/timestamps/setupTimestamps.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@ module.exports = function setupTimestamps(schema, timestamps) {
const ts = s.schema.options.timestamps;
return !!ts;
}

if (!timestamps && !childHasTimestamp) {
return;
}

const createdAt = handleTimestampOption(timestamps, 'createdAt');
const updatedAt = handleTimestampOption(timestamps, 'updatedAt');
const currentTime = timestamps != null && timestamps.hasOwnProperty('currentTime') ?
Expand Down Expand Up @@ -57,15 +55,15 @@ module.exports = function setupTimestamps(schema, timestamps) {

schema.methods.initializeTimestamps = function() {
const ts = currentTime != null ?
currentTime() :
this.constructor.base.now();
currentTime() : this.constructor.base.now();


if (createdAt && !this.get(createdAt)) {
this.$set(createdAt, ts);
}
if (updatedAt && !this.get(updatedAt)) {
this.$set(updatedAt, ts);
}

if (this.$isSubdocument) {
return this;
}
Expand Down
1 change: 1 addition & 0 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ let id = 0;
* - [skipVersioning](https://mongoosejs.com/docs/guide.html#skipVersioning): object - paths to exclude from versioning
* - [timestamps](https://mongoosejs.com/docs/guide.html#timestamps): object or boolean - defaults to `false`. If true, Mongoose adds `createdAt` and `updatedAt` properties to your schema and manages those properties for you.
* - [pluginTags](https://mongoosejs.com/docs/guide.html#pluginTags): array of strings - defaults to `undefined`. If set and plugin called with `tags` option, will only apply that plugin to schemas with a matching tag.
* - [virtuals](https://mongoosejs.com/docs/tutorials/virtuals.html#virtuals-via-schema-options): object - virtuals to define, alias for [`.virtual`](https://mongoosejs.com/docs/api/schema.html#Schema.prototype.virtual())
*
* #### Options for Nested Schemas:
*
Expand Down
1 change: 1 addition & 0 deletions lib/schema/SubdocumentPath.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ function _createConstructor(schema, baseClass) {
_embedded.prototype = Object.create(proto);
_embedded.prototype.$__setSchema(schema);
_embedded.prototype.constructor = _embedded;
_embedded.base = schema.base;
_embedded.schema = schema;
_embedded.$isSingleNested = true;
_embedded.events = new EventEmitter();
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mongoose",
"description": "Mongoose MongoDB ODM",
"version": "7.1.1",
"version": "7.1.2",
"author": "Guillermo Rauch <guillermo@learnboost.com>",
"keywords": [
"mongodb",
Expand Down
2 changes: 1 addition & 1 deletion test/docs/debug.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ describe('debug: shell', function() {
await Test.create({ name: 'foo' });
assert.equal(args.length, 1);
assert.equal(args[0][1], 'insertOne');
assert.strictEqual(args[0][3], undefined);
assert.strictEqual(args[0][4], undefined);

await m.disconnect();
});
Expand Down
72 changes: 72 additions & 0 deletions test/docs/virtuals.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,76 @@ describe('Virtuals', function() {
assert.equal(doc.author.email, 'test@gmail.com');
// acquit:ignore:end
});

it('schema-options fullName', function() {
const userSchema = mongoose.Schema({
firstName: String,
lastName: String
}, {
virtuals: {
// Create a virtual property `fullName` with a getter and setter
fullName: {
get() { return `${this.firstName} ${this.lastName}`; },
set(v) {
// `v` is the value being set, so use the value to set
// `firstName` and `lastName`.
const firstName = v.substring(0, v.indexOf(' '));
const lastName = v.substring(v.indexOf(' ') + 1);
this.set({ firstName, lastName });
}
}
}
});
const User = mongoose.model('User', userSchema);

const doc = new User();
// Vanilla JavaScript assignment triggers the setter
doc.fullName = 'Jean-Luc Picard';

doc.fullName; // 'Jean-Luc Picard'
doc.firstName; // 'Jean-Luc'
doc.lastName; // 'Picard'
// acquit:ignore:start
assert.equal(doc.fullName, 'Jean-Luc Picard');
assert.equal(doc.firstName, 'Jean-Luc');
assert.equal(doc.lastName, 'Picard');
// acquit:ignore:end
});

it('schema-options populate', async function() {
const userSchema = mongoose.Schema({ _id: Number, email: String });
const blogPostSchema = mongoose.Schema({
title: String,
authorId: Number
}, {
virtuals: {
// When you `populate()` the `author` virtual, Mongoose will find the
// first document in the User model whose `_id` matches this document's
// `authorId` property.
author: {
options: {
ref: 'User',
localField: 'authorId',
foreignField: '_id',
justOne: true
}
}
}
});
const User = mongoose.model('User', userSchema);
const BlogPost = mongoose.model('BlogPost', blogPostSchema);

// acquit:ignore:start
await BlogPost.deleteMany({});
await User.deleteMany({});
// acquit:ignore:end
await BlogPost.create({ title: 'Introduction to Mongoose', authorId: 1 });
await User.create({ _id: 1, email: 'test@gmail.com' });

const doc = await BlogPost.findOne().populate('author');
doc.author.email; // 'test@gmail.com'
// acquit:ignore:start
assert.equal(doc.author.email, 'test@gmail.com');
// acquit:ignore:end
});
});
41 changes: 41 additions & 0 deletions test/helpers/getModelsMapForPopulate.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict';

const assert = require('assert');
const start = require('../common');
const util = require('../util');

const mongoose = start.mongoose;
const Schema = mongoose.Schema;

describe('getModelsMapForPopulate', function() {
let db;

beforeEach(() => db.deleteModel(/.*/));

before(function() {
db = start();
});

after(async function() {
await db.close();
});

afterEach(() => util.clearTestData(db));
afterEach(() => util.stopRemainingOps(db));

it('should error on missing options on populate', async function() {
const sch = new Schema({
test: mongoose.Schema.Types.ObjectId
}, {
virtuals: {
someVirtual: {}
}
});

const model = db.model('Test', sch);

const doc = await model.create({ test: new mongoose.Types.ObjectId() });

await assert.rejects(() => model.findById(doc._id).populate('someVirtual').exec(), /Cannot populate virtual `someVirtual` on model `Test`, because options `localField` and \/ or `foreignField` are missing/);
});
});
13 changes: 13 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ describe('mongoose module:', function() {
await mongoose.disconnect();
});

it('should collect the args correctly gh-13364', async function() {
const util = require('util');
const mongoose = new Mongoose();
const conn = await mongoose.connect(start.uri);
let actual = '';
mongoose.set('debug', (collectionName, methodName, ...methodArgs) => {
actual = `${collectionName}.${methodName}(${util.inspect(methodArgs).slice(2, -2)})`;
});
const user = conn.connection.collection('User');
await user.findOne({ key: 'value' });
assert.equal('User.findOne({ key: \'value\' })', actual);
});

it('{g,s}etting options', function() {
const mongoose = new Mongoose();

Expand Down
1 change: 1 addition & 0 deletions test/mocha-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports.mochaGlobalSetup = async function mochaGlobalSetup() {
let replseturi;

process.env.RUNTIME_DOWNLOAD = '1'; // ensure MMS is able to download binaries in this context
process.env.MONGOMS_MD5_CHECK = '1';

// set some options when running in a CI
if (process.env.CI) {
Expand Down
2 changes: 1 addition & 1 deletion test/query.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4077,7 +4077,7 @@ describe('Query', function() {

let lastOptions = {};
m.set('debug', function(_coll, _method, ...args) {
lastOptions = args[args.length - 1];
lastOptions = args[args.length - 2];
});

const connDebug = m.createConnection(start.uri);
Expand Down
27 changes: 27 additions & 0 deletions test/schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3051,4 +3051,31 @@ describe('schema', function() {
});
}, { message: 'Cannot create use schema for property "subdoc" because the schema has the timeseries option enabled.' });
});
it('should allow timestamps on a sub document when having _id field in the main document gh-13343', async function() {
const ImageSchema = new Schema({
dimensions: {
type: new Schema({
width: { type: Number, required: true },
height: { type: Number, required: true }
}, { timestamps: true }),
required: true
}
}, { timestamps: true });

const DataSchema = new Schema({
tags: { type: ImageSchema, required: false, _id: false }
});

const Test = db.model('gh13343', DataSchema);
const res = await Test.insertMany([
{
tags: {
dimensions: { width: 960, height: 589 }
}
}
]);
assert.ok(res);
assert.ok(res[0].tags.createdAt);
assert.ok(res[0].tags.updatedAt);
});
});

0 comments on commit 3a333f8

Please # to comment.