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) {

Asynchronous Post Hooks

-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(); +}); +``` +

Define Middleware Before Compiling Models

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. */