diff --git a/CHANGELOG.md b/CHANGELOG.md
index 447adcf183b..9ebaf9e4e54 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,23 @@
+8.3.2 / 2024-04-16
+==================
+ * fix(populate): avoid match function filtering out null values in populate result #14518 #14494
+ * types(query): make FilterQuery props resolve to any for generics support #14510 #14473 #14459
+ * types(DocumentArray): pass DocType generic to Document for correct toJSON() and toObject() return types #14526 #14469
+ * types(models): fix incorrect bulk write options #14513 [emiljanitzek](https://github.com/emiljanitzek)
+ * docs: add documentation for calling schema.post() with async function #14514 #14305
+
+7.6.11 / 2024-04-11
+===================
+ * fix(populate): avoid match function filtering out null values in populate result #14518
+ * fix(schema): support setting discriminator options in Schema.prototype.discriminator() #14493 #14448
+ * fix(schema): deduplicate idGetter so creating multiple models with same schema doesn't result in multiple id getters #14492 #14457
+
+6.12.8 / 2024-04-10
+===================
+ * fix(document): handle virtuals that are stored as objects but getter returns string with toJSON #14468 #14446
+ * fix(schematype): consistently set wasPopulated to object with `value` property rather than boolean #14418
+ * docs(model): add extra note about lean option for insertMany() skipping casting #14415 #14376
+
8.3.1 / 2024-04-08
==================
* fix(document): make update minimization unset property rather than setting to null #14504 #14445
@@ -68,7 +88,6 @@
* docs(connections): add note about using asPromise() with createConnection() for error handling #14364 #14266
* docs(model+query+findoneandupdate): add more details about overwriteDiscriminatorKey option to docs #14264 #14246
-<<<<<<< HEAD
8.2.0 / 2024-02-22
==================
* feat(model): add recompileSchema() function to models to allow applying schema changes after compiling #14306 #14296
@@ -102,6 +121,11 @@
* types(query): add back context and setDefaultsOnInsert as Mongoose-specific query options #14284 #14282
* types(query): add missing runValidators back to MongooseQueryOptions #14278 #14275
+6.12.6 / 2024-01-22
+===================
+ * fix(collection): correctly handle buffer timeouts with find() #14277
+ * fix(document): allow calling push() with different $position arguments #14254
+
8.1.0 / 2024-01-16
==================
* feat: upgrade MongoDB driver -> 6.3.0 #14241 #14189 #14108 #14104
@@ -126,12 +150,6 @@
* docs: update TLS/SSL guide for Mongoose v8 - MongoDB v6 driver deprecations #14170 [andylwelch](https://github.com/andylwelch)
* docs: update findOneAndUpdate tutorial to use includeResultMetadata #14208 #14207
* docs: clarify disabling _id on subdocs #14195 #14194
-=======
-6.12.6 / 2024-01-22
-===================
- * fix(collection): correctly handle buffer timeouts with find() #14277
- * fix(document): allow calling push() with different $position arguments #14254
->>>>>>> 7.x
7.6.8 / 2024-01-08
==================
@@ -432,6 +450,7 @@
* perf: speed up mapOfSubdocs benchmark by 4x by avoiding unnecessary O(n^2) loop in getPathsToValidate() #13614
7.3.4 / 2023-07-12
+==================
* chore: release 7.4.4 to overwrite accidental publish of 5.13.20 to latest tag
6.11.3 / 2023-07-11
diff --git a/docs/middleware.md b/docs/middleware.md
index 41440c9a6bc..03a40983a7a 100644
--- a/docs/middleware.md
+++ b/docs/middleware.md
@@ -151,7 +151,7 @@ schema.pre('save', function() {
then(() => doMoreStuff());
});
-// Or, in Node.js >= 7.6.0:
+// Or, using async functions
schema.pre('save', async function() {
await doStuff();
await doMoreStuff();
@@ -250,9 +250,7 @@ schema.post('deleteOne', function(doc) {
-If your post hook function takes at least 2 parameters, mongoose will
-assume the second parameter is a `next()` function that you will call to
-trigger the next middleware in the sequence.
+If your post hook function takes at least 2 parameters, mongoose will assume the second parameter is a `next()` function that you will call to trigger the next middleware in the sequence.
```javascript
// Takes 2 parameters: this is an asynchronous post hook
@@ -271,6 +269,25 @@ schema.post('save', function(doc, next) {
});
```
+You can also pass an async function to `post()`.
+If you pass an async function that takes at least 2 parameters, you are still responsible for calling `next()`.
+However, you can also pass in an async function that takes less than 2 parameters, and Mongoose will wait for the promise to resolve.
+
+```javascript
+schema.post('save', async function(doc) {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ console.log('post1');
+ // If less than 2 parameters, no need to call `next()`
+});
+
+schema.post('save', async function(doc, next) {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ console.log('post1');
+ // If there's a `next` parameter, you need to call `next()`.
+ next();
+});
+```
+
Calling `pre()` or `post()` after [compiling a model](models.html#compiling)
diff --git a/lib/document.js b/lib/document.js
index fcfb6026ec6..b5f949b2fa5 100644
--- a/lib/document.js
+++ b/lib/document.js
@@ -1053,7 +1053,11 @@ Document.prototype.$set = function $set(path, val, type, options) {
if (path.$__isNested) {
path = path.toObject();
} else {
- path = path._doc;
+ // This ternary is to support gh-7898 (copying virtuals if same schema)
+ // while not breaking gh-10819, which for some reason breaks if we use toObject()
+ path = path.$__schema === this.$__schema
+ ? applyVirtuals(path, { ...path._doc })
+ : path._doc;
}
}
if (path == null) {
@@ -4087,6 +4091,7 @@ function applyVirtuals(self, json, options, toObjectOptions) {
? toObjectOptions.aliases
: true;
+ options = options || {};
let virtualsToApply = null;
if (Array.isArray(options.virtuals)) {
virtualsToApply = new Set(options.virtuals);
@@ -4103,7 +4108,6 @@ function applyVirtuals(self, json, options, toObjectOptions) {
return json;
}
- options = options || {};
for (i = 0; i < numPaths; ++i) {
path = paths[i];
@@ -4184,7 +4188,12 @@ function applyGetters(self, json, options) {
for (let ii = 0; ii < plen; ++ii) {
part = parts[ii];
v = cur[part];
- if (ii === last) {
+ // If we've reached a non-object part of the branch, continuing would
+ // cause "Cannot create property 'foo' on string 'bar'" error.
+ // Necessary for mongoose-intl plugin re: gh-14446
+ if (branch != null && typeof branch !== 'object') {
+ break;
+ } else if (ii === last) {
const val = self.$get(path);
branch[part] = clone(val, options);
if (Array.isArray(branch[part]) && schema.paths[path].$embeddedSchemaType) {
diff --git a/lib/helpers/discriminator/applyEmbeddedDiscriminators.js b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js
index fa0c41c5be5..01593a9b316 100644
--- a/lib/helpers/discriminator/applyEmbeddedDiscriminators.js
+++ b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js
@@ -20,12 +20,15 @@ function applyEmbeddedDiscriminators(schema, seen = new WeakSet(), overwriteExis
continue;
}
for (const discriminatorKey of schemaType.schema._applyDiscriminators.keys()) {
- const discriminatorSchema = schemaType.schema._applyDiscriminators.get(discriminatorKey);
+ const {
+ schema: discriminatorSchema,
+ options
+ } = schemaType.schema._applyDiscriminators.get(discriminatorKey);
applyEmbeddedDiscriminators(discriminatorSchema, seen);
schemaType.discriminator(
discriminatorKey,
discriminatorSchema,
- overwriteExisting ? { overwriteExisting: true } : null
+ overwriteExisting ? { ...options, overwriteExisting: true } : options
);
}
schemaType._appliedDiscriminators = true;
diff --git a/lib/helpers/populate/assignVals.js b/lib/helpers/populate/assignVals.js
index eeeb6c622c9..9aff29fd538 100644
--- a/lib/helpers/populate/assignVals.js
+++ b/lib/helpers/populate/assignVals.js
@@ -101,8 +101,8 @@ module.exports = function assignVals(o) {
valueToSet = numDocs(rawIds[i]);
} else if (Array.isArray(o.match)) {
valueToSet = Array.isArray(rawIds[i]) ?
- rawIds[i].filter(sift(o.match[i])) :
- [rawIds[i]].filter(sift(o.match[i]))[0];
+ rawIds[i].filter(v => v == null || sift(o.match[i])(v)) :
+ [rawIds[i]].filter(v => v == null || sift(o.match[i])(v))[0];
} else {
valueToSet = rawIds[i];
}
diff --git a/lib/helpers/populate/getModelsMapForPopulate.js b/lib/helpers/populate/getModelsMapForPopulate.js
index ae313bb1e87..276698217d0 100644
--- a/lib/helpers/populate/getModelsMapForPopulate.js
+++ b/lib/helpers/populate/getModelsMapForPopulate.js
@@ -410,26 +410,12 @@ function _virtualPopulate(model, docs, options, _virtualRes) {
justOne = options.justOne;
}
+ modelNames = virtual._getModelNamesForPopulate(doc);
if (virtual.options.refPath) {
- modelNames =
- modelNamesFromRefPath(virtual.options.refPath, doc, options.path);
justOne = !!virtual.options.justOne;
data.isRefPath = true;
} else if (virtual.options.ref) {
- let normalizedRef;
- if (typeof virtual.options.ref === 'function' && !virtual.options.ref[modelSymbol]) {
- normalizedRef = virtual.options.ref.call(doc, doc);
- } else {
- normalizedRef = virtual.options.ref;
- }
justOne = !!virtual.options.justOne;
- // When referencing nested arrays, the ref should be an Array
- // of modelNames.
- if (Array.isArray(normalizedRef)) {
- modelNames = normalizedRef;
- } else {
- modelNames = [normalizedRef];
- }
}
data.isVirtual = true;
diff --git a/lib/model.js b/lib/model.js
index 477a14c76b9..197a2d4a952 100644
--- a/lib/model.js
+++ b/lib/model.js
@@ -4774,7 +4774,7 @@ function _assign(model, vals, mod, assignmentOpts) {
}
// flag each as result of population
if (!lean) {
- val.$__.wasPopulated = val.$__.wasPopulated || true;
+ val.$__.wasPopulated = val.$__.wasPopulated || { value: _val };
}
}
}
diff --git a/lib/mongoose.js b/lib/mongoose.js
index 97475a76868..915720b59f7 100644
--- a/lib/mongoose.js
+++ b/lib/mongoose.js
@@ -637,7 +637,11 @@ Mongoose.prototype._model = function(name, schema, collection, options) {
if (schema._applyDiscriminators != null) {
for (const disc of schema._applyDiscriminators.keys()) {
- model.discriminator(disc, schema._applyDiscriminators.get(disc));
+ const {
+ schema: discriminatorSchema,
+ options
+ } = schema._applyDiscriminators.get(disc);
+ model.discriminator(disc, discriminatorSchema, options);
}
}
diff --git a/lib/schema.js b/lib/schema.js
index 2b833e5ebc3..04c631eb799 100644
--- a/lib/schema.js
+++ b/lib/schema.js
@@ -626,12 +626,18 @@ Schema.prototype.defaultOptions = function(options) {
*
* @param {String} name the name of the discriminator
* @param {Schema} schema the discriminated Schema
+ * @param {Object} [options] discriminator options
+ * @param {String} [options.value] the string stored in the `discriminatorKey` property. If not specified, Mongoose uses the `name` parameter.
+ * @param {Boolean} [options.clone=true] By default, `discriminator()` clones the given `schema`. Set to `false` to skip cloning.
+ * @param {Boolean} [options.overwriteModels=false] by default, Mongoose does not allow you to define a discriminator with the same name as another discriminator. Set this to allow overwriting discriminators with the same name.
+ * @param {Boolean} [options.mergeHooks=true] By default, Mongoose merges the base schema's hooks with the discriminator schema's hooks. Set this option to `false` to make Mongoose use the discriminator schema's hooks instead.
+ * @param {Boolean} [options.mergePlugins=true] By default, Mongoose merges the base schema's plugins with the discriminator schema's plugins. Set this option to `false` to make Mongoose use the discriminator schema's plugins instead.
* @return {Schema} the Schema instance
* @api public
*/
-Schema.prototype.discriminator = function(name, schema) {
+Schema.prototype.discriminator = function(name, schema, options) {
this._applyDiscriminators = this._applyDiscriminators || new Map();
- this._applyDiscriminators.set(name, schema);
+ this._applyDiscriminators.set(name, { schema, options });
return this;
};
@@ -2291,7 +2297,10 @@ Schema.prototype.virtual = function(name, options) {
throw new Error('Reference virtuals require `foreignField` option');
}
- this.pre('init', function virtualPreInit(obj) {
+ const virtual = this.virtual(name);
+ virtual.options = options;
+
+ this.pre('init', function virtualPreInit(obj, opts) {
if (mpath.has(name, obj)) {
const _v = mpath.get(name, obj);
if (!this.$$populatedVirtuals) {
@@ -2308,13 +2317,26 @@ Schema.prototype.virtual = function(name, options) {
_v == null ? [] : [_v];
}
+ if (opts?.hydratedPopulatedDocs && !options.count) {
+ const modelNames = virtual._getModelNamesForPopulate(this);
+ const populatedVal = this.$$populatedVirtuals[name];
+ if (!Array.isArray(populatedVal) && !populatedVal.$__ && modelNames?.length === 1) {
+ const PopulateModel = this.db.model(modelNames[0]);
+ this.$$populatedVirtuals[name] = PopulateModel.hydrate(populatedVal);
+ } else if (Array.isArray(populatedVal) && modelNames?.length === 1) {
+ const PopulateModel = this.db.model(modelNames[0]);
+ for (let i = 0; i < populatedVal.length; ++i) {
+ if (!populatedVal[i].$__) {
+ populatedVal[i] = PopulateModel.hydrate(populatedVal[i]);
+ }
+ }
+ }
+ }
+
mpath.unset(name, obj);
}
});
- const virtual = this.virtual(name);
- virtual.options = options;
-
virtual.
set(function(v) {
if (!this.$$populatedVirtuals) {
diff --git a/lib/schema/documentArray.js b/lib/schema/documentArray.js
index 50ae0e12a6b..2af276500ff 100644
--- a/lib/schema/documentArray.js
+++ b/lib/schema/documentArray.js
@@ -447,19 +447,9 @@ SchemaDocumentArray.prototype.cast = function(value, doc, init, prev, options) {
const Constructor = getConstructor(this.casterConstructor, rawArray[i]);
- // Check if the document has a different schema (re gh-3701)
- if (rawArray[i].$__ != null && !(rawArray[i] instanceof Constructor)) {
- const spreadDoc = handleSpreadDoc(rawArray[i], true);
- if (rawArray[i] !== spreadDoc) {
- rawArray[i] = spreadDoc;
- } else {
- rawArray[i] = rawArray[i].toObject({
- transform: false,
- // Special case: if different model, but same schema, apply virtuals
- // re: gh-7898
- virtuals: rawArray[i].schema === Constructor.schema
- });
- }
+ const spreadDoc = handleSpreadDoc(rawArray[i], true);
+ if (rawArray[i] !== spreadDoc) {
+ rawArray[i] = spreadDoc;
}
if (rawArray[i] instanceof Subdocument) {
diff --git a/lib/schemaType.js b/lib/schemaType.js
index eb9be85f2c1..b54e83fd6a8 100644
--- a/lib/schemaType.js
+++ b/lib/schemaType.js
@@ -1542,7 +1542,7 @@ SchemaType.prototype._castRef = function _castRef(value, doc, init) {
}
if (value.$__ != null) {
- value.$__.wasPopulated = value.$__.wasPopulated || true;
+ value.$__.wasPopulated = value.$__.wasPopulated || { value: value._id };
return value;
}
@@ -1568,7 +1568,7 @@ SchemaType.prototype._castRef = function _castRef(value, doc, init) {
!doc.$__.populated[path].options.options ||
!doc.$__.populated[path].options.options.lean) {
ret = new pop.options[populateModelSymbol](value);
- ret.$__.wasPopulated = true;
+ ret.$__.wasPopulated = { value: ret._id };
}
return ret;
diff --git a/lib/virtualType.js b/lib/virtualType.js
index 87c912fdd70..2008ebf8bb4 100644
--- a/lib/virtualType.js
+++ b/lib/virtualType.js
@@ -1,7 +1,10 @@
'use strict';
+const modelNamesFromRefPath = require('./helpers/populate/modelNamesFromRefPath');
const utils = require('./utils');
+const modelSymbol = require('./helpers/symbols').modelSymbol;
+
/**
* VirtualType constructor
*
@@ -168,6 +171,32 @@ VirtualType.prototype.applySetters = function(value, doc) {
return v;
};
+/**
+ * Get the names of models used to populate this model given a doc
+ *
+ * @param {Document} doc
+ * @return {Array | null}
+ * @api private
+ */
+
+VirtualType.prototype._getModelNamesForPopulate = function _getModelNamesForPopulate(doc) {
+ if (this.options.refPath) {
+ return modelNamesFromRefPath(this.options.refPath, doc, this.path);
+ }
+
+ let normalizedRef = null;
+ if (typeof this.options.ref === 'function' && !this.options.ref[modelSymbol]) {
+ normalizedRef = this.options.ref.call(doc, doc);
+ } else {
+ normalizedRef = this.options.ref;
+ }
+ if (normalizedRef != null && !Array.isArray(normalizedRef)) {
+ return [normalizedRef];
+ }
+
+ return normalizedRef;
+};
+
/*!
* exports
*/
diff --git a/package.json b/package.json
index 5036ca37f78..121426ec6cf 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "mongoose",
"description": "Mongoose MongoDB ODM",
- "version": "8.3.1",
+ "version": "8.3.2",
"author": "Guillermo Rauch ",
"keywords": [
"mongodb",
diff --git a/test/document.test.js b/test/document.test.js
index 6e125b82fc0..a770b3b8352 100644
--- a/test/document.test.js
+++ b/test/document.test.js
@@ -13234,7 +13234,100 @@ describe('document', function() {
const savedDocSecond = await Test.findById(doc.id).orFail();
assert.deepStrictEqual(savedDocSecond.toObject({ minimize: false }).sub, {});
+ });
+ it('avoids depopulating populated subdocs underneath document arrays when copying to another document (gh-14418)', async function() {
+ const cartSchema = new mongoose.Schema({
+ products: [
+ {
+ product: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'Product'
+ },
+ quantity: Number
+ }
+ ],
+ singleProduct: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'Product'
+ }
+ });
+ const purchaseSchema = new mongoose.Schema({
+ products: [
+ {
+ product: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'Product'
+ },
+ quantity: Number
+ }
+ ],
+ singleProduct: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'Product'
+ }
+ });
+ const productSchema = new mongoose.Schema({
+ name: String
+ });
+
+ const Cart = db.model('Cart', cartSchema);
+ const Purchase = db.model('Purchase', purchaseSchema);
+ const Product = db.model('Product', productSchema);
+
+ const dbProduct = await Product.create({ name: 'Bug' });
+
+ const dbCart = await Cart.create({
+ products: [
+ {
+ product: dbProduct,
+ quantity: 2
+ }
+ ],
+ singleProduct: dbProduct
+ });
+
+ const foundCart = await Cart.findById(dbCart._id).
+ populate('products.product singleProduct');
+
+ const purchaseFromDbCart = new Purchase({
+ products: foundCart.products,
+ singleProduct: foundCart.singleProduct
+ });
+ assert.equal(purchaseFromDbCart.products[0].product.name, 'Bug');
+ assert.equal(purchaseFromDbCart.singleProduct.name, 'Bug');
+ });
+
+ it('handles virtuals that are stored as objects but getter returns string with toJSON (gh-14446)', async function() {
+ const childSchema = new mongoose.Schema();
+
+ childSchema.virtual('name')
+ .set(function(values) {
+ for (const [lang, val] of Object.entries(values)) {
+ this.set(`name.${lang}`, val);
+ }
+ })
+ .get(function() {
+ return this.$__getValue(`name.${this.lang}`);
+ });
+
+ childSchema.add({ name: { en: { type: String }, de: { type: String } } });
+
+ const ChildModel = db.model('Child', childSchema);
+ const ParentModel = db.model('Parent', new mongoose.Schema({
+ children: [childSchema]
+ }));
+
+ const child = await ChildModel.create({ name: { en: 'Stephen', de: 'Stefan' } });
+ child.lang = 'en';
+ assert.equal(child.name, 'Stephen');
+
+ const parent = await ParentModel.create({
+ children: [{ name: { en: 'Stephen', de: 'Stefan' } }]
+ });
+ parent.children[0].lang = 'de';
+ const obj = parent.toJSON({ getters: true });
+ assert.equal(obj.children[0].name, 'Stefan');
});
});
diff --git a/test/model.hydrate.test.js b/test/model.hydrate.test.js
index bc6632f5b15..63947f95961 100644
--- a/test/model.hydrate.test.js
+++ b/test/model.hydrate.test.js
@@ -117,5 +117,61 @@ describe('model', function() {
const C = Company.hydrate(company, null, { hydratedPopulatedDocs: true });
assert.equal(C.users[0].name, 'Val');
});
+ it('should hydrate documents in virtual populate (gh-14503)', async function() {
+ const StorySchema = new Schema({
+ userId: {
+ type: Schema.Types.ObjectId,
+ ref: 'User'
+ },
+ title: {
+ type: String
+ }
+ }, { timestamps: true });
+
+ const UserSchema = new Schema({
+ name: String
+ }, { timestamps: true });
+
+ UserSchema.virtual('stories', {
+ ref: 'Story',
+ localField: '_id',
+ foreignField: 'userId'
+ });
+ UserSchema.virtual('storiesCount', {
+ ref: 'Story',
+ localField: '_id',
+ foreignField: 'userId',
+ count: true
+ });
+
+ const User = db.model('User', UserSchema);
+ const Story = db.model('Story', StorySchema);
+
+ const user = await User.create({ name: 'Alex' });
+ const story1 = await Story.create({ title: 'Ticket 1', userId: user._id });
+ const story2 = await Story.create({ title: 'Ticket 2', userId: user._id });
+
+ const populated = await User.findOne({ name: 'Alex' }).populate(['stories', 'storiesCount']).lean();
+ const hydrated = User.hydrate(
+ JSON.parse(JSON.stringify(populated)),
+ null,
+ { hydratedPopulatedDocs: true }
+ );
+
+ assert.equal(hydrated.stories[0]._id.toString(), story1._id.toString());
+ assert(typeof hydrated.stories[0]._id == 'object', typeof hydrated.stories[0]._id);
+ assert(hydrated.stories[0]._id instanceof mongoose.Types.ObjectId);
+ assert(typeof hydrated.stories[0].createdAt == 'object');
+ assert(hydrated.stories[0].createdAt instanceof Date);
+
+ assert.equal(hydrated.stories[1]._id.toString(), story2._id.toString());
+ assert(typeof hydrated.stories[1]._id == 'object');
+
+ assert(hydrated.stories[1]._id instanceof mongoose.Types.ObjectId);
+ assert(typeof hydrated.stories[1].createdAt == 'object');
+ assert(hydrated.stories[1].createdAt instanceof Date);
+
+ assert.strictEqual(hydrated.storiesCount, 2);
+ });
});
});
diff --git a/test/model.populate.test.js b/test/model.populate.test.js
index 65b3b36b2cc..a987bd6d4cc 100644
--- a/test/model.populate.test.js
+++ b/test/model.populate.test.js
@@ -10968,4 +10968,57 @@ describe('model: populate:', function() {
assert.equal(res.children[0].subdoc.name, 'A');
assert.equal(res.children[1].subdoc.name, 'B');
});
+
+ it('avoids filtering out `null` values when applying match function (gh-14494)', async function() {
+ const gradeSchema = new mongoose.Schema({
+ studentId: mongoose.Types.ObjectId,
+ classId: mongoose.Types.ObjectId,
+ grade: String
+ });
+
+ const Grade = db.model('Test', gradeSchema);
+
+ const studentSchema = new mongoose.Schema({
+ name: String
+ });
+
+ studentSchema.virtual('grade', {
+ ref: Grade,
+ localField: '_id',
+ foreignField: 'studentId',
+ match: (doc) => ({
+ classId: doc._id
+ }),
+ justOne: true
+ });
+
+ const classSchema = new mongoose.Schema({
+ name: String,
+ students: [studentSchema]
+ });
+
+ const Class = db.model('Test2', classSchema);
+
+ const newClass = await Class.create({
+ name: 'History',
+ students: [{ name: 'Henry' }, { name: 'Robert' }]
+ });
+
+ const studentRobert = newClass.students.find(
+ ({ name }) => name === 'Robert'
+ );
+
+ await Grade.create({
+ studentId: studentRobert._id,
+ classId: newClass._id,
+ grade: 'B'
+ });
+
+ const latestClass = await Class.findOne({ name: 'History' }).populate('students.grade');
+
+ assert.equal(latestClass.students[0].name, 'Henry');
+ assert.equal(latestClass.students[0].grade, null);
+ assert.equal(latestClass.students[1].name, 'Robert');
+ assert.equal(latestClass.students[1].grade.grade, 'B');
+ });
});
diff --git a/test/schema.test.js b/test/schema.test.js
index 9931ecdc157..0092a44a4ee 100644
--- a/test/schema.test.js
+++ b/test/schema.test.js
@@ -72,6 +72,7 @@ describe('schema', function() {
},
b: { $type: String }
}, { typeKey: '$type' });
+ db.deleteModel(/Test/);
NestedModel = db.model('Test', NestedSchema);
});
@@ -3219,4 +3220,21 @@ describe('schema', function() {
assert.equal(schema.path('tags.$').caster.instance, 'String'); // actually Mixed
assert.equal(schema.path('subdocs.$').casterConstructor.schema.path('name').instance, 'String'); // actually Mixed
});
+ it('handles discriminator options with Schema.prototype.discriminator (gh-14448)', async function() {
+ const eventSchema = new mongoose.Schema({
+ name: String
+ }, { discriminatorKey: 'kind' });
+ const clickedEventSchema = new mongoose.Schema({ element: String });
+ eventSchema.discriminator(
+ 'Test2',
+ clickedEventSchema,
+ { value: 'click' }
+ );
+ const Event = db.model('Test', eventSchema);
+ const ClickedModel = db.model('Test2');
+
+ const doc = await Event.create({ kind: 'click', element: '#hero' });
+ assert.equal(doc.element, '#hero');
+ assert.ok(doc instanceof ClickedModel);
+ });
});
diff --git a/test/types/docArray.test.ts b/test/types/docArray.test.ts
index cf41df0b82e..c296ce6fea7 100644
--- a/test/types/docArray.test.ts
+++ b/test/types/docArray.test.ts
@@ -1,4 +1,4 @@
-import { Schema, model, Types, InferSchemaType } from 'mongoose';
+import { Schema, model, Model, Types, InferSchemaType } from 'mongoose';
import { expectError, expectType } from 'tsd';
async function gh10293() {
@@ -127,3 +127,38 @@ async function gh14367() {
expectType({} as IUser['reminders'][0]['toggle']);
expectType({} as IUser['avatar']);
}
+
+function gh14469() {
+ interface Names {
+ _id: Types.ObjectId;
+ firstName: string;
+ }
+ // Document definition
+ interface User {
+ names: Names[];
+ }
+
+ // TMethodsAndOverrides
+ type UserDocumentProps = {
+ names: Types.DocumentArray;
+ };
+ type UserModelType = Model;
+
+ const userSchema = new Schema(
+ {
+ names: [new Schema({ firstName: String })]
+ },
+ { timestamps: true }
+ );
+
+ // Create model
+ const UserModel = model('User', userSchema);
+
+ const doc = new UserModel({ names: [{ firstName: 'John' }] });
+
+ const jsonDoc = doc?.toJSON();
+ expectType(jsonDoc?.names[0]?.firstName);
+
+ const jsonNames = doc?.names[0]?.toJSON();
+ expectType(jsonNames?.firstName);
+}
diff --git a/test/types/middleware.test.ts b/test/types/middleware.test.ts
index e127c3b683b..96e40ecbe81 100644
--- a/test/types/middleware.test.ts
+++ b/test/types/middleware.test.ts
@@ -1,6 +1,6 @@
-import { Schema, model, Model, Document, SaveOptions, Query, Aggregate, HydratedDocument, PreSaveMiddlewareFunction, ModifyResult } from 'mongoose';
+import { Schema, model, Model, Document, SaveOptions, Query, Aggregate, HydratedDocument, PreSaveMiddlewareFunction, ModifyResult, AnyBulkWriteOperation } from 'mongoose';
import { expectError, expectType, expectNotType, expectAssignable } from 'tsd';
-import { AnyBulkWriteOperation, CreateCollectionOptions } from 'mongodb';
+import { CreateCollectionOptions } from 'mongodb';
const preMiddlewareFn: PreSaveMiddlewareFunction = function(next, opts) {
this.$markValid('name');
@@ -63,6 +63,10 @@ schema.post('save', function() {
console.log(this.name);
});
+schema.post('save', async function() {
+ console.log(this.name);
+});
+
schema.post('save', function(err: Error, res: ITest, next: Function) {
console.log(this.name, err.stack);
});
diff --git a/test/types/models.test.ts b/test/types/models.test.ts
index 3e1bd32449f..1633c8d35b5 100644
--- a/test/types/models.test.ts
+++ b/test/types/models.test.ts
@@ -824,7 +824,7 @@ async function gh14072() {
);
const M = mongoose.model('Test', schema);
- const bulkWriteArray = [
+ await M.bulkWrite([
{
insertOne: {
document: { num: 3 }
@@ -844,9 +844,7 @@ async function gh14072() {
timestamps: false
}
}
- ];
-
- await M.bulkWrite(bulkWriteArray);
+ ]);
}
async function gh14003() {
diff --git a/test/types/populate.test.ts b/test/types/populate.test.ts
index c846e7b346e..fe9afa4ef84 100644
--- a/test/types/populate.test.ts
+++ b/test/types/populate.test.ts
@@ -373,3 +373,49 @@ async function gh13070() {
const doc2 = await Child.populate<{ child: IChild }>(doc, 'child');
const name: string = doc2.child.name;
}
+
+function gh14441() {
+ interface Parent {
+ child?: Types.ObjectId;
+ name?: string;
+ }
+ const ParentModel = model(
+ 'Parent',
+ new Schema({
+ child: { type: Schema.Types.ObjectId, ref: 'Child' },
+ name: String
+ })
+ );
+
+ interface Child {
+ name: string;
+ }
+ const childSchema: Schema = new Schema({ name: String });
+ model('Child', childSchema);
+
+ ParentModel.findOne({})
+ .populate<{ child: Child }>('child')
+ .orFail()
+ .then(doc => {
+ expectType(doc.child.name);
+ const docObject = doc.toObject();
+ expectType(docObject.child.name);
+ });
+
+ ParentModel.findOne({})
+ .populate<{ child: Child }>('child')
+ .lean()
+ .orFail()
+ .then(doc => {
+ expectType(doc.child.name);
+ });
+
+ ParentModel.find({})
+ .populate<{ child: Child }>('child')
+ .orFail()
+ .then(docs => {
+ expectType(docs[0]!.child.name);
+ const docObject = docs[0]!.toObject();
+ expectType(docObject.child.name);
+ });
+}
diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts
index 3ebcaa861d0..297360b62d1 100644
--- a/test/types/queries.test.ts
+++ b/test/types/queries.test.ts
@@ -12,7 +12,6 @@ import {
FilterQuery,
UpdateQuery,
UpdateQueryKnownOnly,
- ApplyBasicQueryCasting,
QuerySelector,
InferSchemaType,
ProjectionFields,
@@ -325,7 +324,7 @@ function gh11964() {
}
function gh14397() {
- type Condition = ApplyBasicQueryCasting | QuerySelector>; // redefined here because it's not exported by mongoose
+ type Condition = T | QuerySelector; // redefined here because it's not exported by mongoose
type WithId = T & { id: string };
@@ -592,3 +591,48 @@ function mongooseQueryOptions() {
populate: 'test'
});
}
+
+function gh14473() {
+ class AbstractSchema {
+ _id: any;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: Date;
+
+ constructor() {
+ this._id = 4;
+ this.createdAt = new Date();
+ this.updatedAt = new Date();
+ this.deletedAt = new Date();
+ }
+ }
+
+ const generateExists = () => {
+ const query: FilterQuery = { deletedAt: { $ne: null } };
+ const query2: FilterQuery = { deletedAt: { $lt: new Date() } };
+ };
+}
+
+async function gh14525() {
+ type BeAnObject = Record;
+
+ interface SomeDoc {
+ something: string;
+ func(this: TestDoc): string;
+ }
+
+ interface PluginExtras {
+ pfunc(): number;
+ }
+
+ type TestDoc = Document & PluginExtras;
+
+ type ModelType = Model;
+
+ const doc = await ({} as ModelType).findOne({}).populate('test').orFail().exec();
+
+ doc.func();
+
+ let doc2 = await ({} as ModelType).create({});
+ doc2 = await ({} as ModelType).findOne({}).populate('test').orFail().exec();
+}
diff --git a/types/index.d.ts b/types/index.d.ts
index f6835f6b329..0402bfa32fa 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -431,7 +431,7 @@ declare module 'mongoose' {
fn: (
this: T,
next: (err?: CallbackError) => void,
- ops: Array & MongooseBulkWritePerWriteOptions>,
+ ops: Array>,
options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions
) => void | Promise
): this;
diff --git a/types/models.d.ts b/types/models.d.ts
index aa207f6b45b..b0edc0a2b72 100644
--- a/types/models.d.ts
+++ b/types/models.d.ts
@@ -23,13 +23,21 @@ declare module 'mongoose' {
): U;
}
- interface MongooseBulkWriteOptions {
+ interface MongooseBulkWriteOptions extends mongodb.BulkWriteOptions {
+ session?: ClientSession;
skipValidation?: boolean;
throwOnValidationError?: boolean;
- timestamps?: boolean;
strict?: boolean | 'throw';
}
+ interface MongooseBulkSaveOptions extends mongodb.BulkWriteOptions {
+ timestamps?: boolean;
+ session?: ClientSession;
+ }
+
+ /**
+ * @deprecated use AnyBulkWriteOperation instead
+ */
interface MongooseBulkWritePerWriteOptions {
timestamps?: boolean;
strict?: boolean | 'throw';
@@ -200,6 +208,8 @@ declare module 'mongoose' {
hint?: mongodb.Hint;
/** When true, creates a new document if no document matches the query. */
upsert?: boolean;
+ /** When false, do not add timestamps. */
+ timestamps?: boolean;
}
export interface UpdateManyModel {
@@ -215,6 +225,8 @@ declare module 'mongoose' {
hint?: mongodb.Hint;
/** When true, creates a new document if no document matches the query. */
upsert?: boolean;
+ /** When false, do not add timestamps. */
+ timestamps?: boolean;
}
export interface DeleteOneModel {
@@ -281,11 +293,11 @@ declare module 'mongoose' {
*/
bulkWrite(
writes: Array>,
- options: mongodb.BulkWriteOptions & MongooseBulkWriteOptions & { ordered: false }
+ options: MongooseBulkWriteOptions & { ordered: false }
): Promise;
bulkWrite(
writes: Array>,
- options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions
+ options?: MongooseBulkWriteOptions
): Promise;
/**
@@ -293,7 +305,7 @@ declare module 'mongoose' {
* sending multiple `save()` calls because with `bulkSave()` there is only one
* network round trip to the MongoDB server.
*/
- bulkSave(documents: Array, options?: mongodb.BulkWriteOptions & { timestamps?: boolean }): Promise;
+ bulkSave(documents: Array, options?: MongooseBulkSaveOptions): Promise;
/** Collection the model uses. */
collection: Collection;
diff --git a/types/query.d.ts b/types/query.d.ts
index 32a502ed5a0..3145dfef477 100644
--- a/types/query.d.ts
+++ b/types/query.d.ts
@@ -1,24 +1,7 @@
declare module 'mongoose' {
import mongodb = require('mongodb');
- type StringQueryTypeCasting = string | RegExp;
- type ObjectIdQueryTypeCasting = Types.ObjectId | string;
- type UUIDQueryTypeCasting = Types.UUID | string;
-
- type QueryTypeCasting = T extends string
- ? StringQueryTypeCasting
- : T extends Types.ObjectId
- ? ObjectIdQueryTypeCasting
- : T extends Types.UUID
- ? UUIDQueryTypeCasting
- : T | any;
-
- export type ApplyBasicQueryCasting = T | T[] | (T extends (infer U)[] ? QueryTypeCasting : T);
- export type Condition = ApplyBasicQueryCasting> | QuerySelector>>;
-
- type _FilterQuery = {
- [P in keyof T]?: Condition;
- } & RootQuerySelector;
+ export type Condition = T | QuerySelector | any;
/**
* Filter query to select the documents that match the query
@@ -27,7 +10,9 @@ declare module 'mongoose' {
* { age: { $gte: 30 } }
* ```
*/
- type FilterQuery = _FilterQuery;
+ type FilterQuery = {
+ [P in keyof T]?: Condition;
+ } & RootQuerySelector;
type MongooseBaseQueryOptionKeys =
| 'context'
@@ -220,6 +205,18 @@ declare module 'mongoose' {
? (ResultType extends any[] ? Require_id>[] : Require_id>)
: ResultType;
+ type MergePopulatePaths = QueryOp extends QueryOpThatReturnsDocument
+ ? ResultType extends null
+ ? ResultType
+ : ResultType extends (infer U)[]
+ ? U extends Document
+ ? HydratedDocument, Record, TQueryHelpers>[]
+ : (MergeType)[]
+ : ResultType extends Document
+ ? HydratedDocument, Record, TQueryHelpers>
+ : MergeType
+ : MergeType;
+
class Query implements SessionOperation {
_mongooseOptions: MongooseQueryOptions;
@@ -617,22 +614,43 @@ declare module 'mongoose' {
polygon(...coordinatePairs: number[][]): this;
/** Specifies paths which should be populated with other documents. */
- populate(
+ populate(
+ path: string | string[],
+ select?: string | any,
+ model?: string | Model,
+ match?: any
+ ): QueryWithHelpers<
+ ResultType,
+ DocType,
+ THelpers,
+ RawDocType,
+ QueryOp
+ >;
+ populate(
+ options: PopulateOptions | (PopulateOptions | string)[]
+ ): QueryWithHelpers<
+ ResultType,
+ DocType,
+ THelpers,
+ RawDocType,
+ QueryOp
+ >;
+ populate(
path: string | string[],
select?: string | any,
model?: string | Model,
match?: any
): QueryWithHelpers<
- UnpackedIntersection,
+ MergePopulatePaths,
DocType,
THelpers,
UnpackedIntersection,
QueryOp
>;
- populate(
+ populate(
options: PopulateOptions | (PopulateOptions | string)[]
): QueryWithHelpers<
- UnpackedIntersection,
+ MergePopulatePaths,
DocType,
THelpers,
UnpackedIntersection,
diff --git a/types/types.d.ts b/types/types.d.ts
index cce6b12504c..f63b1934907 100644
--- a/types/types.d.ts
+++ b/types/types.d.ts
@@ -60,7 +60,7 @@ declare module 'mongoose' {
class Decimal128 extends mongodb.Decimal128 { }
- class DocumentArray extends Types.Array> & T> {
+ class DocumentArray extends Types.Array, any, T> & T> {
/** DocumentArray constructor */
constructor(values: AnyObject[]);
@@ -83,7 +83,7 @@ declare module 'mongoose' {
class ObjectId extends mongodb.ObjectId {
}
- class Subdocument extends Document {
+ class Subdocument extends Document {
$isSingleNested: true;
/** Returns the top level document of this sub-document. */