From 92175732bab462528f284313e9853a43f22eb08f Mon Sep 17 00:00:00 2001 From: Brady Davis Date: Wed, 10 Jan 2024 07:19:56 -0600 Subject: [PATCH 001/209] Adding missing pluralizations, fixing pluralization: virus -> viruses --- lib/helpers/pluralize.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/helpers/pluralize.js b/lib/helpers/pluralize.js index 2f9cbf8a2e0..a0f4642dae4 100644 --- a/lib/helpers/pluralize.js +++ b/lib/helpers/pluralize.js @@ -8,13 +8,13 @@ module.exports = pluralize; exports.pluralization = [ [/human$/gi, 'humans'], - [/(m)an$/gi, '$1en'], + [/(m|wom)an$/gi, '$1en'], [/(pe)rson$/gi, '$1ople'], [/(child)$/gi, '$1ren'], [/^(ox)$/gi, '$1en'], [/(ax|test)is$/gi, '$1es'], - [/(octop|vir)us$/gi, '$1i'], - [/(alias|status)$/gi, '$1es'], + [/(octop|cact|foc|fung|nucle)us$/gi, '$1i'], + [/(alias|status|virus)$/gi, '$1es'], [/(bu)s$/gi, '$1ses'], [/(buffal|tomat|potat)o$/gi, '$1oes'], [/([ti])um$/gi, '$1a'], From 9538f4d55bb84ab378b39fd948ec133dc6b1e792 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 5 Jun 2024 15:51:15 -0400 Subject: [PATCH 002/209] BREAKING CHANGE: call virtual `ref` function with subdoc, not top-level doc Fix #12363 Fix #12440 --- .../populate/getModelsMapForPopulate.js | 10 +++- test/model.populate.test.js | 47 +++++++++++++++++-- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/lib/helpers/populate/getModelsMapForPopulate.js b/lib/helpers/populate/getModelsMapForPopulate.js index 276698217d0..7035f0522e5 100644 --- a/lib/helpers/populate/getModelsMapForPopulate.js +++ b/lib/helpers/populate/getModelsMapForPopulate.js @@ -410,7 +410,15 @@ function _virtualPopulate(model, docs, options, _virtualRes) { justOne = options.justOne; } - modelNames = virtual._getModelNamesForPopulate(doc); + // Use the correct target doc/sub-doc for dynamic ref on nested schema. See gh-12363 + if (_virtualRes.nestedSchemaPath && typeof virtual.options.ref === 'function') { + const subdocs = utils.getValue(_virtualRes.nestedSchemaPath, doc); + modelNames = Array.isArray(subdocs) + ? subdocs.flatMap(subdoc => virtual._getModelNamesForPopulate(subdoc)) + : virtual._getModelNamesForPopulate(subdocs); + } else { + modelNames = virtual._getModelNamesForPopulate(doc); + } if (virtual.options.refPath) { justOne = !!virtual.options.justOne; data.isRefPath = true; diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 9ad6376f89a..4952a489e9f 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -4247,6 +4247,45 @@ describe('model: populate:', function() { catch(done); }); + it('with functions for ref with subdoc virtual populate (gh-12440) (gh-12363)', async function() { + const ASchema = new Schema({ + name: String + }); + + const BSchema = new Schema({ + referencedModel: String, + aId: ObjectId + }); + + BSchema.virtual('a', { + ref: function() { + return this.referencedModel; + }, + localField: 'aId', + foreignField: '_id', + justOne: true + }); + + const ParentSchema = new Schema({ + b: BSchema + }); + + const A1 = db.model('Test1', ASchema); + const A2 = db.model('Test2', ASchema); + const Parent = db.model('Parent', ParentSchema); + + const as = await Promise.all([ + A1.create({ name: 'a1' }), + A2.create({ name: 'a2' }) + ]); + await Parent.create([ + { b: { name: 'test1', referencedModel: 'Test1', aId: as[0]._id } }, + { b: { name: 'test2', referencedModel: 'Test2', aId: as[1]._id } } + ]); + const parents = await Parent.find().populate('b.a').sort({ _id: 1 }); + assert.deepStrictEqual(parents.map(p => p.b.a.name), ['a1', 'a2']); + }); + it('with functions for match (gh-7397)', async function() { const ASchema = new Schema({ name: String, @@ -6642,8 +6681,8 @@ describe('model: populate:', function() { }); clickedSchema.virtual('users_$', { - ref: function(doc) { - return doc.events[0].users[0].refKey; + ref: function(subdoc) { + return subdoc.users[0].refKey; }, localField: 'users.ID', foreignField: 'employeeId' @@ -6706,8 +6745,8 @@ describe('model: populate:', function() { }); clickedSchema.virtual('users_$', { - ref: function(doc) { - const refKeys = doc.events[0].users.map(user => user.refKey); + ref: function(subdoc) { + const refKeys = subdoc.users.map(user => user.refKey); return refKeys; }, localField: 'users.ID', From a65d858179d3d0aaab26375b44b4cedddebfd8b7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 5 Jun 2024 16:02:43 -0400 Subject: [PATCH 003/209] test: cover case where ref not found Re: #12440 Re: #12363 --- test/model.populate.test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 4952a489e9f..1c152d6736c 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -4280,10 +4280,11 @@ describe('model: populate:', function() { ]); await Parent.create([ { b: { name: 'test1', referencedModel: 'Test1', aId: as[0]._id } }, - { b: { name: 'test2', referencedModel: 'Test2', aId: as[1]._id } } + { b: { name: 'test2', referencedModel: 'Test2', aId: as[1]._id } }, + { b: { name: 'test3', referencedModel: 'Test2', aId: '0'.repeat(24) } } ]); const parents = await Parent.find().populate('b.a').sort({ _id: 1 }); - assert.deepStrictEqual(parents.map(p => p.b.a.name), ['a1', 'a2']); + assert.deepStrictEqual(parents.map(p => p.b.a?.name), ['a1', 'a2', undefined]); }); it('with functions for match (gh-7397)', async function() { From 15027c9168a6839eefcc7939eab824484adcd759 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 5 Nov 2024 10:32:07 -0500 Subject: [PATCH 004/209] fix(model+query): make `findOne(null)`, `find(null)`, etc. throw an error instead of returning first doc Re: #14948 --- lib/model.js | 6 +++--- lib/query.js | 44 +++++++++++++++++++++++++++++--------------- test/query.test.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 18 deletions(-) diff --git a/lib/model.js b/lib/model.js index c7b3956f6aa..8a05e72dd9f 100644 --- a/lib/model.js +++ b/lib/model.js @@ -2288,8 +2288,8 @@ Model.findOneAndUpdate = function(conditions, update, options) { if (arguments.length === 1) { update = conditions; - conditions = null; - options = null; + conditions = undefined; + options = undefined; } let fields; @@ -2864,7 +2864,7 @@ Model.$__insertMany = function(arr, options, callback) { const _this = this; if (typeof options === 'function') { callback = options; - options = null; + options = undefined; } callback = callback || utils.noop; diff --git a/lib/query.js b/lib/query.js index 6333c153c68..9fab755558d 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2414,7 +2414,7 @@ Query.prototype.find = function(conditions) { this.op = 'find'; - if (mquery.canMerge(conditions)) { + if (canMerge(conditions)) { this.merge(conditions); prepareDiscriminatorCriteria(this); @@ -2436,9 +2436,14 @@ Query.prototype.find = function(conditions) { Query.prototype.merge = function(source) { if (!source) { + if (source === null) { + this._conditions = null; + } return this; } + this._conditions = this._conditions ?? {}; + const opts = { overwrite: true }; if (source instanceof Query) { @@ -2700,7 +2705,7 @@ Query.prototype.findOne = function(conditions, projection, options) { this.select(projection); } - if (mquery.canMerge(conditions)) { + if (canMerge(conditions)) { this.merge(conditions); prepareDiscriminatorCriteria(this); @@ -2874,7 +2879,7 @@ Query.prototype.countDocuments = function(conditions, options) { this.op = 'countDocuments'; this._validateOp(); - if (mquery.canMerge(conditions)) { + if (canMerge(conditions)) { this.merge(conditions); } @@ -2940,7 +2945,7 @@ Query.prototype.distinct = function(field, conditions, options) { this.op = 'distinct'; this._validateOp(); - if (mquery.canMerge(conditions)) { + if (canMerge(conditions)) { this.merge(conditions); prepareDiscriminatorCriteria(this); @@ -3108,7 +3113,7 @@ Query.prototype.deleteOne = function deleteOne(filter, options) { this.op = 'deleteOne'; this.setOptions(options); - if (mquery.canMerge(filter)) { + if (canMerge(filter)) { this.merge(filter); prepareDiscriminatorCriteria(this); @@ -3181,7 +3186,7 @@ Query.prototype.deleteMany = function(filter, options) { this.setOptions(options); this.op = 'deleteMany'; - if (mquery.canMerge(filter)) { + if (canMerge(filter)) { this.merge(filter); prepareDiscriminatorCriteria(this); @@ -3354,7 +3359,7 @@ Query.prototype.findOneAndUpdate = function(filter, doc, options) { break; } - if (mquery.canMerge(filter)) { + if (canMerge(filter)) { this.merge(filter); } else if (filter != null) { this.error( @@ -3524,7 +3529,7 @@ Query.prototype.findOneAndDelete = function(filter, options) { this._validateOp(); this._validate(); - if (mquery.canMerge(filter)) { + if (canMerge(filter)) { this.merge(filter); } @@ -3629,7 +3634,7 @@ Query.prototype.findOneAndReplace = function(filter, replacement, options) { this._validateOp(); this._validate(); - if (mquery.canMerge(filter)) { + if (canMerge(filter)) { this.merge(filter); } else if (filter != null) { this.error( @@ -4037,13 +4042,13 @@ Query.prototype.updateMany = function(conditions, doc, options, callback) { if (typeof options === 'function') { // .update(conditions, doc, callback) callback = options; - options = null; + options = undefined; } else if (typeof doc === 'function') { // .update(doc, callback); callback = doc; doc = conditions; conditions = {}; - options = null; + options = undefined; } else if (typeof conditions === 'function') { // .update(callback) callback = conditions; @@ -4108,13 +4113,13 @@ Query.prototype.updateOne = function(conditions, doc, options, callback) { if (typeof options === 'function') { // .update(conditions, doc, callback) callback = options; - options = null; + options = undefined; } else if (typeof doc === 'function') { // .update(doc, callback); callback = doc; doc = conditions; conditions = {}; - options = null; + options = undefined; } else if (typeof conditions === 'function') { // .update(callback) callback = conditions; @@ -4175,13 +4180,13 @@ Query.prototype.replaceOne = function(conditions, doc, options, callback) { if (typeof options === 'function') { // .update(conditions, doc, callback) callback = options; - options = null; + options = undefined; } else if (typeof doc === 'function') { // .update(doc, callback); callback = doc; doc = conditions; conditions = {}; - options = null; + options = undefined; } else if (typeof conditions === 'function') { // .update(callback) callback = conditions; @@ -5541,6 +5546,15 @@ Query.prototype.selectedExclusively = function selectedExclusively() { Query.prototype.model; +/** + * Determine if we can merge the given value as a query filter. Override for mquery.canMerge() to allow null + */ + +function canMerge(value) { + return value instanceof Query || utils.isObject(value) || value === null; + +} + /*! * Export */ diff --git a/test/query.test.js b/test/query.test.js index bca5f706cfd..d8079b155fc 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -4412,4 +4412,50 @@ describe('Query', function() { assert.strictEqual(doc.passwordHash, undefined); }); }); + + it('throws an error if calling find(null), findOne(null), updateOne(null, update), etc. (gh-14948)', async function() { + const userSchema = new Schema({ + name: String + }); + const UserModel = db.model('User', userSchema); + await UserModel.deleteMany({}); + await UserModel.updateOne({ name: 'test' }, { name: 'test' }, { upsert: true }); + + await assert.rejects( + () => UserModel.find(null), + /MongoServerError: Expected field filterto be of type object/ + ); + await assert.rejects( + () => UserModel.findOne(null), + /MongoServerError: Expected field filterto be of type object/ + ); + await assert.rejects( + () => UserModel.findOneAndUpdate(null, { name: 'test2' }), + /MongoInvalidArgumentError: Argument "filter" must be an object/ + ); + await assert.rejects( + () => UserModel.findOneAndReplace(null, { name: 'test2' }), + /MongoInvalidArgumentError: Argument "filter" must be an object/ + ); + await assert.rejects( + () => UserModel.findOneAndDelete(null), + /MongoInvalidArgumentError: Argument "filter" must be an object/ + ); + await assert.rejects( + () => UserModel.updateOne(null, { name: 'test2' }), + /MongoInvalidArgumentError: Selector must be a valid JavaScript object/ + ); + await assert.rejects( + () => UserModel.updateMany(null, { name: 'test2' }), + /MongoInvalidArgumentError: Selector must be a valid JavaScript object/ + ); + await assert.rejects( + () => UserModel.deleteOne(null), + /MongoServerError: BSON field 'delete.deletes.q' is missing but a required field/ + ); + await assert.rejects( + () => UserModel.deleteMany(null), + /MongoServerError: BSON field 'delete.deletes.q' is missing but a required field/ + ); + }); }); From cd1d6da50f2e04a071b9ee9c66b5149af9705ca2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 5 Nov 2024 10:43:38 -0500 Subject: [PATCH 005/209] consistent error messages --- lib/error/objectParameter.js | 3 +-- lib/query.js | 4 ++++ test/query.test.js | 18 +++++++++--------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/error/objectParameter.js b/lib/error/objectParameter.js index 0a2108e5c9b..3d5e04633f2 100644 --- a/lib/error/objectParameter.js +++ b/lib/error/objectParameter.js @@ -17,10 +17,9 @@ const MongooseError = require('./mongooseError'); */ class ObjectParameterError extends MongooseError { - constructor(value, paramName, fnName) { super('Parameter "' + paramName + '" to ' + fnName + - '() must be an object, got "' + value.toString() + '" (type ' + typeof value + ')'); + '() must be an object, got "' + (value == null ? value : value.toString()) + '" (type ' + typeof value + ')'); } } diff --git a/lib/query.js b/lib/query.js index 9fab755558d..0305d9a3a58 100644 --- a/lib/query.js +++ b/lib/query.js @@ -469,6 +469,10 @@ Query.prototype._validateOp = function() { if (this.op != null && !validOpsSet.has(this.op)) { this.error(new Error('Query has invalid `op`: "' + this.op + '"')); } + + if (this.op !== 'estimatedDocumentCount' && this._conditions === null) { + throw new ObjectParameterError(this._conditions, 'filter', this.op); + } }; /** diff --git a/test/query.test.js b/test/query.test.js index d8079b155fc..cc27166050a 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -4423,39 +4423,39 @@ describe('Query', function() { await assert.rejects( () => UserModel.find(null), - /MongoServerError: Expected field filterto be of type object/ + /ObjectParameterError: Parameter "filter" to find\(\) must be an object, got "null"/ ); await assert.rejects( () => UserModel.findOne(null), - /MongoServerError: Expected field filterto be of type object/ + /ObjectParameterError: Parameter "filter" to findOne\(\) must be an object, got "null"/ ); await assert.rejects( () => UserModel.findOneAndUpdate(null, { name: 'test2' }), - /MongoInvalidArgumentError: Argument "filter" must be an object/ + /ObjectParameterError: Parameter "filter" to findOneAndUpdate\(\) must be an object, got "null"/ ); await assert.rejects( () => UserModel.findOneAndReplace(null, { name: 'test2' }), - /MongoInvalidArgumentError: Argument "filter" must be an object/ + /ObjectParameterError: Parameter "filter" to findOneAndReplace\(\) must be an object, got "null"/ ); await assert.rejects( () => UserModel.findOneAndDelete(null), - /MongoInvalidArgumentError: Argument "filter" must be an object/ + /ObjectParameterError: Parameter "filter" to findOneAndDelete\(\) must be an object, got "null"/ ); await assert.rejects( () => UserModel.updateOne(null, { name: 'test2' }), - /MongoInvalidArgumentError: Selector must be a valid JavaScript object/ + /ObjectParameterError: Parameter "filter" to updateOne\(\) must be an object, got "null"/ ); await assert.rejects( () => UserModel.updateMany(null, { name: 'test2' }), - /MongoInvalidArgumentError: Selector must be a valid JavaScript object/ + /ObjectParameterError: Parameter "filter" to updateMany\(\) must be an object, got "null"/ ); await assert.rejects( () => UserModel.deleteOne(null), - /MongoServerError: BSON field 'delete.deletes.q' is missing but a required field/ + /ObjectParameterError: Parameter "filter" to deleteOne\(\) must be an object, got "null"/ ); await assert.rejects( () => UserModel.deleteMany(null), - /MongoServerError: BSON field 'delete.deletes.q' is missing but a required field/ + /ObjectParameterError: Parameter "filter" to deleteMany\(\) must be an object, got "null"/ ); }); }); From 9316aae5ce6d5b780dfec27b0b7dcd6b75d9cefb Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 5 Nov 2024 12:32:15 -0500 Subject: [PATCH 006/209] BREAKING CHANGE: change `this` to HydratedDocument for default() and required(), HydratedDocument | Query for validate() --- test/types/schema.test.ts | 49 ++++++++++++++++++++++++++++----------- types/index.d.ts | 26 ++++++++++----------- types/schematypes.d.ts | 22 +++++++++--------- types/validation.d.ts | 27 ++++++++++++++------- 4 files changed, 78 insertions(+), 46 deletions(-) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 82988a05b12..c01fdd8fbff 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1232,12 +1232,24 @@ async function gh13797() { interface IUser { name: string; } - new Schema({ name: { type: String, required: function() { - expectType(this); return true; - } } }); - new Schema({ name: { type: String, default: function() { - expectType(this); return ''; - } } }); + new Schema({ + name: { + type: String, + required: function() { + expectType>(this); + return true; + } + } + }); + new Schema({ + name: { + type: String, + default: function() { + expectType>(this); + return ''; + } + } + }); } declare const brand: unique symbol; @@ -1529,12 +1541,17 @@ function gh14696() { const x: ValidateOpts = { validator(v: any) { - expectAssignable(this); - return !v || this.name === 'super admin'; + expectAssignable>(this); + return !v || this instanceof Query || (this.name === 'super admin'); } }; - const userSchema = new Schema({ + interface IUserMethods { + isSuperAdmin(): boolean; + } + + type UserModelType = Model; + const userSchema = new Schema({ name: { type: String, required: [true, 'Name on card is required'] @@ -1544,8 +1561,14 @@ function gh14696() { default: false, validate: { validator(v: any) { - expectAssignable(this); - return !v || this.name === 'super admin'; + expectAssignable>(this); + if (!v) { + return true; + } + if (this instanceof Query) { + return true; + } + return this.name === 'super admin' || this.isSuperAdmin(); } } }, @@ -1554,8 +1577,8 @@ function gh14696() { default: false, validate: { async validator(v: any) { - expectAssignable(this); - return !v || this.name === 'super admin'; + expectAssignable>(this); + return !v || this.get('name') === 'super admin'; } } } diff --git a/types/index.d.ts b/types/index.d.ts index 668f67e55d1..63a3cfc94ad 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -273,7 +273,7 @@ declare module 'mongoose' { /** * Create a new schema */ - constructor(definition?: SchemaDefinition, RawDocType> | DocType, options?: SchemaOptions, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals, THydratedDocumentType> | ResolveSchemaOptions); + constructor(definition?: SchemaDefinition, RawDocType, THydratedDocumentType> | DocType, options?: SchemaOptions, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals, THydratedDocumentType> | ResolveSchemaOptions); /** Adds key path / schema type pairs to this schema. */ add(obj: SchemaDefinition> | Schema, prefix?: string): this; @@ -539,26 +539,26 @@ declare module 'mongoose' { ? DateSchemaDefinition : (Function | string); - export type SchemaDefinitionProperty = SchemaDefinitionWithBuiltInClass | - SchemaTypeOptions | + export type SchemaDefinitionProperty = SchemaDefinitionWithBuiltInClass | + SchemaTypeOptions | typeof SchemaType | - Schema | - Schema[] | - SchemaTypeOptions, EnforcedDocType>[] | - Function[] | - SchemaDefinition | - SchemaDefinition, EnforcedDocType>[] | + Schema | + Schema[] | + SchemaTypeOptions, EnforcedDocType, THydratedDocumentType>[] | + Function[] | + SchemaDefinition | + SchemaDefinition, EnforcedDocType, THydratedDocumentType>[] | typeof Schema.Types.Mixed | - MixedSchemaTypeOptions; + MixedSchemaTypeOptions; - export type SchemaDefinition = T extends undefined + export type SchemaDefinition = T extends undefined ? { [path: string]: SchemaDefinitionProperty; } - : { [path in keyof T]?: SchemaDefinitionProperty; }; + : { [path in keyof T]?: SchemaDefinitionProperty; }; export type AnyArray = T[] | ReadonlyArray; export type ExtractMongooseArray = T extends Types.Array ? AnyArray> : T; - export interface MixedSchemaTypeOptions extends SchemaTypeOptions { + export interface MixedSchemaTypeOptions extends SchemaTypeOptions { type: typeof Schema.Types.Mixed; } diff --git a/types/schematypes.d.ts b/types/schematypes.d.ts index aff686e1ec9..48e4b9886af 100644 --- a/types/schematypes.d.ts +++ b/types/schematypes.d.ts @@ -39,7 +39,7 @@ declare module 'mongoose' { type DefaultType = T extends Schema.Types.Mixed ? any : Partial>; - class SchemaTypeOptions { + class SchemaTypeOptions { type?: T extends string ? StringSchemaDefinition : T extends number ? NumberSchemaDefinition : @@ -48,19 +48,19 @@ declare module 'mongoose' { T extends Map ? SchemaDefinition : T extends Buffer ? SchemaDefinition : T extends Types.ObjectId ? ObjectIdSchemaDefinition : - T extends Types.ObjectId[] ? AnyArray | AnyArray> : - T extends object[] ? (AnyArray> | AnyArray>> | AnyArray, EnforcedDocType>>) : - T extends string[] ? AnyArray | AnyArray> : - T extends number[] ? AnyArray | AnyArray> : - T extends boolean[] ? AnyArray | AnyArray> : - T extends Function[] ? AnyArray | AnyArray, EnforcedDocType>> : - T | typeof SchemaType | Schema | SchemaDefinition | Function | AnyArray; + T extends Types.ObjectId[] ? AnyArray | AnyArray> : + T extends object[] ? (AnyArray> | AnyArray, EnforcedDocType, THydratedDocumentType>> | AnyArray, EnforcedDocType, THydratedDocumentType>>) : + T extends string[] ? AnyArray | AnyArray> : + T extends number[] ? AnyArray | AnyArray> : + T extends boolean[] ? AnyArray | AnyArray> : + T extends Function[] ? AnyArray | AnyArray, EnforcedDocType, THydratedDocumentType>> : + T | typeof SchemaType | Schema | SchemaDefinition | Function | AnyArray; /** Defines a virtual with the given name that gets/sets this path. */ alias?: string | string[]; /** Function or object describing how to validate this schematype. See [validation docs](https://mongoosejs.com/docs/validation.html). */ - validate?: SchemaValidator | AnyArray>; + validate?: SchemaValidator | AnyArray>; /** Allows overriding casting logic for this individual path. If a string, the given string overwrites Mongoose's default cast error message. */ cast?: string | @@ -74,13 +74,13 @@ declare module 'mongoose' { * path cannot be set to a nullish value. If a function, Mongoose calls the * function and only checks for nullish values if the function returns a truthy value. */ - required?: boolean | ((this: EnforcedDocType) => boolean) | [boolean, string] | [(this: EnforcedDocType) => boolean, string]; + required?: boolean | ((this: THydratedDocumentType) => boolean) | [boolean, string] | [(this: THydratedDocumentType) => boolean, string]; /** * The default value for this path. If a function, Mongoose executes the function * and uses the return value as the default. */ - default?: DefaultType | ((this: EnforcedDocType, doc: any) => DefaultType) | null; + default?: DefaultType | ((this: THydratedDocumentType, doc: THydratedDocumentType) => DefaultType) | null; /** * The model that `populate()` should use if populating this path. diff --git a/types/validation.d.ts b/types/validation.d.ts index 3310d954435..1300afa7329 100644 --- a/types/validation.d.ts +++ b/types/validation.d.ts @@ -1,6 +1,10 @@ declare module 'mongoose' { - - type SchemaValidator = RegExp | [RegExp, string] | Function | [Function, string] | ValidateOpts | ValidateOpts[]; + type SchemaValidator = RegExp | + [RegExp, string] | + Function | + [Function, string] | + ValidateOpts | + ValidateOpts[]; interface ValidatorProps { path: string; @@ -13,18 +17,23 @@ declare module 'mongoose' { (props: ValidatorProps): string; } - type ValidateFn = - (this: EnforcedDocType, value: any, props?: ValidatorProps & Record) => boolean; + type ValidateFn = ( + this: THydratedDocumentType | Query, + value: any, + props?: ValidatorProps & Record + ) => boolean; - type AsyncValidateFn = - (this: EnforcedDocType, value: any, props?: ValidatorProps & Record) => Promise; + type AsyncValidateFn = ( + this: THydratedDocumentType | Query, + value: any, + props?: ValidatorProps & Record + ) => Promise; - interface ValidateOpts { + interface ValidateOpts { msg?: string; message?: string | ValidatorMessageFn; type?: string; - validator: ValidateFn - | AsyncValidateFn; + validator: ValidateFn | AsyncValidateFn; propsParameter?: boolean; } } From 3773c0671261abfd34687f472cf6aebbd7d09e78 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 5 Nov 2024 12:50:18 -0500 Subject: [PATCH 007/209] test improvements --- test/types/schema.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index c01fdd8fbff..1155b9bc693 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1542,7 +1542,7 @@ function gh14696() { const x: ValidateOpts = { validator(v: any) { expectAssignable>(this); - return !v || this instanceof Query || (this.name === 'super admin'); + return !v || this instanceof Query || this.name === 'super admin'; } }; @@ -1565,10 +1565,7 @@ function gh14696() { if (!v) { return true; } - if (this instanceof Query) { - return true; - } - return this.name === 'super admin' || this.isSuperAdmin(); + return this.get('name') === 'super admin' || (!(this instanceof Query) && this.isSuperAdmin()); } } }, @@ -1578,6 +1575,10 @@ function gh14696() { validate: { async validator(v: any) { expectAssignable>(this); + if (this instanceof Query) { + const doc = await this.clone().findOne().orFail(); + return doc.isSuperAdmin(); + } return !v || this.get('name') === 'super admin'; } } From 0996b2b22743125221ce495a228fd817efbb63ca Mon Sep 17 00:00:00 2001 From: hasezoey Date: Thu, 9 Jan 2025 12:51:02 +0100 Subject: [PATCH 008/209] test: remove testing for "q" "q" has been deprecated and superseded by js native Promise. --- package.json | 1 - test/connection.test.js | 15 --------------- 2 files changed, 16 deletions(-) diff --git a/package.json b/package.json index 6bf50dd9c44..f3ee6908b29 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "ncp": "^2.0.0", "nyc": "15.1.0", "pug": "3.0.3", - "q": "1.5.1", "sinon": "19.0.2", "stream-browserify": "3.0.0", "tsd": "0.31.2", diff --git a/test/connection.test.js b/test/connection.test.js index 03f87b40f3d..e2c3ee4d57d 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -7,7 +7,6 @@ const start = require('./common'); const STATES = require('../lib/connectionState'); -const Q = require('q'); const assert = require('assert'); const mongodb = require('mongodb'); const MongooseError = require('../lib/error/index'); @@ -119,20 +118,6 @@ describe('connections:', function() { }, /string.*createConnection/); }); - it('resolving with q (gh-5714)', async function() { - const bootMongo = Q.defer(); - - const conn = mongoose.createConnection(start.uri); - - conn.on('connected', function() { - bootMongo.resolve(this); - }); - - const _conn = await bootMongo.promise; - assert.equal(_conn, conn); - await conn.close(); - }); - it('connection plugins (gh-7378)', async function() { const conn1 = mongoose.createConnection(start.uri); const conn2 = mongoose.createConnection(start.uri); From ca0f47124ebfd982272a1fe928d26eaea174262a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 3 Mar 2025 14:58:29 -0500 Subject: [PATCH 009/209] style: apply changes from master --- types/index.d.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 15d2ccba26f..23e217582d1 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -543,17 +543,17 @@ declare module 'mongoose' { ? DateSchemaDefinition : (Function | string); - export type SchemaDefinitionProperty> = SchemaDefinitionWithBuiltInClass | - SchemaTypeOptions | - typeof SchemaType | - Schema | - Schema[] | - SchemaTypeOptions, EnforcedDocType, THydratedDocumentType>[] | - Function[] | - SchemaDefinition | - SchemaDefinition, EnforcedDocType, THydratedDocumentType>[] | - typeof Schema.Types.Mixed | - MixedSchemaTypeOptions; + export type SchemaDefinitionProperty> = SchemaDefinitionWithBuiltInClass + | SchemaTypeOptions + | typeof SchemaType + | Schema + | Schema[] + | SchemaTypeOptions, EnforcedDocType, THydratedDocumentType>[] + | Function[] + | SchemaDefinition + | SchemaDefinition, EnforcedDocType, THydratedDocumentType>[] + | typeof Schema.Types.Mixed + | MixedSchemaTypeOptions; export type SchemaDefinition> = T extends undefined ? { [path: string]: SchemaDefinitionProperty; } From c81ab9c8bc10fe0f56bea0c5d8127745ea1dda64 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 3 Mar 2025 16:16:48 -0500 Subject: [PATCH 010/209] BREAKING CHANGE: remove _executionStack, use _execCount instead to avoid perf overhead Fix #14906 --- lib/error/validation.js | 8 -------- lib/query.js | 26 +++++++------------------- lib/schemaType.js | 9 ++------- test/query.test.js | 2 -- 4 files changed, 9 insertions(+), 36 deletions(-) diff --git a/lib/error/validation.js b/lib/error/validation.js index faa4ea799aa..c90180bab80 100644 --- a/lib/error/validation.js +++ b/lib/error/validation.js @@ -44,14 +44,6 @@ class ValidationError extends MongooseError { return this.name + ': ' + combinePathErrors(this); } - /** - * inspect helper - * @api private - */ - inspect() { - return Object.assign(new Error(this.message), this); - } - /** * add message * @param {String} path diff --git a/lib/query.js b/lib/query.js index ffab30d0406..245554c5be7 100644 --- a/lib/query.js +++ b/lib/query.js @@ -116,7 +116,7 @@ function Query(conditions, options, model, collection) { this._transforms = []; this._hooks = new Kareem(); - this._executionStack = null; + this._execCount = 0; // this is the case where we have a CustomQuery, we need to check if we got // options passed in, and if we did, merge them in @@ -274,7 +274,6 @@ Query.prototype.toConstructor = function toConstructor() { p.setOptions(options); p.op = this.op; - p._validateOp(); p._conditions = clone(this._conditions); p._fields = clone(this._fields); p._update = clone(this._update, { @@ -323,7 +322,6 @@ Query.prototype.clone = function() { q.setOptions(options); q.op = this.op; - q._validateOp(); q._conditions = clone(this._conditions); q._fields = clone(this._fields); q._update = clone(this._update, { @@ -488,7 +486,7 @@ Query.prototype._validateOp = function() { this.error(new Error('Query has invalid `op`: "' + this.op + '"')); } - if (this.op !== 'estimatedDocumentCount' && this._conditions === null) { + if (this.op !== 'estimatedDocumentCount' && this._conditions == null) { throw new ObjectParameterError(this._conditions, 'filter', this.op); } }; @@ -2716,7 +2714,6 @@ Query.prototype.findOne = function(conditions, projection, options) { } this.op = 'findOne'; - this._validateOp(); if (options) { this.setOptions(options); @@ -2843,7 +2840,6 @@ Query.prototype.estimatedDocumentCount = function(options) { } this.op = 'estimatedDocumentCount'; - this._validateOp(); if (options != null) { this.setOptions(options); @@ -2898,7 +2894,6 @@ Query.prototype.countDocuments = function(conditions, options) { } this.op = 'countDocuments'; - this._validateOp(); if (canMerge(conditions)) { this.merge(conditions); @@ -2964,7 +2959,6 @@ Query.prototype.distinct = function(field, conditions, options) { } this.op = 'distinct'; - this._validateOp(); if (canMerge(conditions)) { this.merge(conditions); @@ -3366,7 +3360,6 @@ Query.prototype.findOneAndUpdate = function(filter, doc, options) { } this.op = 'findOneAndUpdate'; - this._validateOp(); this._validate(); switch (arguments.length) { @@ -3459,7 +3452,7 @@ Query.prototype._findOneAndUpdate = async function _findOneAndUpdate() { delete $set._id; this._update = { $set }; } else { - this._executionStack = null; + this._execCount = 0; const res = await this._findOne(); return res; } @@ -3539,7 +3532,6 @@ Query.prototype.findOneAndDelete = function(filter, options) { } this.op = 'findOneAndDelete'; - this._validateOp(); this._validate(); if (canMerge(filter)) { @@ -3637,7 +3629,6 @@ Query.prototype.findOneAndReplace = function(filter, replacement, options) { } this.op = 'findOneAndReplace'; - this._validateOp(); this._validate(); if (canMerge(filter)) { @@ -4229,7 +4220,6 @@ Query.prototype.replaceOne = function(conditions, doc, options, callback) { function _update(query, op, filter, doc, options, callback) { // make sure we don't send in the whole Document to merge() query.op = op; - query._validateOp(); doc = doc || {}; // strict is an option used in the update checking, make sure it gets set @@ -4414,6 +4404,7 @@ Query.prototype.exec = async function exec(op) { throw new MongooseError('Query.prototype.exec() no longer accepts a callback'); } + this._validateOp(); if (typeof op === 'string') { this.op = op; } @@ -4434,17 +4425,14 @@ Query.prototype.exec = async function exec(op) { throw new Error('Invalid field "" passed to sort()'); } - if (this._executionStack != null) { + if (this._execCount > 0) { let str = this.toString(); if (str.length > 60) { str = str.slice(0, 60) + '...'; } - const err = new MongooseError('Query was already executed: ' + str); - err.originalStack = this._executionStack; - throw err; - } else { - this._executionStack = new Error().stack; + throw new MongooseError('Query was already executed: ' + str); } + this._execCount++; let skipWrappedFunction = null; try { diff --git a/lib/schemaType.js b/lib/schemaType.js index 22c9edbd473..aae3e244423 100644 --- a/lib/schemaType.js +++ b/lib/schemaType.js @@ -12,7 +12,6 @@ const clone = require('./helpers/clone'); const handleImmutable = require('./helpers/schematype/handleImmutable'); const isAsyncFunction = require('./helpers/isAsyncFunction'); const isSimpleValidator = require('./helpers/isSimpleValidator'); -const immediate = require('./helpers/immediate'); const schemaTypeSymbol = require('./helpers/symbols').schemaTypeSymbol; const utils = require('./utils'); const validatorErrorSymbol = require('./helpers/symbols').validatorErrorSymbol; @@ -1395,17 +1394,13 @@ SchemaType.prototype.doValidate = function(value, fn, scope, options) { } if (ok === undefined || ok) { if (--count <= 0) { - immediate(function() { - fn(null); - }); + fn(null); } } else { const ErrorConstructor = validatorProperties.ErrorConstructor || ValidatorError; err = new ErrorConstructor(validatorProperties, scope); err[validatorErrorSymbol] = true; - immediate(function() { - fn(err); - }); + fn(err); } } }; diff --git a/test/query.test.js b/test/query.test.js index 7df766346e7..d6ea137f964 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -3081,7 +3081,6 @@ describe('Query', function() { it('throws an error if executed multiple times (gh-7398)', async function() { const Test = db.model('Test', Schema({ name: String })); - const q = Test.findOne(); await q; @@ -3090,7 +3089,6 @@ describe('Query', function() { assert.ok(err); assert.equal(err.name, 'MongooseError'); assert.equal(err.message, 'Query was already executed: Test.findOne({})'); - assert.ok(err.originalStack); err = await q.clone().then(() => null, err => err); assert.ifError(err); From 559225c0a19324b86c74b8ecdbd027090bfa8455 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 3 Mar 2025 17:08:35 -0500 Subject: [PATCH 011/209] refactor: remove unnecessary immediate() around path validation re: #14906 --- lib/document.js | 105 +++++++++++++++++++++++------------------------- 1 file changed, 51 insertions(+), 54 deletions(-) diff --git a/lib/document.js b/lib/document.js index e43c0e67157..2a7f951e304 100644 --- a/lib/document.js +++ b/lib/document.js @@ -2655,7 +2655,8 @@ Document.prototype.validate = async function validate(pathsToValidate, options) this.$op = null; this.$__.validating = null; if (error != null) { - return reject(error); + reject(error); + return; } resolve(); }); @@ -3012,13 +3013,14 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) { } const validated = {}; - let total = 0; + let total = paths.length; let pathsToSave = this.$__.saveOptions?.pathsToSave; if (Array.isArray(pathsToSave)) { pathsToSave = new Set(pathsToSave); for (const path of paths) { if (!pathsToSave.has(path)) { + --total || complete(); continue; } validatePath(path); @@ -3031,67 +3033,62 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) { function validatePath(path) { if (path == null || validated[path]) { - return; + return --total || complete(); } validated[path] = true; - total++; + const schemaType = _this.$__schema.path(path); - immediate(function() { - const schemaType = _this.$__schema.path(path); + if (!schemaType) { + return --total || complete(); + } - if (!schemaType) { - return --total || complete(); - } + // If user marked as invalid or there was a cast error, don't validate + if (!_this.$isValid(path)) { + return --total || complete(); + } - // If user marked as invalid or there was a cast error, don't validate - if (!_this.$isValid(path)) { - --total || complete(); - return; - } + // If setting a path under a mixed path, avoid using the mixed path validator (gh-10141) + if (schemaType[schemaMixedSymbol] != null && path !== schemaType.path) { + return --total || complete(); + } - // If setting a path under a mixed path, avoid using the mixed path validator (gh-10141) - if (schemaType[schemaMixedSymbol] != null && path !== schemaType.path) { - return --total || complete(); - } + let val = _this.$__getValue(path); - let val = _this.$__getValue(path); - - // If you `populate()` and get back a null value, required validators - // shouldn't fail (gh-8018). We should always fall back to the populated - // value. - let pop; - if ((pop = _this.$populated(path))) { - val = pop; - } else if (val != null && val.$__ != null && val.$__.wasPopulated) { - // Array paths, like `somearray.1`, do not show up as populated with `$populated()`, - // so in that case pull out the document's id - val = val._doc._id; - } - const scope = _this.$__.pathsToScopes != null && path in _this.$__.pathsToScopes ? - _this.$__.pathsToScopes[path] : - _this; - - const doValidateOptions = { - ...doValidateOptionsByPath[path], - path: path, - validateAllPaths, - _nestedValidate: true - }; - - schemaType.doValidate(val, function(err) { - if (err) { - const isSubdoc = schemaType.$isSingleNested || - schemaType.$isArraySubdocument || - schemaType.$isMongooseDocumentArray; - if (isSubdoc && err instanceof ValidationError) { - return --total || complete(); - } - _this.invalidate(path, err, undefined, true); + // If you `populate()` and get back a null value, required validators + // shouldn't fail (gh-8018). We should always fall back to the populated + // value. + let pop; + if ((pop = _this.$populated(path))) { + val = pop; + } else if (val != null && val.$__ != null && val.$__.wasPopulated) { + // Array paths, like `somearray.1`, do not show up as populated with `$populated()`, + // so in that case pull out the document's id + val = val._doc._id; + } + const scope = _this.$__.pathsToScopes != null && path in _this.$__.pathsToScopes ? + _this.$__.pathsToScopes[path] : + _this; + + const doValidateOptions = { + ...doValidateOptionsByPath[path], + path: path, + validateAllPaths, + _nestedValidate: true + }; + + schemaType.doValidate(val, function(err) { + if (err) { + const isSubdoc = schemaType.$isSingleNested || + schemaType.$isArraySubdocument || + schemaType.$isMongooseDocumentArray; + if (isSubdoc && err instanceof ValidationError) { + return --total || complete(); } - --total || complete(); - }, scope, doValidateOptions); - }); + _this.invalidate(path, err, undefined, true); + } + --total || complete(); + }, scope, doValidateOptions); } function complete() { From 072a1d0265a4fdda9d6078013f0f796fe4421e52 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 4 Mar 2025 15:33:28 -0500 Subject: [PATCH 012/209] BREAKING CHANGE: refactor validate() to be async and validate hooks out of kareem wrappers --- lib/browser.js | 2 + lib/browserDocument.js | 31 ++++++++ lib/document.js | 129 +++++++++++++++++--------------- lib/helpers/model/applyHooks.js | 31 ++++++-- lib/schema/documentArray.js | 2 +- test/document.test.js | 1 - test/query.test.js | 2 +- test/types.document.test.js | 8 +- 8 files changed, 133 insertions(+), 73 deletions(-) diff --git a/lib/browser.js b/lib/browser.js index a01c9187b0d..5369dedf44c 100644 --- a/lib/browser.js +++ b/lib/browser.js @@ -5,6 +5,7 @@ require('./driver').set(require('./drivers/browser')); const DocumentProvider = require('./documentProvider.js'); +const applyHooks = require('./helpers/model/applyHooks.js'); DocumentProvider.setBrowser(true); @@ -127,6 +128,7 @@ exports.model = function(name, schema) { } } Model.modelName = name; + applyHooks(Model, schema); return Model; }; diff --git a/lib/browserDocument.js b/lib/browserDocument.js index bf9b22a0bf4..fcdf78a5cb9 100644 --- a/lib/browserDocument.js +++ b/lib/browserDocument.js @@ -93,6 +93,37 @@ Document.$emitter = new EventEmitter(); }; }); +/*! + * ignore + */ + +Document.prototype._execDocumentPreHooks = function _execDocumentPreHooks(opName) { + return new Promise((resolve, reject) => { + this._middleware.execPre(opName, this, [], (error) => { + if (error != null) { + reject(error); + return; + } + resolve(); + }); + }); +}; + +/*! + * ignore + */ + +Document.prototype._execDocumentPostHooks = function _execDocumentPostHooks(opName, error) { + return new Promise((resolve, reject) => { + this._middleware.execPost(opName, this, [this], { error }, function(error) { + if (error) { + return reject(error); + } + resolve(); + }); + }); +}; + /*! * Module exports. */ diff --git a/lib/document.js b/lib/document.js index 2a7f951e304..151cb5ef8de 100644 --- a/lib/document.js +++ b/lib/document.js @@ -30,7 +30,6 @@ const getEmbeddedDiscriminatorPath = require('./helpers/document/getEmbeddedDisc const getKeysInSchemaOrder = require('./helpers/schema/getKeysInSchemaOrder'); const getSubdocumentStrictValue = require('./helpers/schema/getSubdocumentStrictValue'); const handleSpreadDoc = require('./helpers/document/handleSpreadDoc'); -const immediate = require('./helpers/immediate'); const isBsonType = require('./helpers/isBsonType'); const isDefiningProjection = require('./helpers/projection/isDefiningProjection'); const isExclusive = require('./helpers/projection/isExclusive'); @@ -2650,17 +2649,12 @@ Document.prototype.validate = async function validate(pathsToValidate, options) throw parallelValidate; } - return new Promise((resolve, reject) => { - this.$__validate(pathsToValidate, options, (error) => { - this.$op = null; - this.$__.validating = null; - if (error != null) { - reject(error); - return; - } - resolve(); - }); - }); + try { + await this.$__validate(pathsToValidate, options); + } finally { + this.$op = null; + this.$__.validating = null; + } }; /** @@ -2894,16 +2888,42 @@ function _pushNestedArrayPaths(val, paths, path) { * ignore */ -Document.prototype.$__validate = function(pathsToValidate, options, callback) { +Document.prototype._execDocumentPreHooks = function _execDocumentPreHooks(opName) { + return new Promise((resolve, reject) => { + this.constructor._middleware.execPre(opName, this, [], (error) => { + if (error != null) { + reject(error); + return; + } + resolve(); + }); + }); +}; + +/*! + * ignore + */ + +Document.prototype._execDocumentPostHooks = function _execDocumentPostHooks(opName, error) { + return new Promise((resolve, reject) => { + this.constructor._middleware.execPost(opName, this, [this], { error }, function(error) { + if (error) { + return reject(error); + } + resolve(); + }); + }); +}; + +/*! + * ignore + */ + +Document.prototype.$__validate = async function $__validate(pathsToValidate, options) { + await this._execDocumentPreHooks('validate'); + if (this.$__.saveOptions && this.$__.saveOptions.pathsToSave && !pathsToValidate) { pathsToValidate = [...this.$__.saveOptions.pathsToSave]; - } else if (typeof pathsToValidate === 'function') { - callback = pathsToValidate; - options = null; - pathsToValidate = null; - } else if (typeof options === 'function') { - callback = options; - options = null; } const hasValidateModifiedOnlyOption = options && @@ -3001,56 +3021,52 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) { } if (paths.length === 0) { - return immediate(function() { - const error = _complete(); - if (error) { - return _this.$__schema.s.hooks.execPost('validate:error', _this, [_this], { error: error }, function(error) { - callback(error); - }); - } - callback(null, _this); - }); + const error = _complete(); + await this._execDocumentPostHooks('validate', error); + return; } const validated = {}; - let total = paths.length; let pathsToSave = this.$__.saveOptions?.pathsToSave; + const promises = []; if (Array.isArray(pathsToSave)) { pathsToSave = new Set(pathsToSave); for (const path of paths) { if (!pathsToSave.has(path)) { - --total || complete(); continue; } - validatePath(path); + promises.push(validatePath(path)); } } else { for (const path of paths) { - validatePath(path); + promises.push(validatePath(path)); } } + await Promise.all(promises); + const error = _complete(); + await this._execDocumentPostHooks('validate', error); - function validatePath(path) { + async function validatePath(path) { if (path == null || validated[path]) { - return --total || complete(); + return; } validated[path] = true; const schemaType = _this.$__schema.path(path); if (!schemaType) { - return --total || complete(); + return; } // If user marked as invalid or there was a cast error, don't validate if (!_this.$isValid(path)) { - return --total || complete(); + return; } // If setting a path under a mixed path, avoid using the mixed path validator (gh-10141) if (schemaType[schemaMixedSymbol] != null && path !== schemaType.path) { - return --total || complete(); + return; } let val = _this.$__getValue(path); @@ -3077,30 +3093,23 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) { _nestedValidate: true }; - schemaType.doValidate(val, function(err) { - if (err) { - const isSubdoc = schemaType.$isSingleNested || - schemaType.$isArraySubdocument || - schemaType.$isMongooseDocumentArray; - if (isSubdoc && err instanceof ValidationError) { - return --total || complete(); + await new Promise((resolve) => { + schemaType.doValidate(val, function doValidateCallback(err) { + if (err) { + const isSubdoc = schemaType.$isSingleNested || + schemaType.$isArraySubdocument || + schemaType.$isMongooseDocumentArray; + if (isSubdoc && err instanceof ValidationError) { + return resolve(); + } + _this.invalidate(path, err, undefined, true); + resolve(); + } else { + resolve(); } - _this.invalidate(path, err, undefined, true); - } - --total || complete(); - }, scope, doValidateOptions); - } - - function complete() { - const error = _complete(); - if (error) { - return _this.$__schema.s.hooks.execPost('validate:error', _this, [_this], { error: error }, function(error) { - callback(error); - }); - } - callback(null, _this); + }, scope, doValidateOptions); + }); } - }; /*! diff --git a/lib/helpers/model/applyHooks.js b/lib/helpers/model/applyHooks.js index 998da62f42a..00262792bf6 100644 --- a/lib/helpers/model/applyHooks.js +++ b/lib/helpers/model/applyHooks.js @@ -16,7 +16,6 @@ module.exports = applyHooks; applyHooks.middlewareFunctions = [ 'deleteOne', 'save', - 'validate', 'remove', 'updateOne', 'init' @@ -47,15 +46,29 @@ function applyHooks(model, schema, options) { contextParameter: true }; const objToDecorate = options.decorateDoc ? model : model.prototype; - model.$appliedHooks = true; for (const key of Object.keys(schema.paths)) { - const type = schema.paths[key]; + let type = schema.paths[key]; let childModel = null; if (type.$isSingleNested) { childModel = type.caster; } else if (type.$isMongooseDocumentArray) { childModel = type.Constructor; + } else if (type.instance === 'Array') { + let curType = type; + // Drill into nested arrays to check if nested array contains document array + while (curType.instance === 'Array') { + if (curType.$isMongooseDocumentArray) { + childModel = curType.Constructor; + type = curType; + break; + } + curType = curType.getEmbeddedSchemaType(); + } + + if (childModel == null) { + continue; + } } else { continue; } @@ -64,7 +77,11 @@ function applyHooks(model, schema, options) { continue; } - applyHooks(childModel, type.schema, { ...options, isChildSchema: true }); + applyHooks(childModel, type.schema, { + ...options, + decorateDoc: false, // Currently subdocs inherit directly from NodeJSDocument in browser + isChildSchema: true + }); if (childModel.discriminators != null) { const keys = Object.keys(childModel.discriminators); for (const key of keys) { @@ -102,11 +119,9 @@ function applyHooks(model, schema, options) { model._middleware = middleware; - objToDecorate.$__originalValidate = objToDecorate.$__originalValidate || objToDecorate.$__validate; - - const internalMethodsToWrap = options && options.isChildSchema ? ['save', 'validate', 'deleteOne'] : ['save', 'validate']; + const internalMethodsToWrap = options && options.isChildSchema ? ['save', 'deleteOne'] : ['save']; for (const method of internalMethodsToWrap) { - const toWrap = method === 'validate' ? '$__originalValidate' : `$__${method}`; + const toWrap = `$__${method}`; const wrapped = middleware. createWrapper(method, objToDecorate[toWrap], null, kareemOptions); objToDecorate[`$__${method}`] = wrapped; diff --git a/lib/schema/documentArray.js b/lib/schema/documentArray.js index 77b78fa860e..cf84b303f51 100644 --- a/lib/schema/documentArray.js +++ b/lib/schema/documentArray.js @@ -283,7 +283,7 @@ SchemaDocumentArray.prototype.doValidate = function(array, fn, scope, options) { continue; } - doc.$__validate(null, options, callback); + doc.$__validate(null, options).then(() => callback(), err => callback(err)); } } }; diff --git a/test/document.test.js b/test/document.test.js index 755efc34e67..b52f522d6ca 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -1725,7 +1725,6 @@ describe('document', function() { assert.equal(d.nested.setr, 'undefined setter'); dateSetterCalled = false; d.date = undefined; - await d.validate(); assert.ok(dateSetterCalled); }); diff --git a/test/query.test.js b/test/query.test.js index d6ea137f964..38667e5e02f 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -3314,7 +3314,6 @@ describe('Query', function() { quiz_title: String, questions: [questionSchema] }, { strict: 'throw' }); - const Quiz = db.model('Test', quizSchema); const mcqQuestionSchema = new Schema({ text: String, @@ -3322,6 +3321,7 @@ describe('Query', function() { }, { strict: 'throw' }); quizSchema.path('questions').discriminator('mcq', mcqQuestionSchema); + const Quiz = db.model('Test', quizSchema); const id1 = new mongoose.Types.ObjectId(); const id2 = new mongoose.Types.ObjectId(); diff --git a/test/types.document.test.js b/test/types.document.test.js index 8a87b06917e..46e5efc31ac 100644 --- a/test/types.document.test.js +++ b/test/types.document.test.js @@ -7,11 +7,13 @@ const start = require('./common'); -const assert = require('assert'); -const mongoose = start.mongoose; const ArraySubdocument = require('../lib/types/arraySubdocument'); const EventEmitter = require('events').EventEmitter; const DocumentArray = require('../lib/types/documentArray'); +const applyHooks = require('../lib/helpers/model/applyHooks'); +const assert = require('assert'); + +const mongoose = start.mongoose; const Schema = mongoose.Schema; const ValidationError = mongoose.Document.ValidationError; @@ -54,6 +56,8 @@ describe('types.document', function() { work: { type: String, validate: /^good/ } })); + applyHooks(Subdocument, Subdocument.prototype.schema); + RatingSchema = new Schema({ stars: Number, description: { source: { url: String, time: Date } } From 6717014c53fcb4f2b2a8ba738bea5a8bd70586bd Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 9 Mar 2025 13:14:59 -0400 Subject: [PATCH 013/209] BREAKING CHANGE: make parallel validate error not track original validate() call stack --- lib/document.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/document.js b/lib/document.js index 151cb5ef8de..31cb0263f7d 100644 --- a/lib/document.js +++ b/lib/document.js @@ -2619,7 +2619,6 @@ Document.prototype.validate = async function validate(pathsToValidate, options) if (typeof pathsToValidate === 'function' || typeof options === 'function' || typeof arguments[2] === 'function') { throw new MongooseError('Document.prototype.validate() no longer accepts a callback'); } - let parallelValidate; this.$op = 'validate'; if (arguments.length === 1) { @@ -2637,16 +2636,9 @@ Document.prototype.validate = async function validate(pathsToValidate, options) if (this.$isSubdocument != null) { // Skip parallel validate check for subdocuments } else if (this.$__.validating && !_skipParallelValidateCheck) { - parallelValidate = new ParallelValidateError(this, { - parentStack: options && options.parentStack, - conflictStack: this.$__.validating.stack - }); + throw new ParallelValidateError(this); } else if (!_skipParallelValidateCheck) { - this.$__.validating = new ParallelValidateError(this, { parentStack: options && options.parentStack }); - } - - if (parallelValidate != null) { - throw parallelValidate; + this.$__.validating = true; } try { From b20df583056e9afcd53599ce84970de67aec1780 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 10 Mar 2025 14:00:41 -0400 Subject: [PATCH 014/209] BREAKING CHANGE: make SchemaType.prototype.doValidate() an async function for cleaner stack traces --- docs/migrating_to_9.md | 13 + lib/document.js | 27 +- lib/helpers/updateValidators.js | 162 ++++-------- lib/model.js | 57 ++-- lib/query.js | 9 +- lib/schema/documentArray.js | 77 ++---- lib/schema/documentArrayElement.js | 10 +- lib/schema/subdocument.js | 22 +- lib/schemaType.js | 83 +++--- test/document.test.js | 6 +- test/schema.documentarray.test.js | 9 +- test/schema.number.test.js | 21 +- test/schema.validation.test.js | 405 ++++++++++------------------- test/updateValidators.unit.test.js | 111 -------- 14 files changed, 325 insertions(+), 687 deletions(-) create mode 100644 docs/migrating_to_9.md delete mode 100644 test/updateValidators.unit.test.js diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md new file mode 100644 index 00000000000..9dd3dbbfa88 --- /dev/null +++ b/docs/migrating_to_9.md @@ -0,0 +1,13 @@ +# Migrating from 8.x to 9.x + + + +There are several backwards-breaking changes you should be aware of when migrating from Mongoose 8.x to Mongoose 9.x. + +If you're still on Mongoose 7.x or earlier, please read the [Mongoose 7.x to 8.x migration guide](migrating_to_8.html) and upgrade to Mongoose 8.x first before upgrading to Mongoose 9. + +## `Schema.prototype.doValidate()` now returns a promise diff --git a/lib/document.js b/lib/document.js index 31cb0263f7d..d4935c33e0a 100644 --- a/lib/document.js +++ b/lib/document.js @@ -3085,22 +3085,17 @@ Document.prototype.$__validate = async function $__validate(pathsToValidate, opt _nestedValidate: true }; - await new Promise((resolve) => { - schemaType.doValidate(val, function doValidateCallback(err) { - if (err) { - const isSubdoc = schemaType.$isSingleNested || - schemaType.$isArraySubdocument || - schemaType.$isMongooseDocumentArray; - if (isSubdoc && err instanceof ValidationError) { - return resolve(); - } - _this.invalidate(path, err, undefined, true); - resolve(); - } else { - resolve(); - } - }, scope, doValidateOptions); - }); + try { + await schemaType.doValidate(val, scope, doValidateOptions); + } catch (err) { + const isSubdoc = schemaType.$isSingleNested || + schemaType.$isArraySubdocument || + schemaType.$isMongooseDocumentArray; + if (isSubdoc && err instanceof ValidationError) { + return; + } + _this.invalidate(path, err, undefined, true); + } } }; diff --git a/lib/helpers/updateValidators.js b/lib/helpers/updateValidators.js index 176eff26e16..f819074d48a 100644 --- a/lib/helpers/updateValidators.js +++ b/lib/helpers/updateValidators.js @@ -21,7 +21,7 @@ const modifiedPaths = require('./common').modifiedPaths; * @api private */ -module.exports = function(query, schema, castedDoc, options, callback) { +module.exports = async function updateValidators(query, schema, castedDoc, options) { const keys = Object.keys(castedDoc || {}); let updatedKeys = {}; let updatedValues = {}; @@ -32,9 +32,8 @@ module.exports = function(query, schema, castedDoc, options, callback) { const modified = {}; let currentUpdate; let key; - let i; - for (i = 0; i < numKeys; ++i) { + for (let i = 0; i < numKeys; ++i) { if (keys[i].startsWith('$')) { hasDollarUpdate = true; if (keys[i] === '$push' || keys[i] === '$addToSet') { @@ -89,161 +88,108 @@ module.exports = function(query, schema, castedDoc, options, callback) { const alreadyValidated = []; const context = query; - function iter(i, v) { + for (let i = 0; i < numUpdates; ++i) { + const v = updatedValues[updates[i]]; const schemaPath = schema._getSchema(updates[i]); if (schemaPath == null) { - return; + continue; } if (schemaPath.instance === 'Mixed' && schemaPath.path !== updates[i]) { - return; + continue; } if (v && Array.isArray(v.$in)) { v.$in.forEach((v, i) => { - validatorsToExecute.push(function(callback) { - schemaPath.doValidate( - v, - function(err) { - if (err) { - err.path = updates[i] + '.$in.' + i; - validationErrors.push(err); - } - callback(null); - }, - context, - { updateValidator: true }); - }); + validatorsToExecute.push( + schemaPath.doValidate(v, context, { updateValidator: true }).catch(err => { + err.path = updates[i] + '.$in.' + i; + validationErrors.push(err); + }) + ); }); } else { if (isPull[updates[i]] && schemaPath.$isMongooseArray) { - return; + continue; } if (schemaPath.$isMongooseDocumentArrayElement && v != null && v.$__ != null) { alreadyValidated.push(updates[i]); - validatorsToExecute.push(function(callback) { - schemaPath.doValidate(v, function(err) { - if (err) { - if (err.errors) { - for (const key of Object.keys(err.errors)) { - const _err = err.errors[key]; - _err.path = updates[i] + '.' + key; - validationErrors.push(_err); - } - } else { - err.path = updates[i]; - validationErrors.push(err); + validatorsToExecute.push( + schemaPath.doValidate(v, context, { updateValidator: true }).catch(err => { + if (err.errors) { + for (const key of Object.keys(err.errors)) { + const _err = err.errors[key]; + _err.path = updates[i] + '.' + key; + validationErrors.push(_err); } + } else { + err.path = updates[i]; + validationErrors.push(err); } - - return callback(null); - }, context, { updateValidator: true }); - }); + }) + ); } else { - validatorsToExecute.push(function(callback) { - for (const path of alreadyValidated) { - if (updates[i].startsWith(path + '.')) { - return callback(null); - } + for (const path of alreadyValidated) { + if (updates[i].startsWith(path + '.')) { + continue; } - - schemaPath.doValidate(v, function(err) { + } + validatorsToExecute.push( + schemaPath.doValidate(v, context, { updateValidator: true }).catch(err => { if (schemaPath.schema != null && schemaPath.schema.options.storeSubdocValidationError === false && err instanceof ValidationError) { - return callback(null); + return; } if (err) { err.path = updates[i]; validationErrors.push(err); } - callback(null); - }, context, { updateValidator: true }); - }); + }) + ); } } } - for (i = 0; i < numUpdates; ++i) { - iter(i, updatedValues[updates[i]]); - } const arrayUpdates = Object.keys(arrayAtomicUpdates); for (const arrayUpdate of arrayUpdates) { let schemaPath = schema._getSchema(arrayUpdate); if (schemaPath && schemaPath.$isMongooseDocumentArray) { - validatorsToExecute.push(function(callback) { + validatorsToExecute.push( schemaPath.doValidate( arrayAtomicUpdates[arrayUpdate], - getValidationCallback(arrayUpdate, validationErrors, callback), - options && options.context === 'query' ? query : null); - }); + options && options.context === 'query' ? query : null + ).catch(err => { + err.path = arrayUpdate; + validationErrors.push(err); + }) + ); } else { schemaPath = schema._getSchema(arrayUpdate + '.0'); for (const atomicUpdate of arrayAtomicUpdates[arrayUpdate]) { - validatorsToExecute.push(function(callback) { + validatorsToExecute.push( schemaPath.doValidate( atomicUpdate, - getValidationCallback(arrayUpdate, validationErrors, callback), options && options.context === 'query' ? query : null, - { updateValidator: true }); - }); + { updateValidator: true } + ).catch(err => { + err.path = arrayUpdate; + validationErrors.push(err); + }) + ); } } } - if (callback != null) { - let numValidators = validatorsToExecute.length; - if (numValidators === 0) { - return _done(callback); - } - for (const validator of validatorsToExecute) { - validator(function() { - if (--numValidators <= 0) { - _done(callback); - } - }); - } - - return; - } - - return function(callback) { - let numValidators = validatorsToExecute.length; - if (numValidators === 0) { - return _done(callback); - } - for (const validator of validatorsToExecute) { - validator(function() { - if (--numValidators <= 0) { - _done(callback); - } - }); - } - }; + await Promise.all(validatorsToExecute); + if (validationErrors.length) { + const err = new ValidationError(null); - function _done(callback) { - if (validationErrors.length) { - const err = new ValidationError(null); - - for (const validationError of validationErrors) { - err.addError(validationError.path, validationError); - } - - return callback(err); + for (const validationError of validationErrors) { + err.addError(validationError.path, validationError); } - callback(null); - } - - function getValidationCallback(arrayUpdate, validationErrors, callback) { - return function(err) { - if (err) { - err.path = arrayUpdate; - validationErrors.push(err); - } - callback(null); - }; + throw err; } }; - diff --git a/lib/model.js b/lib/model.js index e39ea7d4b52..4c2247a4ea1 100644 --- a/lib/model.js +++ b/lib/model.js @@ -4289,43 +4289,34 @@ Model.validate = async function validate(obj, pathsOrOptions, context) { } } - let remaining = paths.size; - - return new Promise((resolve, reject) => { - for (const path of paths) { - const schemaType = schema.path(path); - if (schemaType == null) { - _checkDone(); - continue; - } + const promises = []; + for (const path of paths) { + const schemaType = schema.path(path); + if (schemaType == null) { + continue; + } - const pieces = path.indexOf('.') === -1 ? [path] : path.split('.'); - let cur = obj; - for (let i = 0; i < pieces.length - 1; ++i) { - cur = cur[pieces[i]]; - } + const pieces = path.indexOf('.') === -1 ? [path] : path.split('.'); + let cur = obj; + for (let i = 0; i < pieces.length - 1; ++i) { + cur = cur[pieces[i]]; + } - const val = get(obj, path, void 0); + const val = get(obj, path, void 0); + promises.push( + schemaType.doValidate(val, context, { path: path }).catch(err => { + error = error || new ValidationError(); + error.addError(path, err); + }) + ); + } - schemaType.doValidate(val, err => { - if (err) { - error = error || new ValidationError(); - error.addError(path, err); - } - _checkDone(); - }, context, { path: path }); - } + await Promise.all(promises); + if (error != null) { + throw error; + } - function _checkDone() { - if (--remaining <= 0) { - if (error) { - reject(error); - } else { - resolve(obj); - } - } - } - }); + return obj; }; /** diff --git a/lib/query.js b/lib/query.js index 245554c5be7..ea177e5dd5d 100644 --- a/lib/query.js +++ b/lib/query.js @@ -3945,14 +3945,7 @@ Query.prototype.validate = async function validate(castedDoc, options, isOverwri if (isOverwriting) { await castedDoc.$validate(); } else { - await new Promise((resolve, reject) => { - updateValidators(this, this.model.schema, castedDoc, options, (err) => { - if (err != null) { - return reject(err); - } - resolve(); - }); - }); + await updateValidators(this, this.model.schema, castedDoc, options); } await _executePostHooks(this, null, null, 'validate'); diff --git a/lib/schema/documentArray.js b/lib/schema/documentArray.js index cf84b303f51..c117cb1c6a6 100644 --- a/lib/schema/documentArray.js +++ b/lib/schema/documentArray.js @@ -220,72 +220,45 @@ SchemaDocumentArray.prototype.discriminator = function(name, schema, options) { /** * Performs local validations first, then validations on each embedded doc * - * @api private + * @api public */ -SchemaDocumentArray.prototype.doValidate = function(array, fn, scope, options) { +SchemaDocumentArray.prototype.doValidate = async function doValidate(array, scope, options) { // lazy load MongooseDocumentArray || (MongooseDocumentArray = require('../types/documentArray')); - const _this = this; - try { - SchemaType.prototype.doValidate.call(this, array, cb, scope); - } catch (err) { - return fn(err); + await SchemaType.prototype.doValidate.call(this, array, scope); + if (options?.updateValidator) { + return; + } + if (!utils.isMongooseDocumentArray(array)) { + array = new MongooseDocumentArray(array, this.path, scope); } - function cb(err) { - if (err) { - return fn(err); - } - - let count = array && array.length; - let error; - - if (!count) { - return fn(); - } - if (options && options.updateValidator) { - return fn(); - } - if (!utils.isMongooseDocumentArray(array)) { - array = new MongooseDocumentArray(array, _this.path, scope); - } - + const promises = []; + for (let i = 0; i < array.length; ++i) { // handle sparse arrays, do not use array.forEach which does not // iterate over sparse elements yet reports array.length including // them :( - - function callback(err) { - if (err != null) { - error = err; - } - --count || fn(error); + let doc = array[i]; + if (doc == null) { + continue; + } + // If you set the array index directly, the doc might not yet be + // a full fledged mongoose subdoc, so make it into one. + if (!(doc instanceof Subdocument)) { + const Constructor = getConstructor(this.casterConstructor, array[i]); + doc = array[i] = new Constructor(doc, array, undefined, undefined, i); } - for (let i = 0, len = count; i < len; ++i) { - // sidestep sparse entries - let doc = array[i]; - if (doc == null) { - --count || fn(error); - continue; - } - - // If you set the array index directly, the doc might not yet be - // a full fledged mongoose subdoc, so make it into one. - if (!(doc instanceof Subdocument)) { - const Constructor = getConstructor(_this.casterConstructor, array[i]); - doc = array[i] = new Constructor(doc, array, undefined, undefined, i); - } - - if (options != null && options.validateModifiedOnly && !doc.$isModified()) { - --count || fn(error); - continue; - } - - doc.$__validate(null, options).then(() => callback(), err => callback(err)); + if (options != null && options.validateModifiedOnly && !doc.$isModified()) { + continue; } + + promises.push(doc.$__validate(null, options)); } + + await Promise.all(promises); }; /** diff --git a/lib/schema/documentArrayElement.js b/lib/schema/documentArrayElement.js index 5250b74b505..552dd94a428 100644 --- a/lib/schema/documentArrayElement.js +++ b/lib/schema/documentArrayElement.js @@ -58,21 +58,19 @@ SchemaDocumentArrayElement.prototype.cast = function(...args) { }; /** - * Casts contents for queries. + * Async validation on this individual array element * - * @param {String} $cond - * @param {any} [val] - * @api private + * @api public */ -SchemaDocumentArrayElement.prototype.doValidate = function(value, fn, scope, options) { +SchemaDocumentArrayElement.prototype.doValidate = async function doValidate(value, scope, options) { const Constructor = getConstructor(this.caster, value); if (value && !(value instanceof Constructor)) { value = new Constructor(value, scope, null, null, options && options.index != null ? options.index : null); } - return SchemaSubdocument.prototype.doValidate.call(this, value, fn, scope, options); + return SchemaSubdocument.prototype.doValidate.call(this, value, scope, options); }; /** diff --git a/lib/schema/subdocument.js b/lib/schema/subdocument.js index 3afdb8ee281..affe228c3d1 100644 --- a/lib/schema/subdocument.js +++ b/lib/schema/subdocument.js @@ -246,10 +246,10 @@ SchemaSubdocument.prototype.castForQuery = function($conditional, val, context, /** * Async validation on this single nested doc. * - * @api private + * @api public */ -SchemaSubdocument.prototype.doValidate = function(value, fn, scope, options) { +SchemaSubdocument.prototype.doValidate = async function doValidate(value, scope, options) { const Constructor = getConstructor(this.caster, value); if (value && !(value instanceof Constructor)) { @@ -258,21 +258,15 @@ SchemaSubdocument.prototype.doValidate = function(value, fn, scope, options) { if (options && options.skipSchemaValidators) { if (!value) { - return fn(null); + return; } - return value.validate().then(() => fn(null), err => fn(err)); + return value.validate(); } - SchemaType.prototype.doValidate.call(this, value, function(error) { - if (error) { - return fn(error); - } - if (!value) { - return fn(null); - } - - value.validate().then(() => fn(null), err => fn(err)); - }, scope, options); + await SchemaType.prototype.doValidate.call(this, value, scope, options); + if (value != null) { + await value.validate(); + } }; /** diff --git a/lib/schemaType.js b/lib/schemaType.js index aae3e244423..688a433f923 100644 --- a/lib/schemaType.js +++ b/lib/schemaType.js @@ -1307,7 +1307,6 @@ SchemaType.prototype.select = function select(val) { * Performs a validation of `value` using the validators declared for this SchemaType. * * @param {Any} value - * @param {Function} callback * @param {Object} scope * @param {Object} [options] * @param {String} [options.path] @@ -1315,28 +1314,20 @@ SchemaType.prototype.select = function select(val) { * @api public */ -SchemaType.prototype.doValidate = function(value, fn, scope, options) { +SchemaType.prototype.doValidate = async function doValidate(value, scope, options) { let err = false; const path = this.path; - if (typeof fn !== 'function') { - throw new TypeError(`Must pass callback function to doValidate(), got ${typeof fn}`); - } // Avoid non-object `validators` const validators = this.validators. filter(v => typeof v === 'object' && v !== null); - let count = validators.length; - - if (!count) { - return fn(null); + if (!validators.length) { + return; } + const promises = []; for (let i = 0, len = validators.length; i < len; ++i) { - if (err) { - break; - } - const v = validators[i]; const validator = v.validator; let ok; @@ -1346,17 +1337,18 @@ SchemaType.prototype.doValidate = function(value, fn, scope, options) { validatorProperties.fullPath = this.$fullPath; validatorProperties.value = value; - if (validator instanceof RegExp) { - validate(validator.test(value), validatorProperties, scope); - continue; - } - - if (typeof validator !== 'function') { + if (value === undefined && validator !== this.requiredValidator) { continue; } - - if (value === undefined && validator !== this.requiredValidator) { - validate(true, validatorProperties, scope); + if (validator instanceof RegExp) { + ok = validator.test(value); + if (ok === false) { + const ErrorConstructor = validatorProperties.ErrorConstructor || ValidatorError; + err = new ErrorConstructor(validatorProperties, scope); + err[validatorErrorSymbol] = true; + throw err; + } + } else if (typeof validator !== 'function') { continue; } @@ -1375,34 +1367,35 @@ SchemaType.prototype.doValidate = function(value, fn, scope, options) { } if (ok != null && typeof ok.then === 'function') { - ok.then( - function(ok) { validate(ok, validatorProperties, scope); }, - function(error) { - validatorProperties.reason = error; - validatorProperties.message = error.message; - ok = false; - validate(ok, validatorProperties, scope); - }); - } else { - validate(ok, validatorProperties, scope); - } - } - - function validate(ok, validatorProperties, scope) { - if (err) { - return; - } - if (ok === undefined || ok) { - if (--count <= 0) { - fn(null); - } - } else { + promises.push( + ok.then( + function(ok) { + if (ok === false) { + const ErrorConstructor = validatorProperties.ErrorConstructor || ValidatorError; + err = new ErrorConstructor(validatorProperties, scope); + err[validatorErrorSymbol] = true; + throw err; + } + }, + function(error) { + validatorProperties.reason = error; + validatorProperties.message = error.message; + ok = false; + const ErrorConstructor = validatorProperties.ErrorConstructor || ValidatorError; + err = new ErrorConstructor(validatorProperties, scope); + err[validatorErrorSymbol] = true; + throw err; + }) + ); + } else if (ok !== undefined && !ok) { const ErrorConstructor = validatorProperties.ErrorConstructor || ValidatorError; err = new ErrorConstructor(validatorProperties, scope); err[validatorErrorSymbol] = true; - fn(err); + throw err; } } + + await Promise.all(promises); }; diff --git a/test/document.test.js b/test/document.test.js index b52f522d6ca..c7463e01229 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -6502,14 +6502,16 @@ describe('document', function() { }); const Model = db.model('Test', schema); - await Model.create({ + let doc = new Model({ roles: [ { name: 'admin' }, { name: 'mod', folders: [{ folderId: 'foo' }] } ] }); + await doc.validate().then(() => null, err => console.log(err)); + await doc.save(); - const doc = await Model.findOne(); + doc = await Model.findOne(); doc.roles[1].folders.push({ folderId: 'bar' }); diff --git a/test/schema.documentarray.test.js b/test/schema.documentarray.test.js index d9ccb6c1f6b..92eee9a4320 100644 --- a/test/schema.documentarray.test.js +++ b/test/schema.documentarray.test.js @@ -150,14 +150,7 @@ describe('schema.documentarray', function() { const TestModel = mongoose.model('Test', testSchema); const testDoc = new TestModel(); - const err = await new Promise((resolve, reject) => { - testSchema.path('comments').$embeddedSchemaType.doValidate({}, err => { - if (err != null) { - return reject(err); - } - resolve(); - }, testDoc.comments, { index: 1 }); - }).then(() => null, err => err); + const err = await testSchema.path('comments').$embeddedSchemaType.doValidate({}, testDoc.comments, { index: 1 }).then(() => null, err => err); assert.equal(err.name, 'ValidationError'); assert.equal(err.message, 'Validation failed: text: Path `text` is required.'); }); diff --git a/test/schema.number.test.js b/test/schema.number.test.js index 99c69ca1540..461873535dc 100644 --- a/test/schema.number.test.js +++ b/test/schema.number.test.js @@ -10,35 +10,20 @@ describe('SchemaNumber', function() { it('allows 0 with required: true and ref set (gh-11912)', async function() { const schema = new Schema({ x: { type: Number, required: true, ref: 'Foo' } }); - await new Promise((resolve, reject) => { - schema.path('x').doValidate(0, err => { - if (err != null) { - return reject(err); - } - resolve(); - }); - }); + await schema.path('x').doValidate(0); }); it('allows calling `min()` with no message arg (gh-15236)', async function() { const schema = new Schema({ x: { type: Number } }); schema.path('x').min(0); - const err = await new Promise((resolve) => { - schema.path('x').doValidate(-1, err => { - resolve(err); - }); - }); + const err = await schema.path('x').doValidate(-1).then(() => null, err => err); assert.ok(err); assert.equal(err.message, 'Path `x` (-1) is less than minimum allowed value (0).'); schema.path('x').min(0, 'Invalid value!'); - const err2 = await new Promise((resolve) => { - schema.path('x').doValidate(-1, err => { - resolve(err); - }); - }); + const err2 = await schema.path('x').doValidate(-1).then(() => null, err => err); assert.equal(err2.message, 'Invalid value!'); }); }); diff --git a/test/schema.validation.test.js b/test/schema.validation.test.js index d70643c1825..243328c2f76 100644 --- a/test/schema.validation.test.js +++ b/test/schema.validation.test.js @@ -48,7 +48,7 @@ describe('schema', function() { done(); }); - it('string enum', function(done) { + it('string enum', async function() { const Test = new Schema({ complex: { type: String, enum: ['a', 'b', undefined, 'c', null] }, state: { type: String } @@ -71,92 +71,58 @@ describe('schema', function() { assert.equal(Test.path('state').validators.length, 1); assert.deepEqual(Test.path('state').enumValues, ['opening', 'open', 'closing', 'closed']); - Test.path('complex').doValidate('x', function(err) { - assert.ok(err instanceof ValidatorError); - }); + await assert.rejects(Test.path('complex').doValidate('x'), ValidatorError); // allow unsetting enums - Test.path('complex').doValidate(undefined, function(err) { - assert.ifError(err); - }); + await Test.path('complex').doValidate(undefined); - Test.path('complex').doValidate(null, function(err) { - assert.ifError(err); - }); - - Test.path('complex').doValidate('da', function(err) { - assert.ok(err instanceof ValidatorError); - }); + await Test.path('complex').doValidate(null); - Test.path('state').doValidate('x', function(err) { - assert.ok(err instanceof ValidatorError); - assert.equal(err.message, - 'enum validator failed for path `state`: test'); - }); + await assert.rejects( + Test.path('complex').doValidate('da'), + ValidatorError + ); - Test.path('state').doValidate('opening', function(err) { - assert.ifError(err); - }); + await assert.rejects( + Test.path('state').doValidate('x'), + err => { + assert.ok(err instanceof ValidatorError); + assert.equal(err.message, + 'enum validator failed for path `state`: test'); + return true; + } + ); - Test.path('state').doValidate('open', function(err) { - assert.ifError(err); - }); + await Test.path('state').doValidate('opening'); - done(); + await Test.path('state').doValidate('open'); }); - it('string regexp', function(done) { - let remaining = 10; + it('string regexp', async function() { const Test = new Schema({ simple: { type: String, match: /[a-z]/ } }); assert.equal(Test.path('simple').validators.length, 1); - Test.path('simple').doValidate('az', function(err) { - assert.ifError(err); - --remaining || done(); - }); + await Test.path('simple').doValidate('az'); Test.path('simple').match(/[0-9]/); assert.equal(Test.path('simple').validators.length, 2); - Test.path('simple').doValidate('12', function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects(Test.path('simple').doValidate('12'), ValidatorError); - Test.path('simple').doValidate('a12', function(err) { - assert.ifError(err); - --remaining || done(); - }); + await Test.path('simple').doValidate('a12'); - Test.path('simple').doValidate('', function(err) { - assert.ifError(err); - --remaining || done(); - }); - Test.path('simple').doValidate(null, function(err) { - assert.ifError(err); - --remaining || done(); - }); - Test.path('simple').doValidate(undefined, function(err) { - assert.ifError(err); - --remaining || done(); - }); + await Test.path('simple').doValidate(''); + await Test.path('simple').doValidate(null); + await Test.path('simple').doValidate(undefined); Test.path('simple').validators = []; Test.path('simple').match(/[1-9]/); - Test.path('simple').doValidate(0, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects(Test.path('simple').doValidate(0), ValidatorError); Test.path('simple').match(null); - Test.path('simple').doValidate(0, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); - - done(); + await assert.rejects(Test.path('simple').doValidate(0), ValidatorError); }); describe('non-required fields', function() { @@ -198,39 +164,32 @@ describe('schema', function() { }); }); - it('number min and max', function(done) { - let remaining = 4; + it('number min and max', async function() { const Tobi = new Schema({ friends: { type: Number, max: 15, min: 5 } }); assert.equal(Tobi.path('friends').validators.length, 2); - Tobi.path('friends').doValidate(10, function(err) { - assert.ifError(err); - --remaining || done(); - }); + await Tobi.path('friends').doValidate(10); - Tobi.path('friends').doValidate(100, function(err) { + await assert.rejects(Tobi.path('friends').doValidate(100), (err) => { assert.ok(err instanceof ValidatorError); assert.equal(err.path, 'friends'); assert.equal(err.kind, 'max'); assert.equal(err.value, 100); - --remaining || done(); + return true; }); - Tobi.path('friends').doValidate(1, function(err) { + await assert.rejects(Tobi.path('friends').doValidate(1), (err) => { assert.ok(err instanceof ValidatorError); assert.equal(err.path, 'friends'); assert.equal(err.kind, 'min'); - --remaining || done(); + return true; }); // null is allowed - Tobi.path('friends').doValidate(null, function(err) { - assert.ifError(err); - --remaining || done(); - }); + await Tobi.path('friends').doValidate(null); Tobi.path('friends').min(); Tobi.path('friends').max(); @@ -240,8 +199,7 @@ describe('schema', function() { }); describe('required', function() { - it('string required', function(done) { - let remaining = 4; + it('string required', async function() { const Test = new Schema({ simple: String }); @@ -249,29 +207,16 @@ describe('schema', function() { Test.path('simple').required(true); assert.equal(Test.path('simple').validators.length, 1); - Test.path('simple').doValidate(null, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects(Test.path('simple').doValidate(null), ValidatorError); - Test.path('simple').doValidate(undefined, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects(Test.path('simple').doValidate(undefined), ValidatorError); - Test.path('simple').doValidate('', function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects(Test.path('simple').doValidate(''), ValidatorError); - Test.path('simple').doValidate('woot', function(err) { - assert.ifError(err); - --remaining || done(); - }); + await Test.path('simple').doValidate('woot'); }); - it('string conditional required', function(done) { - let remaining = 8; + it('string conditional required', async function() { const Test = new Schema({ simple: String }); @@ -284,240 +229,172 @@ describe('schema', function() { Test.path('simple').required(isRequired); assert.equal(Test.path('simple').validators.length, 1); - Test.path('simple').doValidate(null, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects( + Test.path('simple').doValidate(null), + ValidatorError + ); - Test.path('simple').doValidate(undefined, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects( + Test.path('simple').doValidate(undefined), + ValidatorError + ); - Test.path('simple').doValidate('', function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects( + Test.path('simple').doValidate(''), + ValidatorError + ); - Test.path('simple').doValidate('woot', function(err) { - assert.ifError(err); - --remaining || done(); - }); + await Test.path('simple').doValidate('woot'); required = false; - Test.path('simple').doValidate(null, function(err) { - assert.ifError(err); - --remaining || done(); - }); + await Test.path('simple').doValidate(null); - Test.path('simple').doValidate(undefined, function(err) { - assert.ifError(err); - --remaining || done(); - }); + await Test.path('simple').doValidate(undefined); - Test.path('simple').doValidate('', function(err) { - assert.ifError(err); - --remaining || done(); - }); + await Test.path('simple').doValidate(''); - Test.path('simple').doValidate('woot', function(err) { - assert.ifError(err); - --remaining || done(); - }); + await Test.path('simple').doValidate('woot'); }); - it('number required', function(done) { - let remaining = 3; + it('number required', async function() { const Edwald = new Schema({ friends: { type: Number, required: true } }); - Edwald.path('friends').doValidate(null, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects( + Edwald.path('friends').doValidate(null), + ValidatorError + ); - Edwald.path('friends').doValidate(undefined, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects( + Edwald.path('friends').doValidate(undefined), + ValidatorError + ); - Edwald.path('friends').doValidate(0, function(err) { - assert.ifError(err); - --remaining || done(); - }); + await Edwald.path('friends').doValidate(0); }); - it('date required', function(done) { - let remaining = 3; + it('date required', async function() { const Loki = new Schema({ birth_date: { type: Date, required: true } }); - Loki.path('birth_date').doValidate(null, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects( + Loki.path('birth_date').doValidate(null), + ValidatorError + ); - Loki.path('birth_date').doValidate(undefined, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects( + Loki.path('birth_date').doValidate(undefined), + ValidatorError + ); - Loki.path('birth_date').doValidate(new Date(), function(err) { - assert.ifError(err); - --remaining || done(); - }); + await Loki.path('birth_date').doValidate(new Date()); }); - it('date not empty string (gh-3132)', function(done) { + it('date not empty string (gh-3132)', async function() { const HappyBirthday = new Schema({ date: { type: Date, required: true } }); - HappyBirthday.path('date').doValidate('', function(err) { - assert.ok(err instanceof ValidatorError); - done(); - }); + await assert.rejects( + HappyBirthday.path('date').doValidate(''), + ValidatorError + ); }); - it('objectid required', function(done) { - let remaining = 3; + it('objectid required', async function() { const Loki = new Schema({ owner: { type: ObjectId, required: true } }); - Loki.path('owner').doValidate(new DocumentObjectId(), function(err) { - assert.ifError(err); - --remaining || done(); - }); + await assert.rejects( + Loki.path('owner').doValidate(null), + ValidatorError + ); - Loki.path('owner').doValidate(null, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); - - Loki.path('owner').doValidate(undefined, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects( + Loki.path('owner').doValidate(undefined), + ValidatorError + ); }); - it('array required', function(done) { + it('array required', async function() { const Loki = new Schema({ likes: { type: Array, required: true } }); - let remaining = 2; - - Loki.path('likes').doValidate(null, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects( + Loki.path('likes').doValidate(null), + ValidatorError + ); - Loki.path('likes').doValidate(undefined, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects( + Loki.path('likes').doValidate(undefined), + ValidatorError + ); }); - it('array required custom required', function(done) { + it('array required custom required', async function() { const requiredOrig = mongoose.Schema.Types.Array.checkRequired(); mongoose.Schema.Types.Array.checkRequired(v => Array.isArray(v) && v.length); - const doneWrapper = (err) => { - mongoose.Schema.Types.Array.checkRequired(requiredOrig); - done(err); - }; - - const Loki = new Schema({ - likes: { type: Array, required: true } - }); - - let remaining = 2; + try { + const Loki = new Schema({ + likes: { type: Array, required: true } + }); - Loki.path('likes').doValidate([], function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || doneWrapper(); - }); + await assert.rejects( + Loki.path('likes').doValidate([]), + ValidatorError + ); - Loki.path('likes').doValidate(['cake'], function(err) { - assert(!err); - --remaining || doneWrapper(); - }); + await Loki.path('likes').doValidate(['cake']); + } finally { + mongoose.Schema.Types.Array.checkRequired(requiredOrig); + } }); - it('boolean required', function(done) { + it('boolean required', async function() { const Animal = new Schema({ isFerret: { type: Boolean, required: true } }); - let remaining = 4; - - Animal.path('isFerret').doValidate(null, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); - - Animal.path('isFerret').doValidate(undefined, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); - - Animal.path('isFerret').doValidate(true, function(err) { - assert.ifError(err); - --remaining || done(); - }); - - Animal.path('isFerret').doValidate(false, function(err) { - assert.ifError(err); - --remaining || done(); - }); + await assert.rejects(Animal.path('isFerret').doValidate(null), ValidatorError); + await assert.rejects(Animal.path('isFerret').doValidate(undefined), ValidatorError); + await Animal.path('isFerret').doValidate(true); + await Animal.path('isFerret').doValidate(false); }); - it('mixed required', function(done) { + it('mixed required', async function() { const Animal = new Schema({ characteristics: { type: Mixed, required: true } }); - let remaining = 4; + await assert.rejects( + Animal.path('characteristics').doValidate(null), + ValidatorError + ); - Animal.path('characteristics').doValidate(null, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); + await assert.rejects( + Animal.path('characteristics').doValidate(undefined), + ValidatorError + ); - Animal.path('characteristics').doValidate(undefined, function(err) { - assert.ok(err instanceof ValidatorError); - --remaining || done(); - }); - - Animal.path('characteristics').doValidate({ + await Animal.path('characteristics').doValidate({ aggresive: true - }, function(err) { - assert.ifError(err); - --remaining || done(); }); - Animal.path('characteristics').doValidate('none available', function(err) { - assert.ifError(err); - --remaining || done(); - }); + await Animal.path('characteristics').doValidate('none available'); }); }); describe('async', function() { - it('works', function(done) { - let executed = 0; - + it('works', async function() { function validator(value) { return new Promise(function(resolve) { setTimeout(function() { - executed++; resolve(value === true); - if (executed === 2) { - done(); - } }, 5); }); } @@ -526,16 +403,15 @@ describe('schema', function() { ferret: { type: Boolean, validate: validator } }); - Animal.path('ferret').doValidate(true, function(err) { - assert.ifError(err); - }); + await Animal.path('ferret').doValidate(true); - Animal.path('ferret').doValidate(false, function(err) { - assert.ok(err instanceof Error); - }); + await assert.rejects( + Animal.path('ferret').doValidate(false), + ValidatorError + ); }); - it('scope', function(done) { + it('scope', async function() { let called = false; function validator() { @@ -555,11 +431,8 @@ describe('schema', function() { } }); - Animal.path('ferret').doValidate(true, function(err) { - assert.ifError(err); - assert.equal(called, true); - done(); - }, { a: 'b' }); + await Animal.path('ferret').doValidate(true, { a: 'b' }); + assert.equal(called, true); }); it('doValidateSync should ignore async function and script waiting for promises (gh-4885)', function(done) { diff --git a/test/updateValidators.unit.test.js b/test/updateValidators.unit.test.js deleted file mode 100644 index 62a545261e6..00000000000 --- a/test/updateValidators.unit.test.js +++ /dev/null @@ -1,111 +0,0 @@ -'use strict'; - -require('./common'); - -const Schema = require('../lib/schema'); -const assert = require('assert'); -const updateValidators = require('../lib/helpers/updateValidators'); -const emitter = require('events').EventEmitter; - -describe('updateValidators', function() { - let schema; - - beforeEach(function() { - schema = {}; - schema._getSchema = function(p) { - schema._getSchema.calls.push(p); - return schema; - }; - schema._getSchema.calls = []; - schema.doValidate = function(v, cb) { - schema.doValidate.calls.push({ v: v, cb: cb }); - schema.doValidate.emitter.emit('called', { v: v, cb: cb }); - }; - schema.doValidate.calls = []; - schema.doValidate.emitter = new emitter(); - }); - - describe('validators', function() { - it('flattens paths', function(done) { - const fn = updateValidators({}, schema, { test: { a: 1, b: null } }, {}); - schema.doValidate.emitter.on('called', function(args) { - args.cb(); - }); - fn(function(err) { - assert.ifError(err); - assert.equal(schema._getSchema.calls.length, 3); - assert.equal(schema.doValidate.calls.length, 3); - assert.equal(schema._getSchema.calls[0], 'test'); - assert.equal(schema._getSchema.calls[1], 'test.a'); - assert.equal(schema._getSchema.calls[2], 'test.b'); - assert.deepEqual(schema.doValidate.calls[0].v, { - a: 1, - b: null - }); - assert.equal(schema.doValidate.calls[1].v, 1); - assert.equal(schema.doValidate.calls[2].v, null); - done(); - }); - }); - - it('doesnt flatten dates (gh-3194)', function(done) { - const dt = new Date(); - const fn = updateValidators({}, schema, { test: dt }, {}); - schema.doValidate.emitter.on('called', function(args) { - args.cb(); - }); - fn(function(err) { - assert.ifError(err); - assert.equal(schema._getSchema.calls.length, 1); - assert.equal(schema.doValidate.calls.length, 1); - assert.equal(schema._getSchema.calls[0], 'test'); - assert.equal(dt, schema.doValidate.calls[0].v); - done(); - }); - }); - - it('doesnt flatten empty arrays (gh-3554)', function(done) { - const fn = updateValidators({}, schema, { test: [] }, {}); - schema.doValidate.emitter.on('called', function(args) { - args.cb(); - }); - fn(function(err) { - assert.ifError(err); - assert.equal(schema._getSchema.calls.length, 1); - assert.equal(schema.doValidate.calls.length, 1); - assert.equal(schema._getSchema.calls[0], 'test'); - assert.deepEqual(schema.doValidate.calls[0].v, []); - done(); - }); - }); - - it('doesnt flatten decimal128 (gh-7561)', function(done) { - const Decimal128Type = require('../lib/types/decimal128'); - const schema = Schema({ test: { type: 'Decimal128', required: true } }); - const fn = updateValidators({}, schema, { - test: new Decimal128Type('33.426') - }, {}); - fn(function(err) { - assert.ifError(err); - done(); - }); - }); - - it('handles nested paths correctly (gh-3587)', function(done) { - const schema = Schema({ - nested: { - a: { type: String, required: true }, - b: { type: String, required: true } - } - }); - const fn = updateValidators({}, schema, { - nested: { b: 'test' } - }, {}); - fn(function(err) { - assert.ok(err); - assert.deepEqual(Object.keys(err.errors), ['nested.a']); - done(); - }); - }); - }); -}); From 50bca1fc2d8714629f7983a393139a78ade34e60 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 10 Mar 2025 14:06:16 -0400 Subject: [PATCH 015/209] refactor: make validateBeforeSave async for better stack traces --- lib/plugins/validateBeforeSave.js | 16 +++------------- test/schema.validation.test.js | 1 - 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/lib/plugins/validateBeforeSave.js b/lib/plugins/validateBeforeSave.js index c55824184ac..6d1cebdd9d5 100644 --- a/lib/plugins/validateBeforeSave.js +++ b/lib/plugins/validateBeforeSave.js @@ -6,11 +6,10 @@ module.exports = function validateBeforeSave(schema) { const unshift = true; - schema.pre('save', false, function validateBeforeSave(next, options) { - const _this = this; + schema.pre('save', false, async function validateBeforeSave(_next, options) { // Nested docs have their own presave if (this.$isSubdocument) { - return next(); + return; } const hasValidateBeforeSaveOption = options && @@ -32,20 +31,11 @@ module.exports = function validateBeforeSave(schema) { const validateOptions = hasValidateModifiedOnlyOption ? { validateModifiedOnly: options.validateModifiedOnly } : null; - this.$validate(validateOptions).then( + await this.$validate(validateOptions).then( () => { this.$op = 'save'; - next(); - }, - error => { - _this.$__schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { - _this.$op = 'save'; - next(error); - }); } ); - } else { - next(); } }, null, unshift); }; diff --git a/test/schema.validation.test.js b/test/schema.validation.test.js index 243328c2f76..d246630c4fd 100644 --- a/test/schema.validation.test.js +++ b/test/schema.validation.test.js @@ -16,7 +16,6 @@ const ValidatorError = mongoose.Error.ValidatorError; const SchemaTypes = Schema.Types; const ObjectId = SchemaTypes.ObjectId; const Mixed = SchemaTypes.Mixed; -const DocumentObjectId = mongoose.Types.ObjectId; describe('schema', function() { describe('validation', function() { From fae948fc97fac3eaf587fbe5867553f989bfd91b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 11 Mar 2025 18:09:58 -0400 Subject: [PATCH 016/209] BREAKING CHANGE: make validateBeforeSave async and stop relying on Kareem wrappers for save hooks --- lib/document.js | 11 ++- lib/helpers/model/applyHooks.js | 4 +- lib/model.js | 153 +++++++++++++++++--------------- lib/types/subdocument.js | 21 +++-- test/document.test.js | 4 +- test/model.middleware.test.js | 2 +- 6 files changed, 106 insertions(+), 89 deletions(-) diff --git a/lib/document.js b/lib/document.js index d4935c33e0a..afd652db08e 100644 --- a/lib/document.js +++ b/lib/document.js @@ -2880,9 +2880,9 @@ function _pushNestedArrayPaths(val, paths, path) { * ignore */ -Document.prototype._execDocumentPreHooks = function _execDocumentPreHooks(opName) { +Document.prototype._execDocumentPreHooks = function _execDocumentPreHooks(opName, ...args) { return new Promise((resolve, reject) => { - this.constructor._middleware.execPre(opName, this, [], (error) => { + this.constructor._middleware.execPre(opName, this, [...args], (error) => { if (error != null) { reject(error); return; @@ -2912,7 +2912,12 @@ Document.prototype._execDocumentPostHooks = function _execDocumentPostHooks(opNa */ Document.prototype.$__validate = async function $__validate(pathsToValidate, options) { - await this._execDocumentPreHooks('validate'); + try { + await this._execDocumentPreHooks('validate'); + } catch (error) { + await this._execDocumentPostHooks('validate', error); + return; + } if (this.$__.saveOptions && this.$__.saveOptions.pathsToSave && !pathsToValidate) { pathsToValidate = [...this.$__.saveOptions.pathsToSave]; diff --git a/lib/helpers/model/applyHooks.js b/lib/helpers/model/applyHooks.js index 00262792bf6..87edc322ded 100644 --- a/lib/helpers/model/applyHooks.js +++ b/lib/helpers/model/applyHooks.js @@ -15,8 +15,8 @@ module.exports = applyHooks; applyHooks.middlewareFunctions = [ 'deleteOne', - 'save', 'remove', + 'save', 'updateOne', 'init' ]; @@ -119,7 +119,7 @@ function applyHooks(model, schema, options) { model._middleware = middleware; - const internalMethodsToWrap = options && options.isChildSchema ? ['save', 'deleteOne'] : ['save']; + const internalMethodsToWrap = options && options.isChildSchema ? ['deleteOne'] : []; for (const method of internalMethodsToWrap) { const toWrap = `$__${method}`; const wrapped = middleware. diff --git a/lib/model.js b/lib/model.js index 4c2247a4ea1..19f9cfd0f2f 100644 --- a/lib/model.js +++ b/lib/model.js @@ -493,70 +493,84 @@ Model.prototype.$__handleSave = function(options, callback) { * ignore */ -Model.prototype.$__save = function(options, callback) { - this.$__handleSave(options, (error, result) => { - if (error) { - error = this.$__schema._transformDuplicateKeyError(error); - const hooks = this.$__schema.s.hooks; - return hooks.execPost('save:error', this, [this], { error: error }, (error) => { - callback(error, this); - }); - } - let numAffected = 0; - const writeConcern = options != null ? - options.writeConcern != null ? - options.writeConcern.w : - options.w : - 0; - if (writeConcern !== 0) { - // Skip checking if write succeeded if writeConcern is set to - // unacknowledged writes, because otherwise `numAffected` will always be 0 - if (result != null) { - if (Array.isArray(result)) { - numAffected = result.length; - } else if (result.matchedCount != null) { - numAffected = result.matchedCount; - } else { - numAffected = result; - } - } +Model.prototype.$__save = async function $__save(options) { + try { + await this._execDocumentPreHooks('save', options); + } catch (error) { + await this._execDocumentPostHooks('save', error); + return; + } - const versionBump = this.$__.version; - // was this an update that required a version bump? - if (versionBump && !this.$__.inserting) { - const doIncrement = VERSION_INC === (VERSION_INC & this.$__.version); - this.$__.version = undefined; - const key = this.$__schema.options.versionKey; - const version = this.$__getValue(key) || 0; - if (numAffected <= 0) { - // the update failed. pass an error back - this.$__undoReset(); - const err = this.$__.$versionError || - new VersionError(this, version, this.$__.modifiedPaths); - return callback(err); - } - // increment version if was successful - if (doIncrement) { - this.$__setValue(key, version + 1); + let result = null; + try { + result = await new Promise((resolve, reject) => { + this.$__handleSave(options, (error, result) => { + if (error) { + return reject(error); } + resolve(result); + }); + }); + } catch (err) { + const error = this.$__schema._transformDuplicateKeyError(err); + await this._execDocumentPostHooks('save', error); + return; + } + + let numAffected = 0; + const writeConcern = options != null ? + options.writeConcern != null ? + options.writeConcern.w : + options.w : + 0; + if (writeConcern !== 0) { + // Skip checking if write succeeded if writeConcern is set to + // unacknowledged writes, because otherwise `numAffected` will always be 0 + if (result != null) { + if (Array.isArray(result)) { + numAffected = result.length; + } else if (result.matchedCount != null) { + numAffected = result.matchedCount; + } else { + numAffected = result; } - if (result != null && numAffected <= 0) { + } + + const versionBump = this.$__.version; + // was this an update that required a version bump? + if (versionBump && !this.$__.inserting) { + const doIncrement = VERSION_INC === (VERSION_INC & this.$__.version); + this.$__.version = undefined; + const key = this.$__schema.options.versionKey; + const version = this.$__getValue(key) || 0; + if (numAffected <= 0) { + // the update failed. pass an error back this.$__undoReset(); - error = new DocumentNotFoundError(result.$where, - this.constructor.modelName, numAffected, result); - const hooks = this.$__schema.s.hooks; - return hooks.execPost('save:error', this, [this], { error: error }, (error) => { - callback(error, this); - }); + const err = this.$__.$versionError || + new VersionError(this, version, this.$__.modifiedPaths); + await this._execDocumentPostHooks('save', err); + return; + } + + // increment version if was successful + if (doIncrement) { + this.$__setValue(key, version + 1); } } - this.$__.saving = undefined; - this.$__.savedState = {}; - this.$emit('save', this, numAffected); - this.constructor.emit('save', this, numAffected); - callback(null, this); - }); + if (result != null && numAffected <= 0) { + this.$__undoReset(); + const error = new DocumentNotFoundError(result.$where, + this.constructor.modelName, numAffected, result); + await this._execDocumentPostHooks('save', error); + return; + } + } + this.$__.saving = undefined; + this.$__.savedState = {}; + this.$emit('save', this, numAffected); + this.constructor.emit('save', this, numAffected); + await this._execDocumentPostHooks('save'); }; /*! @@ -636,20 +650,17 @@ Model.prototype.save = async function save(options) { this.$__.saveOptions = options; - await new Promise((resolve, reject) => { - this.$__save(options, error => { - this.$__.saving = null; - this.$__.saveOptions = null; - this.$__.$versionError = null; - this.$op = null; - if (error != null) { - this.$__handleReject(error); - return reject(error); - } - - resolve(); - }); - }); + try { + await this.$__save(options); + } catch (error) { + this.$__handleReject(error); + throw error; + } finally { + this.$__.saving = null; + this.$__.saveOptions = null; + this.$__.$versionError = null; + this.$op = null; + } return this; }; diff --git a/lib/types/subdocument.js b/lib/types/subdocument.js index b1984d08ebf..550471a59db 100644 --- a/lib/types/subdocument.js +++ b/lib/types/subdocument.js @@ -1,7 +1,6 @@ 'use strict'; const Document = require('../document'); -const immediate = require('../helpers/immediate'); const internalToObjectOptions = require('../options').internalToObjectOptions; const util = require('util'); const utils = require('../utils'); @@ -80,14 +79,7 @@ Subdocument.prototype.save = async function save(options) { 'if you\'re sure this behavior is right for your app.'); } - return new Promise((resolve, reject) => { - this.$__save((err) => { - if (err != null) { - return reject(err); - } - resolve(this); - }); - }); + return await this.$__save(); }; /** @@ -141,8 +133,15 @@ Subdocument.prototype.$__pathRelativeToParent = function(p) { * @api private */ -Subdocument.prototype.$__save = function(fn) { - return immediate(() => fn(null, this)); +Subdocument.prototype.$__save = async function $__save() { + try { + await this._execDocumentPreHooks('save'); + } catch (error) { + await this._execDocumentPostHooks('save', error); + return; + } + + await this._execDocumentPostHooks('save'); }; /*! diff --git a/test/document.test.js b/test/document.test.js index c7463e01229..06ac93e0459 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -9750,7 +9750,7 @@ describe('document', function() { const schema = Schema({ name: String }); let called = 0; - schema.pre(/.*/, { document: true, query: false }, function() { + schema.pre(/.*/, { document: true, query: false }, function testPreSave9190() { ++called; }); const Model = db.model('Test', schema); @@ -9765,7 +9765,9 @@ describe('document', function() { await Model.countDocuments(); assert.equal(called, 0); + console.log('-----'); const docs = await Model.create([{ name: 'test' }], { validateBeforeSave: false }); + console.log('------'); assert.equal(called, 1); await docs[0].validate(); diff --git a/test/model.middleware.test.js b/test/model.middleware.test.js index 92ca5224dee..4aca8c5516c 100644 --- a/test/model.middleware.test.js +++ b/test/model.middleware.test.js @@ -293,7 +293,7 @@ describe('model middleware', function() { title: String }); - schema.post('save', function() { + schema.post('save', function postSaveTestError() { throw new Error('woops!'); }); From 0f6b4aadb91984db9cf0d5f73d6c11746eaa4a83 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 12 Mar 2025 10:48:09 -0400 Subject: [PATCH 017/209] address code review comments --- docs/migrating_to_9.md | 25 +++++++++++++++++++++++++ lib/helpers/model/applyHooks.js | 1 + lib/schema.js | 2 +- test/document.test.js | 2 -- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 9dd3dbbfa88..771839b60c2 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -11,3 +11,28 @@ There are several backwards-breaking changes you should be aware of when migrati If you're still on Mongoose 7.x or earlier, please read the [Mongoose 7.x to 8.x migration guide](migrating_to_8.html) and upgrade to Mongoose 8.x first before upgrading to Mongoose 9. ## `Schema.prototype.doValidate()` now returns a promise + +`Schema.prototype.doValidate()` now returns a promise that rejects with a validation error if one occurred. +In Mongoose 8.x, `doValidate()` took a callback and did not return a promise. + +```javascript +// Mongoose 8.x function signature +function doValidate(value, cb, scope, options) {} + +// Mongoose 8.x example usage +schema.doValidate(value, function(error) { + if (error) { + // Handle validation error + } +}, scope, options); + +// Mongoose 9.x function signature +async function doValidate(value, scope, options) {} + +// Mongoose 9.x example usage +try { + await schema.doValidate(value, scope, options); +} catch (error) { + // Handle validation error +} +``` diff --git a/lib/helpers/model/applyHooks.js b/lib/helpers/model/applyHooks.js index 87edc322ded..25ec30eb9e6 100644 --- a/lib/helpers/model/applyHooks.js +++ b/lib/helpers/model/applyHooks.js @@ -18,6 +18,7 @@ applyHooks.middlewareFunctions = [ 'remove', 'save', 'updateOne', + 'validate', 'init' ]; diff --git a/lib/schema.js b/lib/schema.js index f7528f6b4b4..a9e795120d9 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -31,7 +31,7 @@ const hasNumericSubpathRegex = /\.\d+(\.|$)/; let MongooseTypes; const queryHooks = require('./constants').queryMiddlewareFunctions; -const documentHooks = require('./helpers/model/applyHooks').middlewareFunctions; +const documentHooks = require('./constants').documentMiddlewareFunctions; const hookNames = queryHooks.concat(documentHooks). reduce((s, hook) => s.add(hook), new Set()); diff --git a/test/document.test.js b/test/document.test.js index 06ac93e0459..0d958f409f8 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -9765,9 +9765,7 @@ describe('document', function() { await Model.countDocuments(); assert.equal(called, 0); - console.log('-----'); const docs = await Model.create([{ name: 'test' }], { validateBeforeSave: false }); - console.log('------'); assert.equal(called, 1); await docs[0].validate(); From 59e9381def353633dd7df5e7e9050ac5c96e92e8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 12 Mar 2025 14:54:30 -0400 Subject: [PATCH 018/209] remove vestigial :error post hook calls --- lib/plugins/saveSubdocs.js | 18 ++---------------- test/model.test.js | 6 ++++++ 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/lib/plugins/saveSubdocs.js b/lib/plugins/saveSubdocs.js index bb88db59f85..6a163762ed2 100644 --- a/lib/plugins/saveSubdocs.js +++ b/lib/plugins/saveSubdocs.js @@ -32,9 +32,7 @@ module.exports = function saveSubdocs(schema) { _this.$__.saveOptions.__subdocs = null; } if (error) { - return _this.$__schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { - next(error); - }); + return next(error); } next(); }); @@ -67,7 +65,6 @@ module.exports = function saveSubdocs(schema) { return; } - const _this = this; const subdocs = this.$getAllSubdocs({ useCache: true }); if (!subdocs.length) { @@ -86,17 +83,6 @@ module.exports = function saveSubdocs(schema) { })); } - try { - await Promise.all(promises); - } catch (error) { - await new Promise((resolve, reject) => { - this.$__schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { - if (error) { - return reject(error); - } - resolve(); - }); - }); - } + await Promise.all(promises); }, null, unshift); }; diff --git a/test/model.test.js b/test/model.test.js index d4aa35ec686..7546313e1a7 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -2154,17 +2154,23 @@ describe('Model', function() { next(); }); + let schemaPostSaveCalls = 0; const schema = new Schema({ name: String, child: [childSchema] }); schema.pre('save', function(next) { this.name = 'parent'; next(); }); + schema.post('save', function testSchemaPostSave(err, res, next) { + ++schemaPostSaveCalls; + next(err); + }); const S = db.model('Test', schema); const s = new S({ name: 'a', child: [{ name: 'b', grand: [{ name: 'c' }] }] }); const err = await s.save().then(() => null, err => err); assert.equal(err.message, 'Error 101'); + assert.equal(schemaPostSaveCalls, 1); }); describe('init', function() { From 92d6ca5055eef2e91d75c755ea615a459e9c027b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 12 Mar 2025 15:12:33 -0400 Subject: [PATCH 019/209] refactor: make saveSubdocsPreSave async --- lib/plugins/saveSubdocs.js | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/lib/plugins/saveSubdocs.js b/lib/plugins/saveSubdocs.js index 6a163762ed2..22d8e2a8046 100644 --- a/lib/plugins/saveSubdocs.js +++ b/lib/plugins/saveSubdocs.js @@ -1,16 +1,13 @@ 'use strict'; -const each = require('../helpers/each'); - /*! * ignore */ module.exports = function saveSubdocs(schema) { const unshift = true; - schema.s.hooks.pre('save', false, function saveSubdocsPreSave(next) { + schema.s.hooks.pre('save', false, async function saveSubdocsPreSave() { if (this.$isSubdocument) { - next(); return; } @@ -18,24 +15,22 @@ module.exports = function saveSubdocs(schema) { const subdocs = this.$getAllSubdocs({ useCache: true }); if (!subdocs.length) { - next(); return; } - each(subdocs, function(subdoc, cb) { - subdoc.$__schema.s.hooks.execPre('save', subdoc, function(err) { - cb(err); + await Promise.all(subdocs.map(async (subdoc) => { + return new Promise((resolve, reject) => { + subdoc.$__schema.s.hooks.execPre('save', subdoc, function(err) { + if (err) reject(err); + else resolve(); + }); }); - }, function(error) { - // Invalidate subdocs cache because subdoc pre hooks can add new subdocuments - if (_this.$__.saveOptions) { - _this.$__.saveOptions.__subdocs = null; - } - if (error) { - return next(error); - } - next(); - }); + })); + + // Invalidate subdocs cache because subdoc pre hooks can add new subdocuments + if (_this.$__.saveOptions) { + _this.$__.saveOptions.__subdocs = null; + } }, null, unshift); schema.s.hooks.post('save', async function saveSubdocsPostDeleteOne() { From 80a2ed33fadc03bad28bcbe16be16dd5f271f6d7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 12 Mar 2025 15:14:20 -0400 Subject: [PATCH 020/209] style: fix lint --- lib/plugins/saveSubdocs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/plugins/saveSubdocs.js b/lib/plugins/saveSubdocs.js index 22d8e2a8046..d4c899cba5e 100644 --- a/lib/plugins/saveSubdocs.js +++ b/lib/plugins/saveSubdocs.js @@ -18,7 +18,7 @@ module.exports = function saveSubdocs(schema) { return; } - await Promise.all(subdocs.map(async (subdoc) => { + await Promise.all(subdocs.map(async(subdoc) => { return new Promise((resolve, reject) => { subdoc.$__schema.s.hooks.execPre('save', subdoc, function(err) { if (err) reject(err); From 45fd784e32868e2d908673301e5c678c3b0a8b68 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 17 Mar 2025 10:35:56 -0400 Subject: [PATCH 021/209] make saveSubdocs fully async --- lib/document.js | 1 + lib/helpers/model/applyHooks.js | 52 +++++++++++++++++++++------------ lib/plugins/saveSubdocs.js | 32 ++++---------------- 3 files changed, 40 insertions(+), 45 deletions(-) diff --git a/lib/document.js b/lib/document.js index afd652db08e..fdcceefc1d0 100644 --- a/lib/document.js +++ b/lib/document.js @@ -2882,6 +2882,7 @@ function _pushNestedArrayPaths(val, paths, path) { Document.prototype._execDocumentPreHooks = function _execDocumentPreHooks(opName, ...args) { return new Promise((resolve, reject) => { + // console.log('ABB', this.constructor._middleware, this.constructor.schema?.obj, this.$__fullPath()); this.constructor._middleware.execPre(opName, this, [...args], (error) => { if (error != null) { reject(error); diff --git a/lib/helpers/model/applyHooks.js b/lib/helpers/model/applyHooks.js index 25ec30eb9e6..06750e8d643 100644 --- a/lib/helpers/model/applyHooks.js +++ b/lib/helpers/model/applyHooks.js @@ -51,25 +51,11 @@ function applyHooks(model, schema, options) { for (const key of Object.keys(schema.paths)) { let type = schema.paths[key]; let childModel = null; - if (type.$isSingleNested) { - childModel = type.caster; - } else if (type.$isMongooseDocumentArray) { - childModel = type.Constructor; - } else if (type.instance === 'Array') { - let curType = type; - // Drill into nested arrays to check if nested array contains document array - while (curType.instance === 'Array') { - if (curType.$isMongooseDocumentArray) { - childModel = curType.Constructor; - type = curType; - break; - } - curType = curType.getEmbeddedSchemaType(); - } - if (childModel == null) { - continue; - } + const result = findChildModel(type); + if (result) { + childModel = result.childModel; + type = result.type; } else { continue; } @@ -164,3 +150,33 @@ function applyHooks(model, schema, options) { createWrapper(method, originalMethod, null, customMethodOptions); } } + +/** + * Check if there is an embedded schematype in the given schematype. Handles drilling down into primitive + * arrays and maps in case of array of array of subdocs or map of subdocs. + * + * @param {SchemaType} curType + * @returns { childModel: Model | typeof Subdocument, curType: SchemaType } | null + */ + +function findChildModel(curType) { + if (curType.$isSingleNested) { + return { childModel: curType.caster, type: curType }; + } + if (curType.$isMongooseDocumentArray) { + return { childModel: curType.Constructor, type: curType }; + } + if (curType.instance === 'Array') { + const embedded = curType.getEmbeddedSchemaType(); + if (embedded) { + return findChildModel(embedded); + } + } + if (curType.instance === 'Map') { + const mapType = curType.getEmbeddedSchemaType(); + if (mapType) { + return findChildModel(mapType); + } + } + return null; +} diff --git a/lib/plugins/saveSubdocs.js b/lib/plugins/saveSubdocs.js index d4c899cba5e..744f8765fff 100644 --- a/lib/plugins/saveSubdocs.js +++ b/lib/plugins/saveSubdocs.js @@ -11,25 +11,17 @@ module.exports = function saveSubdocs(schema) { return; } - const _this = this; const subdocs = this.$getAllSubdocs({ useCache: true }); if (!subdocs.length) { return; } - await Promise.all(subdocs.map(async(subdoc) => { - return new Promise((resolve, reject) => { - subdoc.$__schema.s.hooks.execPre('save', subdoc, function(err) { - if (err) reject(err); - else resolve(); - }); - }); - })); + await Promise.all(subdocs.map(subdoc => subdoc._execDocumentPreHooks('save'))); // Invalidate subdocs cache because subdoc pre hooks can add new subdocuments - if (_this.$__.saveOptions) { - _this.$__.saveOptions.__subdocs = null; + if (this.$__.saveOptions) { + this.$__.saveOptions.__subdocs = null; } }, null, unshift); @@ -41,14 +33,7 @@ module.exports = function saveSubdocs(schema) { const promises = []; for (const subdoc of removedSubdocs) { - promises.push(new Promise((resolve, reject) => { - subdoc.$__schema.s.hooks.execPost('deleteOne', subdoc, [subdoc], function(err) { - if (err) { - return reject(err); - } - resolve(); - }); - })); + promises.push(subdoc._execDocumentPostHooks('deleteOne')); } this.$__.removedSubdocs = null; @@ -68,14 +53,7 @@ module.exports = function saveSubdocs(schema) { const promises = []; for (const subdoc of subdocs) { - promises.push(new Promise((resolve, reject) => { - subdoc.$__schema.s.hooks.execPost('save', subdoc, [subdoc], function(err) { - if (err) { - return reject(err); - } - resolve(); - }); - })); + promises.push(subdoc._execDocumentPostHooks('save')); } await Promise.all(promises); From f4840bbbf7e81107e5a34cdfe964b7eab22bb12e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 20 Mar 2025 18:05:47 -0400 Subject: [PATCH 022/209] BREAKING CHANGE: use kareem@v3 promise-based execPre() for document hooks Re: #15317 --- docs/migrating_to_9.md | 14 +++++++ lib/aggregate.js | 42 ++++++++++--------- lib/browserDocument.js | 10 +---- lib/cursor/aggregationCursor.js | 38 +++++++----------- lib/cursor/queryCursor.js | 7 +++- lib/document.js | 17 ++------ lib/helpers/model/applyStaticHooks.js | 6 ++- lib/model.js | 58 +++++++++------------------ lib/plugins/sharding.js | 6 +-- lib/query.js | 18 +-------- package.json | 2 +- test/document.test.js | 14 +++++++ test/model.middleware.test.js | 8 ++-- test/query.cursor.test.js | 10 ++++- 14 files changed, 116 insertions(+), 134 deletions(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 771839b60c2..0abd5cf90ef 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -36,3 +36,17 @@ try { // Handle validation error } ``` + +## Errors in middleware functions take priority over `next()` calls + +In Mongoose 8.x, if a middleware function threw an error after calling `next()`, that error would be ignored. + +```javascript +schema.pre('save', function(next) { + next(); + // In Mongoose 8, this error will not get reported, because you already called next() + throw new Error('woops!'); +}); +``` + +In Mongoose 9, errors in the middleware function take priority, so the above `save()` would throw an error. diff --git a/lib/aggregate.js b/lib/aggregate.js index e475736da2e..aaaef070b28 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -800,18 +800,19 @@ Aggregate.prototype.explain = async function explain(verbosity) { prepareDiscriminatorPipeline(this._pipeline, this._model.schema); - await new Promise((resolve, reject) => { - model.hooks.execPre('aggregate', this, error => { - if (error) { - const _opts = { error: error }; - return model.hooks.execPost('aggregate', this, [null], _opts, error => { - reject(error); - }); - } else { + try { + await model.hooks.execPre('aggregate', this); + } catch (error) { + const _opts = { error: error }; + return await new Promise((resolve, reject) => { + model.hooks.execPost('aggregate', this, [null], _opts, error => { + if (error) { + return reject(error); + } resolve(); - } + }); }); - }); + } const cursor = model.collection.aggregate(this._pipeline, this.options); @@ -1079,18 +1080,19 @@ Aggregate.prototype.exec = async function exec() { prepareDiscriminatorPipeline(this._pipeline, this._model.schema); stringifyFunctionOperators(this._pipeline); - await new Promise((resolve, reject) => { - model.hooks.execPre('aggregate', this, error => { - if (error) { - const _opts = { error: error }; - return model.hooks.execPost('aggregate', this, [null], _opts, error => { - reject(error); - }); - } else { + try { + await model.hooks.execPre('aggregate', this); + } catch (error) { + const _opts = { error: error }; + return await new Promise((resolve, reject) => { + model.hooks.execPost('aggregate', this, [null], _opts, error => { + if (error) { + return reject(error); + } resolve(); - } + }); }); - }); + } if (!this._pipeline.length) { throw new MongooseError('Aggregate has empty pipeline'); diff --git a/lib/browserDocument.js b/lib/browserDocument.js index fcdf78a5cb9..48f8596abf6 100644 --- a/lib/browserDocument.js +++ b/lib/browserDocument.js @@ -98,15 +98,7 @@ Document.$emitter = new EventEmitter(); */ Document.prototype._execDocumentPreHooks = function _execDocumentPreHooks(opName) { - return new Promise((resolve, reject) => { - this._middleware.execPre(opName, this, [], (error) => { - if (error != null) { - reject(error); - return; - } - resolve(); - }); - }); + return this._middleware.execPre(opName, this, []); }; /*! diff --git a/lib/cursor/aggregationCursor.js b/lib/cursor/aggregationCursor.js index 01cf961d5dd..a528076fe62 100644 --- a/lib/cursor/aggregationCursor.js +++ b/lib/cursor/aggregationCursor.js @@ -63,34 +63,24 @@ util.inherits(AggregationCursor, Readable); function _init(model, c, agg) { if (!model.collection.buffer) { - model.hooks.execPre('aggregate', agg, function(err) { - if (err != null) { - _handlePreHookError(c, err); - return; - } - if (typeof agg.options?.cursor?.transform === 'function') { - c._transforms.push(agg.options.cursor.transform); - } - - c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {}); - c.emit('cursor', c.cursor); - }); + model.hooks.execPre('aggregate', agg).then(() => onPreComplete(null), err => onPreComplete(err)); } else { model.collection.emitter.once('queue', function() { - model.hooks.execPre('aggregate', agg, function(err) { - if (err != null) { - _handlePreHookError(c, err); - return; - } + model.hooks.execPre('aggregate', agg).then(() => onPreComplete(null), err => onPreComplete(err)); + }); + } - if (typeof agg.options?.cursor?.transform === 'function') { - c._transforms.push(agg.options.cursor.transform); - } + function onPreComplete(err) { + if (err != null) { + _handlePreHookError(c, err); + return; + } + if (typeof agg.options?.cursor?.transform === 'function') { + c._transforms.push(agg.options.cursor.transform); + } - c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {}); - c.emit('cursor', c.cursor); - }); - }); + c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {}); + c.emit('cursor', c.cursor); } } diff --git a/lib/cursor/queryCursor.js b/lib/cursor/queryCursor.js index f25a06f2fd1..18c5ba5836a 100644 --- a/lib/cursor/queryCursor.js +++ b/lib/cursor/queryCursor.js @@ -49,7 +49,8 @@ function QueryCursor(query) { this._transforms = []; this.model = model; this.options = {}; - model.hooks.execPre('find', query, (err) => { + + const onPreComplete = (err) => { if (err != null) { if (err instanceof kareem.skipWrappedFunction) { const resultValue = err.args[0]; @@ -91,7 +92,9 @@ function QueryCursor(query) { } else { _getRawCursor(query, this); } - }); + }; + + model.hooks.execPre('find', query).then(() => onPreComplete(null), err => onPreComplete(err)); } util.inherits(QueryCursor, Readable); diff --git a/lib/document.js b/lib/document.js index fdcceefc1d0..3db1dc4f4df 100644 --- a/lib/document.js +++ b/lib/document.js @@ -847,8 +847,8 @@ function init(self, obj, doc, opts, prefix) { Document.prototype.updateOne = function updateOne(doc, options, callback) { const query = this.constructor.updateOne({ _id: this._doc._id }, doc, options); const self = this; - query.pre(function queryPreUpdateOne(cb) { - self.constructor._middleware.execPre('updateOne', self, [self], cb); + query.pre(function queryPreUpdateOne() { + return self.constructor._middleware.execPre('updateOne', self, [self]); }); query.post(function queryPostUpdateOne(cb) { self.constructor._middleware.execPost('updateOne', self, [self], {}, cb); @@ -2880,17 +2880,8 @@ function _pushNestedArrayPaths(val, paths, path) { * ignore */ -Document.prototype._execDocumentPreHooks = function _execDocumentPreHooks(opName, ...args) { - return new Promise((resolve, reject) => { - // console.log('ABB', this.constructor._middleware, this.constructor.schema?.obj, this.$__fullPath()); - this.constructor._middleware.execPre(opName, this, [...args], (error) => { - if (error != null) { - reject(error); - return; - } - resolve(); - }); - }); +Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks(opName, ...args) { + return this.constructor._middleware.execPre(opName, this, [...args]); }; /*! diff --git a/lib/helpers/model/applyStaticHooks.js b/lib/helpers/model/applyStaticHooks.js index 40116462f26..cb9cc6c9f5e 100644 --- a/lib/helpers/model/applyStaticHooks.js +++ b/lib/helpers/model/applyStaticHooks.js @@ -45,7 +45,9 @@ module.exports = function applyStaticHooks(model, hooks, statics) { // Special case: can't use `Kareem#wrap()` because it doesn't currently // support wrapped functions that return a promise. return promiseOrCallback(cb, callback => { - hooks.execPre(key, model, args, function(err) { + hooks.execPre(key, model, args).then(() => onPreComplete(null), err => onPreComplete(err)); + + function onPreComplete(err) { if (err != null) { return callback(err); } @@ -72,7 +74,7 @@ module.exports = function applyStaticHooks(model, hooks, statics) { callback(null, res); }); } - }); + } }, model.events); }; } diff --git a/lib/model.js b/lib/model.js index 19f9cfd0f2f..e8c2cd2a2f8 100644 --- a/lib/model.js +++ b/lib/model.js @@ -810,19 +810,16 @@ Model.prototype.deleteOne = function deleteOne(options) { } } - query.pre(function queryPreDeleteOne(cb) { - self.constructor._middleware.execPre('deleteOne', self, [self], cb); + query.pre(function queryPreDeleteOne() { + return self.constructor._middleware.execPre('deleteOne', self, [self]); }); - query.pre(function callSubdocPreHooks(cb) { - each(self.$getAllSubdocs(), (subdoc, cb) => { - subdoc.constructor._middleware.execPre('deleteOne', subdoc, [subdoc], cb); - }, cb); + query.pre(function callSubdocPreHooks() { + return Promise.all(self.$getAllSubdocs().map(subdoc => subdoc.constructor._middleware.execPre('deleteOne', subdoc, [subdoc]))); }); - query.pre(function skipIfAlreadyDeleted(cb) { + query.pre(function skipIfAlreadyDeleted() { if (self.$__.isDeleted) { - return cb(Kareem.skipWrappedFunction()); + throw new Kareem.skipWrappedFunction(); } - return cb(); }); query.post(function callSubdocPostHooks(cb) { each(self.$getAllSubdocs(), (subdoc, cb) => { @@ -1185,16 +1182,11 @@ Model.createCollection = async function createCollection(options) { throw new MongooseError('Model.createCollection() no longer accepts a callback'); } - const shouldSkip = await new Promise((resolve, reject) => { - this.hooks.execPre('createCollection', this, [options], (err) => { - if (err != null) { - if (err instanceof Kareem.skipWrappedFunction) { - return resolve(true); - } - return reject(err); - } - resolve(); - }); + const shouldSkip = await this.hooks.execPre('createCollection', this, [options]).catch(err => { + if (err instanceof Kareem.skipWrappedFunction) { + return true; + } + throw err; }); const collectionOptions = this && @@ -3380,17 +3372,15 @@ Model.bulkWrite = async function bulkWrite(ops, options) { } options = options || {}; - const shouldSkip = await new Promise((resolve, reject) => { - this.hooks.execPre('bulkWrite', this, [ops, options], (err) => { - if (err != null) { - if (err instanceof Kareem.skipWrappedFunction) { - return resolve(err); - } - return reject(err); + const shouldSkip = await this.hooks.execPre('bulkWrite', this, [ops, options]).then( + () => null, + err => { + if (err instanceof Kareem.skipWrappedFunction) { + return err; } - resolve(); - }); - }); + throw err; + } + ); if (shouldSkip) { return shouldSkip.args[0]; @@ -3622,15 +3612,7 @@ Model.bulkSave = async function bulkSave(documents, options) { }; function buildPreSavePromise(document, options) { - return new Promise((resolve, reject) => { - document.schema.s.hooks.execPre('save', document, [options], (err) => { - if (err) { - reject(err); - return; - } - resolve(); - }); - }); + return document.schema.s.hooks.execPre('save', document, [options]); } function handleSuccessfulWrite(document) { diff --git a/lib/plugins/sharding.js b/lib/plugins/sharding.js index 7d905f31c0f..76d95f9acfb 100644 --- a/lib/plugins/sharding.js +++ b/lib/plugins/sharding.js @@ -12,13 +12,11 @@ module.exports = function shardingPlugin(schema) { storeShard.call(this); return this; }); - schema.pre('save', function shardingPluginPreSave(next) { + schema.pre('save', function shardingPluginPreSave() { applyWhere.call(this); - next(); }); - schema.pre('remove', function shardingPluginPreRemove(next) { + schema.pre('remove', function shardingPluginPreRemove() { applyWhere.call(this); - next(); }); schema.post('save', function shardingPluginPostSave() { storeShard.call(this); diff --git a/lib/query.js b/lib/query.js index ea177e5dd5d..442e8cd8d4f 100644 --- a/lib/query.js +++ b/lib/query.js @@ -4511,14 +4511,7 @@ function _executePostHooks(query, res, error, op) { */ function _executePreExecHooks(query) { - return new Promise((resolve, reject) => { - query._hooks.execPre('exec', query, [], (error) => { - if (error != null) { - return reject(error); - } - resolve(); - }); - }); + return query._hooks.execPre('exec', query, []); } /*! @@ -4530,14 +4523,7 @@ function _executePreHooks(query, op) { return; } - return new Promise((resolve, reject) => { - query._queryMiddleware.execPre(op || query.op, query, [], (error) => { - if (error != null) { - return reject(error); - } - resolve(); - }); - }); + return query._queryMiddleware.execPre(op || query.op, query, []); } /** diff --git a/package.json b/package.json index 2fbe343e75c..6e8497dd384 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "license": "MIT", "dependencies": { "bson": "^6.10.3", - "kareem": "2.6.3", + "kareem": "git@github.com:mongoosejs/kareem.git#vkarpov15/v3", "mongodb": "~6.14.0", "mpath": "0.9.0", "mquery": "5.0.0", diff --git a/test/document.test.js b/test/document.test.js index 0d958f409f8..743aa82e1ba 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -14423,6 +14423,20 @@ describe('document', function() { sinon.restore(); } }); + + describe('async stack traces (gh-15317)', function() { + it('works with save() validation errors', async function() { + const userSchema = new mongoose.Schema({ + name: { type: String, required: true, validate: v => v.length > 3 }, + age: Number + }); + const User = db.model('User', userSchema); + const doc = new User({ name: 'A' }); + const err = await doc.save().then(() => null, err => err); + assert.ok(err instanceof Error); + assert.ok(err.stack.includes('document.test.js'), err.stack); + }); + }); }); describe('Check if instance function that is supplied in schema option is available', function() { diff --git a/test/model.middleware.test.js b/test/model.middleware.test.js index 4aca8c5516c..75bcd84a645 100644 --- a/test/model.middleware.test.js +++ b/test/model.middleware.test.js @@ -320,7 +320,7 @@ describe('model middleware', function() { schema.pre('save', function(next) { next(); - // This error will not get reported, because you already called next() + // Error takes precedence over next() throw new Error('woops!'); }); @@ -333,8 +333,10 @@ describe('model middleware', function() { const test = new TestMiddleware({ title: 'Test' }); - await test.save(); - assert.equal(called, 1); + const err = await test.save().then(() => null, err => err); + assert.ok(err); + assert.equal(err.message, 'woops!'); + assert.equal(called, 0); }); it('validate + remove', async function() { diff --git a/test/query.cursor.test.js b/test/query.cursor.test.js index e7265be1d06..3a736dd0f57 100644 --- a/test/query.cursor.test.js +++ b/test/query.cursor.test.js @@ -905,6 +905,10 @@ describe('QueryCursor', function() { it('returns the underlying Node driver cursor with getDriverCursor()', async function() { const schema = new mongoose.Schema({ name: String }); + // Add some middleware to ensure the cursor hasn't been created yet when `cursor()` is called. + schema.pre('find', async function() { + await new Promise(resolve => setTimeout(resolve, 10)); + }); const Movie = db.model('Movie', schema); @@ -927,7 +931,7 @@ describe('QueryCursor', function() { const TestModel = db.model('Test', mongoose.Schema({ name: String })); const stream = await TestModel.find().cursor(); - await once(stream, 'cursor'); + assert.ok(stream.cursor); assert.ok(!stream.cursor.closed); stream.destroy(); @@ -939,7 +943,9 @@ describe('QueryCursor', function() { it('handles destroy() before cursor is created (gh-14966)', async function() { db.deleteModel(/Test/); - const TestModel = db.model('Test', mongoose.Schema({ name: String })); + const schema = mongoose.Schema({ name: String }); + schema.pre('find', () => new Promise(resolve => setTimeout(resolve, 10))); + const TestModel = db.model('Test', schema); const stream = await TestModel.find().cursor(); assert.ok(!stream.cursor); From 1ab5638ac0d1984f21a95df57a7108d29d4607b5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 2 Apr 2025 06:15:43 -0400 Subject: [PATCH 023/209] add note about dropping support for passing args to next middleware --- docs/migrating_to_9.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 0abd5cf90ef..537ba040438 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -50,3 +50,20 @@ schema.pre('save', function(next) { ``` In Mongoose 9, errors in the middleware function take priority, so the above `save()` would throw an error. + +## `next()` no longer supports passing arguments to the next middleware + +Previously, you could call `next(null, 'new arg')` in a hook and the args to the next middleware would get overwritten by 'new arg'. + +```javascript +schema.pre('save', function(next, options) { + options; // options passed to `save()` + next(null, 'new arg'); +}); + +schema.pre('save', function(next, arg) { + arg; // In Mongoose 8, this would be 'new arg', overwrote the options passed to `save()` +}); +``` + +In Mongoose 9, `next(null, 'new arg')` doesn't overwrite the args to the next middleware. From 37896da6814c5d45bb4693e7dd4b7c5ac14ab443 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 2 Apr 2025 06:16:35 -0400 Subject: [PATCH 024/209] Update lib/browserDocument.js Co-authored-by: hasezoey --- lib/browserDocument.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/browserDocument.js b/lib/browserDocument.js index 48f8596abf6..7b7e6c1ccb6 100644 --- a/lib/browserDocument.js +++ b/lib/browserDocument.js @@ -97,7 +97,7 @@ Document.$emitter = new EventEmitter(); * ignore */ -Document.prototype._execDocumentPreHooks = function _execDocumentPreHooks(opName) { +Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks(opName) { return this._middleware.execPre(opName, this, []); }; From bf60a68d56edc12b4fe83f0a162f94dbf4487fcb Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 2 Apr 2025 06:16:42 -0400 Subject: [PATCH 025/209] Update lib/helpers/model/applyHooks.js Co-authored-by: hasezoey --- lib/helpers/model/applyHooks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers/model/applyHooks.js b/lib/helpers/model/applyHooks.js index 06750e8d643..a1ee62f31ef 100644 --- a/lib/helpers/model/applyHooks.js +++ b/lib/helpers/model/applyHooks.js @@ -156,7 +156,7 @@ function applyHooks(model, schema, options) { * arrays and maps in case of array of array of subdocs or map of subdocs. * * @param {SchemaType} curType - * @returns { childModel: Model | typeof Subdocument, curType: SchemaType } | null + * @returns {{ childModel: Model | typeof Subdocument, curType: SchemaType } | null} */ function findChildModel(curType) { From 32ab2d56ef6eb0c38c0cd67635f443232e2ec52e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 2 Apr 2025 06:21:39 -0400 Subject: [PATCH 026/209] Update lib/model.js Co-authored-by: hasezoey --- lib/model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model.js b/lib/model.js index 23d340526e3..a54a19121b8 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3611,7 +3611,7 @@ Model.bulkSave = async function bulkSave(documents, options) { return bulkWriteResult; }; -function buildPreSavePromise(document, options) { +async function buildPreSavePromise(document, options) { return document.schema.s.hooks.execPre('save', document, [options]); } From 333b72f71d5638e51cdcc8925524911300071c8c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 2 Apr 2025 06:21:48 -0400 Subject: [PATCH 027/209] Update lib/model.js Co-authored-by: hasezoey --- lib/model.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/model.js b/lib/model.js index a54a19121b8..aa9a2387781 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3372,9 +3372,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) { } options = options || {}; - const shouldSkip = await this.hooks.execPre('bulkWrite', this, [ops, options]).then( - () => null, - err => { + const shouldSkip = await this.hooks.execPre('bulkWrite', this, [ops, options]).catch(err => { if (err instanceof Kareem.skipWrappedFunction) { return err; } From 32d5ab7f0b782de7e2ab8b0ae0ddcb687272c762 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 2 Apr 2025 06:21:57 -0400 Subject: [PATCH 028/209] Update lib/query.js Co-authored-by: hasezoey --- lib/query.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/query.js b/lib/query.js index 442e8cd8d4f..b4b81fb9e2b 100644 --- a/lib/query.js +++ b/lib/query.js @@ -4510,7 +4510,7 @@ function _executePostHooks(query, res, error, op) { * ignore */ -function _executePreExecHooks(query) { +async function _executePreExecHooks(query) { return query._hooks.execPre('exec', query, []); } From f98aab464642822ba4f8fc23c73cd1403835eb2f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 2 Apr 2025 13:05:01 -0400 Subject: [PATCH 029/209] refactor: use assert.rejects() --- test/model.middleware.test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/model.middleware.test.js b/test/model.middleware.test.js index 75bcd84a645..ac2d68924f2 100644 --- a/test/model.middleware.test.js +++ b/test/model.middleware.test.js @@ -333,9 +333,7 @@ describe('model middleware', function() { const test = new TestMiddleware({ title: 'Test' }); - const err = await test.save().then(() => null, err => err); - assert.ok(err); - assert.equal(err.message, 'woops!'); + await assert.rejects(test.save(), /woops!/); assert.equal(called, 0); }); From cb59f373386a03e9a8d6d6ca77af05fb02c42073 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 2 Apr 2025 13:07:55 -0400 Subject: [PATCH 030/209] docs: remove obsolete comment --- lib/helpers/model/applyStaticHooks.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/helpers/model/applyStaticHooks.js b/lib/helpers/model/applyStaticHooks.js index cb9cc6c9f5e..ec715f881e0 100644 --- a/lib/helpers/model/applyStaticHooks.js +++ b/lib/helpers/model/applyStaticHooks.js @@ -42,8 +42,6 @@ module.exports = function applyStaticHooks(model, hooks, statics) { const cb = typeof lastArg === 'function' ? lastArg : null; const args = Array.prototype.slice. call(arguments, 0, cb == null ? numArgs : numArgs - 1); - // Special case: can't use `Kareem#wrap()` because it doesn't currently - // support wrapped functions that return a promise. return promiseOrCallback(cb, callback => { hooks.execPre(key, model, args).then(() => onPreComplete(null), err => onPreComplete(err)); From eb0368fe89330140845de1d0f249c4d8fd02973d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 2 Apr 2025 13:08:21 -0400 Subject: [PATCH 031/209] style: fix lint --- lib/model.js | 8 ++++---- test/document.test.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/model.js b/lib/model.js index aa9a2387781..ef437896f22 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3373,11 +3373,11 @@ Model.bulkWrite = async function bulkWrite(ops, options) { options = options || {}; const shouldSkip = await this.hooks.execPre('bulkWrite', this, [ops, options]).catch(err => { - if (err instanceof Kareem.skipWrappedFunction) { - return err; - } - throw err; + if (err instanceof Kareem.skipWrappedFunction) { + return err; } + throw err; + } ); if (shouldSkip) { diff --git a/test/document.test.js b/test/document.test.js index db5ade0f7c3..0b6397b5ce7 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -14424,8 +14424,8 @@ describe('document', function() { } }); - describe('async stack traces (gh-15317)', function () { - it('works with save() validation errors', async function () { + describe('async stack traces (gh-15317)', function() { + it('works with save() validation errors', async function() { const userSchema = new mongoose.Schema({ name: { type: String, required: true, validate: v => v.length > 3 }, age: Number From a97893576c893ea02612e33d97d78722b3065608 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 27 Apr 2025 14:39:33 -0400 Subject: [PATCH 032/209] BREAKING CHANGE: make all post hooks use async functions, complete async stack traces for common workflows --- lib/aggregate.js | 61 +--------- lib/browserDocument.js | 9 +- lib/cursor/queryCursor.js | 14 +-- lib/document.js | 13 +-- lib/helpers/model/applyStaticHooks.js | 7 +- lib/model.js | 77 +++--------- lib/query.js | 42 +------ test/document.test.js | 162 +++++++++++++++++++++++++- 8 files changed, 193 insertions(+), 192 deletions(-) diff --git a/lib/aggregate.js b/lib/aggregate.js index aaaef070b28..8ad43f5b689 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -803,15 +803,7 @@ Aggregate.prototype.explain = async function explain(verbosity) { try { await model.hooks.execPre('aggregate', this); } catch (error) { - const _opts = { error: error }; - return await new Promise((resolve, reject) => { - model.hooks.execPost('aggregate', this, [null], _opts, error => { - if (error) { - return reject(error); - } - resolve(); - }); - }); + return await model.hooks.execPost('aggregate', this, [null], { error }); } const cursor = model.collection.aggregate(this._pipeline, this.options); @@ -824,26 +816,10 @@ Aggregate.prototype.explain = async function explain(verbosity) { try { result = await cursor.explain(verbosity); } catch (error) { - await new Promise((resolve, reject) => { - const _opts = { error: error }; - model.hooks.execPost('aggregate', this, [null], _opts, error => { - if (error) { - return reject(error); - } - return resolve(); - }); - }); + return await model.hooks.execPost('aggregate', this, [null], { error }); } - const _opts = { error: null }; - await new Promise((resolve, reject) => { - model.hooks.execPost('aggregate', this, [result], _opts, error => { - if (error) { - return reject(error); - } - return resolve(); - }); - }); + await model.hooks.execPost('aggregate', this, [result], { error: null }); return result; }; @@ -1083,15 +1059,7 @@ Aggregate.prototype.exec = async function exec() { try { await model.hooks.execPre('aggregate', this); } catch (error) { - const _opts = { error: error }; - return await new Promise((resolve, reject) => { - model.hooks.execPost('aggregate', this, [null], _opts, error => { - if (error) { - return reject(error); - } - resolve(); - }); - }); + return await model.hooks.execPost('aggregate', this, [null], { error }); } if (!this._pipeline.length) { @@ -1105,27 +1073,10 @@ Aggregate.prototype.exec = async function exec() { const cursor = await collection.aggregate(this._pipeline, options); result = await cursor.toArray(); } catch (error) { - await new Promise((resolve, reject) => { - const _opts = { error: error }; - model.hooks.execPost('aggregate', this, [null], _opts, (error) => { - if (error) { - return reject(error); - } - - resolve(); - }); - }); + return await model.hooks.execPost('aggregate', this, [null], { error }); } - const _opts = { error: null }; - await new Promise((resolve, reject) => { - model.hooks.execPost('aggregate', this, [result], _opts, error => { - if (error) { - return reject(error); - } - return resolve(); - }); - }); + await model.hooks.execPost('aggregate', this, [result], { error: null }); return result; }; diff --git a/lib/browserDocument.js b/lib/browserDocument.js index 7b7e6c1ccb6..b49e1997ac8 100644 --- a/lib/browserDocument.js +++ b/lib/browserDocument.js @@ -106,14 +106,7 @@ Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks( */ Document.prototype._execDocumentPostHooks = function _execDocumentPostHooks(opName, error) { - return new Promise((resolve, reject) => { - this._middleware.execPost(opName, this, [this], { error }, function(error) { - if (error) { - return reject(error); - } - resolve(); - }); - }); + return this._middleware.execPost(opName, this, [this], { error }); }; /*! diff --git a/lib/cursor/queryCursor.js b/lib/cursor/queryCursor.js index 18c5ba5836a..8fbf5d7e633 100644 --- a/lib/cursor/queryCursor.js +++ b/lib/cursor/queryCursor.js @@ -594,12 +594,7 @@ function _populateBatch() { function _nextDoc(ctx, doc, pop, callback) { if (ctx.query._mongooseOptions.lean) { - return ctx.model.hooks.execPost('find', ctx.query, [[doc]], err => { - if (err != null) { - return callback(err); - } - callback(null, doc); - }); + return ctx.model.hooks.execPost('find', ctx.query, [[doc]]).then(() => callback(null, doc), err => callback(err)); } const { model, _fields, _userProvidedFields, options } = ctx.query; @@ -607,12 +602,7 @@ function _nextDoc(ctx, doc, pop, callback) { if (err != null) { return callback(err); } - ctx.model.hooks.execPost('find', ctx.query, [[doc]], err => { - if (err != null) { - return callback(err); - } - callback(null, doc); - }); + ctx.model.hooks.execPost('find', ctx.query, [[doc]]).then(() => callback(null, doc), err => callback(err)); }); } diff --git a/lib/document.js b/lib/document.js index b31819bd3b8..0df1d59c3d4 100644 --- a/lib/document.js +++ b/lib/document.js @@ -850,8 +850,8 @@ Document.prototype.updateOne = function updateOne(doc, options, callback) { query.pre(function queryPreUpdateOne() { return self.constructor._middleware.execPre('updateOne', self, [self]); }); - query.post(function queryPostUpdateOne(cb) { - self.constructor._middleware.execPost('updateOne', self, [self], {}, cb); + query.post(function queryPostUpdateOne() { + return self.constructor._middleware.execPost('updateOne', self, [self], {}); }); if (this.$session() != null) { @@ -2911,14 +2911,7 @@ Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks( */ Document.prototype._execDocumentPostHooks = function _execDocumentPostHooks(opName, error) { - return new Promise((resolve, reject) => { - this.constructor._middleware.execPost(opName, this, [this], { error }, function(error) { - if (error) { - return reject(error); - } - resolve(); - }); - }); + return this.constructor._middleware.execPost(opName, this, [this], { error }); }; /*! diff --git a/lib/helpers/model/applyStaticHooks.js b/lib/helpers/model/applyStaticHooks.js index ec715f881e0..4abcb86b8f9 100644 --- a/lib/helpers/model/applyStaticHooks.js +++ b/lib/helpers/model/applyStaticHooks.js @@ -65,12 +65,7 @@ module.exports = function applyStaticHooks(model, hooks, statics) { return callback(error); } - hooks.execPost(key, model, [res], function(error) { - if (error != null) { - return callback(error); - } - callback(null, res); - }); + hooks.execPost(key, model, [res]).then(() => callback(null, res), err => callback(err)); } } }, model.events); diff --git a/lib/model.js b/lib/model.js index ef437896f22..3e9f5dc240b 100644 --- a/lib/model.js +++ b/lib/model.js @@ -821,13 +821,11 @@ Model.prototype.deleteOne = function deleteOne(options) { throw new Kareem.skipWrappedFunction(); } }); - query.post(function callSubdocPostHooks(cb) { - each(self.$getAllSubdocs(), (subdoc, cb) => { - subdoc.constructor._middleware.execPost('deleteOne', subdoc, [subdoc], {}, cb); - }, cb); + query.post(function callSubdocPostHooks() { + return Promise.all(self.$getAllSubdocs().map(subdoc => subdoc.constructor._middleware.execPost('deleteOne', subdoc, [subdoc]))); }); - query.post(function queryPostDeleteOne(cb) { - self.constructor._middleware.execPost('deleteOne', self, [self], {}, cb); + query.post(function queryPostDeleteOne() { + return self.constructor._middleware.execPost('deleteOne', self, [self], {}); }); return query; @@ -1247,26 +1245,11 @@ Model.createCollection = async function createCollection(options) { } } catch (err) { if (err != null && (err.name !== 'MongoServerError' || err.code !== 48)) { - await new Promise((resolve, reject) => { - const _opts = { error: err }; - this.hooks.execPost('createCollection', this, [null], _opts, (err) => { - if (err != null) { - return reject(err); - } - resolve(); - }); - }); + await this.hooks.execPost('createCollection', this, [null], { error: err }); } } - await new Promise((resolve, reject) => { - this.hooks.execPost('createCollection', this, [this.$__collection], (err) => { - if (err != null) { - return reject(err); - } - resolve(); - }); - }); + await this.hooks.execPost('createCollection', this, [this.$__collection]); return this.$__collection; }; @@ -3415,15 +3398,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) { try { res = await this.$__collection.bulkWrite(ops, options); } catch (error) { - await new Promise((resolve, reject) => { - const _opts = { error: error }; - this.hooks.execPost('bulkWrite', this, [null], _opts, (err) => { - if (err != null) { - return reject(err); - } - resolve(); - }); - }); + await this.hooks.execPost('bulkWrite', this, [null], { error }); } } else { let remaining = validations.length; @@ -3488,15 +3463,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) { decorateBulkWriteResult(error, validationErrors, results); } - await new Promise((resolve, reject) => { - const _opts = { error: error }; - this.hooks.execPost('bulkWrite', this, [null], _opts, (err) => { - if (err != null) { - return reject(err); - } - resolve(); - }); - }); + await this.hooks.execPost('bulkWrite', this, [null], { error }); } if (validationErrors.length > 0) { @@ -3513,14 +3480,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) { } } - await new Promise((resolve, reject) => { - this.hooks.execPost('bulkWrite', this, [res], (err) => { - if (err != null) { - return reject(err); - } - resolve(); - }); - }); + await this.hooks.execPost('bulkWrite', this, [res]); return res; }; @@ -3614,21 +3574,12 @@ async function buildPreSavePromise(document, options) { } function handleSuccessfulWrite(document) { - return new Promise((resolve, reject) => { - if (document.$isNew) { - _setIsNew(document, false); - } - - document.$__reset(); - document.schema.s.hooks.execPost('save', document, [document], {}, (err) => { - if (err) { - reject(err); - return; - } - resolve(); - }); + if (document.$isNew) { + _setIsNew(document, false); + } - }); + document.$__reset(); + return document.schema.s.hooks.execPost('save', document, [document]); } /** diff --git a/lib/query.js b/lib/query.js index cac1d9cb8a0..23ef6f267e9 100644 --- a/lib/query.js +++ b/lib/query.js @@ -4427,7 +4427,7 @@ Query.prototype.exec = async function exec(op) { let skipWrappedFunction = null; try { - await _executePreExecHooks(this); + await this._hooks.execPre('exec', this, []); } catch (err) { if (err instanceof Kareem.skipWrappedFunction) { skipWrappedFunction = err; @@ -4458,27 +4458,11 @@ Query.prototype.exec = async function exec(op) { res = await _executePostHooks(this, res, error); - await _executePostExecHooks(this); + await this._hooks.execPost('exec', this, []); return res; }; -/*! - * ignore - */ - -function _executePostExecHooks(query) { - return new Promise((resolve, reject) => { - query._hooks.execPost('exec', query, [], {}, (error) => { - if (error) { - return reject(error); - } - - resolve(); - }); - }); -} - /*! * ignore */ @@ -4491,27 +4475,13 @@ function _executePostHooks(query, res, error, op) { return res; } - return new Promise((resolve, reject) => { - const opts = error ? { error } : {}; - - query._queryMiddleware.execPost(op || query.op, query, [res], opts, (error, res) => { - if (error) { - return reject(error); - } - - resolve(res); - }); + const opts = error ? { error } : {}; + return query._queryMiddleware.execPost(op || query.op, query, [res], opts).then((res) => { + // `res` is array of return args, but queries only return one result. + return res[0]; }); } -/*! - * ignore - */ - -async function _executePreExecHooks(query) { - return query._hooks.execPre('exec', query, []); -} - /*! * ignore */ diff --git a/test/document.test.js b/test/document.test.js index cd2c9e91ce2..16838d2057f 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -14425,7 +14425,7 @@ describe('document', function() { }); describe('async stack traces (gh-15317)', function() { - it('works with save() validation errors', async function() { + it('works with save() validation errors', async function asyncSaveValidationErrors() { const userSchema = new mongoose.Schema({ name: { type: String, required: true, validate: v => v.length > 3 }, age: Number @@ -14434,7 +14434,165 @@ describe('document', function() { const doc = new User({ name: 'A' }); const err = await doc.save().then(() => null, err => err); assert.ok(err instanceof Error); - assert.ok(err.stack.includes('document.test.js'), err.stack); + assert.ok(err.stack.includes('asyncSaveValidationErrors'), err.stack); + }); + + it('works with async pre save errors', async function asyncPreSaveErrors() { + const userSchema = new mongoose.Schema({ + name: String, + age: Number + }); + userSchema.pre('save', async function() { + await new Promise(resolve => setTimeout(resolve, 5)); + throw new Error('pre save error'); + }); + const User = db.model('User', userSchema); + const doc = new User({ name: 'A' }); + const err = await doc.save().then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.message, 'pre save error'); + assert.ok(err.stack.includes('asyncPreSaveErrors'), err.stack); + }); + + it('works with async pre save errors with bulkSave()', async function asyncPreBulkSaveErrors() { + const userSchema = new mongoose.Schema({ + name: String, + age: Number + }); + userSchema.pre('save', async function() { + await new Promise(resolve => setTimeout(resolve, 5)); + throw new Error('pre bulk save error'); + }); + const User = db.model('User', userSchema); + const doc = new User({ name: 'A' }); + const err = await User.bulkSave([doc]).then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.message, 'pre bulk save error'); + assert.ok(err.stack.includes('asyncPreBulkSaveErrors'), err.stack); + }); + + it('works with async pre validate errors', async function asyncPreValidateErrors() { + const userSchema = new mongoose.Schema({ + name: String, + age: Number + }); + userSchema.pre('validate', async function() { + await new Promise(resolve => setTimeout(resolve, 5)); + throw new Error('pre validate error'); + }); + const User = db.model('User', userSchema); + const doc = new User({ name: 'A' }); + const err = await doc.save().then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.message, 'pre validate error'); + assert.ok(err.stack.includes('asyncPreValidateErrors'), err.stack); + }); + + it('works with async post save errors', async function asyncPostSaveErrors() { + const userSchema = new mongoose.Schema({ + name: String, + age: Number + }); + userSchema.post('save', async function() { + await new Promise(resolve => setTimeout(resolve, 5)); + throw new Error('post save error'); + }); + const User = db.model('User', userSchema); + const doc = new User({ name: 'A' }); + const err = await doc.save().then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.message, 'post save error'); + assert.ok(err.stack.includes('asyncPostSaveErrors'), err.stack); + }); + + it('works with async pre find errors', async function asyncPreFindErrors() { + const userSchema = new mongoose.Schema({ + name: String, + age: Number + }); + userSchema.pre('find', async function() { + await new Promise(resolve => setTimeout(resolve, 5)); + throw new Error('pre find error'); + }); + const User = db.model('User', userSchema); + const err = await User.find().then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.message, 'pre find error'); + assert.ok(err.stack.includes('asyncPreFindErrors'), err.stack); + }); + + it('works with async post find errors', async function asyncPostFindErrors() { + const userSchema = new mongoose.Schema({ + name: String, + age: Number + }); + userSchema.post('find', async function() { + await new Promise(resolve => setTimeout(resolve, 5)); + throw new Error('post find error'); + }); + const User = db.model('User', userSchema); + const err = await User.find().then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.message, 'post find error'); + assert.ok(err.stack.includes('asyncPostFindErrors'), err.stack); + }); + + it('works with find server errors', async function asyncPostFindErrors() { + const userSchema = new mongoose.Schema({ + name: String, + age: Number + }); + const User = db.model('User', userSchema); + // Fails on the MongoDB server because $notAnOperator is not a valid operator + const err = await User.find({ someProp: { $notAnOperator: 'value' } }).then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.name, 'MongoServerError'); + assert.ok(err.stack.includes('asyncPostFindErrors'), err.stack); + }); + + it('works with async pre aggregate errors', async function asyncPreAggregateErrors() { + const userSchema = new mongoose.Schema({ + name: String, + age: Number + }); + userSchema.pre('aggregate', async function() { + await new Promise(resolve => setTimeout(resolve, 5)); + throw new Error('pre aggregate error'); + }); + const User = db.model('User', userSchema); + const err = await User.aggregate([{ $match: {} }]).then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.message, 'pre aggregate error'); + assert.ok(err.stack.includes('asyncPreAggregateErrors'), err.stack); + }); + + it('works with async post aggregate errors', async function asyncPostAggregateErrors() { + const userSchema = new mongoose.Schema({ + name: String, + age: Number + }); + userSchema.post('aggregate', async function() { + await new Promise(resolve => setTimeout(resolve, 5)); + throw new Error('post aggregate error'); + }); + const User = db.model('User', userSchema); + const err = await User.aggregate([{ $match: {} }]).then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.message, 'post aggregate error'); + assert.ok(err.stack.includes('asyncPostAggregateErrors'), err.stack); + }); + + it('works with aggregate server errors', async function asyncAggregateServerErrors() { + const userSchema = new mongoose.Schema({ + name: String, + age: Number + }); + const User = db.model('User', userSchema); + // Fails on the MongoDB server because $notAnOperator is not a valid pipeline stage + const err = await User.aggregate([{ $notAnOperator: {} }]).then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.name, 'MongoServerError'); + assert.ok(err.stack.includes('asyncAggregateServerErrors'), err.stack); }); }); From b67f7971aaeb46c5c3f6e83ed26d9e29eb0c1483 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 27 Apr 2025 15:01:04 -0400 Subject: [PATCH 033/209] make $__handleSave() async for async stack traces on save() server errors --- lib/document.js | 2 +- lib/model.js | 75 +++++++++++-------------------------------- test/document.test.js | 52 ++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 57 deletions(-) diff --git a/lib/document.js b/lib/document.js index 0df1d59c3d4..346a435ec28 100644 --- a/lib/document.js +++ b/lib/document.js @@ -5014,7 +5014,7 @@ Document.prototype.$__delta = function $__delta() { } if (divergent.length) { - return new DivergentArrayError(divergent); + throw new DivergentArrayError(divergent); } if (this.$__.version) { diff --git a/lib/model.js b/lib/model.js index 3e9f5dc240b..7b030227dc0 100644 --- a/lib/model.js +++ b/lib/model.js @@ -318,7 +318,7 @@ function _applyCustomWhere(doc, where) { * ignore */ -Model.prototype.$__handleSave = function(options, callback) { +Model.prototype.$__handleSave = async function $__handleSave(options) { const saveOptions = {}; applyWriteConcern(this.$__schema, options); @@ -365,27 +365,18 @@ Model.prototype.$__handleSave = function(options, callback) { // wouldn't know what _id was generated by mongodb either // nor would the ObjectId generated by mongodb necessarily // match the schema definition. - immediate(function() { - callback(new MongooseError('document must have an _id before saving')); - }); - return; + throw new MongooseError('document must have an _id before saving'); } this.$__version(true, obj); - this[modelCollectionSymbol].insertOne(obj, saveOptions).then( - ret => callback(null, ret), - err => { - _setIsNew(this, true); - - callback(err, null); - } - ); - this.$__reset(); _setIsNew(this, false); // Make it possible to retry the insert this.$__.inserting = true; - return; + return this[modelCollectionSymbol].insertOne(obj, saveOptions).catch(err => { + _setIsNew(this, true); + throw err; + }); } // Make sure we don't treat it as a new object on error, @@ -405,17 +396,7 @@ Model.prototype.$__handleSave = function(options, callback) { } } if (delta) { - if (delta instanceof MongooseError) { - callback(delta); - return; - } - const where = this.$__where(delta[0]); - if (where instanceof MongooseError) { - callback(where); - return; - } - _applyCustomWhere(this, where); const update = delta[1]; @@ -441,33 +422,26 @@ Model.prototype.$__handleSave = function(options, callback) { } } - this[modelCollectionSymbol].updateOne(where, update, saveOptions).then( + // store the modified paths before the document is reset + this.$__.modifiedPaths = this.modifiedPaths(); + this.$__reset(); + + _setIsNew(this, false); + return this[modelCollectionSymbol].updateOne(where, update, saveOptions).then( ret => { if (ret == null) { ret = { $where: where }; } else { ret.$where = where; } - callback(null, ret); + return ret; }, err => { this.$__undoReset(); - - callback(err); + throw err; } ); } else { - handleEmptyUpdate.call(this); - return; - } - - // store the modified paths before the document is reset - this.$__.modifiedPaths = this.modifiedPaths(); - this.$__reset(); - - _setIsNew(this, false); - - function handleEmptyUpdate() { const optionsWithCustomValues = Object.assign({}, options, saveOptions); const where = this.$__where(); const optimisticConcurrency = this.$__schema.options.optimisticConcurrency; @@ -480,12 +454,11 @@ Model.prototype.$__handleSave = function(options, callback) { } applyReadConcern(this.$__schema, optionsWithCustomValues); - this.constructor.collection.findOne(where, optionsWithCustomValues) + return this.constructor.collection.findOne(where, optionsWithCustomValues) .then(documentExists => { const matchedCount = !documentExists ? 0 : 1; - callback(null, { $where: where, matchedCount }); - }) - .catch(callback); + return { $where: where, matchedCount }; + }); } }; @@ -504,14 +477,7 @@ Model.prototype.$__save = async function $__save(options) { let result = null; try { - result = await new Promise((resolve, reject) => { - this.$__handleSave(options, (error, result) => { - if (error) { - return reject(error); - } - resolve(result); - }); - }); + result = await this.$__handleSave(options); } catch (err) { const error = this.$__schema._transformDuplicateKeyError(err); await this._execDocumentPostHooks('save', error); @@ -758,7 +724,7 @@ Model.prototype.$__where = function _where(where) { } if (this._doc._id === void 0) { - return new MongooseError('No _id found on document!'); + throw new MongooseError('No _id found on document!'); } return where; @@ -799,9 +765,6 @@ Model.prototype.deleteOne = function deleteOne(options) { const self = this; const where = this.$__where(); - if (where instanceof Error) { - throw where; - } const query = self.constructor.deleteOne(where, options); if (this.$session() != null) { diff --git a/test/document.test.js b/test/document.test.js index 16838d2057f..e368ec498c6 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -14454,6 +14454,22 @@ describe('document', function() { assert.ok(err.stack.includes('asyncPreSaveErrors'), err.stack); }); + it('works with save server errors', async function saveServerErrors() { + const userSchema = new mongoose.Schema({ + name: { type: String, unique: true }, + age: Number + }); + const User = db.model('User', userSchema); + await User.init(); + + await User.create({ name: 'A' }); + const doc = new User({ name: 'A' }); + const err = await doc.save().then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.name, 'MongoServerError'); + assert.ok(err.stack.includes('saveServerErrors'), err.stack); + }); + it('works with async pre save errors with bulkSave()', async function asyncPreBulkSaveErrors() { const userSchema = new mongoose.Schema({ name: String, @@ -14505,6 +14521,42 @@ describe('document', function() { assert.ok(err.stack.includes('asyncPostSaveErrors'), err.stack); }); + it('works with async pre updateOne errors', async function asyncPreUpdateOneErrors() { + const userSchema = new mongoose.Schema({ + name: String, + age: Number + }); + userSchema.pre('updateOne', async function() { + await new Promise(resolve => setTimeout(resolve, 5)); + throw new Error('pre updateOne error'); + }); + const User = db.model('User', userSchema); + const doc = new User({ name: 'A' }); + await doc.save(); + const err = await doc.updateOne({ name: 'B' }).then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.message, 'pre updateOne error'); + assert.ok(err.stack.includes('asyncPreUpdateOneErrors'), err.stack); + }); + + it('works with async post updateOne errors', async function asyncPostUpdateOneErrors() { + const userSchema = new mongoose.Schema({ + name: String, + age: Number + }); + userSchema.post('updateOne', async function() { + await new Promise(resolve => setTimeout(resolve, 5)); + throw new Error('post updateOne error'); + }); + const User = db.model('User', userSchema); + const doc = new User({ name: 'A' }); + await doc.save(); + const err = await doc.updateOne({ name: 'B' }).then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.message, 'post updateOne error'); + assert.ok(err.stack.includes('asyncPostUpdateOneErrors'), err.stack); + }); + it('works with async pre find errors', async function asyncPreFindErrors() { const userSchema = new mongoose.Schema({ name: String, From 9c082bea70d100db2b43a61485b8b30de33911bd Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 27 Apr 2025 15:07:39 -0400 Subject: [PATCH 034/209] add test case for #15317 covering doc updateOne server errors --- test/document.test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/document.test.js b/test/document.test.js index e368ec498c6..990bee4f27f 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -14539,6 +14539,22 @@ describe('document', function() { assert.ok(err.stack.includes('asyncPreUpdateOneErrors'), err.stack); }); + it('works with updateOne server errors', async function updateOneServerErrors() { + const userSchema = new mongoose.Schema({ + name: { type: String, unique: true }, + age: Number + }); + const User = db.model('User', userSchema); + await User.init(); + const doc = new User({ name: 'A' }); + await doc.save(); + await User.create({ name: 'B' }); + const err = await doc.updateOne({ name: 'B' }).then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.name, 'MongoServerError'); + assert.ok(err.stack.includes('updateOneServerErrors'), err.stack); + }); + it('works with async post updateOne errors', async function asyncPostUpdateOneErrors() { const userSchema = new mongoose.Schema({ name: String, From 4b2e830aebf4a85f3a37f21e1b02f90a7d438140 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 27 Apr 2025 15:11:56 -0400 Subject: [PATCH 035/209] refactor: clean up createSaveOptions logic to simplify $__handleSave --- lib/model.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/model.js b/lib/model.js index 7b030227dc0..fda4b9ff646 100644 --- a/lib/model.js +++ b/lib/model.js @@ -317,11 +317,10 @@ function _applyCustomWhere(doc, where) { /*! * ignore */ - -Model.prototype.$__handleSave = async function $__handleSave(options) { +function _createSaveOptions(doc, options) { const saveOptions = {}; - applyWriteConcern(this.$__schema, options); + applyWriteConcern(doc.$__schema, options); if (typeof options.writeConcern !== 'undefined') { saveOptions.writeConcern = {}; if ('w' in options.writeConcern) { @@ -348,14 +347,25 @@ Model.prototype.$__handleSave = async function $__handleSave(options) { saveOptions.checkKeys = options.checkKeys; } - const session = this.$session(); - const asyncLocalStorage = this[modelDbSymbol].base.transactionAsyncLocalStorage?.getStore(); + const session = doc.$session(); + const asyncLocalStorage = doc[modelDbSymbol].base.transactionAsyncLocalStorage?.getStore(); if (session != null) { saveOptions.session = session; } else if (!options.hasOwnProperty('session') && asyncLocalStorage?.session != null) { // Only set session from asyncLocalStorage if `session` option wasn't originally passed in options saveOptions.session = asyncLocalStorage.session; } + + return saveOptions; +} + +/*! + * ignore + */ + +Model.prototype.$__handleSave = async function $__handleSave(options) { + const saveOptions = _createSaveOptions(this, options); + if (this.$isNew) { // send entire doc const obj = this.toObject(saveToObjectOptions); From 68641b695a6add49928d80c0a90f0fe1c9be2684 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 27 Apr 2025 15:33:59 -0400 Subject: [PATCH 036/209] refactor: remove $__handleSave(), move logic into $__save() --- lib/model.js | 202 +++++++++++++++++++++++---------------------------- 1 file changed, 90 insertions(+), 112 deletions(-) diff --git a/lib/model.js b/lib/model.js index fda4b9ff646..95ae4b5e99c 100644 --- a/lib/model.js +++ b/lib/model.js @@ -363,131 +363,110 @@ function _createSaveOptions(doc, options) { * ignore */ -Model.prototype.$__handleSave = async function $__handleSave(options) { - const saveOptions = _createSaveOptions(this, options); - - if (this.$isNew) { - // send entire doc - const obj = this.toObject(saveToObjectOptions); - if ((obj || {})._id === void 0) { - // documents must have an _id else mongoose won't know - // what to update later if more changes are made. the user - // wouldn't know what _id was generated by mongodb either - // nor would the ObjectId generated by mongodb necessarily - // match the schema definition. - throw new MongooseError('document must have an _id before saving'); - } - - this.$__version(true, obj); - this.$__reset(); - _setIsNew(this, false); - // Make it possible to retry the insert - this.$__.inserting = true; - return this[modelCollectionSymbol].insertOne(obj, saveOptions).catch(err => { - _setIsNew(this, true); - throw err; - }); +Model.prototype.$__save = async function $__save(options) { + try { + await this._execDocumentPreHooks('save', options); + } catch (error) { + await this._execDocumentPostHooks('save', error); + return; } - // Make sure we don't treat it as a new object on error, - // since it already exists - this.$__.inserting = false; - const delta = this.$__delta(); - if (options.pathsToSave) { - for (const key in delta[1]['$set']) { - if (options.pathsToSave.includes(key)) { - continue; - } else if (options.pathsToSave.some(pathToSave => key.slice(0, pathToSave.length) === pathToSave && key.charAt(pathToSave.length) === '.')) { - continue; - } else { - delete delta[1]['$set'][key]; + let result = null; + let where = null; + try { + const saveOptions = _createSaveOptions(this, options); + + if (this.$isNew) { + // send entire doc + const obj = this.toObject(saveToObjectOptions); + if ((obj || {})._id === void 0) { + // documents must have an _id else mongoose won't know + // what to update later if more changes are made. the user + // wouldn't know what _id was generated by mongodb either + // nor would the ObjectId generated by mongodb necessarily + // match the schema definition. + throw new MongooseError('document must have an _id before saving'); } - } - } - if (delta) { - const where = this.$__where(delta[0]); - _applyCustomWhere(this, where); - const update = delta[1]; - if (this.$__schema.options.minimize) { - for (const updateOp of Object.values(update)) { - if (updateOp == null) { - continue; - } - for (const key of Object.keys(updateOp)) { - if (updateOp[key] == null || typeof updateOp[key] !== 'object') { + this.$__version(true, obj); + this.$__reset(); + _setIsNew(this, false); + // Make it possible to retry the insert + this.$__.inserting = true; + result = await this[modelCollectionSymbol].insertOne(obj, saveOptions).catch(err => { + _setIsNew(this, true); + throw err; + }); + } else { + // Make sure we don't treat it as a new object on error, + // since it already exists + this.$__.inserting = false; + const delta = this.$__delta(); + + if (options.pathsToSave) { + for (const key in delta[1]['$set']) { + if (options.pathsToSave.includes(key)) { continue; - } - if (!utils.isPOJO(updateOp[key])) { + } else if (options.pathsToSave.some(pathToSave => key.slice(0, pathToSave.length) === pathToSave && key.charAt(pathToSave.length) === '.')) { continue; - } - minimize(updateOp[key]); - if (Object.keys(updateOp[key]).length === 0) { - delete updateOp[key]; - update.$unset = update.$unset || {}; - update.$unset[key] = 1; + } else { + delete delta[1]['$set'][key]; } } } - } + if (delta) { + where = this.$__where(delta[0]); + _applyCustomWhere(this, where); + + const update = delta[1]; + if (this.$__schema.options.minimize) { + for (const updateOp of Object.values(update)) { + if (updateOp == null) { + continue; + } + for (const key of Object.keys(updateOp)) { + if (updateOp[key] == null || typeof updateOp[key] !== 'object') { + continue; + } + if (!utils.isPOJO(updateOp[key])) { + continue; + } + minimize(updateOp[key]); + if (Object.keys(updateOp[key]).length === 0) { + delete updateOp[key]; + update.$unset = update.$unset || {}; + update.$unset[key] = 1; + } + } + } + } - // store the modified paths before the document is reset - this.$__.modifiedPaths = this.modifiedPaths(); - this.$__reset(); + // store the modified paths before the document is reset + this.$__.modifiedPaths = this.modifiedPaths(); + this.$__reset(); - _setIsNew(this, false); - return this[modelCollectionSymbol].updateOne(where, update, saveOptions).then( - ret => { - if (ret == null) { - ret = { $where: where }; - } else { - ret.$where = where; + _setIsNew(this, false); + result = await this[modelCollectionSymbol].updateOne(where, update, saveOptions).catch(err => { + this.$__undoReset(); + throw err; + }); + } else { + where = this.$__where(); + const optimisticConcurrency = this.$__schema.options.optimisticConcurrency; + if (optimisticConcurrency && !Array.isArray(optimisticConcurrency)) { + const key = this.$__schema.options.versionKey; + const val = this.$__getValue(key); + if (val != null) { + where[key] = val; + } } - return ret; - }, - err => { - this.$__undoReset(); - throw err; - } - ); - } else { - const optionsWithCustomValues = Object.assign({}, options, saveOptions); - const where = this.$__where(); - const optimisticConcurrency = this.$__schema.options.optimisticConcurrency; - if (optimisticConcurrency && !Array.isArray(optimisticConcurrency)) { - const key = this.$__schema.options.versionKey; - const val = this.$__getValue(key); - if (val != null) { - where[key] = val; + + applyReadConcern(this.$__schema, saveOptions); + result = await this.constructor.collection.findOne(where, saveOptions) + .then(documentExists => ({ matchedCount: !documentExists ? 0 : 1 })); } } - - applyReadConcern(this.$__schema, optionsWithCustomValues); - return this.constructor.collection.findOne(where, optionsWithCustomValues) - .then(documentExists => { - const matchedCount = !documentExists ? 0 : 1; - return { $where: where, matchedCount }; - }); - } -}; - -/*! - * ignore - */ - -Model.prototype.$__save = async function $__save(options) { - try { - await this._execDocumentPreHooks('save', options); - } catch (error) { - await this._execDocumentPostHooks('save', error); - return; - } - - - let result = null; - try { - result = await this.$__handleSave(options); } catch (err) { const error = this.$__schema._transformDuplicateKeyError(err); await this._execDocumentPostHooks('save', error); @@ -536,8 +515,7 @@ Model.prototype.$__save = async function $__save(options) { } if (result != null && numAffected <= 0) { this.$__undoReset(); - const error = new DocumentNotFoundError(result.$where, - this.constructor.modelName, numAffected, result); + const error = new DocumentNotFoundError(where, this.constructor.modelName, numAffected, result); await this._execDocumentPostHooks('save', error); return; } From 3fc683ab6e4c22de1f2095a20cb652d3bc0c9acf Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 27 Apr 2025 15:52:56 -0400 Subject: [PATCH 037/209] refactor: simplify optimistic concurrency handling to rely on $__delta setting $__.version --- lib/model.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/model.js b/lib/model.js index 95ae4b5e99c..43111422676 100644 --- a/lib/model.js +++ b/lib/model.js @@ -453,13 +453,9 @@ Model.prototype.$__save = async function $__save(options) { }); } else { where = this.$__where(); - const optimisticConcurrency = this.$__schema.options.optimisticConcurrency; - if (optimisticConcurrency && !Array.isArray(optimisticConcurrency)) { - const key = this.$__schema.options.versionKey; - const val = this.$__getValue(key); - if (val != null) { - where[key] = val; - } + _applyCustomWhere(this, where); + if (this.$__.version) { + this.$__version(where, delta); } applyReadConcern(this.$__schema, saveOptions); From b19ed8f070ceb9b3149a5a30066db19db2f748a5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 27 Apr 2025 16:25:25 -0400 Subject: [PATCH 038/209] fix: better handling for deleteOne hooks on deleted subdocs --- lib/helpers/model/applyHooks.js | 7 ------ lib/plugins/saveSubdocs.js | 14 +++++++++++ lib/types/subdocument.js | 25 +------------------ test/document.test.js | 43 +++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 31 deletions(-) diff --git a/lib/helpers/model/applyHooks.js b/lib/helpers/model/applyHooks.js index a1ee62f31ef..08e42417f3c 100644 --- a/lib/helpers/model/applyHooks.js +++ b/lib/helpers/model/applyHooks.js @@ -106,13 +106,6 @@ function applyHooks(model, schema, options) { model._middleware = middleware; - const internalMethodsToWrap = options && options.isChildSchema ? ['deleteOne'] : []; - for (const method of internalMethodsToWrap) { - const toWrap = `$__${method}`; - const wrapped = middleware. - createWrapper(method, objToDecorate[toWrap], null, kareemOptions); - objToDecorate[`$__${method}`] = wrapped; - } objToDecorate.$__init = middleware. createWrapperSync('init', objToDecorate.$__init, null, kareemOptions); diff --git a/lib/plugins/saveSubdocs.js b/lib/plugins/saveSubdocs.js index 744f8765fff..eb1e99a03f8 100644 --- a/lib/plugins/saveSubdocs.js +++ b/lib/plugins/saveSubdocs.js @@ -25,6 +25,20 @@ module.exports = function saveSubdocs(schema) { } }, null, unshift); + schema.s.hooks.pre('save', async function saveSubdocsPreDeleteOne() { + const removedSubdocs = this.$__.removedSubdocs; + if (!removedSubdocs || !removedSubdocs.length) { + return; + } + + const promises = []; + for (const subdoc of removedSubdocs) { + promises.push(subdoc._execDocumentPreHooks('deleteOne')); + } + + await Promise.all(promises); + }); + schema.s.hooks.post('save', async function saveSubdocsPostDeleteOne() { const removedSubdocs = this.$__.removedSubdocs; if (!removedSubdocs || !removedSubdocs.length) { diff --git a/lib/types/subdocument.js b/lib/types/subdocument.js index 550471a59db..b4e1f1807c3 100644 --- a/lib/types/subdocument.js +++ b/lib/types/subdocument.js @@ -332,22 +332,6 @@ Subdocument.prototype.parent = function() { Subdocument.prototype.$parent = Subdocument.prototype.parent; -/** - * no-op for hooks - * @param {Function} cb - * @method $__deleteOne - * @memberOf Subdocument - * @instance - * @api private - */ - -Subdocument.prototype.$__deleteOne = function(cb) { - if (cb == null) { - return; - } - return cb(null, this); -}; - /** * ignore * @method $__removeFromParent @@ -364,14 +348,9 @@ Subdocument.prototype.$__removeFromParent = function() { * Null-out this subdoc * * @param {Object} [options] - * @param {Function} [callback] optional callback for compatibility with Document.prototype.remove */ -Subdocument.prototype.deleteOne = function(options, callback) { - if (typeof options === 'function') { - callback = options; - options = null; - } +Subdocument.prototype.deleteOne = function deleteOne(options) { registerRemoveListener(this); // If removing entire doc, no need to remove subdoc @@ -382,8 +361,6 @@ Subdocument.prototype.deleteOne = function(options, callback) { owner.$__.removedSubdocs = owner.$__.removedSubdocs || []; owner.$__.removedSubdocs.push(this); } - - return this.$__deleteOne(callback); }; /*! diff --git a/test/document.test.js b/test/document.test.js index 990bee4f27f..9f97fdebdfb 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -10139,6 +10139,8 @@ describe('document', function() { }; const document = await Model.create(newModel); document.mySubdoc[0].deleteOne(); + await new Promise(resolve => setTimeout(resolve, 10)); + assert.equal(count, 0); await document.save().catch((error) => { console.error(error); }); @@ -14454,6 +14456,26 @@ describe('document', function() { assert.ok(err.stack.includes('asyncPreSaveErrors'), err.stack); }); + it('works with async pre save errors on subdocuments', async function asyncSubdocPreSaveErrors() { + const addressSchema = new mongoose.Schema({ + street: String + }); + addressSchema.pre('save', async function() { + await new Promise(resolve => setTimeout(resolve, 5)); + throw new Error('subdoc pre save error'); + }); + const userSchema = new mongoose.Schema({ + name: String, + address: addressSchema + }); + const User = db.model('User', userSchema); + const doc = new User({ name: 'A', address: { street: 'Main St' } }); + const err = await doc.save().then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.message, 'subdoc pre save error'); + assert.ok(err.stack.includes('asyncSubdocPreSaveErrors'), err.stack); + }); + it('works with save server errors', async function saveServerErrors() { const userSchema = new mongoose.Schema({ name: { type: String, unique: true }, @@ -14573,6 +14595,27 @@ describe('document', function() { assert.ok(err.stack.includes('asyncPostUpdateOneErrors'), err.stack); }); + it('works with async pre deleteOne errors on subdocuments', async function asyncSubdocPreDeleteOneErrors() { + const addressSchema = new mongoose.Schema({ + street: String + }); + addressSchema.post('deleteOne', { document: true, query: false }, async function() { + await new Promise(resolve => setTimeout(resolve, 5)); + throw new Error('subdoc pre deleteOne error'); + }); + const userSchema = new mongoose.Schema({ + name: String, + address: addressSchema + }); + const User = db.model('User', userSchema); + const doc = new User({ name: 'A', address: { street: 'Main St' } }); + await doc.save(); + const err = await doc.deleteOne().then(() => null, err => err); + assert.ok(err instanceof Error); + assert.equal(err.message, 'subdoc pre deleteOne error'); + assert.ok(err.stack.includes('asyncSubdocPreDeleteOneErrors'), err.stack); + }); + it('works with async pre find errors', async function asyncPreFindErrors() { const userSchema = new mongoose.Schema({ name: String, From bd5fd4d078287e6037e77c2d40851a5f3ca16a62 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 27 Apr 2025 16:28:43 -0400 Subject: [PATCH 039/209] add note about deleteOne hooks to migrating_to_9 --- docs/migrating_to_9.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 537ba040438..55de889183d 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -67,3 +67,44 @@ schema.pre('save', function(next, arg) { ``` In Mongoose 9, `next(null, 'new arg')` doesn't overwrite the args to the next middleware. + +## Subdocument `deleteOne()` hooks execute only when subdocument is deleted + +Currently, calling `deleteOne()` on a subdocument will execute the `deleteOne()` hooks on the subdocument regardless of whether the subdocument is actually deleted. + +```javascript +const SubSchema = new Schema({ + myValue: { + type: String + } +}, {}); +let count = 0; +SubSchema.pre('deleteOne', { document: true, query: false }, function(next) { + count++; + next(); +}); +const schema = new Schema({ + foo: { + type: String, + required: true + }, + mySubdoc: { + type: [SubSchema], + required: true + } +}, { minimize: false, collection: 'test' }); + +const Model = db.model('TestModel', schema); + +const newModel = { + foo: 'bar', + mySubdoc: [{ myValue: 'some value' }] +}; +const doc = await Model.create(newModel); + +// In Mongoose 8, the following would trigger the `deleteOne` hook, even if `doc` is not saved or deleted. +doc.mySubdoc[0].deleteOne(); + +// In Mongoose 9, you would need to either `save()` or `deleteOne()` on `doc` to trigger the subdocument `deleteOne` hook. +await doc.save(); +``` From 91791a043879c8668b12d1cad7510b30a3a8458a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 27 Apr 2025 17:37:42 -0400 Subject: [PATCH 040/209] refactor: make insertMany use async functions for async stack traces re: #15317 --- lib/helpers/model/applyStaticHooks.js | 8 - lib/helpers/parallelLimit.js | 54 ++-- lib/model.js | 405 ++++++++++++-------------- test/document.test.js | 1 + test/model.insertMany.test.js | 34 +++ test/parallelLimit.test.js | 53 ++-- 6 files changed, 252 insertions(+), 303 deletions(-) diff --git a/lib/helpers/model/applyStaticHooks.js b/lib/helpers/model/applyStaticHooks.js index 4abcb86b8f9..46c8faaacbf 100644 --- a/lib/helpers/model/applyStaticHooks.js +++ b/lib/helpers/model/applyStaticHooks.js @@ -13,14 +13,6 @@ const middlewareFunctions = Array.from( ); module.exports = function applyStaticHooks(model, hooks, statics) { - const kareemOptions = { - useErrorHandlers: true, - numCallbackParams: 1 - }; - - model.$__insertMany = hooks.createWrapper('insertMany', - model.$__insertMany, model, kareemOptions); - hooks = hooks.filter(hook => { // If the custom static overwrites an existing middleware, don't apply // middleware to it by default. This avoids a potential backwards breaking diff --git a/lib/helpers/parallelLimit.js b/lib/helpers/parallelLimit.js index 9b07c028bf8..a2170e480f2 100644 --- a/lib/helpers/parallelLimit.js +++ b/lib/helpers/parallelLimit.js @@ -6,50 +6,32 @@ module.exports = parallelLimit; * ignore */ -function parallelLimit(fns, limit, callback) { - let numInProgress = 0; - let numFinished = 0; - let error = null; - +async function parallelLimit(params, fn, limit) { if (limit <= 0) { throw new Error('Limit must be positive'); } - if (fns.length === 0) { - return callback(null, []); + if (params.length === 0) { + return []; } - for (let i = 0; i < fns.length && i < limit; ++i) { - _start(); - } + const results = []; + const executing = new Set(); - function _start() { - fns[numFinished + numInProgress](_done(numFinished + numInProgress)); - ++numInProgress; - } + for (let index = 0; index < params.length; index++) { + const param = params[index]; + const p = fn(param, index); + results.push(p); - const results = []; + executing.add(p); + + const clean = () => executing.delete(p); + p.then(clean).catch(clean); - function _done(index) { - return (err, res) => { - --numInProgress; - ++numFinished; - - if (error != null) { - return; - } - if (err != null) { - error = err; - return callback(error); - } - - results[index] = res; - - if (numFinished === fns.length) { - return callback(null, results); - } else if (numFinished + numInProgress < fns.length) { - _start(); - } - }; + if (executing.size >= limit) { + await Promise.race(executing); + } } + + return Promise.all(results); } diff --git a/lib/model.js b/lib/model.js index 43111422676..db0d9e4b6ff 100644 --- a/lib/model.js +++ b/lib/model.js @@ -2898,37 +2898,15 @@ Model.insertMany = async function insertMany(arr, options) { throw new MongooseError('Model.insertMany() no longer accepts a callback'); } - return new Promise((resolve, reject) => { - this.$__insertMany(arr, options, (err, res) => { - if (err != null) { - return reject(err); - } - resolve(res); - }); - }); -}; - -/** - * ignore - * - * @param {Array} arr - * @param {Object} options - * @param {Function} callback - * @api private - * @memberOf Model - * @method $__insertMany - * @static - */ - -Model.$__insertMany = function(arr, options, callback) { - const _this = this; - if (typeof options === 'function') { - callback = options; - options = undefined; + try { + await this._middleware.execPre('insertMany', this, [arr]); + } catch (error) { + await this._middleware.execPost('insertMany', this, [arr], { error }); } - callback = callback || utils.noop; + options = options || {}; + const ThisModel = this; const limit = options.limit || 1000; const rawResult = !!options.rawResult; const ordered = typeof options.ordered === 'boolean' ? options.ordered : true; @@ -2947,236 +2925,211 @@ Model.$__insertMany = function(arr, options, callback) { const validationErrors = []; const validationErrorsToOriginalOrder = new Map(); const results = ordered ? null : new Array(arr.length); - const toExecute = arr.map((doc, index) => - callback => { - // If option `lean` is set to true bypass validation and hydration - if (lean) { - // we have to execute callback at the nextTick to be compatible - // with parallelLimit, as `results` variable has TDZ issue if we - // execute the callback synchronously - return immediate(() => callback(null, doc)); - } - let createdNewDoc = false; - if (!(doc instanceof _this)) { - if (doc != null && typeof doc !== 'object') { - return callback(new ObjectParameterError(doc, 'arr.' + index, 'insertMany')); - } - try { - doc = new _this(doc); - createdNewDoc = true; - } catch (err) { - return callback(err); - } + async function validateDoc(doc, index) { + // If option `lean` is set to true bypass validation and hydration + if (lean) { + return doc; + } + let createdNewDoc = false; + if (!(doc instanceof ThisModel)) { + if (doc != null && typeof doc !== 'object') { + throw new ObjectParameterError(doc, 'arr.' + index, 'insertMany'); } + doc = new ThisModel(doc); + createdNewDoc = true; + } - if (options.session != null) { - doc.$session(options.session); - } - // If option `lean` is set to true bypass validation - if (lean) { - // we have to execute callback at the nextTick to be compatible - // with parallelLimit, as `results` variable has TDZ issue if we - // execute the callback synchronously - return immediate(() => callback(null, doc)); - } - doc.$validate(createdNewDoc ? { _skipParallelValidateCheck: true } : null).then( - () => { callback(null, doc); }, - error => { - if (ordered === false) { - validationErrors.push(error); - validationErrorsToOriginalOrder.set(error, index); - results[index] = error; - return callback(null, null); - } - callback(error); + if (options.session != null) { + doc.$session(options.session); + } + return doc.$validate(createdNewDoc ? { _skipParallelValidateCheck: true } : null) + .then(() => doc) + .catch(error => { + if (ordered === false) { + validationErrors.push(error); + validationErrorsToOriginalOrder.set(error, index); + results[index] = error; + return; } - ); - }); + throw error; + }); + } - parallelLimit(toExecute, limit, function(error, docs) { - if (error) { - callback(error, null); - return; - } + const docs = await parallelLimit(arr, validateDoc, limit); - const originalDocIndex = new Map(); - const validDocIndexToOriginalIndex = new Map(); - for (let i = 0; i < docs.length; ++i) { - originalDocIndex.set(docs[i], i); - } + const originalDocIndex = new Map(); + const validDocIndexToOriginalIndex = new Map(); + for (let i = 0; i < docs.length; ++i) { + originalDocIndex.set(docs[i], i); + } + + // We filter all failed pre-validations by removing nulls + const docAttributes = docs.filter(function(doc) { + return doc != null; + }); + for (let i = 0; i < docAttributes.length; ++i) { + validDocIndexToOriginalIndex.set(i, originalDocIndex.get(docAttributes[i])); + } - // We filter all failed pre-validations by removing nulls - const docAttributes = docs.filter(function(doc) { - return doc != null; + // Make sure validation errors are in the same order as the + // original documents, so if both doc1 and doc2 both fail validation, + // `Model.insertMany([doc1, doc2])` will always have doc1's validation + // error before doc2's. Re: gh-12791. + if (validationErrors.length > 0) { + validationErrors.sort((err1, err2) => { + return validationErrorsToOriginalOrder.get(err1) - validationErrorsToOriginalOrder.get(err2); }); - for (let i = 0; i < docAttributes.length; ++i) { - validDocIndexToOriginalIndex.set(i, originalDocIndex.get(docAttributes[i])); - } + } - // Make sure validation errors are in the same order as the - // original documents, so if both doc1 and doc2 both fail validation, - // `Model.insertMany([doc1, doc2])` will always have doc1's validation - // error before doc2's. Re: gh-12791. - if (validationErrors.length > 0) { - validationErrors.sort((err1, err2) => { - return validationErrorsToOriginalOrder.get(err1) - validationErrorsToOriginalOrder.get(err2); - }); + // Quickly escape while there aren't any valid docAttributes + if (docAttributes.length === 0) { + if (throwOnValidationError) { + throw new MongooseBulkWriteError( + validationErrors, + results, + null, + 'insertMany' + ); } - - // Quickly escape while there aren't any valid docAttributes - if (docAttributes.length === 0) { - if (throwOnValidationError) { - return callback(new MongooseBulkWriteError( - validationErrors, - results, - null, - 'insertMany' - )); - } - if (rawResult) { - const res = { - acknowledged: true, - insertedCount: 0, - insertedIds: {} - }; - decorateBulkWriteResult(res, validationErrors, validationErrors); - return callback(null, res); - } - callback(null, []); - return; + if (rawResult) { + const res = { + acknowledged: true, + insertedCount: 0, + insertedIds: {} + }; + decorateBulkWriteResult(res, validationErrors, validationErrors); + return res; } - const docObjects = lean ? docAttributes : docAttributes.map(function(doc) { - if (doc.$__schema.options.versionKey) { - doc[doc.$__schema.options.versionKey] = 0; - } - const shouldSetTimestamps = (!options || options.timestamps !== false) && doc.initializeTimestamps && (!doc.$__ || doc.$__.timestamps !== false); - if (shouldSetTimestamps) { - doc.initializeTimestamps(); - } - if (doc.$__hasOnlyPrimitiveValues()) { - return doc.$__toObjectShallow(); - } - return doc.toObject(internalToObjectOptions); - }); - - _this.$__collection.insertMany(docObjects, options).then( - res => { - if (!lean) { - for (const attribute of docAttributes) { - attribute.$__reset(); - _setIsNew(attribute, false); - } - } + return []; + } + const docObjects = lean ? docAttributes : docAttributes.map(function(doc) { + if (doc.$__schema.options.versionKey) { + doc[doc.$__schema.options.versionKey] = 0; + } + const shouldSetTimestamps = (!options || options.timestamps !== false) && doc.initializeTimestamps && (!doc.$__ || doc.$__.timestamps !== false); + if (shouldSetTimestamps) { + doc.initializeTimestamps(); + } + if (doc.$__hasOnlyPrimitiveValues()) { + return doc.$__toObjectShallow(); + } + return doc.toObject(internalToObjectOptions); + }); - if (ordered === false && throwOnValidationError && validationErrors.length > 0) { - for (let i = 0; i < results.length; ++i) { - if (results[i] === void 0) { - results[i] = docs[i]; - } - } - return callback(new MongooseBulkWriteError( - validationErrors, - results, - res, - 'insertMany' - )); - } + let res; + try { + res = await this.$__collection.insertMany(docObjects, options); + } catch (error) { + // `writeErrors` is a property reported by the MongoDB driver, + // just not if there's only 1 error. + if (error.writeErrors == null && + (error.result && error.result.result && error.result.result.writeErrors) != null) { + error.writeErrors = error.result.result.writeErrors; + } - if (rawResult) { - if (ordered === false) { - for (let i = 0; i < results.length; ++i) { - if (results[i] === void 0) { - results[i] = docs[i]; - } - } + // `insertedDocs` is a Mongoose-specific property + const hasWriteErrors = error && error.writeErrors; + const erroredIndexes = new Set((error && error.writeErrors || []).map(err => err.index)); - // Decorate with mongoose validation errors in case of unordered, - // because then still do `insertMany()` - decorateBulkWriteResult(res, validationErrors, results); - } - return callback(null, res); + if (error.writeErrors != null) { + for (let i = 0; i < error.writeErrors.length; ++i) { + const originalIndex = validDocIndexToOriginalIndex.get(error.writeErrors[i].index); + error.writeErrors[i] = { ...error.writeErrors[i], index: originalIndex }; + if (!ordered) { + results[originalIndex] = error.writeErrors[i]; } + } + } - if (options.populate != null) { - return _this.populate(docAttributes, options.populate).then( - docs => { callback(null, docs); }, - err => { - if (err != null) { - err.insertedDocs = docAttributes; - } - throw err; - } - ); + if (!ordered) { + for (let i = 0; i < results.length; ++i) { + if (results[i] === void 0) { + results[i] = docs[i]; } + } - callback(null, docAttributes); - }, - error => { - // `writeErrors` is a property reported by the MongoDB driver, - // just not if there's only 1 error. - if (error.writeErrors == null && - (error.result && error.result.result && error.result.result.writeErrors) != null) { - error.writeErrors = error.result.result.writeErrors; - } + error.results = results; + } - // `insertedDocs` is a Mongoose-specific property - const hasWriteErrors = error && error.writeErrors; - const erroredIndexes = new Set((error && error.writeErrors || []).map(err => err.index)); + let firstErroredIndex = -1; + error.insertedDocs = docAttributes. + filter((doc, i) => { + const isErrored = !hasWriteErrors || erroredIndexes.has(i); - if (error.writeErrors != null) { - for (let i = 0; i < error.writeErrors.length; ++i) { - const originalIndex = validDocIndexToOriginalIndex.get(error.writeErrors[i].index); - error.writeErrors[i] = { ...error.writeErrors[i], index: originalIndex }; - if (!ordered) { - results[originalIndex] = error.writeErrors[i]; - } + if (ordered) { + if (firstErroredIndex > -1) { + return i < firstErroredIndex; } - } - if (!ordered) { - for (let i = 0; i < results.length; ++i) { - if (results[i] === void 0) { - results[i] = docs[i]; - } + if (isErrored) { + firstErroredIndex = i; } + } - error.results = results; + return !isErrored; + }). + map(function setIsNewForInsertedDoc(doc) { + if (lean) { + return doc; } + doc.$__reset(); + _setIsNew(doc, false); + return doc; + }); - let firstErroredIndex = -1; - error.insertedDocs = docAttributes. - filter((doc, i) => { - const isErrored = !hasWriteErrors || erroredIndexes.has(i); + if (rawResult && ordered === false) { + decorateBulkWriteResult(error, validationErrors, results); + } - if (ordered) { - if (firstErroredIndex > -1) { - return i < firstErroredIndex; - } + await this._middleware.execPost('insertMany', this, [arr], { error }); + } - if (isErrored) { - firstErroredIndex = i; - } - } + if (!lean) { + for (const attribute of docAttributes) { + attribute.$__reset(); + _setIsNew(attribute, false); + } + } - return !isErrored; - }). - map(function setIsNewForInsertedDoc(doc) { - if (lean) { - return doc; - } - doc.$__reset(); - _setIsNew(doc, false); - return doc; - }); + if (ordered === false && throwOnValidationError && validationErrors.length > 0) { + for (let i = 0; i < results.length; ++i) { + if (results[i] === void 0) { + results[i] = docs[i]; + } + } + throw new MongooseBulkWriteError( + validationErrors, + results, + res, + 'insertMany' + ); + } - if (rawResult && ordered === false) { - decorateBulkWriteResult(error, validationErrors, results); + if (rawResult) { + if (ordered === false) { + for (let i = 0; i < results.length; ++i) { + if (results[i] === void 0) { + results[i] = docs[i]; } + } + + // Decorate with mongoose validation errors in case of unordered, + // because then still do `insertMany()` + decorateBulkWriteResult(res, validationErrors, results); + } + return res; + } - callback(error, null); + if (options.populate != null) { + return this.populate(docAttributes, options.populate).catch(err => { + if (err != null) { + err.insertedDocs = docAttributes; } - ); - }); + throw err; + }); + } + + return await this._middleware.execPost('insertMany', this, [docAttributes]).then(res => res[0]); }; /*! diff --git a/test/document.test.js b/test/document.test.js index 9f97fdebdfb..1bd0ae78c6e 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -10139,6 +10139,7 @@ describe('document', function() { }; const document = await Model.create(newModel); document.mySubdoc[0].deleteOne(); + // Set timeout to make sure that we aren't calling the deleteOne hooks synchronously await new Promise(resolve => setTimeout(resolve, 10)); assert.equal(count, 0); await document.save().catch((error) => { diff --git a/test/model.insertMany.test.js b/test/model.insertMany.test.js index db8c96b535c..5b8e8270738 100644 --- a/test/model.insertMany.test.js +++ b/test/model.insertMany.test.js @@ -646,4 +646,38 @@ describe('insertMany()', function() { await Money.insertMany([{ amount: '123.45' }]); }); + + it('async stack traces with server error (gh-15317)', async function insertManyWithServerError() { + const schema = new mongoose.Schema({ + name: { type: String, unique: true } + }); + const User = db.model('Test', schema); + await User.init(); + + const err = await User.insertMany([ + { name: 'A' }, + { name: 'A' } + ]).then(() => null, err => err); + assert.equal(err.name, 'MongoBulkWriteError'); + assert.ok(err.stack.includes('insertManyWithServerError')); + }); + + it('async stack traces with post insertMany error (gh-15317)', async function postInsertManyError() { + const schema = new mongoose.Schema({ + name: { type: String } + }); + schema.post('insertMany', async function() { + await new Promise(resolve => setTimeout(resolve, 10)); + throw new Error('postInsertManyError'); + }); + const User = db.model('Test', schema); + await User.init(); + + const err = await User.insertMany([ + { name: 'A' }, + { name: 'A' } + ]).then(() => null, err => err); + assert.equal(err.message, 'postInsertManyError'); + assert.ok(err.stack.includes('postInsertManyError')); + }); }); diff --git a/test/parallelLimit.test.js b/test/parallelLimit.test.js index 82f1addf864..2e1bab663c6 100644 --- a/test/parallelLimit.test.js +++ b/test/parallelLimit.test.js @@ -4,46 +4,33 @@ const assert = require('assert'); const parallelLimit = require('../lib/helpers/parallelLimit'); describe('parallelLimit', function() { - it('works with zero functions', function(done) { - parallelLimit([], 1, (err, res) => { - assert.ifError(err); - assert.deepEqual(res, []); - done(); - }); + it('works with zero functions', async function() { + const results = await parallelLimit([], value => Promise.resolve(value), 1); + assert.deepEqual(results, []); }); - it('executes functions in parallel', function(done) { + it('executes functions in parallel', async function() { let started = 0; let finished = 0; - const fns = [ - cb => { - ++started; - setTimeout(() => { - ++finished; - setTimeout(cb, 0); - }, 100); - }, - cb => { - ++started; - setTimeout(() => { - ++finished; - setTimeout(cb, 0); - }, 100); - }, - cb => { + const params = [1, 2, 3]; + + const fn = async() => { + ++started; + await new Promise(resolve => setTimeout(resolve, 10)); + ++finished; + return finished; + }; + + const results = await parallelLimit(params, async (param, index) => { + if (index === 2) { assert.equal(started, 2); assert.ok(finished > 0); - ++started; - ++finished; - setTimeout(cb, 0); } - ]; + return fn(); + }, 2); - parallelLimit(fns, 2, (err) => { - assert.ifError(err); - assert.equal(started, 3); - assert.equal(finished, 3); - done(); - }); + assert.equal(started, 3); + assert.equal(finished, 3); + assert.deepStrictEqual(results, [1, 2, 3]); }); }); From 57c60e19635761bc756d480a33194b20081d6909 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 27 Apr 2025 17:41:27 -0400 Subject: [PATCH 041/209] style: fix lint --- test/parallelLimit.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallelLimit.test.js b/test/parallelLimit.test.js index 2e1bab663c6..769fb70eff4 100644 --- a/test/parallelLimit.test.js +++ b/test/parallelLimit.test.js @@ -21,7 +21,7 @@ describe('parallelLimit', function() { return finished; }; - const results = await parallelLimit(params, async (param, index) => { + const results = await parallelLimit(params, async(param, index) => { if (index === 2) { assert.equal(started, 2); assert.ok(finished > 0); From a1733f8139caac45d685bc23b0a2d26fb49a6c27 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 28 Apr 2025 11:17:12 -0400 Subject: [PATCH 042/209] Update lib/browserDocument.js Co-authored-by: hasezoey --- lib/browserDocument.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/browserDocument.js b/lib/browserDocument.js index b49e1997ac8..6bd73318b9a 100644 --- a/lib/browserDocument.js +++ b/lib/browserDocument.js @@ -105,7 +105,7 @@ Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks( * ignore */ -Document.prototype._execDocumentPostHooks = function _execDocumentPostHooks(opName, error) { +Document.prototype._execDocumentPostHooks = async function _execDocumentPostHooks(opName, error) { return this._middleware.execPost(opName, this, [this], { error }); }; From 48b2c51ce4b354a01bcb2d7f2df81f68130d4723 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 28 Apr 2025 11:17:18 -0400 Subject: [PATCH 043/209] Update lib/document.js Co-authored-by: hasezoey --- lib/document.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/document.js b/lib/document.js index 346a435ec28..c978b6c2c2a 100644 --- a/lib/document.js +++ b/lib/document.js @@ -2910,7 +2910,7 @@ Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks( * ignore */ -Document.prototype._execDocumentPostHooks = function _execDocumentPostHooks(opName, error) { +Document.prototype._execDocumentPostHooks = async function _execDocumentPostHooks(opName, error) { return this.constructor._middleware.execPost(opName, this, [this], { error }); }; From c3a97af0d874a771732f83e4da0e328099746ba4 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 28 Apr 2025 11:17:25 -0400 Subject: [PATCH 044/209] Update lib/model.js Co-authored-by: hasezoey --- lib/model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model.js b/lib/model.js index db0d9e4b6ff..c48983daa35 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3473,7 +3473,7 @@ async function buildPreSavePromise(document, options) { return document.schema.s.hooks.execPre('save', document, [options]); } -function handleSuccessfulWrite(document) { +async function handleSuccessfulWrite(document) { if (document.$isNew) { _setIsNew(document, false); } From 754a825415a08589543152f0252669f96920883d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 28 Apr 2025 11:18:13 -0400 Subject: [PATCH 045/209] BREAKING CHANGE: enforce consistent async for custom methods with hooks re: #15317 --- docs/migrating_to_9.md | 32 ++++++++++++++++++++++++++++++++ lib/helpers/model/applyHooks.js | 14 ++------------ test/document.test.js | 25 ++++++++++++++++++++++--- 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 55de889183d..291345f4667 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -108,3 +108,35 @@ doc.mySubdoc[0].deleteOne(); // In Mongoose 9, you would need to either `save()` or `deleteOne()` on `doc` to trigger the subdocument `deleteOne` hook. await doc.save(); ``` + +## Hooks for custom methods no longer support callbacks + +Previously, you could use Mongoose middleware with custom methods that took callbacks. +In Mongoose 9, this is no longer supported. +If you want to use Mongoose middleware with a custom method, that custom method must be an async function or return a Promise. + +```javascript +const mySchema = new Schema({ + name: String +}); + +// This is an example of a custom method that uses callbacks. While this method by itself still works in Mongoose 9, +// Mongoose 9 no longer supports hooks for this method. +mySchema.methods.foo = async function(cb) { + return cb(null, this.name); +}; + +// This is no longer supported because `foo()` uses callbacks. +mySchema.pre('foo', function() { + console.log('foo pre hook'); +}); + +// The following is a custom method that uses async functions. The following works correctly in Mongoose 9: `pre('bar')` +// is executed when you call `bar()`. +mySchema.methods.bar = async function bar(arg) { + return arg; +}; +mySchema.pre('bar', async function bar() { + console.log('bar pre hook'); +}); +``` diff --git a/lib/helpers/model/applyHooks.js b/lib/helpers/model/applyHooks.js index 08e42417f3c..95980adf204 100644 --- a/lib/helpers/model/applyHooks.js +++ b/lib/helpers/model/applyHooks.js @@ -1,7 +1,6 @@ 'use strict'; const symbols = require('../../schema/symbols'); -const promiseOrCallback = require('../promiseOrCallback'); /*! * ignore @@ -129,17 +128,8 @@ function applyHooks(model, schema, options) { continue; } const originalMethod = objToDecorate[method]; - objToDecorate[method] = function() { - const args = Array.prototype.slice.call(arguments); - const cb = args.slice(-1).pop(); - const argsWithoutCallback = typeof cb === 'function' ? - args.slice(0, args.length - 1) : args; - return promiseOrCallback(cb, callback => { - return this[`$__${method}`].apply(this, - argsWithoutCallback.concat([callback])); - }, model.events); - }; - objToDecorate[`$__${method}`] = middleware. + objToDecorate[`$__${method}`] = objToDecorate[method]; + objToDecorate[method] = middleware. createWrapper(method, originalMethod, null, customMethodOptions); } } diff --git a/test/document.test.js b/test/document.test.js index 1bd0ae78c6e..42042acda92 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -4534,13 +4534,13 @@ describe('document', function() { assert.equal(p.children[0].grandchild.foo(), 'bar'); }); - it('hooks/middleware for custom methods (gh-6385) (gh-7456)', async function() { + it('hooks/middleware for custom methods (gh-6385) (gh-7456)', async function hooksForCustomMethods() { const mySchema = new Schema({ name: String }); - mySchema.methods.foo = function(cb) { - return cb(null, this.name); + mySchema.methods.foo = function() { + return Promise.resolve(this.name); }; mySchema.methods.bar = function() { return this.name; @@ -4548,6 +4548,10 @@ describe('document', function() { mySchema.methods.baz = function(arg) { return Promise.resolve(arg); }; + mySchema.methods.qux = async function qux() { + await new Promise(resolve => setTimeout(resolve, 5)); + throw new Error('error!'); + }; let preFoo = 0; let postFoo = 0; @@ -4567,6 +4571,15 @@ describe('document', function() { ++postBaz; }); + let preQux = 0; + let postQux = 0; + mySchema.pre('qux', function() { + ++preQux; + }); + mySchema.post('qux', function() { + ++postQux; + }); + const MyModel = db.model('Test', mySchema); @@ -4588,6 +4601,12 @@ describe('document', function() { assert.equal(await doc.baz('foobar'), 'foobar'); assert.equal(preBaz, 1); assert.equal(preBaz, 1); + + const err = await doc.qux().then(() => null, err => err); + assert.equal(err.message, 'error!'); + assert.ok(err.stack.includes('hooksForCustomMethods')); + assert.equal(preQux, 1); + assert.equal(postQux, 0); }); it('custom methods with promises (gh-6385)', async function() { From 26c6f1d399dddf405fc24e9bc9af7e5c80d619fc Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 28 Apr 2025 12:10:40 -0400 Subject: [PATCH 046/209] async stack traces for hooks for custom statics and methods re: #15317 --- docs/migrating_to_9.md | 41 +++++++-- lib/helpers/model/applyStaticHooks.js | 36 +------- lib/helpers/promiseOrCallback.js | 54 ------------ lib/utils.js | 7 -- test/helpers/promiseOrCallback.test.js | 110 ------------------------- test/model.middleware.test.js | 30 +++++++ 6 files changed, 63 insertions(+), 215 deletions(-) delete mode 100644 lib/helpers/promiseOrCallback.js delete mode 100644 test/helpers/promiseOrCallback.test.js diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 291345f4667..4727c348773 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -109,11 +109,11 @@ doc.mySubdoc[0].deleteOne(); await doc.save(); ``` -## Hooks for custom methods no longer support callbacks +## Hooks for custom methods and statics no longer support callbacks -Previously, you could use Mongoose middleware with custom methods that took callbacks. +Previously, you could use Mongoose middleware with custom methods and statics that took callbacks. In Mongoose 9, this is no longer supported. -If you want to use Mongoose middleware with a custom method, that custom method must be an async function or return a Promise. +If you want to use Mongoose middleware with a custom method or static, that custom method or static must be an async function or return a Promise. ```javascript const mySchema = new Schema({ @@ -125,18 +125,41 @@ const mySchema = new Schema({ mySchema.methods.foo = async function(cb) { return cb(null, this.name); }; +mySchema.statics.bar = async function(cb) { + return cb(null, 'bar'); +}; -// This is no longer supported because `foo()` uses callbacks. +// This is no longer supported because `foo()` and `bar()` use callbacks. mySchema.pre('foo', function() { console.log('foo pre hook'); }); +mySchema.pre('bar', function() { + console.log('bar pre hook'); +}); -// The following is a custom method that uses async functions. The following works correctly in Mongoose 9: `pre('bar')` -// is executed when you call `bar()`. -mySchema.methods.bar = async function bar(arg) { +// The following code has a custom method and a custom static that use async functions. +// The following works correctly in Mongoose 9: `pre('bar')` is executed when you call `bar()` and +// `pre('qux')` is executed when you call `qux()`. +mySchema.methods.baz = async function baz(arg) { return arg; }; -mySchema.pre('bar', async function bar() { - console.log('bar pre hook'); +mySchema.pre('baz', async function baz() { + console.log('baz pre hook'); +}); +mySchema.statics.qux = async function qux(arg) { + return arg; +}; +mySchema.pre('qux', async function qux() { + console.log('qux pre hook'); }); ``` + +## Removed `promiseOrCallback` + +Mongoose 9 removed the `promiseOrCallback` helper function. + +```javascript +const { promiseOrCallback } = require('mongoose'); + +promiseOrCallback; // undefined in Mongoose 9 +``` diff --git a/lib/helpers/model/applyStaticHooks.js b/lib/helpers/model/applyStaticHooks.js index 46c8faaacbf..eb0caaff420 100644 --- a/lib/helpers/model/applyStaticHooks.js +++ b/lib/helpers/model/applyStaticHooks.js @@ -1,6 +1,5 @@ 'use strict'; -const promiseOrCallback = require('../promiseOrCallback'); const { queryMiddlewareFunctions, aggregateMiddlewareFunctions, modelMiddlewareFunctions, documentMiddlewareFunctions } = require('../../constants'); const middlewareFunctions = Array.from( @@ -28,40 +27,7 @@ module.exports = function applyStaticHooks(model, hooks, statics) { if (hooks.hasHooks(key)) { const original = model[key]; - model[key] = function() { - const numArgs = arguments.length; - const lastArg = numArgs > 0 ? arguments[numArgs - 1] : null; - const cb = typeof lastArg === 'function' ? lastArg : null; - const args = Array.prototype.slice. - call(arguments, 0, cb == null ? numArgs : numArgs - 1); - return promiseOrCallback(cb, callback => { - hooks.execPre(key, model, args).then(() => onPreComplete(null), err => onPreComplete(err)); - - function onPreComplete(err) { - if (err != null) { - return callback(err); - } - - let postCalled = 0; - const ret = original.apply(model, args.concat(post)); - if (ret != null && typeof ret.then === 'function') { - ret.then(res => post(null, res), err => post(err)); - } - - function post(error, res) { - if (postCalled++ > 0) { - return; - } - - if (error != null) { - return callback(error); - } - - hooks.execPost(key, model, [res]).then(() => callback(null, res), err => callback(err)); - } - } - }, model.events); - }; + model[key] = hooks.createWrapper(key, original); } } }; diff --git a/lib/helpers/promiseOrCallback.js b/lib/helpers/promiseOrCallback.js deleted file mode 100644 index 952eecf4bf8..00000000000 --- a/lib/helpers/promiseOrCallback.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -const immediate = require('./immediate'); - -const emittedSymbol = Symbol('mongoose#emitted'); - -module.exports = function promiseOrCallback(callback, fn, ee, Promise) { - if (typeof callback === 'function') { - try { - return fn(function(error) { - if (error != null) { - if (ee != null && ee.listeners != null && ee.listeners('error').length > 0 && !error[emittedSymbol]) { - error[emittedSymbol] = true; - ee.emit('error', error); - } - try { - callback(error); - } catch (error) { - return immediate(() => { - throw error; - }); - } - return; - } - callback.apply(this, arguments); - }); - } catch (error) { - if (ee != null && ee.listeners != null && ee.listeners('error').length > 0 && !error[emittedSymbol]) { - error[emittedSymbol] = true; - ee.emit('error', error); - } - - return callback(error); - } - } - - Promise = Promise || global.Promise; - - return new Promise((resolve, reject) => { - fn(function(error, res) { - if (error != null) { - if (ee != null && ee.listeners != null && ee.listeners('error').length > 0 && !error[emittedSymbol]) { - error[emittedSymbol] = true; - ee.emit('error', error); - } - return reject(error); - } - if (arguments.length > 2) { - return resolve(Array.prototype.slice.call(arguments, 1)); - } - resolve(res); - }); - }); -}; diff --git a/lib/utils.js b/lib/utils.js index e0cc40fc94c..4a0132ea18f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -18,7 +18,6 @@ const isBsonType = require('./helpers/isBsonType'); const isPOJO = require('./helpers/isPOJO'); const getFunctionName = require('./helpers/getFunctionName'); const isMongooseObject = require('./helpers/isMongooseObject'); -const promiseOrCallback = require('./helpers/promiseOrCallback'); const schemaMerge = require('./helpers/schema/merge'); const specialProperties = require('./helpers/specialProperties'); const { trustedSymbol } = require('./helpers/query/trusted'); @@ -197,12 +196,6 @@ exports.last = function(arr) { return void 0; }; -/*! - * ignore - */ - -exports.promiseOrCallback = promiseOrCallback; - /*! * ignore */ diff --git a/test/helpers/promiseOrCallback.test.js b/test/helpers/promiseOrCallback.test.js deleted file mode 100644 index 2ce3f7a3d3c..00000000000 --- a/test/helpers/promiseOrCallback.test.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const promiseOrCallback = require('../../lib/helpers/promiseOrCallback'); - -describe('promiseOrCallback()', () => { - const myError = new Error('This is My Error'); - const myRes = 'My Res'; - const myOtherArg = 'My Other Arg'; - - describe('apply callback', () => { - it('without error', (done) => { - promiseOrCallback( - (error, arg, otherArg) => { - assert.equal(arg, myRes); - assert.equal(otherArg, myOtherArg); - assert.equal(error, undefined); - done(); - }, - (fn) => { fn(null, myRes, myOtherArg); } - ); - }); - - describe('with error', () => { - it('without event emitter', (done) => { - promiseOrCallback( - (error) => { - assert.equal(error, myError); - done(); - }, - (fn) => { fn(myError); } - ); - }); - - it('with event emitter', (done) => { - promiseOrCallback( - () => { }, - (fn) => { return fn(myError); }, - { - listeners: () => [1], - emit: (eventType, error) => { - assert.equal(eventType, 'error'); - assert.equal(error, myError); - done(); - } - } - ); - }); - }); - }); - - describe('chain promise', () => { - describe('without error', () => { - it('two args', (done) => { - const promise = promiseOrCallback( - null, - (fn) => { fn(null, myRes); } - ); - promise.then((res) => { - assert.equal(res, myRes); - done(); - }); - }); - - it('more args', (done) => { - const promise = promiseOrCallback( - null, - (fn) => { fn(null, myRes, myOtherArg); } - ); - promise.then((args) => { - assert.equal(args[0], myRes); - assert.equal(args[1], myOtherArg); - done(); - }); - }); - }); - - describe('with error', () => { - it('without event emitter', (done) => { - const promise = promiseOrCallback( - null, - (fn) => { fn(myError); } - ); - promise.catch((error) => { - assert.equal(error, myError); - done(); - }); - }); - - - it('with event emitter', (done) => { - const promise = promiseOrCallback( - null, - (fn) => { return fn(myError); }, - { - listeners: () => [1], - emit: (eventType, error) => { - assert.equal(eventType, 'error'); - assert.equal(error, myError); - } - } - ); - promise.catch((error) => { - assert.equal(error, myError); - done(); - }); - }); - }); - }); -}); diff --git a/test/model.middleware.test.js b/test/model.middleware.test.js index ac2d68924f2..8e28512963b 100644 --- a/test/model.middleware.test.js +++ b/test/model.middleware.test.js @@ -416,6 +416,36 @@ describe('model middleware', function() { assert.equal(postCalled, 1); }); + it('static hooks async stack traces (gh-15317) (gh-5982)', async function staticHookAsyncStackTrace() { + const schema = new Schema({ + name: String + }); + + schema.statics.findByName = function() { + return this.find({ otherProp: { $notAnOperator: 'value' } }); + }; + + let preCalled = 0; + schema.pre('findByName', function() { + ++preCalled; + }); + + let postCalled = 0; + schema.post('findByName', function() { + ++postCalled; + }); + + const Model = db.model('Test', schema); + + await Model.create({ name: 'foo' }); + + const err = await Model.findByName('foo').then(() => null, err => err); + assert.equal(err.name, 'MongoServerError'); + assert.ok(err.stack.includes('staticHookAsyncStackTrace')); + assert.equal(preCalled, 1); + assert.equal(postCalled, 0); + }); + it('deleteOne hooks (gh-7538)', async function() { const schema = new Schema({ name: String From 0b79373efb48a26f8773345e413c394c1aea6a61 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 28 Apr 2025 12:14:18 -0400 Subject: [PATCH 047/209] BREAKING CHANGE: require Node 18 --- .github/workflows/test.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f078f0cf240..4552c62ed72 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,7 @@ jobs: strategy: fail-fast: false matrix: - node: [16, 18, 20, 22] + node: [18, 20, 22] os: [ubuntu-22.04, ubuntu-24.04] mongodb: [6.0.15, 7.0.12, 8.0.0] include: diff --git a/package.json b/package.json index b9ba66492a0..178fb05714a 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "main": "./index.js", "types": "./types/index.d.ts", "engines": { - "node": ">=16.20.1" + "node": ">=18.0.0" }, "bugs": { "url": "https://github.com/Automattic/mongoose/issues/new" From e32a1bd1ca0112c3f07264902d65f72512e849d5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 28 Apr 2025 13:01:52 -0400 Subject: [PATCH 048/209] add note re: mongoosejs/kareem#39 --- docs/migrating_to_9.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 4727c348773..5164e4ccd5a 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -163,3 +163,36 @@ const { promiseOrCallback } = require('mongoose'); promiseOrCallback; // undefined in Mongoose 9 ``` + +## In isAsync middleware `next()` errors take priority over `done()` errors + +Due to Mongoose middleware now relying on promises and async/await, `next()` errors take priority over `done()` errors. +If you use `isAsync` middleware, any errors in `next()` will be thrown first, and `done()` errors will only be thrown if there are no `next()` errors. + +```javascript +const schema = new Schema({}); + +schema.pre('save', true, function(next, done) { + execed.first = true; + setTimeout( + function() { + done(new Error('first done() error')); + }, + 5); + + next(); +}); + +schema.pre('save', true, function(next, done) { + execed.second = true; + setTimeout( + function() { + next(new Error('second next() error')); + done(new Error('second done() error')); + }, + 25); +}); + +// In Mongoose 8, with the above middleware, `save()` would error with 'first done() error' +// In Mongoose 9, with the above middleware, `save()` will error with 'second next() error' +``` From 1d0beb62b9579570c79d3bead75d5a355cadb940 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 28 Apr 2025 14:23:46 -0400 Subject: [PATCH 049/209] Update docs/migrating_to_9.md Co-authored-by: hasezoey --- docs/migrating_to_9.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 5164e4ccd5a..4816d80bc71 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -164,7 +164,7 @@ const { promiseOrCallback } = require('mongoose'); promiseOrCallback; // undefined in Mongoose 9 ``` -## In isAsync middleware `next()` errors take priority over `done()` errors +## In `isAsync` middleware `next()` errors take priority over `done()` errors Due to Mongoose middleware now relying on promises and async/await, `next()` errors take priority over `done()` errors. If you use `isAsync` middleware, any errors in `next()` will be thrown first, and `done()` errors will only be thrown if there are no `next()` errors. From 53896104522ed4cc04f8dbcd526b763da7751f86 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 28 Apr 2025 14:25:35 -0400 Subject: [PATCH 050/209] docs: add note about version support to changelog --- docs/migrating_to_9.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index ec9a4425835..26a34b50df9 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -203,3 +203,7 @@ In Mongoose 8, Mongoose queries store an `_executionStack` property that stores This behavior can cause performance issues with bundlers and source maps. `skipOriginalStackTraces` was added to work around this behavior. In Mongoose 9, this option is no longer necessary because Mongoose no longer stores the original stack trace. + +## Node.js version support + +Mongoose 9 requires Node.js 18 or higher. From fb983486610e062e47ae98848abe02b4c55b4129 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 28 Apr 2025 14:27:23 -0400 Subject: [PATCH 051/209] style: fix lint --- test/query.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/query.test.js b/test/query.test.js index 63ae854f61d..43aa4ad35f1 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -4411,7 +4411,7 @@ describe('Query', function() { }); }); - it('throws an error if calling find(null), findOne(null), updateOne(null, update), etc. (gh-14948)', async function () { + it('throws an error if calling find(null), findOne(null), updateOne(null, update), etc. (gh-14948)', async function() { const userSchema = new Schema({ name: String }); From ae143f497a6dd7e9be431124c2651aa3eabae0ac Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 28 Apr 2025 16:39:42 -0400 Subject: [PATCH 052/209] BREAKING CHANGE: make UUID schema type return bson UUIDs --- docs/migrating_to_9.md | 29 ++++++++++ lib/cast/uuid.js | 53 ++---------------- lib/schema/uuid.js | 19 ------- test/model.populate.test.js | 10 ++-- test/schema.uuid.test.js | 105 +++++++++++++++++++++++++++++------- test/types/schema.test.ts | 14 ++--- types/inferschematype.d.ts | 8 +-- 7 files changed, 136 insertions(+), 102 deletions(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 26a34b50df9..867508f4a18 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -207,3 +207,32 @@ In Mongoose 9, this option is no longer necessary because Mongoose no longer sto ## Node.js version support Mongoose 9 requires Node.js 18 or higher. + +## UUID's are now MongoDB UUID objects + +Mongoose 9 now returns UUID objects as instances of `bson.UUID`. In Mongoose 8, UUIDs were Mongoose Buffers that were converted to strings via a getter. + +```javascript +const schema = new Schema({ uuid: 'UUID' }); +const TestModel = mongoose.model('Test', schema); + +const test = new TestModel({ uuid: new bson.UUID() }); +await test.save(); + +test.uuid; // string in Mongoose 8, bson.UUID instance in Mongoose 9 +``` + +If you want to convert UUIDs to strings via a getter by default, you can use `mongoose.Schema.Types.UUID.get()`: + +```javascript +// Configure all UUIDs to have a getter which converts the UUID to a string +mongoose.Schema.Types.UUID.get(v => v == null ? v : v.toString()); + +const schema = new Schema({ uuid: 'UUID' }); +const TestModel = mongoose.model('Test', schema); + +const test = new TestModel({ uuid: new bson.UUID() }); +await test.save(); + +test.uuid; // string +``` diff --git a/lib/cast/uuid.js b/lib/cast/uuid.js index 6e296bf3e24..480f9e4e056 100644 --- a/lib/cast/uuid.js +++ b/lib/cast/uuid.js @@ -1,43 +1,31 @@ 'use strict'; -const MongooseBuffer = require('../types/buffer'); +const UUID = require('bson').UUID; const UUID_FORMAT = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i; -const Binary = MongooseBuffer.Binary; module.exports = function castUUID(value) { if (value == null) { return value; } - function newBuffer(initbuff) { - const buff = new MongooseBuffer(initbuff); - buff._subtype = 4; - return buff; + if (value instanceof UUID) { + return value; } - if (typeof value === 'string') { if (UUID_FORMAT.test(value)) { - return stringToBinary(value); + return new UUID(value); } else { throw new Error(`"${value}" is not a valid UUID string`); } } - if (Buffer.isBuffer(value)) { - return newBuffer(value); - } - - if (value instanceof Binary) { - return newBuffer(value.value(true)); - } - // Re: gh-647 and gh-3030, we're ok with casting using `toString()` // **unless** its the default Object.toString, because "[object Object]" // doesn't really qualify as useful data if (value.toString && value.toString !== Object.prototype.toString) { if (UUID_FORMAT.test(value.toString())) { - return stringToBinary(value.toString()); + return new UUID(value.toString()); } } @@ -45,34 +33,3 @@ module.exports = function castUUID(value) { }; module.exports.UUID_FORMAT = UUID_FORMAT; - -/** - * Helper function to convert the input hex-string to a buffer - * @param {String} hex The hex string to convert - * @returns {Buffer} The hex as buffer - * @api private - */ - -function hex2buffer(hex) { - // use buffer built-in function to convert from hex-string to buffer - const buff = hex != null && Buffer.from(hex, 'hex'); - return buff; -} - -/** - * Convert a String to Binary - * @param {String} uuidStr The value to process - * @returns {MongooseBuffer} The binary to store - * @api private - */ - -function stringToBinary(uuidStr) { - // Protect against undefined & throwing err - if (typeof uuidStr !== 'string') uuidStr = ''; - const hex = uuidStr.replace(/[{}-]/g, ''); // remove extra characters - const bytes = hex2buffer(hex); - const buff = new MongooseBuffer(bytes); - buff._subtype = 4; - - return buff; -} diff --git a/lib/schema/uuid.js b/lib/schema/uuid.js index 94fb6cbe682..c79350d7a3a 100644 --- a/lib/schema/uuid.js +++ b/lib/schema/uuid.js @@ -43,21 +43,6 @@ function binaryToString(uuidBin) { function SchemaUUID(key, options) { SchemaType.call(this, key, options, 'UUID'); - this.getters.push(function(value) { - // For populated - if (value != null && value.$__ != null) { - return value; - } - if (Buffer.isBuffer(value)) { - return binaryToString(value); - } else if (value instanceof Binary) { - return binaryToString(value.buffer); - } else if (utils.isPOJO(value) && value.type === 'Buffer' && Array.isArray(value.data)) { - // Cloned buffers look like `{ type: 'Buffer', data: [5, 224, ...] }` - return binaryToString(Buffer.from(value.data)); - } - return value; - }); } /** @@ -249,11 +234,7 @@ SchemaUUID.prototype.$conditionalHandlers = { $bitsAllSet: handleBitwiseOperator, $bitsAnySet: handleBitwiseOperator, $all: handleArray, - $gt: handleSingle, - $gte: handleSingle, $in: handleArray, - $lt: handleSingle, - $lte: handleSingle, $ne: handleSingle, $nin: handleArray }; diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 1e14b75acb0..61e0e652fe8 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -11343,7 +11343,7 @@ describe('model: populate:', function() { assert.equal(fromDb.children[2].toHexString(), newChild._id.toHexString()); }); - it('handles converting uuid documents to strings when calling toObject() (gh-14869)', async function() { + it('handles populating uuids (gh-14869)', async function() { const nodeSchema = new Schema({ _id: { type: 'UUID' }, name: 'String' }); const rootSchema = new Schema({ _id: { type: 'UUID' }, @@ -11370,14 +11370,14 @@ describe('model: populate:', function() { const foundRoot = await Root.findById(root._id).populate('node'); let doc = foundRoot.toJSON({ getters: true }); - assert.strictEqual(doc._id, '05c7953e-c6e9-4c2f-8328-fe2de7df560d'); + assert.strictEqual(doc._id.toString(), '05c7953e-c6e9-4c2f-8328-fe2de7df560d'); assert.strictEqual(doc.node.length, 1); - assert.strictEqual(doc.node[0]._id, '65c7953e-c6e9-4c2f-8328-fe2de7df560d'); + assert.strictEqual(doc.node[0]._id.toString(), '65c7953e-c6e9-4c2f-8328-fe2de7df560d'); doc = foundRoot.toObject({ getters: true }); - assert.strictEqual(doc._id, '05c7953e-c6e9-4c2f-8328-fe2de7df560d'); + assert.strictEqual(doc._id.toString(), '05c7953e-c6e9-4c2f-8328-fe2de7df560d'); assert.strictEqual(doc.node.length, 1); - assert.strictEqual(doc.node[0]._id, '65c7953e-c6e9-4c2f-8328-fe2de7df560d'); + assert.strictEqual(doc.node[0]._id.toString(), '65c7953e-c6e9-4c2f-8328-fe2de7df560d'); }); it('avoids repopulating if forceRepopulate is disabled (gh-14979)', async function() { diff --git a/test/schema.uuid.test.js b/test/schema.uuid.test.js index e93538f78cf..e95424dc137 100644 --- a/test/schema.uuid.test.js +++ b/test/schema.uuid.test.js @@ -36,8 +36,8 @@ describe('SchemaUUID', function() { it('basic functionality should work', async function() { const doc = new Model({ x: '09190f70-3d30-11e5-8814-0f4df9a59c41' }); assert.ifError(doc.validateSync()); - assert.ok(typeof doc.x === 'string'); - assert.strictEqual(doc.x, '09190f70-3d30-11e5-8814-0f4df9a59c41'); + assert.ok(doc.x instanceof mongoose.Types.UUID); + assert.strictEqual(doc.x.toString(), '09190f70-3d30-11e5-8814-0f4df9a59c41'); await doc.save(); const query = Model.findOne({ x: '09190f70-3d30-11e5-8814-0f4df9a59c41' }); @@ -45,8 +45,8 @@ describe('SchemaUUID', function() { const res = await query; assert.ifError(res.validateSync()); - assert.ok(typeof res.x === 'string'); - assert.strictEqual(res.x, '09190f70-3d30-11e5-8814-0f4df9a59c41'); + assert.ok(res.x instanceof mongoose.Types.UUID); + assert.strictEqual(res.x.toString(), '09190f70-3d30-11e5-8814-0f4df9a59c41'); // check that the data is actually a buffer in the database with the correct subtype const col = db.client.db(db.name).collection(Model.collection.name); @@ -54,6 +54,11 @@ describe('SchemaUUID', function() { assert.ok(rawDoc); assert.ok(rawDoc.x instanceof bson.Binary); assert.strictEqual(rawDoc.x.sub_type, 4); + + const rawDoc2 = await col.findOne({ x: new bson.UUID('09190f70-3d30-11e5-8814-0f4df9a59c41') }); + assert.ok(rawDoc2); + assert.ok(rawDoc2.x instanceof bson.UUID); + assert.strictEqual(rawDoc2.x.sub_type, 4); }); it('should throw error in case of invalid string', function() { @@ -80,9 +85,9 @@ describe('SchemaUUID', function() { assert.strictEqual(foundDocIn.length, 1); assert.ok(foundDocIn[0].y); assert.strictEqual(foundDocIn[0].y.length, 3); - assert.strictEqual(foundDocIn[0].y[0], 'f8010af3-bc2c-45e6-85c6-caa30c4a7d34'); - assert.strictEqual(foundDocIn[0].y[1], 'c6f59133-4f84-45a8-bc1d-8f172803e4fe'); - assert.strictEqual(foundDocIn[0].y[2], 'df1309e0-58c5-427a-b22f-6c0fc445ccc0'); + assert.strictEqual(foundDocIn[0].y[0].toString(), 'f8010af3-bc2c-45e6-85c6-caa30c4a7d34'); + assert.strictEqual(foundDocIn[0].y[1].toString(), 'c6f59133-4f84-45a8-bc1d-8f172803e4fe'); + assert.strictEqual(foundDocIn[0].y[2].toString(), 'df1309e0-58c5-427a-b22f-6c0fc445ccc0'); // test $nin const foundDocNin = await Model.find({ y: { $nin: ['f8010af3-bc2c-45e6-85c6-caa30c4a7d34'] } }); @@ -90,9 +95,9 @@ describe('SchemaUUID', function() { assert.strictEqual(foundDocNin.length, 1); assert.ok(foundDocNin[0].y); assert.strictEqual(foundDocNin[0].y.length, 3); - assert.strictEqual(foundDocNin[0].y[0], '13d51406-cd06-4fc2-93d1-4fad9b3eecd7'); - assert.strictEqual(foundDocNin[0].y[1], 'f004416b-e02a-4212-ac77-2d3fcf04898b'); - assert.strictEqual(foundDocNin[0].y[2], '5b544b71-8988-422b-a4df-bf691939fe4e'); + assert.strictEqual(foundDocNin[0].y[0].toString(), '13d51406-cd06-4fc2-93d1-4fad9b3eecd7'); + assert.strictEqual(foundDocNin[0].y[1].toString(), 'f004416b-e02a-4212-ac77-2d3fcf04898b'); + assert.strictEqual(foundDocNin[0].y[2].toString(), '5b544b71-8988-422b-a4df-bf691939fe4e'); // test for $all const foundDocAll = await Model.find({ y: { $all: ['13d51406-cd06-4fc2-93d1-4fad9b3eecd7', 'f004416b-e02a-4212-ac77-2d3fcf04898b'] } }); @@ -100,9 +105,9 @@ describe('SchemaUUID', function() { assert.strictEqual(foundDocAll.length, 1); assert.ok(foundDocAll[0].y); assert.strictEqual(foundDocAll[0].y.length, 3); - assert.strictEqual(foundDocAll[0].y[0], '13d51406-cd06-4fc2-93d1-4fad9b3eecd7'); - assert.strictEqual(foundDocAll[0].y[1], 'f004416b-e02a-4212-ac77-2d3fcf04898b'); - assert.strictEqual(foundDocAll[0].y[2], '5b544b71-8988-422b-a4df-bf691939fe4e'); + assert.strictEqual(foundDocAll[0].y[0].toString(), '13d51406-cd06-4fc2-93d1-4fad9b3eecd7'); + assert.strictEqual(foundDocAll[0].y[1].toString(), 'f004416b-e02a-4212-ac77-2d3fcf04898b'); + assert.strictEqual(foundDocAll[0].y[2].toString(), '5b544b71-8988-422b-a4df-bf691939fe4e'); }); it('should not convert to string nullish UUIDs (gh-13032)', async function() { @@ -152,6 +157,21 @@ describe('SchemaUUID', function() { await pop.save(); }); + it('works with lean', async function() { + const userSchema = new mongoose.Schema({ + _id: { type: 'UUID' }, + name: String + }); + const User = db.model('User', userSchema); + + const u1 = await User.create({ _id: randomUUID(), name: 'admin' }); + + const lean = await User.findById(u1._id).lean().orFail(); + assert.equal(lean.name, 'admin'); + assert.ok(lean._id instanceof mongoose.Types.UUID); + assert.equal(lean._id.toString(), u1._id.toString()); + }); + it('handles built-in UUID type (gh-13103)', async function() { const schema = new Schema({ _id: { @@ -165,12 +185,12 @@ describe('SchemaUUID', function() { const uuid = new mongoose.Types.UUID(); let { _id } = await Test.create({ _id: uuid }); assert.ok(_id); - assert.equal(typeof _id, 'string'); + assert.ok(_id instanceof mongoose.Types.UUID); assert.equal(_id, uuid.toString()); ({ _id } = await Test.findById(uuid)); assert.ok(_id); - assert.equal(typeof _id, 'string'); + assert.ok(_id instanceof mongoose.Types.UUID); assert.equal(_id, uuid.toString()); }); @@ -202,11 +222,56 @@ describe('SchemaUUID', function() { const exists = await Test.findOne({ 'doc_map.role_1': { $type: 'binData' } }); assert.ok(exists); - assert.equal(typeof user.get('doc_map.role_1'), 'string'); + assert.ok(user.get('doc_map.role_1') instanceof mongoose.Types.UUID); }); - // the following are TODOs based on SchemaUUID.prototype.$conditionalHandlers which are not tested yet - it('should work with $bits* operators'); - it('should work with $all operator'); - it('should work with $lt, $lte, $gt, $gte operators'); + it('should work with $bits* operators', async function() { + const schema = new Schema({ + uuid: mongoose.Schema.Types.UUID + }); + db.deleteModel(/Test/); + const Test = db.model('Test', schema); + + const uuid = new mongoose.Types.UUID('ff' + '0'.repeat(30)); + await Test.create({ uuid }); + + let doc = await Test.findOne({ uuid: { $bitsAllSet: [0, 4] } }); + assert.ok(doc); + doc = await Test.findOne({ uuid: { $bitsAllSet: 2 ** 15 } }); + assert.ok(!doc); + + doc = await Test.findOne({ uuid: { $bitsAnySet: 3 } }); + assert.ok(doc); + doc = await Test.findOne({ uuid: { $bitsAnySet: [8] } }); + assert.ok(!doc); + + doc = await Test.findOne({ uuid: { $bitsAnyClear: [0, 32] } }); + assert.ok(doc); + doc = await Test.findOne({ uuid: { $bitsAnyClear: 7 } }); + assert.ok(!doc); + + doc = await Test.findOne({ uuid: { $bitsAllClear: [16, 17, 18] } }); + assert.ok(doc); + doc = await Test.findOne({ uuid: { $bitsAllClear: 3 } }); + assert.ok(!doc); + }); + + it('should work with $all operator', async function() { + const schema = new Schema({ + uuids: [mongoose.Schema.Types.UUID] + }); + db.deleteModel(/Test/); + const Test = db.model('Test', schema); + + const uuid1 = new mongoose.Types.UUID(); + const uuid2 = new mongoose.Types.UUID(); + const uuid3 = new mongoose.Types.UUID(); + await Test.create({ uuids: [uuid1, uuid2] }); + + let doc = await Test.findOne({ uuids: { $all: [uuid1, uuid2] } }); + assert.ok(doc); + + doc = await Test.findOne({ uuids: { $all: [uuid1, uuid3] } }); + assert.ok(!doc); + }); }); diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index d96018d6386..dd3b00053a4 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -24,7 +24,7 @@ import { ValidateOpts, BufferToBinary } from 'mongoose'; -import { Binary } from 'mongodb'; +import { Binary, UUID } from 'mongodb'; import { IsPathRequired } from '../../types/inferschematype'; import { expectType, expectError, expectAssignable } from 'tsd'; import { ObtainDocumentPathType, ResolvePathType } from '../../types/inferschematype'; @@ -120,7 +120,7 @@ expectError[0]>({ tile: false }); // tes // Using `SchemaDefinition` interface IProfile { age: number; -} +}Buffer const ProfileSchemaDef: SchemaDefinition = { age: Number }; export const ProfileSchema = new Schema>(ProfileSchemaDef); @@ -910,23 +910,23 @@ async function gh12593() { const testSchema = new Schema({ x: { type: Schema.Types.UUID } }); type Example = InferSchemaType; - expectType<{ x?: Buffer | null }>({} as Example); + expectType<{ x?: UUID | null }>({} as Example); const Test = model('Test', testSchema); const doc = await Test.findOne({ x: '4709e6d9-61fd-435e-b594-d748eb196d8f' }).orFail(); - expectType(doc.x); + expectType(doc.x); const doc2 = new Test({ x: '4709e6d9-61fd-435e-b594-d748eb196d8f' }); - expectType(doc2.x); + expectType(doc2.x); const doc3 = await Test.findOne({}).orFail().lean(); - expectType(doc3.x); + expectType(doc3.x); const arrSchema = new Schema({ arr: [{ type: Schema.Types.UUID }] }); type ExampleArr = InferSchemaType; - expectType<{ arr: Buffer[] }>({} as ExampleArr); + expectType<{ arr: UUID[] }>({} as ExampleArr); } function gh12562() { diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index dac99d09d6c..68151b96910 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -245,7 +245,9 @@ type IsSchemaTypeFromBuiltinClass = T extends (typeof String) ? false : T extends Buffer ? true - : false; + : T extends Types.UUID + ? true + : false; /** * @summary Resolve path type by returning the corresponding type. @@ -311,9 +313,9 @@ type ResolvePathType extends true ? bigint : IfEquals extends true ? bigint : PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint : - PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? Buffer : + PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? Types.UUID : PathValueType extends 'double' | 'Double' | typeof Schema.Types.Double ? Types.Double : - IfEquals extends true ? Buffer : + IfEquals extends true ? Types.UUID : PathValueType extends MapConstructor | 'Map' ? Map> : IfEquals extends true ? Map> : PathValueType extends ArrayConstructor ? any[] : From f26d218002b4b8549760e236d9b6c1ceceb7b4a2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 28 Apr 2025 16:52:37 -0400 Subject: [PATCH 053/209] types: convert UUID to string for JSON serialization --- docs/migrating_to_9.md | 2 ++ test/types/schema.test.ts | 6 ++++-- types/index.d.ts | 24 +++++++++++++++++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 867508f4a18..a2b508cb93d 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -222,6 +222,8 @@ await test.save(); test.uuid; // string in Mongoose 8, bson.UUID instance in Mongoose 9 ``` +With this change, UUIDs will be represented in hex string format in JSON, even if `getters: true` is not set. + If you want to convert UUIDs to strings via a getter by default, you can use `mongoose.Schema.Types.UUID.get()`: ```javascript diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index dd3b00053a4..771da67b34d 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1722,7 +1722,8 @@ async function gh14451() { myMap: { type: Map, of: String - } + }, + myUUID: 'UUID' }); const Test = model('Test', exampleSchema); @@ -1736,7 +1737,8 @@ async function gh14451() { subdocProp?: string | undefined | null } | null, docArr: { nums: number[], times: string[] }[], - myMap?: Record | null | undefined + myMap?: Record | null | undefined, + myUUID?: string | null | undefined }>({} as TestJSON); } diff --git a/types/index.d.ts b/types/index.d.ts index deea5f75992..172b06dd0db 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -32,6 +32,7 @@ declare module 'mongoose' { import events = require('events'); import mongodb = require('mongodb'); import mongoose = require('mongoose'); + import bson = require('bson'); export type Mongoose = typeof mongoose; @@ -766,6 +767,25 @@ declare module 'mongoose' { : BufferToBinary; } : T; + /** + * Converts any Buffer properties into { type: 'buffer', data: [1, 2, 3] } format for JSON serialization + */ + export type UUIDToJSON = T extends bson.UUID + ? string + : T extends Document + ? T + : T extends TreatAsPrimitives + ? T + : T extends Record ? { + [K in keyof T]: T[K] extends bson.UUID + ? string + : T[K] extends Types.DocumentArray + ? Types.DocumentArray> + : T[K] extends Types.Subdocument + ? HydratedSingleSubdocument + : UUIDToJSON; + } : T; + /** * Converts any ObjectId properties into strings for JSON serialization */ @@ -825,7 +845,9 @@ declare module 'mongoose' { FlattenMaps< BufferToJSON< ObjectIdToString< - DateToString + UUIDToJSON< + DateToString + > > > > From 12b96e74941965e6a6acd8075458427a0217fa7e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 28 Apr 2025 16:54:16 -0400 Subject: [PATCH 054/209] style: fix lint --- lib/schema/uuid.js | 2 -- test/types/schema.test.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/schema/uuid.js b/lib/schema/uuid.js index c79350d7a3a..0dad255cc46 100644 --- a/lib/schema/uuid.js +++ b/lib/schema/uuid.js @@ -4,7 +4,6 @@ 'use strict'; -const MongooseBuffer = require('../types/buffer'); const SchemaType = require('../schemaType'); const CastError = SchemaType.CastError; const castUUID = require('../cast/uuid'); @@ -13,7 +12,6 @@ const utils = require('../utils'); const handleBitwiseOperator = require('./operators/bitwise'); const UUID_FORMAT = castUUID.UUID_FORMAT; -const Binary = MongooseBuffer.Binary; /** * Convert binary to a uuid string diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 771da67b34d..675e3bc8e31 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -120,7 +120,7 @@ expectError[0]>({ tile: false }); // tes // Using `SchemaDefinition` interface IProfile { age: number; -}Buffer +} const ProfileSchemaDef: SchemaDefinition = { age: Number }; export const ProfileSchema = new Schema>(ProfileSchemaDef); From 9b9ba10905b94339c290a67823555a8466b7b830 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Tue, 29 Apr 2025 13:18:32 +0200 Subject: [PATCH 055/209] deps(kareem): change dependency notation to be HTTPS and properly "git" for yarn v1 and maybe other package managers --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0b4c50f3160..756e6dfe84d 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "license": "MIT", "dependencies": { "bson": "^6.10.3", - "kareem": "git@github.com:mongoosejs/kareem.git#vkarpov15/v3", + "kareem": "git+https://github.com/mongoosejs/kareem.git#vkarpov15/v3", "mongodb": "~6.16.0", "mpath": "0.9.0", "mquery": "5.0.0", From 72aadafc73c5580d48eda369c060c27d68fe70d5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 29 Apr 2025 10:08:22 -0400 Subject: [PATCH 056/209] Update types/index.d.ts Co-authored-by: hasezoey --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 172b06dd0db..95d57f66597 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -768,7 +768,7 @@ declare module 'mongoose' { } : T; /** - * Converts any Buffer properties into { type: 'buffer', data: [1, 2, 3] } format for JSON serialization + * Converts any Buffer properties into "{ type: 'buffer', data: [1, 2, 3] }" format for JSON serialization */ export type UUIDToJSON = T extends bson.UUID ? string From 03cc91f680ceaf407079ac34f9f7e9ec68c321a7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 29 Apr 2025 11:04:03 -0400 Subject: [PATCH 057/209] fix: remove unnecessary workaround for #15315 now that #15378 is fixed --- lib/helpers/populate/assignRawDocsToIdStructure.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/helpers/populate/assignRawDocsToIdStructure.js b/lib/helpers/populate/assignRawDocsToIdStructure.js index 67fa17f4c53..765d69f06af 100644 --- a/lib/helpers/populate/assignRawDocsToIdStructure.js +++ b/lib/helpers/populate/assignRawDocsToIdStructure.js @@ -78,12 +78,7 @@ function assignRawDocsToIdStructure(rawIds, resultDocs, resultOrder, options, re continue; } - if (id?.constructor?.name === 'Binary' && id.sub_type === 4 && typeof id.toUUID === 'function') { - // Workaround for gh-15315 because Mongoose UUIDs don't use BSON UUIDs yet. - sid = String(id.toUUID()); - } else { - sid = String(id); - } + sid = String(id); doc = resultDocs[sid]; // If user wants separate copies of same doc, use this option if (options.clone && doc != null) { From 1e8aee5adbbe1f8e111ca5d832872bf7df0610e9 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 29 Apr 2025 11:47:31 -0400 Subject: [PATCH 058/209] fix(SchemaType): add missing continue Fix #15380 --- lib/schemaType.js | 1 + test/schema.string.test.js | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/lib/schemaType.js b/lib/schemaType.js index 688a433f923..31344925ca9 100644 --- a/lib/schemaType.js +++ b/lib/schemaType.js @@ -1348,6 +1348,7 @@ SchemaType.prototype.doValidate = async function doValidate(value, scope, option err[validatorErrorSymbol] = true; throw err; } + continue; } else if (typeof validator !== 'function') { continue; } diff --git a/test/schema.string.test.js b/test/schema.string.test.js index 16d58aade98..0e9448e5f26 100644 --- a/test/schema.string.test.js +++ b/test/schema.string.test.js @@ -21,4 +21,13 @@ describe('SchemaString', function() { assert.ifError(doc.validateSync()); assert.ifError(doc.validateSync()); }); + + it('regex validator works with validate() (gh-15380)', async function() { + const schema = new Schema({ x: { type: String, validate: /abc/g } }); + mongoose.deleteModel(/Test/); + M = mongoose.model('Test', schema); + + const doc = new M({ x: 'abc' }); + await doc.validate(); + }); }); From f55d676186876168fbdd1944b9cb958b6b8ca895 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 29 Apr 2025 16:34:36 -0400 Subject: [PATCH 059/209] BREAKING CHANGE: remove browser build, move to @mongoosejs/browser instead Fix #15296 --- browser.js | 8 -- docs/nextjs.md | 1 - lib/browser.js | 143 ------------------------------ lib/browserDocument.js | 117 ------------------------ lib/document.js | 14 +-- lib/documentProvider.js | 30 ------- lib/drivers/browser/binary.js | 14 --- lib/drivers/browser/decimal128.js | 7 -- lib/drivers/browser/index.js | 13 --- lib/drivers/browser/objectid.js | 29 ------ lib/helpers/model/applyHooks.js | 24 +---- lib/mongoose.js | 12 +-- lib/schema.js | 28 ++++++ package.json | 13 +-- scripts/build-browser.js | 18 ---- test/browser.test.js | 88 ------------------ test/deno_mocha.js | 2 +- test/docs/lean.test.js | 3 + test/files/index.html | 9 -- test/files/sample.js | 7 -- webpack.config.js | 59 ------------ 21 files changed, 44 insertions(+), 595 deletions(-) delete mode 100644 browser.js delete mode 100644 lib/browser.js delete mode 100644 lib/browserDocument.js delete mode 100644 lib/documentProvider.js delete mode 100644 lib/drivers/browser/binary.js delete mode 100644 lib/drivers/browser/decimal128.js delete mode 100644 lib/drivers/browser/index.js delete mode 100644 lib/drivers/browser/objectid.js delete mode 100644 scripts/build-browser.js delete mode 100644 test/browser.test.js delete mode 100644 test/files/index.html delete mode 100644 test/files/sample.js delete mode 100644 webpack.config.js diff --git a/browser.js b/browser.js deleted file mode 100644 index 4cf822804e8..00000000000 --- a/browser.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Export lib/mongoose - * - */ - -'use strict'; - -module.exports = require('./lib/browser'); diff --git a/docs/nextjs.md b/docs/nextjs.md index 673587c6829..94d0071f472 100644 --- a/docs/nextjs.md +++ b/docs/nextjs.md @@ -34,5 +34,4 @@ And Next.js forces ESM mode. ## Next.js Edge Runtime Mongoose does **not** currently support [Next.js Edge Runtime](https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes#edge-runtime). -While you can import Mongoose in Edge Runtime, you'll get [Mongoose's browser library](browser.html). There is no way for Mongoose to connect to MongoDB in Edge Runtime, because [Edge Runtime currently doesn't support Node.js `net` API](https://edge-runtime.vercel.app/features/available-apis#unsupported-apis), which is what the MongoDB Node Driver uses to connect to MongoDB. diff --git a/lib/browser.js b/lib/browser.js deleted file mode 100644 index 5369dedf44c..00000000000 --- a/lib/browser.js +++ /dev/null @@ -1,143 +0,0 @@ -/* eslint-env browser */ - -'use strict'; - -require('./driver').set(require('./drivers/browser')); - -const DocumentProvider = require('./documentProvider.js'); -const applyHooks = require('./helpers/model/applyHooks.js'); - -DocumentProvider.setBrowser(true); - -/** - * The [MongooseError](https://mongoosejs.com/docs/api/error.html#Error()) constructor. - * - * @method Error - * @api public - */ - -exports.Error = require('./error/index'); - -/** - * The Mongoose [Schema](https://mongoosejs.com/docs/api/schema.html#Schema()) constructor - * - * #### Example: - * - * const mongoose = require('mongoose'); - * const Schema = mongoose.Schema; - * const CatSchema = new Schema(..); - * - * @method Schema - * @api public - */ - -exports.Schema = require('./schema'); - -/** - * The various Mongoose Types. - * - * #### Example: - * - * const mongoose = require('mongoose'); - * const array = mongoose.Types.Array; - * - * #### Types: - * - * - [Array](https://mongoosejs.com/docs/schematypes.html#arrays) - * - [Buffer](https://mongoosejs.com/docs/schematypes.html#buffers) - * - [Embedded](https://mongoosejs.com/docs/schematypes.html#schemas) - * - [DocumentArray](https://mongoosejs.com/docs/api/documentarraypath.html) - * - [Decimal128](https://mongoosejs.com/docs/api/decimal128.html#Decimal128()) - * - [ObjectId](https://mongoosejs.com/docs/schematypes.html#objectids) - * - [Map](https://mongoosejs.com/docs/schematypes.html#maps) - * - [Subdocument](https://mongoosejs.com/docs/schematypes.html#schemas) - * - * Using this exposed access to the `ObjectId` type, we can construct ids on demand. - * - * const ObjectId = mongoose.Types.ObjectId; - * const id1 = new ObjectId; - * - * @property Types - * @api public - */ -exports.Types = require('./types'); - -/** - * The Mongoose [VirtualType](https://mongoosejs.com/docs/api/virtualtype.html#VirtualType()) constructor - * - * @method VirtualType - * @api public - */ -exports.VirtualType = require('./virtualType'); - -/** - * The various Mongoose SchemaTypes. - * - * #### Note: - * - * _Alias of mongoose.Schema.Types for backwards compatibility._ - * - * @property SchemaTypes - * @see Schema.SchemaTypes https://mongoosejs.com/docs/api/schema.html#Schema.Types - * @api public - */ - -exports.SchemaType = require('./schemaType.js'); - -/** - * The constructor used for schematype options - * - * @method SchemaTypeOptions - * @api public - */ - -exports.SchemaTypeOptions = require('./options/schemaTypeOptions'); - -/** - * Internal utils - * - * @property utils - * @api private - */ - -exports.utils = require('./utils.js'); - -/** - * The Mongoose browser [Document](/api/document.html) constructor. - * - * @method Document - * @api public - */ -exports.Document = DocumentProvider(); - -/** - * Return a new browser model. In the browser, a model is just - * a simplified document with a schema - it does **not** have - * functions like `findOne()`, etc. - * - * @method model - * @api public - * @param {String} name - * @param {Schema} schema - * @return Class - */ -exports.model = function(name, schema) { - class Model extends exports.Document { - constructor(obj, fields) { - super(obj, schema, fields); - } - } - Model.modelName = name; - applyHooks(Model, schema); - - return Model; -}; - -/*! - * Module exports. - */ - -if (typeof window !== 'undefined') { - window.mongoose = module.exports; - window.Buffer = Buffer; -} diff --git a/lib/browserDocument.js b/lib/browserDocument.js deleted file mode 100644 index 6bd73318b9a..00000000000 --- a/lib/browserDocument.js +++ /dev/null @@ -1,117 +0,0 @@ -/*! - * Module dependencies. - */ - -'use strict'; - -const NodeJSDocument = require('./document'); -const EventEmitter = require('events').EventEmitter; -const MongooseError = require('./error/index'); -const Schema = require('./schema'); -const ObjectId = require('./types/objectid'); -const ValidationError = MongooseError.ValidationError; -const applyHooks = require('./helpers/model/applyHooks'); -const isObject = require('./helpers/isObject'); - -/** - * Document constructor. - * - * @param {Object} obj the values to set - * @param {Object} schema - * @param {Object} [fields] optional object containing the fields which were selected in the query returning this document and any populated paths data - * @param {Boolean} [skipId] bool, should we auto create an ObjectId _id - * @inherits NodeJS EventEmitter https://nodejs.org/api/events.html#class-eventemitter - * @event `init`: Emitted on a document after it has was retrieved from the db and fully hydrated by Mongoose. - * @event `save`: Emitted when the document is successfully saved - * @api private - */ - -function Document(obj, schema, fields, skipId, skipInit) { - if (!(this instanceof Document)) { - return new Document(obj, schema, fields, skipId, skipInit); - } - - if (isObject(schema) && !schema.instanceOfSchema) { - schema = new Schema(schema); - } - - // When creating EmbeddedDocument, it already has the schema and he doesn't need the _id - schema = this.schema || schema; - - // Generate ObjectId if it is missing, but it requires a scheme - if (!this.schema && schema.options._id) { - obj = obj || {}; - - if (obj._id === undefined) { - obj._id = new ObjectId(); - } - } - - if (!schema) { - throw new MongooseError.MissingSchemaError(); - } - - this.$__setSchema(schema); - - NodeJSDocument.call(this, obj, fields, skipId, skipInit); - - applyHooks(this, schema, { decorateDoc: true }); - - // apply methods - for (const m in schema.methods) { - this[m] = schema.methods[m]; - } - // apply statics - for (const s in schema.statics) { - this[s] = schema.statics[s]; - } -} - -/*! - * Inherit from the NodeJS document - */ - -Document.prototype = Object.create(NodeJSDocument.prototype); -Document.prototype.constructor = Document; - -/*! - * ignore - */ - -Document.events = new EventEmitter(); - -/*! - * Browser doc exposes the event emitter API - */ - -Document.$emitter = new EventEmitter(); - -['on', 'once', 'emit', 'listeners', 'removeListener', 'setMaxListeners', - 'removeAllListeners', 'addListener'].forEach(function(emitterFn) { - Document[emitterFn] = function() { - return Document.$emitter[emitterFn].apply(Document.$emitter, arguments); - }; -}); - -/*! - * ignore - */ - -Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks(opName) { - return this._middleware.execPre(opName, this, []); -}; - -/*! - * ignore - */ - -Document.prototype._execDocumentPostHooks = async function _execDocumentPostHooks(opName, error) { - return this._middleware.execPost(opName, this, [this], { error }); -}; - -/*! - * Module exports. - */ - -Document.ValidationError = ValidationError; -module.exports = exports = Document; diff --git a/lib/document.js b/lib/document.js index 5a9795fe65c..1291a77aea0 100644 --- a/lib/document.js +++ b/lib/document.js @@ -92,19 +92,20 @@ function Document(obj, fields, skipId, options) { } options = Object.assign({}, options); + this.$__ = new InternalCache(); + // Support `browserDocument.js` syntax if (this.$__schema == null) { const _schema = utils.isObject(fields) && !fields.instanceOfSchema ? new Schema(fields) : fields; + this.$__setSchema(_schema); fields = skipId; skipId = options; options = arguments[4] || {}; } - this.$__ = new InternalCache(); - // Avoid setting `isNew` to `true`, because it is `true` by default if (options.isNew != null && options.isNew !== true) { this.$isNew = options.isNew; @@ -848,10 +849,10 @@ Document.prototype.updateOne = function updateOne(doc, options, callback) { const query = this.constructor.updateOne({ _id: this._doc._id }, doc, options); const self = this; query.pre(function queryPreUpdateOne() { - return self.constructor._middleware.execPre('updateOne', self, [self]); + return self._execDocumentPreHooks('updateOne', [self]); }); query.post(function queryPostUpdateOne() { - return self.constructor._middleware.execPost('updateOne', self, [self], {}); + return self._execDocumentPostHooks('updateOne'); }); if (this.$session() != null) { @@ -2903,7 +2904,7 @@ function _pushNestedArrayPaths(val, paths, path) { */ Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks(opName, ...args) { - return this.constructor._middleware.execPre(opName, this, [...args]); + return this.$__middleware.execPre(opName, this, [...args]); }; /*! @@ -2911,7 +2912,7 @@ Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks( */ Document.prototype._execDocumentPostHooks = async function _execDocumentPostHooks(opName, error) { - return this.constructor._middleware.execPost(opName, this, [this], { error }); + return this.$__middleware.execPost(opName, this, [this], { error }); }; /*! @@ -3653,6 +3654,7 @@ Document.prototype.$__setSchema = function(schema) { this.schema = schema; } this.$__schema = schema; + this.$__middleware = schema._getDocumentMiddleware(); this[documentSchemaSymbol] = schema; }; diff --git a/lib/documentProvider.js b/lib/documentProvider.js deleted file mode 100644 index 894494403f4..00000000000 --- a/lib/documentProvider.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -/* eslint-env browser */ - -/*! - * Module dependencies. - */ -const Document = require('./document.js'); -const BrowserDocument = require('./browserDocument.js'); - -let isBrowser = false; - -/** - * Returns the Document constructor for the current context - * - * @api private - */ -module.exports = function documentProvider() { - if (isBrowser) { - return BrowserDocument; - } - return Document; -}; - -/*! - * ignore - */ -module.exports.setBrowser = function(flag) { - isBrowser = flag; -}; diff --git a/lib/drivers/browser/binary.js b/lib/drivers/browser/binary.js deleted file mode 100644 index 4658f7b9e0f..00000000000 --- a/lib/drivers/browser/binary.js +++ /dev/null @@ -1,14 +0,0 @@ - -/*! - * Module dependencies. - */ - -'use strict'; - -const Binary = require('bson').Binary; - -/*! - * Module exports. - */ - -module.exports = exports = Binary; diff --git a/lib/drivers/browser/decimal128.js b/lib/drivers/browser/decimal128.js deleted file mode 100644 index 5668182b354..00000000000 --- a/lib/drivers/browser/decimal128.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * ignore - */ - -'use strict'; - -module.exports = require('bson').Decimal128; diff --git a/lib/drivers/browser/index.js b/lib/drivers/browser/index.js deleted file mode 100644 index 2c77c712dde..00000000000 --- a/lib/drivers/browser/index.js +++ /dev/null @@ -1,13 +0,0 @@ -/*! - * Module exports. - */ - -'use strict'; - -exports.Collection = function() { - throw new Error('Cannot create a collection from browser library'); -}; -exports.Connection = function() { - throw new Error('Cannot create a connection from browser library'); -}; -exports.BulkWriteResult = function() {}; diff --git a/lib/drivers/browser/objectid.js b/lib/drivers/browser/objectid.js deleted file mode 100644 index d847afe3b8e..00000000000 --- a/lib/drivers/browser/objectid.js +++ /dev/null @@ -1,29 +0,0 @@ - -/*! - * [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) ObjectId - * @constructor NodeMongoDbObjectId - * @see ObjectId - */ - -'use strict'; - -const ObjectId = require('bson').ObjectID; - -/** - * Getter for convenience with populate, see gh-6115 - * @api private - */ - -Object.defineProperty(ObjectId.prototype, '_id', { - enumerable: false, - configurable: true, - get: function() { - return this; - } -}); - -/*! - * ignore - */ - -module.exports = exports = ObjectId; diff --git a/lib/helpers/model/applyHooks.js b/lib/helpers/model/applyHooks.js index 95980adf204..bd187ff0372 100644 --- a/lib/helpers/model/applyHooks.js +++ b/lib/helpers/model/applyHooks.js @@ -65,7 +65,7 @@ function applyHooks(model, schema, options) { applyHooks(childModel, type.schema, { ...options, - decorateDoc: false, // Currently subdocs inherit directly from NodeJSDocument in browser + decorateDoc: false, isChildSchema: true }); if (childModel.discriminators != null) { @@ -81,27 +81,7 @@ function applyHooks(model, schema, options) { // promises and make it so that `doc.save.toString()` provides meaningful // information. - const middleware = schema.s.hooks. - filter(hook => { - if (hook.name === 'updateOne' || hook.name === 'deleteOne') { - return !!hook['document']; - } - if (hook.name === 'remove' || hook.name === 'init') { - return hook['document'] == null || !!hook['document']; - } - if (hook.query != null || hook.document != null) { - return hook.document !== false; - } - return true; - }). - filter(hook => { - // If user has overwritten the method, don't apply built-in middleware - if (schema.methods[hook.name]) { - return !hook.fn[symbols.builtInMiddleware]; - } - - return true; - }); + const middleware = schema._getDocumentMiddleware(); model._middleware = middleware; diff --git a/lib/mongoose.js b/lib/mongoose.js index ca035eb1e6d..acdf46ec625 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -39,7 +39,7 @@ require('./helpers/printJestWarning'); const objectIdHexRegexp = /^[0-9A-Fa-f]{24}$/; -const { AsyncLocalStorage } = require('node:async_hooks'); +const { AsyncLocalStorage } = require('async_hooks'); /** * Mongoose constructor. @@ -1029,16 +1029,6 @@ Mongoose.prototype.Model = Model; Mongoose.prototype.Document = Document; -/** - * The Mongoose DocumentProvider constructor. Mongoose users should not have to - * use this directly - * - * @method DocumentProvider - * @api public - */ - -Mongoose.prototype.DocumentProvider = require('./documentProvider'); - /** * The Mongoose ObjectId [SchemaType](https://mongoosejs.com/docs/schematypes.html). Used for * declaring paths in your schema that should be diff --git a/lib/schema.js b/lib/schema.js index a9e795120d9..4ebc0d32eb5 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -23,6 +23,7 @@ const merge = require('./helpers/schema/merge'); const mpath = require('mpath'); const setPopulatedVirtualValue = require('./helpers/populate/setPopulatedVirtualValue'); const setupTimestamps = require('./helpers/timestamps/setupTimestamps'); +const symbols = require('./schema/symbols'); const utils = require('./utils'); const validateRef = require('./helpers/populate/validateRef'); @@ -643,6 +644,33 @@ Schema.prototype.discriminator = function(name, schema, options) { return this; }; +/*! + * Get the document middleware for this schema, filtering out any hooks that are specific to queries. + */ +Schema.prototype._getDocumentMiddleware = function _getDocumentMiddleware() { + return this.s.hooks. + filter(hook => { + if (hook.name === 'updateOne' || hook.name === 'deleteOne') { + return !!hook['document']; + } + if (hook.name === 'remove' || hook.name === 'init') { + return hook['document'] == null || !!hook['document']; + } + if (hook.query != null || hook.document != null) { + return hook.document !== false; + } + return true; + }). + filter(hook => { + // If user has overwritten the method, don't apply built-in middleware + if (this.methods[hook.name]) { + return !hook.fn[symbols.builtInMiddleware]; + } + + return true; + }); +} + /*! * Get this schema's default toObject/toJSON options, including Mongoose global * options. diff --git a/package.json b/package.json index 756e6dfe84d..a5881d5bb9a 100644 --- a/package.json +++ b/package.json @@ -29,20 +29,14 @@ "sift": "17.1.3" }, "devDependencies": { - "@babel/core": "7.26.10", - "@babel/preset-env": "7.26.9", "@typescript-eslint/eslint-plugin": "^8.19.1", "@typescript-eslint/parser": "^8.19.1", "acquit": "1.3.0", "acquit-ignore": "0.2.1", "acquit-require": "0.1.1", "ajv": "8.17.1", - "assert-browserify": "2.0.0", - "babel-loader": "8.2.5", "broken-link-checker": "^0.7.8", - "buffer": "^5.6.0", "cheerio": "1.0.0", - "crypto-browserify": "3.12.1", "dox": "1.0.0", "eslint": "8.57.1", "eslint-plugin-markdown": "^5.1.0", @@ -62,11 +56,9 @@ "nyc": "15.1.0", "pug": "3.0.3", "sinon": "20.0.0", - "stream-browserify": "3.0.0", "tsd": "0.31.2", "typescript": "5.7.3", - "uuid": "11.1.0", - "webpack": "5.98.0" + "uuid": "11.1.0" }, "directories": { "lib": "./lib/mongoose" @@ -92,8 +84,6 @@ "lint-js": "eslint . --ext .js --ext .cjs", "lint-ts": "eslint . --ext .ts", "lint-md": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#benchmarks\"", - "build-browser": "(rm ./dist/* || true) && node ./scripts/build-browser.js", - "prepublishOnly": "npm run build-browser", "release": "git pull && git push origin master --tags && npm publish", "release-5x": "git pull origin 5.x && git push origin 5.x && git push origin 5.x --tags && npm publish --tag 5x", "release-6x": "git pull origin 6.x && git push origin 6.x && git push origin 6.x --tags && npm publish --tag 6x", @@ -122,7 +112,6 @@ "url": "git://github.com/Automattic/mongoose.git" }, "homepage": "https://mongoosejs.com", - "browser": "./dist/browser.umd.js", "config": { "mongodbMemoryServer": { "disablePostinstall": true diff --git a/scripts/build-browser.js b/scripts/build-browser.js deleted file mode 100644 index f6f0680f9af..00000000000 --- a/scripts/build-browser.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -const config = require('../webpack.config.js'); -const webpack = require('webpack'); - -const compiler = webpack(config); - -console.log('Starting browser build...'); -compiler.run((err, stats) => { - if (err) { - console.err(stats.toString()); - console.err('Browser build unsuccessful.'); - process.exit(1); - } - console.log(stats.toString()); - console.log('Browser build successful.'); - process.exit(0); -}); diff --git a/test/browser.test.js b/test/browser.test.js deleted file mode 100644 index e26251f07f9..00000000000 --- a/test/browser.test.js +++ /dev/null @@ -1,88 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -const Document = require('../lib/browserDocument'); -const Schema = require('../lib/schema'); -const assert = require('assert'); -const exec = require('child_process').exec; - -/** - * Test. - */ -describe('browser', function() { - it('require() works with no other require calls (gh-5842)', function(done) { - exec('node --eval "require(\'./lib/browser\')"', done); - }); - - it('using schema (gh-7170)', function(done) { - exec('node --eval "const mongoose = require(\'./lib/browser\'); new mongoose.Schema();"', done); - }); - - it('document works (gh-4987)', function() { - const schema = new Schema({ - name: { type: String, required: true }, - quest: { type: String, match: /Holy Grail/i, required: true }, - favoriteColor: { type: String, enum: ['Red', 'Blue'], required: true } - }); - - assert.doesNotThrow(function() { - new Document({}, schema); - }); - }); - - it('document validation with arrays (gh-6175)', async function() { - const Point = new Schema({ - latitude: { - type: Number, - required: true, - min: -90, - max: 90 - }, - longitude: { - type: Number, - required: true, - min: -180, - max: 180 - } - }); - - const schema = new Schema({ - name: { - type: String, - required: true - }, - vertices: { - type: [Point], - required: true - } - }); - - let test = new Document({ - name: 'Test Polygon', - vertices: [ - { - latitude: -37.81902680201739, - longitude: 144.9821037054062 - } - ] - }, schema); - - // Should not throw - await test.validate(); - - test = new Document({ - name: 'Test Polygon', - vertices: [ - { - latitude: -37.81902680201739 - } - ] - }, schema); - - const error = await test.validate().then(() => null, err => err); - assert.ok(error.errors['vertices.0.longitude']); - }); -}); diff --git a/test/deno_mocha.js b/test/deno_mocha.js index a5cf5af5e0b..bd06f431737 100644 --- a/test/deno_mocha.js +++ b/test/deno_mocha.js @@ -38,7 +38,7 @@ const files = fs.readdirSync(testDir). concat(fs.readdirSync(path.join(testDir, 'docs')).map(file => path.join('docs', file))). concat(fs.readdirSync(path.join(testDir, 'helpers')).map(file => path.join('helpers', file))); -const ignoreFiles = new Set(['browser.test.js']); +const ignoreFiles = new Set([]); for (const file of files) { if (!file.endsWith('.test.js') || ignoreFiles.has(file)) { diff --git a/test/docs/lean.test.js b/test/docs/lean.test.js index e571987864b..c784c8c1cf7 100644 --- a/test/docs/lean.test.js +++ b/test/docs/lean.test.js @@ -41,6 +41,9 @@ describe('Lean Tutorial', function() { // To enable the `lean` option for a query, use the `lean()` function. const leanDoc = await MyModel.findOne().lean(); + // acquit:ignore:start + delete normalDoc.$__.middleware; // To make v8Serialize() not crash because it can't clone functions + // acquit:ignore:end v8Serialize(normalDoc).length; // approximately 180 v8Serialize(leanDoc).length; // approximately 55, about 3x smaller! diff --git a/test/files/index.html b/test/files/index.html deleted file mode 100644 index 67526cc96dd..00000000000 --- a/test/files/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - Test - - diff --git a/test/files/sample.js b/test/files/sample.js deleted file mode 100644 index 8328e6f27cf..00000000000 --- a/test/files/sample.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; -import mongoose from './dist/browser.umd.js'; - -const doc = new mongoose.Document({}, new mongoose.Schema({ - name: String -})); -console.log(doc.validateSync()); diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 49a5c1eb83d..00000000000 --- a/webpack.config.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -const webpack = require('webpack'); -const paths = require('path'); - -const webpackConfig = { - entry: require.resolve('./browser.js'), - output: { - filename: './dist/browser.umd.js', - path: paths.resolve(__dirname, ''), - library: 'mongoose', - libraryTarget: 'umd', - // override default 'window' globalObject so browser build will work in SSR environments - // may become unnecessary in webpack 5 - globalObject: 'typeof self !== \'undefined\' ? self : this' - }, - externals: [ - /^node_modules\/.+$/ - ], - module: { - rules: [ - { - test: /\.js$/, - include: [ - /\/mongoose\//i, - /\/kareem\//i - ], - loader: 'babel-loader', - options: { - presets: ['@babel/preset-env'] - } - } - ] - }, - resolve: { - alias: { - 'bn.js': require.resolve('bn.js') - }, - fallback: { - assert: require.resolve('assert-browserify'), - buffer: require.resolve('buffer'), - crypto: require.resolve('crypto-browserify'), - stream: require.resolve('stream-browserify') - } - }, - target: 'web', - mode: 'production', - plugins: [ - new webpack.DefinePlugin({ - process: '({env:{}})' - }), - new webpack.ProvidePlugin({ - Buffer: ['buffer', 'Buffer'] - }) - ] -}; - -module.exports = webpackConfig; - From 8f4ec142da69cc28dadd9061791c1b2db6d5f948 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 29 Apr 2025 16:38:12 -0400 Subject: [PATCH 060/209] remove node: from async_hooks import for @mongoosejs/browser webpack --- lib/mongoose.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mongoose.js b/lib/mongoose.js index ca035eb1e6d..8a4a30f3496 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -39,7 +39,7 @@ require('./helpers/printJestWarning'); const objectIdHexRegexp = /^[0-9A-Fa-f]{24}$/; -const { AsyncLocalStorage } = require('node:async_hooks'); +const { AsyncLocalStorage } = require('async_hooks'); /** * Mongoose constructor. From 0ab148bb4a9394325cdf755c2ea27f22319dc1d7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 29 Apr 2025 16:45:54 -0400 Subject: [PATCH 061/209] style: fix lint --- lib/helpers/model/applyHooks.js | 2 -- lib/schema.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/helpers/model/applyHooks.js b/lib/helpers/model/applyHooks.js index bd187ff0372..451bdd7fc06 100644 --- a/lib/helpers/model/applyHooks.js +++ b/lib/helpers/model/applyHooks.js @@ -1,7 +1,5 @@ 'use strict'; -const symbols = require('../../schema/symbols'); - /*! * ignore */ diff --git a/lib/schema.js b/lib/schema.js index 4ebc0d32eb5..1bb8cc022cc 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -669,7 +669,7 @@ Schema.prototype._getDocumentMiddleware = function _getDocumentMiddleware() { return true; }); -} +}; /*! * Get this schema's default toObject/toJSON options, including Mongoose global From 42ed27e9d4eb318f6b8bc69855ef881313e99a76 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 29 Apr 2025 16:54:07 -0400 Subject: [PATCH 062/209] Update test/docs/lean.test.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/docs/lean.test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/docs/lean.test.js b/test/docs/lean.test.js index c784c8c1cf7..3205586744c 100644 --- a/test/docs/lean.test.js +++ b/test/docs/lean.test.js @@ -42,7 +42,11 @@ describe('Lean Tutorial', function() { const leanDoc = await MyModel.findOne().lean(); // acquit:ignore:start - delete normalDoc.$__.middleware; // To make v8Serialize() not crash because it can't clone functions + // The `normalDoc.$__.middleware` property is an internal Mongoose object that stores middleware functions. + // These functions cannot be cloned by `v8.serialize()`, which causes the method to throw an error. + // Since this test only compares the serialized size of the document, it is safe to delete this property + // to prevent the crash. This operation does not affect the document's data or behavior in this context. + delete normalDoc.$__.middleware; // acquit:ignore:end v8Serialize(normalDoc).length; // approximately 180 v8Serialize(leanDoc).length; // approximately 55, about 3x smaller! From a8363e527d33402839448c7a8c4e75e8b0f15f03 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 5 May 2025 14:54:21 -0400 Subject: [PATCH 063/209] fix lint --- scripts/website.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/website.js b/scripts/website.js index 70568b2ca00..f8cdb9c49d8 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -612,7 +612,7 @@ if (isMain) { const config = generateSearch.getConfig(); generateSearchPromise = generateSearch.generateSearch(config); } catch (err) { - console.error("Generating Search failed:", err); + console.error('Generating Search failed:', err); } await deleteAllHtmlFiles(); await pugifyAllFiles(); @@ -621,7 +621,7 @@ if (isMain) { await moveDocsToTemp(); } - if (!!generateSearchPromise) { + if (generateSearchPromise) { await generateSearchPromise; } From aa23bad027ebb7c136a992e8a38a41797fc74985 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 5 May 2025 15:01:08 -0400 Subject: [PATCH 064/209] docs: link browser docs out to @mongoosejs/browser readme --- docs/browser.md | 37 ++----------------------------------- 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/docs/browser.md b/docs/browser.md index 43bc487384a..3044900eebc 100644 --- a/docs/browser.md +++ b/docs/browser.md @@ -1,37 +1,4 @@ # Mongoose in the Browser -Mongoose supports creating schemas and validating documents in the browser. -Mongoose's browser library does **not** support saving documents, [queries](http://mongoosejs.com/docs/queries.html), [populate](http://mongoosejs.com/docs/populate.html), [discriminators](http://mongoosejs.com/docs/discriminators.html), or any other Mongoose feature other than schemas and validating documents. - -Mongoose has a pre-built bundle of the browser library. If you're bundling your code with [Webpack](https://webpack.js.org/), you should be able to import Mongoose's browser library as shown below if your Webpack `target` is `'web'`: - -```javascript -import mongoose from 'mongoose'; -``` - -You can use the below syntax to access the Mongoose browser library from Node.js: - -```javascript -// Using `require()` -const mongoose = require('mongoose/browser'); - -// Using ES6 imports -import mongoose from 'mongoose/browser'; -``` - -## Using the Browser Library {#usage} - -Mongoose's browser library is very limited. The only use case it supports is validating documents as shown below. - -```javascript -import mongoose from 'mongoose'; - -// Mongoose's browser library does **not** have models. It only supports -// schemas and documents. The primary use case is validating documents -// against Mongoose schemas. -const doc = new mongoose.Document({}, new mongoose.Schema({ - name: { type: String, required: true } -})); -// Prints an error because `name` is required. -console.log(doc.validateSync()); -``` +As of Mongoose 9, [Mongoose's browser build is now in the `@mongoosejs/browser` npm package](https://github.com/mongoosejs/mongoose-browser). +The documentation has been moved to the [`@mongoosejs/browser` REAME](https://github.com/mongoosejs/mongoose-browser?tab=readme-ov-file#mongoosejsbrowser). From dc6071d35d6b827c24c43f5f5857235c35d76fa2 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Wed, 30 Apr 2025 14:05:13 +0200 Subject: [PATCH 065/209] chore(dev-deps): update "@typescript-eslint/*" to 8.31.1 make sure it is the latest currently available for eslint 9 upgrade --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ab3ce9337f1..b21b6ced5a0 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,8 @@ "sift": "17.1.3" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^8.19.1", - "@typescript-eslint/parser": "^8.19.1", + "@typescript-eslint/eslint-plugin": "^8.31.1", + "@typescript-eslint/parser": "^8.31.1", "acquit": "1.3.0", "acquit-ignore": "0.2.1", "acquit-require": "0.1.1", From 3617299f7db504cca8616976791034e77f9afbb4 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Wed, 30 Apr 2025 14:17:20 +0200 Subject: [PATCH 066/209] chore(dev-deps): update "eslint" to 9.25.1 Migrating the configs later. Removing the unused catch error parameters can be done since ES2019 "optional catch binding" is supported by the minimal nodejs 18.0 --- docs/source/index.js | 4 ++-- examples/redis-todo/middleware/auth.js | 2 +- examples/redis-todo/routers/todoRouter.js | 6 +++--- examples/redis-todo/routers/userRouter.js | 6 +++--- lib/helpers/populate/createPopulateQueryFilter.js | 2 +- lib/helpers/query/cast$expr.js | 10 +++++----- lib/helpers/query/castUpdate.js | 4 ++-- lib/query.js | 8 ++++---- lib/schema/documentArray.js | 10 +++------- lib/schema/string.js | 2 +- lib/types/documentArray/methods/index.js | 2 +- package.json | 8 ++++---- scripts/loadSponsorData.js | 2 +- test/docs/transactions.test.js | 2 +- test/document.test.js | 2 +- test/model.middleware.preposttypes.test.js | 2 +- test/model.test.js | 14 +++++++------- test/query.test.js | 2 +- test/types.array.test.js | 10 +++++----- 19 files changed, 47 insertions(+), 51 deletions(-) diff --git a/docs/source/index.js b/docs/source/index.js index 50a9a597063..fdcec2cc703 100644 --- a/docs/source/index.js +++ b/docs/source/index.js @@ -4,11 +4,11 @@ let sponsors = []; try { sponsors = require('../data/sponsors.json'); -} catch (err) {} +} catch {} let jobs = []; try { jobs = require('../data/jobs.json'); -} catch (err) {} +} catch {} const api = require('./api'); diff --git a/examples/redis-todo/middleware/auth.js b/examples/redis-todo/middleware/auth.js index 4cbed4107fa..095f2620c99 100644 --- a/examples/redis-todo/middleware/auth.js +++ b/examples/redis-todo/middleware/auth.js @@ -13,7 +13,7 @@ module.exports = async function(req, res, next) { req.userId = decodedValue.userId; next(); - } catch (err) { + } catch { res.status(401).send({ msg: 'Invalid Authentication' }); } }; diff --git a/examples/redis-todo/routers/todoRouter.js b/examples/redis-todo/routers/todoRouter.js index 96851c45ebc..b88174f72b6 100644 --- a/examples/redis-todo/routers/todoRouter.js +++ b/examples/redis-todo/routers/todoRouter.js @@ -33,7 +33,7 @@ Router.post('/create', auth, clearCache, async function({ userId, body }, res) { }); await todo.save(); res.status(201).json({ todo }); - } catch (err) { + } catch { res.status(501).send('Server Error'); } }); @@ -52,7 +52,7 @@ Router.post('/update', auth, async function({ userId, body }, res) { await updatedTodo.save(); res.status(200).json({ todo: updatedTodo }); - } catch (err) { + } catch { res.status(501).send('Server Error'); } }); @@ -65,7 +65,7 @@ Router.delete('/delete', auth, async function({ userId, body: { todoId } }, res) try { await Todo.findOneAndDelete({ $and: [{ userId }, { _id: todoId }] }); res.status(200).send({ msg: 'Todo deleted' }); - } catch (err) { + } catch { res.status(501).send('Server Error'); } }); diff --git a/examples/redis-todo/routers/userRouter.js b/examples/redis-todo/routers/userRouter.js index 23a77477714..763808df46d 100644 --- a/examples/redis-todo/routers/userRouter.js +++ b/examples/redis-todo/routers/userRouter.js @@ -52,7 +52,7 @@ Router.post('/login', async function({ body }, res) { const token = user.genAuthToken(); res.status(201).json({ token }); - } catch (err) { + } catch { res.status(501).send('Server Error'); } }); @@ -77,7 +77,7 @@ Router.post('/update', auth, async function({ userId, body }, res) { } res.status(200).json({ user: updatedUser }); - } catch (err) { + } catch { res.status(500).send('Server Error'); } }); @@ -90,7 +90,7 @@ Router.delete('/delete', auth, async function({ userId }, res) { await User.findByIdAndRemove({ _id: userId }); await Todo.deleteMany({ userId }); res.status(200).send({ msg: 'User deleted' }); - } catch (err) { + } catch { res.status(501).send('Server Error'); } }); diff --git a/lib/helpers/populate/createPopulateQueryFilter.js b/lib/helpers/populate/createPopulateQueryFilter.js index 47509a35658..d0f1d8bfdc7 100644 --- a/lib/helpers/populate/createPopulateQueryFilter.js +++ b/lib/helpers/populate/createPopulateQueryFilter.js @@ -73,7 +73,7 @@ function _filterInvalidIds(ids, foreignSchemaType, skipInvalidIds) { try { foreignSchemaType.cast(id); return true; - } catch (err) { + } catch { return false; } }); diff --git a/lib/helpers/query/cast$expr.js b/lib/helpers/query/cast$expr.js index 8e84011b2c3..24d8365ab88 100644 --- a/lib/helpers/query/cast$expr.js +++ b/lib/helpers/query/cast$expr.js @@ -143,7 +143,7 @@ function castNumberOperator(val) { try { return castNumber(val); - } catch (err) { + } catch { throw new CastError('Number', val); } } @@ -184,7 +184,7 @@ function castArithmetic(val) { } try { return castNumber(val); - } catch (err) { + } catch { throw new CastError('Number', val); } } @@ -195,7 +195,7 @@ function castArithmetic(val) { } try { return castNumber(v); - } catch (err) { + } catch { throw new CastError('Number', v); } }); @@ -247,13 +247,13 @@ function castComparison(val, schema, strictQuery) { if (is$literal) { try { val[1] = { $literal: caster(val[1].$literal) }; - } catch (err) { + } catch { throw new CastError(caster.name.replace(/^cast/, ''), val[1], path + '.$literal'); } } else { try { val[1] = caster(val[1]); - } catch (err) { + } catch { throw new CastError(caster.name.replace(/^cast/, ''), val[1], path); } } diff --git a/lib/helpers/query/castUpdate.js b/lib/helpers/query/castUpdate.js index f0819b3a586..194ea6d5601 100644 --- a/lib/helpers/query/castUpdate.js +++ b/lib/helpers/query/castUpdate.js @@ -541,7 +541,7 @@ function castUpdateVal(schema, val, op, $conditional, context, path) { if (op in numberOps) { try { return castNumber(val); - } catch (err) { + } catch { throw new CastError('number', val, path); } } @@ -600,7 +600,7 @@ function castUpdateVal(schema, val, op, $conditional, context, path) { } try { return castNumber(val); - } catch (error) { + } catch { throw new CastError('number', val, schema.path); } } diff --git a/lib/query.js b/lib/query.js index 0dff5602910..badd223c6c9 100644 --- a/lib/query.js +++ b/lib/query.js @@ -894,7 +894,7 @@ Query.prototype.limit = function limit(v) { if (typeof v === 'string') { try { v = castNumber(v); - } catch (err) { + } catch { throw new CastError('Number', v, 'limit'); } } @@ -928,7 +928,7 @@ Query.prototype.skip = function skip(v) { if (typeof v === 'string') { try { v = castNumber(v); - } catch (err) { + } catch { throw new CastError('Number', v, 'skip'); } } @@ -1758,14 +1758,14 @@ Query.prototype.setOptions = function(options, overwrite) { if (typeof options.limit === 'string') { try { options.limit = castNumber(options.limit); - } catch (err) { + } catch { throw new CastError('Number', options.limit, 'limit'); } } if (typeof options.skip === 'string') { try { options.skip = castNumber(options.skip); - } catch (err) { + } catch { throw new CastError('Number', options.skip, 'skip'); } } diff --git a/lib/schema/documentArray.js b/lib/schema/documentArray.js index c117cb1c6a6..7023407ca80 100644 --- a/lib/schema/documentArray.js +++ b/lib/schema/documentArray.js @@ -204,13 +204,9 @@ SchemaDocumentArray.prototype.discriminator = function(name, schema, options) { const EmbeddedDocument = _createConstructor(schema, null, this.casterConstructor); EmbeddedDocument.baseCasterConstructor = this.casterConstructor; - try { - Object.defineProperty(EmbeddedDocument, 'name', { - value: name - }); - } catch (error) { - // Ignore error, only happens on old versions of node - } + Object.defineProperty(EmbeddedDocument, 'name', { + value: name + }); this.casterConstructor.discriminators[name] = EmbeddedDocument; diff --git a/lib/schema/string.js b/lib/schema/string.js index 1e84cac6271..25202b1c796 100644 --- a/lib/schema/string.js +++ b/lib/schema/string.js @@ -603,7 +603,7 @@ SchemaString.prototype.cast = function(value, doc, init, prev, options) { try { return castString(value); - } catch (error) { + } catch { throw new CastError('string', value, this.path, null, this); } }; diff --git a/lib/types/documentArray/methods/index.js b/lib/types/documentArray/methods/index.js index 00b47c434ba..29e4b0d77fd 100644 --- a/lib/types/documentArray/methods/index.js +++ b/lib/types/documentArray/methods/index.js @@ -127,7 +127,7 @@ const methods = { try { casted = castObjectId(id).toString(); - } catch (e) { + } catch { casted = null; } diff --git a/package.json b/package.json index b21b6ced5a0..5916ca4dc84 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "broken-link-checker": "^0.7.8", "cheerio": "1.0.0", "dox": "1.0.0", - "eslint": "8.57.1", + "eslint": "9.25.1", "eslint-plugin-markdown": "^5.1.0", "eslint-plugin-mocha-no-only": "1.2.0", "express": "^4.19.2", @@ -80,9 +80,9 @@ "docs:prepare:publish:6x": "git checkout 6.x && git merge 6.x && npm run docs:clean:stable && env DOCS_DEPLOY=true npm run docs:generate && mv ./docs/6.x ./tmp && git checkout gh-pages && npm run docs:copy:tmp:6x", "docs:prepare:publish:7x": "env DOCS_DEPLOY=true npm run docs:generate && git checkout gh-pages && rimraf ./docs/7.x && mv ./tmp ./docs/7.x", "docs:check-links": "blc http://127.0.0.1:8089 -ro", - "lint": "eslint .", - "lint-js": "eslint . --ext .js --ext .cjs", - "lint-ts": "eslint . --ext .ts", + "lint": "ESLINT_USE_FLAT_CONFIG=false eslint .", + "lint-js": "ESLINT_USE_FLAT_CONFIG=false eslint . --ext .js --ext .cjs", + "lint-ts": "ESLINT_USE_FLAT_CONFIG=false eslint . --ext .ts", "lint-md": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#benchmarks\"", "release": "git pull && git push origin master --tags && npm publish", "release-5x": "git pull origin 5.x && git push origin 5.x && git push origin 5.x --tags && npm publish --tag 5x", diff --git a/scripts/loadSponsorData.js b/scripts/loadSponsorData.js index 0a6b4d6baff..594903fcb40 100644 --- a/scripts/loadSponsorData.js +++ b/scripts/loadSponsorData.js @@ -68,7 +68,7 @@ async function run() { try { fs.mkdirSync(`${docsDir}/data`); - } catch (err) {} + } catch {} const subscribers = await Subscriber. find({ companyName: { $exists: true }, description: { $exists: true }, logo: { $exists: true } }). diff --git a/test/docs/transactions.test.js b/test/docs/transactions.test.js index 10f366e36ba..100b4a95db7 100644 --- a/test/docs/transactions.test.js +++ b/test/docs/transactions.test.js @@ -35,7 +35,7 @@ describe('transactions', function() { _skipped = true; this.skip(); } - } catch (err) { + } catch { _skipped = true; this.skip(); } diff --git a/test/document.test.js b/test/document.test.js index 73f425e6519..3678b793370 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -906,7 +906,7 @@ describe('document', function() { let str; try { str = JSON.stringify(arr); - } catch (_) { + } catch { err = true; } assert.equal(err, false); diff --git a/test/model.middleware.preposttypes.test.js b/test/model.middleware.preposttypes.test.js index 93a42f8dc1f..9a8ec6086b1 100644 --- a/test/model.middleware.preposttypes.test.js +++ b/test/model.middleware.preposttypes.test.js @@ -29,7 +29,7 @@ function getTypeName(obj) { } else { try { return this.constructor.name; - } catch (err) { + } catch { return 'unknown'; } } diff --git a/test/model.test.js b/test/model.test.js index faa4be394c3..57fb27c58c4 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -558,7 +558,7 @@ describe('Model', function() { let post; try { post = new BlogPost({ date: 'Test', meta: { date: 'Test' } }); - } catch (e) { + } catch { threw = true; } @@ -566,7 +566,7 @@ describe('Model', function() { try { post.set('title', 'Test'); - } catch (e) { + } catch { threw = true; } @@ -591,7 +591,7 @@ describe('Model', function() { date: 'Test' } }); - } catch (e) { + } catch { threw = true; } @@ -599,7 +599,7 @@ describe('Model', function() { try { post.set('meta.date', 'Test'); - } catch (e) { + } catch { threw = true; } @@ -657,7 +657,7 @@ describe('Model', function() { post.get('comments').push({ date: 'Bad date' }); - } catch (e) { + } catch { threw = true; } @@ -1313,7 +1313,7 @@ describe('Model', function() { JSON.stringify(meta); getter1 = JSON.stringify(post.get('meta')); getter2 = JSON.stringify(post.meta); - } catch (err) { + } catch { threw = true; } @@ -2403,7 +2403,7 @@ describe('Model', function() { let threw = false; try { new P({ path: 'i should not throw' }); - } catch (err) { + } catch { threw = true; } diff --git a/test/query.test.js b/test/query.test.js index 5effc0298fc..cf4698fa665 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -571,7 +571,7 @@ describe('Query', function() { try { q.find(); - } catch (err) { + } catch { threw = true; } diff --git a/test/types.array.test.js b/test/types.array.test.js index 3aea341915c..225a37972ed 100644 --- a/test/types.array.test.js +++ b/test/types.array.test.js @@ -70,7 +70,7 @@ describe('types array', function() { try { b.hasAtomics; - } catch (_) { + } catch { threw = true; } @@ -79,8 +79,8 @@ describe('types array', function() { const a = new MongooseArray([67, 8]).filter(Boolean); try { a.push(3, 4); - } catch (_) { - console.error(_); + } catch (err) { + console.error(err); threw = true; } @@ -1693,7 +1693,7 @@ describe('types array', function() { arr.num1.push({ x: 1 }); arr.num1.push(9); arr.num1.push('woah'); - } catch (err) { + } catch { threw1 = true; } @@ -1703,7 +1703,7 @@ describe('types array', function() { arr.num2.push({ x: 1 }); arr.num2.push(9); arr.num2.push('woah'); - } catch (err) { + } catch { threw2 = true; } From 9dc238b2e85a2302873b78bd6fa61acde8fb333a Mon Sep 17 00:00:00 2001 From: hasezoey Date: Wed, 30 Apr 2025 15:05:26 +0200 Subject: [PATCH 067/209] chore: migrate to eslint flat configs --- .eslintrc.js | 225 --------------------------------------- eslint.config.mjs | 198 ++++++++++++++++++++++++++++++++++ lib/types/subdocument.js | 4 +- package.json | 9 +- scripts/website.js | 6 +- test/.eslintrc.yml | 7 -- test/deno.mjs | 20 ++-- test/deno_mocha.mjs | 10 +- test/types/.eslintrc.yml | 2 - 9 files changed, 220 insertions(+), 261 deletions(-) delete mode 100644 .eslintrc.js create mode 100644 eslint.config.mjs delete mode 100644 test/.eslintrc.yml delete mode 100644 test/types/.eslintrc.yml diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index eefc6c37869..00000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,225 +0,0 @@ -'use strict'; - -module.exports = { - extends: [ - 'eslint:recommended' - ], - ignorePatterns: [ - 'tools', - 'dist', - 'test/files/*', - 'benchmarks', - '*.min.js', - '**/docs/js/native.js', - '!.*', - 'node_modules', - '.git', - 'data' - ], - overrides: [ - { - files: [ - '**/*.{ts,tsx}', - '**/*.md/*.ts', - '**/*.md/*.typescript' - ], - parserOptions: { - project: './tsconfig.json' - }, - extends: [ - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended' - ], - plugins: [ - '@typescript-eslint' - ], - rules: { - '@typescript-eslint/triple-slash-reference': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-empty-function': 'off', - 'spaced-comment': [ - 'error', - 'always', - { - block: { - markers: [ - '!' - ], - balanced: true - }, - markers: [ - '/' - ] - } - ], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/ban-types': 'off', - '@typescript-eslint/no-unused-vars': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/prefer-optional-chain': 'error', - '@typescript-eslint/no-dupe-class-members': 'error', - '@typescript-eslint/no-redeclare': 'error', - '@typescript-eslint/space-infix-ops': 'off', - '@typescript-eslint/no-require-imports': 'off', - '@typescript-eslint/no-empty-object-type': 'off', - '@typescript-eslint/no-wrapper-object-types': 'off', - '@typescript-eslint/no-unused-expressions': 'off', - '@typescript-eslint/no-unsafe-function-type': 'off' - } - }, - { - files: [ - '**/docs/js/**/*.js' - ], - env: { - node: false, - browser: true - } - } - // // eslint-plugin-markdown has been disabled because of out-standing issues, see https://github.com/eslint/eslint-plugin-markdown/issues/214 - // { - // files: ['**/*.md'], - // processor: 'markdown/markdown' - // }, - // { - // files: ['**/*.md/*.js', '**/*.md/*.javascript', '**/*.md/*.ts', '**/*.md/*.typescript'], - // parserOptions: { - // ecmaFeatures: { - // impliedStrict: true - // }, - // sourceType: 'module', // required to allow "import" statements - // ecmaVersion: 'latest' // required to allow top-level await - // }, - // rules: { - // 'no-undef': 'off', - // 'no-unused-expressions': 'off', - // 'no-unused-vars': 'off', - // 'no-redeclare': 'off', - // '@typescript-eslint/no-redeclare': 'off' - // } - // } - ], - plugins: [ - 'mocha-no-only' - // 'markdown' - ], - parserOptions: { - ecmaVersion: 2022 - }, - env: { - node: true, - es6: true, - es2020: true - }, - rules: { - 'comma-style': 'error', - indent: [ - 'error', - 2, - { - SwitchCase: 1, - VariableDeclarator: 2 - } - ], - 'keyword-spacing': 'error', - 'no-whitespace-before-property': 'error', - 'no-buffer-constructor': 'warn', - 'no-console': 'off', - 'no-constant-condition': 'off', - 'no-multi-spaces': 'error', - 'func-call-spacing': 'error', - 'no-trailing-spaces': 'error', - 'no-undef': 'error', - 'no-unneeded-ternary': 'error', - 'no-const-assign': 'error', - 'no-useless-rename': 'error', - 'no-dupe-keys': 'error', - 'space-in-parens': [ - 'error', - 'never' - ], - 'spaced-comment': [ - 'error', - 'always', - { - block: { - markers: [ - '!' - ], - balanced: true - } - } - ], - 'key-spacing': [ - 'error', - { - beforeColon: false, - afterColon: true - } - ], - 'comma-spacing': [ - 'error', - { - before: false, - after: true - } - ], - 'array-bracket-spacing': 1, - 'arrow-spacing': [ - 'error', - { - before: true, - after: true - } - ], - 'object-curly-spacing': [ - 'error', - 'always' - ], - 'comma-dangle': [ - 'error', - 'never' - ], - 'no-unreachable': 'error', - quotes: [ - 'error', - 'single' - ], - 'quote-props': [ - 'error', - 'as-needed' - ], - semi: 'error', - 'no-extra-semi': 'error', - 'semi-spacing': 'error', - 'no-spaced-func': 'error', - 'no-throw-literal': 'error', - 'space-before-blocks': 'error', - 'space-before-function-paren': [ - 'error', - 'never' - ], - 'space-infix-ops': 'error', - 'space-unary-ops': 'error', - 'no-var': 'warn', - 'prefer-const': 'warn', - strict: [ - 'error', - 'global' - ], - 'no-restricted-globals': [ - 'error', - { - name: 'context', - message: 'Don\'t use Mocha\'s global context' - } - ], - 'no-prototype-builtins': 'off', - 'mocha-no-only/mocha-no-only': [ - 'error' - ], - 'no-empty': 'off', - 'eol-last': 'warn', - 'no-multiple-empty-lines': ['warn', { max: 2 }] - } -}; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000000..29bafed34d9 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,198 @@ +import { defineConfig, globalIgnores } from 'eslint/config'; +import mochaNoOnly from 'eslint-plugin-mocha-no-only'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; +import js from '@eslint/js'; + +export default defineConfig([ + globalIgnores([ + '**/tools', + '**/dist', + 'test/files/*', + '**/benchmarks', + '**/*.min.js', + '**/docs/js/native.js', + '!**/.*', + '**/node_modules', + '**/.git', + '**/data' + ]), + js.configs.recommended, + // general options + { + languageOptions: { + globals: globals.node, + ecmaVersion: 2022, // nodejs 18.0.0, + sourceType: 'commonjs' + }, + rules: { + 'comma-style': 'error', + + indent: ['error', 2, { + SwitchCase: 1, + VariableDeclarator: 2 + }], + + 'keyword-spacing': 'error', + 'no-whitespace-before-property': 'error', + 'no-buffer-constructor': 'warn', + 'no-console': 'off', + 'no-constant-condition': 'off', + 'no-multi-spaces': 'error', + 'func-call-spacing': 'error', + 'no-trailing-spaces': 'error', + 'no-undef': 'error', + 'no-unneeded-ternary': 'error', + 'no-const-assign': 'error', + 'no-useless-rename': 'error', + 'no-dupe-keys': 'error', + 'space-in-parens': ['error', 'never'], + + 'spaced-comment': ['error', 'always', { + block: { + markers: ['!'], + balanced: true + } + }], + + 'key-spacing': ['error', { + beforeColon: false, + afterColon: true + }], + + 'comma-spacing': ['error', { + before: false, + after: true + }], + + 'array-bracket-spacing': 1, + + 'arrow-spacing': ['error', { + before: true, + after: true + }], + + 'object-curly-spacing': ['error', 'always'], + 'comma-dangle': ['error', 'never'], + 'no-unreachable': 'error', + quotes: ['error', 'single'], + 'quote-props': ['error', 'as-needed'], + semi: 'error', + 'no-extra-semi': 'error', + 'semi-spacing': 'error', + 'no-spaced-func': 'error', + 'no-throw-literal': 'error', + 'space-before-blocks': 'error', + 'space-before-function-paren': ['error', 'never'], + 'space-infix-ops': 'error', + 'space-unary-ops': 'error', + 'no-var': 'warn', + 'prefer-const': 'warn', + strict: ['error', 'global'], + + 'no-restricted-globals': ['error', { + name: 'context', + message: 'Don\'t use Mocha\'s global context' + }], + + 'no-prototype-builtins': 'off', + 'no-empty': 'off', + 'eol-last': 'warn', + + 'no-multiple-empty-lines': ['warn', { + max: 2 + }] + } + }, + // general typescript options + { + files: ['**/*.{ts,tsx}', '**/*.md/*.ts', '**/*.md/*.typescript'], + extends: [ + tseslint.configs.recommended + ], + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: [], + defaultProject: 'tsconfig.json' + } + } + }, + rules: { + '@typescript-eslint/triple-slash-reference': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-empty-function': 'off', + + 'spaced-comment': ['error', 'always', { + block: { + markers: ['!'], + balanced: true + }, + + markers: ['/'] + }], + + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/prefer-optional-chain': 'error', + '@typescript-eslint/no-dupe-class-members': 'error', + '@typescript-eslint/no-redeclare': 'error', + '@typescript-eslint/space-infix-ops': 'off', + '@typescript-eslint/no-require-imports': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + '@typescript-eslint/no-wrapper-object-types': 'off', + '@typescript-eslint/no-unused-expressions': 'off', + '@typescript-eslint/no-unsafe-function-type': 'off' + } + }, + // type test specific options + { + files: ['test/types/**/*.ts'], + rules: { + '@typescript-eslint/no-empty-interface': 'off' + } + }, + // test specific options (including type tests) + { + files: ['test/**/*.js', 'test/**/*.ts'], + ignores: ['deno*.mjs'], + plugins: { + 'mocha-no-only': mochaNoOnly + }, + languageOptions: { + globals: globals.mocha + }, + rules: { + 'no-self-assign': 'off', + 'mocha-no-only/mocha-no-only': ['error'] + } + }, + // deno specific options + { + files: ['**/deno*.mjs'], + languageOptions: { + globals: { + // "globals" currently has no definition for deno + Deno: 'readonly' + } + } + }, + // general options for module files + { + files: ['**/*.mjs'], + languageOptions: { + sourceType: 'module' + } + }, + // doc script specific options + { + files: ['**/docs/js/**/*.js'], + languageOptions: { + globals: { + ...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])), + ...globals.browser } + } + } +]); diff --git a/lib/types/subdocument.js b/lib/types/subdocument.js index 651567c6e5c..caac6a0ca87 100644 --- a/lib/types/subdocument.js +++ b/lib/types/subdocument.js @@ -257,7 +257,7 @@ Subdocument.prototype.ownerDocument = function() { return this.$__.ownerDocument; } - let parent = this; // eslint-disable-line consistent-this + let parent = this; const paths = []; const seenDocs = new Set([parent]); @@ -289,7 +289,7 @@ Subdocument.prototype.ownerDocument = function() { */ Subdocument.prototype.$__fullPathWithIndexes = function() { - let parent = this; // eslint-disable-line consistent-this + let parent = this; const paths = []; const seenDocs = new Set([parent]); diff --git a/package.json b/package.json index 5916ca4dc84..1c1a18dcc8b 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,7 @@ "sift": "17.1.3" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^8.31.1", - "@typescript-eslint/parser": "^8.31.1", + "typescript-eslint": "^8.31.1", "acquit": "1.3.0", "acquit-ignore": "0.2.1", "acquit-require": "0.1.1", @@ -80,9 +79,9 @@ "docs:prepare:publish:6x": "git checkout 6.x && git merge 6.x && npm run docs:clean:stable && env DOCS_DEPLOY=true npm run docs:generate && mv ./docs/6.x ./tmp && git checkout gh-pages && npm run docs:copy:tmp:6x", "docs:prepare:publish:7x": "env DOCS_DEPLOY=true npm run docs:generate && git checkout gh-pages && rimraf ./docs/7.x && mv ./tmp ./docs/7.x", "docs:check-links": "blc http://127.0.0.1:8089 -ro", - "lint": "ESLINT_USE_FLAT_CONFIG=false eslint .", - "lint-js": "ESLINT_USE_FLAT_CONFIG=false eslint . --ext .js --ext .cjs", - "lint-ts": "ESLINT_USE_FLAT_CONFIG=false eslint . --ext .ts", + "lint": "eslint .", + "lint-js": "eslint . --ext .js --ext .cjs", + "lint-ts": "eslint . --ext .ts", "lint-md": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#benchmarks\"", "release": "git pull && git push origin master --tags && npm publish", "release-5x": "git pull origin 5.x && git push origin 5.x && git push origin 5.x --tags && npm publish --tag 5x", diff --git a/scripts/website.js b/scripts/website.js index f8cdb9c49d8..4f1f5fbf226 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -26,12 +26,12 @@ const isMain = require.main === module; let jobs = []; try { jobs = require('../docs/data/jobs.json'); -} catch (err) {} +} catch {} let opencollectiveSponsors = []; try { opencollectiveSponsors = require('../docs/data/opencollective.json'); -} catch (err) {} +} catch {} require('acquit-ignore')(); @@ -328,7 +328,7 @@ const versionObj = (() => { // Create api dir if it doesn't already exist try { fs.mkdirSync(path.join(cwd, './docs/api')); -} catch (err) {} // eslint-disable-line no-empty +} catch {} const docsFilemap = require('../docs/source/index'); const files = Object.keys(docsFilemap.fileMap); diff --git a/test/.eslintrc.yml b/test/.eslintrc.yml deleted file mode 100644 index b71fc46a9be..00000000000 --- a/test/.eslintrc.yml +++ /dev/null @@ -1,7 +0,0 @@ -env: - mocha: true -rules: - # In `document.test.js` we sometimes use self assignment to test setters - no-self-assign: off -ignorePatterns: - - deno*.mjs diff --git a/test/deno.mjs b/test/deno.mjs index c65e54807ed..e700c520943 100644 --- a/test/deno.mjs +++ b/test/deno.mjs @@ -1,17 +1,15 @@ -'use strict'; +import { createRequire } from 'node:module'; +import process from 'node:process'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; -import { createRequire } from "node:module"; -import process from "node:process"; -import { resolve } from "node:path"; -import {fileURLToPath} from "node:url"; - -import { spawn } from "node:child_process"; +import { spawn } from 'node:child_process'; Error.stackTraceLimit = 100; const require = createRequire(import.meta.url); -const fixtures = require('./mocha-fixtures.js') +const fixtures = require('./mocha-fixtures.js'); await fixtures.mochaGlobalSetup(); @@ -26,9 +24,9 @@ child.on('exit', (code, signal) => { signal ? doExit(-100) : doExit(code); }); -Deno.addSignalListener("SIGINT", () => { - console.log("SIGINT"); - child.kill("SIGINT"); +Deno.addSignalListener('SIGINT', () => { + console.log('SIGINT'); + child.kill('SIGINT'); doExit(-2); }); diff --git a/test/deno_mocha.mjs b/test/deno_mocha.mjs index bd06f431737..03cb9bf193c 100644 --- a/test/deno_mocha.mjs +++ b/test/deno_mocha.mjs @@ -1,7 +1,5 @@ -'use strict'; - -import { createRequire } from "node:module"; -import process from "node:process"; +import { createRequire } from 'node:module'; +import process from 'node:process'; // Workaround for Mocha getting terminal width, which currently requires `--unstable` Object.defineProperty(process.stdout, 'getWindowSize', { @@ -10,7 +8,7 @@ Object.defineProperty(process.stdout, 'getWindowSize', { } }); -import { parse } from "https://deno.land/std/flags/mod.ts" +import { parse } from 'https://deno.land/std/flags/mod.ts'; const args = parse(Deno.args); Error.stackTraceLimit = 100; @@ -49,6 +47,6 @@ for (const file of files) { } mocha.run(function(failures) { - process.exitCode = failures ? 1 : 0; // exit with non-zero status if there were failures + process.exitCode = failures ? 1 : 0; // exit with non-zero status if there were failures process.exit(process.exitCode); }); diff --git a/test/types/.eslintrc.yml b/test/types/.eslintrc.yml deleted file mode 100644 index 7e081732529..00000000000 --- a/test/types/.eslintrc.yml +++ /dev/null @@ -1,2 +0,0 @@ -rules: - "@typescript-eslint/no-empty-interface": off \ No newline at end of file From 7adfc99dffe0efbc723cb5afd802411afe3f73f1 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Tue, 6 May 2025 12:10:05 +0200 Subject: [PATCH 068/209] chore(dev-deps): change acquit to use espree PR to fix invalid syntax errors in website generation. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1c1a18dcc8b..2fd3b8678ff 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "typescript-eslint": "^8.31.1", - "acquit": "1.3.0", + "acquit": "git+https://github.com/hasezoey/acquit.git#0e98e3292212dae9f25c889fb3a46f3d6688ca55", "acquit-ignore": "0.2.1", "acquit-require": "0.1.1", "ajv": "8.17.1", From 80f2c5e2a342a84cf7fe2c43a0c78be9ef231111 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Tue, 6 May 2025 12:10:31 +0200 Subject: [PATCH 069/209] docs(browser): fix typo --- docs/browser.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/browser.md b/docs/browser.md index 3044900eebc..81b723ddef0 100644 --- a/docs/browser.md +++ b/docs/browser.md @@ -1,4 +1,4 @@ # Mongoose in the Browser As of Mongoose 9, [Mongoose's browser build is now in the `@mongoosejs/browser` npm package](https://github.com/mongoosejs/mongoose-browser). -The documentation has been moved to the [`@mongoosejs/browser` REAME](https://github.com/mongoosejs/mongoose-browser?tab=readme-ov-file#mongoosejsbrowser). +The documentation has been moved to the [`@mongoosejs/browser` README](https://github.com/mongoosejs/mongoose-browser?tab=readme-ov-file#mongoosejsbrowser). From 0b456671b166f60d9793f2813b5f04c34eab2888 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 6 May 2025 17:23:18 -0400 Subject: [PATCH 070/209] use new version of acquit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2fd3b8678ff..f79b0305387 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "typescript-eslint": "^8.31.1", - "acquit": "git+https://github.com/hasezoey/acquit.git#0e98e3292212dae9f25c889fb3a46f3d6688ca55", + "acquit": "1.4.0", "acquit-ignore": "0.2.1", "acquit-require": "0.1.1", "ajv": "8.17.1", From c66b840baf39ad1c822160bfc7506a8aa2a4b196 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 13 May 2025 15:26:23 -0400 Subject: [PATCH 071/209] BREAKING CHANGE: make FilterQuery properties no longer resolve to `any` in TypeScript --- test/query.test.js | 1 + test/types/queries.test.ts | 2 +- types/query.d.ts | 26 ++++++++++++++++++++------ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/test/query.test.js b/test/query.test.js index 9cc990591e9..1ac46d11627 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -4452,6 +4452,7 @@ describe('Query', function() { assert.strictEqual(deletedTarget?.name, targetName); const target = await Person.find({}).findById(_id); + assert.strictEqual(target, null); }); }); diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 35ff6f24d6e..ff9bc9c4b14 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -610,7 +610,7 @@ function gh14473() { const generateExists = () => { const query: FilterQuery = { deletedAt: { $ne: null } }; - const query2: FilterQuery = { deletedAt: { $lt: new Date() } }; + const query2: FilterQuery = { deletedAt: { $lt: new Date() } } as FilterQuery; }; } diff --git a/types/query.d.ts b/types/query.d.ts index 020ba181bb3..cc25267f4ab 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -1,7 +1,23 @@ declare module 'mongoose' { import mongodb = require('mongodb'); - export type Condition = T | QuerySelector | any; + type StringQueryTypeCasting = string | RegExp; + type ObjectIdQueryTypeCasting = Types.ObjectId | string; + type UUIDQueryTypeCasting = Types.UUID | string; + type BufferQueryCasting = Buffer | mongodb.Binary | number[] | string | { $binary: string | mongodb.Binary }; + type QueryTypeCasting = T extends string + ? StringQueryTypeCasting + : T extends Types.ObjectId + ? ObjectIdQueryTypeCasting + : T extends Types.UUID + ? UUIDQueryTypeCasting + : T extends Buffer + ? BufferQueryCasting + : T; + + export type ApplyBasicQueryCasting = T | T[] | (T extends (infer U)[] ? QueryTypeCasting : T); + + export type Condition = ApplyBasicQueryCasting> | QuerySelector>>; /** * Filter query to select the documents that match the query @@ -12,9 +28,7 @@ declare module 'mongoose' { */ type RootFilterQuery = FilterQuery | Query | Types.ObjectId; - type FilterQuery = { - [P in keyof T]?: Condition; - } & RootQuerySelector & { _id?: Condition; }; + type FilterQuery = { [P in keyof T]?: Condition; } & RootQuerySelector; type MongooseBaseQueryOptionKeys = | 'context' @@ -58,13 +72,13 @@ declare module 'mongoose' { type QuerySelector = { // Comparison - $eq?: T; + $eq?: T | null | undefined; $gt?: T; $gte?: T; $in?: [T] extends AnyArray ? Unpacked[] : T[]; $lt?: T; $lte?: T; - $ne?: T; + $ne?: T | null | undefined; $nin?: [T] extends AnyArray ? Unpacked[] : T[]; // Logical $not?: T extends string ? QuerySelector | RegExp : QuerySelector; From e109cab746979f503d28cdf0456f49f1677c869d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 13 May 2025 15:37:56 -0400 Subject: [PATCH 072/209] test: add some docs --- test/types/queries.test.ts | 9 +++++++++ types/query.d.ts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index ff9bc9c4b14..95fdff2ab23 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -678,3 +678,12 @@ function gh14841() { $expr: { $lt: [{ $size: '$owners' }, 10] } }; } + +function gh14510() { + // From https://stackoverflow.com/questions/56505560/how-to-fix-ts2322-could-be-instantiated-with-a-different-subtype-of-constraint: + // "Never assign a concrete type to a generic type parameter, consider it as read-only!" + // This function is generally something you shouldn't do in TypeScript, can work around it with `as` though. + function findById(model: Model, _id: Types.ObjectId | string) { + return model.find({_id: _id} as FilterQuery); + } +} diff --git a/types/query.d.ts b/types/query.d.ts index cc25267f4ab..6ef11a21e9f 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -26,7 +26,7 @@ declare module 'mongoose' { * { age: { $gte: 30 } } * ``` */ - type RootFilterQuery = FilterQuery | Query | Types.ObjectId; + type RootFilterQuery = FilterQuery; type FilterQuery = { [P in keyof T]?: Condition; } & RootQuerySelector; From e946ff055f4e0e37f422888d56ffd14d49207d09 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 13 May 2025 15:46:34 -0400 Subject: [PATCH 073/209] docs: add note about FilterQuery changes to migrating_to_9 --- docs/migrating_to_9.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index a2b508cb93d..211efac6b71 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -238,3 +238,29 @@ await test.save(); test.uuid; // string ``` + +## TypeScript + +### FilterQuery Properties No Longer Resolve to any + +In Mongoose 9, the `FilterQuery` type, which is the type of the first param to `Model.find()`, `Model.findOne()`, etc. now enforces stronger types for top-level keys. + +```typescript +const schema = new Schema({ age: Number }); +const TestModel = mongoose.model('Test', schema); + +TestModel.find({ age: 'not a number' }); // Works in Mongoose 8, TS error in Mongoose 9 +TestModel.find({ age: { $notAnOperator: 42 } }); // Works in Mongoose 8, TS error in Mongoose 9 +``` + +This change is backwards breaking if you use generics when creating queries as shown in the following example. +If you run into the following issue or any similar issues, you can use `as FilterQuery`. + +```typescript +// From https://stackoverflow.com/questions/56505560/how-to-fix-ts2322-could-be-instantiated-with-a-different-subtype-of-constraint: +// "Never assign a concrete type to a generic type parameter, consider it as read-only!" +// This function is generally something you shouldn't do in TypeScript, can work around it with `as` though. +function findById(model: Model, _id: Types.ObjectId | string) { + return model.find({_id: _id} as FilterQuery); // In Mongoose 8, this `as` was not required +} +``` From f6260d5848101a9f4aa98128afa2b4ec470d9a28 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 13 May 2025 15:58:38 -0400 Subject: [PATCH 074/209] style: fix lint --- test/types/queries.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 95fdff2ab23..e9b7f39991d 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -684,6 +684,6 @@ function gh14510() { // "Never assign a concrete type to a generic type parameter, consider it as read-only!" // This function is generally something you shouldn't do in TypeScript, can work around it with `as` though. function findById(model: Model, _id: Types.ObjectId | string) { - return model.find({_id: _id} as FilterQuery); + return model.find({ _id: _id } as FilterQuery); } } From 848478a9ad6a7a7de508249f8cef127ae348af72 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 26 May 2025 14:36:50 -0400 Subject: [PATCH 075/209] refactor: remove unnecessary async iterator checks --- lib/aggregate.js | 15 +++------------ lib/query.js | 19 +++++-------------- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/lib/aggregate.js b/lib/aggregate.js index 8ad43f5b689..8dba993b086 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -1139,24 +1139,15 @@ Aggregate.prototype.finally = function(onFinally) { * console.log(doc.name); * } * - * Node.js 10.x supports async iterators natively without any flags. You can - * enable async iterators in Node.js 8.x using the [`--harmony_async_iteration` flag](https://github.com/tc39/proposal-async-iteration/issues/117#issuecomment-346695187). - * - * **Note:** This function is not set if `Symbol.asyncIterator` is undefined. If - * `Symbol.asyncIterator` is undefined, that means your Node.js version does not - * support async iterators. - * * @method [Symbol.asyncIterator] * @memberOf Aggregate * @instance * @api public */ -if (Symbol.asyncIterator != null) { - Aggregate.prototype[Symbol.asyncIterator] = function() { - return this.cursor({ useMongooseAggCursor: true }).transformNull()._transformForAsyncIterator(); - }; -} + Aggregate.prototype[Symbol.asyncIterator] = function() { + return this.cursor({ useMongooseAggCursor: true }).transformNull()._transformForAsyncIterator(); + }; /*! * Helpers diff --git a/lib/query.js b/lib/query.js index d8961e4b015..9a8be9c0259 100644 --- a/lib/query.js +++ b/lib/query.js @@ -5406,26 +5406,17 @@ Query.prototype.nearSphere = function() { * console.log(doc.name); * } * - * Node.js 10.x supports async iterators natively without any flags. You can - * enable async iterators in Node.js 8.x using the [`--harmony_async_iteration` flag](https://github.com/tc39/proposal-async-iteration/issues/117#issuecomment-346695187). - * - * **Note:** This function is not if `Symbol.asyncIterator` is undefined. If - * `Symbol.asyncIterator` is undefined, that means your Node.js version does not - * support async iterators. - * * @method [Symbol.asyncIterator] * @memberOf Query * @instance * @api public */ -if (Symbol.asyncIterator != null) { - Query.prototype[Symbol.asyncIterator] = function queryAsyncIterator() { - // Set so QueryCursor knows it should transform results for async iterators into `{ value, done }` syntax - this._mongooseOptions._asyncIterator = true; - return this.cursor(); - }; -} + Query.prototype[Symbol.asyncIterator] = function queryAsyncIterator() { + // Set so QueryCursor knows it should transform results for async iterators into `{ value, done }` syntax + this._mongooseOptions._asyncIterator = true; + return this.cursor(); + }; /** * Specifies a `$polygon` condition From 56137d488076005e8718d01eac0345637d93ba02 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 26 May 2025 14:39:13 -0400 Subject: [PATCH 076/209] style: fix lint --- lib/aggregate.js | 6 +++--- lib/query.js | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/aggregate.js b/lib/aggregate.js index 8dba993b086..560c9e228c8 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -1145,9 +1145,9 @@ Aggregate.prototype.finally = function(onFinally) { * @api public */ - Aggregate.prototype[Symbol.asyncIterator] = function() { - return this.cursor({ useMongooseAggCursor: true }).transformNull()._transformForAsyncIterator(); - }; +Aggregate.prototype[Symbol.asyncIterator] = function() { + return this.cursor({ useMongooseAggCursor: true }).transformNull()._transformForAsyncIterator(); +}; /*! * Helpers diff --git a/lib/query.js b/lib/query.js index 9a8be9c0259..8f3702d0c11 100644 --- a/lib/query.js +++ b/lib/query.js @@ -5412,11 +5412,11 @@ Query.prototype.nearSphere = function() { * @api public */ - Query.prototype[Symbol.asyncIterator] = function queryAsyncIterator() { - // Set so QueryCursor knows it should transform results for async iterators into `{ value, done }` syntax - this._mongooseOptions._asyncIterator = true; - return this.cursor(); - }; +Query.prototype[Symbol.asyncIterator] = function queryAsyncIterator() { + // Set so QueryCursor knows it should transform results for async iterators into `{ value, done }` syntax + this._mongooseOptions._asyncIterator = true; + return this.cursor(); +}; /** * Specifies a `$polygon` condition From 4df6527ab034d270a14181b8cac0c77c3d03daa8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 18 Jun 2025 16:07:43 -0400 Subject: [PATCH 077/209] WIP adding Schema.create() for better TypeScript typings --- test/types/schema.create.test.ts | 1820 ++++++++++++++++++++++++++++++ types/inferhydrateddoctype.d.ts | 147 +++ 2 files changed, 1967 insertions(+) create mode 100644 test/types/schema.create.test.ts create mode 100644 types/inferhydrateddoctype.d.ts diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts new file mode 100644 index 00000000000..bf6535b69cc --- /dev/null +++ b/test/types/schema.create.test.ts @@ -0,0 +1,1820 @@ +import { + DefaultSchemaOptions, + HydratedArraySubdocument, + HydratedSingleSubdocument, + Schema, + Document, + HydratedDocument, + IndexDefinition, + IndexOptions, + InferRawDocType, + InferSchemaType, + InsertManyOptions, + JSONSerialized, + ObtainDocumentType, + ObtainSchemaGeneric, + ResolveSchemaOptions, + SchemaDefinition, + SchemaTypeOptions, + Model, + SchemaType, + Types, + Query, + model, + ValidateOpts, + BufferToBinary, + CallbackWithoutResultAndOptionalError +} from 'mongoose'; +import { Binary, BSON } from 'mongodb'; +import { expectType, expectError, expectAssignable } from 'tsd'; +import { ObtainDocumentPathType, ResolvePathType } from '../../types/inferschematype'; + +enum Genre { + Action, + Adventure, + Comedy +} + +interface Actor { + name: string, + age: number +} +const actorSchema = + new Schema, Actor>({ name: { type: String }, age: { type: Number } }); + +interface Movie { + title?: string, + featuredIn?: string, + rating?: number, + genre?: string, + actionIntensity?: number, + status?: string, + actors: Actor[] +} + +const movieSchema = new Schema>({ + title: { + type: String, + index: 'text' + }, + featuredIn: { + type: String, + enum: ['Favorites', null], + default: null + }, + rating: { + type: Number, + required: [true, 'Required'], + min: [0, 'MinValue'], + max: [5, 'MaxValue'] + }, + genre: { + type: String, + enum: Genre, + required: true + }, + actionIntensity: { + type: Number, + required: [ + function(this: { genre: Genre }) { + return this.genre === Genre.Action; + }, + 'Action intensity required for action genre' + ] + }, + status: { + type: String, + enum: { + values: ['Announced', 'Released'], + message: 'Invalid value for `status`' + } + }, + actors: { + type: [actorSchema], + default: undefined + } +}); + +movieSchema.index({ status: 1, 'actors.name': 1 }); +movieSchema.index({ title: 'text' }, { + weights: { title: 10 } +}); +movieSchema.index({ rating: -1 }); +movieSchema.index({ title: 1 }, { unique: true }); +movieSchema.index({ title: 1 }, { unique: [true, 'Title must be unique'] as const }); +movieSchema.index({ tile: 'ascending' }); +movieSchema.index({ tile: 'asc' }); +movieSchema.index({ tile: 'descending' }); +movieSchema.index({ tile: 'desc' }); +movieSchema.index({ tile: 'hashed' }); +movieSchema.index({ tile: 'geoHaystack' }); + +expectError[0]>({ tile: 2 }); // test invalid number +expectError[0]>({ tile: -2 }); // test invalid number +expectError[0]>({ tile: '' }); // test empty string +expectError[0]>({ tile: 'invalid' }); // test invalid string +expectError[0]>({ tile: new Date() }); // test invalid type +expectError[0]>({ tile: true }); // test that booleans are not allowed +expectError[0]>({ tile: false }); // test that booleans are not allowed + +// Using `SchemaDefinition` +interface IProfile { + age: number; +} +const ProfileSchemaDef: SchemaDefinition = { age: Number }; +export const ProfileSchema = new Schema>(ProfileSchemaDef); + +interface IUser { + email: string; + profile: IProfile; +} + +const ProfileSchemaDef2: SchemaDefinition = { + age: Schema.Types.Number +}; + +const ProfileSchema2: Schema> = new Schema(ProfileSchemaDef2); + +const UserSchemaDef: SchemaDefinition = { + email: String, + profile: ProfileSchema2 +}; + +async function gh9857() { + interface User { + name: number; + active: boolean; + points: number; + } + + type UserDocument = Document; + type UserSchemaDefinition = SchemaDefinition; + type UserModel = Model; + + let u: UserSchemaDefinition; + expectError(u = { + name: { type: String }, + active: { type: Boolean }, + points: Number + }); +} + +function gh10261() { + interface ValuesEntity { + values: string[]; + } + + const type: ReadonlyArray = [String]; + const colorEntitySchemaDefinition: SchemaDefinition = { + values: { + type: type, + required: true + } + }; +} + +function gh10287() { + interface SubSchema { + testProp: string; + } + + const subSchema = new Schema, SubSchema>({ + testProp: Schema.Types.String + }); + + interface MainSchema { + subProp: SubSchema + } + + const mainSchema1 = new Schema, MainSchema>({ + subProp: subSchema + }); + + const mainSchema2 = new Schema, MainSchema>({ + subProp: { + type: subSchema + } + }); +} + +function gh10370() { + const movieSchema = new Schema, Movie>({ + actors: { + type: [actorSchema] + } + }); +} + +function gh10409() { + interface Something { + field: Date; + } + const someSchema = new Schema, Something>({ + field: { type: Date } + }); +} + +function gh10605() { + interface ITest { + arrayField?: string[]; + object: { + value: number + }; + } + const schema = new Schema({ + arrayField: [String], + object: { + type: { + value: { + type: Number + } + } + } + }); +} + +function gh10605_2() { + interface ITestSchema { + someObject: Array<{ id: string }> + } + + const testSchema = new Schema({ + someObject: { type: [{ id: String }] } + }); +} + +function gh10731() { + interface IProduct { + keywords: string[]; + } + + const productSchema = new Schema({ + keywords: { + type: [ + { + type: String, + trim: true, + lowercase: true, + required: true + } + ], + required: true + } + }); +} + +function gh10789() { + interface IAddress { + city: string; + state: string; + country: string; + } + + interface IUser { + name: string; + addresses: IAddress[]; + } + + const addressSchema = new Schema({ + city: { + type: String, + required: true + }, + state: { + type: String, + required: true + }, + country: { + type: String, + required: true + } + }); + + const userSchema = new Schema({ + name: { + type: String, + required: true + }, + addresses: { + type: [ + { + type: addressSchema, + required: true + } + ], + required: true + } + }); +} + +function gh11439() { + type Book = { + collection: string + }; + + const bookSchema = new Schema({ + collection: String + }, { + suppressReservedKeysWarning: true + }); +} + +function gh11448() { + interface IUser { + name: string; + age: number; + } + + const userSchema = new Schema({ name: String, age: Number }); + + userSchema.pick>(['age']); +} + +function gh11435(): void { + interface User { + ids: Types.Array; + } + + const schema = new Schema({ + ids: { + type: [{ type: Schema.Types.ObjectId, ref: 'Something' }], + default: [] + } + }); +} + +// timeSeries +Schema.create({}, { expires: '5 seconds' }); +expectError(Schema.create({}, { expireAfterSeconds: '5 seconds' })); +Schema.create({}, { expireAfterSeconds: 5 }); + +function gh10900(): void { + type TMenuStatus = Record[]; + + interface IUserProp { + menuStatus: TMenuStatus; + } + + const patientSchema = new Schema({ + menuStatus: { type: Schema.Types.Mixed, default: {} } + }); +} + +export function autoTypedSchema() { + // Test auto schema type obtaining with all possible path types. + + class Int8 extends SchemaType { + constructor(key, options) { + super(key, options, 'Int8'); + } + cast(val) { + let _val = Number(val); + if (isNaN(_val)) { + throw new Error('Int8: ' + val + ' is not a number'); + } + _val = Math.round(_val); + if (_val < -0x80 || _val > 0x7F) { + throw new Error('Int8: ' + val + + ' is outside of the range of valid 8-bit ints'); + } + return _val; + } + } + + type TestSchemaType = { + string1?: string | null; + string2?: string | null; + string3?: string | null; + string4?: string | null; + string5: string; + number1?: number | null; + number2?: number | null; + number3?: number | null; + number4?: number | null; + number5: number; + date1?: Date | null; + date2?: Date | null; + date3?: Date | null; + date4?: Date | null; + date5: Date; + buffer1?: Buffer | null; + buffer2?: Buffer | null; + buffer3?: Buffer | null; + buffer4?: Buffer | null; + boolean1?: boolean | null; + boolean2?: boolean | null; + boolean3?: boolean | null; + boolean4?: boolean | null; + boolean5: boolean; + mixed1?: any | null; + mixed2?: any | null; + mixed3?: any | null; + objectId1?: Types.ObjectId | null; + objectId2?: Types.ObjectId | null; + objectId3?: Types.ObjectId | null; + customSchema?: Int8 | null; + map1?: Map | null; + map2?: Map | null; + array1: string[]; + array2: any[]; + array3: any[]; + array4: any[]; + array5: any[]; + array6: string[]; + array7?: string[] | null; + array8?: string[] | null; + decimal1?: Types.Decimal128 | null; + decimal2?: Types.Decimal128 | null; + decimal3?: Types.Decimal128 | null; + }; + + const TestSchema = Schema.create({ + string1: String, + string2: 'String', + string3: 'string', + string4: Schema.Types.String, + string5: { type: String, default: 'ABCD' }, + number1: Number, + number2: 'Number', + number3: 'number', + number4: Schema.Types.Number, + number5: { type: Number, default: 10 }, + date1: Date, + date2: 'Date', + date3: 'date', + date4: Schema.Types.Date, + date5: { type: Date, default: new Date() }, + buffer1: Buffer, + buffer2: 'Buffer', + buffer3: 'buffer', + buffer4: Schema.Types.Buffer, + boolean1: Boolean, + boolean2: 'Boolean', + boolean3: 'boolean', + boolean4: Schema.Types.Boolean, + boolean5: { type: Boolean, default: true }, + mixed1: Object, + mixed2: {}, + mixed3: Schema.Types.Mixed, + objectId1: Schema.Types.ObjectId, + objectId2: 'ObjectId', + objectId3: 'ObjectID', + customSchema: Int8, + map1: { type: Map, of: String }, + map2: { type: Map, of: Number }, + array1: [String], + array2: Array, + array3: [Schema.Types.Mixed], + array4: [{}], + array5: [], + array6: { type: [String] }, + array7: { type: [String], default: undefined }, + array8: { type: [String], default: () => undefined }, + decimal1: Schema.Types.Decimal128, + decimal2: 'Decimal128', + decimal3: 'decimal128' + }); + + type InferredTestSchemaType = InferSchemaType; + + expectType({} as InferredTestSchemaType); + + const SchemaWithCustomTypeKey = Schema.create({ + name: { + customTypeKey: String, + required: true + } + }, { + typeKey: 'customTypeKey' + }); + + expectType({} as InferSchemaType['name']); + + const AutoTypedSchema = Schema.create({ + userName: { + type: String, + required: [true, 'userName is required'] + }, + description: String, + nested: Schema.create({ + age: { + type: Number, + required: true + }, + hobby: { + type: String, + required: false + } + }), + favoritDrink: { + type: String, + enum: ['Coffee', 'Tea'] + }, + favoritColorMode: { + type: String, + enum: { + values: ['dark', 'light'], + message: '{VALUE} is not supported' + }, + required: true + }, + friendID: { + type: Schema.Types.ObjectId + }, + nestedArray: { + type: [ + Schema.create({ + date: { type: Date, required: true }, + messages: Number + }) + ] + } + }, { + statics: { + staticFn() { + expectType>>(this); + return 'Returned from staticFn' as const; + } + }, + methods: { + instanceFn() { + expectType>>(this); + return 'Returned from DocumentInstanceFn' as const; + } + }, + query: { + byUserName(userName) { + expectAssignable>>(this); + return this.where({ userName }); + } + } + }); + + return AutoTypedSchema; +} + +export type AutoTypedSchemaType = { + schema: { + userName: string; + description?: string | null; + nested?: { + age: number; + hobby?: string | null + } | null, + favoritDrink?: 'Tea' | 'Coffee' | null, + favoritColorMode: 'dark' | 'light' + friendID?: Types.ObjectId | null; + nestedArray: Types.DocumentArray<{ + date: Date; + messages?: number | null; + }> + } + , statics: { + staticFn: () => 'Returned from staticFn' + }, + methods: { + instanceFn: () => 'Returned from DocumentInstanceFn' + } +}; + +// discriminator +const eventSchema = new Schema<{ message: string }>({ message: String }, { discriminatorKey: 'kind' }); +const batchSchema = new Schema<{ name: string }>({ name: String }, { discriminatorKey: 'kind' }); +batchSchema.discriminator('event', eventSchema); + +// discriminator statics +const eventSchema2 = Schema.create({ message: String }, { discriminatorKey: 'kind', statics: { static1: function() { + return 0; +} } }); +const batchSchema2 = Schema.create({ name: String }, { discriminatorKey: 'kind', statics: { static2: function() { + return 1; +} } }); +batchSchema2.discriminator('event', eventSchema2); + + +function encryptionType() { + const keyId = new BSON.UUID(); + expectError(Schema.create({ name: { type: String, encrypt: { keyId } } }, { encryptionType: 'newFakeEncryptionType' })); + expectError(Schema.create({ name: { type: String, encrypt: { keyId } } }, { encryptionType: 1 })); + + expectType(Schema.create({ name: { type: String, encrypt: { keyId } } }, { encryptionType: 'queryableEncryption' })); + expectType(Schema.create({ name: { type: String, encrypt: { keyId } } }, { encryptionType: 'csfle' })); +} + +function gh11828() { + interface IUser { + name: string; + age: number; + bornAt: Date; + isActive: boolean; + } + + const t: SchemaTypeOptions = { + type: Boolean, + default() { + return this.name === 'Hafez'; + } + }; + + new Schema({ + name: { type: String, default: () => 'Hafez' }, + age: { type: Number, default: () => 27 }, + bornAt: { type: Date, default: () => new Date() }, + isActive: { + type: Boolean, + default(): boolean { + return this.name === 'Hafez'; + } + } + }); +} + +function gh11997() { + interface IUser { + name: string; + } + + const userSchema = new Schema({ + name: { type: String, default: () => 'Hafez' } + }); + userSchema.index({ name: 1 }, { weights: { name: 1 } }); +} + +function gh12003() { + const baseSchemaOptions = { + versionKey: false + }; + + const BaseSchema = Schema.create({ + name: String + }, baseSchemaOptions); + + type BaseSchemaType = InferSchemaType; + + type TSchemaOptions = ResolveSchemaOptions>; + expectType<'type'>({} as TSchemaOptions['typeKey']); + + expectType<{ name?: string | null }>({} as BaseSchemaType); +} + +function gh11987() { + interface IUser { + name: string; + email: string; + organization: Types.ObjectId; + } + + const userSchema = new Schema({ + name: { type: String, required: true }, + email: { type: String, required: true }, + organization: { type: Schema.Types.ObjectId, ref: 'Organization' } + }); + + expectType>(userSchema.path<'name'>('name')); + expectError(userSchema.path<'foo'>('name')); + expectType>(userSchema.path<'name'>('name').OptionsConstructor); +} + +function gh12030() { + const Schema1 = Schema.create({ + users: [ + { + username: { type: String } + } + ] + }); + + type A = ResolvePathType<[ + { + username: { type: String } + } + ]>; + expectType>({} as A); + + type B = ObtainDocumentType<{ + users: [ + { + username: { type: String } + } + ] + }>; + expectType<{ + users: Types.DocumentArray<{ + username?: string | null + }>; + }>({} as B); + + expectType<{ + users: Types.DocumentArray<{ + username?: string | null + }>; + }>({} as InferSchemaType); + + const Schema2 = Schema.create({ + createdAt: { type: Date, default: Date.now } + }); + + expectType<{ createdAt: Date }>({} as InferSchemaType); + + const Schema3 = Schema.create({ + users: [ + Schema.create({ + username: { type: String }, + credit: { type: Number, default: 0 } + }) + ] + }); + + expectType<{ + users: Types.DocumentArray<{ + credit: number; + username?: string | null; + }>; + }>({} as InferSchemaType); + + + const Schema4 = Schema.create({ + data: { type: { role: String }, default: {} } + }); + + expectType<{ data: { role?: string | null } }>({} as InferSchemaType); + + const Schema5 = Schema.create({ + data: { type: { role: Object }, default: {} } + }); + + expectType<{ data: { role?: any } }>({} as InferSchemaType); + + const Schema6 = Schema.create({ + track: { + backupCount: { + type: Number, + default: 0 + }, + count: { + type: Number, + default: 0 + } + } + }); + + expectType<{ + track?: { + backupCount: number; + count: number; + } | null; + }>({} as InferSchemaType); + +} + +function pluginOptions() { + interface SomePluginOptions { + option1?: string; + option2: number; + } + + function pluginFunction(schema: Schema, options: SomePluginOptions) { + return; // empty function, to satisfy lint option + } + + const schema = Schema.create({}); + expectType>(schema.plugin(pluginFunction)); // test that chaining would be possible + + // could not add strict tests that the parameters are inferred correctly, because i dont know how this would be done in tsd + + // test basic inferrence + expectError(schema.plugin(pluginFunction, {})); // should error because "option2" is not optional + schema.plugin(pluginFunction, { option2: 0 }); + schema.plugin(pluginFunction, { option1: 'string', option2: 1 }); + expectError(schema.plugin(pluginFunction, { option1: 'string' })); // should error because "option2" is not optional + expectError(schema.plugin(pluginFunction, { option2: 'string' })); // should error because "option2" type is "number" + expectError(schema.plugin(pluginFunction, { option1: 0 })); // should error because "option1" type is "string" + + // test plugins without options defined + function pluginFunction2(schema: Schema) { + return; // empty function, to satisfy lint option + } + schema.plugin(pluginFunction2); + expectError(schema.plugin(pluginFunction2, {})); // should error because no options argument is defined + + // test overwriting options + schema.plugin(pluginFunction2, { option2: 0 }); + expectError(schema.plugin(pluginFunction2, {})); // should error because "option2" is not optional +} + +function gh12205() { + const campaignSchema = Schema.create( + { + client: { + type: new Types.ObjectId(), + required: true + } + } + ); + + const Campaign = model('Campaign', campaignSchema); + const doc = new Campaign(); + expectType(doc.client); + + type ICampaign = InferSchemaType; + expectType<{ client: Types.ObjectId }>({} as ICampaign); + + type A = ObtainDocumentType<{ client: { type: Schema.Types.ObjectId, required: true } }>; + expectType<{ client: Types.ObjectId }>({} as A); + + type Foo = ObtainDocumentPathType<{ type: Schema.Types.ObjectId, required: true }, 'type'>; + expectType({} as Foo); + + type Bar = ResolvePathType; + expectType({} as Bar); + + /* type Baz = Schema.Types.ObjectId extends typeof Schema.Types.ObjectId ? string : number; + expectType({} as Baz); */ +} + + +function gh12450() { + const ObjectIdSchema = Schema.create({ + user: { type: Schema.Types.ObjectId } + }); + + expectType<{ + user?: Types.ObjectId | null; + }>({} as InferSchemaType); + + const Schema2 = Schema.create({ + createdAt: { type: Date, required: true }, + decimalValue: { type: Schema.Types.Decimal128, required: true } + }); + + expectType<{ createdAt: Date, decimalValue: Types.Decimal128 }>({} as InferSchemaType); + + const Schema3 = Schema.create({ + createdAt: { type: Date, required: true }, + decimalValue: { type: Schema.Types.Decimal128 } + }); + + expectType<{ createdAt: Date, decimalValue?: Types.Decimal128 | null }>({} as InferSchemaType); + + const Schema4 = Schema.create({ + createdAt: { type: Date }, + decimalValue: { type: Schema.Types.Decimal128 } + }); + + expectType<{ createdAt?: Date | null, decimalValue?: Types.Decimal128 | null }>({} as InferSchemaType); +} + +function gh12242() { + const dbExample = Schema.create( + { + active: { type: Number, enum: [0, 1] as const, required: true } + } + ); + + type Example = InferSchemaType; + expectType<0 | 1>({} as Example['active']); +} + +function testInferTimestamps() { + const schema = Schema.create({ + name: String + }, { timestamps: true }); + + type WithTimestamps = InferSchemaType; + // For some reason, expectType<{ createdAt: Date, updatedAt: Date, name?: string }> throws + // an error "Parameter type { createdAt: Date; updatedAt: Date; name?: string | undefined; } + // is not identical to argument type { createdAt: NativeDate; updatedAt: NativeDate; } & + // { name?: string | undefined; }" + expectType<{ createdAt: Date, updatedAt: Date } & { name?: string | null }>({} as WithTimestamps); + + const schema2 = Schema.create({ + name: String + }, { + timestamps: true, + methods: { myName(): string | undefined | null { + return this.name; + } } + }); + + type WithTimestamps2 = InferSchemaType; + // For some reason, expectType<{ createdAt: Date, updatedAt: Date, name?: string }> throws + // an error "Parameter type { createdAt: Date; updatedAt: Date; name?: string | undefined; } + // is not identical to argument type { createdAt: NativeDate; updatedAt: NativeDate; } & + // { name?: string | undefined; }" + expectType<{ name?: string | null }>({} as WithTimestamps2); +} + +function gh12431() { + const testSchema = Schema.create({ + testDate: { type: Date }, + testDecimal: { type: Schema.Types.Decimal128 } + }); + + type Example = InferSchemaType; + expectType<{ testDate?: Date | null, testDecimal?: Types.Decimal128 | null }>({} as Example); +} + +async function gh12593() { + const testSchema = Schema.create({ x: { type: Schema.Types.UUID } }); + + type Example = InferSchemaType; + expectType<{ x?: Buffer | null }>({} as Example); + + const Test = model('Test', testSchema); + + const doc = await Test.findOne({ x: '4709e6d9-61fd-435e-b594-d748eb196d8f' }).orFail(); + expectType(doc.x); + + const doc2 = new Test({ x: '4709e6d9-61fd-435e-b594-d748eb196d8f' }); + expectType(doc2.x); + + const doc3 = await Test.findOne({}).orFail().lean(); + expectType(doc3.x); + + const arrSchema = Schema.create({ arr: [{ type: Schema.Types.UUID }] }); + + type ExampleArr = InferSchemaType; + expectType<{ arr: Buffer[] }>({} as ExampleArr); +} + +function gh12562() { + const emailRegExp = /@/; + const userSchema = Schema.create( + { + email: { + type: String, + trim: true, + validate: { + validator: (value: string) => emailRegExp.test(value), + message: 'Email is not valid' + }, + index: { // uncomment the index object and for me trim was throwing an error + partialFilterExpression: { + email: { + $exists: true, + $ne: null + } + } + }, + select: false + } + } + ); +} + +function gh12590() { + const UserSchema = Schema.create({ + _password: String + }); + + type User = InferSchemaType; + + const path = UserSchema.path('hashed_password'); + expectType>>(path); + + UserSchema.path('hashed_password').validate(function(v) { + expectType>(this); + if (this._password && this._password.length < 8) { + this.invalidate('password', 'Password must be at least 8 characters.'); + } + }); + +} + +function gh12611() { + const reusableFields = { + description: { type: String, required: true }, + skills: { type: [Schema.Types.ObjectId], ref: 'Skill', default: [] } + } as const; + + const firstSchema = Schema.create({ + ...reusableFields, + anotherField: String + }); + + type Props = InferSchemaType; + expectType<{ + description: string; + skills: Types.ObjectId[]; + anotherField?: string | null; + }>({} as Props); +} + +function gh12782() { + const schemaObj = { test: { type: String, required: true } }; + const schema = Schema.create(schemaObj); + type Props = InferSchemaType; + expectType<{ + test: string + }>({} as Props); +} + +function gh12816() { + const schema = Schema.create({}, { overwriteModels: true }); +} + +function gh12869() { + const dbExampleConst = Schema.create( + { + active: { type: String, enum: ['foo', 'bar'] as const, required: true } + } + ); + + type ExampleConst = InferSchemaType; + expectType<'foo' | 'bar'>({} as ExampleConst['active']); + + const dbExample = Schema.create( + { + active: { type: String, enum: ['foo', 'bar'], required: true } + } + ); + + type Example = InferSchemaType; + expectType<'foo' | 'bar'>({} as Example['active']); +} + +function gh12882() { + // Array of strings + const arrString = Schema.create({ + fooArray: { + type: [{ + type: String, + required: true + }], + required: true + } + }); + type tArrString = InferSchemaType; + // Array of numbers using string definition + const arrNum = Schema.create({ + fooArray: { + type: [{ + type: 'Number', + required: true + }], + required: true + } + } as const); + type tArrNum = InferSchemaType; + expectType<{ + fooArray: number[] + } & { _id: Types.ObjectId }>({} as tArrNum); + // Array of object with key named "type" + const arrType = Schema.create({ + fooArray: { + type: [{ + type: { + type: String, + required: true + }, + foo: { + type: Number, + required: true + } + }], + required: true + } + }); + type tArrType = InferSchemaType; + expectType<{ + fooArray: Types.DocumentArray<{ + type: string; + foo: number; + }> + }>({} as tArrType); + // Readonly array of strings + const rArrString = Schema.create({ + fooArray: { + type: [{ + type: String, + required: true + }] as const, + required: true + } + }); + type rTArrString = InferSchemaType; + expectType<{ + fooArray: string[] + }>({} as rTArrString); + // Readonly array of numbers using string definition + const rArrNum = Schema.create({ + fooArray: { + type: [{ + type: 'Number', + required: true + }] as const, + required: true + } + }); + type rTArrNum = InferSchemaType; + expectType<{ + fooArray: number[] + } & { _id: Types.ObjectId }>({} as rTArrNum); + // Readonly array of object with key named "type" + const rArrType = Schema.create({ + fooArray: { + type: [{ + type: { + type: String, + required: true + }, + foo: { + type: Number, + required: true + } + }] as const, + required: true + } + }); + type rTArrType = InferSchemaType; + expectType<{ + fooArray: Array<{ + type: string; + foo: number; + } & { _id: Types.ObjectId }> + } & { _id: Types.ObjectId }>({} as rTArrType); +} + +function gh13534() { + const schema = Schema.create({ + myId: { type: Schema.ObjectId, required: true } + }); + const Test = model('Test', schema); + + const doc = new Test({ myId: '0'.repeat(24) }); + expectType(doc.myId); +} + +function maps() { + const schema = Schema.create({ + myMap: { type: Schema.Types.Map, of: Number, required: true } + }); + const Test = model('Test', schema); + + const doc = new Test({ myMap: { answer: 42 } }); + expectType>(doc.myMap); + expectType(doc.myMap!.get('answer')); +} + +function gh13514() { + const schema = Schema.create({ + email: { + type: String, + required: { + isRequired: true, + message: 'Email is required' + } as const + } + }); + const Test = model('Test', schema); + + const doc = new Test({ email: 'bar' }); + const str: string = doc.email; +} + +function gh13633() { + const schema = Schema.create({ name: String }); + + schema.pre('updateOne', { document: true, query: false }, function(next) { + }); + + schema.pre('updateOne', { document: true, query: false }, function(next, options) { + expectType | undefined>(options); + }); + + schema.post('save', function(res, next) { + }); + schema.pre('insertMany', function(next, docs) { + }); + schema.pre('insertMany', function(next, docs, options) { + expectType<(InsertManyOptions & { lean?: boolean }) | undefined>(options); + }); +} + +function gh13702() { + const schema = Schema.create({ name: String }); + expectType<[IndexDefinition, IndexOptions][]>(schema.indexes()); +} + +function gh13780() { + const schema = Schema.create({ num: Schema.Types.BigInt }); + type InferredType = InferSchemaType; + expectType(null as unknown as InferredType['num']); +} + +function gh13800() { + interface IUser { + firstName: string; + lastName: string; + someOtherField: string; + } + interface IUserMethods { + fullName(): string; + } + type UserModel = Model; + + // Typed Schema + const schema = new Schema({ + firstName: { type: String, required: true }, + lastName: { type: String, required: true } + }); + schema.method('fullName', function fullName() { + expectType(this.firstName); + expectType(this.lastName); + expectType(this.someOtherField); + expectType(this.fullName); + }); + + // Auto Typed Schema + const autoTypedSchema = Schema.create({ + firstName: { type: String, required: true }, + lastName: { type: String, required: true } + }); + autoTypedSchema.method('fullName', function fullName() { + expectType(this.firstName); + expectType(this.lastName); + expectError(this.someOtherField); + }); +} + +async function gh13797() { + interface IUser { + name: string; + } + new Schema({ name: { type: String, required: function() { + expectType(this); return true; + } } }); + new Schema({ name: { type: String, default: function() { + expectType(this); return ''; + } } }); +} + +declare const brand: unique symbol; +function gh14002() { + type Brand = T & { [brand]: U }; + type UserId = Brand; + + interface IUser { + userId: UserId; + } + + const userIdTypeHint = 'placeholder' as UserId; + const schema = Schema.create({ + userId: { type: String, required: true, __typehint: userIdTypeHint } + }); + expectType({} as InferSchemaType); +} + +function gh14028_methods() { + // Methods that have access to `this` should have access to typing of other methods on the schema + interface IUser { + firstName: string; + lastName: string; + age: number; + } + interface IUserMethods { + fullName(): string; + isAdult(): boolean; + } + type UserModel = Model; + + // Define methods on schema + const schema = new Schema({ + firstName: { type: String, required: true }, + lastName: { type: String, required: true }, + age: { type: Number, required: true } + }, { + methods: { + fullName() { + // Expect type of `this` to have fullName method + expectType(this.fullName); + return this.firstName + ' ' + this.lastName; + }, + isAdult() { + // Expect type of `this` to have isAdult method + expectType(this.isAdult); + return this.age >= 18; + } + } + }); + + const User = model('User', schema); + const user = new User({ firstName: 'John', lastName: 'Doe', age: 20 }); + // Trigger type assertions inside methods + user.fullName(); + user.isAdult(); + + // Expect type of methods to be inferred if accessed directly + expectType(schema.methods.fullName); + expectType(schema.methods.isAdult); + + // Define methods outside of schema + const schema2 = new Schema({ + firstName: { type: String, required: true }, + lastName: { type: String, required: true }, + age: { type: Number, required: true } + }); + + schema2.methods.fullName = function fullName() { + expectType(this.fullName); + return this.firstName + ' ' + this.lastName; + }; + + schema2.methods.isAdult = function isAdult() { + expectType(this.isAdult); + return true; + }; + + const User2 = model('User2', schema2); + const user2 = new User2({ firstName: 'John', lastName: 'Doe', age: 20 }); + user2.fullName(); + user2.isAdult(); + + type UserModelWithoutMethods = Model; + // Skip InstanceMethods + const schema3 = new Schema({ + firstName: { type: String, required: true }, + lastName: { type: String, required: true }, + age: { type: Number, required: true } + }, { + methods: { + fullName() { + // Expect methods to still have access to `this` type + expectType(this.firstName); + // As InstanceMethods type is not specified, expect type of this.fullName to be undefined + expectError(this.fullName); + return this.firstName + ' ' + this.lastName; + } + } + }); + + const User3 = model('User2', schema3); + const user3 = new User3({ firstName: 'John', lastName: 'Doe', age: 20 }); + expectError(user3.fullName()); +} + +function gh14028_statics() { + // Methods that have access to `this` should have access to typing of other methods on the schema + interface IUser { + firstName: string; + lastName: string; + age: number; + } + interface IUserStatics { + createWithFullName(name: string): Promise; + } + type UserModel = Model; + + // Define statics on schema + const schema = new Schema({ + firstName: { type: String, required: true }, + lastName: { type: String, required: true }, + age: { type: Number, required: true } + }, { + statics: { + createWithFullName(name: string) { + expectType(schema.statics.createWithFullName); + expectType(this.create); + + const [firstName, lastName] = name.split(' '); + return this.create({ firstName, lastName }); + } + } + }); + + // Trigger type assertions inside statics + schema.statics.createWithFullName('John Doe'); +} + +function gh13424() { + const subDoc = { + name: { type: String, required: true }, + controls: { type: String, required: true } + }; + + const testSchema = { + question: { type: String, required: true }, + subDocArray: { type: [subDoc], required: true } + }; + + const TestModel = model('TestModel', Schema.create(testSchema)); + + const doc = new TestModel({}); + expectType(doc.subDocArray[0]._id); +} + +function gh14147() { + const affiliateSchema = Schema.create({ + balance: { type: BigInt, default: BigInt(0) } + }); + + const AffiliateModel = model('Affiliate', affiliateSchema); + + const doc = new AffiliateModel(); + expectType(doc.balance); +} + +function gh14235() { + interface IUser { + name: string; + age: number; + } + + const userSchema = new Schema({ name: String, age: Number }); + + userSchema.omit>(['age']); +} + +function gh14496() { + const schema = Schema.create({ + name: { + type: String + } + }); + schema.path('name').validate({ + validator: () => { + throw new Error('Oops!'); + }, + // `errors['name']` will be "Oops!" + message: (props) => { + expectType(props.reason); + return 'test'; + } + }); +} + +function gh14367() { + const UserSchema = Schema.create({ + counts: [Schema.Types.Number], + roles: [Schema.Types.String], + dates: [Schema.Types.Date], + flags: [Schema.Types.Boolean] + }); + + type IUser = InferSchemaType; + + const x: IUser = { + _id: new Types.ObjectId(), + counts: [12], + roles: ['test'], + dates: [new Date('2016-06-01')], + flags: [true] + }; +} + +function gh14573() { + interface Names { + _id: Types.ObjectId; + firstName: string; + } + + // Document definition + interface User { + names: Names; + } + + // Define property overrides for hydrated documents + type THydratedUserDocument = { + names?: HydratedSingleSubdocument; + }; + + type UserMethods = { + getName(): Names | undefined; + }; + + type UserModelType = Model; + + const userSchema = new Schema< + User, + UserModelType, + UserMethods, + {}, + {}, + {}, + DefaultSchemaOptions, + User, + THydratedUserDocument + >( + { + names: new Schema({ firstName: String }) + }, + { + methods: { + getName() { + const str: string | undefined = this.names?.firstName; + return this.names?.toObject(); + } + } + } + ); + const UserModel = model('User', userSchema); + const doc = new UserModel({ names: { _id: '0'.repeat(24), firstName: 'foo' } }); + doc.names?.ownerDocument(); +} + +function gh13772() { + const schemaDefinition = { + name: String, + docArr: [{ name: String }] + } as const; + const schema = Schema.create(schemaDefinition); + + const TestModel = model('User', schema); + type RawDocType = InferRawDocType; + expectAssignable< + { name?: string | null, docArr?: Array<{ name?: string | null }> | null } + >({} as RawDocType); + + const doc = new TestModel(); + expectAssignable(doc.toObject()); + expectAssignable(doc.toJSON()); +} + +function gh14696() { + interface User { + name: string; + isActive: boolean; + isActiveAsync: boolean; + } + + const x: ValidateOpts = { + validator(v: any) { + expectAssignable(this); + return !v || this.name === 'super admin'; + } + }; + + const userSchema = new Schema({ + name: { + type: String, + required: [true, 'Name on card is required'] + }, + isActive: { + type: Boolean, + default: false, + validate: { + validator(v: any) { + expectAssignable(this); + return !v || this.name === 'super admin'; + } + } + }, + isActiveAsync: { + type: Boolean, + default: false, + validate: { + async validator(v: any) { + expectAssignable(this); + return !v || this.name === 'super admin'; + } + } + } + }); + +} + +function gh14748() { + const nestedSchema = Schema.create({ name: String }); + + const schema = Schema.create({ + arr: [nestedSchema], + singleNested: nestedSchema + }); + + const subdoc = schema.path('singleNested') + .cast>({ name: 'bar' }); + expectAssignable<{ name: string }>(subdoc); + + const subdoc2 = schema.path('singleNested').cast({ name: 'bar' }); + expectAssignable<{ name: string }>(subdoc2); + + const subdoc3 = schema.path>('singleNested').cast({ name: 'bar' }); + expectAssignable<{ name: string }>(subdoc3); +} + +function gh13215() { + const schemaDefinition = { + userName: { type: String, required: true } + } as const; + const schemaOptions = { + typeKey: 'type', + timestamps: { + createdAt: 'date', + updatedAt: false + } + } as const; + + type RawDocType = InferRawDocType< + typeof schemaDefinition, + typeof schemaOptions + >; + type User = { + userName: string; + } & { + date: Date; + } & { _id: Types.ObjectId }; + + expectType({} as RawDocType); + + const schema = Schema.create(schemaDefinition, schemaOptions); + type SchemaType = InferSchemaType; + expectType({} as SchemaType); +} + +function gh14825() { + const schemaDefinition = { + userName: { type: String, required: true } + } as const; + const schemaOptions = { + typeKey: 'type' as const, + timestamps: { + createdAt: 'date', + updatedAt: false + } + }; + + type RawDocType = InferRawDocType< + typeof schemaDefinition, + typeof schemaOptions + >; + type User = { + userName: string; + }; + + expectAssignable({} as RawDocType); + + const schema = Schema.create(schemaDefinition, schemaOptions); + type SchemaType = InferSchemaType; + expectAssignable({} as SchemaType); +} + +function gh8389() { + const schema = Schema.create({ name: String, tags: [String] }); + + expectAssignable | undefined>(schema.path('name').getEmbeddedSchemaType()); + expectAssignable | undefined>(schema.path('tags').getEmbeddedSchemaType()); +} + +function gh14879() { + Schema.Types.String.setters.push((val?: unknown) => typeof val === 'string' ? val.trim() : val); +} + +async function gh14950() { + const SightingSchema = Schema.create( + { + _id: { type: Schema.Types.ObjectId, required: true }, + location: { + type: { type: String, required: true }, + coordinates: [{ type: Number }] + } + } + ); + + const TestModel = model('Test', SightingSchema); + const doc = await TestModel.findOne().orFail(); + + expectType(doc.location!.type); + expectType(doc.location!.coordinates); +} + +async function gh14902() { + const subdocSchema = Schema.create({ + testBuf: Buffer + }); + + const exampleSchema = Schema.create({ + image: { type: Buffer }, + subdoc: { + type: Schema.create({ + testBuf: Buffer + } as const) + } + }); + const Test = model('Test', exampleSchema); + + const doc = await Test.findOne().lean().orFail(); + expectType(doc.image); + expectType(doc.subdoc!.testBuf); +} + +async function gh14451() { + const exampleSchema = Schema.create({ + myId: { type: 'ObjectId' }, + myRequiredId: { type: 'ObjectId', required: true }, + myBuf: { type: Buffer, required: true }, + subdoc: { + type: Schema.create({ + subdocProp: Date + }) + }, + docArr: [{ nums: [Number], times: [{ type: Date }] }], + myMap: { + type: Map, + of: String + } + } as const); + + const Test = model('Test', exampleSchema); + + type TestJSON = JSONSerialized>; + expectAssignable<{ + myId?: string | undefined | null, + myRequiredId: string, + myBuf: { type: 'buffer', data: number[] }, + subdoc?: { + subdocProp?: string | undefined | null + } | null, + docArr: { nums: number[], times: string[] }[], + myMap?: Record | null | undefined, + _id: string + }>({} as TestJSON); +} + +async function gh12959() { + const schema = Schema.create({ name: String }); + const TestModel = model('Test', schema); + + const doc = await TestModel.findOne().orFail(); + expectType(doc.__v); + const leanDoc = await TestModel.findOne().lean().orFail(); + expectType(leanDoc.__v); +} + +async function gh15236() { + const schema = Schema.create({ + myNum: { type: Number } + }); + + schema.path('myNum').min(0); +} + +function gh15244() { + const schema = Schema.create({}); + schema.discriminator('Name', Schema.create({}), { value: 'value' }); +} + +async function schemaDouble() { + const schema = Schema.create({ balance: 'Double' } as const); + const TestModel = model('Test', schema); + + const doc = await TestModel.findOne().orFail(); + expectType(doc.balance); +} + +function gh15301() { + interface IUser { + time: { hours: number, minutes: number } + } + const userSchema = new Schema({ + time: { + type: Schema.create( + { + hours: { type: Number, required: true }, + minutes: { type: Number, required: true } + }, + { _id: false } + ), + required: true + } + }); + + const timeStringToObject = (time) => { + if (typeof time !== 'string') return time; + const [hours, minutes] = time.split(':'); + return { hours: parseInt(hours), minutes: parseInt(minutes) }; + }; + + userSchema.pre('init', function(rawDoc) { + expectType(rawDoc); + if (typeof rawDoc.time === 'string') { + rawDoc.time = timeStringToObject(rawDoc.time); + } + }); +} + +function gh15412() { + const ScheduleEntrySchema = Schema.create({ + startDate: { type: Date, required: true }, + endDate: { type: Date, required: false } + }); + const ScheduleEntry = model('ScheduleEntry', ScheduleEntrySchema); + + type ScheduleEntryDoc = ReturnType + + ScheduleEntrySchema.post('init', function(this: ScheduleEntryDoc, _res: any, next: CallbackWithoutResultAndOptionalError) { + expectType(this.startDate); + expectType(this.endDate); + next(); + }); +} + +function defaultReturnsUndefined() { + const schema = new Schema<{ arr: number[] }>({ + arr: { + type: [Number], + default: () => void 0 + } + }); +} diff --git a/types/inferhydrateddoctype.d.ts b/types/inferhydrateddoctype.d.ts new file mode 100644 index 00000000000..07a20aa89ae --- /dev/null +++ b/types/inferhydrateddoctype.d.ts @@ -0,0 +1,147 @@ +import { + IsPathRequired, + IsSchemaTypeFromBuiltinClass, + RequiredPaths, + OptionalPaths, + PathWithTypePropertyBaseType, + PathEnumOrString +} from './inferschematype'; +import { UUID } from 'mongodb'; + +declare module 'mongoose' { + export type InferHydratedDocType< + DocDefinition, + TSchemaOptions extends Record = DefaultSchemaOptions + > = Require_id & + OptionalPaths) + ]: IsPathRequired extends true + ? ObtainHydratedDocumentPathType + : ObtainHydratedDocumentPathType | null; + }, TSchemaOptions>>; + + /** + * @summary Obtains schema Path type. + * @description Obtains Path type by separating path type from other options and calling {@link ResolveHydratedPathType} + * @param {PathValueType} PathValueType Document definition path type. + * @param {TypeKey} TypeKey A generic refers to document definition. + */ + type ObtainHydratedDocumentPathType< + PathValueType, + TypeKey extends string = DefaultTypeKey + > = ResolveHydratedPathType< + PathValueType extends PathWithTypePropertyBaseType + ? PathValueType[TypeKey] extends PathWithTypePropertyBaseType + ? PathValueType + : PathValueType[TypeKey] + : PathValueType, + PathValueType extends PathWithTypePropertyBaseType + ? PathValueType[TypeKey] extends PathWithTypePropertyBaseType + ? {} + : Omit + : {}, + TypeKey + >; + + /** + * Same as inferSchemaType, except: + * + * 1. Replace `Types.DocumentArray` and `Types.Array` with vanilla `Array` + * 2. Replace `ObtainDocumentPathType` with `ObtainHydratedDocumentPathType` + * 3. Replace `ResolvePathType` with `ResolveHydratedPathType` + * + * @summary Resolve path type by returning the corresponding type. + * @param {PathValueType} PathValueType Document definition path type. + * @param {Options} Options Document definition path options except path type. + * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". + * @returns Number, "Number" or "number" will be resolved to number type. + */ + type ResolveHydratedPathType = {}, TypeKey extends string = DefaultSchemaOptions['typeKey']> = + PathValueType extends Schema ? + THydratedDocumentType : + PathValueType extends (infer Item)[] ? + IfEquals ? + // If Item is a schema, infer its type. + Types.DocumentArray< + EmbeddedRawDocType, + Types.Subdocument & EmbeddedHydratedDocType + > : + Item extends Record ? + Item[TypeKey] extends Function | String ? + // If Item has a type key that's a string or a callable, it must be a scalar, + // so we can directly obtain its path type. + Types.Array> : + // If the type key isn't callable, then this is an array of objects, in which case + // we need to call InferHydratedDocType to correctly infer its type. + Types.DocumentArray< + InferRawDocType, + Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType + > : + IsSchemaTypeFromBuiltinClass extends true ? + Types.Array> : + IsItRecordAndNotAny extends true ? + Item extends Record ? + Types.Array> : + Types.DocumentArray< + InferRawDocType, + Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType + > : + Types.Array> + > : + PathValueType extends ReadonlyArray ? + IfEquals ? + Types.DocumentArray< + EmbeddedRawDocType, + Types.Subdocument & EmbeddedHydratedDocType + > : + Item extends Record ? + Item[TypeKey] extends Function | String ? + Types.Array> : + Types.DocumentArray< + InferRawDocType, + Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType + >: + IsSchemaTypeFromBuiltinClass extends true ? + Types.Array> : + IsItRecordAndNotAny extends true ? + Item extends Record ? + Types.Array> : + Types.DocumentArray< + InferRawDocType, + Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType + > : + Types.Array> + > : + PathValueType extends StringSchemaDefinition ? PathEnumOrString : + IfEquals extends true ? PathEnumOrString : + IfEquals extends true ? PathEnumOrString : + PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray ? Options['enum'][number] : number : + IfEquals extends true ? number : + PathValueType extends DateSchemaDefinition ? NativeDate : + IfEquals extends true ? NativeDate : + PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer : + PathValueType extends BooleanSchemaDefinition ? boolean : + IfEquals extends true ? boolean : + PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId : + IfEquals extends true ? Types.ObjectId : + IfEquals extends true ? Types.ObjectId : + PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Types.Decimal128 : + IfEquals extends true ? Types.Decimal128 : + IfEquals extends true ? Types.Decimal128 : + IfEquals extends true ? bigint : + IfEquals extends true ? bigint : + PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint : + PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? UUID : + PathValueType extends 'double' | 'Double' | typeof Schema.Types.Double ? Types.Double : + IfEquals extends true ? Buffer : + PathValueType extends MapConstructor | 'Map' ? Map> : + IfEquals extends true ? Map> : + PathValueType extends ArrayConstructor ? any[] : + PathValueType extends typeof Schema.Types.Mixed ? any: + IfEquals extends true ? any: + IfEquals extends true ? any: + PathValueType extends typeof SchemaType ? PathValueType['prototype'] : + PathValueType extends Record ? InferHydratedDocType : + unknown; +} From 91d803decb2d3167444c424d36cb2e833922077f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 18 Jun 2025 16:22:12 -0400 Subject: [PATCH 078/209] WIP Schema.create() re: #14954 --- test/types/inferrawdoctype.test.ts | 4 +- test/types/schema.create.test.ts | 62 +++++++++++++++--------------- test/types/schema.test.ts | 2 +- types/index.d.ts | 52 ++++++++++++++++++++++++- types/inferrawdoctype.d.ts | 52 +++++++++++++++---------- types/types.d.ts | 2 +- 6 files changed, 117 insertions(+), 57 deletions(-) diff --git a/test/types/inferrawdoctype.test.ts b/test/types/inferrawdoctype.test.ts index 7d162b03975..1e2141c316d 100644 --- a/test/types/inferrawdoctype.test.ts +++ b/test/types/inferrawdoctype.test.ts @@ -1,4 +1,4 @@ -import { InferRawDocType } from 'mongoose'; +import { InferRawDocType, Types } from 'mongoose'; import { expectType, expectError } from 'tsd'; function gh14839() { @@ -21,5 +21,5 @@ function gh14839() { }; type UserType = InferRawDocType< typeof schemaDefinition>; - expectType<{ email: string, password: string, dateOfBirth: Date }>({} as UserType); + expectType<{ email: string, password: string, dateOfBirth: Date } & { _id: Types.ObjectId }>({} as UserType); } diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index bf6535b69cc..0c6328797e9 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -25,7 +25,7 @@ import { BufferToBinary, CallbackWithoutResultAndOptionalError } from 'mongoose'; -import { Binary, BSON } from 'mongodb'; +import { Binary, BSON, UUID } from 'mongodb'; import { expectType, expectError, expectAssignable } from 'tsd'; import { ObtainDocumentPathType, ResolvePathType } from '../../types/inferschematype'; @@ -473,7 +473,7 @@ export function autoTypedSchema() { decimal1: Schema.Types.Decimal128, decimal2: 'Decimal128', decimal3: 'decimal128' - }); + } as const); type InferredTestSchemaType = InferSchemaType; @@ -486,7 +486,7 @@ export function autoTypedSchema() { } }, { typeKey: 'customTypeKey' - }); + } as const); expectType({} as InferSchemaType['name']); @@ -654,7 +654,7 @@ function gh12003() { type TSchemaOptions = ResolveSchemaOptions>; expectType<'type'>({} as TSchemaOptions['typeKey']); - expectType<{ name?: string | null }>({} as BaseSchemaType); + expectType<{ name?: string | null } & { _id: Types.ObjectId }>({} as BaseSchemaType); } function gh11987() { @@ -707,16 +707,16 @@ function gh12030() { }>({} as B); expectType<{ - users: Types.DocumentArray<{ + users: Array<{ username?: string | null }>; - }>({} as InferSchemaType); + } & { _id: Types.ObjectId }>({} as InferSchemaType); const Schema2 = Schema.create({ createdAt: { type: Date, default: Date.now } }); - expectType<{ createdAt: Date }>({} as InferSchemaType); + expectType<{ createdAt: Date } & { _id: Types.ObjectId }>({} as InferSchemaType); const Schema3 = Schema.create({ users: [ @@ -728,24 +728,24 @@ function gh12030() { }); expectType<{ - users: Types.DocumentArray<{ + users: Array<{ credit: number; username?: string | null; - }>; - }>({} as InferSchemaType); + } & { _id: Types.ObjectId }>; + } & { _id: Types.ObjectId }>({} as InferSchemaType); const Schema4 = Schema.create({ data: { type: { role: String }, default: {} } }); - expectType<{ data: { role?: string | null } }>({} as InferSchemaType); + expectType<{ data: { role?: string | null } } & { _id: Types.ObjectId }>({} as InferSchemaType); const Schema5 = Schema.create({ data: { type: { role: Object }, default: {} } }); - expectType<{ data: { role?: any } }>({} as InferSchemaType); + expectType<{ data: { role?: any } } & { _id: Types.ObjectId }>({} as InferSchemaType); const Schema6 = Schema.create({ track: { @@ -842,28 +842,28 @@ function gh12450() { expectType<{ user?: Types.ObjectId | null; - }>({} as InferSchemaType); + } & { _id: Types.ObjectId }>({} as InferSchemaType); const Schema2 = Schema.create({ createdAt: { type: Date, required: true }, decimalValue: { type: Schema.Types.Decimal128, required: true } }); - expectType<{ createdAt: Date, decimalValue: Types.Decimal128 }>({} as InferSchemaType); + expectType<{ createdAt: Date, decimalValue: Types.Decimal128 } & { _id: Types.ObjectId }>({} as InferSchemaType); const Schema3 = Schema.create({ createdAt: { type: Date, required: true }, decimalValue: { type: Schema.Types.Decimal128 } }); - expectType<{ createdAt: Date, decimalValue?: Types.Decimal128 | null }>({} as InferSchemaType); + expectType<{ createdAt: Date, decimalValue?: Types.Decimal128 | null } & { _id: Types.ObjectId }>({} as InferSchemaType); const Schema4 = Schema.create({ createdAt: { type: Date }, decimalValue: { type: Schema.Types.Decimal128 } }); - expectType<{ createdAt?: Date | null, decimalValue?: Types.Decimal128 | null }>({} as InferSchemaType); + expectType<{ createdAt?: Date | null, decimalValue?: Types.Decimal128 | null } & { _id: Types.ObjectId }>({} as InferSchemaType); } function gh12242() { @@ -887,7 +887,7 @@ function testInferTimestamps() { // an error "Parameter type { createdAt: Date; updatedAt: Date; name?: string | undefined; } // is not identical to argument type { createdAt: NativeDate; updatedAt: NativeDate; } & // { name?: string | undefined; }" - expectType<{ createdAt: Date, updatedAt: Date } & { name?: string | null }>({} as WithTimestamps); + expectType<{ createdAt: Date, updatedAt: Date } & { name?: string | null } & { _id: Types.ObjectId }>({} as WithTimestamps); const schema2 = Schema.create({ name: String @@ -903,7 +903,7 @@ function testInferTimestamps() { // an error "Parameter type { createdAt: Date; updatedAt: Date; name?: string | undefined; } // is not identical to argument type { createdAt: NativeDate; updatedAt: NativeDate; } & // { name?: string | undefined; }" - expectType<{ name?: string | null }>({} as WithTimestamps2); + expectType<{ name?: string | null } & { _id: Types.ObjectId }>({} as WithTimestamps2); } function gh12431() { @@ -913,30 +913,30 @@ function gh12431() { }); type Example = InferSchemaType; - expectType<{ testDate?: Date | null, testDecimal?: Types.Decimal128 | null }>({} as Example); + expectType<{ testDate?: Date | null, testDecimal?: Types.Decimal128 | null } & { _id: Types.ObjectId }>({} as Example); } async function gh12593() { const testSchema = Schema.create({ x: { type: Schema.Types.UUID } }); type Example = InferSchemaType; - expectType<{ x?: Buffer | null }>({} as Example); + expectType<{ x?: UUID | null } & { _id: Types.ObjectId }>({} as Example); const Test = model('Test', testSchema); const doc = await Test.findOne({ x: '4709e6d9-61fd-435e-b594-d748eb196d8f' }).orFail(); - expectType(doc.x); + expectType(doc.x); const doc2 = new Test({ x: '4709e6d9-61fd-435e-b594-d748eb196d8f' }); - expectType(doc2.x); + expectType(doc2.x); const doc3 = await Test.findOne({}).orFail().lean(); - expectType(doc3.x); + expectType(doc3.x); const arrSchema = Schema.create({ arr: [{ type: Schema.Types.UUID }] }); type ExampleArr = InferSchemaType; - expectType<{ arr: Buffer[] }>({} as ExampleArr); + expectType<{ arr: UUID[] } & { _id: Types.ObjectId }>({} as ExampleArr); } function gh12562() { @@ -999,7 +999,7 @@ function gh12611() { description: string; skills: Types.ObjectId[]; anotherField?: string | null; - }>({} as Props); + } & { _id: Types.ObjectId }>({} as Props); } function gh12782() { @@ -1008,7 +1008,7 @@ function gh12782() { type Props = InferSchemaType; expectType<{ test: string - }>({} as Props); + } & { _id: Types.ObjectId }>({} as Props); } function gh12816() { @@ -1028,7 +1028,7 @@ function gh12869() { const dbExample = Schema.create( { active: { type: String, enum: ['foo', 'bar'], required: true } - } + } as const ); type Example = InferSchemaType; @@ -1079,11 +1079,11 @@ function gh12882() { }); type tArrType = InferSchemaType; expectType<{ - fooArray: Types.DocumentArray<{ + fooArray: Array<{ type: string; foo: number; - }> - }>({} as tArrType); + } & { _id: Types.ObjectId }> + } & { _id: Types.ObjectId }>({} as tArrType); // Readonly array of strings const rArrString = Schema.create({ fooArray: { @@ -1097,7 +1097,7 @@ function gh12882() { type rTArrString = InferSchemaType; expectType<{ fooArray: string[] - }>({} as rTArrString); + } & { _id: Types.ObjectId }>({} as rTArrString); // Readonly array of numbers using string definition const rArrNum = Schema.create({ fooArray: { diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index bd3aaea80b8..57b8224caa8 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1615,7 +1615,7 @@ function gh13215() { date: Date; }; - expectType({} as RawDocType); + expectType({} as RawDocType); const schema = new Schema(schemaDefinition, schemaOptions); type SchemaType = InferSchemaType; diff --git a/types/index.d.ts b/types/index.d.ts index c50c7fbc850..058e7ec2321 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -20,6 +20,7 @@ /// /// /// +/// /// /// /// @@ -272,7 +273,8 @@ declare module 'mongoose' { ObtainDocumentType>, ResolveSchemaOptions >, - THydratedDocumentType = HydratedDocument, TVirtuals & TInstanceMethods, {}, TVirtuals> + THydratedDocumentType = HydratedDocument, TVirtuals & TInstanceMethods, {}, TVirtuals>, + TSchemaDefinition = SchemaDefinition, RawDocType, THydratedDocumentType> > extends events.EventEmitter { /** @@ -280,6 +282,54 @@ declare module 'mongoose' { */ constructor(definition?: SchemaDefinition, RawDocType, THydratedDocumentType> | DocType, options?: SchemaOptions, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals, THydratedDocumentType> | ResolveSchemaOptions); + static create< + TSchemaDefinition extends SchemaDefinition, + TSchemaOptions extends DefaultSchemaOptions, + RawDocType extends ApplySchemaOptions< + InferRawDocType>, + ResolveSchemaOptions + >, + THydratedDocumentType extends AnyObject = HydratedDocument>> + >(def: TSchemaDefinition): Schema< + RawDocType, + Model, + TSchemaOptions extends { methods: infer M } ? M : {}, + TSchemaOptions extends { query: any } ? TSchemaOptions['query'] : {}, + TSchemaOptions extends { virtuals: any } ? TSchemaOptions['virtuals'] : {}, + TSchemaOptions extends { statics: any } ? TSchemaOptions['statics'] : {}, + TSchemaOptions, + ApplySchemaOptions< + ObtainDocumentType>, + ResolveSchemaOptions + >, + THydratedDocumentType, + TSchemaDefinition + >; + + static create< + TSchemaDefinition extends SchemaDefinition, + TSchemaOptions extends SchemaOptions>, + RawDocType extends ApplySchemaOptions< + InferRawDocType>, + ResolveSchemaOptions + >, + THydratedDocumentType extends AnyObject = HydratedDocument>> + >(def: TSchemaDefinition, options: TSchemaOptions): Schema< + RawDocType, + Model, + TSchemaOptions extends { methods: infer M } ? M : {}, + TSchemaOptions extends { query: any } ? TSchemaOptions['query'] : {}, + TSchemaOptions extends { virtuals: any } ? TSchemaOptions['virtuals'] : {}, + TSchemaOptions extends { statics: any } ? TSchemaOptions['statics'] : {}, + TSchemaOptions, + ApplySchemaOptions< + ObtainDocumentType>, + ResolveSchemaOptions + >, + THydratedDocumentType, + TSchemaDefinition + >; + /** Adds key path / schema type pairs to this schema. */ add(obj: SchemaDefinition, RawDocType> | Schema, prefix?: string): this; diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index 9cee6747d85..6a4f0323732 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -6,19 +6,20 @@ import { PathWithTypePropertyBaseType, PathEnumOrString } from './inferschematype'; +import { UUID } from 'mongodb'; declare module 'mongoose' { export type InferRawDocType< DocDefinition, TSchemaOptions extends Record = DefaultSchemaOptions - > = ApplySchemaOptions<{ + > = Require_id & OptionalPaths) ]: IsPathRequired extends true ? ObtainRawDocumentPathType : ObtainRawDocumentPathType | null; - }, TSchemaOptions>; + }, TSchemaOptions>>; /** * @summary Obtains schema Path type. @@ -30,8 +31,16 @@ declare module 'mongoose' { PathValueType, TypeKey extends string = DefaultTypeKey > = ResolveRawPathType< - PathValueType extends PathWithTypePropertyBaseType ? PathValueType[TypeKey] : PathValueType, - PathValueType extends PathWithTypePropertyBaseType ? Omit : {}, + PathValueType extends PathWithTypePropertyBaseType + ? PathValueType[TypeKey] extends PathWithTypePropertyBaseType + ? PathValueType + : PathValueType[TypeKey] + : PathValueType, + PathValueType extends PathWithTypePropertyBaseType + ? PathValueType[TypeKey] extends PathWithTypePropertyBaseType + ? {} + : Omit + : {}, TypeKey >; @@ -49,12 +58,12 @@ declare module 'mongoose' { * @returns Number, "Number" or "number" will be resolved to number type. */ type ResolveRawPathType = {}, TypeKey extends string = DefaultSchemaOptions['typeKey']> = - PathValueType extends Schema ? - InferSchemaType : + PathValueType extends Schema ? + IsItRecordAndNotAny extends true ? RawDocType : InferRawDocType : PathValueType extends (infer Item)[] ? - IfEquals ? // If Item is a schema, infer its type. - Array> : + Array : Item extends Record ? Item[TypeKey] extends Function | String ? // If Item has a type key that's a string or a callable, it must be a scalar, @@ -72,8 +81,8 @@ declare module 'mongoose' { ObtainRawDocumentPathType[] >: PathValueType extends ReadonlyArray ? - IfEquals> : + IfEquals ? + Array : Item extends Record ? Item[TypeKey] extends Function | String ? ObtainRawDocumentPathType[] : @@ -105,15 +114,16 @@ declare module 'mongoose' { IfEquals extends true ? bigint : IfEquals extends true ? bigint : PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint : - PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? Buffer : - IfEquals extends true ? Buffer : - PathValueType extends MapConstructor | 'Map' ? Map> : - IfEquals extends true ? Map> : - PathValueType extends ArrayConstructor ? any[] : - PathValueType extends typeof Schema.Types.Mixed ? any: - IfEquals extends true ? any: - IfEquals extends true ? any: - PathValueType extends typeof SchemaType ? PathValueType['prototype'] : - PathValueType extends Record ? InferRawDocType : - unknown; + PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? UUID : + PathValueType extends 'double' | 'Double' | typeof Schema.Types.Double ? Types.Double : + IfEquals extends true ? Buffer : + PathValueType extends MapConstructor | 'Map' ? Map> : + IfEquals extends true ? Map> : + PathValueType extends ArrayConstructor ? any[] : + PathValueType extends typeof Schema.Types.Mixed ? any: + IfEquals extends true ? any: + IfEquals extends true ? any: + PathValueType extends typeof SchemaType ? PathValueType['prototype'] : + PathValueType extends Record ? ObtainRawDocumentPathType : + unknown; } diff --git a/types/types.d.ts b/types/types.d.ts index c29da93be23..c9d86a44b9b 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 = Types.Subdocument, any, T> & T> extends Types.Array { + class DocumentArray = Types.Subdocument, any, T> & T> extends Types.Array { /** DocumentArray constructor */ constructor(values: AnyObject[]); From b581bf8655ac491798ea280452b35d2c0cb848d9 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 19 Jun 2025 12:45:48 -0400 Subject: [PATCH 079/209] fix remaining tests re: #14954 --- lib/schema.js | 21 +++++ test/types/schema.create.test.ts | 48 ++++++---- types/inferhydrateddoctype.d.ts | 148 ++++++++++++++++--------------- types/inferrawdoctype.d.ts | 126 ++++++++++++++------------ types/inferschematype.d.ts | 6 +- 5 files changed, 201 insertions(+), 148 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index 46e93890e38..71624177dbd 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -372,6 +372,27 @@ Schema.prototype.paths; Schema.prototype.tree; +/** + * Creates a new schema with the given definition and options. Equivalent to `new Schema(definition, options)`. + * + * `Schema.create()` is primarily useful for automatic schema type inference in TypeScript. + * + * #### Example: + * + * const schema = Schema.create({ name: String }, { toObject: { virtuals: true } }); + * // Equivalent: + * const schema2 = new Schema({ name: String }, { toObject: { virtuals: true } }); + * + * @return {Schema} the new schema + * @api public + * @memberOf Schema + * @static + */ + +Schema.create = function create(definition, options) { + return new Schema(definition, options); +}; + /** * Returns a deep copy of the schema * diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index 0c6328797e9..cf203da4da7 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -22,8 +22,8 @@ import { Query, model, ValidateOpts, - BufferToBinary, - CallbackWithoutResultAndOptionalError + CallbackWithoutResultAndOptionalError, + InferHydratedDocType, } from 'mongoose'; import { Binary, BSON, UUID } from 'mongodb'; import { expectType, expectError, expectAssignable } from 'tsd'; @@ -426,7 +426,7 @@ export function autoTypedSchema() { decimal1?: Types.Decimal128 | null; decimal2?: Types.Decimal128 | null; decimal3?: Types.Decimal128 | null; - }; + } & { _id: Types.ObjectId }; const TestSchema = Schema.create({ string1: String, @@ -709,7 +709,7 @@ function gh12030() { expectType<{ users: Array<{ username?: string | null - }>; + } & { _id: Types.ObjectId }>; } & { _id: Types.ObjectId }>({} as InferSchemaType); const Schema2 = Schema.create({ @@ -734,18 +734,30 @@ function gh12030() { } & { _id: Types.ObjectId }>; } & { _id: Types.ObjectId }>({} as InferSchemaType); + type HydratedDoc3 = ObtainSchemaGeneric; + expectType< + HydratedDocument<{ + users: Types.DocumentArray< + { credit: number; username?: string | null; } & { _id: Types.ObjectId }, + Types.Subdocument & { credit: number; username?: string | null; } & { _id: Types.ObjectId } + >; + } & { _id: Types.ObjectId }> + >({} as HydratedDoc3); + expectType< + Types.Subdocument & { credit: number; username?: string | null; } & { _id: Types.ObjectId } + >({} as HydratedDoc3['users'][0]); const Schema4 = Schema.create({ data: { type: { role: String }, default: {} } - }); + } as const); - expectType<{ data: { role?: string | null } } & { _id: Types.ObjectId }>({} as InferSchemaType); + expectType<{ data: { role?: string | null } & { _id: Types.ObjectId } } & { _id: Types.ObjectId }>({} as InferSchemaType); const Schema5 = Schema.create({ data: { type: { role: Object }, default: {} } }); - expectType<{ data: { role?: any } } & { _id: Types.ObjectId }>({} as InferSchemaType); + expectType<{ data: { role?: any } & { _id: Types.ObjectId } } & { _id: Types.ObjectId }>({} as InferSchemaType); const Schema6 = Schema.create({ track: { @@ -761,11 +773,11 @@ function gh12030() { }); expectType<{ - track?: { + track?: ({ backupCount: number; count: number; - } | null; - }>({} as InferSchemaType); + } & { _id: Types.ObjectId }) | null; + } & { _id: Types.ObjectId }>({} as InferSchemaType); } @@ -780,7 +792,7 @@ function pluginOptions() { } const schema = Schema.create({}); - expectType>(schema.plugin(pluginFunction)); // test that chaining would be possible + expectAssignable>(schema.plugin(pluginFunction)); // test that chaining would be possible // could not add strict tests that the parameters are inferred correctly, because i dont know how this would be done in tsd @@ -819,7 +831,7 @@ function gh12205() { expectType(doc.client); type ICampaign = InferSchemaType; - expectType<{ client: Types.ObjectId }>({} as ICampaign); + expectType<{ client: Types.ObjectId } & { _id: Types.ObjectId }>({} as ICampaign); type A = ObtainDocumentType<{ client: { type: Schema.Types.ObjectId, required: true } }>; expectType<{ client: Types.ObjectId }>({} as A); @@ -1261,10 +1273,12 @@ function gh14002() { } const userIdTypeHint = 'placeholder' as UserId; - const schema = Schema.create({ - userId: { type: String, required: true, __typehint: userIdTypeHint } - }); - expectType({} as InferSchemaType); + const schemaDef = { + userId: { type: String, required: true, __rawDocTypeHint: userIdTypeHint, __hydratedDocTypeHint: userIdTypeHint } + } as const; + const schema = Schema.create(schemaDef); + expectType({} as InferSchemaType); + expectType({} as InferHydratedDocType['userId']); } function gh14028_methods() { @@ -1621,6 +1635,8 @@ function gh13215() { const schema = Schema.create(schemaDefinition, schemaOptions); type SchemaType = InferSchemaType; expectType({} as SchemaType); + type HydratedDoc = ObtainSchemaGeneric; + expectType>({} as HydratedDoc); } function gh14825() { diff --git a/types/inferhydrateddoctype.d.ts b/types/inferhydrateddoctype.d.ts index 07a20aa89ae..280c2b15903 100644 --- a/types/inferhydrateddoctype.d.ts +++ b/types/inferhydrateddoctype.d.ts @@ -41,9 +41,15 @@ declare module 'mongoose' { ? {} : Omit : {}, - TypeKey + TypeKey, + HydratedDocTypeHint >; + /** + * @summary Allows users to optionally choose their own type for a schema field for stronger typing. + */ + type HydratedDocTypeHint = T extends { __hydratedDocTypeHint: infer U } ? U: never; + /** * Same as inferSchemaType, except: * @@ -57,51 +63,27 @@ declare module 'mongoose' { * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". * @returns Number, "Number" or "number" will be resolved to number type. */ - type ResolveHydratedPathType = {}, TypeKey extends string = DefaultSchemaOptions['typeKey']> = - PathValueType extends Schema ? - THydratedDocumentType : - PathValueType extends (infer Item)[] ? - IfEquals ? - // If Item is a schema, infer its type. - Types.DocumentArray< - EmbeddedRawDocType, - Types.Subdocument & EmbeddedHydratedDocType - > : - Item extends Record ? - Item[TypeKey] extends Function | String ? - // If Item has a type key that's a string or a callable, it must be a scalar, - // so we can directly obtain its path type. - Types.Array> : - // If the type key isn't callable, then this is an array of objects, in which case - // we need to call InferHydratedDocType to correctly infer its type. - Types.DocumentArray< - InferRawDocType, - Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType - > : - IsSchemaTypeFromBuiltinClass extends true ? - Types.Array> : - IsItRecordAndNotAny extends true ? - Item extends Record ? - Types.Array> : - Types.DocumentArray< - InferRawDocType, - Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType - > : - Types.Array> - > : - PathValueType extends ReadonlyArray ? - IfEquals ? - Types.DocumentArray< - EmbeddedRawDocType, - Types.Subdocument & EmbeddedHydratedDocType - > : + type ResolveHydratedPathType = {}, TypeKey extends string = DefaultSchemaOptions['typeKey'], TypeHint = never> = + IfEquals ? + THydratedDocumentType : + PathValueType extends (infer Item)[] ? + IfEquals ? + // If Item is a schema, infer its type. + IsItRecordAndNotAny extends true ? + Types.DocumentArray & EmbeddedHydratedDocType> : + Types.DocumentArray, Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType> : Item extends Record ? Item[TypeKey] extends Function | String ? + // If Item has a type key that's a string or a callable, it must be a scalar, + // so we can directly obtain its path type. Types.Array> : + // If the type key isn't callable, then this is an array of objects, in which case + // we need to call InferHydratedDocType to correctly infer its type. Types.DocumentArray< InferRawDocType, Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType - >: + > : IsSchemaTypeFromBuiltinClass extends true ? Types.Array> : IsItRecordAndNotAny extends true ? @@ -113,35 +95,59 @@ declare module 'mongoose' { > : Types.Array> > : - PathValueType extends StringSchemaDefinition ? PathEnumOrString : - IfEquals extends true ? PathEnumOrString : - IfEquals extends true ? PathEnumOrString : - PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray ? Options['enum'][number] : number : - IfEquals extends true ? number : - PathValueType extends DateSchemaDefinition ? NativeDate : - IfEquals extends true ? NativeDate : - PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer : - PathValueType extends BooleanSchemaDefinition ? boolean : - IfEquals extends true ? boolean : - PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId : - IfEquals extends true ? Types.ObjectId : - IfEquals extends true ? Types.ObjectId : - PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Types.Decimal128 : - IfEquals extends true ? Types.Decimal128 : - IfEquals extends true ? Types.Decimal128 : - IfEquals extends true ? bigint : - IfEquals extends true ? bigint : - PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint : - PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? UUID : - PathValueType extends 'double' | 'Double' | typeof Schema.Types.Double ? Types.Double : - IfEquals extends true ? Buffer : - PathValueType extends MapConstructor | 'Map' ? Map> : - IfEquals extends true ? Map> : - PathValueType extends ArrayConstructor ? any[] : - PathValueType extends typeof Schema.Types.Mixed ? any: - IfEquals extends true ? any: - IfEquals extends true ? any: - PathValueType extends typeof SchemaType ? PathValueType['prototype'] : - PathValueType extends Record ? InferHydratedDocType : - unknown; + PathValueType extends ReadonlyArray ? + IfEquals ? + IsItRecordAndNotAny extends true ? + Types.DocumentArray & EmbeddedHydratedDocType> : + Types.DocumentArray, Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType> : + Item extends Record ? + Item[TypeKey] extends Function | String ? + Types.Array> : + Types.DocumentArray< + InferRawDocType, + Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType + >: + IsSchemaTypeFromBuiltinClass extends true ? + Types.Array> : + IsItRecordAndNotAny extends true ? + Item extends Record ? + Types.Array> : + Types.DocumentArray< + InferRawDocType, + Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType + > : + Types.Array> + > : + PathValueType extends StringSchemaDefinition ? PathEnumOrString : + IfEquals extends true ? PathEnumOrString : + IfEquals extends true ? PathEnumOrString : + PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray ? Options['enum'][number] : number : + IfEquals extends true ? number : + PathValueType extends DateSchemaDefinition ? NativeDate : + IfEquals extends true ? NativeDate : + PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer : + PathValueType extends BooleanSchemaDefinition ? boolean : + IfEquals extends true ? boolean : + PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId : + IfEquals extends true ? Types.ObjectId : + IfEquals extends true ? Types.ObjectId : + PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Types.Decimal128 : + IfEquals extends true ? Types.Decimal128 : + IfEquals extends true ? Types.Decimal128 : + IfEquals extends true ? bigint : + IfEquals extends true ? bigint : + PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint : + PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? UUID : + PathValueType extends 'double' | 'Double' | typeof Schema.Types.Double ? Types.Double : + IfEquals extends true ? Buffer : + PathValueType extends MapConstructor | 'Map' ? Map> : + IfEquals extends true ? Map> : + PathValueType extends ArrayConstructor ? any[] : + PathValueType extends typeof Schema.Types.Mixed ? any: + IfEquals extends true ? any: + IfEquals extends true ? any: + PathValueType extends typeof SchemaType ? PathValueType['prototype'] : + PathValueType extends Record ? InferHydratedDocType : + unknown, + TypeHint>; } diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index 6a4f0323732..c40c8c48c73 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -21,6 +21,11 @@ declare module 'mongoose' { : ObtainRawDocumentPathType | null; }, TSchemaOptions>>; + /** + * @summary Allows users to optionally choose their own type for a schema field for stronger typing. + */ + type RawDocTypeHint = T extends { __rawDocTypeHint: infer U } ? U: never; + /** * @summary Obtains schema Path type. * @description Obtains Path type by separating path type from other options and calling {@link ResolveRawPathType} @@ -41,7 +46,8 @@ declare module 'mongoose' { ? {} : Omit : {}, - TypeKey + TypeKey, + RawDocTypeHint >; /** @@ -57,36 +63,22 @@ declare module 'mongoose' { * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". * @returns Number, "Number" or "number" will be resolved to number type. */ - type ResolveRawPathType = {}, TypeKey extends string = DefaultSchemaOptions['typeKey']> = - PathValueType extends Schema ? - IsItRecordAndNotAny extends true ? RawDocType : InferRawDocType : - PathValueType extends (infer Item)[] ? - IfEquals ? - // If Item is a schema, infer its type. - Array : - Item extends Record ? - Item[TypeKey] extends Function | String ? - // If Item has a type key that's a string or a callable, it must be a scalar, - // so we can directly obtain its path type. - ObtainRawDocumentPathType[] : - // If the type key isn't callable, then this is an array of objects, in which case - // we need to call InferRawDocType to correctly infer its type. - Array> : - IsSchemaTypeFromBuiltinClass extends true ? - ObtainRawDocumentPathType[] : - IsItRecordAndNotAny extends true ? - Item extends Record ? - ObtainRawDocumentPathType[] : - Array> : - ObtainRawDocumentPathType[] - >: - PathValueType extends ReadonlyArray ? - IfEquals ? - Array : + type ResolveRawPathType = {}, TypeKey extends string = DefaultSchemaOptions['typeKey'], TypeHint = never> = + IfEquals ? + IsItRecordAndNotAny extends true ? RawDocType : InferRawDocType : + PathValueType extends (infer Item)[] ? + IfEquals ? + // If Item is a schema, infer its type. + Array extends true ? RawDocType : InferRawDocType> : Item extends Record ? Item[TypeKey] extends Function | String ? + // If Item has a type key that's a string or a callable, it must be a scalar, + // so we can directly obtain its path type. ObtainRawDocumentPathType[] : - InferRawDocType[]: + // If the type key isn't callable, then this is an array of objects, in which case + // we need to call InferRawDocType to correctly infer its type. + Array> : IsSchemaTypeFromBuiltinClass extends true ? ObtainRawDocumentPathType[] : IsItRecordAndNotAny extends true ? @@ -95,35 +87,51 @@ declare module 'mongoose' { Array> : ObtainRawDocumentPathType[] >: - PathValueType extends StringSchemaDefinition ? PathEnumOrString : - IfEquals extends true ? PathEnumOrString : - IfEquals extends true ? PathEnumOrString : - PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray ? Options['enum'][number] : number : - IfEquals extends true ? number : - PathValueType extends DateSchemaDefinition ? NativeDate : - IfEquals extends true ? NativeDate : - PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer : - PathValueType extends BooleanSchemaDefinition ? boolean : - IfEquals extends true ? boolean : - PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId : - IfEquals extends true ? Types.ObjectId : - IfEquals extends true ? Types.ObjectId : - PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Types.Decimal128 : - IfEquals extends true ? Types.Decimal128 : - IfEquals extends true ? Types.Decimal128 : - IfEquals extends true ? bigint : - IfEquals extends true ? bigint : - PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint : - PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? UUID : - PathValueType extends 'double' | 'Double' | typeof Schema.Types.Double ? Types.Double : - IfEquals extends true ? Buffer : - PathValueType extends MapConstructor | 'Map' ? Map> : - IfEquals extends true ? Map> : - PathValueType extends ArrayConstructor ? any[] : - PathValueType extends typeof Schema.Types.Mixed ? any: - IfEquals extends true ? any: - IfEquals extends true ? any: - PathValueType extends typeof SchemaType ? PathValueType['prototype'] : - PathValueType extends Record ? ObtainRawDocumentPathType : - unknown; + PathValueType extends ReadonlyArray ? + IfEquals ? + Array extends true ? RawDocType : InferRawDocType> : + Item extends Record ? + Item[TypeKey] extends Function | String ? + ObtainRawDocumentPathType[] : + InferRawDocType[]: + IsSchemaTypeFromBuiltinClass extends true ? + ObtainRawDocumentPathType[] : + IsItRecordAndNotAny extends true ? + Item extends Record ? + ObtainRawDocumentPathType[] : + Array> : + ObtainRawDocumentPathType[] + >: + PathValueType extends StringSchemaDefinition ? PathEnumOrString : + IfEquals extends true ? PathEnumOrString : + IfEquals extends true ? PathEnumOrString : + PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray ? Options['enum'][number] : number : + IfEquals extends true ? number : + PathValueType extends DateSchemaDefinition ? NativeDate : + IfEquals extends true ? NativeDate : + PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer : + PathValueType extends BooleanSchemaDefinition ? boolean : + IfEquals extends true ? boolean : + PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId : + IfEquals extends true ? Types.ObjectId : + IfEquals extends true ? Types.ObjectId : + PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Types.Decimal128 : + IfEquals extends true ? Types.Decimal128 : + IfEquals extends true ? Types.Decimal128 : + IfEquals extends true ? bigint : + IfEquals extends true ? bigint : + PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint : + PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? UUID : + PathValueType extends 'double' | 'Double' | typeof Schema.Types.Double ? Types.Double : + IfEquals extends true ? Buffer : + PathValueType extends MapConstructor | 'Map' ? Map> : + IfEquals extends true ? Map> : + PathValueType extends ArrayConstructor ? any[] : + PathValueType extends typeof Schema.Types.Mixed ? any: + IfEquals extends true ? any: + IfEquals extends true ? any: + PathValueType extends typeof SchemaType ? PathValueType['prototype'] : + PathValueType extends Record ? InferRawDocType : + unknown, + TypeHint>; } diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index dac99d09d6c..d28b7dc56ac 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -56,8 +56,8 @@ declare module 'mongoose' { * @param {TSchema} TSchema A generic of schema type instance. * @param {alias} alias Targeted generic alias. */ - type ObtainSchemaGeneric = - TSchema extends Schema + type ObtainSchemaGeneric = + TSchema extends Schema ? { EnforcedDocType: EnforcedDocType; M: M; @@ -67,6 +67,8 @@ declare module 'mongoose' { TStaticMethods: TStaticMethods; TSchemaOptions: TSchemaOptions; DocType: DocType; + THydratedDocumentType: THydratedDocumentType; + TSchemaDefinition: TSchemaDefinition; }[alias] : unknown; From f23215a7e2946cac34317c5943aba05d4ee4db75 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 19 Jun 2025 13:05:46 -0400 Subject: [PATCH 080/209] fix lint --- test/types/schema.create.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index cf203da4da7..607bce5ca38 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -23,7 +23,7 @@ import { model, ValidateOpts, CallbackWithoutResultAndOptionalError, - InferHydratedDocType, + InferHydratedDocType } from 'mongoose'; import { Binary, BSON, UUID } from 'mongodb'; import { expectType, expectError, expectAssignable } from 'tsd'; @@ -773,11 +773,11 @@ function gh12030() { }); expectType<{ - track?: ({ + track?:({ backupCount: number; count: number; } & { _id: Types.ObjectId }) | null; - } & { _id: Types.ObjectId }>({} as InferSchemaType); + } & { _id: Types.ObjectId }>({} as InferSchemaType); } From 469a9f335fb9c803d4ea7b1300a3bc81d6032170 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 23 Jun 2025 09:52:47 -0400 Subject: [PATCH 081/209] WIP replace caster with embeddedSchemaType and Constructor re: #15179 --- lib/document.js | 16 ++++++++-------- lib/helpers/query/cast$expr.js | 4 ++-- lib/schema.js | 14 +++++++------- lib/schema/array.js | 2 +- lib/schema/documentArray.js | 6 +++--- lib/schemaType.js | 2 +- lib/types/array/index.js | 4 ++-- test/schema.documentarray.test.js | 2 +- types/schematypes.d.ts | 8 +++++++- 9 files changed, 32 insertions(+), 26 deletions(-) diff --git a/lib/document.js b/lib/document.js index 21bbd43534c..955ad789dac 100644 --- a/lib/document.js +++ b/lib/document.js @@ -2798,9 +2798,9 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate paths.delete(path); } else if (_pathType.$isMongooseArray && !_pathType.$isMongooseDocumentArray && // Skip document arrays... - !_pathType.$embeddedSchemaType.$isMongooseArray && // and arrays of arrays + !_pathType.embeddedSchemaType.$isMongooseArray && // and arrays of arrays _pathType.validators.length === 0 && // and arrays with top-level validators - _pathType.$embeddedSchemaType.validators.length === 0) { + _pathType.embeddedSchemaType.validators.length === 0) { paths.delete(path); } } @@ -2885,8 +2885,8 @@ function _addArrayPathsToValidate(doc, paths) { // on the array type, there's no need to run validation on the individual array elements. if (_pathType.$isMongooseArray && !_pathType.$isMongooseDocumentArray && // Skip document arrays... - !_pathType.$embeddedSchemaType.$isMongooseArray && // and arrays of arrays - _pathType.$embeddedSchemaType.validators.length === 0) { + !_pathType.embeddedSchemaType.$isMongooseArray && // and arrays of arrays + _pathType.embeddedSchemaType.validators.length === 0) { continue; } @@ -4255,9 +4255,9 @@ function applyGetters(self, json) { branch[part], self ); - if (Array.isArray(branch[part]) && schema.paths[path].$embeddedSchemaType) { + if (Array.isArray(branch[part]) && schema.paths[path].embeddedSchemaType) { for (let i = 0; i < branch[part].length; ++i) { - branch[part][i] = schema.paths[path].$embeddedSchemaType.applyGetters( + branch[part][i] = schema.paths[path].embeddedSchemaType.applyGetters( branch[part][i], self ); @@ -4299,8 +4299,8 @@ function applySchemaTypeTransforms(self, json) { for (const path of paths) { const schematype = schema.paths[path]; const topLevelTransformFunction = schematype.options.transform ?? schematype.constructor?.defaultOptions?.transform; - const embeddedSchemaTypeTransformFunction = schematype.$embeddedSchemaType?.options?.transform - ?? schematype.$embeddedSchemaType?.constructor?.defaultOptions?.transform; + const embeddedSchemaTypeTransformFunction = schematype.embeddedSchemaType?.options?.transform + ?? schematype.embeddedSchemaType?.constructor?.defaultOptions?.transform; if (typeof topLevelTransformFunction === 'function') { const val = self.$get(path); if (val === undefined) { diff --git a/lib/helpers/query/cast$expr.js b/lib/helpers/query/cast$expr.js index c45e13c14e9..a8d9a2ec795 100644 --- a/lib/helpers/query/cast$expr.js +++ b/lib/helpers/query/cast$expr.js @@ -174,7 +174,7 @@ function castIn(val, schema, strictQuery) { } return [ - schematype.$isMongooseDocumentArray ? schematype.$embeddedSchemaType.cast(search) : schematype.caster.cast(search), + schematype.$isMongooseDocumentArray ? schematype.embeddedSchemaType.cast(search) : schematype.caster.cast(search), path ]; } @@ -230,7 +230,7 @@ function castComparison(val, schema, strictQuery) { schematype = schema.path(lhs[key].slice(1)); if (schematype != null) { if (schematype.$isMongooseDocumentArray) { - schematype = schematype.$embeddedSchemaType; + schematype = schematype.embeddedSchemaType; } else if (schematype.$isMongooseArray) { schematype = schematype.caster; } diff --git a/lib/schema.js b/lib/schema.js index 4a47946da81..ab68d835825 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1389,9 +1389,9 @@ Schema.prototype.path = function(path, obj) { // Skip arrays of document arrays if (_schemaType.$isMongooseDocumentArray) { - _schemaType.$embeddedSchemaType._arrayPath = arrayPath; - _schemaType.$embeddedSchemaType._arrayParentPath = path; - _schemaType = _schemaType.$embeddedSchemaType; + _schemaType.embeddedSchemaType._arrayPath = arrayPath; + _schemaType.embeddedSchemaType._arrayParentPath = path; + _schemaType = _schemaType.embeddedSchemaType; } else { _schemaType.caster._arrayPath = arrayPath; _schemaType.caster._arrayParentPath = path; @@ -2009,7 +2009,7 @@ function getPositionalPathType(self, path, cleanPath) { if (i === last && val && !/\D/.test(subpath)) { if (val.$isMongooseDocumentArray) { - val = val.$embeddedSchemaType; + val = val.embeddedSchemaType; } else if (val instanceof MongooseTypes.Array) { // StringSchema, NumberSchema, etc val = val.caster; @@ -2932,8 +2932,8 @@ Schema.prototype._getSchema = function(path) { // If there is no foundschema.schema we are dealing with // a path like array.$ if (p !== parts.length) { - if (p + 1 === parts.length && foundschema.$embeddedSchemaType && (parts[p] === '$' || isArrayFilter(parts[p]))) { - return foundschema.$embeddedSchemaType; + if (p + 1 === parts.length && foundschema.embeddedSchemaType && (parts[p] === '$' || isArrayFilter(parts[p]))) { + return foundschema.embeddedSchemaType; } if (foundschema.schema) { @@ -2941,7 +2941,7 @@ Schema.prototype._getSchema = function(path) { if (parts[p] === '$' || isArrayFilter(parts[p])) { if (p + 1 === parts.length) { // comments.$ - return foundschema.$embeddedSchemaType; + return foundschema.embeddedSchemaType; } // comments.$.comments.$.title ret = search(parts.slice(p + 1), foundschema.schema); diff --git a/lib/schema/array.js b/lib/schema/array.js index 9e689ec5201..b0e1897d54b 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -100,7 +100,7 @@ function SchemaArray(key, cast, options, schemaOptions) { } } - this.$embeddedSchemaType = this.caster; + this.embeddedSchemaType = this.caster; } this.$isMongooseArray = true; diff --git a/lib/schema/documentArray.js b/lib/schema/documentArray.js index 7023407ca80..096c308d7f7 100644 --- a/lib/schema/documentArray.js +++ b/lib/schema/documentArray.js @@ -82,15 +82,15 @@ function SchemaDocumentArray(key, schema, options, schemaOptions) { } const $parentSchemaType = this; - this.$embeddedSchemaType = new DocumentArrayElement(key + '.$', { + this.embeddedSchemaType = new DocumentArrayElement(key + '.$', { required: this && this.schemaOptions && this.schemaOptions.required || false, $parentSchemaType }); - this.$embeddedSchemaType.caster = this.Constructor; - this.$embeddedSchemaType.schema = this.schema; + this.embeddedSchemaType.caster = this.Constructor; + this.embeddedSchemaType.schema = this.schema; } /** diff --git a/lib/schemaType.js b/lib/schemaType.js index 183e28e9d84..5cdee46889d 100644 --- a/lib/schemaType.js +++ b/lib/schemaType.js @@ -1749,7 +1749,7 @@ SchemaType.prototype.clone = function() { */ SchemaType.prototype.getEmbeddedSchemaType = function getEmbeddedSchemaType() { - return this.$embeddedSchemaType; + return this.embeddedSchemaType; }; /*! diff --git a/lib/types/array/index.js b/lib/types/array/index.js index c08dbe6b9c3..b21c23bfd12 100644 --- a/lib/types/array/index.js +++ b/lib/types/array/index.js @@ -88,8 +88,8 @@ function MongooseArray(values, path, doc, schematype) { if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) { return schematype.virtuals[prop].applyGetters(undefined, target); } - if (typeof prop === 'string' && numberRE.test(prop) && schematype?.$embeddedSchemaType != null) { - return schematype.$embeddedSchemaType.applyGetters(__array[prop], doc); + if (typeof prop === 'string' && numberRE.test(prop) && schematype?.embeddedSchemaType != null) { + return schematype.embeddedSchemaType.applyGetters(__array[prop], doc); } return __array[prop]; diff --git a/test/schema.documentarray.test.js b/test/schema.documentarray.test.js index 92eee9a4320..19246fc35a4 100644 --- a/test/schema.documentarray.test.js +++ b/test/schema.documentarray.test.js @@ -150,7 +150,7 @@ describe('schema.documentarray', function() { const TestModel = mongoose.model('Test', testSchema); const testDoc = new TestModel(); - const err = await testSchema.path('comments').$embeddedSchemaType.doValidate({}, testDoc.comments, { index: 1 }).then(() => null, err => err); + const err = await testSchema.path('comments').embeddedSchemaType.doValidate({}, testDoc.comments, { index: 1 }).then(() => null, err => err); assert.equal(err.name, 'ValidationError'); assert.equal(err.message, 'Validation failed: text: Path `text` is required.'); }); diff --git a/types/schematypes.d.ts b/types/schematypes.d.ts index 6ba62a6b102..36d40827688 100644 --- a/types/schematypes.d.ts +++ b/types/schematypes.d.ts @@ -365,7 +365,7 @@ declare module 'mongoose' { discriminator(name: string | number, schema: Schema, value?: string): Model; /** The schematype embedded in this array */ - caster?: SchemaType; + embeddedSchemaType: SchemaType; /** Default options for this SchemaType */ defaultOptions: Record; @@ -458,6 +458,12 @@ declare module 'mongoose' { /** The schema used for documents in this array */ schema: Schema; + /** The schematype embedded in this array */ + embeddedSchemaType: Subdocument; + + /** The constructor used for subdocuments in this array */ + Constructor: typeof Types.Subdocument; + /** The constructor used for subdocuments in this array */ caster?: typeof Types.Subdocument; From 761fd307e4fb9063524e329e0f4765135ede7d94 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 23 Jun 2025 10:18:18 -0400 Subject: [PATCH 082/209] WIP remove some more .caster uses re: #15179 --- lib/cast.js | 4 ++-- lib/document.js | 2 +- lib/helpers/model/applyDefaultsToPOJO.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/cast.js b/lib/cast.js index 03cbb3415c2..8e4acbb800d 100644 --- a/lib/cast.js +++ b/lib/cast.js @@ -175,12 +175,12 @@ module.exports = function cast(schema, obj, options, context) { // If a substring of the input path resolves to an actual real path... if (schematype) { // Apply the casting; similar code for $elemMatch in schema/array.js - if (schematype.caster && schematype.caster.schema) { + if (schematype.schema) { remainingConds = {}; pathLastHalf = split.slice(j).join('.'); remainingConds[pathLastHalf] = val; - const ret = cast(schematype.caster.schema, remainingConds, options, context)[pathLastHalf]; + const ret = cast(schematype.schema, remainingConds, options, context)[pathLastHalf]; if (ret === void 0) { delete obj[path]; } else { diff --git a/lib/document.js b/lib/document.js index 955ad789dac..2197e37af74 100644 --- a/lib/document.js +++ b/lib/document.js @@ -2794,7 +2794,7 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate // Optimization: if primitive path with no validators, or array of primitives // with no validators, skip validating this path entirely. - if (!_pathType.caster && _pathType.validators.length === 0 && !_pathType.$parentSchemaDocArray) { + if (!_pathType.schema && !_pathType.embeddedSchemaType && _pathType.validators.length === 0 && !_pathType.$parentSchemaDocArray) { paths.delete(path); } else if (_pathType.$isMongooseArray && !_pathType.$isMongooseDocumentArray && // Skip document arrays... diff --git a/lib/helpers/model/applyDefaultsToPOJO.js b/lib/helpers/model/applyDefaultsToPOJO.js index 4aca295cd29..0570c69d2a6 100644 --- a/lib/helpers/model/applyDefaultsToPOJO.js +++ b/lib/helpers/model/applyDefaultsToPOJO.js @@ -23,7 +23,7 @@ module.exports = function applyDefaultsToPOJO(doc, schema) { if (j === len - 1) { if (typeof doc_[piece] !== 'undefined') { if (type.$isSingleNested) { - applyDefaultsToPOJO(doc_[piece], type.caster.schema); + applyDefaultsToPOJO(doc_[piece], type.schema); } else if (type.$isMongooseDocumentArray && Array.isArray(doc_[piece])) { doc_[piece].forEach(el => applyDefaultsToPOJO(el, type.schema)); } @@ -36,7 +36,7 @@ module.exports = function applyDefaultsToPOJO(doc, schema) { doc_[piece] = def; if (type.$isSingleNested) { - applyDefaultsToPOJO(def, type.caster.schema); + applyDefaultsToPOJO(def, type.schema); } else if (type.$isMongooseDocumentArray && Array.isArray(def)) { def.forEach(el => applyDefaultsToPOJO(el, type.schema)); } From 638e2c45c7dd19d871af6c4d189762bf932a1a6a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 23 Jun 2025 10:28:39 -0400 Subject: [PATCH 083/209] WIP remove some more caster refs re: #15179 --- lib/helpers/model/applyHooks.js | 5 +---- lib/helpers/model/applyMethods.js | 4 ++-- lib/helpers/populate/getModelsMapForPopulate.js | 2 +- lib/schema/subdocument.js | 1 + types/schematypes.d.ts | 3 +++ 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/helpers/model/applyHooks.js b/lib/helpers/model/applyHooks.js index 451bdd7fc06..df08087756a 100644 --- a/lib/helpers/model/applyHooks.js +++ b/lib/helpers/model/applyHooks.js @@ -121,10 +121,7 @@ function applyHooks(model, schema, options) { */ function findChildModel(curType) { - if (curType.$isSingleNested) { - return { childModel: curType.caster, type: curType }; - } - if (curType.$isMongooseDocumentArray) { + if (curType.$isSingleNested || curType.$isMongooseDocumentArray) { return { childModel: curType.Constructor, type: curType }; } if (curType.instance === 'Array') { diff --git a/lib/helpers/model/applyMethods.js b/lib/helpers/model/applyMethods.js index e864bb1f12a..a75beceb218 100644 --- a/lib/helpers/model/applyMethods.js +++ b/lib/helpers/model/applyMethods.js @@ -60,8 +60,8 @@ module.exports = function applyMethods(model, schema) { model.$appliedMethods = true; for (const key of Object.keys(schema.paths)) { const type = schema.paths[key]; - if (type.$isSingleNested && !type.caster.$appliedMethods) { - applyMethods(type.caster, type.schema); + if (type.$isSingleNested && !type.Constructor.$appliedMethods) { + applyMethods(type.Constructor, type.schema); } if (type.$isMongooseDocumentArray && !type.Constructor.$appliedMethods) { applyMethods(type.Constructor, type.schema); diff --git a/lib/helpers/populate/getModelsMapForPopulate.js b/lib/helpers/populate/getModelsMapForPopulate.js index 74f62251ffe..168664e331b 100644 --- a/lib/helpers/populate/getModelsMapForPopulate.js +++ b/lib/helpers/populate/getModelsMapForPopulate.js @@ -219,7 +219,7 @@ module.exports = function getModelsMapForPopulate(model, docs, options) { const originalSchema = schema; if (schema && schema.instance === 'Array') { - schema = schema.caster; + schema = schema.embeddedSchemaType; } if (schema && schema.$isSchemaMap) { schema = schema.$__schemaType; diff --git a/lib/schema/subdocument.js b/lib/schema/subdocument.js index 630f6ec9686..e92ddb490d6 100644 --- a/lib/schema/subdocument.js +++ b/lib/schema/subdocument.js @@ -52,6 +52,7 @@ function SchemaSubdocument(schema, path, options) { this.caster = _createConstructor(schema, null, options); this.caster.path = path; this.caster.prototype.$basePath = path; + this.Constructor = this.caster; this.schema = schema; this.$isSingleNested = true; this.base = schema.base; diff --git a/types/schematypes.d.ts b/types/schematypes.d.ts index 36d40827688..2c17334a0c8 100644 --- a/types/schematypes.d.ts +++ b/types/schematypes.d.ts @@ -530,6 +530,9 @@ declare module 'mongoose' { /** The document's schema */ schema: Schema; + /** The constructor used for subdocuments in this array */ + Constructor: typeof Types.Subdocument; + /** Default options for this SchemaType */ defaultOptions: Record; From 1fdaee6609f4389c6652c5aac12ac75dd5229b24 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 23 Jun 2025 16:34:01 -0400 Subject: [PATCH 084/209] WIP remove some more caster usages for #15179 --- lib/helpers/populate/getModelsMapForPopulate.js | 14 +++++++------- lib/helpers/populate/getSchemaTypes.js | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/helpers/populate/getModelsMapForPopulate.js b/lib/helpers/populate/getModelsMapForPopulate.js index 168664e331b..7d1b3f47fde 100644 --- a/lib/helpers/populate/getModelsMapForPopulate.js +++ b/lib/helpers/populate/getModelsMapForPopulate.js @@ -281,8 +281,8 @@ module.exports = function getModelsMapForPopulate(model, docs, options) { schemaForCurrentDoc = modelForCurrentDoc.schema._getSchema(options.path); - if (schemaForCurrentDoc && schemaForCurrentDoc.caster) { - schemaForCurrentDoc = schemaForCurrentDoc.caster; + if (schemaForCurrentDoc && schemaForCurrentDoc.embeddedSchemaType) { + schemaForCurrentDoc = schemaForCurrentDoc.embeddedSchemaType; } } else { schemaForCurrentDoc = schema; @@ -719,16 +719,16 @@ function _findRefPathForDiscriminators(doc, modelSchema, data, options, normaliz cur = cur + (cur.length === 0 ? '' : '.') + piece; const schematype = modelSchema.path(cur); if (schematype != null && - schematype.$isMongooseArray && - schematype.caster.discriminators != null && - Object.keys(schematype.caster.discriminators).length !== 0) { + schematype.$isMongooseDocumentArray && + schematype.Constructor.discriminators != null && + Object.keys(schematype.Constructor.discriminators).length !== 0) { const subdocs = utils.getValue(cur, doc); const remnant = options.path.substring(cur.length + 1); - const discriminatorKey = schematype.caster.schema.options.discriminatorKey; + const discriminatorKey = schematype.Constructor.schema.options.discriminatorKey; modelNames = []; for (const subdoc of subdocs) { const discriminatorName = utils.getValue(discriminatorKey, subdoc); - const discriminator = schematype.caster.discriminators[discriminatorName]; + const discriminator = schematype.Constructor.discriminators[discriminatorName]; const discriminatorSchema = discriminator && discriminator.schema; if (discriminatorSchema == null) { continue; diff --git a/lib/helpers/populate/getSchemaTypes.js b/lib/helpers/populate/getSchemaTypes.js index 8bf3285ab5e..25f6dcb55f3 100644 --- a/lib/helpers/populate/getSchemaTypes.js +++ b/lib/helpers/populate/getSchemaTypes.js @@ -58,10 +58,10 @@ module.exports = function getSchemaTypes(model, schema, doc, path) { continue; } - if (foundschema.caster) { + if (foundschema.embeddedSchemaType) { // array of Mixed? - if (foundschema.caster instanceof Mixed) { - return foundschema.caster; + if (foundschema.embeddedSchemaType instanceof Mixed) { + return foundschema.embeddedSchemaType; } let schemas = null; @@ -142,11 +142,11 @@ module.exports = function getSchemaTypes(model, schema, doc, path) { } } else if (p !== parts.length && foundschema.$isMongooseArray && - foundschema.casterConstructor.$isMongooseArray) { + foundschema.embeddedSchemaType.$isMongooseArray) { // Nested arrays. Drill down to the bottom of the nested array. let type = foundschema; while (type.$isMongooseArray && !type.$isMongooseDocumentArray) { - type = type.casterConstructor; + type = type.embeddedSchemaType; } const ret = search( From 45bf3587465a8ed7cc743bc0b22201acb0571915 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 24 Jun 2025 11:47:37 -0400 Subject: [PATCH 085/209] WIP remove some more caster usages for #15179 --- lib/helpers/query/cast$expr.js | 6 ++---- lib/helpers/query/castFilterPath.js | 2 +- lib/helpers/query/castUpdate.js | 17 ++++++++--------- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/lib/helpers/query/cast$expr.js b/lib/helpers/query/cast$expr.js index a8d9a2ec795..dfbfd47b43d 100644 --- a/lib/helpers/query/cast$expr.js +++ b/lib/helpers/query/cast$expr.js @@ -174,7 +174,7 @@ function castIn(val, schema, strictQuery) { } return [ - schematype.$isMongooseDocumentArray ? schematype.embeddedSchemaType.cast(search) : schematype.caster.cast(search), + schematype.embeddedSchemaType.cast(search), path ]; } @@ -229,10 +229,8 @@ function castComparison(val, schema, strictQuery) { path = lhs[key].slice(1) + '.' + key; schematype = schema.path(lhs[key].slice(1)); if (schematype != null) { - if (schematype.$isMongooseDocumentArray) { + if (schematype.$isMongooseArray) { schematype = schematype.embeddedSchemaType; - } else if (schematype.$isMongooseArray) { - schematype = schematype.caster; } } } diff --git a/lib/helpers/query/castFilterPath.js b/lib/helpers/query/castFilterPath.js index c5c8d0fadfd..530385216f9 100644 --- a/lib/helpers/query/castFilterPath.js +++ b/lib/helpers/query/castFilterPath.js @@ -22,7 +22,7 @@ module.exports = function castFilterPath(ctx, schematype, val) { const nested = val[$cond]; if ($cond === '$not') { - if (nested && schematype && !schematype.caster) { + if (nested && schematype && !schematype.embeddedSchemaType && !schematype.Constructor) { const _keys = Object.keys(nested); if (_keys.length && isOperator(_keys[0])) { for (const key of Object.keys(nested)) { diff --git a/lib/helpers/query/castUpdate.js b/lib/helpers/query/castUpdate.js index 194ea6d5601..bea5d5e9545 100644 --- a/lib/helpers/query/castUpdate.js +++ b/lib/helpers/query/castUpdate.js @@ -281,7 +281,7 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) { continue; } - if (schematype && schematype.caster && op in castOps) { + if (schematype && (schematype.embeddedSchemaType || schematype.Constructor) && op in castOps) { // embedded doc schema if ('$each' in val) { hasKeys = true; @@ -444,9 +444,9 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) { if (Array.isArray(obj[key]) && (op === '$addToSet' || op === '$push') && key !== '$each') { if (schematype && - schematype.caster && - !schematype.caster.$isMongooseArray && - !schematype.caster[schemaMixedSymbol]) { + schematype.embeddedSchemaType && + !schematype.embeddedSchemaType.$isMongooseArray && + !schematype.embeddedSchemaType[schemaMixedSymbol]) { obj[key] = { $each: obj[key] }; } } @@ -548,10 +548,9 @@ function castUpdateVal(schema, val, op, $conditional, context, path) { return val; } - // console.log('CastUpdateVal', path, op, val, schema); - - const cond = schema.caster && op in castOps && - (utils.isObject(val) || Array.isArray(val)); + const cond = schema.$isMongooseArray + && op in castOps + && (utils.isObject(val) || Array.isArray(val)); if (cond && !overwriteOps[op]) { // Cast values for ops that add data to MongoDB. // Ensures embedded documents get ObjectIds etc. @@ -559,7 +558,7 @@ function castUpdateVal(schema, val, op, $conditional, context, path) { let cur = schema; while (cur.$isMongooseArray) { ++schemaArrayDepth; - cur = cur.caster; + cur = cur.embeddedSchemaType; } let arrayDepth = 0; let _val = val; From 321a5f97596795cf126a62ab8c5ec01cfe96b697 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 24 Jun 2025 12:30:04 -0400 Subject: [PATCH 086/209] WIP remove some more caster usages for #15179 --- lib/helpers/query/castUpdate.js | 5 ++++- lib/helpers/query/getEmbeddedDiscriminatorPath.js | 2 +- lib/helpers/schema/applyPlugins.js | 2 +- lib/schema.js | 8 ++++---- lib/schema/documentArray.js | 8 +++----- lib/schema/documentArrayElement.js | 9 ++++++--- lib/schema/subdocument.js | 2 +- 7 files changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/helpers/query/castUpdate.js b/lib/helpers/query/castUpdate.js index bea5d5e9545..48e2465bd4d 100644 --- a/lib/helpers/query/castUpdate.js +++ b/lib/helpers/query/castUpdate.js @@ -619,7 +619,10 @@ function castUpdateVal(schema, val, op, $conditional, context, path) { } if (overwriteOps[op]) { - const skipQueryCastForUpdate = val != null && schema.$isMongooseArray && schema.$fullPath != null && !schema.$fullPath.match(/\d+$/); + const skipQueryCastForUpdate = val != null + && schema.$isMongooseArray + && schema.$fullPath != null + && !schema.$fullPath.match(/\d+$/); const applySetters = schema[schemaMixedSymbol] != null; if (skipQueryCastForUpdate || applySetters) { return schema.applySetters(val, context); diff --git a/lib/helpers/query/getEmbeddedDiscriminatorPath.js b/lib/helpers/query/getEmbeddedDiscriminatorPath.js index 60bad97f816..c8b9be7ffeb 100644 --- a/lib/helpers/query/getEmbeddedDiscriminatorPath.js +++ b/lib/helpers/query/getEmbeddedDiscriminatorPath.js @@ -82,7 +82,7 @@ module.exports = function getEmbeddedDiscriminatorPath(schema, update, filter, p continue; } - const discriminator = getDiscriminatorByValue(schematype.caster.discriminators, discriminatorKey); + const discriminator = getDiscriminatorByValue(schematype.Constructor.discriminators, discriminatorKey); const discriminatorSchema = discriminator && discriminator.schema; if (discriminatorSchema == null) { continue; diff --git a/lib/helpers/schema/applyPlugins.js b/lib/helpers/schema/applyPlugins.js index fe976800771..2bc499c8309 100644 --- a/lib/helpers/schema/applyPlugins.js +++ b/lib/helpers/schema/applyPlugins.js @@ -33,7 +33,7 @@ module.exports = function applyPlugins(schema, plugins, options, cacheKey) { applyPlugins(type.schema, plugins, options, cacheKey); // Recompile schema because plugins may have changed it, see gh-7572 - type.caster.prototype.$__setSchema(type.schema); + type.Constructor.prototype.$__setSchema(type.schema); } } } diff --git a/lib/schema.js b/lib/schema.js index ab68d835825..89ddb909440 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -2918,11 +2918,11 @@ Schema.prototype._getSchema = function(path) { if (foundschema) { resultPath.push(trypath); - if (foundschema.caster) { + if (foundschema.embeddedSchemaType || foundschema.Constructor) { // array of Mixed? - if (foundschema.caster instanceof MongooseTypes.Mixed) { - foundschema.caster.$fullPath = resultPath.join('.'); - return foundschema.caster; + if (foundschema.embeddedSchemaType instanceof MongooseTypes.Mixed) { + foundschema.embeddedSchemaType.$fullPath = resultPath.join('.'); + return foundschema.embeddedSchemaType; } // Now that we found the array, we need to check if there diff --git a/lib/schema/documentArray.js b/lib/schema/documentArray.js index 096c308d7f7..a01beb2f66d 100644 --- a/lib/schema/documentArray.js +++ b/lib/schema/documentArray.js @@ -82,15 +82,13 @@ function SchemaDocumentArray(key, schema, options, schemaOptions) { } const $parentSchemaType = this; - this.embeddedSchemaType = new DocumentArrayElement(key + '.$', { + this.embeddedSchemaType = new DocumentArrayElement(key + '.$', this.schema, { required: this && this.schemaOptions && this.schemaOptions.required || false, - $parentSchemaType + $parentSchemaType, + Constructor: this.Constructor }); - - this.embeddedSchemaType.caster = this.Constructor; - this.embeddedSchemaType.schema = this.schema; } /** diff --git a/lib/schema/documentArrayElement.js b/lib/schema/documentArrayElement.js index 552dd94a428..749f3e5f552 100644 --- a/lib/schema/documentArrayElement.js +++ b/lib/schema/documentArrayElement.js @@ -18,7 +18,7 @@ const getConstructor = require('../helpers/discriminator/getConstructor'); * @api public */ -function SchemaDocumentArrayElement(path, options) { +function SchemaDocumentArrayElement(path, schema, options) { this.$parentSchemaType = options && options.$parentSchemaType; if (!this.$parentSchemaType) { throw new MongooseError('Cannot create DocumentArrayElement schematype without a parent'); @@ -28,6 +28,9 @@ function SchemaDocumentArrayElement(path, options) { SchemaType.call(this, path, options, 'DocumentArrayElement'); this.$isMongooseDocumentArrayElement = true; + this.Constructor = options && options.Constructor; + this.caster = this.Constructor; + this.schema = schema; } /** @@ -64,7 +67,7 @@ SchemaDocumentArrayElement.prototype.cast = function(...args) { */ SchemaDocumentArrayElement.prototype.doValidate = async function doValidate(value, scope, options) { - const Constructor = getConstructor(this.caster, value); + const Constructor = getConstructor(this.Constructor, value); if (value && !(value instanceof Constructor)) { value = new Constructor(value, scope, null, null, options && options.index != null ? options.index : null); @@ -85,7 +88,7 @@ SchemaDocumentArrayElement.prototype.clone = function() { const ret = SchemaType.prototype.clone.apply(this, arguments); delete this.options.$parentSchemaType; - ret.caster = this.caster; + ret.Constructor = this.Constructor; ret.schema = this.schema; return ret; diff --git a/lib/schema/subdocument.js b/lib/schema/subdocument.js index e92ddb490d6..2ccee0e4259 100644 --- a/lib/schema/subdocument.js +++ b/lib/schema/subdocument.js @@ -252,7 +252,7 @@ SchemaSubdocument.prototype.castForQuery = function($conditional, val, context, */ SchemaSubdocument.prototype.doValidate = async function doValidate(value, scope, options) { - const Constructor = getConstructor(this.caster, value); + const Constructor = getConstructor(this.Constructor, value); if (value && !(value instanceof Constructor)) { value = new Constructor(value, null, (scope != null && scope.$__ != null) ? scope : null); From 7f84bf6e7ebb7f51ac106ae615954581a231a7ff Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 24 Jun 2025 12:30:57 -0400 Subject: [PATCH 087/209] WIP remove some more caster usages for #15179 --- lib/helpers/schema/getIndexes.js | 2 +- lib/model.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/helpers/schema/getIndexes.js b/lib/helpers/schema/getIndexes.js index 706439d321d..de48bfe2bf2 100644 --- a/lib/helpers/schema/getIndexes.js +++ b/lib/helpers/schema/getIndexes.js @@ -67,7 +67,7 @@ module.exports = function getIndexes(schema) { } } - const index = path._index || (path.caster && path.caster._index); + const index = path._index || (path.embeddedSchemaType && path.embeddedSchemaType._index); if (index !== false && index !== null && index !== undefined) { const field = {}; diff --git a/lib/model.js b/lib/model.js index bea9dd819e0..bf32e8b52dd 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3683,7 +3683,7 @@ Model.castObject = function castObject(obj, options) { } } else { cur[pieces[pieces.length - 1]] = [ - Model.castObject.call(schemaType.caster, val) + Model.castObject.call(schemaType.Constructor, val) ]; } @@ -3692,7 +3692,7 @@ Model.castObject = function castObject(obj, options) { } if (schemaType.$isSingleNested || schemaType.$isMongooseDocumentArrayElement) { try { - val = Model.castObject.call(schemaType.caster, val); + val = Model.castObject.call(schemaType.Constructor, val); } catch (err) { if (!options.ignoreCastErrors) { error = error || new ValidationError(); From 659b2551b108b39b265987b739bf7862d1cf0607 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 24 Jun 2025 12:47:17 -0400 Subject: [PATCH 088/209] WIP remove some more caster usages for #15179 --- lib/queryHelpers.js | 2 +- lib/schema.js | 36 +++++++++++++++--------------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/lib/queryHelpers.js b/lib/queryHelpers.js index 0a6ae5ee3c0..272b2722b7e 100644 --- a/lib/queryHelpers.js +++ b/lib/queryHelpers.js @@ -268,7 +268,7 @@ exports.applyPaths = function applyPaths(fields, schema, sanitizeProjection) { let addedPath = analyzePath(path, type); // arrays if (addedPath == null && !Array.isArray(type) && type.$isMongooseArray && !type.$isMongooseDocumentArray) { - addedPath = analyzePath(path, type.caster); + addedPath = analyzePath(path, type.embeddedSchemaType); } if (addedPath != null) { addedPaths.push(addedPath); diff --git a/lib/schema.js b/lib/schema.js index 89ddb909440..bcfbd665f28 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1328,7 +1328,7 @@ Schema.prototype.path = function(path, obj) { if (schemaType.$__schemaType.$isSingleNested) { this.childSchemas.push({ schema: schemaType.$__schemaType.schema, - model: schemaType.$__schemaType.caster, + model: schemaType.$__schemaType.Constructor, path: path }); } @@ -1357,10 +1357,10 @@ Schema.prototype.path = function(path, obj) { value: this.base }); - schemaType.caster.base = this.base; + schemaType.Constructor.base = this.base; this.childSchemas.push({ schema: schemaType.schema, - model: schemaType.caster, + model: schemaType.Constructor, path: path }); } else if (schemaType.$isMongooseDocumentArray) { @@ -1371,15 +1371,15 @@ Schema.prototype.path = function(path, obj) { value: this.base }); - schemaType.casterConstructor.base = this.base; + schemaType.Constructor.base = this.base; this.childSchemas.push({ schema: schemaType.schema, - model: schemaType.casterConstructor, + model: schemaType.Constructor, path: path }); } - if (schemaType.$isMongooseArray && schemaType.caster instanceof SchemaType) { + if (schemaType.$isMongooseArray && !schemaType.$isMongooseDocumentArray) { let arrayPath = path; let _schemaType = schemaType; @@ -1388,15 +1388,9 @@ Schema.prototype.path = function(path, obj) { arrayPath = arrayPath + '.$'; // Skip arrays of document arrays - if (_schemaType.$isMongooseDocumentArray) { - _schemaType.embeddedSchemaType._arrayPath = arrayPath; - _schemaType.embeddedSchemaType._arrayParentPath = path; - _schemaType = _schemaType.embeddedSchemaType; - } else { - _schemaType.caster._arrayPath = arrayPath; - _schemaType.caster._arrayParentPath = path; - _schemaType = _schemaType.caster; - } + _schemaType.embeddedSchemaType._arrayPath = arrayPath; + _schemaType.embeddedSchemaType._arrayParentPath = path; + _schemaType = _schemaType.embeddedSchemaType; this.subpaths[arrayPath] = _schemaType; } @@ -1448,13 +1442,13 @@ Schema.prototype._gatherChildSchemas = function _gatherChildSchemas() { if (schematype.$isMongooseDocumentArray || schematype.$isSingleNested) { childSchemas.push({ schema: schematype.schema, - model: schematype.caster, + model: schematype.Constructor, path: path }); } else if (schematype.$isSchemaMap && schematype.$__schemaType.$isSingleNested) { childSchemas.push({ schema: schematype.$__schemaType.schema, - model: schematype.$__schemaType.caster, + model: schematype.$__schemaType.Constructor, path: path }); } @@ -2012,7 +2006,7 @@ function getPositionalPathType(self, path, cleanPath) { val = val.embeddedSchemaType; } else if (val instanceof MongooseTypes.Array) { // StringSchema, NumberSchema, etc - val = val.caster; + val = val.embeddedSchemaType; } else { val = undefined; } @@ -2023,7 +2017,7 @@ function getPositionalPathType(self, path, cleanPath) { if (!/\D/.test(subpath)) { // Nested array if (val instanceof MongooseTypes.Array && i !== last) { - val = val.caster; + val = val.embeddedSchemaType; } continue; } @@ -3021,9 +3015,9 @@ Schema.prototype._getPathType = function(path) { trypath = parts.slice(0, p).join('.'); foundschema = schema.path(trypath); if (foundschema) { - if (foundschema.caster) { + if (foundschema.embeddedSchemaType || foundschema.Constructor) { // array of Mixed? - if (foundschema.caster instanceof MongooseTypes.Mixed) { + if (foundschema.embeddedSchemaType instanceof MongooseTypes.Mixed) { return { schema: foundschema, pathType: 'mixed' }; } From 452940b79d75f2a571156b5720b027cb0dd30973 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 2 Jul 2025 12:07:08 -0400 Subject: [PATCH 089/209] fix merge issue --- package.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/package.json b/package.json index 51e961fddd4..ef053887e5a 100644 --- a/package.json +++ b/package.json @@ -55,13 +55,7 @@ "ncp": "^2.0.0", "nyc": "15.1.0", "pug": "3.0.3", -<<<<<<< HEAD - "sinon": "20.0.0", -======= - "q": "1.5.1", "sinon": "21.0.0", - "stream-browserify": "3.0.0", ->>>>>>> master "tsd": "0.32.0", "typescript": "5.8.3", "typescript-eslint": "^8.31.1", From f986aaed3fcc6abe3fdbdcb2aad80bd7487d586d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 2 Jul 2025 13:29:42 -0400 Subject: [PATCH 090/209] remove some more usage of .caster --- lib/schema/array.js | 21 +++++++-------- lib/schema/documentArray.js | 34 ++++++++++++------------ lib/schema/subdocument.js | 20 +++++++------- lib/types/array/methods/index.js | 8 +++--- lib/types/documentArray/index.js | 2 +- lib/types/documentArray/methods/index.js | 4 +-- test/document.test.js | 4 +-- test/query.test.js | 2 +- test/schema.test.js | 32 ++++++++++------------ 9 files changed, 61 insertions(+), 66 deletions(-) diff --git a/lib/schema/array.js b/lib/schema/array.js index b0e1897d54b..4f43f95d577 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -262,10 +262,10 @@ SchemaArray.prototype.enum = function() { let arr = this; while (true) { const instance = arr && - arr.caster && - arr.caster.instance; + arr.embeddedSchemaType && + arr.embeddedSchemaType.instance; if (instance === 'Array') { - arr = arr.caster; + arr = arr.embeddedSchemaType; continue; } if (instance !== 'String' && instance !== 'Number') { @@ -280,7 +280,7 @@ SchemaArray.prototype.enum = function() { enumArray = utils.object.vals(enumArray); } - arr.caster.enum.apply(arr.caster, enumArray); + arr.embeddedSchemaType.enum.apply(arr.embeddedSchemaType, enumArray); return this; }; @@ -443,20 +443,19 @@ SchemaArray.prototype._castForPopulate = function _castForPopulate(value, doc) { let i; const rawValue = value.__array ? value.__array : value; const len = rawValue.length; - - const caster = this.caster; - if (caster && this.casterConstructor !== Mixed) { +; + if (this.embeddedSchemaType && this.casterConstructor !== Mixed) { try { for (i = 0; i < len; i++) { const opts = {}; // Perf: creating `arrayPath` is expensive for large arrays. // We only need `arrayPath` if this is a nested array, so // skip if possible. - if (caster.$isMongooseArray && caster._arrayParentPath != null) { + if (this.embeddedSchemaType.$isMongooseArray && this.embeddedSchemaType._arrayParentPath != null) { opts.arrayPathIndex = i; } - rawValue[i] = caster.cast(rawValue[i], doc, false, void 0, opts); + rawValue[i] = this.embeddedSchemaType.cast(rawValue[i], doc, false, void 0, opts); } } catch (e) { // rethrow @@ -494,7 +493,7 @@ SchemaArray.prototype.discriminator = function(...args) { SchemaArray.prototype.clone = function() { const options = Object.assign({}, this.options); - const schematype = new this.constructor(this.path, this.caster, options, this.schemaOptions); + const schematype = new this.constructor(this.path, this.embeddedSchemaType, options, this.schemaOptions); schematype.validators = this.validators.slice(); if (this.requiredValidator !== undefined) { schematype.requiredValidator = this.requiredValidator; @@ -525,7 +524,7 @@ SchemaArray.prototype._castForQuery = function(val, context) { const protoCastForQuery = proto && proto.castForQuery; const protoCast = proto && proto.cast; const constructorCastForQuery = Constructor.castForQuery; - const caster = this.caster; + const caster = this.embeddedSchemaType; if (Array.isArray(val)) { this.setters.reverse().forEach(setter => { diff --git a/lib/schema/documentArray.js b/lib/schema/documentArray.js index a01beb2f66d..d2e193bc20f 100644 --- a/lib/schema/documentArray.js +++ b/lib/schema/documentArray.js @@ -197,18 +197,18 @@ SchemaDocumentArray.prototype.discriminator = function(name, schema, options) { schema = schema.clone(); } - schema = discriminator(this.casterConstructor, name, schema, tiedValue, null, null, options?.overwriteExisting); + schema = discriminator(this.Constructor, name, schema, tiedValue, null, null, options?.overwriteExisting); - const EmbeddedDocument = _createConstructor(schema, null, this.casterConstructor); - EmbeddedDocument.baseCasterConstructor = this.casterConstructor; + const EmbeddedDocument = _createConstructor(schema, null, this.Constructor); + EmbeddedDocument.baseCasterConstructor = this.Constructor; Object.defineProperty(EmbeddedDocument, 'name', { value: name }); - this.casterConstructor.discriminators[name] = EmbeddedDocument; + this.Constructor.discriminators[name] = EmbeddedDocument; - return this.casterConstructor.discriminators[name]; + return this.Constructor.discriminators[name]; }; /** @@ -241,7 +241,7 @@ SchemaDocumentArray.prototype.doValidate = async function doValidate(array, scop // If you set the array index directly, the doc might not yet be // a full fledged mongoose subdoc, so make it into one. if (!(doc instanceof Subdocument)) { - const Constructor = getConstructor(this.casterConstructor, array[i]); + const Constructor = getConstructor(this.Constructor, array[i]); doc = array[i] = new Constructor(doc, array, undefined, undefined, i); } @@ -293,7 +293,7 @@ SchemaDocumentArray.prototype.doValidateSync = function(array, scope, options) { // If you set the array index directly, the doc might not yet be // a full fledged mongoose subdoc, so make it into one. if (!(doc instanceof Subdocument)) { - const Constructor = getConstructor(this.casterConstructor, array[i]); + const Constructor = getConstructor(this.Constructor, array[i]); doc = array[i] = new Constructor(doc, array, undefined, undefined, i); } @@ -338,7 +338,7 @@ SchemaDocumentArray.prototype.getDefault = function(scope, init, options) { ret = new MongooseDocumentArray(ret, this.path, scope); for (let i = 0; i < ret.length; ++i) { - const Constructor = getConstructor(this.casterConstructor, ret[i]); + const Constructor = getConstructor(this.Constructor, ret[i]); const _subdoc = new Constructor({}, ret, undefined, undefined, i); _subdoc.$init(ret[i]); @@ -416,7 +416,7 @@ SchemaDocumentArray.prototype.cast = function(value, doc, init, prev, options) { continue; } - const Constructor = getConstructor(this.casterConstructor, rawArray[i]); + const Constructor = getConstructor(this.Constructor, rawArray[i]); const spreadDoc = handleSpreadDoc(rawArray[i], true); if (rawArray[i] !== spreadDoc) { @@ -601,21 +601,21 @@ function cast$elemMatch(val, context) { // Is this an embedded discriminator and is the discriminator key set? // If so, use the discriminator schema. See gh-7449 const discriminatorKey = this && - this.casterConstructor && - this.casterConstructor.schema && - this.casterConstructor.schema.options && - this.casterConstructor.schema.options.discriminatorKey; + this.Constructor && + this.Constructor.schema && + this.Constructor.schema.options && + this.Constructor.schema.options.discriminatorKey; const discriminators = this && - this.casterConstructor && - this.casterConstructor.schema && - this.casterConstructor.schema.discriminators || {}; + this.Constructor && + this.Constructor.schema && + this.Constructor.schema.discriminators || {}; if (discriminatorKey != null && val[discriminatorKey] != null && discriminators[val[discriminatorKey]] != null) { return cast(discriminators[val[discriminatorKey]], val, null, this && this.$$context); } - const schema = this.casterConstructor.schema ?? context.schema; + const schema = this.Constructor.schema ?? context.schema; return cast(schema, val, null, this && this.$$context); } diff --git a/lib/schema/subdocument.js b/lib/schema/subdocument.js index 2ccee0e4259..2d5e95f041c 100644 --- a/lib/schema/subdocument.js +++ b/lib/schema/subdocument.js @@ -49,10 +49,10 @@ function SchemaSubdocument(schema, path, options) { schema = handleIdOption(schema, options); - this.caster = _createConstructor(schema, null, options); - this.caster.path = path; - this.caster.prototype.$basePath = path; - this.Constructor = this.caster; + this.Constructor = _createConstructor(schema, null, options); + this.Constructor.path = path; + this.Constructor.prototype.$basePath = path; + this.caster = this.Constructor; this.schema = schema; this.$isSingleNested = true; this.base = schema.base; @@ -167,7 +167,7 @@ SchemaSubdocument.prototype.cast = function(val, doc, init, priorVal, options) { const discriminatorKeyPath = this.schema.path(this.schema.options.discriminatorKey); const defaultDiscriminatorValue = discriminatorKeyPath == null ? null : discriminatorKeyPath.getDefault(doc); - const Constructor = getConstructor(this.caster, val, defaultDiscriminatorValue); + const Constructor = getConstructor(this.Constructor, val, defaultDiscriminatorValue); let subdoc; @@ -220,7 +220,7 @@ SchemaSubdocument.prototype.castForQuery = function($conditional, val, context, return val; } - const Constructor = getConstructor(this.caster, val); + const Constructor = getConstructor(this.Constructor, val); if (val instanceof Constructor) { return val; } @@ -322,11 +322,11 @@ SchemaSubdocument.prototype.discriminator = function(name, schema, options) { schema = schema.clone(); } - schema = discriminator(this.caster, name, schema, value, null, null, options.overwriteExisting); + schema = discriminator(this.Constructor, name, schema, value, null, null, options.overwriteExisting); - this.caster.discriminators[name] = _createConstructor(schema, this.caster); + this.Constructor.discriminators[name] = _createConstructor(schema, this.Constructor); - return this.caster.discriminators[name]; + return this.Constructor.discriminators[name]; }; /*! @@ -389,7 +389,7 @@ SchemaSubdocument.prototype.clone = function() { if (this.requiredValidator !== undefined) { schematype.requiredValidator = this.requiredValidator; } - schematype.caster.discriminators = Object.assign({}, this.caster.discriminators); + schematype.Constructor.discriminators = Object.assign({}, this.caster.discriminators); schematype._appliedDiscriminators = this._appliedDiscriminators; return schematype; }; diff --git a/lib/types/array/methods/index.js b/lib/types/array/methods/index.js index 3322bbe56e8..a47a6a5cab3 100644 --- a/lib/types/array/methods/index.js +++ b/lib/types/array/methods/index.js @@ -244,10 +244,10 @@ const methods = { if (!isDisc) { value = new Model(value); } - return this[arraySchemaSymbol].caster.applySetters(value, parent, true); + return this[arraySchemaSymbol].embeddedSchemaType.applySetters(value, parent, true); } - return this[arraySchemaSymbol].caster.applySetters(value, parent, false); + return this[arraySchemaSymbol].embeddedSchemaType.applySetters(value, parent, false); }, /** @@ -1000,7 +1000,7 @@ function _minimizePath(obj, parts, i) { function _checkManualPopulation(arr, docs) { const ref = arr == null ? null : - arr[arraySchemaSymbol] && arr[arraySchemaSymbol].caster && arr[arraySchemaSymbol].caster.options && arr[arraySchemaSymbol].caster.options.ref || null; + arr[arraySchemaSymbol]?.embeddedSchemaType?.options?.ref || null; if (arr.length === 0 && docs.length !== 0) { if (_isAllSubdocs(docs, ref)) { @@ -1018,7 +1018,7 @@ function _checkManualPopulation(arr, docs) { function _depopulateIfNecessary(arr, docs) { const ref = arr == null ? null : - arr[arraySchemaSymbol] && arr[arraySchemaSymbol].caster && arr[arraySchemaSymbol].caster.options && arr[arraySchemaSymbol].caster.options.ref || null; + arr[arraySchemaSymbol]?.embeddedSchemaType?.options?.ref || null; const parentDoc = arr[arrayParentSymbol]; const path = arr[arrayPathSymbol]; if (!ref || !parentDoc.populated(path)) { diff --git a/lib/types/documentArray/index.js b/lib/types/documentArray/index.js index f43522659c4..ccc0d230fdb 100644 --- a/lib/types/documentArray/index.js +++ b/lib/types/documentArray/index.js @@ -61,7 +61,7 @@ function MongooseDocumentArray(values, path, doc, schematype) { while (internals[arraySchemaSymbol] != null && internals[arraySchemaSymbol].$isMongooseArray && !internals[arraySchemaSymbol].$isMongooseDocumentArray) { - internals[arraySchemaSymbol] = internals[arraySchemaSymbol].casterConstructor; + internals[arraySchemaSymbol] = internals[arraySchemaSymbol].embeddedSchemaType; } } diff --git a/lib/types/documentArray/methods/index.js b/lib/types/documentArray/methods/index.js index 29e4b0d77fd..79c5e4b88b8 100644 --- a/lib/types/documentArray/methods/index.js +++ b/lib/types/documentArray/methods/index.js @@ -53,7 +53,7 @@ const methods = { if (this[arraySchemaSymbol] == null) { return value; } - let Constructor = this[arraySchemaSymbol].casterConstructor; + let Constructor = this[arraySchemaSymbol].Constructor; const isInstance = Constructor.$isMongooseDocumentArray ? utils.isMongooseDocumentArray(value) : value instanceof Constructor; @@ -273,7 +273,7 @@ const methods = { */ create(obj) { - let Constructor = this[arraySchemaSymbol].casterConstructor; + let Constructor = this[arraySchemaSymbol].Constructor; if (obj && Constructor.discriminators && Constructor.schema && diff --git a/test/document.test.js b/test/document.test.js index 4c8da07d41c..aec5c4e8dd8 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -873,7 +873,7 @@ describe('document', function() { // override to check if toJSON gets fired const path = TestDocument.prototype.schema.path('em'); - path.casterConstructor.prototype.toJSON = function() { + path.Constructor.prototype.toJSON = function() { return {}; }; @@ -889,7 +889,7 @@ describe('document', function() { assert.equal(clone.em[0].constructor.name, 'Object'); assert.equal(Object.keys(clone.em[0]).length, 0); delete doc.schema.options.toJSON; - delete path.casterConstructor.prototype.toJSON; + delete path.Constructor.prototype.toJSON; doc.schema.options.toJSON = { minimize: false }; delete doc.schema._defaultToObjectOptionsMap; diff --git a/test/query.test.js b/test/query.test.js index c9fc1ae777b..1fde2cc5bfe 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -4211,7 +4211,7 @@ describe('Query', function() { }); const Test = db.model('Test', schema); - const BookHolder = schema.path('bookHolder').caster; + const BookHolder = schema.path('bookHolder').Constructor; await Test.collection.insertOne({ title: 'test-defaults-disabled', diff --git a/test/schema.test.js b/test/schema.test.js index 00eecb46ba0..9645544bbb5 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -1667,7 +1667,7 @@ describe('schema', function() { test: [{ $type: String }] }, { typeKey: '$type' }); - assert.equal(testSchema.paths.test.caster.instance, 'String'); + assert.equal(testSchema.paths.test.embeddedSchemaType.instance, 'String'); const Test = mongoose.model('gh4548', testSchema); const test = new Test({ test: [123] }); @@ -1680,11 +1680,7 @@ describe('schema', function() { test: [Array] }); - assert.ok(testSchema.paths.test.casterConstructor !== Array); - assert.equal(testSchema.paths.test.casterConstructor, - mongoose.Schema.Types.Array); - - + assert.ok(testSchema.paths.test.embeddedSchemaType instanceof mongoose.Schema.Types.Array); }); describe('remove()', function() { @@ -1788,7 +1784,7 @@ describe('schema', function() { nums: ['Decimal128'] }); assert.ok(schema.path('num') instanceof Decimal128); - assert.ok(schema.path('nums').caster instanceof Decimal128); + assert.ok(schema.path('nums').embeddedSchemaType instanceof Decimal128); const casted = schema.path('num').cast('6.2e+23'); assert.ok(casted instanceof mongoose.Types.Decimal128); @@ -1952,7 +1948,7 @@ describe('schema', function() { const clone = bananaSchema.clone(); schema.path('fruits').discriminator('banana', clone); - assert.ok(clone.path('color').caster.discriminators); + assert.ok(clone.path('color').Constructor.discriminators); const Basket = db.model('Test', schema); const b = new Basket({ @@ -2125,7 +2121,7 @@ describe('schema', function() { const schema = Schema({ testId: [{ type: 'ObjectID' }] }); const path = schema.path('testId'); assert.ok(path); - assert.ok(path.caster instanceof Schema.ObjectId); + assert.ok(path.embeddedSchemaType instanceof Schema.ObjectId); }); it('supports getting path under array (gh-8057)', function() { @@ -2579,7 +2575,7 @@ describe('schema', function() { arr: mongoose.Schema.Types.Array }); - assert.equal(schema.path('arr').caster.instance, 'Mixed'); + assert.equal(schema.path('arr').embeddedSchemaType.instance, 'Mixed'); }); it('handles using a schematype when defining a path (gh-9370)', function() { @@ -2670,9 +2666,9 @@ describe('schema', function() { subdocs: { type: Array, of: Schema({ name: String }) } }); - assert.equal(schema.path('nums').caster.instance, 'Number'); - assert.equal(schema.path('tags').caster.instance, 'String'); - assert.equal(schema.path('subdocs').casterConstructor.schema.path('name').instance, 'String'); + assert.equal(schema.path('nums').embeddedSchemaType.instance, 'Number'); + assert.equal(schema.path('tags').embeddedSchemaType.instance, 'String'); + assert.equal(schema.path('subdocs').embeddedSchemaType.schema.path('name').instance, 'String'); }); it('should use the top-most class\'s getter/setter gh-8892', function() { @@ -2817,8 +2813,8 @@ describe('schema', function() { somethingElse: { type: [{ type: { somePath: String } }] } }); - assert.equal(schema.path('something').caster.schema.path('somePath').instance, 'String'); - assert.equal(schema.path('somethingElse').caster.schema.path('somePath').instance, 'String'); + assert.equal(schema.path('something').embeddedSchemaType.schema.path('somePath').instance, 'String'); + assert.equal(schema.path('somethingElse').embeddedSchemaType.schema.path('somePath').instance, 'String'); }); it('handles `Date` with `type` (gh-10807)', function() { @@ -3218,9 +3214,9 @@ describe('schema', function() { tags: [{ type: 'Array', of: String }], subdocs: [{ type: Array, of: Schema({ name: String }) }] }); - assert.equal(schema.path('nums.$').caster.instance, 'Number'); // actually Mixed - assert.equal(schema.path('tags.$').caster.instance, 'String'); // actually Mixed - assert.equal(schema.path('subdocs.$').casterConstructor.schema.path('name').instance, 'String'); // actually Mixed + assert.equal(schema.path('nums.$').embeddedSchemaType.instance, 'Number'); + assert.equal(schema.path('tags.$').embeddedSchemaType.instance, 'String'); + assert.equal(schema.path('subdocs.$').embeddedSchemaType.schema.path('name').instance, 'String'); }); it('handles discriminator options with Schema.prototype.discriminator (gh-14448)', async function() { const eventSchema = new mongoose.Schema({ From 3b77da0f9e9dba5e089777844a4c22f3445caa48 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 2 Jul 2025 13:57:31 -0400 Subject: [PATCH 091/209] remove a few more .caster re: #15179 --- lib/schema/array.js | 21 ++++++++++----------- lib/schema/documentArrayElement.js | 1 - lib/schema/subdocument.js | 3 +-- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/lib/schema/array.js b/lib/schema/array.js index 4f43f95d577..10f9439eca2 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -303,7 +303,7 @@ SchemaArray.prototype.applyGetters = function(value, scope) { }; SchemaArray.prototype._applySetters = function(value, scope, init, priorVal) { - if (this.casterConstructor.$isMongooseArray && + if (this.embeddedSchemaType.$isMongooseArray && SchemaArray.options.castNonArrays && !this[isNestedArraySymbol]) { // Check nesting levels and wrap in array if necessary @@ -313,7 +313,7 @@ SchemaArray.prototype._applySetters = function(value, scope, init, priorVal) { arr.$isMongooseArray && !arr.$isMongooseDocumentArray) { ++depth; - arr = arr.casterConstructor; + arr = arr.embeddedSchemaType; } // No need to wrap empty arrays @@ -387,9 +387,9 @@ SchemaArray.prototype.cast = function(value, doc, init, prev, options) { return value; } - const caster = this.caster; + const caster = this.embeddedSchemaType; const isMongooseArray = caster.$isMongooseArray; - if (caster && this.casterConstructor !== Mixed) { + if (caster && this.embeddedSchemaType.constructor !== Mixed) { try { const len = rawValue.length; for (i = 0; i < len; i++) { @@ -443,8 +443,8 @@ SchemaArray.prototype._castForPopulate = function _castForPopulate(value, doc) { let i; const rawValue = value.__array ? value.__array : value; const len = rawValue.length; -; - if (this.embeddedSchemaType && this.casterConstructor !== Mixed) { + + if (this.embeddedSchemaType && this.embeddedSchemaType.constructor !== Mixed) { try { for (i = 0; i < len; i++) { const opts = {}; @@ -478,11 +478,10 @@ SchemaArray.prototype.$toObject = SchemaArray.prototype.toObject; SchemaArray.prototype.discriminator = function(...args) { let arr = this; while (arr.$isMongooseArray && !arr.$isMongooseDocumentArray) { - arr = arr.casterConstructor; - if (arr == null || typeof arr === 'function') { - throw new MongooseError('You can only add an embedded discriminator on ' + - 'a document array, ' + this.path + ' is a plain array'); - } + arr = arr.embeddedSchemaType; + } + if (!arr.$isMongooseDocumentArray) { + throw new MongooseError('You can only add an embedded discriminator on a document array, ' + this.path + ' is a plain array'); } return arr.discriminator(...args); }; diff --git a/lib/schema/documentArrayElement.js b/lib/schema/documentArrayElement.js index 749f3e5f552..3f2e9fed0ef 100644 --- a/lib/schema/documentArrayElement.js +++ b/lib/schema/documentArrayElement.js @@ -29,7 +29,6 @@ function SchemaDocumentArrayElement(path, schema, options) { this.$isMongooseDocumentArrayElement = true; this.Constructor = options && options.Constructor; - this.caster = this.Constructor; this.schema = schema; } diff --git a/lib/schema/subdocument.js b/lib/schema/subdocument.js index 2d5e95f041c..7d945dbef2a 100644 --- a/lib/schema/subdocument.js +++ b/lib/schema/subdocument.js @@ -52,7 +52,6 @@ function SchemaSubdocument(schema, path, options) { this.Constructor = _createConstructor(schema, null, options); this.Constructor.path = path; this.Constructor.prototype.$basePath = path; - this.caster = this.Constructor; this.schema = schema; this.$isSingleNested = true; this.base = schema.base; @@ -389,7 +388,7 @@ SchemaSubdocument.prototype.clone = function() { if (this.requiredValidator !== undefined) { schematype.requiredValidator = this.requiredValidator; } - schematype.Constructor.discriminators = Object.assign({}, this.caster.discriminators); + schematype.Constructor.discriminators = Object.assign({}, this.Constructor.discriminators); schematype._appliedDiscriminators = this._appliedDiscriminators; return schematype; }; From 74d5ad06020a56f91bf5cc2fc320ae5736809e52 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 3 Jul 2025 11:21:58 -0400 Subject: [PATCH 092/209] final removal of caster and casterConstructor --- lib/schema/array.js | 81 ++++++++++--------------------------- lib/schema/documentArray.js | 27 ++++++------- types/schematypes.d.ts | 5 +-- 3 files changed, 35 insertions(+), 78 deletions(-) diff --git a/lib/schema/array.js b/lib/schema/array.js index 10f9439eca2..2d5266ffac5 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -80,27 +80,17 @@ function SchemaArray(key, cast, options, schemaOptions) { : utils.getFunctionName(cast); const Types = require('./index.js'); - const caster = Types.hasOwnProperty(name) ? Types[name] : cast; - - this.casterConstructor = caster; - - if (this.casterConstructor instanceof SchemaArray) { - this.casterConstructor[isNestedArraySymbol] = true; - } - - if (typeof caster === 'function' && - !caster.$isArraySubdocument && - !caster.$isSchemaMap) { - const path = this.caster instanceof EmbeddedDoc ? null : key; - this.caster = new caster(path, castOptions); - } else { - this.caster = caster; - if (!(this.caster instanceof EmbeddedDoc)) { - this.caster.path = key; + const schemaTypeDefinition = Types.hasOwnProperty(name) ? Types[name] : cast; + + if (typeof schemaTypeDefinition === 'function') { + this.embeddedSchemaType = new schemaTypeDefinition(key, castOptions); + } else if (schemaTypeDefinition instanceof SchemaType) { + this.embeddedSchemaType = schemaTypeDefinition; + if (!(this.embeddedSchemaType instanceof EmbeddedDoc)) { + this.embeddedSchemaType.path = key; } } - this.embeddedSchemaType = this.caster; } this.$isMongooseArray = true; @@ -501,30 +491,21 @@ SchemaArray.prototype.clone = function() { }; SchemaArray.prototype._castForQuery = function(val, context) { - let Constructor = this.casterConstructor; + let embeddedSchemaType = this.embeddedSchemaType; if (val && - Constructor.discriminators && - Constructor.schema && - Constructor.schema.options && - Constructor.schema.options.discriminatorKey) { - if (typeof val[Constructor.schema.options.discriminatorKey] === 'string' && - Constructor.discriminators[val[Constructor.schema.options.discriminatorKey]]) { - Constructor = Constructor.discriminators[val[Constructor.schema.options.discriminatorKey]]; + embeddedSchemaType?.discriminators && + typeof embeddedSchemaType?.schema?.options?.discriminatorKey === 'string') { + if (embeddedSchemaType.discriminators[val[embeddedSchemaType.schema.options.discriminatorKey]]) { + embeddedSchemaType = embeddedSchemaType.discriminators[val[embeddedSchemaType.schema.options.discriminatorKey]]; } else { - const constructorByValue = getDiscriminatorByValue(Constructor.discriminators, val[Constructor.schema.options.discriminatorKey]); + const constructorByValue = getDiscriminatorByValue(embeddedSchemaType.discriminators, val[embeddedSchemaType.schema.options.discriminatorKey]); if (constructorByValue) { - Constructor = constructorByValue; + embeddedSchemaType = constructorByValue; } } } - const proto = this.casterConstructor.prototype; - const protoCastForQuery = proto && proto.castForQuery; - const protoCast = proto && proto.cast; - const constructorCastForQuery = Constructor.castForQuery; - const caster = this.embeddedSchemaType; - if (Array.isArray(val)) { this.setters.reverse().forEach(setter => { val = setter.call(this, val, this); @@ -533,30 +514,10 @@ SchemaArray.prototype._castForQuery = function(val, context) { if (utils.isObject(v) && v.$elemMatch) { return v; } - if (protoCastForQuery) { - v = protoCastForQuery.call(caster, null, v, context); - return v; - } else if (protoCast) { - v = protoCast.call(caster, v); - return v; - } else if (constructorCastForQuery) { - v = constructorCastForQuery.call(caster, null, v, context); - return v; - } - if (v != null) { - v = new Constructor(v); - return v; - } - return v; + return embeddedSchemaType.castForQuery(null, v, context); }); - } else if (protoCastForQuery) { - val = protoCastForQuery.call(caster, null, val, context); - } else if (protoCast) { - val = protoCast.call(caster, val); - } else if (constructorCastForQuery) { - val = constructorCastForQuery.call(caster, null, val, context); - } else if (val != null) { - val = new Constructor(val); + } else { + val = embeddedSchemaType.castForQuery(null, val, context); } return val; @@ -622,12 +583,12 @@ function cast$all(val, context) { return v; } if (v.$elemMatch != null) { - return { $elemMatch: cast(this.casterConstructor.schema, v.$elemMatch, null, this && this.$$context) }; + return { $elemMatch: cast(this.embeddedSchemaType.schema, v.$elemMatch, null, this && this.$$context) }; } const o = {}; o[this.path] = v; - return cast(this.casterConstructor.schema, o, null, this && this.$$context)[this.path]; + return cast(this.embeddedSchemaType.schema, o, null, this && this.$$context)[this.path]; }, this); return this.castForQuery(null, val, context); @@ -665,7 +626,7 @@ function createLogicalQueryOperatorHandler(op) { const ret = []; for (const obj of val) { - ret.push(cast(this.casterConstructor.schema ?? context.schema, obj, null, this && this.$$context)); + ret.push(cast(this.embeddedSchemaType.schema ?? context.schema, obj, null, this && this.$$context)); } return ret; diff --git a/lib/schema/documentArray.js b/lib/schema/documentArray.js index d2e193bc20f..42b86bef7e7 100644 --- a/lib/schema/documentArray.js +++ b/lib/schema/documentArray.js @@ -56,17 +56,25 @@ function SchemaDocumentArray(key, schema, options, schemaOptions) { schema = handleIdOption(schema, options); } - const EmbeddedDocument = _createConstructor(schema, options); - EmbeddedDocument.prototype.$basePath = key; + const Constructor = _createConstructor(schema, options); + Constructor.prototype.$basePath = key; + Constructor.path = key; - SchemaArray.call(this, key, EmbeddedDocument, options); + const $parentSchemaType = this; + const embeddedSchemaType = new DocumentArrayElement(key + '.$', schema, { + required: schemaOptions?.required ?? false, + $parentSchemaType, + Constructor + }); + + SchemaArray.call(this, key, embeddedSchemaType, options); this.schema = schema; this.schemaOptions = schemaOptions || {}; this.$isMongooseDocumentArray = true; - this.Constructor = EmbeddedDocument; + this.Constructor = Constructor; - EmbeddedDocument.base = schema.base; + Constructor.base = schema.base; const fn = this.defaultValue; @@ -80,15 +88,6 @@ function SchemaDocumentArray(key, schema, options, schemaOptions) { return arr; }); } - - const $parentSchemaType = this; - this.embeddedSchemaType = new DocumentArrayElement(key + '.$', this.schema, { - required: this && - this.schemaOptions && - this.schemaOptions.required || false, - $parentSchemaType, - Constructor: this.Constructor - }); } /** diff --git a/types/schematypes.d.ts b/types/schematypes.d.ts index b22b75c7b51..301bcaedfea 100644 --- a/types/schematypes.d.ts +++ b/types/schematypes.d.ts @@ -464,9 +464,6 @@ declare module 'mongoose' { /** The constructor used for subdocuments in this array */ Constructor: typeof Types.Subdocument; - /** The constructor used for subdocuments in this array */ - caster?: typeof Types.Subdocument; - /** Default options for this SchemaType */ defaultOptions: Record; } @@ -530,7 +527,7 @@ declare module 'mongoose' { /** The document's schema */ schema: Schema; - /** The constructor used for subdocuments in this array */ + /** The constructor used to create subdocuments based on this schematype */ Constructor: typeof Types.Subdocument; /** Default options for this SchemaType */ From 19abcb62d5799b637dde05e3af0522e67d5dd366 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 3 Jul 2025 11:32:02 -0400 Subject: [PATCH 093/209] docs: add #15179 to docs --- docs/migrating_to_9.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 211efac6b71..09451f6c146 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -239,6 +239,26 @@ await test.save(); test.uuid; // string ``` +### SchemaType caster and casterConstructor properties were removed + +In Mongoose 8, certain schema type instances had a `caster` property which contained either the embedded schema type or embedded subdocument constructor. +In Mongoose 9, to make types and internal logic more consistent, we removed the `caster` property in favor of `embeddedSchemaType` and `Constructor`. + +```javascript +const schema = new mongoose.Schema({ docArray: [new mongoose.Schema({ name: String })], arr: [String] }); + +// In Mongoose 8: +console.log(schema.path('arr').caster); // String SchemaType +console.log(schema.path('docArray').caster); // EmbeddedDocument constructor + +// In Mongoose 9: +console.log(schema.path('arr').embeddedSchemaType); // SchemaString +console.log(schema.path('docArray').embeddedSchemaType); // SchemaDocumentArrayElement + +console.log(schema.path('arr').Constructor); // undefined +console.log(schema.path('docArray').Constructor); // EmbeddedDocument constructor +``` + ## TypeScript ### FilterQuery Properties No Longer Resolve to any From 671f509a6d22c1f957bc378a4a8c0c26a0d24918 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 3 Jul 2025 11:38:05 -0400 Subject: [PATCH 094/209] use tostring to fix encryption tests --- test/encryption/encryption.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/encryption/encryption.test.js b/test/encryption/encryption.test.js index 79873f404a9..bf590e5587f 100644 --- a/test/encryption/encryption.test.js +++ b/test/encryption/encryption.test.js @@ -154,7 +154,7 @@ describe('encryption integration tests', () => { // mongoose's Buffer does not support deep equality - instead use the Buffer.equals method. assert.ok(doc.field.equals(input)); } else { - assert.deepEqual(doc.field, expected ?? input); + assert.deepEqual(doc.field.toString(), expected ?? input); } } From fac22edfe0bfceb2d7653874e8e268f59cf66c19 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 3 Jul 2025 11:43:37 -0400 Subject: [PATCH 095/209] use uuid.equals() --- test/encryption/encryption.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/encryption/encryption.test.js b/test/encryption/encryption.test.js index bf590e5587f..8d6b77b4c8e 100644 --- a/test/encryption/encryption.test.js +++ b/test/encryption/encryption.test.js @@ -150,11 +150,11 @@ describe('encryption integration tests', () => { isEncryptedValue(encryptedDoc, 'field'); const doc = await model.findOne({ _id }); - if (Buffer.isBuffer(input)) { + if (Buffer.isBuffer(input) || input instanceof UUID) { // mongoose's Buffer does not support deep equality - instead use the Buffer.equals method. assert.ok(doc.field.equals(input)); } else { - assert.deepEqual(doc.field.toString(), expected ?? input); + assert.deepEqual(doc.field, expected ?? input); } } From 0d0d06aa208c3d5cf3a0faf71d71e7d24384873b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 3 Jul 2025 12:14:52 -0400 Subject: [PATCH 096/209] BREAKING CHANGE: remove background option from indexes Fix #15476 --- docs/migrating_to_9.md | 4 ++ lib/helpers/indexes/isIndexEqual.js | 1 - lib/helpers/schema/getIndexes.js | 6 --- lib/model.js | 8 +--- lib/schema.js | 4 +- lib/schemaType.js | 7 --- package.json | 6 --- .../helpers/indexes.getRelatedIndexes.test.js | 46 +++++++------------ test/helpers/indexes.isIndexEqual.test.js | 3 -- test/model.discriminator.test.js | 4 +- test/model.test.js | 21 ++------- test/schema.test.js | 18 ++++---- test/timestamps.test.js | 2 +- test/types/connection.test.ts | 1 - 14 files changed, 39 insertions(+), 92 deletions(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 211efac6b71..2ec3bde7d7f 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -68,6 +68,10 @@ schema.pre('save', function(next, arg) { In Mongoose 9, `next(null, 'new arg')` doesn't overwrite the args to the next middleware. +## Removed background option for indexes + +[MongoDB no longer supports the `background` option for indexes as of MongoDB 4.2](https://www.mongodb.com/docs/manual/core/index-creation/#index-operations). Mongoose 9 will no longer set the background option by default. + ## Subdocument `deleteOne()` hooks execute only when subdocument is deleted Currently, calling `deleteOne()` on a subdocument will execute the `deleteOne()` hooks on the subdocument regardless of whether the subdocument is actually deleted. diff --git a/lib/helpers/indexes/isIndexEqual.js b/lib/helpers/indexes/isIndexEqual.js index 73504123600..414463d2c5c 100644 --- a/lib/helpers/indexes/isIndexEqual.js +++ b/lib/helpers/indexes/isIndexEqual.js @@ -20,7 +20,6 @@ module.exports = function isIndexEqual(schemaIndexKeysObject, options, dbIndex) // key: { _fts: 'text', _ftsx: 1 }, // name: 'name_text', // ns: 'test.tests', - // background: true, // weights: { name: 1 }, // default_language: 'english', // language_override: 'language', diff --git a/lib/helpers/schema/getIndexes.js b/lib/helpers/schema/getIndexes.js index 706439d321d..424fc014bac 100644 --- a/lib/helpers/schema/getIndexes.js +++ b/lib/helpers/schema/getIndexes.js @@ -96,9 +96,6 @@ module.exports = function getIndexes(schema) { } delete options.type; - if (!('background' in options)) { - options.background = true; - } if (schema.options.autoIndex != null) { options._autoIndex = schema.options.autoIndex; } @@ -126,9 +123,6 @@ module.exports = function getIndexes(schema) { } else { schema._indexes.forEach(function(index) { const options = index[1]; - if (!('background' in options)) { - options.background = true; - } decorateDiscriminatorIndexOptions(schema, options); }); indexes = indexes.concat(schema._indexes); diff --git a/lib/model.js b/lib/model.js index dc654346919..41620f6acba 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1229,7 +1229,6 @@ Model.createCollection = async function createCollection(options) { * toCreate; // Array of strings containing names of indexes that `syncIndexes()` will create * * @param {Object} [options] options to pass to `ensureIndexes()` - * @param {Boolean} [options.background=null] if specified, overrides each index's `background` property * @param {Boolean} [options.hideIndexes=false] set to `true` to hide indexes instead of dropping. Requires MongoDB server 4.4 or higher * @return {Promise} * @api public @@ -1627,8 +1626,7 @@ function _ensureIndexes(model, options, callback) { }); return; } - // Indexes are created one-by-one to support how MongoDB < 2.4 deals - // with background indexes. + // Indexes are created one-by-one const indexSingleDone = function(err, fields, options, name) { model.emit('index-single-done', err, fields, options, name); @@ -1680,10 +1678,6 @@ function _ensureIndexes(model, options, callback) { indexSingleStart(indexFields, options); - if ('background' in options) { - indexOptions.background = options.background; - } - // Just in case `createIndex()` throws a sync error let promise = null; try { diff --git a/lib/schema.js b/lib/schema.js index 4a47946da81..f7aaef0d657 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -2519,8 +2519,8 @@ Object.defineProperty(Schema, 'indexTypes', { * registeredAt: { type: Date, index: true } * }); * - * // [ [ { email: 1 }, { unique: true, background: true } ], - * // [ { registeredAt: 1 }, { background: true } ] ] + * // [ [ { email: 1 }, { unique: true } ], + * // [ { registeredAt: 1 }, {} ] ] * userSchema.indexes(); * * [Plugins](https://mongoosejs.com/docs/plugins.html) can use the return value of this function to modify a schema's indexes. diff --git a/lib/schemaType.js b/lib/schemaType.js index 6ea7c8b0676..b528d285409 100644 --- a/lib/schemaType.js +++ b/lib/schemaType.js @@ -417,13 +417,6 @@ SchemaType.prototype.default = function(val) { * s.path('my.date').index({ expires: 60 }); * s.path('my.path').index({ unique: true, sparse: true }); * - * #### Note: - * - * _Indexes are created [in the background](https://www.mongodb.com/docs/manual/core/index-creation/#index-creation-background) - * by default. If `background` is set to `false`, MongoDB will not execute any - * read/write operations you send until the index build. - * Specify `background: false` to override Mongoose's default._ - * * @param {Object|Boolean|String|Number} options * @return {SchemaType} this * @api public diff --git a/package.json b/package.json index 51e961fddd4..ef053887e5a 100644 --- a/package.json +++ b/package.json @@ -55,13 +55,7 @@ "ncp": "^2.0.0", "nyc": "15.1.0", "pug": "3.0.3", -<<<<<<< HEAD - "sinon": "20.0.0", -======= - "q": "1.5.1", "sinon": "21.0.0", - "stream-browserify": "3.0.0", ->>>>>>> master "tsd": "0.32.0", "typescript": "5.8.3", "typescript-eslint": "^8.31.1", diff --git a/test/helpers/indexes.getRelatedIndexes.test.js b/test/helpers/indexes.getRelatedIndexes.test.js index de71b9e324a..b18e887d189 100644 --- a/test/helpers/indexes.getRelatedIndexes.test.js +++ b/test/helpers/indexes.getRelatedIndexes.test.js @@ -33,10 +33,10 @@ describe('getRelatedIndexes', () => { assert.deepStrictEqual( filteredSchemaIndexes, [ - [{ actorId: 1 }, { background: true, unique: true }], + [{ actorId: 1 }, { unique: true }], [ { happenedAt: 1 }, - { background: true, partialFilterExpression: { __t: 'EventButNoDiscriminator' } } + { partialFilterExpression: { __t: 'EventButNoDiscriminator' } } ] ] ); @@ -88,7 +88,7 @@ describe('getRelatedIndexes', () => { assert.deepStrictEqual( filteredSchemaIndexes, [ - [{ actorId: 1 }, { background: true, unique: true }] + [{ actorId: 1 }, { unique: true }] ] ); }); @@ -124,8 +124,7 @@ describe('getRelatedIndexes', () => { filteredSchemaIndexes, [ [{ actorId: 1 }, - { background: true, - unique: true, + { unique: true, partialFilterExpression: { __t: { $exists: true } } } ] @@ -182,7 +181,6 @@ describe('getRelatedIndexes', () => { [ { boughtAt: 1 }, { - background: true, unique: true, partialFilterExpression: { __t: 'BuyEvent', @@ -207,8 +205,7 @@ describe('getRelatedIndexes', () => { unique: true, key: { actorId: 1 }, name: 'actorId_1', - ns: 'mongoose_test.some_collection', - background: true + ns: 'mongoose_test.some_collection' }, { v: 2, @@ -216,8 +213,7 @@ describe('getRelatedIndexes', () => { key: { doesNotMatter: 1 }, name: 'doesNotMatter_1', ns: 'mongoose_test.some_collection', - partialFilterExpression: { __t: 'EventButNoDiscriminator' }, - background: true + partialFilterExpression: { __t: 'EventButNoDiscriminator' } } ]; @@ -234,8 +230,7 @@ describe('getRelatedIndexes', () => { unique: true, key: { actorId: 1 }, name: 'actorId_1', - ns: 'mongoose_test.some_collection', - background: true + ns: 'mongoose_test.some_collection' }, { v: 2, @@ -243,8 +238,7 @@ describe('getRelatedIndexes', () => { key: { doesNotMatter: 1 }, name: 'doesNotMatter_1', ns: 'mongoose_test.some_collection', - partialFilterExpression: { __t: 'EventButNoDiscriminator' }, - background: true + partialFilterExpression: { __t: 'EventButNoDiscriminator' } } ] ); @@ -296,24 +290,21 @@ describe('getRelatedIndexes', () => { unique: true, key: { actorId: 1 }, name: 'actorId_1', - ns: 'mongoose_test.some_collection', - background: true + ns: 'mongoose_test.some_collection' }, { unique: true, key: { boughtAt: 1 }, name: 'boughtAt_1', ns: 'mongoose_test.some_collection', - partialFilterExpression: { __t: 'BuyEvent' }, - background: true + partialFilterExpression: { __t: 'BuyEvent' } }, { unique: true, key: { clickedAt: 1 }, name: 'clickedAt_1', ns: 'mongoose_test.some_collection', - partialFilterExpression: { __t: 'ClickEvent' }, - background: true + partialFilterExpression: { __t: 'ClickEvent' } } ]; @@ -330,8 +321,7 @@ describe('getRelatedIndexes', () => { unique: true, key: { actorId: 1 }, name: 'actorId_1', - ns: 'mongoose_test.some_collection', - background: true + ns: 'mongoose_test.some_collection' } ] ); @@ -383,24 +373,21 @@ describe('getRelatedIndexes', () => { unique: true, key: { actorId: 1 }, name: 'actorId_1', - ns: 'mongoose_test.some_collection', - background: true + ns: 'mongoose_test.some_collection' }, { unique: true, key: { boughtAt: 1 }, name: 'boughtAt_1', ns: 'mongoose_test.some_collection', - partialFilterExpression: { __t: 'BuyEvent' }, - background: true + partialFilterExpression: { __t: 'BuyEvent' } }, { unique: true, key: { clickedAt: 1 }, name: 'clickedAt_1', ns: 'mongoose_test.some_collection', - partialFilterExpression: { __t: 'ClickEvent' }, - background: true + partialFilterExpression: { __t: 'ClickEvent' } } ]; @@ -416,8 +403,7 @@ describe('getRelatedIndexes', () => { key: { boughtAt: 1 }, name: 'boughtAt_1', ns: 'mongoose_test.some_collection', - partialFilterExpression: { __t: 'BuyEvent' }, - background: true + partialFilterExpression: { __t: 'BuyEvent' } } ] ); diff --git a/test/helpers/indexes.isIndexEqual.test.js b/test/helpers/indexes.isIndexEqual.test.js index ee4d343b013..17624a76ed6 100644 --- a/test/helpers/indexes.isIndexEqual.test.js +++ b/test/helpers/indexes.isIndexEqual.test.js @@ -19,7 +19,6 @@ describe('isIndexEqual', function() { unique: true, key: { username: 1 }, name: 'username_1', - background: true, collation: { locale: 'en', caseLevel: false, @@ -43,7 +42,6 @@ describe('isIndexEqual', function() { unique: true, key: { username: 1 }, name: 'username_1', - background: true, collation: { locale: 'en', caseLevel: false, @@ -65,7 +63,6 @@ describe('isIndexEqual', function() { key: { _fts: 'text', _ftsx: 1 }, name: 'name_text', ns: 'test.tests', - background: true, weights: { name: 1 }, default_language: 'english', language_override: 'language', diff --git a/test/model.discriminator.test.js b/test/model.discriminator.test.js index 25e289726eb..45ac0d7823b 100644 --- a/test/model.discriminator.test.js +++ b/test/model.discriminator.test.js @@ -337,10 +337,10 @@ describe('model', function() { }); it('does not inherit indexes', function() { - assert.deepEqual(Person.schema.indexes(), [[{ name: 1 }, { background: true }]]); + assert.deepEqual(Person.schema.indexes(), [[{ name: 1 }, {}]]); assert.deepEqual( Employee.schema.indexes(), - [[{ department: 1 }, { background: true, partialFilterExpression: { __t: 'Employee' } }]] + [[{ department: 1 }, { partialFilterExpression: { __t: 'Employee' } }]] ); }); diff --git a/test/model.test.js b/test/model.test.js index 70d257d87c0..30caa428e86 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -5118,23 +5118,10 @@ describe('Model', function() { ); }); - it('syncIndexes() allows overwriting `background` option (gh-8645)', async function() { - const opts = { autoIndex: false }; - const schema = new Schema({ name: String }, opts); - schema.index({ name: 1 }, { background: true }); - - const M = db.model('Test', schema); - await M.syncIndexes({ background: false }); - - const indexes = await M.listIndexes(); - assert.deepEqual(indexes[1].key, { name: 1 }); - assert.strictEqual(indexes[1].background, false); - }); - it('syncIndexes() does not call createIndex for indexes that already exist', async function() { const opts = { autoIndex: false }; const schema = new Schema({ name: String }, opts); - schema.index({ name: 1 }, { background: true }); + schema.index({ name: 1 }); const M = db.model('Test', schema); await M.syncIndexes(); @@ -5253,9 +5240,9 @@ describe('Model', function() { const BuyEvent = Event.discriminator('BuyEvent', buyEventSchema); // Act - const droppedByEvent = await Event.syncIndexes({ background: false }); - const droppedByClickEvent = await ClickEvent.syncIndexes({ background: false }); - const droppedByBuyEvent = await BuyEvent.syncIndexes({ background: false }); + const droppedByEvent = await Event.syncIndexes(); + const droppedByClickEvent = await ClickEvent.syncIndexes(); + const droppedByBuyEvent = await BuyEvent.syncIndexes(); const eventIndexes = await Event.listIndexes(); diff --git a/test/schema.test.js b/test/schema.test.js index 00eecb46ba0..cfde13323e3 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -829,18 +829,18 @@ describe('schema', function() { const Tobi = new Schema({ name: { type: String, index: true }, last: { type: Number, sparse: true }, - nope: { type: String, index: { background: false } } + nope: { type: String, index: true } }); Tobi.index({ firstname: 1, last: 1 }, { unique: true, expires: '1h' }); - Tobi.index({ firstname: 1, nope: 1 }, { unique: true, background: false }); + Tobi.index({ firstname: 1, nope: 1 }, { unique: true }); assert.deepEqual(Tobi.indexes(), [ - [{ name: 1 }, { background: true }], - [{ last: 1 }, { sparse: true, background: true }], - [{ nope: 1 }, { background: false }], - [{ firstname: 1, last: 1 }, { unique: true, expireAfterSeconds: 60 * 60, background: true }], - [{ firstname: 1, nope: 1 }, { unique: true, background: false }] + [{ name: 1 }, {}], + [{ last: 1 }, { sparse: true }], + [{ nope: 1 }, {}], + [{ firstname: 1, last: 1 }, { unique: true, expireAfterSeconds: 60 * 60 }], + [{ firstname: 1, nope: 1 }, { unique: true }] ]); @@ -889,7 +889,7 @@ describe('schema', function() { }); assert.deepEqual(schema.indexes(), [ - [{ point: '2dsphere' }, { background: true }] + [{ point: '2dsphere' }, {}] ]); }); @@ -2505,7 +2505,7 @@ describe('schema', function() { const TurboManSchema = Schema(); TurboManSchema.add(ToySchema); - assert.deepStrictEqual(TurboManSchema.indexes(), [[{ name: 1 }, { background: true }]]); + assert.deepStrictEqual(TurboManSchema.indexes(), [[{ name: 1 }, {}]]); }); describe('gh-8849', function() { diff --git a/test/timestamps.test.js b/test/timestamps.test.js index 49ab3e82762..bcb4a482836 100644 --- a/test/timestamps.test.js +++ b/test/timestamps.test.js @@ -131,7 +131,7 @@ describe('timestamps', function() { const indexes = testSchema.indexes(); assert.deepEqual(indexes, [ - [{ updatedAt: 1 }, { background: true, expireAfterSeconds: 7200 }] + [{ updatedAt: 1 }, { expireAfterSeconds: 7200 }] ]); }); }); diff --git a/test/types/connection.test.ts b/test/types/connection.test.ts index 77ca1787685..1e9b792d131 100644 --- a/test/types/connection.test.ts +++ b/test/types/connection.test.ts @@ -72,7 +72,6 @@ expectType>(conn.startSession({ causalConsistency expectType>(conn.syncIndexes()); expectType>(conn.syncIndexes({ continueOnError: true })); -expectType>(conn.syncIndexes({ background: true })); expectType(conn.useDb('test')); expectType(conn.useDb('test', {})); From 5734c009c1a2021990c2b0132ce1e54624531a54 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 3 Jul 2025 12:20:51 -0400 Subject: [PATCH 097/209] update compatibility to show Mongoose 9 supporting MongoDB 6 or greater --- docs/compatibility.md | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/docs/compatibility.md b/docs/compatibility.md index ffe6a031778..f3b95148215 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -18,20 +18,13 @@ Below are the [semver](http://semver.org/) ranges representing which versions of | MongoDB Server | Mongoose | | :------------: | :--------------------------------------------: | -| `8.x` | `^8.7.0` | -| `7.x` | `^7.4.0 \| ^8.0.0` | -| `6.x` | `^6.5.0 \| ^7.0.0 \| ^8.0.0` | -| `5.x` | `^5.13.0` \| `^6.0.0 \| ^7.0.0 \| ^8.0.0` | -| `4.4.x` | `^5.10.0 \| ^6.0.0 \| ^7.0.0 \| ^8.0.0` | -| `4.2.x` | `^5.7.0 \| ^6.0.0 \| ^7.0.0 \| ^8.0.0` | -| `4.0.x` | `^5.2.0 \| ^6.0.0 \| ^7.0.0 \| ^8.0.0 <8.16.0` | -| `3.6.x` | `^5.0.0 \| ^6.0.0 \| ^7.0.0 \| ^8.0.0 <8.8.0` | -| `3.4.x` | `^4.7.3 \| ^5.0.0` | -| `3.2.x` | `^4.3.0 \| ^5.0.0` | -| `3.0.x` | `^3.8.22 \| ^4.0.0 \| ^5.0.0` | -| `2.6.x` | `^3.8.8 \| ^4.0.0 \| ^5.0.0` | -| `2.4.x` | `^3.8.0 \| ^4.0.0` | +| `8.x` | `^8.7.0 | ^9.0.0` | +| `7.x` | `^7.4.0 \| ^8.0.0 \| ^9.0.0` | +| `6.x` | `^7.0.0 \| ^8.0.0 \| ^9.0.0` | +| `5.x` | `^6.0.0 \| ^7.0.0 \| ^8.0.0` | +| `4.4.x` | `^6.0.0 \| ^7.0.0 \| ^8.0.0` | +| `4.2.x` | `^6.0.0 \| ^7.0.0 \| ^8.0.0` | +| `4.0.x` | `^6.0.0 \| ^7.0.0 \| ^8.0.0 <8.16.0` | +| `3.6.x` | `^6.0.0 \| ^7.0.0 \| ^8.0.0 <8.8.0` | Mongoose `^6.5.0` also works with MongoDB server 7.x. But not all new MongoDB server 7.x features are supported by Mongoose 6.x. - -Note that Mongoose `5.x` dropped support for all versions of MongoDB before `3.0.0`. If you need to use MongoDB `2.6` or older, use Mongoose `4.x`. From a5100ec18417792511fb099a75f3f0eb9409c9c7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 3 Jul 2025 14:22:13 -0400 Subject: [PATCH 098/209] fix merge conflict --- package.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/package.json b/package.json index 51e961fddd4..ef053887e5a 100644 --- a/package.json +++ b/package.json @@ -55,13 +55,7 @@ "ncp": "^2.0.0", "nyc": "15.1.0", "pug": "3.0.3", -<<<<<<< HEAD - "sinon": "20.0.0", -======= - "q": "1.5.1", "sinon": "21.0.0", - "stream-browserify": "3.0.0", ->>>>>>> master "tsd": "0.32.0", "typescript": "5.8.3", "typescript-eslint": "^8.31.1", From 024e5a7ce062f69b64f170b53315854f1b7197b2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 3 Jul 2025 14:35:43 -0400 Subject: [PATCH 099/209] fix tests --- test/types/schema.create.test.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index 607bce5ca38..e101bb8fad2 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -1256,10 +1256,10 @@ async function gh13797() { name: string; } new Schema({ name: { type: String, required: function() { - expectType(this); return true; + expectAssignable(this); return true; } } }); new Schema({ name: { type: String, default: function() { - expectType(this); return ''; + expectAssignable(this); return ''; } } }); } @@ -1555,7 +1555,10 @@ function gh14696() { const x: ValidateOpts = { validator(v: any) { - expectAssignable(this); + expectAssignable>(this); + if (this instanceof Query) { + return !v; + } return !v || this.name === 'super admin'; } }; @@ -1570,7 +1573,10 @@ function gh14696() { default: false, validate: { validator(v: any) { - expectAssignable(this); + expectAssignable>(this); + if (this instanceof Query) { + return !v; + } return !v || this.name === 'super admin'; } } @@ -1580,7 +1586,10 @@ function gh14696() { default: false, validate: { async validator(v: any) { - expectAssignable(this); + expectAssignable>(this); + if (this instanceof Query) { + return !v; + } return !v || this.name === 'super admin'; } } From b02e3e7181618265cd908b11fa1c9e5cb059d6a8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 3 Jul 2025 14:39:47 -0400 Subject: [PATCH 100/209] address comments --- lib/schema.js | 2 ++ types/index.d.ts | 1 + types/inferhydrateddoctype.d.ts | 7 +++++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index 04e88907f43..ea7bbd4a800 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -384,6 +384,8 @@ Schema.prototype.tree; * // Equivalent: * const schema2 = new Schema({ name: String }, { toObject: { virtuals: true } }); * + * @param {Object} definition + * @param {Object} [options] * @return {Schema} the new schema * @api public * @memberOf Schema diff --git a/types/index.d.ts b/types/index.d.ts index f4d048850bf..942071a7591 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -283,6 +283,7 @@ declare module 'mongoose' { */ constructor(definition?: SchemaDefinition, RawDocType, THydratedDocumentType> | DocType, options?: SchemaOptions, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals, THydratedDocumentType> | ResolveSchemaOptions); + /* Creates a new schema with the given definition and options. Equivalent to `new Schema(definition, options)`, but with better automatic type inference. */ static create< TSchemaDefinition extends SchemaDefinition, TSchemaOptions extends DefaultSchemaOptions, diff --git a/types/inferhydrateddoctype.d.ts b/types/inferhydrateddoctype.d.ts index 280c2b15903..9382033aeb1 100644 --- a/types/inferhydrateddoctype.d.ts +++ b/types/inferhydrateddoctype.d.ts @@ -9,6 +9,9 @@ import { import { UUID } from 'mongodb'; declare module 'mongoose' { + /** + * Given a schema definition, returns the hydrated document type from the schema definition. + */ export type InferHydratedDocType< DocDefinition, TSchemaOptions extends Record = DefaultSchemaOptions @@ -60,8 +63,8 @@ declare module 'mongoose' { * @summary Resolve path type by returning the corresponding type. * @param {PathValueType} PathValueType Document definition path type. * @param {Options} Options Document definition path options except path type. - * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". - * @returns Number, "Number" or "number" will be resolved to number type. + * @param {TypeKey} TypeKey A generic of literal string type. Refers to the property used for path type definition. + * @returns Type */ type ResolveHydratedPathType = {}, TypeKey extends string = DefaultSchemaOptions['typeKey'], TypeHint = never> = IfEquals Date: Thu, 3 Jul 2025 14:41:43 -0400 Subject: [PATCH 101/209] address comments --- test/types/schema.create.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index e101bb8fad2..1b8177f6d59 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -1792,7 +1792,7 @@ function gh15301() { interface IUser { time: { hours: number, minutes: number } } - const userSchema = new Schema({ + const userSchema = Schema.create({ time: { type: Schema.create( { @@ -1812,7 +1812,7 @@ function gh15301() { }; userSchema.pre('init', function(rawDoc) { - expectType(rawDoc); + expectAssignable(rawDoc); if (typeof rawDoc.time === 'string') { rawDoc.time = timeStringToObject(rawDoc.time); } @@ -1836,7 +1836,7 @@ function gh15412() { } function defaultReturnsUndefined() { - const schema = new Schema<{ arr: number[] }>({ + const schema = Schema.create({ arr: { type: [Number], default: () => void 0 From f2ed2f0bf861b30d4293b5f4b96c9c4d36d0eeb5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 3 Jul 2025 14:50:20 -0400 Subject: [PATCH 102/209] types: correct inference for timestamps with methods Fix #12807 --- test/types/schema.create.test.ts | 6 +----- test/types/schema.test.ts | 6 +----- types/inferschematype.d.ts | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index 1b8177f6d59..3c827db6ca1 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -911,11 +911,7 @@ function testInferTimestamps() { }); type WithTimestamps2 = InferSchemaType; - // For some reason, expectType<{ createdAt: Date, updatedAt: Date, name?: string }> throws - // an error "Parameter type { createdAt: Date; updatedAt: Date; name?: string | undefined; } - // is not identical to argument type { createdAt: NativeDate; updatedAt: NativeDate; } & - // { name?: string | undefined; }" - expectType<{ name?: string | null } & { _id: Types.ObjectId }>({} as WithTimestamps2); + expectType<{ createdAt: Date, updatedAt: Date } & { name?: string | null } & { _id: Types.ObjectId }>({} as WithTimestamps2); } function gh12431() { diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index bb821921691..8695e7162b4 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -899,11 +899,7 @@ function testInferTimestamps() { }); type WithTimestamps2 = InferSchemaType; - // For some reason, expectType<{ createdAt: Date, updatedAt: Date, name?: string }> throws - // an error "Parameter type { createdAt: Date; updatedAt: Date; name?: string | undefined; } - // is not identical to argument type { createdAt: NativeDate; updatedAt: NativeDate; } & - // { name?: string | undefined; }" - expectType<{ name?: string | null }>({} as WithTimestamps2); + expectType<{ createdAt: Date, updatedAt: Date } & { name?: string | null }>({} as WithTimestamps2); } function gh12431() { diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 59563af6315..2b01fd40c9e 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -76,7 +76,7 @@ declare module 'mongoose' { type ApplySchemaOptions = ResolveTimestamps; - type ResolveTimestamps = O extends { methods: any } | { statics: any } | { virtuals: any } | { timestamps?: false } ? T + type ResolveTimestamps = O extends { timestamps: false } ? T // For some reason, TypeScript sets all the document properties to unknown // if we use methods, statics, or virtuals. So avoid inferring timestamps // if any of these are set for now. See gh-12807 From fe97fbeb480c0a10f47fe7e01a7ed8db835d8589 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 5 Jul 2025 12:51:02 -0400 Subject: [PATCH 103/209] Update migrating_to_9.md --- docs/migrating_to_9.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 2ec3bde7d7f..dbdfdc8be72 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -70,7 +70,7 @@ In Mongoose 9, `next(null, 'new arg')` doesn't overwrite the args to the next mi ## Removed background option for indexes -[MongoDB no longer supports the `background` option for indexes as of MongoDB 4.2](https://www.mongodb.com/docs/manual/core/index-creation/#index-operations). Mongoose 9 will no longer set the background option by default. +[MongoDB no longer supports the `background` option for indexes as of MongoDB 4.2](https://www.mongodb.com/docs/manual/core/index-creation/#index-operations). Mongoose 9 will no longer set the background option by default and Mongoose 9 no longer supports setting the `background` option on `Schema.prototype.index()`. ## Subdocument `deleteOne()` hooks execute only when subdocument is deleted From aa18053424ace92ab6a5bb2a4ccc45b29475baa8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 5 Jul 2025 12:54:49 -0400 Subject: [PATCH 104/209] Update docs/migrating_to_9.md Co-authored-by: hasezoey --- docs/migrating_to_9.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 09451f6c146..052eb52dce0 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -239,7 +239,7 @@ await test.save(); test.uuid; // string ``` -### SchemaType caster and casterConstructor properties were removed +### SchemaType `caster` and `casterConstructor` properties were removed In Mongoose 8, certain schema type instances had a `caster` property which contained either the embedded schema type or embedded subdocument constructor. In Mongoose 9, to make types and internal logic more consistent, we removed the `caster` property in favor of `embeddedSchemaType` and `Constructor`. From 00426d45ece9ca913d848b7e9e04916efb1564a7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 5 Jul 2025 12:55:57 -0400 Subject: [PATCH 105/209] Update docs/migrating_to_9.md Co-authored-by: hasezoey --- docs/migrating_to_9.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 052eb52dce0..63ca209d1a4 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -248,7 +248,7 @@ In Mongoose 9, to make types and internal logic more consistent, we removed the const schema = new mongoose.Schema({ docArray: [new mongoose.Schema({ name: String })], arr: [String] }); // In Mongoose 8: -console.log(schema.path('arr').caster); // String SchemaType +console.log(schema.path('arr').caster); // SchemaString console.log(schema.path('docArray').caster); // EmbeddedDocument constructor // In Mongoose 9: From 52c75735a20853d1e65cc2f01cbf70e55b76b923 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 5 Jul 2025 14:36:29 -0400 Subject: [PATCH 106/209] types: avoid FlattenMaps by default on toObject(), toJSON(), lean() Re: #13523 --- test/types/lean.test.ts | 18 ++++++ test/types/maps.test.ts | 2 +- test/types/models.test.ts | 7 ++- test/types/schema.create.test.ts | 31 +++++++--- types/document.d.ts | 35 ++++++++---- types/index.d.ts | 62 ++++++++++++-------- types/inferrawdoctype.d.ts | 4 +- types/inferschematype.d.ts | 5 +- types/models.d.ts | 97 ++++++++++++++++---------------- types/query.d.ts | 2 +- 10 files changed, 165 insertions(+), 98 deletions(-) diff --git a/test/types/lean.test.ts b/test/types/lean.test.ts index a35d0f7ad00..eb8196412a1 100644 --- a/test/types/lean.test.ts +++ b/test/types/lean.test.ts @@ -144,6 +144,24 @@ async function gh13010() { expectType>(country.name); } +async function gh13010_1() { + const schema = Schema.create({ + name: { required: true, type: Map, of: String } + }); + + const CountryModel = model('Country', schema); + + await CountryModel.create({ + name: { + en: 'Croatia', + ru: 'Хорватия' + } + }); + + const country = await CountryModel.findOne().lean().orFail().exec(); + expectType>(country.name); +} + async function gh13345_1() { const imageSchema = new Schema({ url: { required: true, type: String } diff --git a/test/types/maps.test.ts b/test/types/maps.test.ts index 69cd016c74f..78173734569 100644 --- a/test/types/maps.test.ts +++ b/test/types/maps.test.ts @@ -70,7 +70,7 @@ function gh10575() { function gh10872(): void { const doc = new Test({}); - doc.toJSON().map1.foo; + doc.toJSON({ flattenMaps: true }).map1.foo; } function gh13755() { diff --git a/test/types/models.test.ts b/test/types/models.test.ts index d7474e61ef6..f6d4f739082 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -16,7 +16,8 @@ import mongoose, { WithLevel1NestedPaths, createConnection, connection, - model + model, + ObtainSchemaGeneric } from 'mongoose'; import { expectAssignable, expectError, expectType } from 'tsd'; import { AutoTypedSchemaType, autoTypedSchema } from './schema.test'; @@ -575,12 +576,14 @@ async function gh12319() { ); const ProjectModel = model('Project', projectSchema); + const doc = new ProjectModel(); + doc.doSomething(); type ProjectModelHydratedDoc = HydratedDocumentFromSchema< typeof projectSchema >; - expectType(await ProjectModel.findOne().orFail()); + expectAssignable(await ProjectModel.findOne().orFail()); } function findWithId() { diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index 3c827db6ca1..f8d3cc9c721 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -413,8 +413,8 @@ export function autoTypedSchema() { objectId2?: Types.ObjectId | null; objectId3?: Types.ObjectId | null; customSchema?: Int8 | null; - map1?: Map | null; - map2?: Map | null; + map1?: Record | null; + map2?: Record | null; array1: string[]; array2: any[]; array3: any[]; @@ -734,17 +734,26 @@ function gh12030() { } & { _id: Types.ObjectId }>; } & { _id: Types.ObjectId }>({} as InferSchemaType); + type RawDocType3 = ObtainSchemaGeneric; type HydratedDoc3 = ObtainSchemaGeneric; expectType< HydratedDocument<{ users: Types.DocumentArray< { credit: number; username?: string | null; } & { _id: Types.ObjectId }, - Types.Subdocument & { credit: number; username?: string | null; } & { _id: Types.ObjectId } + Types.Subdocument< + Types.ObjectId, + unknown, + { credit: number; username?: string | null; } & { _id: Types.ObjectId } + > & { credit: number; username?: string | null; } & { _id: Types.ObjectId } >; - } & { _id: Types.ObjectId }> + } & { _id: Types.ObjectId }, {}, {}, {}, RawDocType3> >({} as HydratedDoc3); expectType< - Types.Subdocument & { credit: number; username?: string | null; } & { _id: Types.ObjectId } + Types.Subdocument< + Types.ObjectId, + unknown, + { credit: number; username?: string | null; } & { _id: Types.ObjectId } + > & { credit: number; username?: string | null; } & { _id: Types.ObjectId } >({} as HydratedDoc3['users'][0]); const Schema4 = Schema.create({ @@ -1164,6 +1173,9 @@ function maps() { const doc = new Test({ myMap: { answer: 42 } }); expectType>(doc.myMap); expectType(doc.myMap!.get('answer')); + + const obj = doc.toObject(); + expectType>(obj.myMap); } function gh13514() { @@ -1697,7 +1709,12 @@ async function gh14950() { const doc = await TestModel.findOne().orFail(); expectType(doc.location!.type); - expectType(doc.location!.coordinates); + expectType>(doc.location!.coordinates); + + const lean = await TestModel.findOne().lean().orFail(); + + expectType(lean.location!.type); + expectType(lean.location!.coordinates); } async function gh14902() { @@ -1748,7 +1765,7 @@ async function gh14451() { subdocProp?: string | undefined | null } | null, docArr: { nums: number[], times: string[] }[], - myMap?: Record | null | undefined, + myMap?: Record | null | undefined, _id: string }>({} as TestJSON); } diff --git a/types/document.d.ts b/types/document.d.ts index dd19f755ac6..a733e2b5ef7 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -256,23 +256,34 @@ declare module 'mongoose' { set(value: string | Record): this; /** The return value of this method is used in calls to JSON.stringify(doc). */ + toJSON(options: ToObjectOptions & { flattenMaps: true, flattenObjectIds: true, virtuals: true }): FlattenMaps>>>; + toJSON(options: ToObjectOptions & { flattenMaps: true, flattenObjectIds: true }): FlattenMaps>>>; + toJSON(options: ToObjectOptions & { flattenMaps: true, virtuals: true }): FlattenMaps>>; + toJSON(options: ToObjectOptions & { flattenObjectIds: true, virtuals: true }): ObjectIdToString>>; + toJSON(options: ToObjectOptions & { flattenMaps: true }): FlattenMaps>>; + toJSON(options: ToObjectOptions & { flattenObjectIds: true }): ObjectIdToString>>; toJSON(options: ToObjectOptions & { virtuals: true }): Default__v>; - toJSON(options?: ToObjectOptions & { flattenMaps?: true, flattenObjectIds?: false }): FlattenMaps>>; - toJSON(options: ToObjectOptions & { flattenObjectIds: false }): FlattenMaps>>; - toJSON(options: ToObjectOptions & { flattenObjectIds: true }): ObjectIdToString>>>; - toJSON(options: ToObjectOptions & { flattenMaps: false }): Default__v>; - toJSON(options: ToObjectOptions & { flattenMaps: false; flattenObjectIds: true }): ObjectIdToString>>; - - toJSON>>(options?: ToObjectOptions & { flattenMaps?: true, flattenObjectIds?: false }): FlattenMaps; - toJSON>>(options: ToObjectOptions & { flattenObjectIds: false }): FlattenMaps; - toJSON>>(options: ToObjectOptions & { flattenObjectIds: true }): ObjectIdToString>; - toJSON>>(options: ToObjectOptions & { flattenMaps: false }): T; - toJSON>>(options: ToObjectOptions & { flattenMaps: false; flattenObjectIds: true }): ObjectIdToString; + toJSON(options?: ToObjectOptions): Default__v>; + + toJSON>>(options: ToObjectOptions & { flattenMaps: true, flattenObjectIds: true }): FlattenMaps>; + toJSON>>(options: ToObjectOptions & { flattenObjectIds: true }): ObjectIdToString; + toJSON>>(options: ToObjectOptions & { flattenMaps: true }): FlattenMaps; + toJSON>>(options?: ToObjectOptions): T; /** Converts this document into a plain-old JavaScript object ([POJO](https://masteringjs.io/tutorials/fundamentals/pojo)). */ + toObject(options: ToObjectOptions & { flattenMaps: true, flattenObjectIds: true, virtuals: true }): FlattenMaps>>>; + toObject(options: ToObjectOptions & { flattenMaps: true, flattenObjectIds: true }): FlattenMaps>>>; + toObject(options: ToObjectOptions & { flattenMaps: true, virtuals: true }): FlattenMaps>>; + toObject(options: ToObjectOptions & { flattenObjectIds: true, virtuals: true }): ObjectIdToString>>; + toObject(options: ToObjectOptions & { flattenMaps: true }): FlattenMaps>>; + toObject(options: ToObjectOptions & { flattenObjectIds: true }): ObjectIdToString>>; toObject(options: ToObjectOptions & { virtuals: true }): Default__v>; toObject(options?: ToObjectOptions): Default__v>; - toObject(options?: ToObjectOptions): Default__v>; + + toObject>>(options: ToObjectOptions & { flattenMaps: true, flattenObjectIds: true }): FlattenMaps>; + toObject>>(options: ToObjectOptions & { flattenObjectIds: true }): ObjectIdToString; + toObject>>(options: ToObjectOptions & { flattenMaps: true }): FlattenMaps; + toObject>>(options?: ToObjectOptions): T; /** Clears the modified state on the specified path. */ unmarkModified(path: T): void; diff --git a/types/index.d.ts b/types/index.d.ts index 942071a7591..126eb6a308b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -85,17 +85,22 @@ declare module 'mongoose' { collection?: string, options?: CompileModelOptions ): Model< - InferSchemaType, - ObtainSchemaGeneric, - ObtainSchemaGeneric, - ObtainSchemaGeneric, - HydratedDocument< - InferSchemaType, - ObtainSchemaGeneric & ObtainSchemaGeneric, - ObtainSchemaGeneric, - ObtainSchemaGeneric - >, - TSchema + InferSchemaType, + ObtainSchemaGeneric, + ObtainSchemaGeneric, + ObtainSchemaGeneric, + // If first schema generic param is set, that means we have an explicit raw doc type, + // so user should also specify a hydrated doc type if the auto inferred one isn't correct. + IsItRecordAndNotAny> extends true + ? ObtainSchemaGeneric + : HydratedDocument< + InferSchemaType, + ObtainSchemaGeneric & ObtainSchemaGeneric, + ObtainSchemaGeneric, + ObtainSchemaGeneric + >, + TSchema, + ObtainSchemaGeneric > & ObtainSchemaGeneric; export function model(name: string, schema?: Schema | Schema, collection?: string, options?: CompileModelOptions): Model; @@ -147,24 +152,26 @@ declare module 'mongoose' { /** Helper type for getting the hydrated document type from the raw document type. The hydrated document type is what `new MyModel()` returns. */ export type HydratedDocument< - DocType, + HydratedDocPathsType, TOverrides = {}, TQueryHelpers = {}, - TVirtuals = {} + TVirtuals = {}, + RawDocType = HydratedDocPathsType > = IfAny< - DocType, + HydratedDocPathsType, any, TOverrides extends Record ? - Document & Default__v> : + Document & Default__v> : IfAny< TOverrides, - Document & Default__v>, - Document & MergeType< - Default__v>, + Document & Default__v>, + Document & MergeType< + Default__v>, TOverrides > > >; + export type HydratedSingleSubdocument< DocType, TOverrides = {} @@ -274,8 +281,9 @@ declare module 'mongoose' { ObtainDocumentType>, ResolveSchemaOptions >, - THydratedDocumentType = HydratedDocument, TVirtuals & TInstanceMethods, {}, TVirtuals>, - TSchemaDefinition = SchemaDefinition, RawDocType, THydratedDocumentType> + THydratedDocumentType = HydratedDocument, + TSchemaDefinition = SchemaDefinition, RawDocType, THydratedDocumentType>, + LeanResultType = IsItRecordAndNotAny extends true ? RawDocType : Default__v>>> > extends events.EventEmitter { /** @@ -291,7 +299,13 @@ declare module 'mongoose' { InferRawDocType>, ResolveSchemaOptions >, - THydratedDocumentType extends AnyObject = HydratedDocument>> + THydratedDocumentType extends AnyObject = HydratedDocument< + InferHydratedDocType>, + TSchemaOptions extends { methods: infer M } ? M : {}, + TSchemaOptions extends { query: any } ? TSchemaOptions['query'] : {}, + TSchemaOptions extends { virtuals: any } ? TSchemaOptions['virtuals'] : {}, + RawDocType + > >(def: TSchemaDefinition): Schema< RawDocType, Model, @@ -305,7 +319,8 @@ declare module 'mongoose' { ResolveSchemaOptions >, THydratedDocumentType, - TSchemaDefinition + TSchemaDefinition, + BufferToBinary >; static create< @@ -329,7 +344,8 @@ declare module 'mongoose' { ResolveSchemaOptions >, THydratedDocumentType, - TSchemaDefinition + TSchemaDefinition, + BufferToBinary >; /** Adds key path / schema type pairs to this schema. */ diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index c40c8c48c73..2e62311d6c8 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -124,8 +124,8 @@ declare module 'mongoose' { PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? UUID : PathValueType extends 'double' | 'Double' | typeof Schema.Types.Double ? Types.Double : IfEquals extends true ? Buffer : - PathValueType extends MapConstructor | 'Map' ? Map> : - IfEquals extends true ? Map> : + PathValueType extends MapConstructor | 'Map' ? Record | undefined> : + IfEquals extends true ? Record | undefined> : PathValueType extends ArrayConstructor ? any[] : PathValueType extends typeof Schema.Types.Mixed ? any: IfEquals extends true ? any: diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 2b01fd40c9e..8149e60d370 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -56,8 +56,8 @@ declare module 'mongoose' { * @param {TSchema} TSchema A generic of schema type instance. * @param {alias} alias Targeted generic alias. */ - type ObtainSchemaGeneric = - TSchema extends Schema + type ObtainSchemaGeneric = + TSchema extends Schema ? { EnforcedDocType: EnforcedDocType; M: M; @@ -69,6 +69,7 @@ declare module 'mongoose' { DocType: DocType; THydratedDocumentType: THydratedDocumentType; TSchemaDefinition: TSchemaDefinition; + TLeanResultType: TLeanResultType; }[alias] : unknown; diff --git a/types/models.d.ts b/types/models.d.ts index bc205aed74c..df9ac8e6ae0 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -264,7 +264,8 @@ declare module 'mongoose' { TInstanceMethods = {}, TVirtuals = {}, THydratedDocumentType = HydratedDocument, - TSchema = any> extends + TSchema = any, + TLeanResultType = TRawDocType> extends NodeJS.EventEmitter, AcceptsDiscriminator, IndexManager, @@ -387,7 +388,7 @@ declare module 'mongoose' { mongodb.DeleteResult, THydratedDocumentType, TQueryHelpers, - TRawDocType, + TLeanResultType, 'deleteMany', TInstanceMethods & TVirtuals >; @@ -404,7 +405,7 @@ declare module 'mongoose' { mongodb.DeleteResult, THydratedDocumentType, TQueryHelpers, - TRawDocType, + TLeanResultType, 'deleteOne', TInstanceMethods & TVirtuals >; @@ -414,7 +415,7 @@ declare module 'mongoose' { mongodb.DeleteResult, THydratedDocumentType, TQueryHelpers, - TRawDocType, + TLeanResultType, 'deleteOne', TInstanceMethods & TVirtuals >; @@ -444,7 +445,7 @@ declare module 'mongoose' { GetLeanResultType | null, ResultDoc, TQueryHelpers, - TRawDocType, + TLeanResultType, 'findOne', TInstanceMethods & TVirtuals >; @@ -467,7 +468,7 @@ declare module 'mongoose' { GetLeanResultType | null, ResultDoc, TQueryHelpers, - TRawDocType, + TLeanResultType, 'findOne', TInstanceMethods & TVirtuals >; @@ -475,14 +476,14 @@ declare module 'mongoose' { filter?: RootFilterQuery, projection?: ProjectionType | null, options?: QueryOptions | null - ): QueryWithHelpers; + ): QueryWithHelpers; findOne( filter?: RootFilterQuery, projection?: ProjectionType | null - ): QueryWithHelpers; + ): QueryWithHelpers; findOne( filter?: RootFilterQuery - ): QueryWithHelpers; + ): QueryWithHelpers; /** * Shortcut for creating a new Document from existing raw data, pre-saved in the DB. @@ -659,7 +660,7 @@ declare module 'mongoose' { >, THydratedDocumentType, TQueryHelpers, - TRawDocType, + TLeanResultType, 'distinct', TInstanceMethods & TVirtuals >; @@ -669,7 +670,7 @@ declare module 'mongoose' { number, THydratedDocumentType, TQueryHelpers, - TRawDocType, + TLeanResultType, 'estimatedDocumentCount', TInstanceMethods & TVirtuals >; @@ -684,7 +685,7 @@ declare module 'mongoose' { { _id: InferId } | null, THydratedDocumentType, TQueryHelpers, - TRawDocType, + TLeanResultType, 'findOne', TInstanceMethods & TVirtuals >; @@ -698,7 +699,7 @@ declare module 'mongoose' { GetLeanResultType, ResultDoc, TQueryHelpers, - TRawDocType, + TLeanResultType, 'find', TInstanceMethods & TVirtuals >; @@ -706,37 +707,37 @@ declare module 'mongoose' { filter: RootFilterQuery, projection?: ProjectionType | null | undefined, options?: QueryOptions | null | undefined - ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'find', TInstanceMethods & TVirtuals>; + ): QueryWithHelpers, ResultDoc, TQueryHelpers, TLeanResultType, 'find', TInstanceMethods & TVirtuals>; find( filter: RootFilterQuery, projection?: ProjectionType | null | undefined - ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'find', TInstanceMethods & TVirtuals>; + ): QueryWithHelpers, ResultDoc, TQueryHelpers, TLeanResultType, 'find', TInstanceMethods & TVirtuals>; find( filter: RootFilterQuery - ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'find', TInstanceMethods & TVirtuals>; + ): QueryWithHelpers, ResultDoc, TQueryHelpers, TLeanResultType, 'find', TInstanceMethods & TVirtuals>; find( - ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'find', TInstanceMethods & TVirtuals>; + ): QueryWithHelpers, ResultDoc, TQueryHelpers, TLeanResultType, 'find', TInstanceMethods & TVirtuals>; /** Creates a `findByIdAndDelete` query, filtering by the given `_id`. */ findByIdAndDelete( id: mongodb.ObjectId | any, options: QueryOptions & { lean: true } ): QueryWithHelpers< - GetLeanResultType | null, + TLeanResultType | null, ResultDoc, TQueryHelpers, - TRawDocType, + TLeanResultType, 'findOneAndDelete', TInstanceMethods & TVirtuals >; findByIdAndDelete( id: mongodb.ObjectId | any, options: QueryOptions & { includeResultMetadata: true } - ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndDelete', TInstanceMethods & TVirtuals>; + ): QueryWithHelpers, ResultDoc, TQueryHelpers, TLeanResultType, 'findOneAndDelete', TInstanceMethods & TVirtuals>; findByIdAndDelete( id?: mongodb.ObjectId | any, options?: QueryOptions | null - ): QueryWithHelpers; + ): QueryWithHelpers; /** Creates a `findOneAndUpdate` query, filtering by the given `_id`. */ findByIdAndUpdate( @@ -747,7 +748,7 @@ declare module 'mongoose' { ModifyResult, ResultDoc, TQueryHelpers, - TRawDocType, + TLeanResultType, 'findOneAndUpdate', TInstanceMethods & TVirtuals >; @@ -756,10 +757,10 @@ declare module 'mongoose' { update: UpdateQuery, options: QueryOptions & { lean: true } ): QueryWithHelpers< - GetLeanResultType | null, + TLeanResultType | null, ResultDoc, TQueryHelpers, - TRawDocType, + TLeanResultType, 'findOneAndUpdate', TInstanceMethods & TVirtuals >; @@ -767,42 +768,42 @@ declare module 'mongoose' { id: mongodb.ObjectId | any, update: UpdateQuery, options: QueryOptions & { includeResultMetadata: true } - ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndUpdate', TInstanceMethods & TVirtuals>; + ): QueryWithHelpers, ResultDoc, TQueryHelpers, TLeanResultType, 'findOneAndUpdate', TInstanceMethods & TVirtuals>; findByIdAndUpdate( id: mongodb.ObjectId | any, update: UpdateQuery, options: QueryOptions & { upsert: true } & ReturnsNewDoc - ): QueryWithHelpers; + ): QueryWithHelpers; findByIdAndUpdate( id?: mongodb.ObjectId | any, update?: UpdateQuery, options?: QueryOptions | null - ): QueryWithHelpers; + ): QueryWithHelpers; findByIdAndUpdate( id: mongodb.ObjectId | any, update: UpdateQuery - ): QueryWithHelpers; + ): QueryWithHelpers; /** Creates a `findOneAndDelete` query: atomically finds the given document, deletes it, and returns the document as it was before deletion. */ findOneAndDelete( filter: RootFilterQuery, options: QueryOptions & { lean: true } ): QueryWithHelpers< - GetLeanResultType | null, + TLeanResultType | null, ResultDoc, TQueryHelpers, - TRawDocType, + TLeanResultType, 'findOneAndDelete', TInstanceMethods & TVirtuals >; findOneAndDelete( filter: RootFilterQuery, options: QueryOptions & { includeResultMetadata: true } - ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndDelete', TInstanceMethods & TVirtuals>; + ): QueryWithHelpers, ResultDoc, TQueryHelpers, TLeanResultType, 'findOneAndDelete', TInstanceMethods & TVirtuals>; findOneAndDelete( filter?: RootFilterQuery | null, options?: QueryOptions | null - ): QueryWithHelpers; + ): QueryWithHelpers; /** Creates a `findOneAndReplace` query: atomically finds the given document and replaces it with `replacement`. */ findOneAndReplace( @@ -810,10 +811,10 @@ declare module 'mongoose' { replacement: TRawDocType | AnyObject, options: QueryOptions & { lean: true } ): QueryWithHelpers< - GetLeanResultType | null, + TLeanResultType | null, ResultDoc, TQueryHelpers, - TRawDocType, + TLeanResultType, 'findOneAndReplace', TInstanceMethods & TVirtuals >; @@ -821,17 +822,17 @@ declare module 'mongoose' { filter: RootFilterQuery, replacement: TRawDocType | AnyObject, options: QueryOptions & { includeResultMetadata: true } - ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndReplace', TInstanceMethods & TVirtuals>; + ): QueryWithHelpers, ResultDoc, TQueryHelpers, TLeanResultType, 'findOneAndReplace', TInstanceMethods & TVirtuals>; findOneAndReplace( filter: RootFilterQuery, replacement: TRawDocType | AnyObject, options: QueryOptions & { upsert: true } & ReturnsNewDoc - ): QueryWithHelpers; + ): QueryWithHelpers; findOneAndReplace( filter?: RootFilterQuery, replacement?: TRawDocType | AnyObject, options?: QueryOptions | null - ): QueryWithHelpers; + ): QueryWithHelpers; /** Creates a `findOneAndUpdate` query: atomically find the first document that matches `filter` and apply `update`. */ findOneAndUpdate( @@ -842,7 +843,7 @@ declare module 'mongoose' { ModifyResult, ResultDoc, TQueryHelpers, - TRawDocType, + TLeanResultType, 'findOneAndUpdate', TInstanceMethods & TVirtuals >; @@ -854,7 +855,7 @@ declare module 'mongoose' { GetLeanResultType | null, ResultDoc, TQueryHelpers, - TRawDocType, + TLeanResultType, 'findOneAndUpdate', TInstanceMethods & TVirtuals >; @@ -862,24 +863,24 @@ declare module 'mongoose' { filter: RootFilterQuery, update: UpdateQuery, options: QueryOptions & { includeResultMetadata: true } - ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndUpdate', TInstanceMethods & TVirtuals>; + ): QueryWithHelpers, ResultDoc, TQueryHelpers, TLeanResultType, 'findOneAndUpdate', TInstanceMethods & TVirtuals>; findOneAndUpdate( filter: RootFilterQuery, update: UpdateQuery, options: QueryOptions & { upsert: true } & ReturnsNewDoc - ): QueryWithHelpers; + ): QueryWithHelpers; findOneAndUpdate( filter?: RootFilterQuery, update?: UpdateQuery, options?: QueryOptions | null - ): QueryWithHelpers; + ): QueryWithHelpers; /** Creates a `replaceOne` query: finds the first document that matches `filter` and replaces it with `replacement`. */ replaceOne( filter?: RootFilterQuery, replacement?: TRawDocType | AnyObject, options?: (mongodb.ReplaceOptions & MongooseQueryOptions) | null - ): QueryWithHelpers; + ): QueryWithHelpers; /** Apply changes made to this model's schema after this model was compiled. */ recompileSchema(): void; @@ -892,17 +893,17 @@ declare module 'mongoose' { filter: RootFilterQuery, update: UpdateQuery | UpdateWithAggregationPipeline, options?: (mongodb.UpdateOptions & MongooseUpdateQueryOptions) | null - ): QueryWithHelpers; + ): QueryWithHelpers; /** Creates a `updateOne` query: updates the first document that matches `filter` with `update`. */ updateOne( filter: RootFilterQuery, update: UpdateQuery | UpdateWithAggregationPipeline, options?: (mongodb.UpdateOptions & MongooseUpdateQueryOptions) | null - ): QueryWithHelpers; + ): QueryWithHelpers; updateOne( update: UpdateQuery | UpdateWithAggregationPipeline - ): QueryWithHelpers; + ): QueryWithHelpers; /** Creates a Query, applies the passed conditions, and returns the Query. */ where( @@ -913,7 +914,7 @@ declare module 'mongoose' { Array, ResultDoc, TQueryHelpers, - TRawDocType, + TLeanResultType, 'find', TInstanceMethods & TVirtuals >; @@ -921,7 +922,7 @@ declare module 'mongoose' { Array, ResultDoc, TQueryHelpers, - TRawDocType, + TLeanResultType, 'find', TInstanceMethods & TVirtuals >; diff --git a/types/query.d.ts b/types/query.d.ts index 80a28924763..eee0df769be 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -237,7 +237,7 @@ declare module 'mongoose' { type QueryOpThatReturnsDocument = 'find' | 'findOne' | 'findOneAndUpdate' | 'findOneAndReplace' | 'findOneAndDelete'; type GetLeanResultType = QueryOp extends QueryOpThatReturnsDocument - ? (ResultType extends any[] ? Default__v>>>[] : Default__v>>>) + ? (ResultType extends any[] ? Default__v>[] : Default__v>) : ResultType; type MergePopulatePaths> = QueryOp extends QueryOpThatReturnsDocument From da378db18d869401157a6ce4bae7325239578ef4 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 6 Jul 2025 13:43:10 -0400 Subject: [PATCH 107/209] types: add TTransformOptions for InferRawDocType so RawDocType and LeanResultType can be computed by the same helper re: #13523 --- types/index.d.ts | 10 +++++-- types/inferrawdoctype.d.ts | 59 ++++++++++++++++++++------------------ 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 126eb6a308b..f69dba7b974 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -320,7 +320,10 @@ declare module 'mongoose' { >, THydratedDocumentType, TSchemaDefinition, - BufferToBinary + ApplySchemaOptions< + InferRawDocType, { bufferToBinary: true }>, + ResolveSchemaOptions + > >; static create< @@ -345,7 +348,10 @@ declare module 'mongoose' { >, THydratedDocumentType, TSchemaDefinition, - BufferToBinary + ApplySchemaOptions< + InferRawDocType, { bufferToBinary: true }>, + ResolveSchemaOptions + > >; /** Adds key path / schema type pairs to this schema. */ diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index 2e62311d6c8..a374fac84cd 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -6,19 +6,20 @@ import { PathWithTypePropertyBaseType, PathEnumOrString } from './inferschematype'; -import { UUID } from 'mongodb'; +import { Binary, UUID } from 'mongodb'; declare module 'mongoose' { export type InferRawDocType< - DocDefinition, - TSchemaOptions extends Record = DefaultSchemaOptions + SchemaDefinition, + TSchemaOptions extends Record = DefaultSchemaOptions, + TTransformOptions = { bufferToBinary: false } > = Require_id & - OptionalPaths) - ]: IsPathRequired extends true - ? ObtainRawDocumentPathType - : ObtainRawDocumentPathType | null; + K in keyof (RequiredPaths & + OptionalPaths) + ]: IsPathRequired extends true + ? ObtainRawDocumentPathType + : ObtainRawDocumentPathType | null; }, TSchemaOptions>>; /** @@ -34,7 +35,8 @@ declare module 'mongoose' { */ type ObtainRawDocumentPathType< PathValueType, - TypeKey extends string = DefaultTypeKey + TypeKey extends string = DefaultTypeKey, + TTransformOptions = { bufferToBinary: false } > = ResolveRawPathType< PathValueType extends PathWithTypePropertyBaseType ? PathValueType[TypeKey] extends PathWithTypePropertyBaseType @@ -47,6 +49,7 @@ declare module 'mongoose' { : Omit : {}, TypeKey, + TTransformOptions, RawDocTypeHint >; @@ -63,44 +66,44 @@ declare module 'mongoose' { * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". * @returns Number, "Number" or "number" will be resolved to number type. */ - type ResolveRawPathType = {}, TypeKey extends string = DefaultSchemaOptions['typeKey'], TypeHint = never> = + type ResolveRawPathType = {}, TypeKey extends string = DefaultSchemaOptions['typeKey'], TTransformOptions = { bufferToBinary: false }, TypeHint = never> = IfEquals ? - IsItRecordAndNotAny extends true ? RawDocType : InferRawDocType : + IsItRecordAndNotAny extends true ? RawDocType : InferRawDocType : PathValueType extends (infer Item)[] ? IfEquals ? // If Item is a schema, infer its type. - Array extends true ? RawDocType : InferRawDocType> : + Array extends true ? RawDocType : InferRawDocType> : Item extends Record ? Item[TypeKey] extends Function | String ? // If Item has a type key that's a string or a callable, it must be a scalar, // so we can directly obtain its path type. - ObtainRawDocumentPathType[] : + ObtainRawDocumentPathType[] : // If the type key isn't callable, then this is an array of objects, in which case // we need to call InferRawDocType to correctly infer its type. - Array> : + Array> : IsSchemaTypeFromBuiltinClass extends true ? - ObtainRawDocumentPathType[] : + ObtainRawDocumentPathType[] : IsItRecordAndNotAny extends true ? Item extends Record ? - ObtainRawDocumentPathType[] : - Array> : - ObtainRawDocumentPathType[] + ObtainRawDocumentPathType[] : + Array> : + ObtainRawDocumentPathType[] >: PathValueType extends ReadonlyArray ? IfEquals ? - Array extends true ? RawDocType : InferRawDocType> : + Array extends true ? RawDocType : InferRawDocType> : Item extends Record ? Item[TypeKey] extends Function | String ? - ObtainRawDocumentPathType[] : - InferRawDocType[]: + ObtainRawDocumentPathType[] : + InferRawDocType[]: IsSchemaTypeFromBuiltinClass extends true ? - ObtainRawDocumentPathType[] : + ObtainRawDocumentPathType[] : IsItRecordAndNotAny extends true ? Item extends Record ? - ObtainRawDocumentPathType[] : - Array> : - ObtainRawDocumentPathType[] + ObtainRawDocumentPathType[] : + Array> : + ObtainRawDocumentPathType[] >: PathValueType extends StringSchemaDefinition ? PathEnumOrString : IfEquals extends true ? PathEnumOrString : @@ -109,7 +112,7 @@ declare module 'mongoose' { IfEquals extends true ? number : PathValueType extends DateSchemaDefinition ? NativeDate : IfEquals extends true ? NativeDate : - PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer : + PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? TTransformOptions extends { bufferToBinary: true } ? Binary : Buffer : PathValueType extends BooleanSchemaDefinition ? boolean : IfEquals extends true ? boolean : PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId : @@ -123,7 +126,7 @@ declare module 'mongoose' { PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint : PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? UUID : PathValueType extends 'double' | 'Double' | typeof Schema.Types.Double ? Types.Double : - IfEquals extends true ? Buffer : + IfEquals extends true ? UUID : PathValueType extends MapConstructor | 'Map' ? Record | undefined> : IfEquals extends true ? Record | undefined> : PathValueType extends ArrayConstructor ? any[] : @@ -131,7 +134,7 @@ declare module 'mongoose' { IfEquals extends true ? any: IfEquals extends true ? any: PathValueType extends typeof SchemaType ? PathValueType['prototype'] : - PathValueType extends Record ? InferRawDocType : + PathValueType extends Record ? InferRawDocType : unknown, TypeHint>; } From cef5e5b1d24bcf6892ee00d7d6cc6a8fdfe5bd77 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 7 Jul 2025 10:09:40 -0400 Subject: [PATCH 108/209] remove unused symbol --- lib/schema/array.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/schema/array.js b/lib/schema/array.js index 2d5266ffac5..965e6f3f7f7 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -28,7 +28,6 @@ const getDiscriminatorByValue = require('../helpers/discriminator/getDiscriminat let MongooseArray; let EmbeddedDoc; -const isNestedArraySymbol = Symbol('mongoose#isNestedArray'); const emptyOpts = Object.freeze({}); /** @@ -294,8 +293,7 @@ SchemaArray.prototype.applyGetters = function(value, scope) { SchemaArray.prototype._applySetters = function(value, scope, init, priorVal) { if (this.embeddedSchemaType.$isMongooseArray && - SchemaArray.options.castNonArrays && - !this[isNestedArraySymbol]) { + SchemaArray.options.castNonArrays) { // Check nesting levels and wrap in array if necessary let depth = 0; let arr = this; From 5155c283a8ed315bed740a1a7b47248c4d51a333 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 7 Jul 2025 10:12:35 -0400 Subject: [PATCH 109/209] docs: add casterConstructor example --- docs/migrating_to_9.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 63ca209d1a4..58081d5f34f 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -251,6 +251,10 @@ const schema = new mongoose.Schema({ docArray: [new mongoose.Schema({ name: Stri console.log(schema.path('arr').caster); // SchemaString console.log(schema.path('docArray').caster); // EmbeddedDocument constructor +console.log(schema.path('arr').casterConstructor); // SchemaString constructor +console.log(schema.path('docArray').casterConstructor); // EmbeddedDocument constructor + + // In Mongoose 9: console.log(schema.path('arr').embeddedSchemaType); // SchemaString console.log(schema.path('docArray').embeddedSchemaType); // SchemaDocumentArrayElement From c5108117fabfa961872bcd988939274b25a5270a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 7 Jul 2025 10:14:49 -0400 Subject: [PATCH 110/209] docs: add note about $embeddedSchemaType -> embeddedSchemaType --- docs/migrating_to_9.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 58081d5f34f..7aa90c96ed0 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -254,7 +254,6 @@ console.log(schema.path('docArray').caster); // EmbeddedDocument constructor console.log(schema.path('arr').casterConstructor); // SchemaString constructor console.log(schema.path('docArray').casterConstructor); // EmbeddedDocument constructor - // In Mongoose 9: console.log(schema.path('arr').embeddedSchemaType); // SchemaString console.log(schema.path('docArray').embeddedSchemaType); // SchemaDocumentArrayElement @@ -263,6 +262,8 @@ console.log(schema.path('arr').Constructor); // undefined console.log(schema.path('docArray').Constructor); // EmbeddedDocument constructor ``` +In Mongoose 8, there was also an internal `$embeddedSchemaType` property. That property has been replaced with `embeddedSchemaType`, which is now part of the public API. + ## TypeScript ### FilterQuery Properties No Longer Resolve to any From 6657fa1aed024ffe97b4f82052f591a9c9c972c7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 7 Jul 2025 10:16:53 -0400 Subject: [PATCH 111/209] docs: remove unused comment --- lib/schema.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/schema.js b/lib/schema.js index bcfbd665f28..abfed2a0a17 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1387,7 +1387,6 @@ Schema.prototype.path = function(path, obj) { while (_schemaType.$isMongooseArray) { arrayPath = arrayPath + '.$'; - // Skip arrays of document arrays _schemaType.embeddedSchemaType._arrayPath = arrayPath; _schemaType.embeddedSchemaType._arrayParentPath = path; _schemaType = _schemaType.embeddedSchemaType; From b713fa832fedb1c4f1ac50fdd0cbd7c04be42761 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 7 Jul 2025 10:19:26 -0400 Subject: [PATCH 112/209] docs: add JSDoc note about documentArrayElement params --- lib/schema/documentArrayElement.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/schema/documentArrayElement.js b/lib/schema/documentArrayElement.js index 3f2e9fed0ef..9cee59bd5ca 100644 --- a/lib/schema/documentArrayElement.js +++ b/lib/schema/documentArrayElement.js @@ -10,9 +10,14 @@ const SchemaSubdocument = require('./subdocument'); const getConstructor = require('../helpers/discriminator/getConstructor'); /** - * DocumentArrayElement SchemaType constructor. + * DocumentArrayElement SchemaType constructor. Mongoose calls this internally when you define a new document array in your schema. + * + * #### Example: + * const schema = new Schema({ users: [{ name: String }] }); + * schema.path('users.$'); // SchemaDocumentArrayElement with schema `new Schema({ name: String })` * * @param {String} path + * @param {Schema} schema * @param {Object} options * @inherits SchemaType * @api public From 7367171d3c74ff3fd4c77b6016df4ec4f04d74c3 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 7 Jul 2025 10:24:52 -0400 Subject: [PATCH 113/209] refactor: use variables to make code more readable --- lib/schema/array.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/schema/array.js b/lib/schema/array.js index 965e6f3f7f7..a4f3dad1d4a 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -490,14 +490,14 @@ SchemaArray.prototype.clone = function() { SchemaArray.prototype._castForQuery = function(val, context) { let embeddedSchemaType = this.embeddedSchemaType; + const discriminatorKey = embeddedSchemaType?.schema?.options?.discriminatorKey; + const discriminators = embeddedSchemaType?.discriminators; - if (val && - embeddedSchemaType?.discriminators && - typeof embeddedSchemaType?.schema?.options?.discriminatorKey === 'string') { - if (embeddedSchemaType.discriminators[val[embeddedSchemaType.schema.options.discriminatorKey]]) { - embeddedSchemaType = embeddedSchemaType.discriminators[val[embeddedSchemaType.schema.options.discriminatorKey]]; + if (val && discriminators && typeof discriminatorKey === 'string') { + if (discriminators[val[discriminatorKey]]) { + embeddedSchemaType = discriminators[val[discriminatorKey]]; } else { - const constructorByValue = getDiscriminatorByValue(embeddedSchemaType.discriminators, val[embeddedSchemaType.schema.options.discriminatorKey]); + const constructorByValue = getDiscriminatorByValue(discriminators, val[discriminatorKey]); if (constructorByValue) { embeddedSchemaType = constructorByValue; } From ae2e9e740e18270c85450b5120749307aced0502 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 7 Jul 2025 11:16:41 -0400 Subject: [PATCH 114/209] types: add InferRawDocTypeFromSchema --- test/types/schema.create.test.ts | 25 ++++++++++++++++++++++++- test/types/schema.test.ts | 26 ++++++++++++++++++++++++-- types/inferrawdoctype.d.ts | 4 ++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index 3c827db6ca1..e0d412666fa 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -23,7 +23,8 @@ import { model, ValidateOpts, CallbackWithoutResultAndOptionalError, - InferHydratedDocType + InferHydratedDocType, + InferRawDocTypeFromSchema } from 'mongoose'; import { Binary, BSON, UUID } from 'mongodb'; import { expectType, expectError, expectAssignable } from 'tsd'; @@ -1839,3 +1840,25 @@ function defaultReturnsUndefined() { } }); } + +function testInferRawDocTypeFromSchema() { + const schema = Schema.create({ + name: String, + arr: [Number], + docArr: [{ name: { type: String, required: true } }], + subdoc: Schema.create({ + answer: { type: Number, required: true } + }), + map: { type: Map, of: String } + }); + + type RawDocType = InferRawDocTypeFromSchema; + + expectType<{ + name?: string | null | undefined, + arr: number[], + docArr: ({ name: string } & { _id: Types.ObjectId })[], + subdoc?: ({ answer: number } & { _id: Types.ObjectId }) | null | undefined, + map?: Map | null | undefined + } & { _id: Types.ObjectId }>({} as RawDocType); +} diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 4e188498724..4c891375313 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -22,8 +22,8 @@ import { Query, model, ValidateOpts, - BufferToBinary, - CallbackWithoutResultAndOptionalError + CallbackWithoutResultAndOptionalError, + InferRawDocTypeFromSchema } from 'mongoose'; import { BSON, Binary, UUID } from 'mongodb'; import { expectType, expectError, expectAssignable } from 'tsd'; @@ -1877,3 +1877,25 @@ function gh15516() { expectType(this); }); } + +function testInferRawDocTypeFromSchema() { + const schema = new Schema({ + name: String, + arr: [Number], + docArr: [{ name: { type: String, required: true } }], + subdoc: new Schema({ + answer: { type: Number, required: true } + }), + map: { type: Map, of: String } + }); + + type RawDocType = InferRawDocTypeFromSchema; + + expectType<{ + name?: string | null | undefined, + arr: number[], + docArr: { name: string }[], + subdoc?: { answer: number } | null | undefined, + map?: Record | null | undefined + }>({} as RawDocType); +} diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index c40c8c48c73..1fa67b0f9fc 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -9,6 +9,10 @@ import { import { UUID } from 'mongodb'; declare module 'mongoose' { + export type InferRawDocTypeFromSchema> = IsItRecordAndNotAny> extends true + ? ObtainSchemaGeneric + : FlattenMaps>>; + export type InferRawDocType< DocDefinition, TSchemaOptions extends Record = DefaultSchemaOptions From db76c6462c7a4b1e45989c72c61f685ce38f3290 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 7 Jul 2025 11:57:37 -0400 Subject: [PATCH 115/209] fix tests --- test/types/schema.create.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index 976a9395af4..244634cecd6 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -1876,6 +1876,6 @@ function testInferRawDocTypeFromSchema() { arr: number[], docArr: ({ name: string } & { _id: Types.ObjectId })[], subdoc?: ({ answer: number } & { _id: Types.ObjectId }) | null | undefined, - map?: Map | null | undefined + map?: Record | null | undefined } & { _id: Types.ObjectId }>({} as RawDocType); } From da055be6461c7c080bc531bbb6eb608d6080dc8f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 7 Jul 2025 13:29:59 -0400 Subject: [PATCH 116/209] fix lint --- test/types/schema.create.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index e0d412666fa..887f95e5cfc 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -1857,8 +1857,8 @@ function testInferRawDocTypeFromSchema() { expectType<{ name?: string | null | undefined, arr: number[], - docArr: ({ name: string } & { _id: Types.ObjectId })[], + docArr:({ name: string } & { _id: Types.ObjectId })[], subdoc?: ({ answer: number } & { _id: Types.ObjectId }) | null | undefined, map?: Map | null | undefined - } & { _id: Types.ObjectId }>({} as RawDocType); + } & { _id: Types.ObjectId }>({} as RawDocType); } From 4cd119fbce5c3601fb0af7bc5522b22e40295a09 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 7 Jul 2025 13:36:33 -0400 Subject: [PATCH 117/209] merge fixes --- test/types/schema.create.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index 5bd306b811e..74f516c707c 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -1877,5 +1877,5 @@ function testInferRawDocTypeFromSchema() { docArr:({ name: string } & { _id: Types.ObjectId })[], subdoc?: ({ answer: number } & { _id: Types.ObjectId }) | null | undefined, map?: Record | null | undefined - } & { _id: Types.ObjectId }>({} as RawDocType); + } & { _id: Types.ObjectId }>({} as RawDocType); } From ee120cf33a930039218f51323f45cec03b9a6ad7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 7 Jul 2025 14:30:51 -0400 Subject: [PATCH 118/209] types: WIP inferHydratedDocTypeFromSchema --- test/types/schema.create.test.ts | 34 ++++++++++++++++++++++++++++---- types/inferhydrateddoctype.d.ts | 2 ++ types/types.d.ts | 2 +- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index 887f95e5cfc..d14c5967ff7 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -24,7 +24,8 @@ import { ValidateOpts, CallbackWithoutResultAndOptionalError, InferHydratedDocType, - InferRawDocTypeFromSchema + InferRawDocTypeFromSchema, + InferHydratedDocTypeFromSchema } from 'mongoose'; import { Binary, BSON, UUID } from 'mongodb'; import { expectType, expectError, expectAssignable } from 'tsd'; @@ -1854,11 +1855,36 @@ function testInferRawDocTypeFromSchema() { type RawDocType = InferRawDocTypeFromSchema; - expectType<{ + type Expected = { name?: string | null | undefined, arr: number[], - docArr:({ name: string } & { _id: Types.ObjectId })[], + docArr: ({ name: string } & { _id: Types.ObjectId })[], subdoc?: ({ answer: number } & { _id: Types.ObjectId }) | null | undefined, map?: Map | null | undefined - } & { _id: Types.ObjectId }>({} as RawDocType); + } & { _id: Types.ObjectId }; + + expectType({} as RawDocType); +} + +function testInferHydratedDocTypeFromSchema() { + const subschema = Schema.create({ answer: { type: Number, required: true } }); + const schema = Schema.create({ + name: String, + arr: [Number], + docArr: [{ name: { type: String, required: true } }], + subdoc: subschema, + map: { type: Map, of: String } + }); + + type HydratedDocType = InferHydratedDocTypeFromSchema; + + type Expected = HydratedDocument<{ + name?: string | null | undefined, + arr: Types.Array, + docArr: Types.DocumentArray<{ name: string } & { _id: Types.ObjectId }>, + subdoc?: HydratedDocument<{ answer: number } & { _id: Types.ObjectId }> | null | undefined, + map?: Map | null | undefined + } & { _id: Types.ObjectId }>; + + expectType({} as HydratedDocType); } diff --git a/types/inferhydrateddoctype.d.ts b/types/inferhydrateddoctype.d.ts index 9382033aeb1..6ffecb2541c 100644 --- a/types/inferhydrateddoctype.d.ts +++ b/types/inferhydrateddoctype.d.ts @@ -9,6 +9,8 @@ import { import { UUID } from 'mongodb'; declare module 'mongoose' { + export type InferHydratedDocTypeFromSchema> = ObtainSchemaGeneric; + /** * Given a schema definition, returns the hydrated document type from the schema definition. */ diff --git a/types/types.d.ts b/types/types.d.ts index c9d86a44b9b..399f9669811 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 = Types.Subdocument, any, T> & T> extends Types.Array { + class DocumentArray = Types.Subdocument, unknown, T> & T> extends Types.Array { /** DocumentArray constructor */ constructor(values: AnyObject[]); From 9d721e25d61af3e6a70b4dd54394642f5c4e7a50 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 8 Jul 2025 12:43:43 -0400 Subject: [PATCH 119/209] types: complete testing inferHydratedDocTypeFromSchema with new Schema() schema inference --- test/types/schema.create.test.ts | 2 +- test/types/schema.test.ts | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index d14c5967ff7..5ade3ab2538 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -1866,7 +1866,7 @@ function testInferRawDocTypeFromSchema() { expectType({} as RawDocType); } -function testInferHydratedDocTypeFromSchema() { +async function testInferHydratedDocTypeFromSchema() { const subschema = Schema.create({ answer: { type: Number, required: true } }); const schema = Schema.create({ name: String, diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 4c891375313..5dc8eaf14be 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -23,7 +23,9 @@ import { model, ValidateOpts, CallbackWithoutResultAndOptionalError, - InferRawDocTypeFromSchema + InferRawDocTypeFromSchema, + InferHydratedDocTypeFromSchema, + FlatRecord } from 'mongoose'; import { BSON, Binary, UUID } from 'mongodb'; import { expectType, expectError, expectAssignable } from 'tsd'; @@ -1899,3 +1901,25 @@ function testInferRawDocTypeFromSchema() { map?: Record | null | undefined }>({} as RawDocType); } + +function testInferHydratedDocTypeFromSchema() { + const schema = new Schema({ + name: String, + arr: [Number], + docArr: [{ name: { type: String, required: true } }], + subdoc: new Schema({ answer: { type: Number, required: true } }), + map: { type: Map, of: String } + }); + + type HydratedDocType = InferHydratedDocTypeFromSchema; + + type Expected = HydratedDocument, + subdoc?: { answer: number } | null | undefined, + map?: Map | null | undefined + }>>; + + expectType({} as HydratedDocType); +} From f21175f047939ad1a32f23e382326f8d8c1ccc93 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 11 Jul 2025 12:52:13 -0400 Subject: [PATCH 120/209] merge conflict cleanup --- test/types/schema.create.test.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index 0d427ebd0b7..a839860e0a6 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -1877,11 +1877,7 @@ function testInferRawDocTypeFromSchema() { arr: number[], docArr: ({ name: string } & { _id: Types.ObjectId })[], subdoc?: ({ answer: number } & { _id: Types.ObjectId }) | null | undefined, -<<<<<<< HEAD - map?: Record | null | undefined - } & { _id: Types.ObjectId }>({} as RawDocType); -======= - map?: Map | null | undefined + map?: Record | null | undefined; } & { _id: Types.ObjectId }; expectType({} as RawDocType); @@ -1908,5 +1904,4 @@ async function testInferHydratedDocTypeFromSchema() { } & { _id: Types.ObjectId }>; expectType({} as HydratedDocType); ->>>>>>> vkarpov15/schema-create } From 8e4b6a6393af49ec38699339b9236e5d8ea7b1b0 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 11 Jul 2025 12:55:36 -0400 Subject: [PATCH 121/209] test: fix tests --- test/types.documentarray.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types.documentarray.test.js b/test/types.documentarray.test.js index 2acae98f083..4fe8324af1c 100644 --- a/test/types.documentarray.test.js +++ b/test/types.documentarray.test.js @@ -786,6 +786,6 @@ describe('types.documentarray', function() { someCustomOption: 'test 42' }] }); - assert.strictEqual(schema.path('docArr').$embeddedSchemaType.options.someCustomOption, 'test 42'); + assert.strictEqual(schema.path('docArr').embeddedSchemaType.options.someCustomOption, 'test 42'); }); }); From d9834e9c0b1614b51dae94b1619769bfae5014e6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 6 Aug 2025 12:00:09 -0400 Subject: [PATCH 122/209] BREAKING CHANGE: make `id` a virtual in TypeScript rather than a property on Document base class Fix #13079 --- test/types/document.test.ts | 44 +++++++++++++++++++++++++++++++++++++ test/types/virtuals.test.ts | 2 +- types/document.d.ts | 3 --- types/inferschematype.d.ts | 2 +- 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/test/types/document.test.ts b/test/types/document.test.ts index 0e173454e07..eb0857fe135 100644 --- a/test/types/document.test.ts +++ b/test/types/document.test.ts @@ -475,3 +475,47 @@ async function gh15316() { expectType(doc.toJSON({ virtuals: true }).upper); expectType(doc.toObject({ virtuals: true }).upper); } + +function gh13079() { + const schema = new Schema({ + name: { type: String, required: true } + }); + const TestModel = model('Test', schema); + + const doc = new TestModel({ name: 'taco' }); + expectType(doc.id); + + const schema2 = new Schema({ + id: { type: Number, required: true }, + name: { type: String, required: true } + }); + const TestModel2 = model('Test', schema2); + + const doc2 = new TestModel2({ name: 'taco' }); + expectType(doc2.id); + + const schema3 = new Schema<{ name: string }>({ + name: { type: String, required: true } + }); + const TestModel3 = model('Test', schema3); + + const doc3 = new TestModel3({ name: 'taco' }); + expectType(doc3.id); + + const schema4 = new Schema<{ name: string, id: number }>({ + id: { type: Number, required: true }, + name: { type: String, required: true } + }); + const TestModel4 = model('Test', schema4); + + const doc4 = new TestModel4({ name: 'taco' }); + expectType(doc4.id); + + const schema5 = new Schema({ + name: { type: String, required: true } + }, { id: false }); + const TestModel5 = model('Test', schema5); + + const doc5 = new TestModel5({ name: 'taco' }); + expectError(doc5.id); +} diff --git a/test/types/virtuals.test.ts b/test/types/virtuals.test.ts index 0ea393eae45..a60bf48c1f6 100644 --- a/test/types/virtuals.test.ts +++ b/test/types/virtuals.test.ts @@ -89,7 +89,7 @@ function gh11543() { async function autoTypedVirtuals() { type AutoTypedSchemaType = InferSchemaType; - type VirtualsType = { domain: string }; + type VirtualsType = { domain: string } & { id: string }; type InferredDocType = AutoTypedSchemaType & ObtainSchemaGeneric; const testSchema = new Schema({ diff --git a/types/document.d.ts b/types/document.d.ts index ea2798566c7..52a275edf91 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -166,9 +166,6 @@ declare module 'mongoose' { */ getChanges(): UpdateQuery; - /** The string version of this documents _id. */ - id?: any; - /** Signal that we desire an increment of this documents version. */ increment(): this; diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index e3c2f02baf8..efb36219672 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -63,7 +63,7 @@ declare module 'mongoose' { M: M; TInstanceMethods: TInstanceMethods; TQueryHelpers: TQueryHelpers; - TVirtuals: TVirtuals; + TVirtuals: (DocType extends { id: any } ? TVirtuals : TSchemaOptions extends { id: false } ? TVirtuals : TVirtuals & { id: string }); TStaticMethods: TStaticMethods; TSchemaOptions: TSchemaOptions; DocType: DocType; From f05e9cadd12ff8e9f3992c0ccf5903794ca93a20 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 8 Aug 2025 16:41:17 -0400 Subject: [PATCH 123/209] fix tests --- test/types/schema.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 7cbee93bab1..79793991464 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1273,7 +1273,7 @@ async function gh13797() { name: { type: String, required: function() { - expectType>(this); + expectAssignable>(this); return true; } } @@ -1282,7 +1282,7 @@ async function gh13797() { name: { type: String, default: function() { - expectType>(this); + expectAssignable>(this); return ''; } } From 4b6e55da2a57c20aa8dd6e3023ff5cd83914b6fe Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 8 Aug 2025 16:48:53 -0400 Subject: [PATCH 124/209] fix tests --- test/types.documentarray.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types.documentarray.test.js b/test/types.documentarray.test.js index 2acae98f083..c8e5fe72cc5 100644 --- a/test/types.documentarray.test.js +++ b/test/types.documentarray.test.js @@ -786,6 +786,6 @@ describe('types.documentarray', function() { someCustomOption: 'test 42' }] }); - assert.strictEqual(schema.path('docArr').$embeddedSchemaType.options.someCustomOption, 'test 42'); + assert.strictEqual(schema.path('docArr').getEmbeddedSchemaType().options.someCustomOption, 'test 42'); }); }); From cf8dbc0b49603b19340a2e1004f2da0c30b1d62d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 8 Aug 2025 16:52:38 -0400 Subject: [PATCH 125/209] types: add DefaultIdVirtual and AddDefaultId types re #15572, #13079 --- types/inferschematype.d.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index efb36219672..e82df4ac2fc 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -51,6 +51,9 @@ declare module 'mongoose' { */ export type InferSchemaType = IfAny>; + export type DefaultIdVirtual = { id: string }; + export type AddDefaultId = (DocType extends { id: any } ? TVirtuals : TSchemaOptions extends { id: false } ? TVirtuals : TVirtuals & { id: string }); + /** * @summary Obtains schema Generic type by using generic alias. * @param {TSchema} TSchema A generic of schema type instance. @@ -63,7 +66,7 @@ declare module 'mongoose' { M: M; TInstanceMethods: TInstanceMethods; TQueryHelpers: TQueryHelpers; - TVirtuals: (DocType extends { id: any } ? TVirtuals : TSchemaOptions extends { id: false } ? TVirtuals : TVirtuals & { id: string }); + TVirtuals: AddDefaultId; TStaticMethods: TStaticMethods; TSchemaOptions: TSchemaOptions; DocType: DocType; From 3ba44b195884cb7bb74a47156873923182489354 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 8 Aug 2025 16:55:49 -0400 Subject: [PATCH 126/209] docs: add note about id change --- docs/migrating_to_9.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 0868cdac09b..47a73fddd35 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -293,3 +293,18 @@ function findById(model: Model return model.find({_id: _id} as FilterQuery); // In Mongoose 8, this `as` was not required } ``` + +### Document `id` is no longer `any` + +In Mongoose 8 and earlier, `id` was a property on the `Document` class that was set to `any`. +This was inconsistent with runtime behavior, where `id` is a virtual property that returns `_id` as a string, _unless_ there is already an `id` property on the schema or the schema has the `id` option set to `false`. + +Mongoose 9 appends `id` as a string property to `TVirtuals`. The `Document` class no longer has an `id` property. + +```ts +const schema = new Schema({ age: Number }); +const TestModel = mongoose.model('Test', schema); + +const doc = new TestModel(); +doc.id; // 'string' in Mongoose 9, 'any' in Mongoose 8. +``` From aeceb7b6d02c47901b0baefc23578e92df2c3b2e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 8 Aug 2025 17:00:41 -0400 Subject: [PATCH 127/209] fix lint --- docs/migrating_to_9.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 47a73fddd35..524cde9b4e0 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -297,7 +297,7 @@ function findById(model: Model ### Document `id` is no longer `any` In Mongoose 8 and earlier, `id` was a property on the `Document` class that was set to `any`. -This was inconsistent with runtime behavior, where `id` is a virtual property that returns `_id` as a string, _unless_ there is already an `id` property on the schema or the schema has the `id` option set to `false`. +This was inconsistent with runtime behavior, where `id` is a virtual property that returns `_id` as a string, unless there is already an `id` property on the schema or the schema has the `id` option set to `false`. Mongoose 9 appends `id` as a string property to `TVirtuals`. The `Document` class no longer has an `id` property. From 1556421ed04466e09d9e32f85297607ddf4ab775 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 10 Aug 2025 18:05:59 -0400 Subject: [PATCH 128/209] BREAKING CHANGE: remove bson as direct dependency, use mongodb/lib/bson instead Fix #15154 --- lib/cast/bigint.js | 2 +- lib/cast/double.js | 2 +- lib/cast/uuid.js | 2 +- lib/helpers/clone.js | 2 +- lib/helpers/common.js | 2 +- lib/types/buffer.js | 20 ++++++++++---------- lib/types/decimal128.js | 2 +- lib/types/double.js | 2 +- lib/types/objectid.js | 2 +- lib/types/uuid.js | 2 +- lib/utils.js | 2 +- package.json | 1 - test/cast.test.js | 2 +- test/double.test.js | 2 +- test/encryptedSchema.test.js | 2 +- test/encryption/encryption.test.js | 2 +- test/int32.test.js | 2 +- test/query.test.js | 2 +- test/schema.uuid.test.js | 2 +- test/schematype.cast.test.js | 2 +- types/index.d.ts | 5 ++--- types/types.d.ts | 5 ++--- 22 files changed, 32 insertions(+), 35 deletions(-) diff --git a/lib/cast/bigint.js b/lib/cast/bigint.js index c046ba0f00a..fc98aeca37f 100644 --- a/lib/cast/bigint.js +++ b/lib/cast/bigint.js @@ -1,6 +1,6 @@ 'use strict'; -const { Long } = require('bson'); +const { Long } = require('mongodb/lib/bson'); /** * Given a value, cast it to a BigInt, or throw an `Error` if the value diff --git a/lib/cast/double.js b/lib/cast/double.js index 5dfc6c1a797..c3887c97b86 100644 --- a/lib/cast/double.js +++ b/lib/cast/double.js @@ -1,7 +1,7 @@ 'use strict'; const assert = require('assert'); -const BSON = require('bson'); +const BSON = require('mongodb/lib/bson'); const isBsonType = require('../helpers/isBsonType'); /** diff --git a/lib/cast/uuid.js b/lib/cast/uuid.js index 480f9e4e056..05b867c952e 100644 --- a/lib/cast/uuid.js +++ b/lib/cast/uuid.js @@ -1,6 +1,6 @@ 'use strict'; -const UUID = require('bson').UUID; +const UUID = require('mongodb/lib/bson').UUID; const UUID_FORMAT = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i; diff --git a/lib/helpers/clone.js b/lib/helpers/clone.js index e2638020ba3..8dd491249c0 100644 --- a/lib/helpers/clone.js +++ b/lib/helpers/clone.js @@ -11,7 +11,7 @@ const isObject = require('./isObject'); const isPOJO = require('./isPOJO'); const symbols = require('./symbols'); const trustedSymbol = require('./query/trusted').trustedSymbol; -const BSON = require('bson'); +const BSON = require('mongodb/lib/bson'); /** * Object clone with Mongoose natives support. diff --git a/lib/helpers/common.js b/lib/helpers/common.js index 5a1bee1c313..a9c45d50470 100644 --- a/lib/helpers/common.js +++ b/lib/helpers/common.js @@ -4,7 +4,7 @@ * Module dependencies. */ -const Binary = require('bson').Binary; +const Binary = require('mongodb/lib/bson').Binary; const isBsonType = require('./isBsonType'); const isMongooseObject = require('./isMongooseObject'); const MongooseError = require('../error'); diff --git a/lib/types/buffer.js b/lib/types/buffer.js index 57320904c2d..06b0611ac3b 100644 --- a/lib/types/buffer.js +++ b/lib/types/buffer.js @@ -4,8 +4,8 @@ 'use strict'; -const Binary = require('bson').Binary; -const UUID = require('bson').UUID; +const Binary = require('mongodb/lib/bson').Binary; +const UUID = require('mongodb/lib/bson').UUID; const utils = require('../utils'); /** @@ -169,14 +169,14 @@ utils.each( * * #### SubTypes: * - * const bson = require('bson') - * bson.BSON_BINARY_SUBTYPE_DEFAULT - * bson.BSON_BINARY_SUBTYPE_FUNCTION - * bson.BSON_BINARY_SUBTYPE_BYTE_ARRAY - * bson.BSON_BINARY_SUBTYPE_UUID - * bson.BSON_BINARY_SUBTYPE_MD5 - * bson.BSON_BINARY_SUBTYPE_USER_DEFINED - * doc.buffer.toObject(bson.BSON_BINARY_SUBTYPE_USER_DEFINED); + * const mongodb = require('mongodb') + * mongodb.BSON.BSON_BINARY_SUBTYPE_DEFAULT + * mongodb.BSON.BSON_BINARY_SUBTYPE_FUNCTION + * mongodb.BSON.BSON_BINARY_SUBTYPE_BYTE_ARRAY + * mongodb.BSON.BSON_BINARY_SUBTYPE_UUID + * mongodb.BSON.BSON_BINARY_SUBTYPE_MD5 + * mongodb.BSON.BSON_BINARY_SUBTYPE_USER_DEFINED + * doc.buffer.toObject(mongodb.BSON.BSON_BINARY_SUBTYPE_USER_DEFINED); * * @see bsonspec https://bsonspec.org/#/specification * @param {Hex} [subtype] diff --git a/lib/types/decimal128.js b/lib/types/decimal128.js index 1250b41a179..ab7b27b0a53 100644 --- a/lib/types/decimal128.js +++ b/lib/types/decimal128.js @@ -10,4 +10,4 @@ 'use strict'; -module.exports = require('bson').Decimal128; +module.exports = require('mongodb/lib/bson').Decimal128; diff --git a/lib/types/double.js b/lib/types/double.js index 6117173570b..65a38929493 100644 --- a/lib/types/double.js +++ b/lib/types/double.js @@ -10,4 +10,4 @@ 'use strict'; -module.exports = require('bson').Double; +module.exports = require('mongodb/lib/bson').Double; diff --git a/lib/types/objectid.js b/lib/types/objectid.js index d38c223659b..5544c243f6e 100644 --- a/lib/types/objectid.js +++ b/lib/types/objectid.js @@ -10,7 +10,7 @@ 'use strict'; -const ObjectId = require('bson').ObjectId; +const ObjectId = require('mongodb/lib/bson').ObjectId; const objectIdSymbol = require('../helpers/symbols').objectIdSymbol; /** diff --git a/lib/types/uuid.js b/lib/types/uuid.js index fc9db855f7d..382c93e5ffa 100644 --- a/lib/types/uuid.js +++ b/lib/types/uuid.js @@ -10,4 +10,4 @@ 'use strict'; -module.exports = require('bson').UUID; +module.exports = require('mongodb/lib/bson').UUID; diff --git a/lib/utils.js b/lib/utils.js index 4a0132ea18f..632fdd6bdbf 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,7 +4,7 @@ * Module dependencies. */ -const UUID = require('bson').UUID; +const UUID = require('mongodb/lib/bson').UUID; const ms = require('ms'); const mpath = require('mpath'); const ObjectId = require('./types/objectid'); diff --git a/package.json b/package.json index 183a3a0fa2c..ebf531083d7 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "type": "commonjs", "license": "MIT", "dependencies": { - "bson": "^6.10.4", "kareem": "git+https://github.com/mongoosejs/kareem.git#vkarpov15/v3", "mongodb": "~6.18.0", "mpath": "0.9.0", diff --git a/test/cast.test.js b/test/cast.test.js index 0ed0c8df9f7..d178b476243 100644 --- a/test/cast.test.js +++ b/test/cast.test.js @@ -9,7 +9,7 @@ require('./common'); const Schema = require('../lib/schema'); const assert = require('assert'); const cast = require('../lib/cast'); -const ObjectId = require('bson').ObjectId; +const ObjectId = require('mongodb/lib/bson').ObjectId; describe('cast: ', function() { describe('when casting an array', function() { diff --git a/test/double.test.js b/test/double.test.js index 6bf7e6c59e7..03ef4402fae 100644 --- a/test/double.test.js +++ b/test/double.test.js @@ -2,7 +2,7 @@ const assert = require('assert'); const start = require('./common'); -const BSON = require('bson'); +const BSON = require('mongodb/lib/bson'); const mongoose = start.mongoose; const Schema = mongoose.Schema; diff --git a/test/encryptedSchema.test.js b/test/encryptedSchema.test.js index 678041077ef..f13109074b3 100644 --- a/test/encryptedSchema.test.js +++ b/test/encryptedSchema.test.js @@ -3,7 +3,7 @@ const assert = require('assert'); const start = require('./common'); const { ObjectId, Decimal128 } = require('../lib/types'); -const { Double, Int32, UUID } = require('bson'); +const { Double, Int32, UUID } = require('mongodb/lib/bson'); const mongoose = start.mongoose; const Schema = mongoose.Schema; diff --git a/test/encryption/encryption.test.js b/test/encryption/encryption.test.js index 8d6b77b4c8e..3d178d32ac6 100644 --- a/test/encryption/encryption.test.js +++ b/test/encryption/encryption.test.js @@ -4,7 +4,7 @@ const assert = require('assert'); const mdb = require('mongodb'); const isBsonType = require('../../lib/helpers/isBsonType'); const { Schema, createConnection } = require('../../lib'); -const { ObjectId, Double, Int32, Decimal128 } = require('bson'); +const { ObjectId, Double, Int32, Decimal128 } = require('mongodb/lib/bson'); const fs = require('fs'); const mongoose = require('../../lib'); const { Map } = require('../../lib/types'); diff --git a/test/int32.test.js b/test/int32.test.js index d74c425eab5..08735aba810 100644 --- a/test/int32.test.js +++ b/test/int32.test.js @@ -2,7 +2,7 @@ const assert = require('assert'); const start = require('./common'); -const BSON = require('bson'); +const BSON = require('mongodb/lib/bson'); const sinon = require('sinon'); const mongoose = start.mongoose; diff --git a/test/query.test.js b/test/query.test.js index 2a8d419366c..14f78fdf7e4 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -6,7 +6,7 @@ const start = require('./common'); -const { EJSON } = require('bson'); +const { EJSON } = require('mongodb/lib/bson'); const Query = require('../lib/query'); const assert = require('assert'); const util = require('./util'); diff --git a/test/schema.uuid.test.js b/test/schema.uuid.test.js index e95424dc137..6819b562ccc 100644 --- a/test/schema.uuid.test.js +++ b/test/schema.uuid.test.js @@ -4,7 +4,7 @@ const start = require('./common'); const util = require('./util'); const assert = require('assert'); -const bson = require('bson'); +const bson = require('mongodb/lib/bson'); const { randomUUID } = require('crypto'); const mongoose = start.mongoose; diff --git a/test/schematype.cast.test.js b/test/schematype.cast.test.js index 77f28e2d9a7..6c7fddbd4c4 100644 --- a/test/schematype.cast.test.js +++ b/test/schematype.cast.test.js @@ -2,7 +2,7 @@ require('./common'); -const ObjectId = require('bson').ObjectId; +const ObjectId = require('mongodb/lib/bson').ObjectId; const Schema = require('../lib/schema'); const assert = require('assert'); diff --git a/types/index.d.ts b/types/index.d.ts index f3256866497..ce59c4f0e0a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -32,7 +32,6 @@ declare module 'mongoose' { import events = require('events'); import mongodb = require('mongodb'); import mongoose = require('mongoose'); - import bson = require('bson'); export type Mongoose = typeof mongoose; @@ -827,14 +826,14 @@ declare module 'mongoose' { /** * Converts any Buffer properties into "{ type: 'buffer', data: [1, 2, 3] }" format for JSON serialization */ - export type UUIDToJSON = T extends bson.UUID + export type UUIDToJSON = T extends mongodb.UUID ? string : T extends Document ? T : T extends TreatAsPrimitives ? T : T extends Record ? { - [K in keyof T]: T[K] extends bson.UUID + [K in keyof T]: T[K] extends mongodb.UUID ? string : T[K] extends Types.DocumentArray ? Types.DocumentArray> diff --git a/types/types.d.ts b/types/types.d.ts index c29da93be23..3bb3816d027 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -1,7 +1,6 @@ declare module 'mongoose' { import mongodb = require('mongodb'); - import bson = require('bson'); class NativeBuffer extends Buffer {} @@ -103,8 +102,8 @@ declare module 'mongoose' { parentArray(): Types.DocumentArray; } - class UUID extends bson.UUID {} + class UUID extends mongodb.UUID {} - class Double extends bson.Double {} + class Double extends mongodb.Double {} } } From be265784a1a4a21cb6f76829d8e95872ce1fb8b5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 12 Aug 2025 16:58:13 -0400 Subject: [PATCH 129/209] test: make test more resilient --- test/model.discriminator.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/model.discriminator.test.js b/test/model.discriminator.test.js index 92319e550ac..cde832acc04 100644 --- a/test/model.discriminator.test.js +++ b/test/model.discriminator.test.js @@ -1626,9 +1626,9 @@ describe('model', function() { const post = await Post.create({}); - await UserWithPost.create({ postId: post._id }); + const { _id } = await UserWithPost.create({ postId: post._id }); - const user = await User.findOne().populate({ path: 'post' }); + const user = await User.findOne({ _id }).populate({ path: 'post' }); assert.ok(user.postId); }); From 6ad040ae4cadfb1a8b19f35a7d4b33b2bbfc3529 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 16 Aug 2025 14:34:19 -0400 Subject: [PATCH 130/209] BREAKING CHANGE: disallow update pipelines by default, require updatePipeline option Fix #14424 --- lib/query.js | 52 +++++++++++++++++++++--------------- test/model.updateOne.test.js | 19 +++++++++---- test/timestamps.test.js | 4 +-- types/query.d.ts | 8 +++++- 4 files changed, 52 insertions(+), 31 deletions(-) diff --git a/lib/query.js b/lib/query.js index 1f195df5370..d2406a5e374 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1748,6 +1748,10 @@ Query.prototype.setOptions = function(options, overwrite) { this._mongooseOptions.overwriteImmutable = options.overwriteImmutable; delete options.overwriteImmutable; } + if ('updatePipeline' in options) { + this._mongooseOptions.updatePipeline = options.updatePipeline; + delete options.updatePipeline; + } if ('sanitizeProjection' in options) { if (options.sanitizeProjection && !this._mongooseOptions.sanitizeProjection) { sanitizeProjection(this._fields); @@ -3385,7 +3389,7 @@ function prepareDiscriminatorCriteria(query) { * @memberOf Query * @instance * @param {Object|Query} [filter] - * @param {Object} [doc] + * @param {Object} [update] * @param {Object} [options] * @param {Boolean} [options.includeResultMetadata] if true, returns the full [ModifyResult from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) rather than just the document * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) @@ -3407,9 +3411,9 @@ function prepareDiscriminatorCriteria(query) { * @api public */ -Query.prototype.findOneAndUpdate = function(filter, doc, options) { +Query.prototype.findOneAndUpdate = function(filter, update, options) { if (typeof filter === 'function' || - typeof doc === 'function' || + typeof update === 'function' || typeof options === 'function' || typeof arguments[3] === 'function') { throw new MongooseError('Query.prototype.findOneAndUpdate() no longer accepts a callback'); @@ -3423,7 +3427,7 @@ Query.prototype.findOneAndUpdate = function(filter, doc, options) { options = undefined; break; case 1: - doc = filter; + update = filter; filter = options = undefined; break; } @@ -3436,11 +3440,6 @@ Query.prototype.findOneAndUpdate = function(filter, doc, options) { ); } - // apply doc - if (doc) { - this._mergeUpdate(doc); - } - options = options ? clone(options) : {}; if (options.projection) { @@ -3463,6 +3462,11 @@ Query.prototype.findOneAndUpdate = function(filter, doc, options) { this.setOptions(options); + // apply doc + if (update) { + this._mergeUpdate(update); + } + return this; }; @@ -3997,39 +4001,43 @@ function _completeManyLean(schema, docs, path, opts) { * Override mquery.prototype._mergeUpdate to handle mongoose objects in * updates. * - * @param {Object} doc + * @param {Object} update * @method _mergeUpdate * @memberOf Query * @instance * @api private */ -Query.prototype._mergeUpdate = function(doc) { +Query.prototype._mergeUpdate = function(update) { + const updatePipeline = this._mongooseOptions.updatePipeline; + if (!updatePipeline && Array.isArray(update)) { + throw new MongooseError('Cannot pass an array to query updates unless the `updatePipeline` option is set.'); + } if (!this._update) { - this._update = Array.isArray(doc) ? [] : {}; + this._update = Array.isArray(update) ? [] : {}; } - if (doc == null || (typeof doc === 'object' && Object.keys(doc).length === 0)) { + if (update == null || (typeof update === 'object' && Object.keys(update).length === 0)) { return; } - if (doc instanceof Query) { + if (update instanceof Query) { if (Array.isArray(this._update)) { - throw new Error('Cannot mix array and object updates'); + throw new MongooseError('Cannot mix array and object updates'); } - if (doc._update) { - utils.mergeClone(this._update, doc._update); + if (update._update) { + utils.mergeClone(this._update, update._update); } - } else if (Array.isArray(doc)) { + } else if (Array.isArray(update)) { if (!Array.isArray(this._update)) { - throw new Error('Cannot mix array and object updates'); + throw new MongooseError('Cannot mix array and object updates'); } - this._update = this._update.concat(doc); + this._update = this._update.concat(update); } else { if (Array.isArray(this._update)) { - throw new Error('Cannot mix array and object updates'); + throw new MongooseError('Cannot mix array and object updates'); } - utils.mergeClone(this._update, doc); + utils.mergeClone(this._update, update); } }; diff --git a/test/model.updateOne.test.js b/test/model.updateOne.test.js index ccb7db6ab35..061fbbd51f3 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -2766,10 +2766,16 @@ describe('model: updateOne: ', function() { const Model = db.model('Test', schema); await Model.create({ oldProp: 'test' }); + + assert.throws( + () => Model.updateOne({}, [{ $set: { newProp: 'test2' } }]), + /Cannot pass an array to query updates unless the `updatePipeline` option is set/ + ); + await Model.updateOne({}, [ { $set: { newProp: 'test2' } }, { $unset: ['oldProp'] } - ]); + ], { updatePipeline: true }); let doc = await Model.findOne(); assert.equal(doc.newProp, 'test2'); assert.strictEqual(doc.oldProp, void 0); @@ -2778,7 +2784,7 @@ describe('model: updateOne: ', function() { await Model.updateOne({}, [ { $addFields: { oldProp: 'test3' } }, { $project: { newProp: 0 } } - ]); + ], { updatePipeline: true }); doc = await Model.findOne(); assert.equal(doc.oldProp, 'test3'); assert.strictEqual(doc.newProp, void 0); @@ -2792,7 +2798,7 @@ describe('model: updateOne: ', function() { await Model.updateOne({}, [ { $set: { newProp: 'test2' } }, { $unset: 'oldProp' } - ]); + ], { updatePipeline: true }); const doc = await Model.findOne(); assert.equal(doc.newProp, 'test2'); assert.strictEqual(doc.oldProp, void 0); @@ -2805,8 +2811,11 @@ describe('model: updateOne: ', function() { const updatedAt = cat.updatedAt; await new Promise(resolve => setTimeout(resolve), 50); - const updated = await Cat.findOneAndUpdate({ _id: cat._id }, - [{ $set: { name: 'Raikou' } }], { new: true }); + const updated = await Cat.findOneAndUpdate( + { _id: cat._id }, + [{ $set: { name: 'Raikou' } }], + { new: true, updatePipeline: true } + ); assert.ok(updated.updatedAt.getTime() > updatedAt.getTime()); }); }); diff --git a/test/timestamps.test.js b/test/timestamps.test.js index bcb4a482836..ef4df2ecf01 100644 --- a/test/timestamps.test.js +++ b/test/timestamps.test.js @@ -1034,9 +1034,7 @@ describe('timestamps', function() { sub: { subName: 'John' } }); await doc.save(); - await Test.updateMany({}, [{ $set: { updateCounter: 1 } }]); - // oddly enough, the null property is not accessible. Doing check.null doesn't return anything even though - // if you were to console.log() the output of a findOne you would be able to see it. This is the workaround. + await Test.updateMany({}, [{ $set: { updateCounter: 1 } }], { updatePipeline: true }); const test = await Test.countDocuments({ null: { $exists: true } }); assert.equal(test, 0); // now we need to make sure that the solution didn't prevent the updateCounter addition diff --git a/types/query.d.ts b/types/query.d.ts index 6d7c7e0774e..ae65340f09e 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -43,7 +43,8 @@ declare module 'mongoose' { | 'setDefaultsOnInsert' | 'strict' | 'strictQuery' - | 'translateAliases'; + | 'translateAliases' + | 'updatePipeline'; type MongooseBaseQueryOptions = Pick, MongooseBaseQueryOptionKeys>; @@ -219,6 +220,11 @@ declare module 'mongoose' { translateAliases?: boolean; upsert?: boolean; useBigInt64?: boolean; + /** + * Set to true to allow passing in an update pipeline instead of an update document. + * Mongoose disallows update pipelines by default because Mongoose does not cast update pipelines. + */ + updatePipeline?: boolean; writeConcern?: mongodb.WriteConcern; [other: string]: any; From ffb1f410c98fd636afebe3d222d1385e36873db8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 16 Aug 2025 14:39:05 -0400 Subject: [PATCH 131/209] docs(migrating_to_9): add note about updatePipeline change --- docs/migrating_to_9.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 524cde9b4e0..1ac8d33a448 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -68,6 +68,23 @@ schema.pre('save', function(next, arg) { In Mongoose 9, `next(null, 'new arg')` doesn't overwrite the args to the next middleware. +## Update pipelines disallowed by default + +As of MongoDB 4.2, you can pass an array of pipeline stages to `updateOne()`, `updateMany()`, and `findOneAndUpdate()` to modify the document in multiple stages. +Mongoose does not cast update pipelines at all, so for Mongoose 9 we've made using update pipelines throw an error by default. + +```javascript +// Throws in Mongoose 9. Works in Mongoose 8 +await Model.updateOne({}, [{ $set: { newProp: 'test2' } }]); +``` + +Set `updatePipeline: true` to enable update pipelines. + +```javascript +// Works in Mongoose 9 +await Model.updateOne({}, [{ $set: { newProp: 'test2' } }], { updatePipeline: true }); +``` + ## Removed background option for indexes [MongoDB no longer supports the `background` option for indexes as of MongoDB 4.2](https://www.mongodb.com/docs/manual/core/index-creation/#index-operations). Mongoose 9 will no longer set the background option by default and Mongoose 9 no longer supports setting the `background` option on `Schema.prototype.index()`. From d11cf04cc9c2f8e0bb040d6d727e80f3175fbc5f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 16 Aug 2025 16:28:47 -0400 Subject: [PATCH 132/209] BREAKING CHANGE: make create() and insertOne() params more strict, remove generics to prevent type inference Fix #15355 --- docs/migrating_to_9.md | 18 ++++++++++++++++++ test/types/connection.test.ts | 4 ++-- test/types/create.test.ts | 36 +++++++++++++++++++++++++++++++---- test/types/document.test.ts | 2 +- test/types/models.test.ts | 4 +--- types/models.d.ts | 18 +++++++++++++----- 6 files changed, 67 insertions(+), 15 deletions(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 524cde9b4e0..a111cca9842 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -294,6 +294,24 @@ function findById(model: Model } ``` +### No more generic parameter for `create()` and `insertOne()` + +In Mongoose 8, `create()` and `insertOne()` accepted a generic parameter, which meant TypeScript let you pass any value to the function. + +```ts +const schema = new Schema({ age: Number }); +const TestModel = mongoose.model('Test', schema); + +// Worked in Mongoose 8, TypeScript error in Mongoose 9 +const doc = await TestModel.create({ age: 'not a number', someOtherProperty: 'value' }); +``` + +In Mongoose 9, `create()` and `insertOne()` no longer accept a generic parameter. Instead, they accept `Partial` with some additional query casting applied that allows objects for maps, strings for ObjectIds, and POJOs for subdocuments and document arrays. + +```ts +const doc = await TestModel.create({ age: 'not a number', someOtherProperty: 'value' } as unknown as Partial>); +``` + ### Document `id` is no longer `any` In Mongoose 8 and earlier, `id` was a property on the `Document` class that was set to `any`. diff --git a/test/types/connection.test.ts b/test/types/connection.test.ts index 1e9b792d131..79954666aa0 100644 --- a/test/types/connection.test.ts +++ b/test/types/connection.test.ts @@ -1,4 +1,4 @@ -import { createConnection, Schema, Collection, Connection, ConnectionSyncIndexesResult, Model, connection, HydratedDocument, Query } from 'mongoose'; +import { createConnection, Schema, Collection, Connection, ConnectionSyncIndexesResult, InferSchemaType, Model, connection, HydratedDocument, Query } from 'mongoose'; import * as mongodb from 'mongodb'; import { expectAssignable, expectError, expectType } from 'tsd'; import { AutoTypedSchemaType, autoTypedSchema } from './schema.test'; @@ -93,7 +93,7 @@ export function autoTypedModelConnection() { (async() => { // Model-functions-test // Create should works with arbitrary objects. - const randomObject = await AutoTypedModel.create({ unExistKey: 'unExistKey', description: 'st' }); + const randomObject = await AutoTypedModel.create({ unExistKey: 'unExistKey', description: 'st' } as Partial>); expectType(randomObject.userName); const testDoc1 = await AutoTypedModel.create({ userName: 'M0_0a' }); diff --git a/test/types/create.test.ts b/test/types/create.test.ts index 618ce84a1cf..6a6aaf8b27b 100644 --- a/test/types/create.test.ts +++ b/test/types/create.test.ts @@ -48,10 +48,8 @@ Test.create([{}]).then(docs => { expectType(docs[0].name); }); -expectError(Test.create({})); - -Test.create({ name: 'test' }); -Test.create({ _id: new Types.ObjectId('0'.repeat(24)), name: 'test' }); +Test.create({ name: 'test' }); +Test.create({ _id: new Types.ObjectId('0'.repeat(24)), name: 'test' }); Test.insertMany({ name: 'test' }, {}).then(docs => { expectType(docs[0]._id); @@ -137,4 +135,34 @@ async function createWithAggregateErrors() { expectType<(HydratedDocument | Error)[]>(await Test.create([{}], { aggregateErrors: true })); } +async function createWithSubdoc() { + const schema = new Schema({ name: String, subdoc: new Schema({ prop: { type: String, required: true } }) }); + const TestModel = model('Test', schema); + const doc = await TestModel.create({ name: 'test', subdoc: { prop: 'value' } }); + expectType(doc.name); + expectType(doc.subdoc!.prop); +} + +async function createWithDocArray() { + const schema = new Schema({ name: String, subdocs: [new Schema({ prop: { type: String, required: true } })] }); + const TestModel = model('Test', schema); + const doc = await TestModel.create({ name: 'test', subdocs: [{ prop: 'value' }] }); + expectType(doc.name); + expectType(doc.subdocs[0].prop); +} + +async function createWithMapOfSubdocs() { + const schema = new Schema({ + name: String, + subdocMap: { + type: Map, + of: new Schema({ prop: { type: String, required: true } }) + } + }); + const TestModel = model('Test', schema); + const doc = await TestModel.create({ name: 'test', subdocMap: { taco: { prop: 'beef' } } }); + expectType(doc.name); + expectType(doc.subdocMap!.get('taco')!.prop); +} + createWithAggregateErrors(); diff --git a/test/types/document.test.ts b/test/types/document.test.ts index eb0857fe135..b4668fe104f 100644 --- a/test/types/document.test.ts +++ b/test/types/document.test.ts @@ -137,7 +137,7 @@ async function gh11117(): Promise { const fooModel = model('foos', fooSchema); - const items = await fooModel.create([ + const items = await fooModel.create([ { someId: new Types.ObjectId(), someDate: new Date(), diff --git a/test/types/models.test.ts b/test/types/models.test.ts index f2e72bf8d7e..5b5eb194613 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -330,8 +330,6 @@ async function gh12277() { } async function overwriteBulkWriteContents() { - type DocumentType = Document & T; - interface BaseModelClassDoc { firstname: string; } @@ -380,7 +378,7 @@ export function autoTypedModel() { (async() => { // Model-functions-test // Create should works with arbitrary objects. - const randomObject = await AutoTypedModel.create({ unExistKey: 'unExistKey', description: 'st' }); + const randomObject = await AutoTypedModel.create({ unExistKey: 'unExistKey', description: 'st' } as Partial>); expectType(randomObject.userName); const testDoc1 = await AutoTypedModel.create({ userName: 'M0_0a' }); diff --git a/types/models.d.ts b/types/models.d.ts index 5dad52a994a..8c0c532745c 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -260,6 +260,14 @@ declare module 'mongoose' { hint?: mongodb.Hint; } + type ApplyBasicCreateCasting = { + [K in keyof T]: NonNullable extends Map + ? (Record | T[K]) + : NonNullable extends Types.DocumentArray + ? RawSubdocType[] | T[K] + : QueryTypeCasting; + }; + /** * Models are fancy constructors compiled from `Schema` definitions. * An instance of a model is called a document. @@ -344,10 +352,10 @@ declare module 'mongoose' { >; /** Creates a new document or documents */ - create>(docs: Array, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>; - create>(docs: Array, options?: CreateOptions): Promise; - create>(doc: DocContents | TRawDocType): Promise; - create>(...docs: Array): Promise; + create(docs: Array>>, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>; + create(docs: Array>>, options?: CreateOptions): Promise; + create(doc: Partial>): Promise; + create(...docs: Array>>): Promise; /** * Create the collection for this model. By default, if no indexes are specified, @@ -616,7 +624,7 @@ declare module 'mongoose' { * `MyModel.insertOne(obj, options)` is almost equivalent to `new MyModel(obj).save(options)`. * The difference is that `insertOne()` checks if `obj` is already a document, and checks for discriminators. */ - insertOne>(doc: DocContents | TRawDocType, options?: SaveOptions): Promise; + insertOne(doc: Partial>, options?: SaveOptions): Promise; /** * List all [Atlas search indexes](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) on this model's collection. From 0bfb74735400beb744cd69d441018093cea884d9 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 17 Aug 2025 10:36:30 -0400 Subject: [PATCH 133/209] types: add numbers/strings for dates and arrays of arrays for maps to create casting Fix #15355 --- test/types/create.test.ts | 31 +++++++++++++++++++++++++++++-- types/models.d.ts | 20 +++++++++++++++----- types/query.d.ts | 5 ++++- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/test/types/create.test.ts b/test/types/create.test.ts index 6a6aaf8b27b..51ea1e8ddaf 100644 --- a/test/types/create.test.ts +++ b/test/types/create.test.ts @@ -136,10 +136,11 @@ async function createWithAggregateErrors() { } async function createWithSubdoc() { - const schema = new Schema({ name: String, subdoc: new Schema({ prop: { type: String, required: true } }) }); + const schema = new Schema({ name: String, registeredAt: Date, subdoc: new Schema({ prop: { type: String, required: true } }) }); const TestModel = model('Test', schema); - const doc = await TestModel.create({ name: 'test', subdoc: { prop: 'value' } }); + const doc = await TestModel.create({ name: 'test', registeredAt: '2022-06-01', subdoc: { prop: 'value' } }); expectType(doc.name); + expectType(doc.registeredAt); expectType(doc.subdoc!.prop); } @@ -160,9 +161,35 @@ async function createWithMapOfSubdocs() { } }); const TestModel = model('Test', schema); + const doc = await TestModel.create({ name: 'test', subdocMap: { taco: { prop: 'beef' } } }); expectType(doc.name); expectType(doc.subdocMap!.get('taco')!.prop); + + const doc2 = await TestModel.create({ name: 'test', subdocMap: [['taco', { prop: 'beef' }]] }); + expectType(doc2.name); + expectType(doc2.subdocMap!.get('taco')!.prop); +} + +async function createWithRawDocTypeNo_id() { + interface RawDocType { + name: string; + registeredAt: Date; + } + + const schema = new Schema({ + name: String, + registeredAt: Date + }); + const TestModel = model('Test', schema); + + const doc = await TestModel.create({ _id: '0'.repeat(24), name: 'test' }); + expectType(doc.name); + expectType(doc._id); + + const doc2 = await TestModel.create({ name: 'test', _id: new Types.ObjectId() }); + expectType(doc2.name); + expectType(doc2._id); } createWithAggregateErrors(); diff --git a/types/models.d.ts b/types/models.d.ts index 8c0c532745c..9942d07cd3c 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -260,9 +260,19 @@ declare module 'mongoose' { hint?: mongodb.Hint; } + /* + * Apply common casting logic to the given type, allowing: + * - strings for ObjectIds + * - strings and numbers for Dates + * - strings for Buffers + * - strings for UUIDs + * - POJOs for subdocuments + * - vanilla arrays of POJOs for document arrays + * - POJOs and array of arrays for maps + */ type ApplyBasicCreateCasting = { [K in keyof T]: NonNullable extends Map - ? (Record | T[K]) + ? (Record | Array<[KeyType, ValueType]> | T[K]) : NonNullable extends Types.DocumentArray ? RawSubdocType[] | T[K] : QueryTypeCasting; @@ -352,10 +362,10 @@ declare module 'mongoose' { >; /** Creates a new document or documents */ - create(docs: Array>>, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>; - create(docs: Array>>, options?: CreateOptions): Promise; - create(doc: Partial>): Promise; - create(...docs: Array>>): Promise; + create(docs: Array>>>, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>; + create(docs: Array>>>, options?: CreateOptions): Promise; + create(doc: Partial>>): Promise; + create(...docs: Array>>>): Promise; /** * Create the collection for this model. By default, if no indexes are specified, diff --git a/types/query.d.ts b/types/query.d.ts index 6d7c7e0774e..eb56d36613f 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -3,6 +3,7 @@ declare module 'mongoose' { type StringQueryTypeCasting = string | RegExp; type ObjectIdQueryTypeCasting = Types.ObjectId | string; + type DateQueryTypeCasting = string | number; type UUIDQueryTypeCasting = Types.UUID | string; type BufferQueryCasting = Buffer | mongodb.Binary | number[] | string | { $binary: string | mongodb.Binary }; type QueryTypeCasting = T extends string @@ -13,7 +14,9 @@ declare module 'mongoose' { ? UUIDQueryTypeCasting : T extends Buffer ? BufferQueryCasting - : T; + : NonNullable extends Date + ? DateQueryTypeCasting | T + : T; export type ApplyBasicQueryCasting = T | T[] | (T extends (infer U)[] ? QueryTypeCasting : T); From 2516a9875f8e605f73ff759ff9ecc82e53efed96 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 17 Aug 2025 10:38:42 -0400 Subject: [PATCH 134/209] docs(migrating_to_9): quick note to provide context for code sample --- docs/migrating_to_9.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index a111cca9842..1854abc9c85 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -308,6 +308,8 @@ const doc = await TestModel.create({ age: 'not a number', someOtherProperty: 'va In Mongoose 9, `create()` and `insertOne()` no longer accept a generic parameter. Instead, they accept `Partial` with some additional query casting applied that allows objects for maps, strings for ObjectIds, and POJOs for subdocuments and document arrays. +If your parameters to `create()` don't match `Partial`, you can use `as` to cast as follows. + ```ts const doc = await TestModel.create({ age: 'not a number', someOtherProperty: 'value' } as unknown as Partial>); ``` From 9c0b30553cf231c3f118ba26cc094bf69fc5c0ee Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 18 Aug 2025 13:55:22 -0400 Subject: [PATCH 135/209] BREAKING CHANGE: rename FilterQuery -> QueryFilter, add 1-level deep nested paths to QueryFilter Fix #12064 --- docs/migrating_to_9.md | 12 ++++-- test/types/models.test.ts | 18 +++++--- test/types/queries.test.ts | 48 +++++++++++++++------ test/types/sanitizeFilter.test.ts | 4 +- types/index.d.ts | 4 +- types/models.d.ts | 72 +++++++++++++++---------------- types/pipelinestage.d.ts | 2 +- types/query.d.ts | 63 +++++++++++++-------------- types/utility.d.ts | 51 +++++++++++++++------- 9 files changed, 162 insertions(+), 112 deletions(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 1ac8d33a448..5f09c05a75e 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -287,9 +287,13 @@ In Mongoose 8, there was also an internal `$embeddedSchemaType` property. That p ## TypeScript -### FilterQuery Properties No Longer Resolve to any +### FilterQuery renamed to QueryFilter -In Mongoose 9, the `FilterQuery` type, which is the type of the first param to `Model.find()`, `Model.findOne()`, etc. now enforces stronger types for top-level keys. +In Mongoose 9, `FilterQuery` (the first parameter to `Model.find()`, `Model.findOne()`, etc.) was renamed to `QueryFilter`. + +### QueryFilter Properties No Longer Resolve to any + +In Mongoose 9, the `QueryFilter` type, which is the type of the first param to `Model.find()`, `Model.findOne()`, etc. now enforces stronger types for top-level keys. ```typescript const schema = new Schema({ age: Number }); @@ -300,14 +304,14 @@ TestModel.find({ age: { $notAnOperator: 42 } }); // Works in Mongoose 8, TS erro ``` This change is backwards breaking if you use generics when creating queries as shown in the following example. -If you run into the following issue or any similar issues, you can use `as FilterQuery`. +If you run into the following issue or any similar issues, you can use `as QueryFilter`. ```typescript // From https://stackoverflow.com/questions/56505560/how-to-fix-ts2322-could-be-instantiated-with-a-different-subtype-of-constraint: // "Never assign a concrete type to a generic type parameter, consider it as read-only!" // This function is generally something you shouldn't do in TypeScript, can work around it with `as` though. function findById(model: Model, _id: Types.ObjectId | string) { - return model.find({_id: _id} as FilterQuery); // In Mongoose 8, this `as` was not required + return model.find({_id: _id} as QueryFilter); // In Mongoose 8, this `as` was not required } ``` diff --git a/test/types/models.test.ts b/test/types/models.test.ts index f2e72bf8d7e..fa500548e04 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -940,8 +940,8 @@ async function gh12064() { function testWithLevel1NestedPaths() { type Test1 = WithLevel1NestedPaths<{ topLevel: number, - nested1Level: { - l2: string + nested1Level?: { + l2?: string | null | undefined }, nested2Level: { l2: { l3: boolean } @@ -950,8 +950,8 @@ function testWithLevel1NestedPaths() { expectType<{ topLevel: number, - nested1Level: { l2: string }, - 'nested1Level.l2': string, + nested1Level: { l2?: string | null | undefined }, + 'nested1Level.l2': string | null | undefined, nested2Level: { l2: { l3: boolean } }, 'nested2Level.l2': { l3: boolean } }>({} as Test1); @@ -968,11 +968,15 @@ function testWithLevel1NestedPaths() { type InferredDocType = InferSchemaType; type Test2 = WithLevel1NestedPaths; - expectAssignable<{ - _id: string | null | undefined, - foo?: { one?: string | null | undefined } | null | undefined, + expectType<{ + _id: string, + foo: { one?: string | null | undefined }, 'foo.one': string | null | undefined }>({} as Test2); + expectType({} as Test2['_id']); + expectType<{ one?: string | null | undefined }>({} as Test2['foo']); + expectType({} as Test2['foo.one']); + expectType<'_id' | 'foo' | 'foo.one'>({} as keyof Test2); } async function gh14802() { diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index bfced89e245..d101c911fe1 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -9,7 +9,6 @@ import { Model, QueryWithHelpers, PopulatedDoc, - FilterQuery, UpdateQuery, UpdateQueryKnownOnly, QuerySelector, @@ -17,8 +16,10 @@ import { InferSchemaType, ProjectionFields, QueryOptions, - ProjectionType + ProjectionType, + QueryFilter } from 'mongoose'; +import mongoose from 'mongoose'; import { ModifyResult, ObjectId } from 'mongodb'; import { expectAssignable, expectError, expectNotAssignable, expectType } from 'tsd'; import { autoTypedModel } from './models.test'; @@ -70,6 +71,9 @@ interface ITest { endDate?: Date; } +type X = mongoose.WithLevel1NestedPaths; +expectType({} as X['docs.id']); + const Test = model>('Test', schema); Test.find({}, {}, { populate: { path: 'child', model: ChildModel, match: true } }).exec().then((res: Array) => console.log(res)); @@ -210,13 +214,13 @@ expectError(Test.find().sort(['invalid'])); // Super generic query function testGenericQuery(): void { - interface CommonInterface extends Document { + interface CommonInterface { something: string; content: T; } async function findSomething(model: Model>): Promise> { - return model.findOne({ something: 'test' }).orFail().exec(); + return model.findOne({ something: 'test' } as mongoose.QueryFilter>).orFail().exec(); } } @@ -257,7 +261,7 @@ function gh10757() { type MyClassDocument = MyClass & Document; - const test: FilterQuery = { status: { $in: [MyEnum.VALUE1, MyEnum.VALUE2] } }; + const test: QueryFilter = { status: { $in: [MyEnum.VALUE1, MyEnum.VALUE2] } }; } function gh10857() { @@ -266,7 +270,7 @@ function gh10857() { status: MyUnion; } type MyClassDocument = MyClass & Document; - const test: FilterQuery = { status: { $in: ['VALUE1', 'VALUE2'] } }; + const test: QueryFilter = { status: { $in: ['VALUE1', 'VALUE2'] } }; } function gh10786() { @@ -352,7 +356,7 @@ function gh11964() { // `as` is necessary because `T` can be `{ id: never }`, // so we need to explicitly coerce - const filter: FilterQuery = { id } as FilterQuery; + const filter: QueryFilter = { id } as QueryFilter; } } } @@ -370,7 +374,7 @@ function gh14397() { const id = 'Test Id'; let idCondition: Condition['id']>; - let filter: FilterQuery>; + let filter: QueryFilter>; expectAssignable(id); expectAssignable({ id }); @@ -510,7 +514,7 @@ async function gh13142() { Projection extends ProjectionFields, Options extends QueryOptions >( - filter: FilterQuery, + filter: QueryFilter>, projection: Projection, options: Options ): Promise< @@ -642,8 +646,8 @@ function gh14473() { } const generateExists = () => { - const query: FilterQuery = { deletedAt: { $ne: null } }; - const query2: FilterQuery = { deletedAt: { $lt: new Date() } } as FilterQuery; + const query: QueryFilter = { deletedAt: { $ne: null } }; + const query2: QueryFilter = { deletedAt: { $lt: new Date() } } as QueryFilter; }; } @@ -707,7 +711,7 @@ async function gh14545() { } function gh14841() { - const filter: FilterQuery<{ owners: string[] }> = { + const filter: QueryFilter<{ owners: string[] }> = { $expr: { $lt: [{ $size: '$owners' }, 10] } }; } @@ -717,7 +721,7 @@ function gh14510() { // "Never assign a concrete type to a generic type parameter, consider it as read-only!" // This function is generally something you shouldn't do in TypeScript, can work around it with `as` though. function findById(model: Model, _id: Types.ObjectId | string) { - return model.find({ _id: _id } as FilterQuery); + return model.find({ _id: _id } as QueryFilter); } } @@ -779,3 +783,21 @@ async function gh3230() { console.log(await Test.findById(test._id).populate('arr.testRef', { name: 1, prop: 1, _id: 0, __t: 0 })); } + +async function gh12064() { + const schema = new Schema({ + subdoc: new Schema({ + subdocProp: Number + }), + nested: { + nestedProp: String + }, + documentArray: [{ documentArrayProp: Boolean }] + }); + const TestModel = model('Model', schema); + + await TestModel.findOne({ 'subdoc.subdocProp': { $gt: 0 }, 'nested.nestedProp': { $in: ['foo', 'bar'] }, 'documentArray.documentArrayProp': { $ne: true } }); + expectError(TestModel.findOne({ 'subdoc.subdocProp': 'taco tuesday' })); + expectError(TestModel.findOne({ 'nested.nestedProp': true })); + expectError(TestModel.findOne({ 'documentArray.documentArrayProp': 'taco' })); +} diff --git a/test/types/sanitizeFilter.test.ts b/test/types/sanitizeFilter.test.ts index 8028e5850a6..234e9016b82 100644 --- a/test/types/sanitizeFilter.test.ts +++ b/test/types/sanitizeFilter.test.ts @@ -1,7 +1,7 @@ -import { FilterQuery, sanitizeFilter } from 'mongoose'; +import { QueryFilter, sanitizeFilter } from 'mongoose'; import { expectType } from 'tsd'; const data = { username: 'val', pwd: { $ne: null } }; type Data = typeof data; -expectType>(sanitizeFilter(data)); +expectType>(sanitizeFilter(data)); diff --git a/types/index.d.ts b/types/index.d.ts index d5863fd5edb..6edcd7f6f81 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -64,7 +64,7 @@ declare module 'mongoose' { * Sanitizes query filters against query selector injection attacks by wrapping * any nested objects that have a property whose name starts with `$` in a `$eq`. */ - export function sanitizeFilter(filter: FilterQuery): FilterQuery; + export function sanitizeFilter(filter: QueryFilter): QueryFilter; /** Gets mongoose options */ export function get(key: K): MongooseOptions[K]; @@ -647,7 +647,7 @@ declare module 'mongoose' { count?: boolean; /** Add an extra match condition to `populate()`. */ - match?: FilterQuery | ((doc: Record, virtual?: this) => Record | null); + match?: QueryFilter | ((doc: Record, virtual?: this) => Record | null); /** Add a default `limit` to the `populate()` query. */ limit?: number; diff --git a/types/models.d.ts b/types/models.d.ts index 5dad52a994a..1d28ba5be68 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -195,7 +195,7 @@ declare module 'mongoose' { export interface ReplaceOneModel { /** The filter to limit the replaced document. */ - filter: RootFilterQuery; + filter: QueryFilter; /** The document with which to replace the matched document. */ replacement: mongodb.WithoutId; /** Specifies a collation. */ @@ -210,7 +210,7 @@ declare module 'mongoose' { export interface UpdateOneModel { /** The filter to limit the updated documents. */ - filter: RootFilterQuery; + filter: QueryFilter; /** A document or pipeline containing update operators. */ update: UpdateQuery; /** A set of filters specifying to which array elements an update should apply. */ @@ -227,7 +227,7 @@ declare module 'mongoose' { export interface UpdateManyModel { /** The filter to limit the updated documents. */ - filter: RootFilterQuery; + filter: QueryFilter; /** A document or pipeline containing update operators. */ update: UpdateQuery; /** A set of filters specifying to which array elements an update should apply. */ @@ -244,7 +244,7 @@ declare module 'mongoose' { export interface DeleteOneModel { /** The filter to limit the deleted documents. */ - filter: RootFilterQuery; + filter: QueryFilter; /** Specifies a collation. */ collation?: mongodb.CollationOptions; /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ @@ -253,7 +253,7 @@ declare module 'mongoose' { export interface DeleteManyModel { /** The filter to limit the deleted documents. */ - filter: RootFilterQuery; + filter: QueryFilter; /** Specifies a collation. */ collation?: mongodb.CollationOptions; /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ @@ -332,7 +332,7 @@ declare module 'mongoose' { /** Creates a `countDocuments` query: counts the number of documents that match `filter`. */ countDocuments( - filter?: RootFilterQuery, + filter?: QueryFilter, options?: (mongodb.CountOptions & MongooseBaseQueryOptions & mongodb.Abortable) | null ): QueryWithHelpers< number, @@ -377,7 +377,7 @@ declare module 'mongoose' { * regardless of the `single` option. */ deleteMany( - filter?: RootFilterQuery, + filter?: QueryFilter, options?: (mongodb.DeleteOptions & MongooseBaseQueryOptions) | null ): QueryWithHelpers< mongodb.DeleteResult, @@ -388,7 +388,7 @@ declare module 'mongoose' { TInstanceMethods & TVirtuals >; deleteMany( - filter: RootFilterQuery + filter: QueryFilter ): QueryWithHelpers< mongodb.DeleteResult, THydratedDocumentType, @@ -404,7 +404,7 @@ declare module 'mongoose' { * `single` option. */ deleteOne( - filter?: RootFilterQuery, + filter?: QueryFilter, options?: (mongodb.DeleteOptions & MongooseBaseQueryOptions) | null ): QueryWithHelpers< mongodb.DeleteResult, @@ -415,7 +415,7 @@ declare module 'mongoose' { TInstanceMethods & TVirtuals >; deleteOne( - filter: RootFilterQuery + filter: QueryFilter ): QueryWithHelpers< mongodb.DeleteResult, THydratedDocumentType, @@ -488,7 +488,7 @@ declare module 'mongoose' { /** Finds one document. */ findOne( - filter: RootFilterQuery, + filter: QueryFilter, projection: ProjectionType | null | undefined, options: QueryOptions & { lean: true } & mongodb.Abortable ): QueryWithHelpers< @@ -500,16 +500,16 @@ declare module 'mongoose' { TInstanceMethods & TVirtuals >; findOne( - filter?: RootFilterQuery, + filter?: QueryFilter, projection?: ProjectionType | null, options?: QueryOptions & mongodb.Abortable | null ): QueryWithHelpers; findOne( - filter?: RootFilterQuery, + filter?: QueryFilter, projection?: ProjectionType | null ): QueryWithHelpers; findOne( - filter?: RootFilterQuery + filter?: QueryFilter ): QueryWithHelpers; /** @@ -677,7 +677,7 @@ declare module 'mongoose' { /** Creates a `distinct` query: returns the distinct values of the given `field` that match `filter`. */ distinct( field: DocKey, - filter?: RootFilterQuery, + filter?: QueryFilter, options?: QueryOptions ): QueryWithHelpers< Array< @@ -707,7 +707,7 @@ declare module 'mongoose' { * the given `filter`, and `null` otherwise. */ exists( - filter: RootFilterQuery + filter: QueryFilter ): QueryWithHelpers< { _id: InferId } | null, THydratedDocumentType, @@ -719,7 +719,7 @@ declare module 'mongoose' { /** Creates a `find` query: gets a list of documents that match `filter`. */ find( - filter: RootFilterQuery, + filter: QueryFilter, projection: ProjectionType | null | undefined, options: QueryOptions & { lean: true } & mongodb.Abortable ): QueryWithHelpers< @@ -731,16 +731,16 @@ declare module 'mongoose' { TInstanceMethods & TVirtuals >; find( - filter: RootFilterQuery, + filter: QueryFilter, projection?: ProjectionType | null | undefined, options?: QueryOptions & mongodb.Abortable | null | undefined ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'find', TInstanceMethods & TVirtuals>; find( - filter: RootFilterQuery, + filter: QueryFilter, projection?: ProjectionType | null | undefined ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'find', TInstanceMethods & TVirtuals>; find( - filter: RootFilterQuery + filter: QueryFilter ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'find', TInstanceMethods & TVirtuals>; find( ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'find', TInstanceMethods & TVirtuals>; @@ -768,7 +768,7 @@ declare module 'mongoose' { /** Creates a `findOneAndUpdate` query, filtering by the given `_id`. */ findByIdAndUpdate( - filter: RootFilterQuery, + filter: QueryFilter, update: UpdateQuery, options: QueryOptions & { includeResultMetadata: true, lean: true } ): QueryWithHelpers< @@ -813,7 +813,7 @@ declare module 'mongoose' { /** Creates a `findOneAndDelete` query: atomically finds the given document, deletes it, and returns the document as it was before deletion. */ findOneAndDelete( - filter: RootFilterQuery, + filter: QueryFilter, options: QueryOptions & { lean: true } ): QueryWithHelpers< GetLeanResultType | null, @@ -824,17 +824,17 @@ declare module 'mongoose' { TInstanceMethods & TVirtuals >; findOneAndDelete( - filter: RootFilterQuery, + filter: QueryFilter, options: QueryOptions & { includeResultMetadata: true } ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndDelete', TInstanceMethods & TVirtuals>; findOneAndDelete( - filter?: RootFilterQuery | null, + filter?: QueryFilter | null, options?: QueryOptions | null ): QueryWithHelpers; /** Creates a `findOneAndReplace` query: atomically finds the given document and replaces it with `replacement`. */ findOneAndReplace( - filter: RootFilterQuery, + filter: QueryFilter, replacement: TRawDocType | AnyObject, options: QueryOptions & { lean: true } ): QueryWithHelpers< @@ -846,24 +846,24 @@ declare module 'mongoose' { TInstanceMethods & TVirtuals >; findOneAndReplace( - filter: RootFilterQuery, + filter: QueryFilter, replacement: TRawDocType | AnyObject, options: QueryOptions & { includeResultMetadata: true } ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndReplace', TInstanceMethods & TVirtuals>; findOneAndReplace( - filter: RootFilterQuery, + filter: QueryFilter, replacement: TRawDocType | AnyObject, options: QueryOptions & { upsert: true } & ReturnsNewDoc ): QueryWithHelpers; findOneAndReplace( - filter?: RootFilterQuery, + filter?: QueryFilter, replacement?: TRawDocType | AnyObject, options?: QueryOptions | null ): QueryWithHelpers; /** Creates a `findOneAndUpdate` query: atomically find the first document that matches `filter` and apply `update`. */ findOneAndUpdate( - filter: RootFilterQuery, + filter: QueryFilter, update: UpdateQuery, options: QueryOptions & { includeResultMetadata: true, lean: true } ): QueryWithHelpers< @@ -875,7 +875,7 @@ declare module 'mongoose' { TInstanceMethods & TVirtuals >; findOneAndUpdate( - filter: RootFilterQuery, + filter: QueryFilter, update: UpdateQuery, options: QueryOptions & { lean: true } ): QueryWithHelpers< @@ -887,24 +887,24 @@ declare module 'mongoose' { TInstanceMethods & TVirtuals >; findOneAndUpdate( - filter: RootFilterQuery, + filter: QueryFilter, update: UpdateQuery, options: QueryOptions & { includeResultMetadata: true } ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndUpdate', TInstanceMethods & TVirtuals>; findOneAndUpdate( - filter: RootFilterQuery, + filter: QueryFilter, update: UpdateQuery, options: QueryOptions & { upsert: true } & ReturnsNewDoc ): QueryWithHelpers; findOneAndUpdate( - filter?: RootFilterQuery, + filter?: QueryFilter, update?: UpdateQuery, options?: QueryOptions | null ): QueryWithHelpers; /** Creates a `replaceOne` query: finds the first document that matches `filter` and replaces it with `replacement`. */ replaceOne( - filter?: RootFilterQuery, + filter?: QueryFilter, replacement?: TRawDocType | AnyObject, options?: (mongodb.ReplaceOptions & QueryOptions) | null ): QueryWithHelpers; @@ -917,14 +917,14 @@ declare module 'mongoose' { /** Creates a `updateMany` query: updates all documents that match `filter` with `update`. */ updateMany( - filter: RootFilterQuery, + filter: QueryFilter, update: UpdateQuery | UpdateWithAggregationPipeline, options?: (mongodb.UpdateOptions & MongooseUpdateQueryOptions) | null ): QueryWithHelpers; /** Creates a `updateOne` query: updates the first document that matches `filter` with `update`. */ updateOne( - filter: RootFilterQuery, + filter: QueryFilter, update: UpdateQuery | UpdateWithAggregationPipeline, options?: (mongodb.UpdateOptions & MongooseUpdateQueryOptions) | null ): QueryWithHelpers; diff --git a/types/pipelinestage.d.ts b/types/pipelinestage.d.ts index 809af303c6d..1f549594dfe 100644 --- a/types/pipelinestage.d.ts +++ b/types/pipelinestage.d.ts @@ -184,7 +184,7 @@ declare module 'mongoose' { export interface Match { /** [`$match` reference](https://www.mongodb.com/docs/manual/reference/operator/aggregation/match/) */ - $match: FilterQuery; + $match: QueryFilter; } export interface Merge { diff --git a/types/query.d.ts b/types/query.d.ts index ae65340f09e..c924fd3e7da 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -26,9 +26,8 @@ declare module 'mongoose' { * { age: { $gte: 30 } } * ``` */ - type RootFilterQuery = FilterQuery; - - type FilterQuery = { [P in keyof T]?: Condition; } & RootQuerySelector; + type _QueryFilter = { [P in keyof T]?: Condition; } & RootQuerySelector + type QueryFilter = _QueryFilter>; type MongooseBaseQueryOptionKeys = | 'context' @@ -107,11 +106,11 @@ declare module 'mongoose' { type RootQuerySelector = { /** @see https://www.mongodb.com/docs/manual/reference/operator/query/and/#op._S_and */ - $and?: Array>; + $and?: Array>; /** @see https://www.mongodb.com/docs/manual/reference/operator/query/nor/#op._S_nor */ - $nor?: Array>; + $nor?: Array>; /** @see https://www.mongodb.com/docs/manual/reference/operator/query/or/#op._S_or */ - $or?: Array>; + $or?: Array>; /** @see https://www.mongodb.com/docs/manual/reference/operator/query/text */ $text?: { $search: string; @@ -278,7 +277,7 @@ declare module 'mongoose' { allowDiskUse(value: boolean): this; /** Specifies arguments for an `$and` condition. */ - and(array: FilterQuery[]): this; + and(array: QueryFilter[]): this; /** Specifies the batchSize option. */ batchSize(val: number): this; @@ -327,7 +326,7 @@ declare module 'mongoose' { /** Specifies this query as a `countDocuments` query. */ countDocuments( - criteria?: RootFilterQuery, + criteria?: QueryFilter, options?: QueryOptions ): QueryWithHelpers; @@ -343,10 +342,10 @@ declare module 'mongoose' { * collection, regardless of the value of `single`. */ deleteMany( - filter?: RootFilterQuery, + filter?: QueryFilter, options?: QueryOptions ): QueryWithHelpers; - deleteMany(filter: RootFilterQuery): QueryWithHelpers< + deleteMany(filter: QueryFilter): QueryWithHelpers< any, DocType, THelpers, @@ -362,10 +361,10 @@ declare module 'mongoose' { * option. */ deleteOne( - filter?: RootFilterQuery, + filter?: QueryFilter, options?: QueryOptions ): QueryWithHelpers; - deleteOne(filter: RootFilterQuery): QueryWithHelpers< + deleteOne(filter: QueryFilter): QueryWithHelpers< any, DocType, THelpers, @@ -378,7 +377,7 @@ declare module 'mongoose' { /** Creates a `distinct` query: returns the distinct values of the given `field` that match `filter`. */ distinct( field: DocKey, - filter?: RootFilterQuery, + filter?: QueryFilter, options?: QueryOptions ): QueryWithHelpers< Array< @@ -431,52 +430,52 @@ declare module 'mongoose' { /** Creates a `find` query: gets a list of documents that match `filter`. */ find( - filter: RootFilterQuery, + filter: QueryFilter, projection?: ProjectionType | null, options?: QueryOptions | null ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find', TDocOverrides>; find( - filter: RootFilterQuery, + filter: QueryFilter, projection?: ProjectionType | null ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find', TDocOverrides>; find( - filter: RootFilterQuery + filter: QueryFilter ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find', TDocOverrides>; find(): QueryWithHelpers, DocType, THelpers, RawDocType, 'find', TDocOverrides>; /** Declares the query a findOne operation. When executed, returns the first found document. */ findOne( - filter?: RootFilterQuery, + filter?: QueryFilter, projection?: ProjectionType | null, options?: QueryOptions | null ): QueryWithHelpers; findOne( - filter?: RootFilterQuery, + filter?: QueryFilter, projection?: ProjectionType | null ): QueryWithHelpers; findOne( - filter?: RootFilterQuery + filter?: QueryFilter ): QueryWithHelpers; /** Creates a `findOneAndDelete` query: atomically finds the given document, deletes it, and returns the document as it was before deletion. */ findOneAndDelete( - filter?: RootFilterQuery, + filter?: QueryFilter, options?: QueryOptions | null ): QueryWithHelpers; /** Creates a `findOneAndUpdate` query: atomically find the first document that matches `filter` and apply `update`. */ findOneAndUpdate( - filter: RootFilterQuery, + filter: QueryFilter, update: UpdateQuery, options: QueryOptions & { includeResultMetadata: true } ): QueryWithHelpers, DocType, THelpers, RawDocType, 'findOneAndUpdate', TDocOverrides>; findOneAndUpdate( - filter: RootFilterQuery, + filter: QueryFilter, update: UpdateQuery, options: QueryOptions & { upsert: true } & ReturnsNewDoc ): QueryWithHelpers; findOneAndUpdate( - filter: RootFilterQuery, + filter: QueryFilter, update: UpdateQuery, options?: QueryOptions | null ): QueryWithHelpers; @@ -541,7 +540,7 @@ declare module 'mongoose' { get(path: string): any; /** Returns the current query filter (also known as conditions) as a POJO. */ - getFilter(): FilterQuery; + getFilter(): QueryFilter; /** Gets query options. */ getOptions(): QueryOptions; @@ -550,7 +549,7 @@ declare module 'mongoose' { getPopulatedPaths(): Array; /** Returns the current query filter. Equivalent to `getFilter()`. */ - getQuery(): FilterQuery; + getQuery(): QueryFilter; /** Returns the current update operations as a JSON object. */ getUpdate(): UpdateQuery | UpdateWithAggregationPipeline | null; @@ -631,7 +630,7 @@ declare module 'mongoose' { maxTimeMS(ms: number): this; /** Merges another Query or conditions object into this one. */ - merge(source: RootFilterQuery): this; + merge(source: QueryFilter): this; /** Specifies a `$mod` condition, filters documents for documents whose `path` property is a number that is equal to `remainder` modulo `divisor`. */ mod(path: K, val: number): this; @@ -659,10 +658,10 @@ declare module 'mongoose' { nin(val: Array): this; /** Specifies arguments for an `$nor` condition. */ - nor(array: Array>): this; + nor(array: Array>): this; /** Specifies arguments for an `$or` condition. */ - or(array: Array>): this; + or(array: Array>): this; /** * Make this query throw an error if no documents match the given `filter`. @@ -750,7 +749,7 @@ declare module 'mongoose' { * not accept any [atomic](https://www.mongodb.com/docs/manual/tutorial/model-data-for-atomic-operations/#pattern) operators (`$set`, etc.) */ replaceOne( - filter?: RootFilterQuery, + filter?: QueryFilter, replacement?: DocType | AnyObject, options?: QueryOptions | null ): QueryWithHelpers; @@ -821,7 +820,7 @@ declare module 'mongoose' { setOptions(options: QueryOptions, overwrite?: boolean): this; /** Sets the query conditions to the provided JSON object. */ - setQuery(val: FilterQuery | null): void; + setQuery(val: QueryFilter | null): void; setUpdate(update: UpdateQuery | UpdateWithAggregationPipeline): void; @@ -864,7 +863,7 @@ declare module 'mongoose' { * the `multi` option. */ updateMany( - filter: RootFilterQuery, + filter: QueryFilter, update: UpdateQuery | UpdateWithAggregationPipeline, options?: QueryOptions | null ): QueryWithHelpers; @@ -877,7 +876,7 @@ declare module 'mongoose' { * `update()`, except it does not support the `multi` or `overwrite` options. */ updateOne( - filter: RootFilterQuery, + filter: QueryFilter, update: UpdateQuery | UpdateWithAggregationPipeline, options?: QueryOptions | null ): QueryWithHelpers; diff --git a/types/utility.d.ts b/types/utility.d.ts index a39a43262d4..dc1f711a37a 100644 --- a/types/utility.d.ts +++ b/types/utility.d.ts @@ -15,8 +15,12 @@ declare module 'mongoose' { // Handle nested paths : P extends `${infer Key}.${infer Rest}` ? Key extends keyof T - ? T[Key] extends (infer U)[] - ? Rest extends keyof NonNullable + ? NonNullable extends (infer U)[] + ? NonNullable extends Types.DocumentArray + ? Rest extends keyof NonNullable + ? NonNullable[Rest] + : never + : Rest extends keyof NonNullable ? NonNullable[Rest] : never : Rest extends keyof NonNullable @@ -26,23 +30,40 @@ declare module 'mongoose' { : never; }; - type NestedPaths = K extends string - ? T[K] extends TreatAsPrimitives - ? never - : Extract, Document> extends never - ? T[K] extends Array - ? U extends Record - ? `${K}.${keyof NonNullable & string}` - : never - : T[K] extends Record | null | undefined - ? `${K}.${keyof NonNullable & string}` + type HasStringIndex = + string extends Extract ? true : false; + + type SafeObjectKeys = + HasStringIndex extends true ? never : Extract; + + type NestedPaths = + K extends string + ? T[K] extends TreatAsPrimitives + ? never + : Extract, Document> extends never + ? NonNullable extends Array + ? NonNullable extends Types.DocumentArray + ? SafeObjectKeys> extends never + ? never + : `${K}.${SafeObjectKeys>}` + : NonNullable extends Record + ? SafeObjectKeys> extends never + ? never + : `${K}.${SafeObjectKeys>}` + : never + : NonNullable extends object + ? SafeObjectKeys> extends never + ? never + : `${K}.${SafeObjectKeys>}` : never : Extract, Document> extends Document - ? DocType extends Record - ? `${K}.${keyof NonNullable & string}` + ? DocType extends object + ? SafeObjectKeys> extends never + ? never + : `${K}.${SafeObjectKeys>}` : never : never - : never; + : never; type WithoutUndefined = T extends undefined ? never : T; From a20e0c2d37994c5488f7d422091c68dca4d149cf Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 18 Aug 2025 16:26:27 -0400 Subject: [PATCH 136/209] BREAKING CHANGE: consolidate RootQuerySelector, Condition, etc. types with MongoDB driver's --- test/types/populate.test.ts | 4 +- test/types/queries.test.ts | 7 ++-- types/query.d.ts | 73 +------------------------------------ 3 files changed, 7 insertions(+), 77 deletions(-) diff --git a/test/types/populate.test.ts b/test/types/populate.test.ts index 47eb053a5e5..169e7824d7e 100644 --- a/test/types/populate.test.ts +++ b/test/types/populate.test.ts @@ -24,14 +24,14 @@ ParentModel. findOne({}). populate<{ child: Document & Child }>('child'). orFail(). - then((doc: Document & Parent) => { + then((doc) => { const child = doc.child; if (child == null || child instanceof ObjectId) { throw new Error('should be populated'); } else { useChildDoc(child); } - const lean = doc.toObject(); + const lean = doc.toObject>(); const leanChild = lean.child; if (leanChild == null || leanChild instanceof ObjectId) { throw new Error('should be populated'); diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index bfced89e245..8bac8278e7e 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -1,5 +1,4 @@ import { - Condition, HydratedDocument, Schema, model, @@ -12,13 +11,13 @@ import { FilterQuery, UpdateQuery, UpdateQueryKnownOnly, - QuerySelector, InferRawDocType, InferSchemaType, ProjectionFields, QueryOptions, ProjectionType } from 'mongoose'; +import mongodb from 'mongodb'; import { ModifyResult, ObjectId } from 'mongodb'; import { expectAssignable, expectError, expectNotAssignable, expectType } from 'tsd'; import { autoTypedModel } from './models.test'; @@ -348,7 +347,7 @@ function autoTypedQuery() { function gh11964() { class Repository { find(id: string) { - const idCondition: Condition = id as Condition; + const idCondition: mongodb.Condition = id as mongodb.Condition; // `as` is necessary because `T` can be `{ id: never }`, // so we need to explicitly coerce @@ -358,7 +357,7 @@ function gh11964() { } function gh14397() { - type Condition = T | QuerySelector; // redefined here because it's not exported by mongoose + type Condition = mongodb.Condition; // redefined here because it's not exported by mongoose type WithId = T & { id: string }; diff --git a/types/query.d.ts b/types/query.d.ts index ae65340f09e..da313452fc1 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -15,9 +15,7 @@ declare module 'mongoose' { ? BufferQueryCasting : T; - export type ApplyBasicQueryCasting = T | T[] | (T extends (infer U)[] ? QueryTypeCasting : T); - - export type Condition = ApplyBasicQueryCasting> | QuerySelector>>; + export type ApplyBasicQueryCasting = T | T[] | (T extends (infer U)[] ? QueryTypeCasting : T) | null; /** * Filter query to select the documents that match the query @@ -28,7 +26,7 @@ declare module 'mongoose' { */ type RootFilterQuery = FilterQuery; - type FilterQuery = { [P in keyof T]?: Condition; } & RootQuerySelector; + type FilterQuery = ({ [P in keyof T]?: mongodb.Condition>>; } & mongodb.RootFilterOperators<{ [P in keyof T]?: ApplyBasicQueryCasting>; }>) | Query; type MongooseBaseQueryOptionKeys = | 'context' @@ -61,73 +59,6 @@ declare module 'mongoose' { TDocOverrides = Record > = Query & THelpers; - type QuerySelector = { - // Comparison - $eq?: T | null | undefined; - $gt?: T; - $gte?: T; - $in?: [T] extends AnyArray ? Unpacked[] : T[]; - $lt?: T; - $lte?: T; - $ne?: T | null | undefined; - $nin?: [T] extends AnyArray ? Unpacked[] : T[]; - // Logical - $not?: T extends string ? QuerySelector | RegExp : QuerySelector; - // Element - /** - * When `true`, `$exists` matches the documents that contain the field, - * including documents where the field value is null. - */ - $exists?: boolean; - $type?: string | number; - // Evaluation - $expr?: any; - $jsonSchema?: any; - $mod?: T extends number ? [number, number] : never; - $regex?: T extends string ? RegExp | string : never; - $options?: T extends string ? string : never; - // Geospatial - // TODO: define better types for geo queries - $geoIntersects?: { $geometry: object }; - $geoWithin?: object; - $near?: object; - $nearSphere?: object; - $maxDistance?: number; - // Array - // TODO: define better types for $all and $elemMatch - $all?: T extends AnyArray ? any[] : never; - $elemMatch?: T extends AnyArray ? object : never; - $size?: T extends AnyArray ? number : never; - // Bitwise - $bitsAllClear?: number | mongodb.Binary | number[]; - $bitsAllSet?: number | mongodb.Binary | number[]; - $bitsAnyClear?: number | mongodb.Binary | number[]; - $bitsAnySet?: number | mongodb.Binary | number[]; - }; - - type RootQuerySelector = { - /** @see https://www.mongodb.com/docs/manual/reference/operator/query/and/#op._S_and */ - $and?: Array>; - /** @see https://www.mongodb.com/docs/manual/reference/operator/query/nor/#op._S_nor */ - $nor?: Array>; - /** @see https://www.mongodb.com/docs/manual/reference/operator/query/or/#op._S_or */ - $or?: Array>; - /** @see https://www.mongodb.com/docs/manual/reference/operator/query/text */ - $text?: { - $search: string; - $language?: string; - $caseSensitive?: boolean; - $diacriticSensitive?: boolean; - }; - /** @see https://www.mongodb.com/docs/manual/reference/operator/query/where/#op._S_where */ - $where?: string | Function; - /** @see https://www.mongodb.com/docs/manual/reference/operator/query/comment/#op._S_comment */ - $comment?: string; - $expr?: Record; - // this will mark all unrecognized properties as any (including nested queries) - [key: string]: any; - }; - interface QueryTimestampsConfig { createdAt?: boolean; updatedAt?: boolean; From 71a69ad9acb998bd0d91e830fac61fe28071d9c5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 22 Aug 2025 11:03:13 -0400 Subject: [PATCH 137/209] remove examples directory --- examples/README.md | 41 ------ examples/aggregate/aggregate.js | 105 ------------- examples/aggregate/package.json | 14 -- examples/aggregate/person.js | 19 --- examples/doc-methods.js | 78 ---------- examples/express/README.md | 1 - examples/express/connection-sharing/README.md | 7 - examples/express/connection-sharing/app.js | 15 -- examples/express/connection-sharing/modelA.js | 6 - .../express/connection-sharing/package.json | 14 -- examples/express/connection-sharing/routes.js | 24 --- examples/geospatial/geoJSONSchema.js | 24 --- examples/geospatial/geoJSONexample.js | 58 -------- examples/geospatial/geospatial.js | 102 ------------- examples/geospatial/package.json | 14 -- examples/geospatial/person.js | 29 ---- examples/globalschemas/gs_example.js | 48 ------ examples/globalschemas/person.js | 16 -- examples/lean/lean.js | 86 ----------- examples/lean/package.json | 14 -- examples/lean/person.js | 18 --- .../population-across-three-collections.js | 135 ----------------- examples/population/population-basic.js | 104 ------------- .../population/population-of-existing-doc.js | 110 -------------- .../population-of-multiple-existing-docs.js | 125 ---------------- examples/population/population-options.js | 139 ------------------ .../population/population-plain-objects.js | 107 -------------- examples/promises/package.json | 14 -- examples/promises/person.js | 17 --- examples/promises/promise.js | 96 ------------ examples/querybuilder/package.json | 14 -- examples/querybuilder/person.js | 17 --- examples/querybuilder/querybuilder.js | 81 ---------- examples/redis-todo/.eslintrc.yml | 2 - examples/redis-todo/.npmrc | 1 - examples/redis-todo/config.js | 7 - examples/redis-todo/db/index.js | 5 - examples/redis-todo/db/models/todoModel.js | 11 -- examples/redis-todo/db/models/userModel.js | 49 ------ examples/redis-todo/middleware/auth.js | 19 --- examples/redis-todo/middleware/clearCache.js | 9 -- examples/redis-todo/package.json | 40 ----- examples/redis-todo/routers/todoRouter.js | 73 --------- examples/redis-todo/routers/userRouter.js | 98 ------------ examples/redis-todo/server.js | 33 ----- examples/redis-todo/services/cache.js | 44 ------ examples/replicasets/package.json | 14 -- examples/replicasets/person.js | 17 --- examples/replicasets/replica-sets.js | 73 --------- examples/schema/schema.js | 121 --------------- .../schema/storing-schemas-as-json/index.js | 29 ---- .../storing-schemas-as-json/schema.json | 9 -- examples/statics/person.js | 22 --- examples/statics/statics.js | 33 ----- 54 files changed, 2401 deletions(-) delete mode 100644 examples/README.md delete mode 100644 examples/aggregate/aggregate.js delete mode 100644 examples/aggregate/package.json delete mode 100644 examples/aggregate/person.js delete mode 100644 examples/doc-methods.js delete mode 100644 examples/express/README.md delete mode 100644 examples/express/connection-sharing/README.md delete mode 100644 examples/express/connection-sharing/app.js delete mode 100644 examples/express/connection-sharing/modelA.js delete mode 100644 examples/express/connection-sharing/package.json delete mode 100644 examples/express/connection-sharing/routes.js delete mode 100644 examples/geospatial/geoJSONSchema.js delete mode 100644 examples/geospatial/geoJSONexample.js delete mode 100644 examples/geospatial/geospatial.js delete mode 100644 examples/geospatial/package.json delete mode 100644 examples/geospatial/person.js delete mode 100644 examples/globalschemas/gs_example.js delete mode 100644 examples/globalschemas/person.js delete mode 100644 examples/lean/lean.js delete mode 100644 examples/lean/package.json delete mode 100644 examples/lean/person.js delete mode 100644 examples/population/population-across-three-collections.js delete mode 100644 examples/population/population-basic.js delete mode 100644 examples/population/population-of-existing-doc.js delete mode 100644 examples/population/population-of-multiple-existing-docs.js delete mode 100644 examples/population/population-options.js delete mode 100644 examples/population/population-plain-objects.js delete mode 100644 examples/promises/package.json delete mode 100644 examples/promises/person.js delete mode 100644 examples/promises/promise.js delete mode 100644 examples/querybuilder/package.json delete mode 100644 examples/querybuilder/person.js delete mode 100644 examples/querybuilder/querybuilder.js delete mode 100644 examples/redis-todo/.eslintrc.yml delete mode 100644 examples/redis-todo/.npmrc delete mode 100644 examples/redis-todo/config.js delete mode 100644 examples/redis-todo/db/index.js delete mode 100644 examples/redis-todo/db/models/todoModel.js delete mode 100644 examples/redis-todo/db/models/userModel.js delete mode 100644 examples/redis-todo/middleware/auth.js delete mode 100644 examples/redis-todo/middleware/clearCache.js delete mode 100644 examples/redis-todo/package.json delete mode 100644 examples/redis-todo/routers/todoRouter.js delete mode 100644 examples/redis-todo/routers/userRouter.js delete mode 100644 examples/redis-todo/server.js delete mode 100644 examples/redis-todo/services/cache.js delete mode 100644 examples/replicasets/package.json delete mode 100644 examples/replicasets/person.js delete mode 100644 examples/replicasets/replica-sets.js delete mode 100644 examples/schema/schema.js delete mode 100644 examples/schema/storing-schemas-as-json/index.js delete mode 100644 examples/schema/storing-schemas-as-json/schema.json delete mode 100644 examples/statics/person.js delete mode 100644 examples/statics/statics.js diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 8511ee44434..00000000000 --- a/examples/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Examples - -This directory contains runnable sample mongoose programs. - -To run: - -* first install [Node.js](http://nodejs.org/) -* from the root of the project, execute `npm install -d` -* in the example directory, run `npm install -d` -* from the command line, execute: `node example.js`, replacing "example.js" with the name of a program. - -Goal is to show: - -* ~~global schemas~~ -* ~~GeoJSON schemas / use (with crs)~~ -* text search (once MongoDB removes the "Experimental/beta" label) -* ~~lean queries~~ -* ~~statics~~ -* methods and statics on subdocs -* custom types -* ~~querybuilder~~ -* ~~promises~~ -* accessing driver collection, db -* ~~connecting to replica sets~~ -* connecting to sharded clusters -* enabling a fail fast mode -* on the fly schemas -* storing files -* ~~map reduce~~ -* ~~aggregation~~ -* advanced hooks -* using $elemMatch to return a subset of an array -* query casting -* upserts -* pagination -* express + mongoose session handling -* ~~group by (use aggregation)~~ -* authentication -* schema migration techniques -* converting documents to plain objects (show transforms) -* how to $unset diff --git a/examples/aggregate/aggregate.js b/examples/aggregate/aggregate.js deleted file mode 100644 index 24f172210f0..00000000000 --- a/examples/aggregate/aggregate.js +++ /dev/null @@ -1,105 +0,0 @@ - -// import async to make control flow simplier -'use strict'; - -const async = require('async'); - -// import the rest of the normal stuff -const mongoose = require('../../lib'); - -require('./person.js')(); - -const Person = mongoose.model('Person'); - -// define some dummy data -const data = [ - { - name: 'bill', - age: 25, - birthday: new Date().setFullYear((new Date().getFullYear() - 25)), - gender: 'Male', - likes: ['movies', 'games', 'dogs'] - }, - { - name: 'mary', - age: 30, - birthday: new Date().setFullYear((new Date().getFullYear() - 30)), - gender: 'Female', - likes: ['movies', 'birds', 'cats'] - }, - { - name: 'bob', - age: 21, - birthday: new Date().setFullYear((new Date().getFullYear() - 21)), - gender: 'Male', - likes: ['tv', 'games', 'rabbits'] - }, - { - name: 'lilly', - age: 26, - birthday: new Date().setFullYear((new Date().getFullYear() - 26)), - gender: 'Female', - likes: ['books', 'cats', 'dogs'] - }, - { - name: 'alucard', - age: 1000, - birthday: new Date().setFullYear((new Date().getFullYear() - 1000)), - gender: 'Male', - likes: ['glasses', 'wine', 'the night'] - } -]; - - -mongoose.connect('mongodb://127.0.0.1/persons', function(err) { - if (err) throw err; - - // create all of the dummy people - async.each(data, function(item, cb) { - Person.create(item, cb); - }, function(err) { - if (err) { - // handle error - } - - // run an aggregate query that will get all of the people who like a given - // item. To see the full documentation on ways to use the aggregate - // framework, see http://www.mongodb.com/docs/manual/core/aggregation/ - Person.aggregate( - // select the fields we want to deal with - { $project: { name: 1, likes: 1 } }, - // unwind 'likes', which will create a document for each like - { $unwind: '$likes' }, - // group everything by the like and then add each name with that like to - // the set for the like - { $group: { - _id: { likes: '$likes' }, - likers: { $addToSet: '$name' } - } }, - function(err, result) { - if (err) throw err; - console.log(result); - /* [ - { _id: { likes: 'the night' }, likers: [ 'alucard' ] }, - { _id: { likes: 'wine' }, likers: [ 'alucard' ] }, - { _id: { likes: 'books' }, likers: [ 'lilly' ] }, - { _id: { likes: 'glasses' }, likers: [ 'alucard' ] }, - { _id: { likes: 'birds' }, likers: [ 'mary' ] }, - { _id: { likes: 'rabbits' }, likers: [ 'bob' ] }, - { _id: { likes: 'cats' }, likers: [ 'lilly', 'mary' ] }, - { _id: { likes: 'dogs' }, likers: [ 'lilly', 'bill' ] }, - { _id: { likes: 'tv' }, likers: [ 'bob' ] }, - { _id: { likes: 'games' }, likers: [ 'bob', 'bill' ] }, - { _id: { likes: 'movies' }, likers: [ 'mary', 'bill' ] } - ] */ - - cleanup(); - }); - }); -}); - -function cleanup() { - Person.remove(function() { - mongoose.disconnect(); - }); -} diff --git a/examples/aggregate/package.json b/examples/aggregate/package.json deleted file mode 100644 index 53ed2e14b7a..00000000000 --- a/examples/aggregate/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "aggregate-example", - "private": "true", - "version": "0.0.0", - "description": "deps for aggregate example", - "main": "aggregate.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { "async": "*" }, - "repository": "", - "author": "", - "license": "BSD" -} diff --git a/examples/aggregate/person.js b/examples/aggregate/person.js deleted file mode 100644 index 76ec8a0cab4..00000000000 --- a/examples/aggregate/person.js +++ /dev/null @@ -1,19 +0,0 @@ - -// import the necessary modules -'use strict'; - -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; - -// create an export function to encapsulate the model creation -module.exports = function() { - // define schema - const PersonSchema = new Schema({ - name: String, - age: Number, - birthday: Date, - gender: String, - likes: [String] - }); - mongoose.model('Person', PersonSchema); -}; diff --git a/examples/doc-methods.js b/examples/doc-methods.js deleted file mode 100644 index d6b34599998..00000000000 --- a/examples/doc-methods.js +++ /dev/null @@ -1,78 +0,0 @@ - -'use strict'; -const mongoose = require('mongoose'); -const Schema = mongoose.Schema; - -console.log('Running mongoose version %s', mongoose.version); - -/** - * Schema - */ - -const CharacterSchema = Schema({ - name: { - type: String, - required: true - }, - health: { - type: Number, - min: 0, - max: 100 - } -}); - -/** - * Methods - */ - -CharacterSchema.methods.attack = function() { - console.log('%s is attacking', this.name); -}; - -/** - * Character model - */ - -const Character = mongoose.model('Character', CharacterSchema); - -/** - * Connect to the database on 127.0.0.1 with - * the default port (27017) - */ - -const dbname = 'mongoose-example-doc-methods-' + ((Math.random() * 10000) | 0); -const uri = 'mongodb://127.0.0.1/' + dbname; - -console.log('connecting to %s', uri); - -mongoose.connect(uri, function(err) { - // if we failed to connect, abort - if (err) throw err; - - // we connected ok - example(); -}); - -/** - * Use case - */ - -function example() { - Character.create({ name: 'Link', health: 100 }, function(err, link) { - if (err) return done(err); - console.log('found', link); - link.attack(); // 'Link is attacking' - done(); - }); -} - -/** - * Clean up - */ - -function done(err) { - if (err) console.error(err); - mongoose.connection.db.dropDatabase(function() { - mongoose.disconnect(); - }); -} diff --git a/examples/express/README.md b/examples/express/README.md deleted file mode 100644 index c3caa9c088d..00000000000 --- a/examples/express/README.md +++ /dev/null @@ -1 +0,0 @@ -# Mongoose + Express examples diff --git a/examples/express/connection-sharing/README.md b/examples/express/connection-sharing/README.md deleted file mode 100644 index b734d875bd8..00000000000 --- a/examples/express/connection-sharing/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Express Connection sharing Example - -To run: - -* Execute `npm install` from this directory -* Execute `node app.js` -* Navigate to `127.0.0.1:8000` diff --git a/examples/express/connection-sharing/app.js b/examples/express/connection-sharing/app.js deleted file mode 100644 index 8c0efae338e..00000000000 --- a/examples/express/connection-sharing/app.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; -const express = require('express'); -const mongoose = require('../../../lib'); - -const uri = 'mongodb://127.0.0.1/mongoose-shared-connection'; -global.db = mongoose.createConnection(uri); - -const routes = require('./routes'); - -const app = express(); -app.get('/', routes.home); -app.get('/insert', routes.insert); -app.get('/name', routes.modelName); - -app.listen(8000, () => console.log('listening on http://127.0.0.1:8000')); diff --git a/examples/express/connection-sharing/modelA.js b/examples/express/connection-sharing/modelA.js deleted file mode 100644 index b52e20c0420..00000000000 --- a/examples/express/connection-sharing/modelA.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; -const Schema = require('../../../lib').Schema; -const mySchema = Schema({ name: String }); - -/* global db */ -module.exports = db.model('MyModel', mySchema); diff --git a/examples/express/connection-sharing/package.json b/examples/express/connection-sharing/package.json deleted file mode 100644 index f53b7c7b3cb..00000000000 --- a/examples/express/connection-sharing/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "connection-sharing", - "private": true, - "version": "0.0.0", - "description": "ERROR: No README.md file found!", - "main": "app.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { "express": "4.x" }, - "repository": "", - "author": "", - "license": "BSD" -} diff --git a/examples/express/connection-sharing/routes.js b/examples/express/connection-sharing/routes.js deleted file mode 100644 index e9d483ae285..00000000000 --- a/examples/express/connection-sharing/routes.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; -const model = require('./modelA'); - -exports.home = async(req, res, next) => { - try { - const docs = await model.find(); - res.send(docs); - } catch (err) { - next(err); - } -}; - -exports.modelName = (req, res) => { - res.send('my model name is ' + model.modelName); -}; - -exports.insert = async(req, res, next) => { - try { - const doc = await model.create({ name: 'inserting ' + Date.now() }); - res.send(doc); - } catch (err) { - next(err); - } -}; diff --git a/examples/geospatial/geoJSONSchema.js b/examples/geospatial/geoJSONSchema.js deleted file mode 100644 index ae3d10675e2..00000000000 --- a/examples/geospatial/geoJSONSchema.js +++ /dev/null @@ -1,24 +0,0 @@ - -// import the necessary modules -'use strict'; - -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; - -// create an export function to encapsulate the model creation -module.exports = function() { - // define schema - // NOTE : This object must conform *precisely* to the geoJSON specification - // you cannot embed a geoJSON doc inside a model or anything like that- IT - // MUST BE VANILLA - const LocationObject = new Schema({ - loc: { - type: { type: String }, - coordinates: [] - } - }); - // define the index - LocationObject.index({ loc: '2dsphere' }); - - mongoose.model('Location', LocationObject); -}; diff --git a/examples/geospatial/geoJSONexample.js b/examples/geospatial/geoJSONexample.js deleted file mode 100644 index 5f1fd2dbb87..00000000000 --- a/examples/geospatial/geoJSONexample.js +++ /dev/null @@ -1,58 +0,0 @@ -// import async to make control flow simplier -'use strict'; - -const async = require('async'); - -// import the rest of the normal stuff -const mongoose = require('../../lib'); - -require('./geoJSONSchema.js')(); - -const Location = mongoose.model('Location'); - -// define some dummy data -// note: the type can be Point, LineString, or Polygon -const data = [ - { loc: { type: 'Point', coordinates: [-20.0, 5.0] } }, - { loc: { type: 'Point', coordinates: [6.0, 10.0] } }, - { loc: { type: 'Point', coordinates: [34.0, -50.0] } }, - { loc: { type: 'Point', coordinates: [-100.0, 70.0] } }, - { loc: { type: 'Point', coordinates: [38.0, 38.0] } } -]; - - -mongoose.connect('mongodb://127.0.0.1/locations', function(err) { - if (err) { - throw err; - } - - Location.on('index', function(err) { - if (err) { - throw err; - } - // create all of the dummy locations - async.each(data, function(item, cb) { - Location.create(item, cb); - }, function(err) { - if (err) { - throw err; - } - // create the location we want to search for - const coords = { type: 'Point', coordinates: [-5, 5] }; - // search for it - Location.find({ loc: { $near: coords } }).limit(1).exec(function(err, res) { - if (err) { - throw err; - } - console.log('Closest to %s is %s', JSON.stringify(coords), res); - cleanup(); - }); - }); - }); -}); - -function cleanup() { - Location.remove(function() { - mongoose.disconnect(); - }); -} diff --git a/examples/geospatial/geospatial.js b/examples/geospatial/geospatial.js deleted file mode 100644 index 8bebb6b2166..00000000000 --- a/examples/geospatial/geospatial.js +++ /dev/null @@ -1,102 +0,0 @@ -// import async to make control flow simplier -'use strict'; - -const async = require('async'); - -// import the rest of the normal stuff -const mongoose = require('../../lib'); - -require('./person.js')(); - -const Person = mongoose.model('Person'); - -// define some dummy data -const data = [ - { - name: 'bill', - age: 25, - birthday: new Date().setFullYear((new Date().getFullYear() - 25)), - gender: 'Male', - likes: ['movies', 'games', 'dogs'], - loc: [0, 0] - }, - { - name: 'mary', - age: 30, - birthday: new Date().setFullYear((new Date().getFullYear() - 30)), - gender: 'Female', - likes: ['movies', 'birds', 'cats'], - loc: [1, 1] - }, - { - name: 'bob', - age: 21, - birthday: new Date().setFullYear((new Date().getFullYear() - 21)), - gender: 'Male', - likes: ['tv', 'games', 'rabbits'], - loc: [3, 3] - }, - { - name: 'lilly', - age: 26, - birthday: new Date().setFullYear((new Date().getFullYear() - 26)), - gender: 'Female', - likes: ['books', 'cats', 'dogs'], - loc: [6, 6] - }, - { - name: 'alucard', - age: 1000, - birthday: new Date().setFullYear((new Date().getFullYear() - 1000)), - gender: 'Male', - likes: ['glasses', 'wine', 'the night'], - loc: [10, 10] - } -]; - - -mongoose.connect('mongodb://127.0.0.1/persons', function(err) { - if (err) { - throw err; - } - - // create all of the dummy people - async.each(data, function(item, cb) { - Person.create(item, cb); - }, function(err) { - if (err) { - // handler error - } - - // let's find the closest person to bob - Person.find({ name: 'bob' }, function(err, res) { - if (err) { - throw err; - } - - res[0].findClosest(function(err, closest) { - if (err) { - throw err; - } - - console.log('%s is closest to %s', res[0].name, closest); - - - // we can also just query straight off of the model. For more - // information about geospatial queries and indexes, see - // http://www.mongodb.com/docs/manual/applications/geospatial-indexes/ - const coords = [7, 7]; - Person.find({ loc: { $nearSphere: coords } }).limit(1).exec(function(err, res) { - console.log('Closest to %s is %s', coords, res); - cleanup(); - }); - }); - }); - }); -}); - -function cleanup() { - Person.remove(function() { - mongoose.disconnect(); - }); -} diff --git a/examples/geospatial/package.json b/examples/geospatial/package.json deleted file mode 100644 index 75c2a0eef22..00000000000 --- a/examples/geospatial/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "geospatial-example", - "private": "true", - "version": "0.0.0", - "description": "deps for geospatial example", - "main": "geospatial.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { "async": "*" }, - "repository": "", - "author": "", - "license": "BSD" -} diff --git a/examples/geospatial/person.js b/examples/geospatial/person.js deleted file mode 100644 index 9f692320bb5..00000000000 --- a/examples/geospatial/person.js +++ /dev/null @@ -1,29 +0,0 @@ -// import the necessary modules -'use strict'; - -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; - -// create an export function to encapsulate the model creation -module.exports = function() { - // define schema - const PersonSchema = new Schema({ - name: String, - age: Number, - birthday: Date, - gender: String, - likes: [String], - // define the geospatial field - loc: { type: [Number], index: '2d' } - }); - - // define a method to find the closest person - PersonSchema.methods.findClosest = function(cb) { - return mongoose.model('Person').find({ - loc: { $nearSphere: this.loc }, - name: { $ne: this.name } - }).limit(1).exec(cb); - }; - - mongoose.model('Person', PersonSchema); -}; diff --git a/examples/globalschemas/gs_example.js b/examples/globalschemas/gs_example.js deleted file mode 100644 index 3b9a74f9dd5..00000000000 --- a/examples/globalschemas/gs_example.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; -const mongoose = require('../../lib'); - - -// import the global schema, this can be done in any file that needs the model -require('./person.js')(); - -// grab the person model object -const Person = mongoose.model('Person'); - -// connect to a server to do a quick write / read example - -mongoose.connect('mongodb://127.0.0.1/persons', function(err) { - if (err) { - throw err; - } - - Person.create({ - name: 'bill', - age: 25, - birthday: new Date().setFullYear((new Date().getFullYear() - 25)) - }, function(err, bill) { - if (err) { - throw err; - } - console.log('People added to db: %s', bill.toString()); - Person.find({}, function(err, people) { - if (err) { - throw err; - } - - people.forEach(function(person) { - console.log('People in the db: %s', person.toString()); - }); - - // make sure to clean things up after we're done - setTimeout(function() { - cleanup(); - }, 2000); - }); - }); -}); - -function cleanup() { - Person.remove(function() { - mongoose.disconnect(); - }); -} diff --git a/examples/globalschemas/person.js b/examples/globalschemas/person.js deleted file mode 100644 index f598dd3fb63..00000000000 --- a/examples/globalschemas/person.js +++ /dev/null @@ -1,16 +0,0 @@ -// import the necessary modules -'use strict'; - -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; - -// create an export function to encapsulate the model creation -module.exports = function() { - // define schema - const PersonSchema = new Schema({ - name: String, - age: Number, - birthday: Date - }); - mongoose.model('Person', PersonSchema); -}; diff --git a/examples/lean/lean.js b/examples/lean/lean.js deleted file mode 100644 index 95759a40a6b..00000000000 --- a/examples/lean/lean.js +++ /dev/null @@ -1,86 +0,0 @@ - -// import async to make control flow simplier -'use strict'; - -const async = require('async'); - -// import the rest of the normal stuff -const mongoose = require('../../lib'); - -require('./person.js')(); - -const Person = mongoose.model('Person'); - -// define some dummy data -const data = [ - { - name: 'bill', - age: 25, - birthday: new Date().setFullYear((new Date().getFullYear() - 25)), - gender: 'Male', - likes: ['movies', 'games', 'dogs'] - }, - { - name: 'mary', - age: 30, - birthday: new Date().setFullYear((new Date().getFullYear() - 30)), - gender: 'Female', - likes: ['movies', 'birds', 'cats'] - }, - { - name: 'bob', - age: 21, - birthday: new Date().setFullYear((new Date().getFullYear() - 21)), - gender: 'Male', - likes: ['tv', 'games', 'rabbits'] - }, - { - name: 'lilly', - age: 26, - birthday: new Date().setFullYear((new Date().getFullYear() - 26)), - gender: 'Female', - likes: ['books', 'cats', 'dogs'] - }, - { - name: 'alucard', - age: 1000, - birthday: new Date().setFullYear((new Date().getFullYear() - 1000)), - gender: 'Male', - likes: ['glasses', 'wine', 'the night'] - } -]; - - -mongoose.connect('mongodb://127.0.0.1/persons', function(err) { - if (err) throw err; - - // create all of the dummy people - async.each(data, function(item, cb) { - Person.create(item, cb); - }, function(err) { - if (err) { - // handle error - } - - // lean queries return just plain javascript objects, not - // MongooseDocuments. This makes them good for high performance read - // situations - - // when using .lean() the default is true, but you can explicitly set the - // value by passing in a boolean value. IE. .lean(false) - const q = Person.find({ age: { $lt: 1000 } }).sort('age').limit(2).lean(); - q.exec(function(err, results) { - if (err) throw err; - console.log('Are the results MongooseDocuments?: %s', results[0] instanceof mongoose.Document); - - console.log(results); - cleanup(); - }); - }); -}); - -function cleanup() { - Person.remove(function() { - mongoose.disconnect(); - }); -} diff --git a/examples/lean/package.json b/examples/lean/package.json deleted file mode 100644 index 6ee511de77a..00000000000 --- a/examples/lean/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "lean-example", - "private": "true", - "version": "0.0.0", - "description": "deps for lean example", - "main": "lean.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { "async": "*" }, - "repository": "", - "author": "", - "license": "BSD" -} diff --git a/examples/lean/person.js b/examples/lean/person.js deleted file mode 100644 index c052f7f24df..00000000000 --- a/examples/lean/person.js +++ /dev/null @@ -1,18 +0,0 @@ -// import the necessary modules -'use strict'; - -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; - -// create an export function to encapsulate the model creation -module.exports = function() { - // define schema - const PersonSchema = new Schema({ - name: String, - age: Number, - birthday: Date, - gender: String, - likes: [String] - }); - mongoose.model('Person', PersonSchema); -}; diff --git a/examples/population/population-across-three-collections.js b/examples/population/population-across-three-collections.js deleted file mode 100644 index e3ef031d9b9..00000000000 --- a/examples/population/population-across-three-collections.js +++ /dev/null @@ -1,135 +0,0 @@ - -'use strict'; -const assert = require('assert'); -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; -const ObjectId = mongoose.Types.ObjectId; - -/** - * Connect to the db - */ - -const dbname = 'testing_populateAdInfinitum_' + require('../../lib/utils').random(); -mongoose.connect('127.0.0.1', dbname); -mongoose.connection.on('error', function() { - console.error('connection error', arguments); -}); - -/** - * Schemas - */ - -const user = new Schema({ - name: String, - friends: [{ - type: Schema.ObjectId, - ref: 'User' - }] -}); -const User = mongoose.model('User', user); - -const blogpost = Schema({ - title: String, - tags: [String], - author: { - type: Schema.ObjectId, - ref: 'User' - } -}); -const BlogPost = mongoose.model('BlogPost', blogpost); - -/** - * example - */ - -mongoose.connection.on('open', function() { - /** - * Generate data - */ - - const userIds = [new ObjectId(), new ObjectId(), new ObjectId(), new ObjectId()]; - const users = []; - - users.push({ - _id: userIds[0], - name: 'mary', - friends: [userIds[1], userIds[2], userIds[3]] - }); - users.push({ - _id: userIds[1], - name: 'bob', - friends: [userIds[0], userIds[2], userIds[3]] - }); - users.push({ - _id: userIds[2], - name: 'joe', - friends: [userIds[0], userIds[1], userIds[3]] - }); - users.push({ - _id: userIds[3], - name: 'sally', - friends: [userIds[0], userIds[1], userIds[2]] - }); - - User.create(users, function(err) { - assert.ifError(err); - - const blogposts = []; - blogposts.push({ - title: 'blog 1', - tags: ['fun', 'cool'], - author: userIds[3] - }); - blogposts.push({ - title: 'blog 2', - tags: ['cool'], - author: userIds[1] - }); - blogposts.push({ - title: 'blog 3', - tags: ['fun', 'odd'], - author: userIds[2] - }); - - BlogPost.create(blogposts, function(err) { - assert.ifError(err); - - /** - * Population - */ - - BlogPost - .find({ tags: 'fun' }) - .lean() - .populate('author') - .exec(function(err, docs) { - assert.ifError(err); - - /** - * Populate the populated documents - */ - - const opts = { - path: 'author.friends', - select: 'name', - options: { limit: 2 } - }; - - BlogPost.populate(docs, opts, function(err, docs) { - assert.ifError(err); - console.log('populated'); - const s = require('util').inspect(docs, { depth: null, colors: true }); - console.log(s); - done(); - }); - }); - }); - }); -}); - -function done(err) { - if (err) console.error(err.stack); - mongoose.connection.db.dropDatabase(function() { - mongoose.connection.close(); - }); -} diff --git a/examples/population/population-basic.js b/examples/population/population-basic.js deleted file mode 100644 index a6c7ea88c7f..00000000000 --- a/examples/population/population-basic.js +++ /dev/null @@ -1,104 +0,0 @@ - -'use strict'; -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; - -console.log('Running mongoose version %s', mongoose.version); - -/** - * Console schema - */ - -const consoleSchema = Schema({ - name: String, - manufacturer: String, - released: Date -}); -const Console = mongoose.model('Console', consoleSchema); - -/** - * Game schema - */ - -const gameSchema = Schema({ - name: String, - developer: String, - released: Date, - consoles: [{ - type: Schema.Types.ObjectId, - ref: 'Console' - }] -}); -const Game = mongoose.model('Game', gameSchema); - -/** - * Connect to the console database on 127.0.0.1 with - * the default port (27017) - */ - -mongoose.connect('mongodb://127.0.0.1/console', function(err) { - // if we failed to connect, abort - if (err) throw err; - - // we connected ok - createData(); -}); - -/** - * Data generation - */ - -function createData() { - Console.create( - { - name: 'Nintendo 64', - manufacturer: 'Nintendo', - released: 'September 29, 1996' - }, - function(err, nintendo64) { - if (err) return done(err); - - Game.create({ - name: 'Legend of Zelda: Ocarina of Time', - developer: 'Nintendo', - released: new Date('November 21, 1998'), - consoles: [nintendo64] - }, - function(err) { - if (err) return done(err); - example(); - }); - } - ); -} - -/** - * Population - */ - -function example() { - Game - .findOne({ name: /^Legend of Zelda/ }) - .populate('consoles') - .exec(function(err, ocinara) { - if (err) return done(err); - - console.log( - '"%s" was released for the %s on %s', - ocinara.name, - ocinara.consoles[0].name, - ocinara.released.toLocaleDateString() - ); - - done(); - }); -} - -function done(err) { - if (err) console.error(err); - Console.remove(function() { - Game.remove(function() { - mongoose.disconnect(); - }); - }); -} diff --git a/examples/population/population-of-existing-doc.js b/examples/population/population-of-existing-doc.js deleted file mode 100644 index 4223f3ae9e4..00000000000 --- a/examples/population/population-of-existing-doc.js +++ /dev/null @@ -1,110 +0,0 @@ - -'use strict'; -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; - -console.log('Running mongoose version %s', mongoose.version); - -/** - * Console schema - */ - -const consoleSchema = Schema({ - name: String, - manufacturer: String, - released: Date -}); -const Console = mongoose.model('Console', consoleSchema); - -/** - * Game schema - */ - -const gameSchema = Schema({ - name: String, - developer: String, - released: Date, - consoles: [{ - type: Schema.Types.ObjectId, - ref: 'Console' - }] -}); -const Game = mongoose.model('Game', gameSchema); - -/** - * Connect to the console database on 127.0.0.1 with - * the default port (27017) - */ - -mongoose.connect('mongodb://127.0.0.1/console', function(err) { - // if we failed to connect, abort - if (err) throw err; - - // we connected ok - createData(); -}); - -/** - * Data generation - */ - -function createData() { - Console.create( - { - name: 'Nintendo 64', - manufacturer: 'Nintendo', - released: 'September 29, 1996' - }, - function(err, nintendo64) { - if (err) return done(err); - - Game.create({ - name: 'Legend of Zelda: Ocarina of Time', - developer: 'Nintendo', - released: new Date('November 21, 1998'), - consoles: [nintendo64] - }, - function(err) { - if (err) return done(err); - example(); - }); - } - ); -} - -/** - * Population - */ - -function example() { - Game - .findOne({ name: /^Legend of Zelda/ }) - .exec(function(err, ocinara) { - if (err) return done(err); - - console.log('"%s" console _id: %s', ocinara.name, ocinara.consoles[0]); - - // population of existing document - ocinara.populate('consoles', function(err) { - if (err) return done(err); - - console.log( - '"%s" was released for the %s on %s', - ocinara.name, - ocinara.consoles[0].name, - ocinara.released.toLocaleDateString() - ); - - done(); - }); - }); -} - -function done(err) { - if (err) console.error(err); - Console.remove(function() { - Game.remove(function() { - mongoose.disconnect(); - }); - }); -} diff --git a/examples/population/population-of-multiple-existing-docs.js b/examples/population/population-of-multiple-existing-docs.js deleted file mode 100644 index 310d0a40c05..00000000000 --- a/examples/population/population-of-multiple-existing-docs.js +++ /dev/null @@ -1,125 +0,0 @@ - -'use strict'; -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; - -console.log('Running mongoose version %s', mongoose.version); - -/** - * Console schema - */ - -const consoleSchema = Schema({ - name: String, - manufacturer: String, - released: Date -}); -const Console = mongoose.model('Console', consoleSchema); - -/** - * Game schema - */ - -const gameSchema = Schema({ - name: String, - developer: String, - released: Date, - consoles: [{ - type: Schema.Types.ObjectId, - ref: 'Console' - }] -}); -const Game = mongoose.model('Game', gameSchema); - -/** - * Connect to the console database on 127.0.0.1 with - * the default port (27017) - */ - -mongoose.connect('mongodb://127.0.0.1/console', function(err) { - // if we failed to connect, abort - if (err) throw err; - - // we connected ok - createData(); -}); - -/** - * Data generation - */ - -function createData() { - Console.create( - { - name: 'Nintendo 64', - manufacturer: 'Nintendo', - released: 'September 29, 1996' - }, - { - name: 'Super Nintendo', - manufacturer: 'Nintendo', - released: 'August 23, 1991' - }, - function(err, nintendo64, superNintendo) { - if (err) return done(err); - - Game.create( - { - name: 'Legend of Zelda: Ocarina of Time', - developer: 'Nintendo', - released: new Date('November 21, 1998'), - consoles: [nintendo64] - }, - { - name: 'Mario Kart', - developer: 'Nintendo', - released: 'September 1, 1992', - consoles: [superNintendo] - }, - function(err) { - if (err) return done(err); - example(); - } - ); - } - ); -} - -/** - * Population - */ - -function example() { - Game - .find({}) - .exec(function(err, games) { - if (err) return done(err); - - console.log('found %d games', games.length); - - const options = { path: 'consoles', select: 'name released -_id' }; - Game.populate(games, options, function(err, games) { - if (err) return done(err); - - games.forEach(function(game) { - console.log( - '"%s" was released for the %s on %s', - game.name, - game.consoles[0].name, - game.released.toLocaleDateString() - ); - }); - - done(); - }); - }); -} - -function done(err) { - if (err) console.error(err); - Console.remove(function() { - Game.remove(function() { - mongoose.disconnect(); - }); - }); -} diff --git a/examples/population/population-options.js b/examples/population/population-options.js deleted file mode 100644 index 2e75556ddd4..00000000000 --- a/examples/population/population-options.js +++ /dev/null @@ -1,139 +0,0 @@ - -'use strict'; -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; - -console.log('Running mongoose version %s', mongoose.version); - -/** - * Console schema - */ - -const consoleSchema = Schema({ - name: String, - manufacturer: String, - released: Date -}); -const Console = mongoose.model('Console', consoleSchema); - -/** - * Game schema - */ - -const gameSchema = Schema({ - name: String, - developer: String, - released: Date, - consoles: [{ - type: Schema.Types.ObjectId, - ref: 'Console' - }] -}); -const Game = mongoose.model('Game', gameSchema); - -/** - * Connect to the console database on 127.0.0.1 with - * the default port (27017) - */ - -mongoose.connect('mongodb://127.0.0.1/console', function(err) { - // if we failed to connect, abort - if (err) throw err; - - // we connected ok - createData(); -}); - -/** - * Data generation - */ - -function createData() { - Console.create( - { - name: 'Nintendo 64', - manufacturer: 'Nintendo', - released: 'September 29, 1996' - }, - { - name: 'Super Nintendo', - manufacturer: 'Nintendo', - released: 'August 23, 1991' - }, - { - name: 'XBOX 360', - manufacturer: 'Microsoft', - released: 'November 22, 2005' - }, - function(err, nintendo64, superNintendo, xbox360) { - if (err) return done(err); - - Game.create( - { - name: 'Legend of Zelda: Ocarina of Time', - developer: 'Nintendo', - released: new Date('November 21, 1998'), - consoles: [nintendo64] - }, - { - name: 'Mario Kart', - developer: 'Nintendo', - released: 'September 1, 1992', - consoles: [superNintendo] - }, - { - name: 'Perfect Dark Zero', - developer: 'Rare', - released: 'November 17, 2005', - consoles: [xbox360] - }, - function(err) { - if (err) return done(err); - example(); - } - ); - } - ); -} - -/** - * Population - */ - -function example() { - Game - .find({}) - .populate({ - path: 'consoles', - match: { manufacturer: 'Nintendo' }, - select: 'name', - options: { comment: 'population' } - }) - .exec(function(err, games) { - if (err) return done(err); - - games.forEach(function(game) { - console.log( - '"%s" was released for the %s on %s', - game.name, - game.consoles.length ? game.consoles[0].name : '??', - game.released.toLocaleDateString() - ); - }); - - return done(); - }); -} - -/** - * Clean up - */ - -function done(err) { - if (err) console.error(err); - Console.remove(function() { - Game.remove(function() { - mongoose.disconnect(); - }); - }); -} diff --git a/examples/population/population-plain-objects.js b/examples/population/population-plain-objects.js deleted file mode 100644 index ed5abe03d1e..00000000000 --- a/examples/population/population-plain-objects.js +++ /dev/null @@ -1,107 +0,0 @@ - -'use strict'; -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; - -console.log('Running mongoose version %s', mongoose.version); - -/** - * Console schema - */ - -const consoleSchema = Schema({ - name: String, - manufacturer: String, - released: Date -}); -const Console = mongoose.model('Console', consoleSchema); - -/** - * Game schema - */ - -const gameSchema = Schema({ - name: String, - developer: String, - released: Date, - consoles: [{ - type: Schema.Types.ObjectId, - ref: 'Console' - }] -}); -const Game = mongoose.model('Game', gameSchema); - -/** - * Connect to the console database on 127.0.0.1 with - * the default port (27017) - */ - -mongoose.connect('mongodb://127.0.0.1/console', function(err) { - // if we failed to connect, abort - if (err) throw err; - - // we connected ok - createData(); -}); - -/** - * Data generation - */ - -function createData() { - Console.create( - { - name: 'Nintendo 64', - manufacturer: 'Nintendo', - released: 'September 29, 1996' - }, - function(err, nintendo64) { - if (err) return done(err); - - Game.create( - { - name: 'Legend of Zelda: Ocarina of Time', - developer: 'Nintendo', - released: new Date('November 21, 1998'), - consoles: [nintendo64] - }, - function(err) { - if (err) return done(err); - example(); - } - ); - } - ); -} - -/** - * Population - */ - -function example() { - Game - .findOne({ name: /^Legend of Zelda/ }) - .populate('consoles') - .lean() // just return plain objects, not documents wrapped by mongoose - .exec(function(err, ocinara) { - if (err) return done(err); - - console.log( - '"%s" was released for the %s on %s', - ocinara.name, - ocinara.consoles[0].name, - ocinara.released.toLocaleDateString() - ); - - done(); - }); -} - -function done(err) { - if (err) console.error(err); - Console.remove(function() { - Game.remove(function() { - mongoose.disconnect(); - }); - }); -} diff --git a/examples/promises/package.json b/examples/promises/package.json deleted file mode 100644 index 19832508002..00000000000 --- a/examples/promises/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "promise-example", - "private": "true", - "version": "0.0.0", - "description": "deps for promise example", - "main": "promise.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { "async": "*" }, - "repository": "", - "author": "", - "license": "BSD" -} diff --git a/examples/promises/person.js b/examples/promises/person.js deleted file mode 100644 index 2f8f6b04299..00000000000 --- a/examples/promises/person.js +++ /dev/null @@ -1,17 +0,0 @@ - -// import the necessary modules -'use strict'; - -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; - -// create an export function to encapsulate the model creation -module.exports = function() { - // define schema - const PersonSchema = new Schema({ - name: String, - age: Number, - birthday: Date - }); - mongoose.model('Person', PersonSchema); -}; diff --git a/examples/promises/promise.js b/examples/promises/promise.js deleted file mode 100644 index a0660c9a1a0..00000000000 --- a/examples/promises/promise.js +++ /dev/null @@ -1,96 +0,0 @@ -// import async to make control flow simplier -'use strict'; - -const async = require('async'); - -// import the rest of the normal stuff -const mongoose = require('../../lib'); - -require('./person.js')(); - -const Person = mongoose.model('Person'); - -// define some dummy data -const data = [ - { - name: 'bill', - age: 25, - birthday: new Date().setFullYear((new Date().getFullYear() - 25)) - }, - { - name: 'mary', - age: 30, - birthday: new Date().setFullYear((new Date().getFullYear() - 30)) - }, - { - name: 'bob', - age: 21, - birthday: new Date().setFullYear((new Date().getFullYear() - 21)) - }, - { - name: 'lilly', - age: 26, - birthday: new Date().setFullYear((new Date().getFullYear() - 26)) - }, - { - name: 'alucard', - age: 1000, - birthday: new Date().setFullYear((new Date().getFullYear() - 1000)) - } -]; - - -mongoose.connect('mongodb://127.0.0.1/persons', function(err) { - if (err) { - throw err; - } - - // create all of the dummy people - async.each(data, function(item, cb) { - Person.create(item, cb); - }, function(err) { - if (err) { - // handle error - } - - // create a promise (get one from the query builder) - const prom = Person.find({ age: { $lt: 1000 } }).exec(); - - // add a callback on the promise. This will be called on both error and - // complete - prom.addBack(function() { - console.log('completed'); - }); - - // add a callback that is only called on complete (success) events - prom.addCallback(function() { - console.log('Successful Completion!'); - }); - - // add a callback that is only called on err (rejected) events - prom.addErrback(function() { - console.log('Fail Boat'); - }); - - // you can chain things just like in the promise/A+ spec - // note: each then() is returning a new promise, so the above methods - // that we defined will all fire after the initial promise is fulfilled - prom.then(function(people) { - // just getting the stuff for the next query - const ids = people.map(function(p) { - return p._id; - }); - - // return the next promise - return Person.find({ _id: { $nin: ids } }).exec(); - }).then(function(oldest) { - console.log('Oldest person is: %s', oldest); - }).then(cleanup); - }); -}); - -function cleanup() { - Person.remove(function() { - mongoose.disconnect(); - }); -} diff --git a/examples/querybuilder/package.json b/examples/querybuilder/package.json deleted file mode 100644 index 1a3450aa159..00000000000 --- a/examples/querybuilder/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "query-builder-example", - "private": "true", - "version": "0.0.0", - "description": "deps for query builder example", - "main": "querybuilder.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { "async": "*" }, - "repository": "", - "author": "", - "license": "BSD" -} diff --git a/examples/querybuilder/person.js b/examples/querybuilder/person.js deleted file mode 100644 index 2f8f6b04299..00000000000 --- a/examples/querybuilder/person.js +++ /dev/null @@ -1,17 +0,0 @@ - -// import the necessary modules -'use strict'; - -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; - -// create an export function to encapsulate the model creation -module.exports = function() { - // define schema - const PersonSchema = new Schema({ - name: String, - age: Number, - birthday: Date - }); - mongoose.model('Person', PersonSchema); -}; diff --git a/examples/querybuilder/querybuilder.js b/examples/querybuilder/querybuilder.js deleted file mode 100644 index a05059c001c..00000000000 --- a/examples/querybuilder/querybuilder.js +++ /dev/null @@ -1,81 +0,0 @@ - -// import async to make control flow simplier -'use strict'; - -const async = require('async'); - -// import the rest of the normal stuff -const mongoose = require('../../lib'); - -require('./person.js')(); - -const Person = mongoose.model('Person'); - -// define some dummy data -const data = [ - { - name: 'bill', - age: 25, - birthday: new Date().setFullYear((new Date().getFullYear() - 25)) - }, - { - name: 'mary', - age: 30, - birthday: new Date().setFullYear((new Date().getFullYear() - 30)) - }, - { - name: 'bob', - age: 21, - birthday: new Date().setFullYear((new Date().getFullYear() - 21)) - }, - { - name: 'lilly', - age: 26, - birthday: new Date().setFullYear((new Date().getFullYear() - 26)) - }, - { - name: 'alucard', - age: 1000, - birthday: new Date().setFullYear((new Date().getFullYear() - 1000)) - } -]; - - -mongoose.connect('mongodb://127.0.0.1/persons', function(err) { - if (err) throw err; - - // create all of the dummy people - async.each(data, function(item, cb) { - Person.create(item, cb); - }, function(err) { - if (err) throw err; - - // when querying data, instead of providing a callback, you can instead - // leave that off and get a query object returned - const query = Person.find({ age: { $lt: 1000 } }); - - // this allows you to continue applying modifiers to it - query.sort('birthday'); - query.select('name'); - - // you can chain them together as well - // a full list of methods can be found: - // http://mongoosejs.com/docs/api/query.html - query.where('age').gt(21); - - // finally, when ready to execute the query, call the exec() function - query.exec(function(err, results) { - if (err) throw err; - - console.log(results); - - cleanup(); - }); - }); -}); - -function cleanup() { - Person.remove(function() { - mongoose.disconnect(); - }); -} diff --git a/examples/redis-todo/.eslintrc.yml b/examples/redis-todo/.eslintrc.yml deleted file mode 100644 index a41589b37c8..00000000000 --- a/examples/redis-todo/.eslintrc.yml +++ /dev/null @@ -1,2 +0,0 @@ -parserOptions: - ecmaVersion: 2019 \ No newline at end of file diff --git a/examples/redis-todo/.npmrc b/examples/redis-todo/.npmrc deleted file mode 100644 index 9cf9495031e..00000000000 --- a/examples/redis-todo/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/examples/redis-todo/config.js b/examples/redis-todo/config.js deleted file mode 100644 index 5d5c1179a01..00000000000 --- a/examples/redis-todo/config.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const JWT_SECRET = 'token'; - -module.exports = { - JWT_SECRET -}; diff --git a/examples/redis-todo/db/index.js b/examples/redis-todo/db/index.js deleted file mode 100644 index c12d30bd323..00000000000 --- a/examples/redis-todo/db/index.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -const mongoose = require('mongoose'); - -mongoose.connect('mongodb://127.0.0.1/redis-todo'); diff --git a/examples/redis-todo/db/models/todoModel.js b/examples/redis-todo/db/models/todoModel.js deleted file mode 100644 index 42ebe6c1a94..00000000000 --- a/examples/redis-todo/db/models/todoModel.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -const mongoose = require('mongoose'); - -const todoSchema = new mongoose.Schema({ - text: { type: String, required: true }, - completed: { type: Boolean, default: false }, - userId: { type: mongoose.Types.ObjectId, required: true } -}, { timestamps: true, versionKey: false }); - -module.exports = mongoose.model('Todo', todoSchema); diff --git a/examples/redis-todo/db/models/userModel.js b/examples/redis-todo/db/models/userModel.js deleted file mode 100644 index b2d2919516a..00000000000 --- a/examples/redis-todo/db/models/userModel.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -const mongoose = require('mongoose'); -const jwt = require('jsonwebtoken'); -const bcrypt = require('bcryptjs'); -const JWT_SECRET = require('../../config').JWT_SECRET; - -const { Schema, model } = mongoose; - -const userSchema = new Schema({ - name: { type: String, required: true }, - username: { type: String, unique: true, required: true }, - email: { type: String, unique: true, required: true }, - passwordId: { type: mongoose.Types.ObjectId, ref: 'Password' } -}, { timestamps: true, versionKey: false }); - -const userPasswordSchema = new Schema({ - password: { type: String, required: true } -}); - -userSchema.methods.toJSON = function() { - const user = this.toObject(); // this = user - delete user.password; - delete user.email; - return user; -}; - -// creating token -userSchema.methods.genAuthToken = function() { - return jwt.sign({ userId: this._id.toString() }, JWT_SECRET); // this = user -}; - -// password hasing -userPasswordSchema.pre('save', async function(next) { - try { - if (this.isModified('password')) { - this.password = await bcrypt.hashSync(this.password, 8); - return next(); - } - next(); - } catch (err) { - return next(err); - } -}); - -module.exports = { - User: model('User', userSchema), - Password: model('Password', userPasswordSchema) -}; diff --git a/examples/redis-todo/middleware/auth.js b/examples/redis-todo/middleware/auth.js deleted file mode 100644 index 095f2620c99..00000000000 --- a/examples/redis-todo/middleware/auth.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -const jwt = require('jsonwebtoken'); -const JWT_SECRET = require('../config').JWT_SECRET; - -module.exports = async function(req, res, next) { - try { - const authToken = req.header('x-auth'); - if (!authToken) return res.status(404).send({ msg: 'AuthToken not found' }); - - const decodedValue = jwt.verify(authToken, JWT_SECRET); - if (!decodedValue) return res.status(401).send({ msg: 'Invalid Authentication' }); - - req.userId = decodedValue.userId; - next(); - } catch { - res.status(401).send({ msg: 'Invalid Authentication' }); - } -}; diff --git a/examples/redis-todo/middleware/clearCache.js b/examples/redis-todo/middleware/clearCache.js deleted file mode 100644 index 446d7d4e303..00000000000 --- a/examples/redis-todo/middleware/clearCache.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const { clearCache } = require('../services/cache'); - -module.exports = async function(req, res, next) { - await next(); // call endpoint - console.log(req.userId); - clearCache(req.userId); -}; diff --git a/examples/redis-todo/package.json b/examples/redis-todo/package.json deleted file mode 100644 index d0606f8242f..00000000000 --- a/examples/redis-todo/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "redis-todo", - "version": "1.0.0", - "description": "todo app build with express redis mongoose", - "main": "server.js", - "scripts": { - "start": "node server.js", - "dev:start": "nodemon server.js", - "fix": "standard --fix || snazzy" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/usama-asfar/redis-todo.git" - }, - "keywords": [ - "express", - "redis", - "mongoose" - ], - "author": "@usama__asfar", - "license": "MIT", - "bugs": { - "url": "https://github.com/usama-asfar/redis-todo/issues" - }, - "homepage": "https://github.com/usama-asfar/redis-todo#readme", - "dependencies": { - "bcryptjs": "^2.4.3", - "express": "^4.18.1", - "express-rate-limit": "^6.4.0", - "jsonwebtoken": "^8.5.1", - "mongoose": "^6.3.5", - "redis": "^4.1.0" - }, - "devDependencies": { - "nodemon": "^2.0.16", - "morgan": "^1.9.1", - "snazzy": "^9.0.0", - "standard": "^17.0.0" - } -} diff --git a/examples/redis-todo/routers/todoRouter.js b/examples/redis-todo/routers/todoRouter.js deleted file mode 100644 index b88174f72b6..00000000000 --- a/examples/redis-todo/routers/todoRouter.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -const Router = require('express').Router(); -const Todo = require('../db/models/todoModel'); -const auth = require('../middleware/auth'); -const clearCache = require('../middleware/clearCache'); - -/* @api private - * @func: fetch all user todos - * @input: user id - * @return: todos - */ -Router.get('/all', auth, async function({ userId }, res) { - try { - res.status(200).json({ todos: await Todo.find({ userId }).sort({ createdAt: -1 }).cache({ key: userId }) }); - } catch (err) { - console.log(err); - res.status(501).send('Server Error'); - } -}); - -/* @api private - * @func: create todo - * @input: todo data, userid - * @return: todo - */ -Router.post('/create', auth, clearCache, async function({ userId, body }, res) { - try { - const todo = new Todo({ - text: body.text, - completed: body.completed, - userId - }); - await todo.save(); - res.status(201).json({ todo }); - } catch { - res.status(501).send('Server Error'); - } -}); - -/* @api private - * @func: update todo - * @input: todo data, todoId, userid - * @return: updated todo - */ -Router.post('/update', auth, async function({ userId, body }, res) { - try { - const updatedTodo = await Todo.findOneAndUpdate({ $and: [{ userId }, { _id: body.todoId }] }, - { ...body }, { new: true, sanitizeFilter: true } - ); - if (!updatedTodo) return res.status(404).send({ msg: 'Todo not found' }); - - await updatedTodo.save(); - res.status(200).json({ todo: updatedTodo }); - } catch { - res.status(501).send('Server Error'); - } -}); - -/* @api private - * @func: delete todo - * @input: todoId, userid - */ -Router.delete('/delete', auth, async function({ userId, body: { todoId } }, res) { - try { - await Todo.findOneAndDelete({ $and: [{ userId }, { _id: todoId }] }); - res.status(200).send({ msg: 'Todo deleted' }); - } catch { - res.status(501).send('Server Error'); - } -}); - -module.exports = Router; diff --git a/examples/redis-todo/routers/userRouter.js b/examples/redis-todo/routers/userRouter.js deleted file mode 100644 index 763808df46d..00000000000 --- a/examples/redis-todo/routers/userRouter.js +++ /dev/null @@ -1,98 +0,0 @@ -'use strict'; - -const Router = require('express').Router(); -const bcrypt = require('bcryptjs'); -const { User, Password } = require('../db/models/userModel'); -const Todo = require('../db/models/todoModel'); -const auth = require('../middleware/auth'); - -/* @public - * @func: create new user - * @input: username,name,email and password - * @return: auth token - */ -Router.post('/create', async function({ body }, res) { - try { - // storing password - const password = new Password({ password: body.password }); - const user = new User({ - name: body.name, - username: body.username, - email: body.email, - passwordId: password._id - }); // body = user data - - // gen auth token - const token = await user.genAuthToken(); - - // hashing password - await password.save(); - await user.save(); - res.status(201).json({ token }); - } catch (err) { - console.log(err); - res.status(501).send('Server Error'); - } -}); - -/* @public - * @func: login user - * @input: user/email, password - * @return: auth token - */ -Router.post('/login', async function({ body }, res) { - try { - const user = await User.findOne( - { $or: [{ email: body.email }, { username: body.username }] } - ).populate('passwordId'); - if (!user) return res.status(404).send({ msg: 'Invalid credential' }); - - const isPassword = await bcrypt.compare(body.password, user.passwordId.password); - if (!isPassword) return res.status(404).send({ msg: 'Invalid credential' }); - - const token = user.genAuthToken(); - res.status(201).json({ token }); - } catch { - res.status(501).send('Server Error'); - } -}); - -/* @api private - * @func: edit user - * @input: username, name or password - * @return: edited user - */ -Router.post('/update', auth, async function({ userId, body }, res) { - try { - const updatedUser = await User.findByIdAndUpdate( - { _id: userId }, - { ...body }, - { new: true }); - - // if password then hash it - if (body.password) { - const password = await Password.findById({ _id: updatedUser.passwordId }); - password.password = body.password; - password.save(); // hashing password - } - - res.status(200).json({ user: updatedUser }); - } catch { - res.status(500).send('Server Error'); - } -}); - -/* @api private - * @func: delete user - */ -Router.delete('/delete', auth, async function({ userId }, res) { - try { - await User.findByIdAndRemove({ _id: userId }); - await Todo.deleteMany({ userId }); - res.status(200).send({ msg: 'User deleted' }); - } catch { - res.status(501).send('Server Error'); - } -}); - -module.exports = Router; diff --git a/examples/redis-todo/server.js b/examples/redis-todo/server.js deleted file mode 100644 index 8c8b47fe537..00000000000 --- a/examples/redis-todo/server.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -const http = require('http'); -const express = require('express'); -const rateLimit = require('express-rate-limit'); - -// DB -require('./db'); -require('./services/cache'); - -const limiter = rateLimit({ - windowMs: 1 * 60 * 1000, // 1 minute - max: 100 -}); - -const app = express(); -app.use(express.json()); - -app.use(limiter); - -// morgan test -app.use(require('morgan')('dev')); - -// ROUTERS -app.use('/user', require('./routers/userRouter')); -app.use('/todo', require('./routers/todoRouter')); - -// Server setup -const httpServer = http.createServer(app); -const PORT = process.env.PORT || 5000; -httpServer.listen(PORT, () => { - console.log(`Server up at PORT:${PORT}`); -}); diff --git a/examples/redis-todo/services/cache.js b/examples/redis-todo/services/cache.js deleted file mode 100644 index 6b2f1adfa81..00000000000 --- a/examples/redis-todo/services/cache.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -const mongoose = require('mongoose'); -const redis = require('redis'); - -// setting up redis server -const client = redis.createClient(); -client.connect().then(); -const exec = mongoose.Query.prototype.exec; - -mongoose.Query.prototype.cache = function(options = {}) { - this.useCache = true; - // setting up primary user key - this.hashKey = JSON.stringify(options.key || ''); - return this; -}; - -mongoose.Query.prototype.exec = async function() { - if (!this.useCache) return exec.apply(this, arguments); - - // setting up query key - const key = JSON.stringify(Object.assign({}, - this.getQuery(), { collection: this.mongooseCollection.name }) - ); - - // looking for cache - const cacheData = await client.hGet(this.hashKey, key).catch((err) => console.log(err)); - if (cacheData) { - console.log('from redis'); - const doc = JSON.parse(cacheData); - // inserting doc to make as actual mongodb query - return Array.isArray(doc) ? doc.map(d => new this.model(d)) : new this.model(doc); - } - - const result = await exec.apply(this, arguments); - client.hSet(this.hashKey, key, JSON.stringify(result)); - return result; -}; - -module.exports = { - clearCache(hashKey) { - client.del(JSON.stringify(hashKey)); - } -}; diff --git a/examples/replicasets/package.json b/examples/replicasets/package.json deleted file mode 100644 index 927dfd24b83..00000000000 --- a/examples/replicasets/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "replica-set-example", - "private": "true", - "version": "0.0.0", - "description": "deps for replica set example", - "main": "querybuilder.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { "async": "*" }, - "repository": "", - "author": "", - "license": "BSD" -} diff --git a/examples/replicasets/person.js b/examples/replicasets/person.js deleted file mode 100644 index 2f8f6b04299..00000000000 --- a/examples/replicasets/person.js +++ /dev/null @@ -1,17 +0,0 @@ - -// import the necessary modules -'use strict'; - -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; - -// create an export function to encapsulate the model creation -module.exports = function() { - // define schema - const PersonSchema = new Schema({ - name: String, - age: Number, - birthday: Date - }); - mongoose.model('Person', PersonSchema); -}; diff --git a/examples/replicasets/replica-sets.js b/examples/replicasets/replica-sets.js deleted file mode 100644 index cb9b91df7e8..00000000000 --- a/examples/replicasets/replica-sets.js +++ /dev/null @@ -1,73 +0,0 @@ - -// import async to make control flow simplier -'use strict'; - -const async = require('async'); - -// import the rest of the normal stuff -const mongoose = require('../../lib'); - -require('./person.js')(); - -const Person = mongoose.model('Person'); - -// define some dummy data -const data = [ - { - name: 'bill', - age: 25, - birthday: new Date().setFullYear((new Date().getFullYear() - 25)) - }, - { - name: 'mary', - age: 30, - birthday: new Date().setFullYear((new Date().getFullYear() - 30)) - }, - { - name: 'bob', - age: 21, - birthday: new Date().setFullYear((new Date().getFullYear() - 21)) - }, - { - name: 'lilly', - age: 26, - birthday: new Date().setFullYear((new Date().getFullYear() - 26)) - }, - { - name: 'alucard', - age: 1000, - birthday: new Date().setFullYear((new Date().getFullYear() - 1000)) - } -]; - - -// to connect to a replica set, pass in the comma delimited uri and optionally -// any connection options such as the rs_name. -const opts = { - replSet: { rs_name: 'rs0' } -}; -mongoose.connect('mongodb://127.0.0.1:27018/persons,127.0.0.1:27019,127.0.0.1:27020', opts, function(err) { - if (err) throw err; - - // create all of the dummy people - async.each(data, function(item, cb) { - Person.create(item, cb); - }, function(err) { - if (err) { - // handle error - } - - // create and delete some data - const prom = Person.find({ age: { $lt: 1000 } }).exec(); - - prom.then(function(people) { - console.log('young people: %s', people); - }).then(cleanup); - }); -}); - -function cleanup() { - Person.remove(function() { - mongoose.disconnect(); - }); -} diff --git a/examples/schema/schema.js b/examples/schema/schema.js deleted file mode 100644 index be82788ae59..00000000000 --- a/examples/schema/schema.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Module dependencies. - */ - -'use strict'; - -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; - -/** - * Schema definition - */ - -// recursive embedded-document schema - -const Comment = new Schema(); - -Comment.add({ - title: { - type: String, - index: true - }, - date: Date, - body: String, - comments: [Comment] -}); - -const BlogPost = new Schema({ - title: { - type: String, - index: true - }, - slug: { - type: String, - lowercase: true, - trim: true - }, - date: Date, - buf: Buffer, - comments: [Comment], - creator: Schema.ObjectId -}); - -const Person = new Schema({ - name: { - first: String, - last: String - }, - email: { - type: String, - required: true, - index: { - unique: true, - sparse: true - } - }, - alive: Boolean -}); - -/** - * Accessing a specific schema type by key - */ - -BlogPost.path('date') - .default(function() { - return new Date(); - }) - .set(function(v) { - return v === 'now' ? new Date() : v; - }); - -/** - * Pre hook. - */ - -BlogPost.pre('save', function(next, done) { - /* global emailAuthor */ - emailAuthor(done); // some async function - next(); -}); - -/** - * Methods - */ - -BlogPost.methods.findCreator = function(callback) { - return this.db.model('Person').findById(this.creator, callback); -}; - -BlogPost.statics.findByTitle = function(title, callback) { - return this.find({ title: title }, callback); -}; - -BlogPost.methods.expressiveQuery = function(creator, date, callback) { - return this.find('creator', creator).where('date').gte(date).run(callback); -}; - -/** - * Plugins - */ - -function slugGenerator(options) { - options = options || {}; - const key = options.key || 'title'; - - return function slugGenerator(schema) { - schema.path(key).set(function(v) { - this.slug = v.toLowerCase().replace(/[^a-z0-9]/g, '').replace(/-+/g, ''); - return v; - }); - }; -} - -BlogPost.plugin(slugGenerator()); - -/** - * Define model. - */ - -mongoose.model('BlogPost', BlogPost); -mongoose.model('Person', Person); diff --git a/examples/schema/storing-schemas-as-json/index.js b/examples/schema/storing-schemas-as-json/index.js deleted file mode 100644 index b20717d2ce6..00000000000 --- a/examples/schema/storing-schemas-as-json/index.js +++ /dev/null @@ -1,29 +0,0 @@ - -// modules -'use strict'; - -const mongoose = require('../../../lib'); -const Schema = mongoose.Schema; - -// parse json -const raw = require('./schema.json'); - -// create a schema -const timeSignatureSchema = Schema(raw); - -// compile the model -const TimeSignature = mongoose.model('TimeSignatures', timeSignatureSchema); - -// create a TimeSignature document -const threeFour = new TimeSignature({ - count: 3, - unit: 4, - description: '3/4', - additive: false, - created: new Date(), - links: ['http://en.wikipedia.org/wiki/Time_signature'], - user_id: '518d31a0ef32bbfa853a9814' -}); - -// print its description -console.log(threeFour); diff --git a/examples/schema/storing-schemas-as-json/schema.json b/examples/schema/storing-schemas-as-json/schema.json deleted file mode 100644 index 5afc626ccab..00000000000 --- a/examples/schema/storing-schemas-as-json/schema.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "count": "number", - "unit": "number", - "description": "string", - "links": ["string"], - "created": "date", - "additive": "boolean", - "user_id": "ObjectId" -} diff --git a/examples/statics/person.js b/examples/statics/person.js deleted file mode 100644 index 8af10c92c14..00000000000 --- a/examples/statics/person.js +++ /dev/null @@ -1,22 +0,0 @@ -// import the necessary modules -'use strict'; - -const mongoose = require('../../lib'); -const Schema = mongoose.Schema; - -// create an export function to encapsulate the model creation -module.exports = function() { - // define schema - const PersonSchema = new Schema({ - name: String, - age: Number, - birthday: Date - }); - - // define a static - PersonSchema.statics.findPersonByName = function(name, cb) { - this.find({ name: new RegExp(name, 'i') }, cb); - }; - - mongoose.model('Person', PersonSchema); -}; diff --git a/examples/statics/statics.js b/examples/statics/statics.js deleted file mode 100644 index b1e5aabb867..00000000000 --- a/examples/statics/statics.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; -const mongoose = require('../../lib'); - - -// import the schema -require('./person.js')(); - -// grab the person model object -const Person = mongoose.model('Person'); - -// connect to a server to do a quick write / read example -run().catch(console.error); - -async function run() { - await mongoose.connect('mongodb://127.0.0.1/persons'); - const bill = await Person.create({ - name: 'bill', - age: 25, - birthday: new Date().setFullYear((new Date().getFullYear() - 25)) - }); - console.log('People added to db: %s', bill.toString()); - - // using the static - const result = await Person.findPersonByName('bill'); - - console.log(result); - await cleanup(); -} - -async function cleanup() { - await Person.remove(); - mongoose.disconnect(); -} From d24762de5abcf3cb268b194f1315a97869d210df Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 25 Aug 2025 17:14:22 -0400 Subject: [PATCH 138/209] BREAKING CHANGE: remove support for callbacks in pre middleware Fix #11531 --- docs/migrating_to_9.md | 20 +++++-- lib/helpers/timestamps/setupTimestamps.js | 9 +-- lib/model.js | 1 - lib/plugins/validateBeforeSave.js | 2 +- package.json | 2 +- test/aggregate.test.js | 27 +++++---- test/docs/discriminators.test.js | 6 +- test/document.modified.test.js | 3 +- test/document.test.js | 55 +++++++----------- test/index.test.js | 3 +- test/model.create.test.js | 20 +++---- test/model.discriminator.test.js | 36 ++++-------- test/model.insertMany.test.js | 6 +- test/model.middleware.test.js | 65 +++++----------------- test/model.test.js | 50 ++++++----------- test/model.updateOne.test.js | 6 +- test/query.cursor.test.js | 7 +-- test/query.middleware.test.js | 26 ++++----- test/query.test.js | 3 +- test/types.documentarray.test.js | 6 +- test/types/middleware.preposttypes.test.ts | 8 +-- test/types/middleware.test.ts | 37 +++++------- test/types/schema.test.ts | 6 +- types/index.d.ts | 3 - types/middlewares.d.ts | 2 - 25 files changed, 151 insertions(+), 258 deletions(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 1ac8d33a448..1f4250525b6 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -185,12 +185,14 @@ const { promiseOrCallback } = require('mongoose'); promiseOrCallback; // undefined in Mongoose 9 ``` -## In `isAsync` middleware `next()` errors take priority over `done()` errors +## `isAsync` middleware no longer supported -Due to Mongoose middleware now relying on promises and async/await, `next()` errors take priority over `done()` errors. -If you use `isAsync` middleware, any errors in `next()` will be thrown first, and `done()` errors will only be thrown if there are no `next()` errors. +Mongoose 9 no longer supports `isAsync` middleware. Middleware functions that use the legacy signature with both `next` and `done` callbacks (i.e., `function(next, done)`) are not supported. We recommend middleware now use promises or async/await. + +If you have code that uses `isAsync` middleware, you must refactor it to use async functions or return a promise instead. ```javascript +// ❌ Not supported in Mongoose 9 const schema = new Schema({}); schema.pre('save', true, function(next, done) { @@ -214,8 +216,16 @@ schema.pre('save', true, function(next, done) { 25); }); -// In Mongoose 8, with the above middleware, `save()` would error with 'first done() error' -// In Mongoose 9, with the above middleware, `save()` will error with 'second next() error' +// ✅ Supported in Mongoose 9: use async functions or return a promise +schema.pre('save', async function() { + execed.first = true; + await new Promise(resolve => setTimeout(resolve, 5)); +}); + +schema.pre('save', async function() { + execed.second = true; + await new Promise(resolve => setTimeout(resolve, 25)); +}); ``` ## Removed `skipOriginalStackTraces` option diff --git a/lib/helpers/timestamps/setupTimestamps.js b/lib/helpers/timestamps/setupTimestamps.js index f6ba12b98b6..cdeca8a2296 100644 --- a/lib/helpers/timestamps/setupTimestamps.js +++ b/lib/helpers/timestamps/setupTimestamps.js @@ -42,15 +42,13 @@ module.exports = function setupTimestamps(schema, timestamps) { schema.add(schemaAdditions); - schema.pre('save', function timestampsPreSave(next) { + schema.pre('save', function timestampsPreSave() { const timestampOption = get(this, '$__.saveOptions.timestamps'); if (timestampOption === false) { - return next(); + return; } setDocumentTimestamps(this, timestampOption, currentTime, createdAt, updatedAt); - - next(); }); schema.methods.initializeTimestamps = function() { @@ -88,7 +86,7 @@ module.exports = function setupTimestamps(schema, timestamps) { schema.pre('updateOne', opts, _setTimestampsOnUpdate); schema.pre('updateMany', opts, _setTimestampsOnUpdate); - function _setTimestampsOnUpdate(next) { + function _setTimestampsOnUpdate() { const now = currentTime != null ? currentTime() : this.model.base.now(); @@ -105,6 +103,5 @@ module.exports = function setupTimestamps(schema, timestamps) { replaceOps.has(this.op) ); applyTimestampsToChildren(now, this.getUpdate(), this.model.schema); - next(); } }; diff --git a/lib/model.js b/lib/model.js index de4998a47d4..0493cbc9453 100644 --- a/lib/model.js +++ b/lib/model.js @@ -2914,7 +2914,6 @@ Model.insertMany = async function insertMany(arr, options) { await this._middleware.execPost('insertMany', this, [arr], { error }); } - options = options || {}; const ThisModel = this; const limit = options.limit || 1000; diff --git a/lib/plugins/validateBeforeSave.js b/lib/plugins/validateBeforeSave.js index 6d1cebdd9d5..627d4bbc9db 100644 --- a/lib/plugins/validateBeforeSave.js +++ b/lib/plugins/validateBeforeSave.js @@ -6,7 +6,7 @@ module.exports = function validateBeforeSave(schema) { const unshift = true; - schema.pre('save', false, async function validateBeforeSave(_next, options) { + schema.pre('save', false, async function validateBeforeSave(options) { // Nested docs have their own presave if (this.$isSubdocument) { return; diff --git a/package.json b/package.json index 8700cd1d270..43640e0c28a 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "type": "commonjs", "license": "MIT", "dependencies": { - "kareem": "git+https://github.com/mongoosejs/kareem.git#vkarpov15/v3", + "kareem": "git+https://github.com/mongoosejs/kareem.git#vkarpov15/remove-isasync", "mongodb": "~6.18.0", "mpath": "0.9.0", "mquery": "5.0.0", diff --git a/test/aggregate.test.js b/test/aggregate.test.js index cb091e0eb23..dd5c2cfa78c 100644 --- a/test/aggregate.test.js +++ b/test/aggregate.test.js @@ -886,9 +886,9 @@ describe('aggregate: ', function() { const s = new Schema({ name: String }); let called = 0; - s.pre('aggregate', function(next) { + s.pre('aggregate', function() { ++called; - next(); + return Promise.resolve(); }); const M = db.model('Test', s); @@ -902,9 +902,9 @@ describe('aggregate: ', function() { it('setting option in pre (gh-7606)', async function() { const s = new Schema({ name: String }); - s.pre('aggregate', function(next) { + s.pre('aggregate', function() { this.options.collation = { locale: 'en_US', strength: 1 }; - next(); + return Promise.resolve(); }); const M = db.model('Test', s); @@ -920,9 +920,9 @@ describe('aggregate: ', function() { it('adding to pipeline in pre (gh-8017)', async function() { const s = new Schema({ name: String }); - s.pre('aggregate', function(next) { + s.pre('aggregate', function() { this.append({ $limit: 1 }); - next(); + return Promise.resolve(); }); const M = db.model('Test', s); @@ -980,8 +980,8 @@ describe('aggregate: ', function() { const s = new Schema({ name: String }); const calledWith = []; - s.pre('aggregate', function(next) { - next(new Error('woops')); + s.pre('aggregate', function() { + return Promise.reject(new Error('woops')); }); s.post('aggregate', function(error, res, next) { calledWith.push(error); @@ -1003,9 +1003,9 @@ describe('aggregate: ', function() { let calledPre = 0; let calledPost = 0; - s.pre('aggregate', function(next) { + s.pre('aggregate', function() { ++calledPre; - next(); + return Promise.resolve(); }); s.post('aggregate', function(res, next) { ++calledPost; @@ -1030,9 +1030,9 @@ describe('aggregate: ', function() { let calledPre = 0; const calledPost = []; - s.pre('aggregate', function(next) { + s.pre('aggregate', function() { ++calledPre; - next(); + return Promise.resolve(); }); s.post('aggregate', function(res, next) { calledPost.push(res); @@ -1295,11 +1295,10 @@ describe('aggregate: ', function() { it('cursor() errors out if schema pre aggregate hook throws an error (gh-15279)', async function() { const schema = new Schema({ name: String }); - schema.pre('aggregate', function(next) { + schema.pre('aggregate', function() { if (!this.options.allowed) { throw new Error('Unauthorized aggregate operation: only allowed operations are permitted'); } - next(); }); const Test = db.model('Test', schema); diff --git a/test/docs/discriminators.test.js b/test/docs/discriminators.test.js index f593f814ed7..09ee82b3016 100644 --- a/test/docs/discriminators.test.js +++ b/test/docs/discriminators.test.js @@ -164,17 +164,15 @@ describe('discriminator docs', function() { const eventSchema = new mongoose.Schema({ time: Date }, options); let eventSchemaCalls = 0; - eventSchema.pre('validate', function(next) { + eventSchema.pre('validate', function() { ++eventSchemaCalls; - next(); }); const Event = mongoose.model('GenericEvent', eventSchema); const clickedLinkSchema = new mongoose.Schema({ url: String }, options); let clickedSchemaCalls = 0; - clickedLinkSchema.pre('validate', function(next) { + clickedLinkSchema.pre('validate', function() { ++clickedSchemaCalls; - next(); }); const ClickedLinkEvent = Event.discriminator('ClickedLinkEvent', clickedLinkSchema); diff --git a/test/document.modified.test.js b/test/document.modified.test.js index 4cacfafc9eb..73d78bfc695 100644 --- a/test/document.modified.test.js +++ b/test/document.modified.test.js @@ -323,9 +323,8 @@ describe('document modified', function() { }); let preCalls = 0; - childSchema.pre('save', function(next) { + childSchema.pre('save', function() { ++preCalls; - next(); }); let postCalls = 0; diff --git a/test/document.test.js b/test/document.test.js index 07daf61080c..0dfc7d8ca4d 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -2210,9 +2210,8 @@ describe('document', function() { }, { _id: false, id: false }); let userHookCount = 0; - userSchema.pre('save', function(next) { + userSchema.pre('save', function() { ++userHookCount; - next(); }); const eventSchema = new mongoose.Schema({ @@ -2221,9 +2220,8 @@ describe('document', function() { }); let eventHookCount = 0; - eventSchema.pre('save', function(next) { + eventSchema.pre('save', function() { ++eventHookCount; - next(); }); const Event = db.model('Event', eventSchema); @@ -2785,9 +2783,8 @@ describe('document', function() { const childSchema = new Schema({ count: Number }); let preCalls = 0; - childSchema.pre('save', function(next) { + childSchema.pre('save', function() { ++preCalls; - next(); }); const SingleNestedSchema = new Schema({ @@ -2981,10 +2978,9 @@ describe('document', function() { name: String }); - ChildSchema.pre('save', function(next) { + ChildSchema.pre('save', function() { assert.ok(this.isModified('name')); ++called; - next(); }); const ParentSchema = new Schema({ @@ -3316,9 +3312,8 @@ describe('document', function() { }); const called = {}; - ChildSchema.pre('deleteOne', { document: true, query: false }, function(next) { + ChildSchema.pre('deleteOne', { document: true, query: false }, function() { called[this.name] = true; - next(); }); const ParentSchema = new Schema({ @@ -4245,9 +4240,8 @@ describe('document', function() { name: String }, { timestamps: true, versionKey: null }); - schema.pre('save', function(next) { + schema.pre('save', function() { this.$where = { updatedAt: this.updatedAt }; - next(); }); schema.post('save', function(error, res, next) { @@ -4331,9 +4325,8 @@ describe('document', function() { }); let count = 0; - childSchema.pre('validate', function(next) { + childSchema.pre('validate', function() { ++count; - next(); }); const parentSchema = new Schema({ @@ -4371,9 +4364,8 @@ describe('document', function() { }); let count = 0; - childSchema.pre('validate', function(next) { + childSchema.pre('validate', function() { ++count; - next(); }); const parentSchema = new Schema({ @@ -4949,8 +4941,8 @@ describe('document', function() { it('handles errors in subdoc pre validate (gh-5215)', async function() { const childSchema = new mongoose.Schema({}); - childSchema.pre('validate', function(next) { - next(new Error('child pre validate')); + childSchema.pre('validate', function() { + throw new Error('child pre validate'); }); const parentSchema = new mongoose.Schema({ @@ -6034,11 +6026,10 @@ describe('document', function() { e: { type: String } }); - MainSchema.pre('save', function(next) { + MainSchema.pre('save', function() { if (this.isModified()) { this.set('a.c', 100, Number); } - next(); }); const Main = db.model('Test', MainSchema); @@ -8561,13 +8552,12 @@ describe('document', function() { const owners = []; // Middleware to set a default location name derived from the parent organization doc - locationSchema.pre('validate', function(next) { + locationSchema.pre('validate', function() { const owner = this.ownerDocument(); owners.push(owner); if (this.isNew && !this.get('name') && owner.get('name')) { this.set('name', `${owner.get('name')} Office`); } - next(); }); const organizationSchema = Schema({ @@ -10101,9 +10091,8 @@ describe('document', function() { } }, {}); let count = 0; - SubSchema.pre('deleteOne', { document: true, query: false }, function(next) { + SubSchema.pre('deleteOne', { document: true, query: false }, function() { count++; - next(); }); const thisSchema = new Schema({ foo: { @@ -10301,10 +10290,8 @@ describe('document', function() { observers: [observerSchema] }); - entrySchema.pre('save', function(next) { + entrySchema.pre('save', function() { this.observers = [{ user: this.creator }]; - - next(); }); const Test = db.model('Test', entrySchema); @@ -10984,15 +10971,13 @@ describe('document', function() { const Book = db.model('Test', BookSchema); - function disallownumflows(next) { + function disallownumflows() { const self = this; - if (self.isNew) return next(); + if (self.isNew) return; if (self.quantity === 27) { - return next(new Error('Wrong Quantity')); + throw new Error('Wrong Quantity'); } - - next(); } const { _id } = await Book.create({ name: 'Hello', price: 50, quantity: 25 }); @@ -13859,17 +13844,15 @@ describe('document', function() { postDeleteOne: 0 }; let postDeleteOneError = null; - ChildSchema.pre('save', function(next) { + ChildSchema.pre('save', function() { ++called.preSave; - next(); }); ChildSchema.post('save', function(subdoc, next) { ++called.postSave; next(); }); - ChildSchema.pre('deleteOne', { document: true, query: false }, function(next) { + ChildSchema.pre('deleteOne', { document: true, query: false }, function() { ++called.preDeleteOne; - next(); }); ChildSchema.post('deleteOne', { document: true, query: false }, function(subdoc, next) { ++called.postDeleteOne; diff --git a/test/index.test.js b/test/index.test.js index cfbd644f1f3..4b0dbb9d30c 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -302,9 +302,8 @@ describe('mongoose module:', function() { mong.plugin(function(s) { calls.push(s); - s.pre('save', function(next) { + s.pre('save', function() { ++preSaveCalls; - next(); }); s.methods.testMethod = function() { return 42; }; diff --git a/test/model.create.test.js b/test/model.create.test.js index d587e70ae16..4f1fdf0d4b0 100644 --- a/test/model.create.test.js +++ b/test/model.create.test.js @@ -79,14 +79,15 @@ describe('model', function() { }); let startTime, endTime; - SchemaWithPreSaveHook.pre('save', true, function hook(next, done) { - setTimeout(function() { - countPre++; - if (countPre === 1) startTime = Date.now(); - else if (countPre === 4) endTime = Date.now(); - next(); - done(); - }, 100); + SchemaWithPreSaveHook.pre('save', function hook() { + return new Promise(resolve => { + setTimeout(() => { + countPre++; + if (countPre === 1) startTime = Date.now(); + else if (countPre === 4) endTime = Date.now(); + resolve(); + }, 100); + }); }); SchemaWithPreSaveHook.post('save', function() { countPost++; @@ -182,10 +183,9 @@ describe('model', function() { const Count = db.model('gh4038', countSchema); - testSchema.pre('save', async function(next) { + testSchema.pre('save', async function() { const doc = await Count.findOneAndUpdate({}, { $inc: { n: 1 } }, { new: true, upsert: true }); this.reference = doc.n; - next(); }); const Test = db.model('gh4038Test', testSchema); diff --git a/test/model.discriminator.test.js b/test/model.discriminator.test.js index 9fa5c7c036f..d369b1096a2 100644 --- a/test/model.discriminator.test.js +++ b/test/model.discriminator.test.js @@ -55,8 +55,7 @@ EmployeeSchema.statics.findByDepartment = function() { EmployeeSchema.path('department').validate(function(value) { return /[a-zA-Z]/.test(value); }, 'Invalid name'); -const employeeSchemaPreSaveFn = function(next) { - next(); +const employeeSchemaPreSaveFn = function() { }; EmployeeSchema.pre('save', employeeSchemaPreSaveFn); EmployeeSchema.set('toObject', { getters: true, virtuals: false }); @@ -396,9 +395,8 @@ describe('model', function() { it('deduplicates hooks (gh-2945)', function() { let called = 0; - function middleware(next) { + function middleware() { ++called; - next(); } function ActivityBaseSchema() { @@ -584,14 +582,12 @@ describe('model', function() { }); let childCalls = 0; let childValidateCalls = 0; - const preValidate = function preValidate(next) { + const preValidate = function preValidate() { ++childValidateCalls; - next(); }; childSchema.pre('validate', preValidate); - childSchema.pre('save', function(next) { + childSchema.pre('save', function() { ++childCalls; - next(); }); const personSchema = new Schema({ @@ -603,9 +599,8 @@ describe('model', function() { heir: childSchema }); let parentCalls = 0; - parentSchema.pre('save', function(next) { + parentSchema.pre('save', function() { ++parentCalls; - next(); }); const Person = db.model('Person', personSchema); @@ -1258,18 +1253,16 @@ describe('model', function() { { message: String }, { discriminatorKey: 'kind', _id: false } ); - eventSchema.pre('validate', function(next) { + eventSchema.pre('validate', function() { counters.eventPreValidate++; - next(); }); eventSchema.post('validate', function() { counters.eventPostValidate++; }); - eventSchema.pre('save', function(next) { + eventSchema.pre('save', function() { counters.eventPreSave++; - next(); }); eventSchema.post('save', function() { @@ -1280,18 +1273,16 @@ describe('model', function() { product: String }, { _id: false }); - purchasedSchema.pre('validate', function(next) { + purchasedSchema.pre('validate', function() { counters.purchasePreValidate++; - next(); }); purchasedSchema.post('validate', function() { counters.purchasePostValidate++; }); - purchasedSchema.pre('save', function(next) { + purchasedSchema.pre('save', function() { counters.purchasePreSave++; - next(); }); purchasedSchema.post('save', function() { @@ -2348,9 +2339,8 @@ describe('model', function() { }); const subdocumentPreSaveHooks = []; - subdocumentSchema.pre('save', function(next) { + subdocumentSchema.pre('save', function() { subdocumentPreSaveHooks.push(this); - next(); }); const schema = mongoose.Schema({ @@ -2359,9 +2349,8 @@ describe('model', function() { }, { discriminatorKey: 'type' }); const documentPreSaveHooks = []; - schema.pre('save', function(next) { + schema.pre('save', function() { documentPreSaveHooks.push(this); - next(); }); const Document = db.model('Document', schema); @@ -2369,9 +2358,8 @@ describe('model', function() { const discriminatorSchema = mongoose.Schema({}); const discriminatorPreSaveHooks = []; - discriminatorSchema.pre('save', function(next) { + discriminatorSchema.pre('save', function() { discriminatorPreSaveHooks.push(this); - next(); }); const Discriminator = Document.discriminator('Discriminator', discriminatorSchema); diff --git a/test/model.insertMany.test.js b/test/model.insertMany.test.js index 5b8e8270738..98b18fc68dd 100644 --- a/test/model.insertMany.test.js +++ b/test/model.insertMany.test.js @@ -279,18 +279,16 @@ describe('insertMany()', function() { }); let calledPre = 0; let calledPost = 0; - schema.pre('insertMany', function(next, docs) { + schema.pre('insertMany', function(docs) { assert.equal(docs.length, 2); assert.equal(docs[0].name, 'Star Wars'); ++calledPre; - next(); }); - schema.pre('insertMany', function(next, docs) { + schema.pre('insertMany', function(docs) { assert.equal(docs.length, 2); assert.equal(docs[0].name, 'Star Wars'); docs[0].name = 'A New Hope'; ++calledPre; - next(); }); schema.post('insertMany', function() { ++calledPost; diff --git a/test/model.middleware.test.js b/test/model.middleware.test.js index 4955fa2051f..7e054e546af 100644 --- a/test/model.middleware.test.js +++ b/test/model.middleware.test.js @@ -135,14 +135,12 @@ describe('model middleware', function() { }); let count = 0; - schema.pre('validate', function(next) { + schema.pre('validate', function() { assert.equal(count++, 0); - next(); }); - schema.pre('save', function(next) { + schema.pre('save', function() { assert.equal(count++, 1); - next(); }); const Book = db.model('Test', schema); @@ -162,14 +160,13 @@ describe('model middleware', function() { called++; }); - schema.pre('save', function(next) { + schema.pre('save', function() { called++; - next(new Error('Error 101')); + throw new Error('Error 101'); }); - schema.pre('deleteOne', { document: true, query: false }, function(next) { + schema.pre('deleteOne', { document: true, query: false }, function() { called++; - next(); }); const TestMiddleware = db.model('TestMiddleware', schema); @@ -242,11 +239,10 @@ describe('model middleware', function() { const childPreCallsByName = {}; let parentPreCalls = 0; - childSchema.pre('save', function(next) { + childSchema.pre('save', function() { childPreCallsByName[this.name] = childPreCallsByName[this.name] || 0; ++childPreCallsByName[this.name]; ++childPreCalls; - next(); }); const parentSchema = new mongoose.Schema({ @@ -254,9 +250,8 @@ describe('model middleware', function() { children: [childSchema] }); - parentSchema.pre('save', function(next) { + parentSchema.pre('save', function() { ++parentPreCalls; - next(); }); const Parent = db.model('Parent', parentSchema); @@ -311,32 +306,6 @@ describe('model middleware', function() { } }); - it('sync error in pre save after next() (gh-3483)', async function() { - const schema = new Schema({ - title: String - }); - - let called = 0; - - schema.pre('save', function(next) { - next(); - // Error takes precedence over next() - throw new Error('woops!'); - }); - - schema.pre('save', function(next) { - ++called; - next(); - }); - - const TestMiddleware = db.model('Test', schema); - - const test = new TestMiddleware({ title: 'Test' }); - - await assert.rejects(test.save(), /woops!/); - assert.equal(called, 0); - }); - it('validate + remove', async function() { const schema = new Schema({ title: String @@ -347,14 +316,12 @@ describe('model middleware', function() { preRemove = 0, postRemove = 0; - schema.pre('validate', function(next) { + schema.pre('validate', function() { ++preValidate; - next(); }); - schema.pre('deleteOne', { document: true, query: false }, function(next) { + schema.pre('deleteOne', { document: true, query: false }, function() { ++preRemove; - next(); }); schema.post('validate', function(doc) { @@ -512,8 +479,8 @@ describe('model middleware', function() { it('allows skipping createCollection from hooks', async function() { const schema = new Schema({ name: String }, { autoCreate: true }); - schema.pre('createCollection', function(next) { - next(mongoose.skipMiddlewareFunction()); + schema.pre('createCollection', function() { + throw mongoose.skipMiddlewareFunction(); }); const Test = db.model('CreateCollectionHookTest', schema); @@ -529,9 +496,8 @@ describe('model middleware', function() { const pre = []; const post = []; - schema.pre('bulkWrite', function(next, ops) { + schema.pre('bulkWrite', function(ops) { pre.push(ops); - next(); }); schema.post('bulkWrite', function(res) { post.push(res); @@ -558,9 +524,8 @@ describe('model middleware', function() { it('allows updating ops', async function() { const schema = new Schema({ name: String, prop: String }); - schema.pre('bulkWrite', function(next, ops) { + schema.pre('bulkWrite', function(ops) { ops[0].updateOne.filter.name = 'baz'; - next(); }); const Test = db.model('Test', schema); @@ -644,8 +609,8 @@ describe('model middleware', function() { it('supports skipping wrapped function', async function() { const schema = new Schema({ name: String, prop: String }); - schema.pre('bulkWrite', function(next) { - next(mongoose.skipMiddlewareFunction('skipMiddlewareFunction test')); + schema.pre('bulkWrite', function(ops) { + throw mongoose.skipMiddlewareFunction('skipMiddlewareFunction test'); }); const Test = db.model('Test', schema); diff --git a/test/model.test.js b/test/model.test.js index a1837fc909f..d270a22d871 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -408,9 +408,8 @@ describe('Model', function() { name: String }); - childSchema.pre('save', function(next) { + childSchema.pre('save', function() { child_hook = this.name; - next(); }); const parentSchema = new Schema({ @@ -418,9 +417,8 @@ describe('Model', function() { children: [childSchema] }); - parentSchema.pre('save', function(next) { + parentSchema.pre('save', function() { parent_hook = this.name; - next(); }); const Parent = db.model('Parent', parentSchema); @@ -1016,11 +1014,10 @@ describe('Model', function() { baz: { type: String } }); - ValidationMiddlewareSchema.pre('validate', function(next) { + ValidationMiddlewareSchema.pre('validate', function() { if (this.get('baz') === 'bad') { this.invalidate('baz', 'bad'); } - next(); }); Post = db.model('Test', ValidationMiddlewareSchema); @@ -2096,14 +2093,12 @@ describe('Model', function() { const schema = new Schema({ name: String }); let called = 0; - schema.pre('save', function(next) { + schema.pre('save', function() { called++; - next(undefined); }); - schema.pre('save', function(next) { + schema.pre('save', function() { called++; - next(null); }); const S = db.model('Test', schema); @@ -2115,22 +2110,19 @@ describe('Model', function() { it('called on all sub levels', async function() { const grandSchema = new Schema({ name: String }); - grandSchema.pre('save', function(next) { + grandSchema.pre('save', function() { this.name = 'grand'; - next(); }); const childSchema = new Schema({ name: String, grand: [grandSchema] }); - childSchema.pre('save', function(next) { + childSchema.pre('save', function() { this.name = 'child'; - next(); }); const schema = new Schema({ name: String, child: [childSchema] }); - schema.pre('save', function(next) { + schema.pre('save', function() { this.name = 'parent'; - next(); }); const S = db.model('Test', schema); @@ -2144,21 +2136,19 @@ describe('Model', function() { it('error on any sub level', async function() { const grandSchema = new Schema({ name: String }); - grandSchema.pre('save', function(next) { - next(new Error('Error 101')); + grandSchema.pre('save', function() { + throw new Error('Error 101'); }); const childSchema = new Schema({ name: String, grand: [grandSchema] }); - childSchema.pre('save', function(next) { + childSchema.pre('save', function() { this.name = 'child'; - next(); }); let schemaPostSaveCalls = 0; const schema = new Schema({ name: String, child: [childSchema] }); - schema.pre('save', function(next) { + schema.pre('save', function() { this.name = 'parent'; - next(); }); schema.post('save', function testSchemaPostSave(err, res, next) { ++schemaPostSaveCalls; @@ -2480,8 +2470,8 @@ describe('Model', function() { describe('when no callback is passed', function() { it('should emit error on its Model when there are listeners', async function() { const DefaultErrSchema = new Schema({}); - DefaultErrSchema.pre('save', function(next) { - next(new Error()); + DefaultErrSchema.pre('save', function() { + throw new Error(); }); const DefaultErr = db.model('Test', DefaultErrSchema); @@ -6072,9 +6062,8 @@ describe('Model', function() { }; let called = 0; - schema.pre('aggregate', function(next) { + schema.pre('aggregate', function() { ++called; - next(); }); const Model = db.model('Test', schema); @@ -6101,9 +6090,8 @@ describe('Model', function() { }; let called = 0; - schema.pre('insertMany', function(next) { + schema.pre('insertMany', function() { ++called; - next(); }); const Model = db.model('Test', schema); @@ -6126,9 +6114,8 @@ describe('Model', function() { }; let called = 0; - schema.pre('save', function(next) { + schema.pre('save', function() { ++called; - next(); }); const Model = db.model('Test', schema); @@ -8144,9 +8131,8 @@ describe('Model', function() { name: String }); let bypass = true; - testSchema.pre('findOne', function(next) { + testSchema.pre('findOne', function() { bypass = false; - next(); }); const Test = db.model('gh13250', testSchema); const doc = await Test.create({ diff --git a/test/model.updateOne.test.js b/test/model.updateOne.test.js index 061fbbd51f3..6e34aa0158a 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -902,9 +902,8 @@ describe('model: updateOne:', function() { let numPres = 0; let numPosts = 0; const band = new Schema({ members: [String] }); - band.pre('updateOne', function(next) { + band.pre('updateOne', function() { ++numPres; - next(); }); band.post('updateOne', function() { ++numPosts; @@ -1237,9 +1236,8 @@ describe('model: updateOne:', function() { it('middleware update with exec (gh-3549)', async function() { const Schema = mongoose.Schema({ name: String }); - Schema.pre('updateOne', function(next) { + Schema.pre('updateOne', function() { this.updateOne({ name: 'Val' }); - next(); }); const Model = db.model('Test', Schema); diff --git a/test/query.cursor.test.js b/test/query.cursor.test.js index 3a736dd0f57..cb8bc53f8ee 100644 --- a/test/query.cursor.test.js +++ b/test/query.cursor.test.js @@ -209,9 +209,8 @@ describe('QueryCursor', function() { it('with pre-find hooks (gh-5096)', async function() { const schema = new Schema({ name: String }); let called = 0; - schema.pre('find', function(next) { + schema.pre('find', function() { ++called; - next(); }); db.deleteModel(/Test/); @@ -883,8 +882,8 @@ describe('QueryCursor', function() { it('throws if calling skipMiddlewareFunction() with non-empty array (gh-13411)', async function() { const schema = new mongoose.Schema({ name: String }); - schema.pre('find', (next) => { - next(mongoose.skipMiddlewareFunction([{ name: 'bar' }])); + schema.pre('find', () => { + throw mongoose.skipMiddlewareFunction([{ name: 'bar' }]); }); const Movie = db.model('Movie', schema); diff --git a/test/query.middleware.test.js b/test/query.middleware.test.js index 48c889e98f2..9f43e93aaa5 100644 --- a/test/query.middleware.test.js +++ b/test/query.middleware.test.js @@ -58,9 +58,8 @@ describe('query middleware', function() { it('has a pre find hook', async function() { let count = 0; - schema.pre('find', function(next) { + schema.pre('find', function() { ++count; - next(); }); await initializeData(); @@ -87,9 +86,8 @@ describe('query middleware', function() { it('works when using a chained query builder', async function() { let count = 0; - schema.pre('find', function(next) { + schema.pre('find', function() { ++count; - next(); }); let postCount = 0; @@ -110,9 +108,8 @@ describe('query middleware', function() { it('has separate pre-findOne() and post-findOne() hooks', async function() { let count = 0; - schema.pre('findOne', function(next) { + schema.pre('findOne', function() { ++count; - next(); }); let postCount = 0; @@ -132,9 +129,8 @@ describe('query middleware', function() { it('with regular expression (gh-6680)', async function() { let count = 0; let postCount = 0; - schema.pre(/find/, function(next) { + schema.pre(/find/, function() { ++count; - next(); }); schema.post(/find/, function(result, next) { @@ -163,9 +159,8 @@ describe('query middleware', function() { }); it('can populate in pre hook', async function() { - schema.pre('findOne', function(next) { + schema.pre('findOne', function() { this.populate('publisher'); - next(); }); await initializeData(); @@ -442,8 +437,8 @@ describe('query middleware', function() { const schema = new Schema({}); let called = false; - schema.pre('find', function(next) { - next(new Error('test')); + schema.pre('find', function() { + throw new Error('test'); }); schema.post('find', function(res, next) { @@ -468,9 +463,8 @@ describe('query middleware', function() { let calledPre = 0; let calledPost = 0; - schema.pre('find', function(next) { + schema.pre('find', function() { ++calledPre; - next(); }); schema.post('find', function(res, next) { @@ -552,8 +546,8 @@ describe('query middleware', function() { const schema = Schema({ name: String }); const now = Date.now(); - schema.pre('find', function(next) { - next(mongoose.skipMiddlewareFunction([{ name: 'from cache' }])); + schema.pre('find', function() { + throw mongoose.skipMiddlewareFunction([{ name: 'from cache' }]); }); schema.post('find', function(res) { res.forEach(doc => { diff --git a/test/query.test.js b/test/query.test.js index 14f78fdf7e4..2906f839896 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -1923,9 +1923,8 @@ describe('Query', function() { ]; ops.forEach(function(op) { - TestSchema.pre(op, function(next) { + TestSchema.pre(op, function() { this.error(new Error(op + ' error')); - next(); }); }); diff --git a/test/types.documentarray.test.js b/test/types.documentarray.test.js index c8e5fe72cc5..bea303725e0 100644 --- a/test/types.documentarray.test.js +++ b/test/types.documentarray.test.js @@ -301,9 +301,8 @@ describe('types.documentarray', function() { describe('push()', function() { it('does not re-cast instances of its embedded doc', async function() { const child = new Schema({ name: String, date: Date }); - child.pre('save', function(next) { + child.pre('save', function() { this.date = new Date(); - next(); }); const schema = new Schema({ children: [child] }); const M = db.model('Test', schema); @@ -469,10 +468,9 @@ describe('types.documentarray', function() { describe('invalidate()', function() { it('works', async function() { const schema = new Schema({ docs: [{ name: 'string' }] }); - schema.pre('validate', function(next) { + schema.pre('validate', function() { const subdoc = this.docs[this.docs.length - 1]; subdoc.invalidate('name', 'boo boo', '%'); - next(); }); mongoose.deleteModel(/Test/); const T = mongoose.model('Test', schema); diff --git a/test/types/middleware.preposttypes.test.ts b/test/types/middleware.preposttypes.test.ts index e830d808517..d1cb386561e 100644 --- a/test/types/middleware.preposttypes.test.ts +++ b/test/types/middleware.preposttypes.test.ts @@ -5,12 +5,10 @@ interface IDocument extends Document { name?: string; } -const preMiddlewareFn: PreSaveMiddlewareFunction = function(next, opts) { +const preMiddlewareFn: PreSaveMiddlewareFunction = function(opts) { this.$markValid('name'); - if (opts.session) { - next(); - } else { - next(new Error('Operation must be in Session.')); + if (!opts.session) { + throw new Error('Operation must be in Session.'); } }; diff --git a/test/types/middleware.test.ts b/test/types/middleware.test.ts index 81846feefd7..98ac297ccf2 100644 --- a/test/types/middleware.test.ts +++ b/test/types/middleware.test.ts @@ -2,12 +2,10 @@ import { Schema, model, Model, Document, SaveOptions, Query, Aggregate, Hydrated import { expectError, expectType, expectNotType, expectAssignable } from 'tsd'; import { CreateCollectionOptions } from 'mongodb'; -const preMiddlewareFn: PreSaveMiddlewareFunction = function(next, opts) { +const preMiddlewareFn: PreSaveMiddlewareFunction = function(opts) { this.$markValid('name'); - if (opts.session) { - next(); - } else { - next(new Error('Operation must be in Session.')); + if (!opts.session) { + throw new Error('Operation must be in Session.'); } }; @@ -45,12 +43,11 @@ schema.pre(['save', 'validate'], { query: false, document: true }, async functio await Test.findOne({}); }); -schema.pre('save', function(next, opts: SaveOptions) { +schema.pre('save', function(opts: SaveOptions) { console.log(opts.session); - next(); }); -schema.pre('save', function(next) { +schema.pre('save', function() { console.log(this.name); }); @@ -80,36 +77,31 @@ schema.pre>('insertMany', function() { console.log(this.name); }); -schema.pre>('insertMany', function(next) { +schema.pre>('insertMany', function() { console.log(this.name); - next(); }); -schema.pre>('insertMany', function(next, doc: ITest) { - console.log(this.name, doc); - next(); +schema.pre>('insertMany', function(docs: ITest[]) { + console.log(this.name, docs); }); -schema.pre>('insertMany', function(next, docs: Array) { +schema.pre>('insertMany', function(docs: Array) { console.log(this.name, docs); - next(); }); -schema.pre>('bulkWrite', function(next, ops: Array>) { - next(); +schema.pre>('bulkWrite', function(ops: Array>) { }); -schema.pre>('createCollection', function(next, opts?: CreateCollectionOptions) { - next(); +schema.pre>('createCollection', function(opts?: CreateCollectionOptions) { }); -schema.pre>('estimatedDocumentCount', function(next) {}); +schema.pre>('estimatedDocumentCount', function() {}); schema.post>('estimatedDocumentCount', function(count, next) { expectType(count); next(); }); -schema.pre>('countDocuments', function(next) {}); +schema.pre>('countDocuments', function() {}); schema.post>('countDocuments', function(count, next) { expectType(count); next(); @@ -139,9 +131,8 @@ function gh11480(): void { const UserSchema = new Schema({ name: { type: String } }); - UserSchema.pre('save', function(next) { + UserSchema.pre('save', function() { expectNotType(this); - next(); }); } diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index ca0180700cb..037618cee39 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1209,15 +1209,15 @@ function gh13633() { schema.pre('updateOne', { document: true, query: false }, function(next) { }); - schema.pre('updateOne', { document: true, query: false }, function(next, options) { + schema.pre('updateOne', { document: true, query: false }, function(options) { expectType | undefined>(options); }); schema.post('save', function(res, next) { }); - schema.pre('insertMany', function(next, docs) { + schema.pre('insertMany', function(docs) { }); - schema.pre('insertMany', function(next, docs, options) { + schema.pre('insertMany', function(docs, options) { expectType<(InsertManyOptions & { lean?: boolean }) | undefined>(options); }); } diff --git a/types/index.d.ts b/types/index.d.ts index d5863fd5edb..a17c4fb52f7 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -497,7 +497,6 @@ declare module 'mongoose' { method: 'insertMany' | RegExp, fn: ( this: T, - next: (err?: CallbackError) => void, docs: any | Array, options?: InsertManyOptions & { lean?: boolean } ) => void | Promise @@ -507,7 +506,6 @@ declare module 'mongoose' { method: 'bulkWrite' | RegExp, fn: ( this: T, - next: (err?: CallbackError) => void, ops: Array>, options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions ) => void | Promise @@ -517,7 +515,6 @@ declare module 'mongoose' { method: 'createCollection' | RegExp, fn: ( this: T, - next: (err?: CallbackError) => void, options?: mongodb.CreateCollectionOptions & Pick ) => void | Promise ): this; diff --git a/types/middlewares.d.ts b/types/middlewares.d.ts index 64d8ca620bb..f3793a7ed01 100644 --- a/types/middlewares.d.ts +++ b/types/middlewares.d.ts @@ -36,12 +36,10 @@ declare module 'mongoose' { type PreMiddlewareFunction = ( this: ThisType, - next: CallbackWithoutResultAndOptionalError, opts?: Record ) => void | Promise | Kareem.SkipWrappedFunction; type PreSaveMiddlewareFunction = ( this: ThisType, - next: CallbackWithoutResultAndOptionalError, opts: SaveOptions ) => void | Promise | Kareem.SkipWrappedFunction; type PostMiddlewareFunction = (this: ThisType, res: ResType, next: CallbackWithoutResultAndOptionalError) => void | Promise | Kareem.OverwriteMiddlewareResult; From 9f40ebf6bf46a76e15414e0d00c3a410cee1f67a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 28 Aug 2025 17:17:44 -0400 Subject: [PATCH 139/209] types: avoid WithLevel1NestedPaths drilling into non-records re: #15592 comments --- types/utility.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/utility.d.ts b/types/utility.d.ts index dc1f711a37a..2b5a1eb97f0 100644 --- a/types/utility.d.ts +++ b/types/utility.d.ts @@ -2,7 +2,7 @@ declare module 'mongoose' { type IfAny = 0 extends (1 & IFTYPE) ? THENTYPE : ELSETYPE; type IfUnknown = unknown extends IFTYPE ? THENTYPE : IFTYPE; - type WithLevel1NestedPaths = { + type WithLevel1NestedPaths = IsItRecordAndNotAny extends true ? { [P in K | NestedPaths, K>]: P extends K // Handle top-level paths // First, drill into documents so we don't end up surfacing `$assertPopulated`, etc. @@ -28,7 +28,7 @@ declare module 'mongoose' { : never : never : never; - }; + } : T; type HasStringIndex = string extends Extract ? true : false; From 41707559142d4d50b601ee2946e757bc3a6b56ce Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 8 Sep 2025 16:07:55 -0400 Subject: [PATCH 140/209] fix query casting weirdnesses --- types/query.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/types/query.d.ts b/types/query.d.ts index a8309443951..5eeaeeba0e1 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -3,7 +3,7 @@ declare module 'mongoose' { type StringQueryTypeCasting = string | RegExp; type ObjectIdQueryTypeCasting = Types.ObjectId | string; - type DateQueryTypeCasting = string | number; + type DateQueryTypeCasting = string | number | NativeDate; type UUIDQueryTypeCasting = Types.UUID | string; type BufferQueryCasting = Buffer | mongodb.Binary | number[] | string | { $binary: string | mongodb.Binary }; type QueryTypeCasting = T extends string @@ -14,8 +14,8 @@ declare module 'mongoose' { ? UUIDQueryTypeCasting : T extends Buffer ? BufferQueryCasting - : NonNullable extends Date - ? DateQueryTypeCasting | T + : T extends NativeDate + ? DateQueryTypeCasting : T; export type ApplyBasicQueryCasting = T | T[] | (T extends (infer U)[] ? QueryTypeCasting : T); From d29cd5358da79abcceeab6d31abe46d89e887492 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 8 Sep 2025 16:20:40 -0400 Subject: [PATCH 141/209] types: use deep partial for create type casting --- test/types/create.test.ts | 16 ++++++++++++++++ types/models.d.ts | 8 ++++---- types/utility.d.ts | 6 ++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/test/types/create.test.ts b/test/types/create.test.ts index 51ea1e8ddaf..95cab3dd7f9 100644 --- a/test/types/create.test.ts +++ b/test/types/create.test.ts @@ -171,6 +171,22 @@ async function createWithMapOfSubdocs() { expectType(doc2.subdocMap!.get('taco')!.prop); } +async function createWithSubdocs() { + const schema = new Schema({ + name: String, + subdoc: new Schema({ + prop: { type: String, required: true }, + otherProp: { type: String, required: true } + }) + }); + const TestModel = model('Test', schema); + + const doc = await TestModel.create({ name: 'test', subdoc: { prop: 'test 1' } }); + expectType(doc.name); + expectType(doc.subdoc!.prop); + expectType(doc.subdoc!.otherProp); +} + async function createWithRawDocTypeNo_id() { interface RawDocType { name: string; diff --git a/types/models.d.ts b/types/models.d.ts index 1176839c42b..1ac4ba9cfd2 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -362,10 +362,10 @@ declare module 'mongoose' { >; /** Creates a new document or documents */ - create(docs: Array>>>, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>; - create(docs: Array>>>, options?: CreateOptions): Promise; - create(doc: Partial>>): Promise; - create(...docs: Array>>>): Promise; + create(docs: Array>>>, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>; + create(docs: Array>>>, options?: CreateOptions): Promise; + create(doc: DeepPartial>>): Promise; + create(...docs: Array>>>): Promise; /** * Create the collection for this model. By default, if no indexes are specified, diff --git a/types/utility.d.ts b/types/utility.d.ts index 2b5a1eb97f0..13a2a22d2a4 100644 --- a/types/utility.d.ts +++ b/types/utility.d.ts @@ -82,6 +82,12 @@ declare module 'mongoose' { U : T extends ReadonlyArray ? U : T; + type DeepPartial = + T extends TreatAsPrimitives ? T : + T extends Array ? DeepPartial[] : + T extends Record ? { [K in keyof T]?: DeepPartial } : + T; + type UnpackedIntersection = T extends null ? null : T extends (infer A)[] ? (Omit & U)[] : keyof U extends never From 364e14b976ea4c012a05cb29f661bc506c0b2637 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 9 Sep 2025 12:25:18 -0400 Subject: [PATCH 142/209] remove use$geoWithin re: mongoosejs/mquery#141 --- docs/migrating_to_9.md | 5 +++ test/query.test.js | 69 ------------------------------------------ 2 files changed, 5 insertions(+), 69 deletions(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 5f09c05a75e..2cd9a95d629 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -285,6 +285,11 @@ console.log(schema.path('docArray').Constructor); // EmbeddedDocument constructo In Mongoose 8, there was also an internal `$embeddedSchemaType` property. That property has been replaced with `embeddedSchemaType`, which is now part of the public API. +### Query use$geoWithin removed, now always true + +`mongoose.Query` had a `use$geoWithin` property that could configure converting `$geoWithin` to `$within` to support MongoDB versions before 2.4. +That property has been removed in Mongoose 9. `$geoWithin` is now never converted to `$within`, because MongoDB no longer supports `$within`. + ## TypeScript ### FilterQuery renamed to QueryFilter diff --git a/test/query.test.js b/test/query.test.js index 14f78fdf7e4..d357c177f53 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -446,75 +446,6 @@ describe('Query', function() { }); }); - describe('within', function() { - describe('box', function() { - it('via where', function() { - const query = new Query({}); - query.where('gps').within().box({ ll: [5, 25], ur: [10, 30] }); - const match = { gps: { $within: { $box: [[5, 25], [10, 30]] } } }; - if (Query.use$geoWithin) { - match.gps.$geoWithin = match.gps.$within; - delete match.gps.$within; - } - assert.deepEqual(query._conditions, match); - - }); - it('via where, no object', function() { - const query = new Query({}); - query.where('gps').within().box([5, 25], [10, 30]); - const match = { gps: { $within: { $box: [[5, 25], [10, 30]] } } }; - if (Query.use$geoWithin) { - match.gps.$geoWithin = match.gps.$within; - delete match.gps.$within; - } - assert.deepEqual(query._conditions, match); - - }); - }); - - describe('center', function() { - it('via where', function() { - const query = new Query({}); - query.where('gps').within().center({ center: [5, 25], radius: 5 }); - const match = { gps: { $within: { $center: [[5, 25], 5] } } }; - if (Query.use$geoWithin) { - match.gps.$geoWithin = match.gps.$within; - delete match.gps.$within; - } - assert.deepEqual(query._conditions, match); - - }); - }); - - describe('centerSphere', function() { - it('via where', function() { - const query = new Query({}); - query.where('gps').within().centerSphere({ center: [5, 25], radius: 5 }); - const match = { gps: { $within: { $centerSphere: [[5, 25], 5] } } }; - if (Query.use$geoWithin) { - match.gps.$geoWithin = match.gps.$within; - delete match.gps.$within; - } - assert.deepEqual(query._conditions, match); - - }); - }); - - describe('polygon', function() { - it('via where', function() { - const query = new Query({}); - query.where('gps').within().polygon({ a: { x: 10, y: 20 }, b: { x: 15, y: 25 }, c: { x: 20, y: 20 } }); - const match = { gps: { $within: { $polygon: [{ a: { x: 10, y: 20 }, b: { x: 15, y: 25 }, c: { x: 20, y: 20 } }] } } }; - if (Query.use$geoWithin) { - match.gps.$geoWithin = match.gps.$within; - delete match.gps.$within; - } - assert.deepEqual(query._conditions, match); - - }); - }); - }); - describe('exists', function() { it('0 args via where', function() { const query = new Query({}); From a3e4c880ae2f6dc5fb1ce1b3ef1b0d8bd5e969a3 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 22 Sep 2025 11:15:34 -0400 Subject: [PATCH 143/209] Remove noListener option from useDb --- lib/connection.js | 1 - lib/drivers/node-mongodb-native/connection.js | 11 ++--------- test/types/connection.test.ts | 1 - types/connection.d.ts | 2 +- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 5a8b2c8ca55..973f7caa114 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -1798,7 +1798,6 @@ Connection.prototype.syncIndexes = async function syncIndexes(options = {}) { * @param {String} name The database name * @param {Object} [options] * @param {Boolean} [options.useCache=false] If true, cache results so calling `useDb()` multiple times with the same name only creates 1 connection object. - * @param {Boolean} [options.noListener=false] If true, the connection object will not make the db listen to events on the original connection. See [issue #9961](https://github.com/Automattic/mongoose/issues/9961). * @return {Connection} New Connection Object * @api public */ diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js index 9e9f8952f6f..e2fc506ed94 100644 --- a/lib/drivers/node-mongodb-native/connection.js +++ b/lib/drivers/node-mongodb-native/connection.js @@ -55,7 +55,6 @@ Object.setPrototypeOf(NativeConnection.prototype, MongooseConnection.prototype); * @param {String} name The database name * @param {Object} [options] * @param {Boolean} [options.useCache=false] If true, cache results so calling `useDb()` multiple times with the same name only creates 1 connection object. - * @param {Boolean} [options.noListener=false] If true, the new connection object won't listen to any events on the base connection. This is better for memory usage in cases where you're calling `useDb()` for every request. * @return {Connection} New Connection Object * @api public */ @@ -107,11 +106,7 @@ NativeConnection.prototype.useDb = function(name, options) { function wireup() { newConn.client = _this.client; - const _opts = {}; - if (options.hasOwnProperty('noListener')) { - _opts.noListener = options.noListener; - } - newConn.db = _this.client.db(name, _opts); + newConn.db = _this.client.db(name); newConn._lastHeartbeatAt = _this._lastHeartbeatAt; newConn.onOpen(); } @@ -119,9 +114,7 @@ NativeConnection.prototype.useDb = function(name, options) { newConn.name = name; // push onto the otherDbs stack, this is used when state changes - if (options.noListener !== true) { - this.otherDbs.push(newConn); - } + this.otherDbs.push(newConn); newConn.otherDbs.push(this); // push onto the relatedDbs cache, this is used when state changes diff --git a/test/types/connection.test.ts b/test/types/connection.test.ts index 77ca1787685..f69f2b0065a 100644 --- a/test/types/connection.test.ts +++ b/test/types/connection.test.ts @@ -76,7 +76,6 @@ expectType>(conn.syncIndexes({ background: expectType(conn.useDb('test')); expectType(conn.useDb('test', {})); -expectType(conn.useDb('test', { noListener: true })); expectType(conn.useDb('test', { useCache: true })); expectType>( diff --git a/types/connection.d.ts b/types/connection.d.ts index d7ee4638edd..76b36d17e6f 100644 --- a/types/connection.d.ts +++ b/types/connection.d.ts @@ -273,7 +273,7 @@ declare module 'mongoose' { transaction(fn: (session: mongodb.ClientSession) => Promise, options?: mongodb.TransactionOptions): Promise; /** Switches to a different database using the same connection pool. */ - useDb(name: string, options?: { useCache?: boolean, noListener?: boolean }): Connection; + useDb(name: string, options?: { useCache?: boolean }): Connection; /** The username specified in the URI */ readonly user: string; From 8f9f02bd5d7d4e7dff12429c60d637802c4f211e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 22 Sep 2025 11:53:11 -0400 Subject: [PATCH 144/209] fix lint --- test/model.middleware.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/model.middleware.test.js b/test/model.middleware.test.js index 7e054e546af..5c49e70c755 100644 --- a/test/model.middleware.test.js +++ b/test/model.middleware.test.js @@ -609,7 +609,7 @@ describe('model middleware', function() { it('supports skipping wrapped function', async function() { const schema = new Schema({ name: String, prop: String }); - schema.pre('bulkWrite', function(ops) { + schema.pre('bulkWrite', function() { throw mongoose.skipMiddlewareFunction('skipMiddlewareFunction test'); }); From f41de586bd8c20892b979159f269a1733b792980 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 22 Sep 2025 12:55:25 -0400 Subject: [PATCH 145/209] docs: update middleware.md for no more next() in pre hooks --- docs/middleware.md | 76 +++++++++++++++--------------------------- lib/schema.js | 10 +++--- test/aggregate.test.js | 2 +- 3 files changed, 31 insertions(+), 57 deletions(-) diff --git a/docs/middleware.md b/docs/middleware.md index 87381fad2e0..43d2c2416d1 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -128,19 +128,17 @@ childSchema.pre('findOneAndUpdate', function() { ## Pre {#pre} -Pre middleware functions are executed one after another, when each -middleware calls `next`. +Pre middleware functions are executed one after another. ```javascript const schema = new Schema({ /* ... */ }); -schema.pre('save', function(next) { +schema.pre('save', function() { // do stuff - next(); }); ``` -In [mongoose 5.x](http://thecodebarbarian.com/introducing-mongoose-5.html#promises-and-async-await-with-middleware), instead of calling `next()` manually, you can use a -function that returns a promise. In particular, you can use [`async/await`](http://thecodebarbarian.com/common-async-await-design-patterns-in-node.js.html). +You can also use a function that returns a promise, including async functions. +Mongoose will wait until the promise resolves to move on to the next middleware. ```javascript schema.pre('save', function() { @@ -153,33 +151,22 @@ schema.pre('save', async function() { await doStuff(); await doMoreStuff(); }); -``` - -If you use `next()`, the `next()` call does **not** stop the rest of the code in your middleware function from executing. Use -[the early `return` pattern](https://www.bennadel.com/blog/2323-use-a-return-statement-when-invoking-callbacks-especially-in-a-guard-statement.htm) -to prevent the rest of your middleware function from running when you call `next()`. -```javascript -const schema = new Schema({ /* ... */ }); -schema.pre('save', function(next) { - if (foo()) { - console.log('calling next!'); - // `return next();` will make sure the rest of this function doesn't run - /* return */ next(); - } - // Unless you comment out the `return` above, 'after next' will print - console.log('after next'); +schema.pre('save', function() { + // Will execute **after** `await doMoreStuff()` is done }); ``` ### Use Cases -Middleware are useful for atomizing model logic. Here are some other ideas: +Middleware is useful for atomizing model logic. Here are some other ideas: * complex validation * removing dependent documents (removing a user removes all their blogposts) * asynchronous defaults * asynchronous tasks that a certain action triggers +* updating denormalized data on other documents +* saving change records ### Errors in Pre Hooks {#error-handling} @@ -189,11 +176,9 @@ and/or reject the returned promise. There are several ways to report an error in middleware: ```javascript -schema.pre('save', function(next) { +schema.pre('save', function() { const err = new Error('something went wrong'); - // If you call `next()` with an argument, that argument is assumed to be - // an error. - next(err); + throw err; }); schema.pre('save', function() { @@ -222,9 +207,6 @@ myDoc.save(function(err) { }); ``` -Calling `next()` multiple times is a no-op. If you call `next()` with an -error `err1` and then throw an error `err2`, mongoose will report `err1`. - ## Post middleware {#post} [post](api.html#schema_Schema-post) middleware are executed *after* @@ -373,16 +355,13 @@ const User = mongoose.model('User', userSchema); await User.findOneAndUpdate({ name: 'John' }, { $set: { age: 30 } }); ``` -For document middleware, like `pre('save')`, Mongoose passes the 1st parameter to `save()` as the 2nd argument to your `pre('save')` callback. -You should use the 2nd argument to get access to the `save()` call's `options`, because Mongoose documents don't store all the options you can pass to `save()`. +Mongoose also passes the 1st parameter to the hooked function, like `save()`, as the 1st argument to your `pre('save')` function. +You should use the argument to get access to the `save()` call's `options`, because Mongoose documents don't store all the options you can pass to `save()`. ```javascript const userSchema = new Schema({ name: String, age: Number }); -userSchema.pre('save', function(next, options) { +userSchema.pre('save', function(options) { options.validateModifiedOnly; // true - - // Remember to call `next()` unless you're using an async function or returning a promise - next(); }); const User = mongoose.model('User', userSchema); @@ -513,10 +492,9 @@ await Model.updateOne({}, { $set: { name: 'test' } }); ## Error Handling Middleware {#error-handling-middleware} -Middleware execution normally stops the first time a piece of middleware -calls `next()` with an error. However, there is a special kind of post -middleware called "error handling middleware" that executes specifically -when an error occurs. Error handling middleware is useful for reporting +Middleware execution normally stops the first time a piece of middleware throws an error, or returns a promise that rejects. +However, there is a special kind of post middleware called "error handling middleware" that executes specifically when an error occurs. +Error handling middleware is useful for reporting errors and making error messages more readable. Error handling middleware is defined as middleware that takes one extra @@ -553,13 +531,13 @@ errors. ```javascript // The same E11000 error can occur when you call `updateOne()` -// This function **must** take 4 parameters. +// This function **must** take exactly 3 parameters. -schema.post('updateOne', function(passRawResult, error, res, next) { +schema.post('updateOne', function(error, res, next) { if (error.name === 'MongoServerError' && error.code === 11000) { - next(new Error('There was a duplicate key error')); + throw new Error('There was a duplicate key error'); } else { - next(); // The `updateOne()` call will still error out. + next(); } }); @@ -570,9 +548,8 @@ await Person.create(people); await Person.updateOne({ name: 'Slash' }, { $set: { name: 'Axl Rose' } }); ``` -Error handling middleware can transform an error, but it can't remove the -error. Even if you call `next()` with no error as shown above, the -function call will still error out. +Error handling middleware can transform an error, but it can't remove the error. +Even if the error handling middleware succeeds, the function call will still error out. ## Aggregation Hooks {#aggregate} @@ -598,10 +575,9 @@ pipeline from middleware. ## Synchronous Hooks {#synchronous} -Certain Mongoose hooks are synchronous, which means they do **not** support -functions that return promises or receive a `next()` callback. Currently, -only `init` hooks are synchronous, because the [`init()` function](api/document.html#document_Document-init) -is synchronous. Below is an example of using pre and post init hooks. +Certain Mongoose hooks are synchronous, which means they do **not** support functions that return promises. +Currently, only `init` hooks are synchronous, because the [`init()` function](api/document.html#document_Document-init) is synchronous. +Below is an example of using pre and post init hooks. ```acquit [require:post init hooks.*success] diff --git a/lib/schema.js b/lib/schema.js index c68203fd7b6..4a9018f8d56 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -2081,23 +2081,21 @@ Schema.prototype.queue = function(name, args) { * * const toySchema = new Schema({ name: String, created: Date }); * - * toySchema.pre('save', function(next) { + * toySchema.pre('save', function() { * if (!this.created) this.created = new Date; - * next(); * }); * - * toySchema.pre('validate', function(next) { + * toySchema.pre('validate', function() { * if (this.name !== 'Woody') this.name = 'Woody'; - * next(); * }); * * // Equivalent to calling `pre()` on `find`, `findOne`, `findOneAndUpdate`. - * toySchema.pre(/^find/, function(next) { + * toySchema.pre(/^find/, function() { * console.log(this.getFilter()); * }); * * // Equivalent to calling `pre()` on `updateOne`, `findOneAndUpdate`. - * toySchema.pre(['updateOne', 'findOneAndUpdate'], function(next) { + * toySchema.pre(['updateOne', 'findOneAndUpdate'], function() { * console.log(this.getFilter()); * }); * diff --git a/test/aggregate.test.js b/test/aggregate.test.js index dd5c2cfa78c..91d427567b0 100644 --- a/test/aggregate.test.js +++ b/test/aggregate.test.js @@ -981,7 +981,7 @@ describe('aggregate: ', function() { const calledWith = []; s.pre('aggregate', function() { - return Promise.reject(new Error('woops')); + throw new Error('woops'); }); s.post('aggregate', function(error, res, next) { calledWith.push(error); From cce174ce2383830d201c935aef0db022d7799e71 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 28 Sep 2025 12:03:32 -0400 Subject: [PATCH 146/209] docs(migrating_to_9): add note about removing noListener re: #15641 --- docs/migrating_to_9.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 2cd9a95d629..6d8a3366982 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -290,6 +290,21 @@ In Mongoose 8, there was also an internal `$embeddedSchemaType` property. That p `mongoose.Query` had a `use$geoWithin` property that could configure converting `$geoWithin` to `$within` to support MongoDB versions before 2.4. That property has been removed in Mongoose 9. `$geoWithin` is now never converted to `$within`, because MongoDB no longer supports `$within`. +## Removed `noListener` option from `useDb()`/connections + +The `noListener` option has been removed from connections and from the `useDb()` method. In Mongoose 8.x, you could call `useDb()` with `{ noListener: true }` to prevent the new connection object from listening to state changes on the base connection, which was sometimes useful to reduce memory usage when dynamically creating connections for every request. + +In Mongoose 9.x, the `noListener` option is no longer supported or documented. The second argument to `useDb()` now only supports `{ useCache }`. + +```javascript +// Mongoose 8.x +conn.useDb('myDb', { noListener: true }); // works + +// Mongoose 9.x +conn.useDb('myDb', { noListener: true }); // TypeError: noListener is not a supported option +conn.useDb('myDb', { useCache: true }); // works +``` + ## TypeScript ### FilterQuery renamed to QueryFilter From e6a447af5b42c25e50e5fcffc5426d6df0140078 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 28 Sep 2025 12:48:16 -0400 Subject: [PATCH 147/209] type cleanup --- types/query.d.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/types/query.d.ts b/types/query.d.ts index ea77e92e564..e3f60b263a0 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -17,6 +17,9 @@ declare module 'mongoose' { export type ApplyBasicQueryCasting = T | T[] | (T extends (infer U)[] ? QueryTypeCasting : T) | null; + type ApplyQueryCastingToObject = { [P in keyof T]?: ApplyBasicQueryCasting>; }; + type ApplyConditionToObject = { [P in keyof T]?: mongodb.Condition>> }; + /** * Filter query to select the documents that match the query * @example @@ -24,7 +27,7 @@ declare module 'mongoose' { * { age: { $gte: 30 } } * ``` */ - type _QueryFilter = ({ [P in keyof T]?: mongodb.Condition>>; } & mongodb.RootFilterOperators<{ [P in keyof T]?: ApplyBasicQueryCasting>; }>) | Query; + type _QueryFilter = (ApplyConditionToObject & mongodb.RootFilterOperators>) | Query; type QueryFilter = _QueryFilter>; type MongooseBaseQueryOptionKeys = From 762446f09369ca98ccff0e5005a45e5e159fa9da Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 28 Sep 2025 12:58:29 -0400 Subject: [PATCH 148/209] clean up types performance --- types/query.d.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/types/query.d.ts b/types/query.d.ts index e3f60b263a0..6b942aaaf3e 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -16,19 +16,10 @@ declare module 'mongoose' { : T; export type ApplyBasicQueryCasting = T | T[] | (T extends (infer U)[] ? QueryTypeCasting : T) | null; - type ApplyQueryCastingToObject = { [P in keyof T]?: ApplyBasicQueryCasting>; }; - type ApplyConditionToObject = { [P in keyof T]?: mongodb.Condition>> }; - - /** - * Filter query to select the documents that match the query - * @example - * ```js - * { age: { $gte: 30 } } - * ``` - */ - type _QueryFilter = (ApplyConditionToObject & mongodb.RootFilterOperators>) | Query; - type QueryFilter = _QueryFilter>; + + type _QueryFilter = ({ [P in keyof T]?: mongodb.Condition; } & mongodb.RootFilterOperators) | Query; + type QueryFilter = _QueryFilter>>; type MongooseBaseQueryOptionKeys = | 'context' From 7bda5b0e6857f27c7fd8badb774604603e12f375 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 28 Sep 2025 13:04:15 -0400 Subject: [PATCH 149/209] more performant types --- types/query.d.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/types/query.d.ts b/types/query.d.ts index 6b942aaaf3e..8bd531ec7a5 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -16,10 +16,9 @@ declare module 'mongoose' { : T; export type ApplyBasicQueryCasting = T | T[] | (T extends (infer U)[] ? QueryTypeCasting : T) | null; - type ApplyQueryCastingToObject = { [P in keyof T]?: ApplyBasicQueryCasting>; }; - type _QueryFilter = ({ [P in keyof T]?: mongodb.Condition; } & mongodb.RootFilterOperators) | Query; - type QueryFilter = _QueryFilter>>; + type _QueryFilter = ({ [P in keyof T]?: mongodb.Condition>>; } & mongodb.RootFilterOperators<{ [P in keyof T]?: ApplyBasicQueryCasting>; }>) | Query; + type QueryFilter = _QueryFilter>; type MongooseBaseQueryOptionKeys = | 'context' From d1ebd20e15badc82c6c2275284e9e3afe5334c20 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 28 Sep 2025 14:00:53 -0400 Subject: [PATCH 150/209] perf: remove some unnecessary typescript generics --- test/types/middleware.test.ts | 24 ++++++++++------------- test/types/queries.test.ts | 1 - test/types/schema.test.ts | 2 +- types/index.d.ts | 26 ++++++++++++------------- types/models.d.ts | 16 ++++++++-------- types/query.d.ts | 36 +++++++++++++++++------------------ 6 files changed, 50 insertions(+), 55 deletions(-) diff --git a/test/types/middleware.test.ts b/test/types/middleware.test.ts index 98ac297ccf2..6f0be474308 100644 --- a/test/types/middleware.test.ts +++ b/test/types/middleware.test.ts @@ -68,31 +68,27 @@ schema.post('save', function(err: Error, res: ITest, next: Function) { console.log(this.name, err.stack); }); -schema.pre>('insertMany', function() { - const name: string = this.name; +schema.pre('insertMany', function() { + const name: string = this.modelName; return Promise.resolve(); }); -schema.pre>('insertMany', function() { - console.log(this.name); -}); - -schema.pre>('insertMany', function() { - console.log(this.name); +schema.pre('insertMany', function() { + console.log(this.modelName); }); -schema.pre>('insertMany', function(docs: ITest[]) { - console.log(this.name, docs); +schema.pre('insertMany', function(docs: ITest[]) { + console.log(this.modelName, docs); }); -schema.pre>('insertMany', function(docs: Array) { - console.log(this.name, docs); +schema.pre('insertMany', function(docs: Array) { + console.log(this.modelName, docs); }); -schema.pre>('bulkWrite', function(ops: Array>) { +schema.pre('bulkWrite', function(ops: Array>) { }); -schema.pre>('createCollection', function(opts?: CreateCollectionOptions) { +schema.pre('createCollection', function(opts?: CreateCollectionOptions) { }); schema.pre>('estimatedDocumentCount', function() {}); diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 3596d63185b..301f7c588ed 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -339,7 +339,6 @@ async function gh11306(): Promise { expectType(await MyModel.distinct('notThereInSchema')); expectType(await MyModel.distinct('name')); - expectType(await MyModel.distinct<'overrideTest', number>('overrideTest')); } function autoTypedQuery() { diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index dc7cf32e886..e8b86a5cbf5 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -133,7 +133,7 @@ const ProfileSchemaDef2: SchemaDefinition = { age: Schema.Types.Number }; -const ProfileSchema2: Schema> = new Schema(ProfileSchemaDef2); +const ProfileSchema2: Schema> = new Schema>(ProfileSchemaDef2); const UserSchemaDef: SchemaDefinition = { email: String, diff --git a/types/index.d.ts b/types/index.d.ts index f149b95a455..fe069c3baca 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -331,7 +331,7 @@ declare module 'mongoose' { clearIndexes(): this; /** Returns a copy of this schema */ - clone(): T; + clone(): this; discriminator(name: string | number, schema: DisSchema, options?: DiscriminatorOptions): this; @@ -410,7 +410,7 @@ declare module 'mongoose' { post>(method: MongooseQueryMiddleware | MongooseQueryMiddleware[] | RegExp, options: SchemaPostOptions & { errorHandler: true }, fn: ErrorHandlingMiddlewareWithOption): this; post(method: MongooseDocumentMiddleware | MongooseDocumentMiddleware[] | RegExp, options: SchemaPostOptions & { errorHandler: true }, fn: ErrorHandlingMiddlewareWithOption): this; post>(method: 'aggregate' | RegExp, options: SchemaPostOptions & { errorHandler: true }, fn: ErrorHandlingMiddlewareWithOption>): this; - post(method: 'insertMany' | RegExp, options: SchemaPostOptions & { errorHandler: true }, fn: ErrorHandlingMiddlewareWithOption): this; + post(method: 'insertMany' | RegExp, options: SchemaPostOptions & { errorHandler: true }, fn: ErrorHandlingMiddlewareWithOption): this; // this = never since it never happens post(method: MongooseQueryOrDocumentMiddleware | MongooseQueryOrDocumentMiddleware[] | RegExp, options: SchemaPostOptions & { document: false, query: false }, fn: PostMiddlewareFunction): this; @@ -451,14 +451,14 @@ declare module 'mongoose' { // method aggregate and insertMany with PostMiddlewareFunction post>(method: 'aggregate' | RegExp, fn: PostMiddlewareFunction>>): this; post>(method: 'aggregate' | RegExp, options: SchemaPostOptions, fn: PostMiddlewareFunction>>): this; - post(method: 'insertMany' | RegExp, fn: PostMiddlewareFunction): this; - post(method: 'insertMany' | RegExp, options: SchemaPostOptions, fn: PostMiddlewareFunction): this; + post(method: 'insertMany' | RegExp, fn: PostMiddlewareFunction): this; + post(method: 'insertMany' | RegExp, options: SchemaPostOptions, fn: PostMiddlewareFunction): this; // method aggregate and insertMany with ErrorHandlingMiddlewareFunction post>(method: 'aggregate' | RegExp, fn: ErrorHandlingMiddlewareFunction>): this; post>(method: 'aggregate' | RegExp, options: SchemaPostOptions, fn: ErrorHandlingMiddlewareFunction>): this; - post(method: 'bulkWrite' | 'createCollection' | 'insertMany' | RegExp, fn: ErrorHandlingMiddlewareFunction): this; - post(method: 'bulkWrite' | 'createCollection' | 'insertMany' | RegExp, options: SchemaPostOptions, fn: ErrorHandlingMiddlewareFunction): this; + post(method: 'bulkWrite' | 'createCollection' | 'insertMany' | RegExp, fn: ErrorHandlingMiddlewareFunction): this; + post(method: 'bulkWrite' | 'createCollection' | 'insertMany' | RegExp, options: SchemaPostOptions, fn: ErrorHandlingMiddlewareFunction): this; /** Defines a pre hook for the model. */ // this = never since it never happens @@ -493,28 +493,28 @@ declare module 'mongoose' { // method aggregate pre>(method: 'aggregate' | RegExp, fn: PreMiddlewareFunction): this; /* method insertMany */ - pre( + pre( method: 'insertMany' | RegExp, fn: ( - this: T, + this: TModelType, docs: any | Array, options?: InsertManyOptions & { lean?: boolean } ) => void | Promise ): this; /* method bulkWrite */ - pre( + pre( method: 'bulkWrite' | RegExp, fn: ( - this: T, + this: TModelType, ops: Array>, options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions ) => void | Promise ): this; /* method createCollection */ - pre( + pre( method: 'createCollection' | RegExp, fn: ( - this: T, + this: TModelType, options?: mongodb.CreateCollectionOptions & Pick ) => void | Promise ): this; @@ -558,7 +558,7 @@ declare module 'mongoose' { virtuals: TVirtuals; /** Returns the virtual type with the given `name`. */ - virtualpath(name: string): VirtualType | null; + virtualpath(name: string): VirtualType | null; static ObjectId: typeof Schema.Types.ObjectId; } diff --git a/types/models.d.ts b/types/models.d.ts index a1cdf7a6749..8869fb719ee 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -675,7 +675,7 @@ declare module 'mongoose' { translateAliases(raw: any): any; /** Creates a `distinct` query: returns the distinct values of the given `field` that match `filter`. */ - distinct( + distinct( field: DocKey, filter?: QueryFilter, options?: QueryOptions @@ -683,7 +683,7 @@ declare module 'mongoose' { Array< DocKey extends keyof WithLevel1NestedPaths ? WithoutUndefined[DocKey]>> - : ResultType + : unknown >, THydratedDocumentType, TQueryHelpers, @@ -916,21 +916,21 @@ declare module 'mongoose' { schema: Schema; /** Creates a `updateMany` query: updates all documents that match `filter` with `update`. */ - updateMany( + updateMany( filter: QueryFilter, update: UpdateQuery | UpdateWithAggregationPipeline, options?: (mongodb.UpdateOptions & MongooseUpdateQueryOptions) | null - ): QueryWithHelpers; + ): QueryWithHelpers; /** Creates a `updateOne` query: updates the first document that matches `filter` with `update`. */ - updateOne( + updateOne( filter: QueryFilter, update: UpdateQuery | UpdateWithAggregationPipeline, options?: (mongodb.UpdateOptions & MongooseUpdateQueryOptions) | null - ): QueryWithHelpers; - updateOne( + ): QueryWithHelpers; + updateOne( update: UpdateQuery | UpdateWithAggregationPipeline - ): QueryWithHelpers; + ): QueryWithHelpers; /** Creates a Query, applies the passed conditions, and returns the Query. */ where( diff --git a/types/query.d.ts b/types/query.d.ts index 8bd531ec7a5..fc8895e892e 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -15,9 +15,9 @@ declare module 'mongoose' { ? BufferQueryCasting : T; - export type ApplyBasicQueryCasting = T | T[] | (T extends (infer U)[] ? QueryTypeCasting : T) | null; + export type ApplyBasicQueryCasting = QueryTypeCasting | QueryTypeCasting | (T extends (infer U)[] ? QueryTypeCasting : T) | null; - type _QueryFilter = ({ [P in keyof T]?: mongodb.Condition>>; } & mongodb.RootFilterOperators<{ [P in keyof T]?: ApplyBasicQueryCasting>; }>) | Query; + type _QueryFilter = ({ [P in keyof T]?: mongodb.Condition>; } & mongodb.RootFilterOperators<{ [P in keyof T]?: ApplyBasicQueryCasting; }>) | Query; type QueryFilter = _QueryFilter>; type MongooseBaseQueryOptionKeys = @@ -59,7 +59,7 @@ declare module 'mongoose' { interface QueryOptions extends PopulateOption, SessionOption { - arrayFilters?: { [key: string]: any }[]; + arrayFilters?: AnyObject[]; batchSize?: number; collation?: mongodb.CollationOptions; comment?: any; @@ -88,7 +88,7 @@ declare module 'mongoose' { * Set `overwriteImmutable` to `true` to allow updating immutable properties using other update operators. */ overwriteImmutable?: boolean; - projection?: { [P in keyof DocType]?: number | string } | AnyObject | string; + projection?: AnyObject | string; /** * if true, returns the full ModifyResult rather than just the document */ @@ -317,7 +317,7 @@ declare module 'mongoose' { >; /** Specifies a `$elemMatch` query condition. When called with one argument, the most recent path passed to `where()` is used. */ - elemMatch(path: K, val: any): this; + elemMatch(path: string, val: any): this; elemMatch(val: Function | any): this; /** @@ -341,7 +341,7 @@ declare module 'mongoose' { >; /** Specifies a `$exists` query condition. When called with one argument, the most recent path passed to `where()` is used. */ - exists(path: K, val: boolean): this; + exists(path: string, val: boolean): this; exists(val: boolean): this; /** @@ -479,18 +479,18 @@ declare module 'mongoose' { getUpdate(): UpdateQuery | UpdateWithAggregationPipeline | null; /** Specifies a `$gt` query condition. When called with one argument, the most recent path passed to `where()` is used. */ - gt(path: K, val: any): this; + gt(path: string, val: any): this; gt(val: number): this; /** Specifies a `$gte` query condition. When called with one argument, the most recent path passed to `where()` is used. */ - gte(path: K, val: any): this; + gte(path: string, val: any): this; gte(val: number): this; /** Sets query hints. */ hint(val: any): this; /** Specifies an `$in` query condition. When called with one argument, the most recent path passed to `where()` is used. */ - in(path: K, val: any[]): this; + in(path: string, val: any[]): this; in(val: Array): this; /** Declares an intersects query for `geometry()`. */ @@ -529,11 +529,11 @@ declare module 'mongoose' { limit(val: number): this; /** Specifies a `$lt` query condition. When called with one argument, the most recent path passed to `where()` is used. */ - lt(path: K, val: any): this; + lt(path: string, val: any): this; lt(val: number): this; /** Specifies a `$lte` query condition. When called with one argument, the most recent path passed to `where()` is used. */ - lte(path: K, val: any): this; + lte(path: string, val: any): this; lte(val: number): this; /** @@ -557,7 +557,7 @@ declare module 'mongoose' { merge(source: QueryFilter): this; /** Specifies a `$mod` condition, filters documents for documents whose `path` property is a number that is equal to `remainder` modulo `divisor`. */ - mod(path: K, val: number): this; + mod(path: string, val: number): this; mod(val: Array): this; /** The model this query was created from */ @@ -570,15 +570,15 @@ declare module 'mongoose' { mongooseOptions(val?: QueryOptions): QueryOptions; /** Specifies a `$ne` query condition. When called with one argument, the most recent path passed to `where()` is used. */ - ne(path: K, val: any): this; + ne(path: string, val: any): this; ne(val: any): this; /** Specifies a `$near` or `$nearSphere` condition */ - near(path: K, val: any): this; + near(path: string, val: any): this; near(val: any): this; /** Specifies an `$nin` query condition. When called with one argument, the most recent path passed to `where()` is used. */ - nin(path: K, val: any[]): this; + nin(path: string, val: any[]): this; nin(val: Array): this; /** Specifies arguments for an `$nor` condition. */ @@ -664,7 +664,7 @@ declare module 'mongoose' { readConcern(level: string): this; /** Specifies a `$regex` query condition. When called with one argument, the most recent path passed to `where()` is used. */ - regex(path: K, val: RegExp): this; + regex(path: string, val: RegExp): this; regex(val: string | RegExp): this; /** @@ -749,7 +749,7 @@ declare module 'mongoose' { setUpdate(update: UpdateQuery | UpdateWithAggregationPipeline): void; /** Specifies an `$size` query condition. When called with one argument, the most recent path passed to `where()` is used. */ - size(path: K, val: number): this; + size(path: string, val: number): this; size(val: number): this; /** Specifies the number of documents to skip. */ @@ -761,7 +761,7 @@ declare module 'mongoose' { /** Sets the sort order. If an object is passed, values allowed are `asc`, `desc`, `ascending`, `descending`, `1`, and `-1`. */ sort( - arg?: string | { [key: string]: SortOrder | { $meta: any } } | [string, SortOrder][] | undefined | null, + arg?: string | Record | [string, SortOrder][] | undefined | null, options?: { override?: boolean } ): this; From cee9a5acfdfbc938db5f2e24323e0bacfbc9f707 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 28 Sep 2025 14:51:27 -0400 Subject: [PATCH 151/209] bump max instantiations --- scripts/tsc-diagnostics-check.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tsc-diagnostics-check.js b/scripts/tsc-diagnostics-check.js index 55a6b01fe59..a498aaa28e3 100644 --- a/scripts/tsc-diagnostics-check.js +++ b/scripts/tsc-diagnostics-check.js @@ -3,7 +3,7 @@ const fs = require('fs'); const stdin = fs.readFileSync(0).toString('utf8'); -const maxInstantiations = isNaN(process.argv[2]) ? 300000 : parseInt(process.argv[2], 10); +const maxInstantiations = isNaN(process.argv[2]) ? 310000 : parseInt(process.argv[2], 10); console.log(stdin); From 49369710f7bcc2969fbd1503bcfecabcf71d59e7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 28 Sep 2025 15:42:11 -0400 Subject: [PATCH 152/209] types: add HydratedDocFromModel to make it easier to get the doc type from model, fix create() with no args --- test/types/discriminator.test.ts | 9 +++++++-- types/index.d.ts | 2 ++ types/models.d.ts | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/test/types/discriminator.test.ts b/test/types/discriminator.test.ts index 16e8f47f6d7..abff17cc44e 100644 --- a/test/types/discriminator.test.ts +++ b/test/types/discriminator.test.ts @@ -1,4 +1,4 @@ -import mongoose, { Document, Model, Schema, SchemaDefinition, SchemaOptions, Types, model } from 'mongoose'; +import mongoose, { Document, Model, Schema, SchemaDefinition, SchemaOptions, Types, model, HydratedDocFromModel, InferSchemaType } from 'mongoose'; import { expectType } from 'tsd'; const schema: Schema = new Schema({ name: { type: 'String' } }); @@ -120,7 +120,7 @@ function gh15535() { async function gh15600() { // Base model with custom static method const baseSchema = new Schema( - { name: String }, + { __t: String, name: String }, { statics: { findByName(name: string) { @@ -140,4 +140,9 @@ async function gh15600() { const res = await DiscriminatorModel.findByName('test'); expectType(res!.name); + + const doc = await BaseModel.create( + { __t: 'Discriminator', name: 'test', extra: 'test' } as InferSchemaType + ) as HydratedDocFromModel; + expectType(doc.extra); } diff --git a/types/index.d.ts b/types/index.d.ts index f149b95a455..ff5f2feee80 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -71,6 +71,8 @@ declare module 'mongoose' { export function omitUndefined>(val: T): T; + export type HydratedDocFromModel> = ReturnType; + /* ! ignore */ export type CompileModelOptions = { overwriteModels?: boolean, diff --git a/types/models.d.ts b/types/models.d.ts index 11093a525a2..4282c3abb8d 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -362,6 +362,7 @@ declare module 'mongoose' { >; /** Creates a new document or documents */ + create(): Promise; create(docs: Array>>>, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>; create(docs: Array>>>, options?: CreateOptions): Promise; create(doc: DeepPartial>>): Promise; From aff67b83d8a3d018a86487ee085e98183cbb98aa Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 30 Sep 2025 15:53:43 -0400 Subject: [PATCH 153/209] BREAKING CHANGE: remove skipId parameter to Document() and Model(), always use as options Fix #8862 --- docs/migrating_to_9.md | 8 +++++++- lib/document.js | 13 ++++++------- lib/helpers/model/castBulkWrite.js | 2 +- lib/model.js | 7 ++++--- lib/query.js | 2 +- lib/queryHelpers.js | 2 +- lib/schema/subdocument.js | 6 +++--- lib/types/arraySubdocument.js | 2 +- lib/types/subdocument.js | 8 ++------ test/document.test.js | 3 +-- types/models.d.ts | 2 +- 11 files changed, 28 insertions(+), 27 deletions(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 204cb56f220..cb242a76f54 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -295,12 +295,18 @@ console.log(schema.path('docArray').Constructor); // EmbeddedDocument constructo In Mongoose 8, there was also an internal `$embeddedSchemaType` property. That property has been replaced with `embeddedSchemaType`, which is now part of the public API. +### Removed `skipId` parameter to `Model()` and `Document()` + +In Mongoose 8, the 3rd parameter to `Model()` and `Document()` was either a boolean or `options` object. +If a boolean, Mongoose would interpret the 3rd parameter as the `skipId` option. +In Mongoose 9, the 3rd parameter is always an `options` object, passing a `boolean` is no longer supported. + ### Query use$geoWithin removed, now always true `mongoose.Query` had a `use$geoWithin` property that could configure converting `$geoWithin` to `$within` to support MongoDB versions before 2.4. That property has been removed in Mongoose 9. `$geoWithin` is now never converted to `$within`, because MongoDB no longer supports `$within`. -## Removed `noListener` option from `useDb()`/connections +### Removed `noListener` option from `useDb()`/connections The `noListener` option has been removed from connections and from the `useDb()` method. In Mongoose 8.x, you could call `useDb()` with `{ noListener: true }` to prevent the new connection object from listening to state changes on the base connection, which was sometimes useful to reduce memory usage when dynamically creating connections for every request. diff --git a/lib/document.js b/lib/document.js index af4dc01a2ea..42439569ac5 100644 --- a/lib/document.js +++ b/lib/document.js @@ -85,12 +85,12 @@ const VERSION_ALL = VERSION_WHERE | VERSION_INC; * @api private */ -function Document(obj, fields, skipId, options) { - if (typeof skipId === 'object' && skipId != null) { - options = skipId; - skipId = options.skipId; +function Document(obj, fields, options) { + if (typeof options === 'boolean') { + throw new Error('Tried to use skipId'); } options = Object.assign({}, options); + let skipId = options.skipId; this.$__ = new InternalCache(); @@ -101,9 +101,8 @@ function Document(obj, fields, skipId, options) { fields; this.$__setSchema(_schema); - fields = skipId; - skipId = options; - options = arguments[4] || {}; + fields = options; + skipId = options.skipId; } // Avoid setting `isNew` to `true`, because it is `true` by default diff --git a/lib/helpers/model/castBulkWrite.js b/lib/helpers/model/castBulkWrite.js index fc053000db3..0c525d1ceff 100644 --- a/lib/helpers/model/castBulkWrite.js +++ b/lib/helpers/model/castBulkWrite.js @@ -224,7 +224,7 @@ module.exports.castReplaceOne = async function castReplaceOne(originalModel, rep }); // set `skipId`, otherwise we get "_id field cannot be changed" - const doc = new model(replaceOne['replacement'], strict, true); + const doc = new model(replaceOne['replacement'], strict, { skipId: true }); if (model.schema.options.timestamps && getTimestampsOpt(replaceOne, options)) { doc.initializeTimestamps(); } diff --git a/lib/model.js b/lib/model.js index 31ff8fce80d..8f5e211d916 100644 --- a/lib/model.js +++ b/lib/model.js @@ -104,7 +104,8 @@ const saveToObjectOptions = Object.assign({}, internalToObjectOptions, { * * @param {Object} doc values for initial set * @param {Object} [fields] optional object containing the fields that were selected in the query which returned this document. You do **not** need to set this parameter to ensure Mongoose handles your [query projection](https://mongoosejs.com/docs/api/query.html#Query.prototype.select()). - * @param {Boolean} [skipId=false] optional boolean. If true, mongoose doesn't add an `_id` field to the document. + * @param {Object} [options] optional object containing the options for the document. + * @param {Boolean} [options.defaults=true] if `false`, skip applying default values to this document. * @inherits Document https://mongoosejs.com/docs/api/document.html * @event `error`: If listening to this event, 'error' is emitted when a document was saved and an `error` occurred. If not listening, the event bubbles to the connection used to create this Model. * @event `index`: Emitted after `Model#ensureIndexes` completes. If an error occurred it is passed with the event. @@ -113,7 +114,7 @@ const saveToObjectOptions = Object.assign({}, internalToObjectOptions, { * @api public */ -function Model(doc, fields, skipId) { +function Model(doc, fields, options) { if (fields instanceof Schema) { throw new TypeError('2nd argument to `Model` constructor must be a POJO or string, ' + '**not** a schema. Make sure you\'re calling `mongoose.model()`, not ' + @@ -124,7 +125,7 @@ function Model(doc, fields, skipId) { '**not** a string. Make sure you\'re calling `mongoose.model()`, not ' + '`mongoose.Model()`.'); } - Document.call(this, doc, fields, skipId); + Document.call(this, doc, fields, options); } /** diff --git a/lib/query.js b/lib/query.js index d2406a5e374..567b62eb55f 100644 --- a/lib/query.js +++ b/lib/query.js @@ -4065,7 +4065,7 @@ async function _updateThunk(op) { this._update = clone(this._update, options); const isOverwriting = op === 'replaceOne'; if (isOverwriting) { - this._update = new this.model(this._update, null, true); + this._update = new this.model(this._update, null, { skipId: true }); } else { this._update = this._castUpdate(this._update); diff --git a/lib/queryHelpers.js b/lib/queryHelpers.js index 272b2722b7e..9cb2f546756 100644 --- a/lib/queryHelpers.js +++ b/lib/queryHelpers.js @@ -90,7 +90,7 @@ exports.createModel = function createModel(model, doc, fields, userProvidedField if (discriminator) { const _fields = clone(userProvidedFields); exports.applyPaths(_fields, discriminator.schema); - return new discriminator(undefined, _fields, true); + return new discriminator(undefined, _fields, { skipId: true }); } } diff --git a/lib/schema/subdocument.js b/lib/schema/subdocument.js index 7e9655aaf53..175954e6de5 100644 --- a/lib/schema/subdocument.js +++ b/lib/schema/subdocument.js @@ -201,7 +201,7 @@ SchemaSubdocument.prototype.cast = function(val, doc, init, priorVal, options) { return obj; }, null); if (init) { - subdoc = new Constructor(void 0, selected, doc, false, { defaults: false }); + subdoc = new Constructor(void 0, selected, doc, { defaults: false }); delete subdoc.$__.defaults; subdoc.$init(val); const exclude = isExclusive(selected); @@ -209,10 +209,10 @@ SchemaSubdocument.prototype.cast = function(val, doc, init, priorVal, options) { } else { options = Object.assign({}, options, { priorDoc: priorVal }); if (Object.keys(val).length === 0) { - return new Constructor({}, selected, doc, undefined, options); + return new Constructor({}, selected, doc, options); } - return new Constructor(val, selected, doc, undefined, options); + return new Constructor(val, selected, doc, options); } return subdoc; diff --git a/lib/types/arraySubdocument.js b/lib/types/arraySubdocument.js index 920088fae76..a723bc51fe4 100644 --- a/lib/types/arraySubdocument.js +++ b/lib/types/arraySubdocument.js @@ -41,7 +41,7 @@ function ArraySubdocument(obj, parentArr, skipId, fields, index) { options = { isNew: true }; } - Subdocument.call(this, obj, fields, this[documentArrayParent], skipId, options); + Subdocument.call(this, obj, fields, this[documentArrayParent], options); } /*! diff --git a/lib/types/subdocument.js b/lib/types/subdocument.js index caac6a0ca87..0d941eaba96 100644 --- a/lib/types/subdocument.js +++ b/lib/types/subdocument.js @@ -14,11 +14,7 @@ module.exports = Subdocument; * @api private */ -function Subdocument(value, fields, parent, skipId, options) { - if (typeof skipId === 'object' && skipId != null && options == null) { - options = skipId; - skipId = undefined; - } +function Subdocument(value, fields, parent, options) { if (parent != null) { // If setting a nested path, should copy isNew from parent re: gh-7048 const parentOptions = { isNew: parent.isNew }; @@ -30,7 +26,7 @@ function Subdocument(value, fields, parent, skipId, options) { if (options != null && options.path != null) { this.$basePath = options.path; } - Document.call(this, value, fields, skipId, options); + Document.call(this, value, fields, options); delete this.$__.priorDoc; } diff --git a/test/document.test.js b/test/document.test.js index c121cc30fbc..b2a1902c01c 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -9104,8 +9104,7 @@ describe('document', function() { }); const Test = db.model('Test', testSchema); - const doc = new Test({ testArray: [{}], testSingleNested: {} }, null, - { defaults: false }); + const doc = new Test({ testArray: [{}], testSingleNested: {} }, null, { defaults: false }); assert.ok(!doc.testTopLevel); assert.ok(!doc.testNested.prop); assert.ok(!doc.testArray[0].prop); diff --git a/types/models.d.ts b/types/models.d.ts index d6b777cfb5e..bf791a6b0c3 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -293,7 +293,7 @@ declare module 'mongoose' { NodeJS.EventEmitter, IndexManager, SessionStarter { - new >(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): THydratedDocumentType; + new >(doc?: DocType, fields?: any | null, options?: AnyObject): THydratedDocumentType; aggregate(pipeline?: PipelineStage[], options?: AggregateOptions): Aggregate>; aggregate(pipeline: PipelineStage[]): Aggregate>; From 4d42dedb867f87f8a60651d9732447080b1f6363 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 2 Oct 2025 11:36:45 -0400 Subject: [PATCH 154/209] Update lib/document.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/document.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/document.js b/lib/document.js index 42439569ac5..00122788c3e 100644 --- a/lib/document.js +++ b/lib/document.js @@ -87,7 +87,7 @@ const VERSION_ALL = VERSION_WHERE | VERSION_INC; function Document(obj, fields, options) { if (typeof options === 'boolean') { - throw new Error('Tried to use skipId'); + throw new Error('The skipId parameter has been removed. Use { skipId: true } in the options parameter instead.'); } options = Object.assign({}, options); let skipId = options.skipId; From e5cff9a014a07e4723e4afffddbe4b1b129ac542 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 2 Oct 2025 11:37:40 -0400 Subject: [PATCH 155/209] docs: add skipId options docs re: code review comments --- lib/document.js | 1 + lib/model.js | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/document.js b/lib/document.js index 00122788c3e..3e0b6da2a6c 100644 --- a/lib/document.js +++ b/lib/document.js @@ -79,6 +79,7 @@ const VERSION_ALL = VERSION_WHERE | VERSION_INC; * @param {Object} [fields] optional object containing the fields which were selected in the query returning this document and any populated paths data * @param {Object} [options] various configuration options for the document * @param {Boolean} [options.defaults=true] if `false`, skip applying default values to this document. + * @param {Boolean} [options.skipId=false] By default, Mongoose document if one is not provided and the document's schema does not override Mongoose's default `_id`. Set `skipId` to `true` to skip this generation step. * @inherits NodeJS EventEmitter https://nodejs.org/api/events.html#class-eventemitter * @event `init`: Emitted on a document after it has been retrieved from the db and fully hydrated by Mongoose. * @event `save`: Emitted when the document is successfully saved diff --git a/lib/model.js b/lib/model.js index 8f5e211d916..b52ec7cc5f6 100644 --- a/lib/model.js +++ b/lib/model.js @@ -106,6 +106,7 @@ const saveToObjectOptions = Object.assign({}, internalToObjectOptions, { * @param {Object} [fields] optional object containing the fields that were selected in the query which returned this document. You do **not** need to set this parameter to ensure Mongoose handles your [query projection](https://mongoosejs.com/docs/api/query.html#Query.prototype.select()). * @param {Object} [options] optional object containing the options for the document. * @param {Boolean} [options.defaults=true] if `false`, skip applying default values to this document. + * @param {Boolean} [options.skipId=false] By default, Mongoose document if one is not provided and the document's schema does not override Mongoose's default `_id`. Set `skipId` to `true` to skip this generation step. * @inherits Document https://mongoosejs.com/docs/api/document.html * @event `error`: If listening to this event, 'error' is emitted when a document was saved and an `error` occurred. If not listening, the event bubbles to the connection used to create this Model. * @event `index`: Emitted after `Model#ensureIndexes` completes. If an error occurred it is passed with the event. From 247e6ac5c18052c8711e3a96285fd11cfe20d80b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 3 Oct 2025 11:49:33 -0400 Subject: [PATCH 156/209] types: fix one more merge issue --- types/inferschematype.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 798a84bf3fd..212567899c4 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -261,6 +261,7 @@ type IsSchemaTypeFromBuiltinClass = : T extends Types.Decimal128 ? true : T extends NativeDate ? true : T extends typeof Schema.Types.Mixed ? true + : T extends Types.UUID ? true : unknown extends Buffer ? false : T extends Buffer ? true : false; @@ -308,12 +309,12 @@ type ResolvePathType< Options['enum'][number] : number : PathValueType extends DateSchemaDefinition ? NativeDate + : PathValueType extends UuidSchemaDefinition ? Types.UUID : PathValueType extends BufferSchemaDefinition ? Buffer : PathValueType extends BooleanSchemaDefinition ? boolean : PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId : PathValueType extends Decimal128SchemaDefinition ? Types.Decimal128 : PathValueType extends BigintSchemaDefinition ? bigint - : PathValueType extends UuidSchemaDefinition ? Types.UUID : PathValueType extends DoubleSchemaDefinition ? Types.Double : PathValueType extends MapSchemaDefinition ? Map> : PathValueType extends UnionSchemaDefinition ? From 47bcd6a01edb20cae2f9f274727db989a17c4c73 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 3 Oct 2025 12:00:30 -0400 Subject: [PATCH 157/209] merge conflict fixed --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index f0beaf3b29e..38d4a8a2218 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -590,7 +590,7 @@ declare module 'mongoose' { export type SchemaDefinitionProperty> = // ThisType intersection here avoids corrupting ThisType for SchemaTypeOptions (see test gh11828) - | SchemaDefinitionWithBuiltInClass & ThisType + | SchemaDefinitionWithBuiltInClass & ThisType | SchemaTypeOptions | typeof SchemaType | Schema From 7eba5ffaabfb07b300f21a12147244f5a8bb5bd8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 5 Oct 2025 12:24:40 -0400 Subject: [PATCH 158/209] fix last tests --- test/types/schema.create.test.ts | 2 +- types/inferrawdoctype.d.ts | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index 43fb77565cd..6c51fbd0f20 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -135,7 +135,7 @@ const ProfileSchemaDef2: SchemaDefinition = { age: Schema.Types.Number }; -const ProfileSchema2: Schema> = new Schema(ProfileSchemaDef2); +const ProfileSchema2 = new Schema>(ProfileSchemaDef2); const UserSchemaDef: SchemaDefinition = { email: String, diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index 8034b44a52d..602437f0312 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -37,10 +37,20 @@ declare module 'mongoose' { * @param {PathValueType} PathValueType Document definition path type. * @param {TypeKey} TypeKey A generic refers to document definition. */ - type ObtainRawDocumentPathType = - TypeKey extends keyof PathValueType - ? ResolveRawPathType, TypeKey, RawDocTypeHint> - : ResolveRawPathType>; + type ObtainRawDocumentPathType = ResolveRawPathType< + TypeKey extends keyof PathValueType ? + TypeKey extends keyof PathValueType[TypeKey] ? + PathValueType + : PathValueType[TypeKey] + : PathValueType, + TypeKey extends keyof PathValueType ? + TypeKey extends keyof PathValueType[TypeKey] ? + {} + : Omit + : {}, + TypeKey, + RawDocTypeHint + >; type neverOrAny = ' ~neverOrAny~'; @@ -67,7 +77,7 @@ declare module 'mongoose' { : [PathValueType] extends [neverOrAny] ? PathValueType : PathValueType extends Schema ? IsItRecordAndNotAny extends true ? RawDocType : InferRawDocType : PathValueType extends ReadonlyArray ? - Item extends never ? any[] + IfEquals extends true ? any[] : Item extends Schema ? // If Item is a schema, infer its type. Array extends true ? RawDocType : InferRawDocType> From 5dc43e0c222b049864b0caddde4efeeac8247c05 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 5 Oct 2025 12:55:43 -0400 Subject: [PATCH 159/209] types: remove a bunch of unnecessary function overrides and simplify inferhydrateddoctype to improve TS perf --- types/inferhydrateddoctype.d.ts | 86 +++++++-------------------- types/models.d.ts | 102 +------------------------------- types/query.d.ts | 46 ++------------ 3 files changed, 27 insertions(+), 207 deletions(-) diff --git a/types/inferhydrateddoctype.d.ts b/types/inferhydrateddoctype.d.ts index a947b51654e..68580a96c89 100644 --- a/types/inferhydrateddoctype.d.ts +++ b/types/inferhydrateddoctype.d.ts @@ -68,38 +68,10 @@ declare module 'mongoose' { * @returns Type */ type ResolveHydratedPathType = {}, TypeKey extends string = DefaultSchemaOptions['typeKey'], TypeHint = never> = - IfEquals ? + IsNotNever extends true ? TypeHint + : PathValueType extends Schema ? THydratedDocumentType : - PathValueType extends (infer Item)[] ? - IfEquals ? - // If Item is a schema, infer its type. - IsItRecordAndNotAny extends true ? - Types.DocumentArray & EmbeddedHydratedDocType> : - Types.DocumentArray, Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType> : - Item extends Record ? - Item[TypeKey] extends Function | String ? - // If Item has a type key that's a string or a callable, it must be a scalar, - // so we can directly obtain its path type. - Types.Array> : - // If the type key isn't callable, then this is an array of objects, in which case - // we need to call InferHydratedDocType to correctly infer its type. - Types.DocumentArray< - InferRawDocType, - Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType - > : - IsSchemaTypeFromBuiltinClass extends true ? - Types.Array> : - IsItRecordAndNotAny extends true ? - Item extends Record ? - Types.Array> : - Types.DocumentArray< - InferRawDocType, - Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType - > : - Types.Array> - > : - PathValueType extends ReadonlyArray ? + PathValueType extends AnyArray ? IfEquals ? IsItRecordAndNotAny extends true ? Types.DocumentArray & EmbeddedHydratedDocType> : @@ -121,37 +93,23 @@ declare module 'mongoose' { Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType > : Types.Array> - > : - PathValueType extends StringSchemaDefinition ? PathEnumOrString : - IfEquals extends true ? PathEnumOrString : - IfEquals extends true ? PathEnumOrString : - PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray ? Options['enum'][number] : number : - IfEquals extends true ? number : - PathValueType extends DateSchemaDefinition ? NativeDate : - IfEquals extends true ? NativeDate : - PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer : - PathValueType extends BooleanSchemaDefinition ? boolean : - IfEquals extends true ? boolean : - PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId : - IfEquals extends true ? Types.ObjectId : - IfEquals extends true ? Types.ObjectId : - PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Types.Decimal128 : - IfEquals extends true ? Types.Decimal128 : - IfEquals extends true ? Types.Decimal128 : - IfEquals extends true ? bigint : - IfEquals extends true ? bigint : - PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint : - PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? UUID : - PathValueType extends 'double' | 'Double' | typeof Schema.Types.Double ? Types.Double : - IfEquals extends true ? Buffer : - PathValueType extends MapConstructor | 'Map' ? Map> : - IfEquals extends true ? Map> : - PathValueType extends ArrayConstructor ? any[] : - PathValueType extends typeof Schema.Types.Mixed ? any: - IfEquals extends true ? any: - IfEquals extends true ? any: - PathValueType extends typeof SchemaType ? PathValueType['prototype'] : - PathValueType extends Record ? InferHydratedDocType : - unknown, - TypeHint>; + > + : PathValueType extends StringSchemaDefinition ? PathEnumOrString + : IfEquals extends true ? PathEnumOrString + : PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray ? Options['enum'][number] : number + : PathValueType extends DateSchemaDefinition ? NativeDate + : PathValueType extends BufferSchemaDefinition ? Buffer + : PathValueType extends BooleanSchemaDefinition ? boolean + : PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId + : PathValueType extends Decimal128SchemaDefinition ? Types.Decimal128 + : PathValueType extends BigintSchemaDefinition ? bigint + : PathValueType extends UuidSchemaDefinition ? UUID + : PathValueType extends DoubleSchemaDefinition ? Types.Double + : PathValueType extends typeof Schema.Types.Mixed ? any + : PathValueType extends MapSchemaDefinition ? Map> + : IfEquals extends true ? any + : PathValueType extends typeof SchemaType ? PathValueType['prototype'] + : PathValueType extends ArrayConstructor ? Types.Array + : PathValueType extends Record ? InferHydratedDocType + : unknown; } diff --git a/types/models.d.ts b/types/models.d.ts index 917b3154e19..453bf285f8a 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -413,16 +413,6 @@ declare module 'mongoose' { 'deleteMany', TInstanceMethods & TVirtuals >; - deleteMany( - filter: QueryFilter - ): QueryWithHelpers< - mongodb.DeleteResult, - THydratedDocumentType, - TQueryHelpers, - TRawDocType, - 'deleteMany', - TInstanceMethods & TVirtuals - >; /** * Deletes the first document that matches `conditions` from the collection. @@ -440,16 +430,6 @@ declare module 'mongoose' { 'deleteOne', TInstanceMethods & TVirtuals >; - deleteOne( - filter: QueryFilter - ): QueryWithHelpers< - mongodb.DeleteResult, - THydratedDocumentType, - TQueryHelpers, - TRawDocType, - 'deleteOne', - TInstanceMethods & TVirtuals - >; /** Adds a discriminator type. */ discriminator>( @@ -514,17 +494,6 @@ declare module 'mongoose' { 'findOne', TInstanceMethods & TVirtuals >; - findById( - id: any, - projection?: ProjectionType | null - ): QueryWithHelpers< - HasLeanOption extends true ? TRawDocType | null : ResultDoc | null, - ResultDoc, - TQueryHelpers, - TRawDocType, - 'findOne', - TInstanceMethods & TVirtuals - >; /** Finds one document. */ findOne( @@ -551,27 +520,6 @@ declare module 'mongoose' { 'findOne', TInstanceMethods & TVirtuals >; - findOne( - filter?: QueryFilter, - projection?: ProjectionType | null - ): QueryWithHelpers< - HasLeanOption extends true ? TRawDocType | null : ResultDoc | null, - ResultDoc, - TQueryHelpers, - TRawDocType, - 'findOne', - TInstanceMethods & TVirtuals - >; - findOne( - filter?: QueryFilter - ): QueryWithHelpers< - HasLeanOption extends true ? TRawDocType | null : ResultDoc | null, - ResultDoc, - TQueryHelpers, - TRawDocType, - 'findOne', - TInstanceMethods & TVirtuals - >; /** * Shortcut for creating a new Document from existing raw data, pre-saved in the DB. @@ -635,10 +583,6 @@ declare module 'mongoose' { docs: Array, options: InsertManyOptions & { lean: true; } ): Promise>>; - insertMany( - docs: DocContents | TRawDocType, - options: InsertManyOptions & { lean: true; } - ): Promise>>; insertMany( docs: Array, options: InsertManyOptions & { rawResult: true; } @@ -792,7 +736,7 @@ declare module 'mongoose' { TInstanceMethods & TVirtuals >; find( - filter: QueryFilter, + filter?: QueryFilter, projection?: ProjectionType | null | undefined, options?: QueryOptions & mongodb.Abortable | null | undefined ): QueryWithHelpers< @@ -803,36 +747,6 @@ declare module 'mongoose' { 'find', TInstanceMethods & TVirtuals >; - find( - filter: QueryFilter, - projection?: ProjectionType | null | undefined - ): QueryWithHelpers< - HasLeanOption extends true ? TRawDocType[] : ResultDoc[], - ResultDoc, - TQueryHelpers, - TRawDocType, - 'find', - TInstanceMethods & TVirtuals - >; - find( - filter: QueryFilter - ): QueryWithHelpers< - HasLeanOption extends true ? TRawDocType[] : ResultDoc[], - ResultDoc, - TQueryHelpers, - TRawDocType, - 'find', - TInstanceMethods & TVirtuals - >; - find( - ): QueryWithHelpers< - HasLeanOption extends true ? TRawDocType[] : ResultDoc[], - ResultDoc, - TQueryHelpers, - TRawDocType, - 'find', - TInstanceMethods & TVirtuals - >; /** Creates a `findByIdAndDelete` query, filtering by the given `_id`. */ findByIdAndDelete( @@ -930,17 +844,6 @@ declare module 'mongoose' { 'findOneAndUpdate', TInstanceMethods & TVirtuals >; - findByIdAndUpdate( - id: mongodb.ObjectId | any, - update: UpdateQuery - ): QueryWithHelpers< - HasLeanOption extends true ? TRawDocType | null : ResultDoc | null, - ResultDoc, - TQueryHelpers, - TRawDocType, - 'findOneAndUpdate', - TInstanceMethods & TVirtuals - >; /** Creates a `findOneAndDelete` query: atomically finds the given document, deletes it, and returns the document as it was before deletion. */ findOneAndDelete( @@ -1115,9 +1018,6 @@ declare module 'mongoose' { update: UpdateQuery | UpdateWithAggregationPipeline, options?: (mongodb.UpdateOptions & MongooseUpdateQueryOptions) | null ): QueryWithHelpers; - updateOne( - update: UpdateQuery | UpdateWithAggregationPipeline - ): QueryWithHelpers; /** Creates a Query, applies the passed conditions, and returns the Query. */ where( diff --git a/types/query.d.ts b/types/query.d.ts index 64c5aa05961..ef3d6fef9b8 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -370,18 +370,10 @@ declare module 'mongoose' { /** Creates a `find` query: gets a list of documents that match `filter`. */ find( - filter: QueryFilter, + filter?: QueryFilter, projection?: ProjectionType | null, options?: QueryOptions | null ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find', TDocOverrides>; - find( - filter: QueryFilter, - projection?: ProjectionType | null - ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find', TDocOverrides>; - find( - filter: QueryFilter - ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find', TDocOverrides>; - find(): QueryWithHelpers, DocType, THelpers, RawDocType, 'find', TDocOverrides>; /** Declares the query a findOne operation. When executed, returns the first found document. */ findOne( @@ -389,13 +381,6 @@ declare module 'mongoose' { projection?: ProjectionType | null, options?: QueryOptions | null ): QueryWithHelpers; - findOne( - filter?: QueryFilter, - projection?: ProjectionType | null - ): QueryWithHelpers; - findOne( - filter?: QueryFilter - ): QueryWithHelpers; /** Creates a `findOneAndDelete` query: atomically finds the given document, deletes it, and returns the document as it was before deletion. */ findOneAndDelete( @@ -415,14 +400,10 @@ declare module 'mongoose' { options: QueryOptions & { upsert: true } & ReturnsNewDoc ): QueryWithHelpers; findOneAndUpdate( - filter: QueryFilter, - update: UpdateQuery, + filter?: QueryFilter, + update?: UpdateQuery, options?: QueryOptions | null ): QueryWithHelpers; - findOneAndUpdate( - update: UpdateQuery - ): QueryWithHelpers; - findOneAndUpdate(): QueryWithHelpers; /** Declares the query a findById operation. When executed, returns the document with the given `_id`. */ findById( @@ -430,13 +411,6 @@ declare module 'mongoose' { projection?: ProjectionType | null, options?: QueryOptions | null ): QueryWithHelpers; - findById( - id: mongodb.ObjectId | any, - projection?: ProjectionType | null - ): QueryWithHelpers; - findById( - id: mongodb.ObjectId | any - ): QueryWithHelpers; /** Creates a `findByIdAndDelete` query, filtering by the given `_id`. */ findByIdAndDelete( @@ -464,10 +438,6 @@ declare module 'mongoose' { update?: UpdateQuery, options?: QueryOptions | null ): QueryWithHelpers; - findByIdAndUpdate( - id: mongodb.ObjectId | any, - update: UpdateQuery - ): QueryWithHelpers; /** Specifies a `$geometry` condition */ geometry(object: { type: string, coordinates: any[] }): this; @@ -832,18 +802,13 @@ declare module 'mongoose' { /** * Declare and/or execute this query as an updateMany() operation. Same as - * `update()`, except MongoDB will update _all_ documents that match - * `filter` (as opposed to just the first one) regardless of the value of - * the `multi` option. + * `update()`, except MongoDB will update _all_ documents that match `filter` */ updateMany( filter: QueryFilter, update: UpdateQuery | UpdateWithAggregationPipeline, options?: QueryOptions | null ): QueryWithHelpers; - updateMany( - update: UpdateQuery | UpdateWithAggregationPipeline - ): QueryWithHelpers; /** * Declare and/or execute this query as an updateOne() operation. Same as @@ -854,9 +819,6 @@ declare module 'mongoose' { update: UpdateQuery | UpdateWithAggregationPipeline, options?: QueryOptions | null ): QueryWithHelpers; - updateOne( - update: UpdateQuery | UpdateWithAggregationPipeline - ): QueryWithHelpers; /** * Sets the specified number of `mongod` servers, or tag set of `mongod` servers, From c30375de09e514804e1ffdc58496d596d5d08fa4 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 Oct 2025 16:28:56 -0400 Subject: [PATCH 160/209] some merge conflict fixes --- test/types/lean.test.ts | 7 +++---- types/document.d.ts | 2 ++ types/index.d.ts | 1 + types/inferhydrateddoctype.d.ts | 2 +- types/inferrawdoctype.d.ts | 2 +- types/models.d.ts | 20 ++++++++++++++++---- types/query.d.ts | 4 ++-- 7 files changed, 26 insertions(+), 12 deletions(-) diff --git a/test/types/lean.test.ts b/test/types/lean.test.ts index c13d4081e0d..b8a448681ed 100644 --- a/test/types/lean.test.ts +++ b/test/types/lean.test.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types, InferSchemaType, FlattenMaps, HydratedDocument, Model, Document, PopulatedDoc } from 'mongoose'; +import mongoose, { Schema, model, Types, InferSchemaType, FlattenMaps, HydratedDocument, Model, Document, PopulatedDoc } from 'mongoose'; import { expectAssignable, expectError, expectType } from 'tsd'; function gh10345() { @@ -47,12 +47,11 @@ async function gh11761() { console.log({ _id, thing1 }); } - // stretch goal, make sure lean works as well const foundDoc = await ThingModel.findOne().lean().limit(1).exec(); { if (!foundDoc) { - return; // Tell TS that it isn't null + return; } const { _id, ...thing2 } = foundDoc; expectType(foundDoc._id); @@ -159,7 +158,7 @@ async function gh13010_1() { }); const country = await CountryModel.findOne().lean().orFail().exec(); - expectType>(country.name); + expectType>(country.name); } async function gh13345_1() { diff --git a/types/document.d.ts b/types/document.d.ts index 0e77d6f88e8..9841543b4b6 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -350,6 +350,8 @@ declare module 'mongoose' { // Default - no special options, just Require_id toObject(options?: ToObjectOptions): Require_id; + toObject(options?: ToObjectOptions): Default__v, ResolveSchemaOptions>; + /** Clears the modified state on the specified path. */ unmarkModified(path: T): void; unmarkModified(path: string): void; diff --git a/types/index.d.ts b/types/index.d.ts index 3d2ab022de1..410253eb53c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -223,6 +223,7 @@ declare module 'mongoose' { ObtainSchemaGeneric & ObtainSchemaGeneric, ObtainSchemaGeneric, ObtainSchemaGeneric, + InferSchemaType, ObtainSchemaGeneric >; diff --git a/types/inferhydrateddoctype.d.ts b/types/inferhydrateddoctype.d.ts index 68580a96c89..ead9ea77b7f 100644 --- a/types/inferhydrateddoctype.d.ts +++ b/types/inferhydrateddoctype.d.ts @@ -106,7 +106,7 @@ declare module 'mongoose' { : PathValueType extends UuidSchemaDefinition ? UUID : PathValueType extends DoubleSchemaDefinition ? Types.Double : PathValueType extends typeof Schema.Types.Mixed ? any - : PathValueType extends MapSchemaDefinition ? Map> + : PathValueType extends MapSchemaDefinition ? Map> : IfEquals extends true ? any : PathValueType extends typeof SchemaType ? PathValueType['prototype'] : PathValueType extends ArrayConstructor ? Types.Array diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index cd987a0a464..e8fceb5bcfb 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -115,7 +115,7 @@ declare module 'mongoose' { : PathValueType extends Decimal128SchemaDefinition ? Types.Decimal128 : PathValueType extends BigintSchemaDefinition ? bigint : PathValueType extends UuidSchemaDefinition ? Types.UUID - : PathValueType extends MapSchemaDefinition ? Map> + : PathValueType extends MapSchemaDefinition ? Record> : PathValueType extends DoubleSchemaDefinition ? Types.Double : PathValueType extends UnionSchemaDefinition ? ResolveRawPathType ? Item : never> diff --git a/types/models.d.ts b/types/models.d.ts index d73d4f23c1c..edf10ee1530 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -476,7 +476,7 @@ declare module 'mongoose' { projection?: ProjectionType | null, options?: QueryOptions | null ): QueryWithHelpers< - HasLeanOption extends true ? TRawDocType | null : ResultDoc | null, + HasLeanOption extends true ? TLeanResultType | null : ResultDoc | null, ResultDoc, TQueryHelpers, TLeanResultType, @@ -490,7 +490,7 @@ declare module 'mongoose' { projection?: ProjectionType | null, options?: QueryOptions & mongodb.Abortable | null ): QueryWithHelpers< - HasLeanOption extends true ? TRawDocType | null : ResultDoc | null, + HasLeanOption extends true ? TLeanResultType | null : ResultDoc | null, ResultDoc, TQueryHelpers, TLeanResultType, @@ -700,12 +700,24 @@ declare module 'mongoose' { >; /** Creates a `find` query: gets a list of documents that match `filter`. */ + find( + filter: QueryFilter, + projection: ProjectionType | null | undefined, + options: QueryOptions & { lean: true } & mongodb.Abortable + ): QueryWithHelpers< + GetLeanResultType, + ResultDoc, + TQueryHelpers, + TLeanResultType, + 'find', + TInstanceMethods & TVirtuals + >; find( filter?: QueryFilter, projection?: ProjectionType | null | undefined, - options?: QueryOptions & { lean: true } & mongodb.Abortable + options?: QueryOptions & mongodb.Abortable ): QueryWithHelpers< - GetLeanResultType, + ResultDoc[], ResultDoc, TQueryHelpers, TLeanResultType, diff --git a/types/query.d.ts b/types/query.d.ts index 0b0e5d97763..98a43ce881a 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -522,7 +522,7 @@ declare module 'mongoose' { QueryOp, TDocOverrides >; - lean(): QueryWithHelpers< + lean(): QueryWithHelpers< ResultType extends null ? LeanResultType | null : LeanResultType, @@ -532,7 +532,7 @@ declare module 'mongoose' { QueryOp, TDocOverrides >; - lean( + lean( val: boolean | LeanOptions ): QueryWithHelpers< ResultType extends null From 3b9b6c8384e6e7910667567fc286b7771d456378 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 Oct 2025 16:32:54 -0400 Subject: [PATCH 161/209] fix THydratedDocumentType params --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 410253eb53c..c4a7c6c2417 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -297,7 +297,7 @@ declare module 'mongoose' { ObtainDocumentType>, ResolveSchemaOptions >, - THydratedDocumentType = HydratedDocument>, + THydratedDocumentType = HydratedDocument>, TSchemaDefinition = SchemaDefinition, RawDocType, THydratedDocumentType>, LeanResultType = IsItRecordAndNotAny extends true ? RawDocType : Default__v>>> > From 64027dcff4d1bdb988474e8c5513c113b099f9e8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 Oct 2025 16:42:54 -0400 Subject: [PATCH 162/209] fix some missing overrides --- types/document.d.ts | 2 ++ types/models.d.ts | 68 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/types/document.d.ts b/types/document.d.ts index 9841543b4b6..ca9eb473cba 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -304,6 +304,8 @@ declare module 'mongoose' { // Default - no special options, just Require_id toJSON(options?: ToObjectOptions): Require_id; + toJSON(options?: ToObjectOptions): Default__v, ResolveSchemaOptions>; + /** Converts this document into a plain-old JavaScript object ([POJO](https://masteringjs.io/tutorials/fundamentals/pojo)). */ // flattenMaps: false (default) cases toObject(options: ToObjectOptions & { flattenMaps: false, flattenObjectIds: true, virtuals: true, versionKey: false }): ObjectIdToString, '__v'>>; diff --git a/types/models.d.ts b/types/models.d.ts index edf10ee1530..2757c75bedd 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -473,7 +473,19 @@ declare module 'mongoose' { */ findById( id: any, - projection?: ProjectionType | null, + projection: ProjectionType | null | undefined, + options: QueryOptions & { lean: true } + ): QueryWithHelpers< + TLeanResultType | null, + ResultDoc, + TQueryHelpers, + TLeanResultType, + 'findOne', + TInstanceMethods & TVirtuals + >; + findById( + id?: any, + projection?: ProjectionType | null | undefined, options?: QueryOptions | null ): QueryWithHelpers< HasLeanOption extends true ? TLeanResultType | null : ResultDoc | null, @@ -485,10 +497,22 @@ declare module 'mongoose' { >; /** Finds one document. */ + findOne( + filter: QueryFilter, + projection: ProjectionType | null | undefined, + options: QueryOptions & { lean: true } & mongodb.Abortable + ): QueryWithHelpers< + TLeanResultType | null, + ResultDoc, + TQueryHelpers, + TLeanResultType, + 'findOne', + TInstanceMethods & TVirtuals + >; findOne( filter?: QueryFilter, - projection?: ProjectionType | null, - options?: QueryOptions & mongodb.Abortable | null + projection?: ProjectionType | null | undefined, + options?: QueryOptions & mongodb.Abortable | null | undefined ): QueryWithHelpers< HasLeanOption extends true ? TLeanResultType | null : ResultDoc | null, ResultDoc, @@ -727,8 +751,19 @@ declare module 'mongoose' { /** Creates a `findByIdAndDelete` query, filtering by the given `_id`. */ findByIdAndDelete( - id?: mongodb.ObjectId | any, - options?: QueryOptions & { lean: true } + id: mongodb.ObjectId | any, + options: QueryOptions & { includeResultMetadata: true, lean: true } + ): QueryWithHelpers< + ModifyResult, + ResultDoc, + TQueryHelpers, + TLeanResultType, + 'findOneAndDelete', + TInstanceMethods & TVirtuals + >; + findByIdAndDelete( + id: mongodb.ObjectId | any, + options: QueryOptions & { lean: true } ): QueryWithHelpers< TLeanResultType | null, ResultDoc, @@ -737,6 +772,29 @@ declare module 'mongoose' { 'findOneAndDelete', TInstanceMethods & TVirtuals >; + findByIdAndDelete( + id: mongodb.ObjectId | any, + options: QueryOptions & { includeResultMetadata: true } + ): QueryWithHelpers< + HasLeanOption extends true ? ModifyResult : ModifyResult, + ResultDoc, + TQueryHelpers, + TLeanResultType, + 'findOneAndDelete', + TInstanceMethods & TVirtuals + >; + findByIdAndDelete( + id?: mongodb.ObjectId | any, + options?: QueryOptions | null + ): QueryWithHelpers< + HasLeanOption extends true ? TLeanResultType | null : ResultDoc | null, + ResultDoc, + TQueryHelpers, + TLeanResultType, + 'findOneAndDelete', + TInstanceMethods & TVirtuals + >; + /** Creates a `findOneAndUpdate` query, filtering by the given `_id`. */ findByIdAndUpdate( From 72eba8c4368289dd7a3366e8253d486fa5a9f8d3 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 Oct 2025 16:50:50 -0400 Subject: [PATCH 163/209] fix: clean up a couple of more merge conflict issues --- types/document.d.ts | 8 ++++---- types/index.d.ts | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/types/document.d.ts b/types/document.d.ts index ca9eb473cba..c848aa54725 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -301,8 +301,8 @@ declare module 'mongoose' { // Handle virtuals: true toJSON(options: ToObjectOptions & { virtuals: true }): Require_id; - // Default - no special options, just Require_id - toJSON(options?: ToObjectOptions): Require_id; + // Default - no special options + toJSON(options?: ToObjectOptions): Default__v, TSchemaOptions>; toJSON(options?: ToObjectOptions): Default__v, ResolveSchemaOptions>; @@ -349,8 +349,8 @@ declare module 'mongoose' { // Handle virtuals: true toObject(options: ToObjectOptions & { virtuals: true }): Require_id; - // Default - no special options, just Require_id - toObject(options?: ToObjectOptions): Require_id; + // Default - no special options + toObject(options?: ToObjectOptions): Default__v, TSchemaOptions>; toObject(options?: ToObjectOptions): Default__v, ResolveSchemaOptions>; diff --git a/types/index.d.ts b/types/index.d.ts index c4a7c6c2417..f4385db7a5c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -98,7 +98,9 @@ declare module 'mongoose' { InferSchemaType, ObtainSchemaGeneric & ObtainSchemaGeneric, ObtainSchemaGeneric, - ObtainSchemaGeneric + ObtainSchemaGeneric, + InferSchemaType, + ObtainSchemaGeneric >, TSchema, ObtainSchemaGeneric From 31778f137e797e4209e670316b115c765db2b0ca Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 Oct 2025 17:03:19 -0400 Subject: [PATCH 164/209] couple of more fixes --- types/index.d.ts | 2 +- types/inferrawdoctype.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index f4385db7a5c..8f91a145cde 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -299,7 +299,7 @@ declare module 'mongoose' { ObtainDocumentType>, ResolveSchemaOptions >, - THydratedDocumentType = HydratedDocument>, + THydratedDocumentType = HydratedDocument>, TSchemaDefinition = SchemaDefinition, RawDocType, THydratedDocumentType>, LeanResultType = IsItRecordAndNotAny extends true ? RawDocType : Default__v>>> > diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index e8fceb5bcfb..2cb845a1aa4 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -109,7 +109,7 @@ declare module 'mongoose' { Options['enum'][number] : number : PathValueType extends DateSchemaDefinition ? NativeDate - : PathValueType extends BufferSchemaDefinition ? Buffer + : PathValueType extends BufferSchemaDefinition ? (TTransformOptions extends { bufferToBinary: true } ? Binary : Buffer) : PathValueType extends BooleanSchemaDefinition ? boolean : PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId : PathValueType extends Decimal128SchemaDefinition ? Types.Decimal128 From d565fb16c8fc59053ea3decb000dfb050fb9dc46 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 Oct 2025 17:29:09 -0400 Subject: [PATCH 165/209] quick test fix --- test/types/schema.create.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index 35238487a1b..8a79381adff 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -1177,7 +1177,7 @@ function maps() { expectType(doc.myMap!.get('answer')); const obj = doc.toObject(); - expectType>(obj.myMap); + expectType>(obj.myMap); } function gh13514() { From 0c7ce68d42fb973e8c4ba6d65546d1662ec154c1 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 Oct 2025 17:41:33 -0400 Subject: [PATCH 166/209] clean up a couple of more type issues --- types/inferhydrateddoctype.d.ts | 2 +- types/inferrawdoctype.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/types/inferhydrateddoctype.d.ts b/types/inferhydrateddoctype.d.ts index ead9ea77b7f..9d1b391d3dc 100644 --- a/types/inferhydrateddoctype.d.ts +++ b/types/inferhydrateddoctype.d.ts @@ -78,7 +78,7 @@ declare module 'mongoose' { Types.DocumentArray, Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType> : Item extends Record ? Item[TypeKey] extends Function | String ? - Types.Array> : + Types.Array> : Types.DocumentArray< InferRawDocType, Types.Subdocument['_id'], unknown, InferHydratedDocType> & InferHydratedDocType diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index 2cb845a1aa4..cfbcfc228b9 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -96,7 +96,7 @@ declare module 'mongoose' { : // If the type key isn't callable, then this is an array of objects, in which case // we need to call InferRawDocType to correctly infer its type. Array> - : IsSchemaTypeFromBuiltinClass extends true ? ResolveRawPathType[] + : IsSchemaTypeFromBuiltinClass extends true ? ObtainRawDocumentPathType[] : IsItRecordAndNotAny extends true ? Item extends Record ? ObtainRawDocumentPathType[] @@ -115,7 +115,7 @@ declare module 'mongoose' { : PathValueType extends Decimal128SchemaDefinition ? Types.Decimal128 : PathValueType extends BigintSchemaDefinition ? bigint : PathValueType extends UuidSchemaDefinition ? Types.UUID - : PathValueType extends MapSchemaDefinition ? Record> + : PathValueType extends MapSchemaDefinition ? Record> : PathValueType extends DoubleSchemaDefinition ? Types.Double : PathValueType extends UnionSchemaDefinition ? ResolveRawPathType ? Item : never> From b579111056062404dd05792fb14ac8060849a4dd Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 Oct 2025 17:49:30 -0400 Subject: [PATCH 167/209] fix enum handling in InferRawDocType --- types/inferrawdoctype.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index cfbcfc228b9..9e54ef3ea0e 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -96,7 +96,7 @@ declare module 'mongoose' { : // If the type key isn't callable, then this is an array of objects, in which case // we need to call InferRawDocType to correctly infer its type. Array> - : IsSchemaTypeFromBuiltinClass extends true ? ObtainRawDocumentPathType[] + : IsSchemaTypeFromBuiltinClass extends true ? ResolveRawPathType[] : IsItRecordAndNotAny extends true ? Item extends Record ? ObtainRawDocumentPathType[] From 47bb200453c2482fba6e45a419cb39530f6d7564 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 Oct 2025 18:07:05 -0400 Subject: [PATCH 168/209] fix out of date test --- test/types/document.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types/document.test.ts b/test/types/document.test.ts index 1af69d330f2..ff59cf321d1 100644 --- a/test/types/document.test.ts +++ b/test/types/document.test.ts @@ -562,7 +562,7 @@ async function gh15578() { const schemaOptions = { versionKey: 'taco' } as const; - type ModelType = Model>; + type ModelType = Model>; const ASchema = new Schema({ testProperty: Number From 5671e58fcfb5140f81f8fac45e94cae27a80c083 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 15 Oct 2025 19:07:15 -0400 Subject: [PATCH 169/209] fix up test inconsistency --- test/types/schema.create.test.ts | 40 ++++++++++++++++++++++++++------ types/index.d.ts | 2 +- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index 8a79381adff..a525d6846ba 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -1898,13 +1898,39 @@ async function testInferHydratedDocTypeFromSchema() { type HydratedDocType = InferHydratedDocTypeFromSchema; - type Expected = HydratedDocument<{ - name?: string | null | undefined, - arr: Types.Array, - docArr: Types.DocumentArray<{ name: string } & { _id: Types.ObjectId }>, - subdoc?: HydratedDocument<{ answer: number } & { _id: Types.ObjectId }> | null | undefined, - map?: Map | null | undefined - } & { _id: Types.ObjectId }>; + type Expected = HydratedDocument< + { + name?: string | null | undefined, + arr: Types.Array, + docArr: Types.DocumentArray<{ name: string } & { _id: Types.ObjectId }>, + subdoc?: HydratedDocument<{ answer: number } & { _id: Types.ObjectId }> | null | undefined, + map?: Map | null | undefined + } & { _id: Types.ObjectId }, + {}, + {}, + {}, + { + name?: string | null | undefined, + arr: number[], + docArr: Array<{ name: string } & { _id: Types.ObjectId }>, + subdoc?: ({ answer: number } & { _id: Types.ObjectId }) | null | undefined, + map?: Record | null | undefined + } & { _id: Types.ObjectId } + >; expectType({} as HydratedDocType); + + const def = { + name: String, + arr: [Number], + docArr: [{ name: { type: String, required: true } }], + map: { type: Map, of: String } + } as const; + type InferredHydratedDocType = InferHydratedDocType; + expectType<{ + name?: string | null | undefined, + arr?: Types.Array | null | undefined, + docArr?: Types.DocumentArray<{ name: string } & { _id: Types.ObjectId }> | null | undefined, + map?: Map | null | undefined + } & { _id: Types.ObjectId }>({} as InferredHydratedDocType); } diff --git a/types/index.d.ts b/types/index.d.ts index 8f91a145cde..e326869e56d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -367,7 +367,7 @@ declare module 'mongoose' { InferRawDocType>, ResolveSchemaOptions >, - THydratedDocumentType extends AnyObject = HydratedDocument>> + THydratedDocumentType extends AnyObject = HydratedDocument>> >(def: TSchemaDefinition, options: TSchemaOptions): Schema< RawDocType, Model, From 659c098b8050804972627f26641772d42faf6f54 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 16 Oct 2025 11:50:13 -0400 Subject: [PATCH 170/209] types: clean up a couple of more test failures due to missing TVirtuals --- test/types/schema.create.test.ts | 6 +++--- types/connection.d.ts | 29 +++++++++++++++++++---------- types/index.d.ts | 2 +- types/schemaoptions.d.ts | 2 +- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index a525d6846ba..64920468885 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -415,8 +415,8 @@ export function autoTypedSchema() { objectId2?: Types.ObjectId | null; objectId3?: Types.ObjectId | null; customSchema?: Int8 | null; - map1?: Record | null; - map2?: Record | null; + map1?: Record | null; + map2?: Record | null; array1: string[]; array2: any[]; array3: any[]; @@ -1880,7 +1880,7 @@ function testInferRawDocTypeFromSchema() { arr: number[], docArr: ({ name: string } & { _id: Types.ObjectId })[], subdoc?: ({ answer: number } & { _id: Types.ObjectId }) | null | undefined, - map?: Record | null | undefined; + map?: Record | null | undefined; } & { _id: Types.ObjectId }; expectType({} as RawDocType); diff --git a/types/connection.d.ts b/types/connection.d.ts index 76b36d17e6f..0581a5e80b4 100644 --- a/types/connection.d.ts +++ b/types/connection.d.ts @@ -186,16 +186,25 @@ declare module 'mongoose' { collection?: string, options?: CompileModelOptions ): Model< - InferSchemaType, - ObtainSchemaGeneric, - ObtainSchemaGeneric, - {}, - HydratedDocument< - InferSchemaType, - ObtainSchemaGeneric, - ObtainSchemaGeneric - >, - TSchema> & ObtainSchemaGeneric; + InferSchemaType, + ObtainSchemaGeneric, + ObtainSchemaGeneric, + ObtainSchemaGeneric, + // If first schema generic param is set, that means we have an explicit raw doc type, + // so user should also specify a hydrated doc type if the auto inferred one isn't correct. + IsItRecordAndNotAny> extends true + ? ObtainSchemaGeneric + : HydratedDocument< + InferSchemaType, + ObtainSchemaGeneric & ObtainSchemaGeneric, + ObtainSchemaGeneric, + ObtainSchemaGeneric, + InferSchemaType, + ObtainSchemaGeneric + >, + TSchema, + ObtainSchemaGeneric + > & ObtainSchemaGeneric; model( name: string, schema?: Schema, diff --git a/types/index.d.ts b/types/index.d.ts index e326869e56d..372453b23b6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -299,7 +299,7 @@ declare module 'mongoose' { ObtainDocumentType>, ResolveSchemaOptions >, - THydratedDocumentType = HydratedDocument>, + THydratedDocumentType = HydratedDocument & TInstanceMethods, TQueryHelpers, TVirtuals, RawDocType, ResolveSchemaOptions>, TSchemaDefinition = SchemaDefinition, RawDocType, THydratedDocumentType>, LeanResultType = IsItRecordAndNotAny extends true ? RawDocType : Default__v>>> > diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index fefc5bd9875..a515afb8ad1 100644 --- a/types/schemaoptions.d.ts +++ b/types/schemaoptions.d.ts @@ -16,7 +16,7 @@ declare module 'mongoose' { QueryHelpers = {}, TStaticMethods = {}, TVirtuals = {}, - THydratedDocumentType = HydratedDocument, + THydratedDocumentType = HydratedDocument, TModelType = Model > { /** From d2b0ace3902b7bc0e4dd098077fa3dfc6044eb45 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 16 Oct 2025 13:24:12 -0400 Subject: [PATCH 171/209] use RawDocType in default THydratedDocumentType only if not any --- types/index.d.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 372453b23b6..ca79e022fc2 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -299,7 +299,14 @@ declare module 'mongoose' { ObtainDocumentType>, ResolveSchemaOptions >, - THydratedDocumentType = HydratedDocument & TInstanceMethods, TQueryHelpers, TVirtuals, RawDocType, ResolveSchemaOptions>, + THydratedDocumentType = HydratedDocument< + DocType, + AddDefaultId & TInstanceMethods, + TQueryHelpers, + TVirtuals, + IsItRecordAndNotAny extends true ? RawDocType : DocType, + ResolveSchemaOptions + >, TSchemaDefinition = SchemaDefinition, RawDocType, THydratedDocumentType>, LeanResultType = IsItRecordAndNotAny extends true ? RawDocType : Default__v>>> > From 4345355055c86ff33593e18cdcea17361da8f15b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 16 Oct 2025 14:45:34 -0400 Subject: [PATCH 172/209] apply id virtual correctly and fix remaining tests --- package.json | 2 +- test/types/schema.test.ts | 4 ++-- types/index.d.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 33ed8c79998..c574979d12f 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "test": "mocha --exit ./test/*.test.js", "test-deno": "deno run --allow-env --allow-read --allow-net --allow-run --allow-sys --allow-write ./test/deno.mjs", "test-rs": "START_REPLICA_SET=1 mocha --timeout 30000 --exit ./test/*.test.js", - "test-tsd": "node ./test/types/check-types-filename && tsd", + "test-tsd": "node ./test/types/check-types-filename && tsd --full", "setup-test-encryption": "node scripts/setup-encryption-tests.js", "test-encryption": "mocha --exit ./test/encryption/*.test.js", "tdd": "mocha ./test/*.test.js --inspect --watch --recursive --watch-files ./**/*.{js,ts}", diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 2627bd87538..f3c724997d5 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -2007,13 +2007,13 @@ function testInferHydratedDocTypeFromSchema() { type HydratedDocType = InferHydratedDocTypeFromSchema; - type Expected = HydratedDocument, subdoc?: { answer: number } | null | undefined, map?: Map | null | undefined - }>>; + }, { id: string }, {}, { id: string }>; expectType({} as HydratedDocType); } diff --git a/types/index.d.ts b/types/index.d.ts index ca79e022fc2..58f2d6e996a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -172,7 +172,7 @@ declare module 'mongoose' { TQueryHelpers = {}, TVirtuals = {}, RawDocType = HydratedDocPathsType, - TSchemaOptions = {} + TSchemaOptions = DefaultSchemaOptions > = IfAny< HydratedDocPathsType, any, @@ -303,7 +303,7 @@ declare module 'mongoose' { DocType, AddDefaultId & TInstanceMethods, TQueryHelpers, - TVirtuals, + AddDefaultId, IsItRecordAndNotAny extends true ? RawDocType : DocType, ResolveSchemaOptions >, From 1956262e97e44ba2a26bcc1c227c1c1dbf9598be Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 23 Oct 2025 18:51:00 -0400 Subject: [PATCH 173/209] upgrade to pre-release of mongodb node driver 7 --- docs/migrating_to_9.md | 12 ++++++++++++ package.json | 2 +- test/index.test.js | 2 +- test/model.query.casting.test.js | 4 ++-- test/types/base.test.ts | 4 ++-- test/types/models.test.ts | 4 ++-- 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index cb242a76f54..d3f8df7c0a5 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -89,6 +89,18 @@ await Model.updateOne({}, [{ $set: { newProp: 'test2' } }], { updatePipeline: tr [MongoDB no longer supports the `background` option for indexes as of MongoDB 4.2](https://www.mongodb.com/docs/manual/core/index-creation/#index-operations). Mongoose 9 will no longer set the background option by default and Mongoose 9 no longer supports setting the `background` option on `Schema.prototype.index()`. +## `mongoose.isValidObjectId()` returns false for numbers + +In Mongoose 8, you could create a new ObjectId from a number, and `isValidObjectId()` would return `true` for numbers. In Mongoose 9, `isValidObjectId()` will return `false` for numbers and you can no longer create a new ObjectId from a number. + +```javascript +// true in mongoose 8, false in mongoose 9 +mongoose.isValidObjectId(6); + +// Works in Mongoose 8, throws in Mongoose 9 +new mongoose.Types.ObjectId(6); +```` + ## Subdocument `deleteOne()` hooks execute only when subdocument is deleted Currently, calling `deleteOne()` on a subdocument will execute the `deleteOne()` hooks on the subdocument regardless of whether the subdocument is actually deleted. diff --git a/package.json b/package.json index 0725b834122..fc26a3795cc 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "license": "MIT", "dependencies": { "kareem": "git+https://github.com/mongoosejs/kareem.git#vkarpov15/remove-isasync", - "mongodb": "~6.20.0", + "mongodb": "6.20.0-dev.20251023.sha.c2b988eb", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", diff --git a/test/index.test.js b/test/index.test.js index 4b0dbb9d30c..7c3d716fe19 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -761,7 +761,7 @@ describe('mongoose module:', function() { assert.ok(mongoose.isValidObjectId('5f5c2d56f6e911019ec2acdc')); assert.ok(mongoose.isValidObjectId('608DE01F32B6A93BBA314159')); assert.ok(mongoose.isValidObjectId(new mongoose.Types.ObjectId())); - assert.ok(mongoose.isValidObjectId(6)); + assert.ok(!mongoose.isValidObjectId(6)); assert.ok(!mongoose.isValidObjectId({ test: 42 })); }); diff --git a/test/model.query.casting.test.js b/test/model.query.casting.test.js index 1dc658e3f29..5efa5eec72e 100644 --- a/test/model.query.casting.test.js +++ b/test/model.query.casting.test.js @@ -436,7 +436,7 @@ describe('model query casting', function() { describe('$elemMatch', function() { it('should cast String to ObjectId in $elemMatch', async function() { - const commentId = new mongoose.Types.ObjectId(111); + const commentId = new mongoose.Types.ObjectId('1'.repeat(24)); const post = new BlogPostB({ comments: [{ _id: commentId }] }); const id = post._id.toString(); @@ -447,7 +447,7 @@ describe('model query casting', function() { }); it('should cast String to ObjectId in $elemMatch inside $not', async function() { - const commentId = new mongoose.Types.ObjectId(111); + const commentId = new mongoose.Types.ObjectId('1'.repeat(24)); const post = new BlogPostB({ comments: [{ _id: commentId }] }); const id = post._id.toString(); diff --git a/test/types/base.test.ts b/test/types/base.test.ts index de3bc9ef685..d13c10ea17d 100644 --- a/test/types/base.test.ts +++ b/test/types/base.test.ts @@ -57,8 +57,8 @@ function gh10139() { } function gh12100() { - mongoose.syncIndexes({ continueOnError: true, noResponse: true }); - mongoose.syncIndexes({ continueOnError: false, noResponse: true }); + mongoose.syncIndexes({ continueOnError: true, sparse: true }); + mongoose.syncIndexes({ continueOnError: false, sparse: true }); } function setAsObject() { diff --git a/test/types/models.test.ts b/test/types/models.test.ts index f7c266cf898..acecc5a3074 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -481,8 +481,8 @@ function gh12100() { const Model = model('Model', schema); - Model.syncIndexes({ continueOnError: true, noResponse: true }); - Model.syncIndexes({ continueOnError: false, noResponse: true }); + Model.syncIndexes({ continueOnError: true, sparse: true }); + Model.syncIndexes({ continueOnError: false, sparse: true }); } (function gh12070() { From 9c62784bd9494262bef3ea14b49ba23d51aa9a01 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 23 Oct 2025 18:55:57 -0400 Subject: [PATCH 174/209] install correct version of client-encryption --- package.json | 1 + scripts/configure-cluster-with-encryption.sh | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index fc26a3795cc..b5edf67e2f2 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "moment": "2.30.1", "mongodb-memory-server": "10.2.1", "mongodb-runner": "^5.8.2", + "mongodb-client-encryption": "7.0.0-alpha.1", "ncp": "^2.0.0", "nyc": "15.1.0", "pug": "3.0.3", diff --git a/scripts/configure-cluster-with-encryption.sh b/scripts/configure-cluster-with-encryption.sh index c87b26e705d..efe2ae87510 100644 --- a/scripts/configure-cluster-with-encryption.sh +++ b/scripts/configure-cluster-with-encryption.sh @@ -7,9 +7,6 @@ export CWD=$(pwd) export DRIVERS_TOOLS_PINNED_COMMIT=4e18803c074231ec9fc3ace8f966e2c49d9874bb -# install extra dependency -npm install --no-save mongodb-client-encryption - # set up mongodb cluster and encryption configuration if the data/ folder does not exist if [ ! -d "data" ]; then From 7139e4a9ac194d759795ca40d14b3717e04d3189 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 23 Oct 2025 19:01:00 -0400 Subject: [PATCH 175/209] Update docs/migrating_to_9.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/migrating_to_9.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index d3f8df7c0a5..1ea356aafea 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -99,7 +99,6 @@ mongoose.isValidObjectId(6); // Works in Mongoose 8, throws in Mongoose 9 new mongoose.Types.ObjectId(6); -```` ## Subdocument `deleteOne()` hooks execute only when subdocument is deleted From 663c759ef120ebe587b3a3cb35c1050f420135e9 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 27 Oct 2025 14:10:21 -0400 Subject: [PATCH 176/209] WIP allow pre('init') hooks to overwrite arguments re: #15389 --- lib/document.js | 4 ++++ lib/model.js | 9 ++++---- lib/mongoose.js | 37 ++++++++++++++++++++++++++++++++ lib/schema/subdocument.js | 2 +- package.json | 2 +- test/document.test.js | 44 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 91 insertions(+), 7 deletions(-) diff --git a/lib/document.js b/lib/document.js index c8d1c47f9fe..bf0e0848741 100644 --- a/lib/document.js +++ b/lib/document.js @@ -733,6 +733,10 @@ Document.prototype.$__init = function(doc, opts) { function init(self, obj, doc, opts, prefix) { prefix = prefix || ''; + if (typeof obj !== 'object' || Array.isArray(obj)) { + throw new ObjectExpectedError(self.$basePath, obj); + } + if (obj.$__ != null) { obj = obj._doc; } diff --git a/lib/model.js b/lib/model.js index 60d83a2fa06..86e0f24eb06 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3266,16 +3266,15 @@ Model.bulkWrite = async function bulkWrite(ops, options) { } options = options || {}; - const shouldSkip = await this.hooks.execPre('bulkWrite', this, [ops, options]).catch(err => { + [ops, options] = await this.hooks.execPre('bulkWrite', this, [ops, options]).catch(err => { if (err instanceof Kareem.skipWrappedFunction) { return err; } throw err; - } - ); + }); - if (shouldSkip) { - return shouldSkip.args[0]; + if (ops instanceof Kareem.skipWrappedFunction) { + return ops.args[0]; } const ordered = options.ordered == null ? true : options.ordered; diff --git a/lib/mongoose.js b/lib/mongoose.js index ee5afb83f64..f6f93aa32cd 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -1327,6 +1327,43 @@ Mongoose.prototype.skipMiddlewareFunction = Kareem.skipWrappedFunction; Mongoose.prototype.overwriteMiddlewareResult = Kareem.overwriteResult; +/** + * Use this function in `pre()` middleware to replace the arguments passed to the next middleware or hook. + * + * #### Example: + * + * // Suppose you have a schema for time in "HH:MM" string format, but you want to store it as an object { hours, minutes } + * const timeStringToObject = (time) => { + * if (typeof time !== 'string') return time; + * const [hours, minutes] = time.split(':'); + * return { hours: parseInt(hours), minutes: parseInt(minutes) }; + * }; + * + * const timeSchema = new Schema({ + * hours: { type: Number, required: true }, + * minutes: { type: Number, required: true }, + * }); + * + * // In a pre('init') hook, replace raw string doc with custom object form + * timeSchema.pre('init', function(doc) { + * if (typeof doc === 'string') { + * return mongoose.overwriteMiddlewareArguments(timeStringToObject(doc)); + * } + * }); + * + * // Now, initializing with a time string gets auto-converted by the hook + * const userSchema = new Schema({ time: timeSchema }); + * const User = mongoose.model('User', userSchema); + * const doc = new User({}); + * doc.$init({ time: '12:30' }); + * + * @method overwriteMiddlewareArguments + * @param {...any} args The new arguments to be passed to the next middleware. Pass multiple arguments as a spread, **not** as an array. + * @api public + */ + +Mongoose.prototype.overwriteMiddlewareArguments = Kareem.overwriteArguments; + /** * Takes in an object and deletes any keys from the object whose values * are strictly equal to `undefined`. diff --git a/lib/schema/subdocument.js b/lib/schema/subdocument.js index 885f01f6fe5..7f30daa612c 100644 --- a/lib/schema/subdocument.js +++ b/lib/schema/subdocument.js @@ -180,7 +180,7 @@ SchemaSubdocument.prototype.cast = function(val, doc, init, priorVal, options) { return val; } - if (val != null && (typeof val !== 'object' || Array.isArray(val))) { + if (!init && val != null && (typeof val !== 'object' || Array.isArray(val))) { throw new ObjectExpectedError(this.path, val); } diff --git a/package.json b/package.json index 0725b834122..dfd16adad43 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "type": "commonjs", "license": "MIT", "dependencies": { - "kareem": "git+https://github.com/mongoosejs/kareem.git#vkarpov15/remove-isasync", + "kareem": "git+https://github.com/mongoosejs/kareem.git#vkarpov15/overwrite-arguments", "mongodb": "~6.20.0", "mpath": "0.9.0", "mquery": "5.0.0", diff --git a/test/document.test.js b/test/document.test.js index b2a1902c01c..efa708edb44 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -6534,6 +6534,50 @@ describe('document', function() { }); }); + it('init single nested to num throws ObjectExpectedError (gh-15839) (gh-6710) (gh-6753)', async function() { + const schema = new Schema({ + nested: new Schema({ + num: Number + }) + }); + + const Test = db.model('Test', schema); + + const doc = new Test({}); + doc.init({ nested: 123 }); + await assert.rejects(() => doc.validate(), /nested: Tried to set nested object field `nested` to primitive value `123`/); + + assert.throws(() => doc.init(123), /ObjectExpectedError/); + }); + + it('allows pre init hook to transform data (gh-15839)', async function() { + const timeStringToObject = (time) => { + if (typeof time !== 'string') return time; + const [hours, minutes] = time.split(':'); + return { hours: parseInt(hours), minutes: parseInt(minutes) }; + }; + + const timeSchema = new Schema({ + hours: { type: Number, required: true }, + minutes: { type: Number, required: true } + }); + + timeSchema.pre('init', function(doc) { + if (typeof doc === 'string') { + return mongoose.overwriteMiddlewareArguments(timeStringToObject(doc)); + } + }); + + const userSchema = new Schema({ + time: timeSchema + }); + + const User = db.model('Test', userSchema); + const doc = new User({}); + doc.$init({ time: '12:30' }); + await doc.validate(); + }); + it('set array to false throws ObjectExpectedError (gh-7242)', function() { const Child = new mongoose.Schema({}); const Parent = new mongoose.Schema({ From a431200fc396ff8e3490888754ca905119239fcf Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 30 Oct 2025 10:04:56 -0400 Subject: [PATCH 177/209] merge conflict cleanup --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b5edf67e2f2..28b6506726b 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "license": "MIT", "dependencies": { "kareem": "git+https://github.com/mongoosejs/kareem.git#vkarpov15/remove-isasync", - "mongodb": "6.20.0-dev.20251023.sha.c2b988eb", + "mongodb": "6.20.0-dev.20251028.sha.447dad7e", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", From ebac5dbf65091631cac39fc286c55986e2090015 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 30 Oct 2025 11:52:42 -0400 Subject: [PATCH 178/209] feat: allow overwriting middleware arguments in init() Fix #15389 --- lib/document.js | 10 +++------ lib/model.js | 15 +++++++------ test/document.test.js | 50 +++++++++++++++++++++++++++++++++++++++++++ test/model.test.js | 18 ++++++++++++++++ 4 files changed, 80 insertions(+), 13 deletions(-) diff --git a/lib/document.js b/lib/document.js index bf0e0848741..54960f97a78 100644 --- a/lib/document.js +++ b/lib/document.js @@ -853,11 +853,11 @@ function init(self, obj, doc, opts, prefix) { * @instance */ -Document.prototype.updateOne = function updateOne(doc, options, callback) { +Document.prototype.updateOne = function updateOne(doc, options) { const query = this.constructor.updateOne({ _id: this._doc._id }, doc, options); const self = this; query.pre(function queryPreUpdateOne() { - return self._execDocumentPreHooks('updateOne', [self]); + return self._execDocumentPreHooks('updateOne'); }); query.post(function queryPostUpdateOne() { return self._execDocumentPostHooks('updateOne'); @@ -869,10 +869,6 @@ Document.prototype.updateOne = function updateOne(doc, options, callback) { } } - if (callback != null) { - return query.exec(callback); - } - return query; }; @@ -2944,7 +2940,7 @@ Document.prototype._execDocumentPostHooks = async function _execDocumentPostHook Document.prototype.$__validate = async function $__validate(pathsToValidate, options) { try { - await this._execDocumentPreHooks('validate'); + [options] = await this._execDocumentPreHooks('validate', options); } catch (error) { await this._execDocumentPostHooks('validate', error); return; diff --git a/lib/model.js b/lib/model.js index 86e0f24eb06..b5c98c56461 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1128,9 +1128,9 @@ Model.createCollection = async function createCollection(options) { throw new MongooseError('Model.createCollection() no longer accepts a callback'); } - const shouldSkip = await this.hooks.execPre('createCollection', this, [options]).catch(err => { + [options] = await this.hooks.execPre('createCollection', this, [options]).catch(err => { if (err instanceof Kareem.skipWrappedFunction) { - return true; + return [err]; } throw err; }); @@ -1188,7 +1188,7 @@ Model.createCollection = async function createCollection(options) { } try { - if (!shouldSkip) { + if (!(options instanceof Kareem.skipWrappedFunction)) { await this.db.createCollection(this.$__collection.collectionName, options); } } catch (err) { @@ -2911,7 +2911,7 @@ Model.insertMany = async function insertMany(arr, options) { } try { - await this._middleware.execPre('insertMany', this, [arr]); + [arr] = await this._middleware.execPre('insertMany', this, [arr]); } catch (error) { await this._middleware.execPost('insertMany', this, [arr], { error }); } @@ -3268,7 +3268,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) { [ops, options] = await this.hooks.execPre('bulkWrite', this, [ops, options]).catch(err => { if (err instanceof Kareem.skipWrappedFunction) { - return err; + return [err]; } throw err; }); @@ -3485,7 +3485,10 @@ Model.bulkSave = async function bulkSave(documents, options) { }; async function buildPreSavePromise(document, options) { - return document.schema.s.hooks.execPre('save', document, [options]); + const [newOptions] = await document.schema.s.hooks.execPre('save', document, [options]); + if (newOptions !== options) { + throw new Error('Cannot overwrite options in pre("save") hook on bulkSave()'); + } } async function handleSuccessfulWrite(document) { diff --git a/test/document.test.js b/test/document.test.js index efa708edb44..28e424e17ff 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -14951,6 +14951,56 @@ describe('document', function() { obj = docNoVersion.toObject(); assert.ok(!obj.hasOwnProperty('__v')); }); + + it('allows using overwriteMiddlewareArguments to override pre("init") hook results (gh-15389)', async function () { + const timeStringToObject = (time) => { + if (typeof time !== 'string') return time; + const [hours, minutes] = time.split(':'); + return { hours: parseInt(hours), minutes: parseInt(minutes) }; + }; + + const timeSchema = new Schema({ + hours: { type: Number, required: true }, + minutes: { type: Number, required: true }, + }); + + // Attempt to transform during init + timeSchema.pre('init', function (rawDoc) { + if (typeof rawDoc === 'string') { + return mongoose.overwriteMiddlewareArguments(timeStringToObject(rawDoc)); + } + }); + + const userSchema = new Schema({ + unknownKey: { + type: timeSchema, + required: true + }, + }); + const User = db.model('Test', userSchema); + await User.collection.insertOne({ unknownKey: '12:34' }); + const user = await User.findOne(); + assert.ok(user.unknownKey.hours === 12); + assert.ok(user.unknownKey.minutes === 34); + }); + + it('allows using overwriteMiddlewareArguments to override pre("validate") hook results (gh-15389)', async function () { + const userSchema = new Schema({ + test: { + type: String, + required: true + }, + }); + userSchema.pre('validate', function (options) { + if (options == null) { + return mongoose.overwriteMiddlewareArguments({ pathsToSkip: ['test'] }); + } + }); + const User = db.model('Test', userSchema); + const user = new User(); + await user.validate(); + await assert.rejects(() => user.validate({}), /Path `test` is required/); + }); }); describe('Check if instance function that is supplied in schema option is available', function() { diff --git a/test/model.test.js b/test/model.test.js index 094969aebc8..b7c24869177 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -6807,6 +6807,24 @@ describe('Model', function() { /bulkSave expects an array of documents to be passed/ ); }); + + it('throws an error if pre("save") middleware updates arguments (gh-15389)', async function() { + const userSchema = new Schema({ + name: { type: String } + }); + + userSchema.pre('save', function () { + return mongoose.overwriteMiddlewareArguments({ password: 'taco' }); + }); + + const User = db.model('User', userSchema); + const doc = new User({ name: 'Hafez' }); + await assert.rejects( + () => User.bulkSave([doc]), + /Cannot overwrite options in pre\("save"\) hook on bulkSave\(\)/ + ); + }); + it('throws an error if one element is not a document', function() { const userSchema = new Schema({ name: { type: String } From aca25601d72d8d00a3b2b79be3df4051f0c6b737 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 30 Oct 2025 11:56:34 -0400 Subject: [PATCH 179/209] fix lint --- test/document.test.js | 14 +++++++------- test/model.test.js | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/document.test.js b/test/document.test.js index 28e424e17ff..cea9f73abdd 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -14952,7 +14952,7 @@ describe('document', function() { assert.ok(!obj.hasOwnProperty('__v')); }); - it('allows using overwriteMiddlewareArguments to override pre("init") hook results (gh-15389)', async function () { + it('allows using overwriteMiddlewareArguments to override pre("init") hook results (gh-15389)', async function() { const timeStringToObject = (time) => { if (typeof time !== 'string') return time; const [hours, minutes] = time.split(':'); @@ -14961,11 +14961,11 @@ describe('document', function() { const timeSchema = new Schema({ hours: { type: Number, required: true }, - minutes: { type: Number, required: true }, + minutes: { type: Number, required: true } }); // Attempt to transform during init - timeSchema.pre('init', function (rawDoc) { + timeSchema.pre('init', function(rawDoc) { if (typeof rawDoc === 'string') { return mongoose.overwriteMiddlewareArguments(timeStringToObject(rawDoc)); } @@ -14975,7 +14975,7 @@ describe('document', function() { unknownKey: { type: timeSchema, required: true - }, + } }); const User = db.model('Test', userSchema); await User.collection.insertOne({ unknownKey: '12:34' }); @@ -14984,14 +14984,14 @@ describe('document', function() { assert.ok(user.unknownKey.minutes === 34); }); - it('allows using overwriteMiddlewareArguments to override pre("validate") hook results (gh-15389)', async function () { + it('allows using overwriteMiddlewareArguments to override pre("validate") hook results (gh-15389)', async function() { const userSchema = new Schema({ test: { type: String, required: true - }, + } }); - userSchema.pre('validate', function (options) { + userSchema.pre('validate', function(options) { if (options == null) { return mongoose.overwriteMiddlewareArguments({ pathsToSkip: ['test'] }); } diff --git a/test/model.test.js b/test/model.test.js index b7c24869177..7ae988e5444 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -6813,7 +6813,7 @@ describe('Model', function() { name: { type: String } }); - userSchema.pre('save', function () { + userSchema.pre('save', function() { return mongoose.overwriteMiddlewareArguments({ password: 'taco' }); }); From dc0316be07f5b34c6857da1d01f17d8ec088067c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 30 Oct 2025 16:13:57 -0400 Subject: [PATCH 180/209] fix: omit hydrate option when calling watch() --- lib/model.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/model.js b/lib/model.js index 60d83a2fa06..61307aaa972 100644 --- a/lib/model.js +++ b/lib/model.js @@ -2805,6 +2805,13 @@ Model.insertOne = async function insertOne(doc, options) { Model.watch = function(pipeline, options) { _checkContext(this, 'watch'); + options = options || {}; + const watchOptions = options?.hydrate !== undefined ? + utils.omit(options, ['hydrate']) : + { ...options }; + options.model = this; + + const changeStreamThunk = cb => { pipeline = pipeline || []; prepareDiscriminatorPipeline(pipeline, this.schema, 'fullDocument'); @@ -2813,18 +2820,15 @@ Model.watch = function(pipeline, options) { if (this.closed) { return; } - const driverChangeStream = this.$__collection.watch(pipeline, options); + const driverChangeStream = this.$__collection.watch(pipeline, watchOptions); cb(null, driverChangeStream); }); } else { - const driverChangeStream = this.$__collection.watch(pipeline, options); + const driverChangeStream = this.$__collection.watch(pipeline, watchOptions); cb(null, driverChangeStream); } }; - options = options || {}; - options.model = this; - return new ChangeStream(changeStreamThunk, pipeline, options); }; From 94c0e98f9592465e0a2a44dc182bedd29032e979 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 30 Oct 2025 17:27:39 -0400 Subject: [PATCH 181/209] try fix tests --- lib/cast/int32.js | 2 +- scripts/tsc-diagnostics-check.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cast/int32.js b/lib/cast/int32.js index 34eeae8565f..6934c273232 100644 --- a/lib/cast/int32.js +++ b/lib/cast/int32.js @@ -21,7 +21,7 @@ module.exports = function castInt32(val) { return null; } - const coercedVal = isBsonType(val, 'Long') ? val.toNumber() : Number(val); + const coercedVal = isBsonType(val, 'Long') ? val.toNumber() : +val; const INT32_MAX = 0x7FFFFFFF; const INT32_MIN = -0x80000000; diff --git a/scripts/tsc-diagnostics-check.js b/scripts/tsc-diagnostics-check.js index a498aaa28e3..460376c0984 100644 --- a/scripts/tsc-diagnostics-check.js +++ b/scripts/tsc-diagnostics-check.js @@ -3,7 +3,7 @@ const fs = require('fs'); const stdin = fs.readFileSync(0).toString('utf8'); -const maxInstantiations = isNaN(process.argv[2]) ? 310000 : parseInt(process.argv[2], 10); +const maxInstantiations = isNaN(process.argv[2]) ? 350000 : parseInt(process.argv[2], 10); console.log(stdin); From ee732951d2ae167bc0df4413d47674a98b50ba25 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 31 Oct 2025 10:48:22 -0400 Subject: [PATCH 182/209] work around nyc weirdness --- lib/cast/int32.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/cast/int32.js b/lib/cast/int32.js index 6934c273232..995c1eb05a5 100644 --- a/lib/cast/int32.js +++ b/lib/cast/int32.js @@ -21,16 +21,23 @@ module.exports = function castInt32(val) { return null; } - const coercedVal = isBsonType(val, 'Long') ? val.toNumber() : +val; + if (isBsonType(val, 'Long')) { + val = Number(val); + } else if (typeof val === 'string' || typeof val === 'boolean') { + val = Number(val); + } else if (typeof val !== 'number') { + throw new Error('Invalid value for Int32: ' + val); + } + assert.ok(!isNaN(val)); const INT32_MAX = 0x7FFFFFFF; const INT32_MIN = -0x80000000; - if (coercedVal === (coercedVal | 0) && - coercedVal >= INT32_MIN && - coercedVal <= INT32_MAX + if (val === (val | 0) && + val >= INT32_MIN && + val <= INT32_MAX ) { - return coercedVal; + return val; } assert.ok(false); }; From 7ae614274fc48ebe82762c6e6df536468db57fdc Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 31 Oct 2025 10:51:42 -0400 Subject: [PATCH 183/209] Revert "work around nyc weirdness" This reverts commit ee732951d2ae167bc0df4413d47674a98b50ba25. --- lib/cast/int32.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/cast/int32.js b/lib/cast/int32.js index 995c1eb05a5..6934c273232 100644 --- a/lib/cast/int32.js +++ b/lib/cast/int32.js @@ -21,23 +21,16 @@ module.exports = function castInt32(val) { return null; } - if (isBsonType(val, 'Long')) { - val = Number(val); - } else if (typeof val === 'string' || typeof val === 'boolean') { - val = Number(val); - } else if (typeof val !== 'number') { - throw new Error('Invalid value for Int32: ' + val); - } - assert.ok(!isNaN(val)); + const coercedVal = isBsonType(val, 'Long') ? val.toNumber() : +val; const INT32_MAX = 0x7FFFFFFF; const INT32_MIN = -0x80000000; - if (val === (val | 0) && - val >= INT32_MIN && - val <= INT32_MAX + if (coercedVal === (coercedVal | 0) && + coercedVal >= INT32_MIN && + coercedVal <= INT32_MAX ) { - return val; + return coercedVal; } assert.ok(false); }; From e25556016b9722d84a458056165187dff65bfe89 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 31 Oct 2025 10:54:12 -0400 Subject: [PATCH 184/209] try alternative approach to work around nyc weirdness --- lib/cast/int32.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/cast/int32.js b/lib/cast/int32.js index 6934c273232..fb33e781125 100644 --- a/lib/cast/int32.js +++ b/lib/cast/int32.js @@ -21,7 +21,11 @@ module.exports = function castInt32(val) { return null; } - const coercedVal = isBsonType(val, 'Long') ? val.toNumber() : +val; + const coercedVal = typeof val === 'string' ? + parseInt(val, 10) : + isBsonType(val, 'Long') ? + val.toNumber() : + Number(val); const INT32_MAX = 0x7FFFFFFF; const INT32_MIN = -0x80000000; From 617fc9f75d9aa3de5727a857b6d26c75d75be995 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 31 Oct 2025 11:02:45 -0400 Subject: [PATCH 185/209] better handling for parsing strings with nyc --- lib/cast/int32.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/cast/int32.js b/lib/cast/int32.js index fb33e781125..d6d88674c67 100644 --- a/lib/cast/int32.js +++ b/lib/cast/int32.js @@ -22,11 +22,13 @@ module.exports = function castInt32(val) { } const coercedVal = typeof val === 'string' ? - parseInt(val, 10) : + parseFloat(val) : isBsonType(val, 'Long') ? val.toNumber() : Number(val); + assert.ok(!isNaN(coercedVal)); + const INT32_MAX = 0x7FFFFFFF; const INT32_MIN = -0x80000000; From 8b83a1ff40a3459e76a770ecf9c795814ffb4065 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 31 Oct 2025 11:06:28 -0400 Subject: [PATCH 186/209] Revert "better handling for parsing strings with nyc" This reverts commit 617fc9f75d9aa3de5727a857b6d26c75d75be995. --- lib/cast/int32.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/cast/int32.js b/lib/cast/int32.js index d6d88674c67..fb33e781125 100644 --- a/lib/cast/int32.js +++ b/lib/cast/int32.js @@ -22,13 +22,11 @@ module.exports = function castInt32(val) { } const coercedVal = typeof val === 'string' ? - parseFloat(val) : + parseInt(val, 10) : isBsonType(val, 'Long') ? val.toNumber() : Number(val); - assert.ok(!isNaN(coercedVal)); - const INT32_MAX = 0x7FFFFFFF; const INT32_MIN = -0x80000000; From 0b1593e14215c3165949e44604942bc257befda6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 31 Oct 2025 11:06:40 -0400 Subject: [PATCH 187/209] Revert "try alternative approach to work around nyc weirdness" This reverts commit e25556016b9722d84a458056165187dff65bfe89. --- lib/cast/int32.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/cast/int32.js b/lib/cast/int32.js index fb33e781125..6934c273232 100644 --- a/lib/cast/int32.js +++ b/lib/cast/int32.js @@ -21,11 +21,7 @@ module.exports = function castInt32(val) { return null; } - const coercedVal = typeof val === 'string' ? - parseInt(val, 10) : - isBsonType(val, 'Long') ? - val.toNumber() : - Number(val); + const coercedVal = isBsonType(val, 'Long') ? val.toNumber() : +val; const INT32_MAX = 0x7FFFFFFF; const INT32_MIN = -0x80000000; From 60064ad3e3880142abd499be780265d8233cf9d4 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 31 Oct 2025 11:13:38 -0400 Subject: [PATCH 188/209] try another fix --- lib/cast/int32.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cast/int32.js b/lib/cast/int32.js index 6934c273232..cd8ffae08c0 100644 --- a/lib/cast/int32.js +++ b/lib/cast/int32.js @@ -21,12 +21,12 @@ module.exports = function castInt32(val) { return null; } - const coercedVal = isBsonType(val, 'Long') ? val.toNumber() : +val; + const coercedVal = isBsonType(val, 'Long') ? val.toNumber() : Number(val); const INT32_MAX = 0x7FFFFFFF; const INT32_MIN = -0x80000000; - if (coercedVal === (coercedVal | 0) && + if (Number.isInteger(coercedVal) && coercedVal >= INT32_MIN && coercedVal <= INT32_MAX ) { From 8ef9a5855d57b53ecb541ab1aa4b4b683b927cd8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 31 Oct 2025 11:23:48 -0400 Subject: [PATCH 189/209] relax tests to avoid nyc weirdness --- lib/cast/int32.js | 2 +- test/double.test.js | 4 ++-- test/int32.test.js | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/cast/int32.js b/lib/cast/int32.js index cd8ffae08c0..34eeae8565f 100644 --- a/lib/cast/int32.js +++ b/lib/cast/int32.js @@ -26,7 +26,7 @@ module.exports = function castInt32(val) { const INT32_MAX = 0x7FFFFFFF; const INT32_MIN = -0x80000000; - if (Number.isInteger(coercedVal) && + if (coercedVal === (coercedVal | 0) && coercedVal >= INT32_MIN && coercedVal <= INT32_MAX ) { diff --git a/test/double.test.js b/test/double.test.js index 03ef4402fae..49c91979ea2 100644 --- a/test/double.test.js +++ b/test/double.test.js @@ -281,9 +281,9 @@ describe('Double', function() { assert.ok(err); assert.ok(err.errors['myDouble']); assert.equal(err.errors['myDouble'].name, 'CastError'); - assert.equal( + assert.match( err.errors['myDouble'].message, - 'Cast to Double failed for value "helloworld" (type string) at path "myDouble"' + /^Cast to Double failed for value "helloworld" \(type string\) at path "myDouble"/ ); }); }); diff --git a/test/int32.test.js b/test/int32.test.js index 08735aba810..b53a1fba419 100644 --- a/test/int32.test.js +++ b/test/int32.test.js @@ -337,9 +337,9 @@ describe('Int32', function() { assert.ok(err); assert.ok(err.errors['myInt']); assert.equal(err.errors['myInt'].name, 'CastError'); - assert.equal( + assert.match( err.errors['myInt'].message, - 'Cast to Int32 failed for value "1.2" (type string) at path "myInt"' + /^Cast to Int32 failed for value "1\.2" \(type string\) at path "myInt"/ ); }); }); @@ -355,9 +355,9 @@ describe('Int32', function() { assert.ok(err); assert.ok(err.errors['myInt']); assert.equal(err.errors['myInt'].name, 'CastError'); - assert.equal( + assert.match( err.errors['myInt'].message, - 'Cast to Int32 failed for value "NaN" (type number) at path "myInt"' + /^Cast to Int32 failed for value "NaN" \(type number\) at path "myInt"/ ); }); }); @@ -373,9 +373,9 @@ describe('Int32', function() { assert.ok(err); assert.ok(err.errors['myInt']); assert.equal(err.errors['myInt'].name, 'CastError'); - assert.equal( + assert.match( err.errors['myInt'].message, - 'Cast to Int32 failed for value "2147483648" (type number) at path "myInt"' + /^Cast to Int32 failed for value "2147483648" \(type number\) at path "myInt"/ ); }); }); @@ -391,9 +391,9 @@ describe('Int32', function() { assert.ok(err); assert.ok(err.errors['myInt']); assert.equal(err.errors['myInt'].name, 'CastError'); - assert.equal( + assert.match( err.errors['myInt'].message, - 'Cast to Int32 failed for value "-2147483649" (type number) at path "myInt"' + /^Cast to Int32 failed for value "-2147483649" \(type number\) at path "myInt"/ ); }); }); From 86f2144fc8ff29c5002076fa98d983998e410e2b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 31 Oct 2025 11:28:41 -0400 Subject: [PATCH 190/209] relax more tests --- test/docs/validation.test.js | 6 ++++-- test/int32.test.js | 8 ++++---- test/model.findOneAndUpdate.test.js | 6 ++++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/test/docs/validation.test.js b/test/docs/validation.test.js index 20e654a4f34..2d153d0162c 100644 --- a/test/docs/validation.test.js +++ b/test/docs/validation.test.js @@ -381,8 +381,10 @@ describe('validation docs', function() { err.errors['numWheels'].message; // acquit:ignore:start assert.equal(err.errors['numWheels'].name, 'CastError'); - assert.equal(err.errors['numWheels'].message, - 'Cast to Number failed for value "not a number" (type string) at path "numWheels"'); + assert.match( + err.errors['numWheels'].message, + /^Cast to Number failed for value "not a number" \(type string\) at path "numWheels"/ + ); // acquit:ignore:end }); diff --git a/test/int32.test.js b/test/int32.test.js index b53a1fba419..747711235ac 100644 --- a/test/int32.test.js +++ b/test/int32.test.js @@ -301,9 +301,9 @@ describe('Int32', function() { assert.ok(err); assert.ok(err.errors['myInt']); assert.equal(err.errors['myInt'].name, 'CastError'); - assert.equal( + assert.match( err.errors['myInt'].message, - 'Cast to Int32 failed for value "-42.4" (type number) at path "myInt"' + /^Cast to Int32 failed for value "-42.4" \(type number\) at path "myInt"/ ); }); }); @@ -319,9 +319,9 @@ describe('Int32', function() { assert.ok(err); assert.ok(err.errors['myInt']); assert.equal(err.errors['myInt'].name, 'CastError'); - assert.equal( + assert.match( err.errors['myInt'].message, - 'Cast to Int32 failed for value "helloworld" (type string) at path "myInt"' + /^Cast to Int32 failed for value "helloworld" \(type string\) at path "myInt"/ ); }); }); diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js index 8648b11db49..272f37faaee 100644 --- a/test/model.findOneAndUpdate.test.js +++ b/test/model.findOneAndUpdate.test.js @@ -1354,8 +1354,10 @@ describe('model: findOneAndUpdate:', function() { const update = { $push: { addresses: { street: 'not a num' } } }; const error = await Person.findOneAndUpdate({}, update).then(() => null, err => err); assert.ok(error.message.indexOf('street') !== -1); - assert.equal(error.reason.message, - 'Cast to Number failed for value "not a num" (type string) at path "street"'); + assert.match( + error.reason.message, + /^Cast to Number failed for value "not a num" \(type string\) at path "street"/ + ); }); it('projection option as alias for fields (gh-4315)', async function() { From b0f47518060fb6d2ac4d851987a82622fd42efc2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 4 Nov 2025 12:09:18 -0500 Subject: [PATCH 191/209] clean up merge conflicts --- package.json | 12 +----------- test/schema.test.js | 4 ++-- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 290e1f4ccce..2e0267ed5f8 100644 --- a/package.json +++ b/package.json @@ -50,27 +50,17 @@ "mkdirp": "^3.0.1", "mocha": "11.7.4", "moment": "2.30.1", -<<<<<<< HEAD - "mongodb-memory-server": "10.2.1", - "mongodb-runner": "^5.8.2", - "mongodb-client-encryption": "7.0.0-alpha.1", -======= "mongodb-memory-server": "10.3.0", "mongodb-runner": "^6.0.0", ->>>>>>> master + "mongodb-client-encryption": "7.0.0-alpha.1", "ncp": "^2.0.0", "nyc": "15.1.0", "pug": "3.0.3", "sinon": "21.0.0", "tsd": "0.33.0", "typescript": "5.9.3", -<<<<<<< HEAD "typescript-eslint": "^8.31.1", "uuid": "11.1.0" -======= - "uuid": "11.1.0", - "webpack": "5.102.1" ->>>>>>> master }, "directories": { "lib": "./lib/mongoose" diff --git a/test/schema.test.js b/test/schema.test.js index 08278b4b2e0..0711b419e49 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -3952,7 +3952,7 @@ describe('schema', function() { const firstCall = schema.indexes(); const secondCall = schema.indexes(); - assert.deepStrictEqual(firstCall, [[{ content: 'text' }, { background: true }]]); - assert.deepStrictEqual(secondCall, [[{ content: 'text' }, { background: true }]]); + assert.deepStrictEqual(firstCall, [[{ content: 'text' }, {}]]); + assert.deepStrictEqual(secondCall, [[{ content: 'text' }, {}]]); }); }); From dd280bab1a11577af408fa9ab6983fc8bc488114 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 7 Nov 2025 10:56:36 -0500 Subject: [PATCH 192/209] feat: use mongodb 7 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2e0267ed5f8..d9bed63af86 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "license": "MIT", "dependencies": { "kareem": "git+https://github.com/mongoosejs/kareem.git#vkarpov15/remove-isasync", - "mongodb": "6.20.0-dev.20251028.sha.447dad7e", + "mongodb": "~7.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -52,7 +52,7 @@ "moment": "2.30.1", "mongodb-memory-server": "10.3.0", "mongodb-runner": "^6.0.0", - "mongodb-client-encryption": "7.0.0-alpha.1", + "mongodb-client-encryption": "~7.0", "ncp": "^2.0.0", "nyc": "15.1.0", "pug": "3.0.3", From 6952e512e638628b0f87147134c651aeea96bfd9 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 7 Nov 2025 11:12:14 -0500 Subject: [PATCH 193/209] Update test/document.test.js Co-authored-by: Hafez --- test/document.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/document.test.js b/test/document.test.js index cea9f73abdd..a48aeb194c2 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -14998,7 +14998,7 @@ describe('document', function() { }); const User = db.model('Test', userSchema); const user = new User(); - await user.validate(); + await user.validate(null); await assert.rejects(() => user.validate({}), /Path `test` is required/); }); }); From e9a0509ceea4c48d145d4f42f7f1653f8be5a6b4 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 7 Nov 2025 11:16:37 -0500 Subject: [PATCH 194/209] add note about Document.prototype.updateOne callback removal --- docs/migrating_to_9.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 1ea356aafea..c67a4efbdd9 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -186,6 +186,20 @@ mySchema.pre('qux', async function qux() { }); ``` +## `Document.prototype.updateOne` no longer accepts a callback + +`Document.prototype.updateOne` still supported callbacks in Mongoose 8. In Mongoose 9, the callback parameter was removed. + +```javascript +const doc = await TestModel.findOne().orFail(); + +// Worked in Mongoose 8, no longer supported in Mongoose 9. +doc.updateOne({ name: 'updated' }, null, (err, res) => { + if (err) throw err; + console.log(res); +}); +``` + ## Removed `promiseOrCallback` Mongoose 9 removed the `promiseOrCallback` helper function. From a32ee02db3d1f153e2b63dee2308570f7958a0a1 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 7 Nov 2025 11:26:59 -0500 Subject: [PATCH 195/209] address test fix for code review --- test/document.test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/document.test.js b/test/document.test.js index a48aeb194c2..ff5c72c014e 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -14978,8 +14978,9 @@ describe('document', function() { } }); const User = db.model('Test', userSchema); - await User.collection.insertOne({ unknownKey: '12:34' }); - const user = await User.findOne(); + const _id = new mongoose.Types.ObjectId(); + await User.collection.insertOne({ _id, unknownKey: '12:34' }); + const user = await User.findOne({ _id }).orFail(); assert.ok(user.unknownKey.hours === 12); assert.ok(user.unknownKey.minutes === 34); }); From 8d36deed7b242563bfc7b48ab94cd8550c608866 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 7 Nov 2025 14:28:21 -0500 Subject: [PATCH 196/209] add overwriteMiddlewareArguments to TypeScript types and handle overwriteArguments in pre("deleteOne") and pre("updateOne") document hooks --- lib/document.js | 15 +++++++++++++-- lib/model.js | 5 +++++ lib/plugins/sharding.js | 5 ++++- test/sharding.test.js | 11 +++++++++++ types/index.d.ts | 2 ++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/lib/document.js b/lib/document.js index 54960f97a78..4a65ea9f800 100644 --- a/lib/document.js +++ b/lib/document.js @@ -856,8 +856,19 @@ function init(self, obj, doc, opts, prefix) { Document.prototype.updateOne = function updateOne(doc, options) { const query = this.constructor.updateOne({ _id: this._doc._id }, doc, options); const self = this; - query.pre(function queryPreUpdateOne() { - return self._execDocumentPreHooks('updateOne'); + query.pre(async function queryPreUpdateOne() { + const res = await self._execDocumentPreHooks('updateOne', self); + // `self` is passed to pre hooks as argument for backwards compatibility, but that + // isn't the actual arguments passed to the wrapped function. + if (res?.length !== 1 || res[0] !== self) { + throw new Error('Document updateOne pre hooks cannot overwrite arguments'); + } + // Apply custom where conditions _after_ document deleteOne middleware for + // consistency with save() - sharding plugin needs to set $where + if (self.$where != null) { + this.where(self.$where); + } + return res; }); query.post(function queryPostUpdateOne() { return self._execDocumentPostHooks('updateOne'); diff --git a/lib/model.js b/lib/model.js index 4ce40dc6d7a..f42054ed926 100644 --- a/lib/model.js +++ b/lib/model.js @@ -764,6 +764,11 @@ Model.prototype.deleteOne = function deleteOne(options) { query.pre(async function queryPreDeleteOne() { const res = await self.constructor._middleware.execPre('deleteOne', self, [self]); + // `self` is passed to pre hooks as argument for backwards compatibility, but that + // isn't the actual arguments passed to the wrapped function. + if (res?.length !== 1 || res[0] !== self) { + throw new Error('Document deleteOne pre hooks cannot overwrite arguments'); + } // Apply custom where conditions _after_ document deleteOne middleware for // consistency with save() - sharding plugin needs to set $where if (self.$where != null) { diff --git a/lib/plugins/sharding.js b/lib/plugins/sharding.js index 1d0be9723b1..25237ff5e2c 100644 --- a/lib/plugins/sharding.js +++ b/lib/plugins/sharding.js @@ -15,7 +15,10 @@ module.exports = function shardingPlugin(schema) { schema.pre('save', function shardingPluginPreSave() { applyWhere.call(this); }); - schema.pre('deleteOne', { document: true, query: false }, function shardingPluginPreRemove() { + schema.pre('deleteOne', { document: true, query: false }, function shardingPluginPreDeleteOne() { + applyWhere.call(this); + }); + schema.pre('updateOne', { document: true, query: false }, function shardingPluginPreUpdateOne() { applyWhere.call(this); }); schema.post('save', function shardingPluginPostSave() { diff --git a/test/sharding.test.js b/test/sharding.test.js index 91762aa493d..e77c547e413 100644 --- a/test/sharding.test.js +++ b/test/sharding.test.js @@ -34,4 +34,15 @@ describe('plugins.sharding', function() { res = await TestModel.deleteOne({ name: 'test2' }); assert.strictEqual(res.deletedCount, 1); }); + + it('applies shard key to updateOne (gh-15701)', async function() { + const TestModel = db.model('Test', new mongoose.Schema({ name: String, shardKey: String })); + const doc = await TestModel.create({ name: 'test', shardKey: 'test1' }); + doc.$__.shardval = { shardKey: 'test2' }; + let res = await doc.updateOne({ $set: { name: 'test2' } }); + assert.strictEqual(res.modifiedCount, 0); + doc.$__.shardval = { shardKey: 'test1' }; + res = await doc.updateOne({ $set: { name: 'test2' } }); + assert.strictEqual(res.modifiedCount, 1); + }); }); diff --git a/types/index.d.ts b/types/index.d.ts index 58f2d6e996a..bdb5e6670ef 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1032,5 +1032,7 @@ declare module 'mongoose' { export function skipMiddlewareFunction(val: any): Kareem.SkipWrappedFunction; + export function overwriteMiddlewareArguments(val: any): Kareem.OverwriteArguments; + export default mongoose; } From 44cf6c65cd36a9f73f4926bc239e6dee04f9402c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 7 Nov 2025 14:32:57 -0500 Subject: [PATCH 197/209] Update lib/document.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/document.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/document.js b/lib/document.js index 4a65ea9f800..45da5eb3f13 100644 --- a/lib/document.js +++ b/lib/document.js @@ -863,7 +863,7 @@ Document.prototype.updateOne = function updateOne(doc, options) { if (res?.length !== 1 || res[0] !== self) { throw new Error('Document updateOne pre hooks cannot overwrite arguments'); } - // Apply custom where conditions _after_ document deleteOne middleware for + // Apply custom where conditions _after_ document updateOne middleware for // consistency with save() - sharding plugin needs to set $where if (self.$where != null) { this.where(self.$where); From 08bdf7aed2ec2cd3713534367ac88d2238afc37a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 7 Nov 2025 14:33:06 -0500 Subject: [PATCH 198/209] Update test/document.test.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/document.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/document.test.js b/test/document.test.js index ff5c72c014e..beb8c890244 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -6574,7 +6574,7 @@ describe('document', function() { const User = db.model('Test', userSchema); const doc = new User({}); - doc.$init({ time: '12:30' }); + doc.init({ time: '12:30' }); await doc.validate(); }); From 8229ae910523c8160c5167baf495a32cb599cc33 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 9 Nov 2025 13:27:59 -0500 Subject: [PATCH 199/209] Update package.json Co-authored-by: hasezoey --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7253bf2db08..2b7e51c44f0 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "main": "./index.js", "types": "./types/index.d.ts", "engines": { - "node": ">=18.0.0" + "node": ">=20.19.0" }, "bugs": { "url": "https://github.com/Automattic/mongoose/issues/new" From 8ac5d157e268a8135b422afdc48931a6f447c73d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 17 Nov 2025 14:48:42 -0500 Subject: [PATCH 200/209] clean up merge conflict issues --- lib/schema/array.js | 2 +- lib/types/documentArray/methods/index.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/schema/array.js b/lib/schema/array.js index b8c186513b4..14b60f22ea2 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -84,7 +84,7 @@ function SchemaArray(key, cast, options, schemaOptions, parentSchema) { if (typeof schemaTypeDefinition === 'function') { if (schemaTypeDefinition === SchemaArray) { - this.embeddedSchemaType = new schemaTypeDefinition(path, castOptions, schemaOptions, null, parentSchema); + this.embeddedSchemaType = new schemaTypeDefinition(key, castOptions, schemaOptions, null, parentSchema); } else { this.embeddedSchemaType = new schemaTypeDefinition(key, castOptions, schemaOptions, parentSchema); } diff --git a/lib/types/documentArray/methods/index.js b/lib/types/documentArray/methods/index.js index ed3ffabe793..b17a31d0638 100644 --- a/lib/types/documentArray/methods/index.js +++ b/lib/types/documentArray/methods/index.js @@ -143,7 +143,9 @@ const methods = { if (idSchemaType) { try { castedId = idSchemaType.cast(id); - } catch (_err) {} + } catch { + // ignore error + } } let _id; From 4990dd9435553db195537649db0ddbe1bdbbec35 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 17 Nov 2025 16:00:47 -0500 Subject: [PATCH 201/209] types: fix auto inferred objectid --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index bdb5e6670ef..0ebeac09338 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -651,7 +651,7 @@ declare module 'mongoose' { export type StringSchemaDefinition = typeof String | 'string' | 'String' | typeof Schema.Types.String | Schema.Types.String; export type BooleanSchemaDefinition = typeof Boolean | 'boolean' | 'Boolean' | typeof Schema.Types.Boolean | Schema.Types.Boolean; export type DateSchemaDefinition = DateConstructor | 'date' | 'Date' | typeof Schema.Types.Date | Schema.Types.Date; - export type ObjectIdSchemaDefinition = 'ObjectId' | 'ObjectID' | typeof Schema.Types.ObjectId | Schema.Types.ObjectId | Types.ObjectId; + export type ObjectIdSchemaDefinition = 'ObjectId' | 'ObjectID' | typeof Schema.Types.ObjectId | Schema.Types.ObjectId | Types.ObjectId | typeof Types.ObjectId; export type BufferSchemaDefinition = typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer; export type Decimal128SchemaDefinition = 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 | Schema.Types.Decimal128 | Types.Decimal128; export type BigintSchemaDefinition = 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | Schema.Types.BigInt | typeof BigInt | BigInt; From 612ff6513d5319ad739fc16904e3359cc918192f Mon Sep 17 00:00:00 2001 From: Hafez Date: Tue, 18 Nov 2025 14:53:27 +0100 Subject: [PATCH 202/209] test: add tests for `updatePipeline` global option in model re: #15756 --- test/model.test.js | 166 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/test/model.test.js b/test/model.test.js index 7ae988e5444..109503249b0 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -6741,6 +6741,172 @@ describe('Model', function() { }); }); + describe('`updatePipeline` global option (gh-15756)', function() { + // Arrange + const originalValue = mongoose.get('updatePipeline'); + + afterEach(() => { + mongoose.set('updatePipeline', originalValue); + }); + + describe('allows update pipelines when global `updatePipeline` is `true`', function() { + it('works with updateOne', async function() { + // Arrange + const { User } = createTestContext({ globalUpdatePipeline: true }); + const createdUser = await User.create({ name: 'Hafez', counter: 0 }); + + // Act + await User.updateOne({ _id: createdUser._id }, [{ $set: { counter: 1 } }]); + const user = await User.findById(createdUser._id); + + // Assert + assert.equal(user.counter, 1); + }); + + it('works with updateMany', async function() { + // Arrange + const { User } = createTestContext({ globalUpdatePipeline: true }); + const createdUser = await User.create({ name: 'Hafez', counter: 0 }); + + // Act + await User.updateMany({ _id: createdUser._id }, [{ $set: { counter: 2 } }]); + const user = await User.findById(createdUser._id); + + // Assert + assert.equal(user.counter, 2); + }); + + it('works with findOneAndUpdate', async function() { + // Arrange + const { User } = createTestContext({ globalUpdatePipeline: true }); + const createdUser = await User.create({ name: 'Hafez', counter: 0 }); + + // Act + const user = await User.findOneAndUpdate({ _id: createdUser._id }, [{ $set: { counter: 3, name: 'Hafez3' } }], { new: true }); + + // Assert + assert.equal(user.counter, 3); + assert.equal(user.name, 'Hafez3'); + }); + + it('works with findByIdAndUpdate', async function() { + // Arrange + const { User } = createTestContext({ globalUpdatePipeline: true }); + const createdUser = await User.create({ name: 'Hafez', counter: 0 }); + + // Act + const user = await User.findByIdAndUpdate(createdUser._id, [{ $set: { counter: 4, name: 'Hafez4' } }], { new: true }); + + // Assert + assert.equal(user.counter, 4); + assert.equal(user.name, 'Hafez4'); + }); + }); + + describe('explicit `updatePipeline` option overrides global setting', function() { + it('explicit false overrides global true for updateOne', async function() { + // Arrange + const { User } = createTestContext({ globalUpdatePipeline: true }); + const createdUser = await User.create({ name: 'Hafez', counter: 0 }); + + // Act & Assert + assert.throws( + () => User.updateOne({ _id: createdUser._id }, [{ $set: { counter: 1 } }], { updatePipeline: false }), + /Cannot pass an array to query updates unless the `updatePipeline` option is set/ + ); + }); + + it('explicit false overrides global true for findOneAndUpdate', async function() { + // Arrange + const { User } = createTestContext({ globalUpdatePipeline: true }); + const createdUser = await User.create({ name: 'Hafez', counter: 0 }); + + // Act & Assert + assert.throws( + () => User.findOneAndUpdate({ _id: createdUser._id }, [{ $set: { counter: 1 } }], { updatePipeline: false }), + /Cannot pass an array to query updates unless the `updatePipeline` option is set/ + ); + }); + }); + + describe('throws error when global `updatePipeline` is false and no explicit option', function() { + it('updateOne should throw error', async function() { + // Arrange + const { User } = createTestContext({ globalUpdatePipeline: false }); + const createdUser = await User.create({ name: 'Hafez', counter: 0 }); + + // Act & Assert + assert.throws( + () => User.updateOne({ _id: createdUser._id }, [{ $set: { counter: 1 } }]), + /Cannot pass an array to query updates unless the `updatePipeline` option is set/ + ); + }); + + it('updateMany should throw error', async function() { + // Arrange + const { User } = createTestContext({ globalUpdatePipeline: false }); + const createdUser = await User.create({ name: 'Hafez', counter: 0 }); + + // Act & Assert + assert.throws( + () => User.updateMany({ _id: createdUser._id }, [{ $set: { counter: 1 } }]), + /Cannot pass an array to query updates unless the `updatePipeline` option is set/ + ); + }); + + it('findOneAndUpdate should throw error', async function() { + // Arrange + const { User } = createTestContext({ globalUpdatePipeline: false }); + const createdUser = await User.create({ name: 'Hafez', counter: 0 }); + + // Act & Assert + assert.throws( + () => User.findOneAndUpdate({ _id: createdUser._id }, [{ $set: { counter: 1 } }]), + /Cannot pass an array to query updates unless the `updatePipeline` option is set/ + ); + }); + }); + + describe('explicit `updatePipeline: true` overrides global `updatePipeline: false`', function() { + it('works with updateOne', async function() { + // Arrange + const { User } = createTestContext({ globalUpdatePipeline: false }); + const createdUser = await User.create({ name: 'Hafez', counter: 0 }); + + // Act + await User.updateOne({ _id: createdUser._id }, [{ $set: { counter: 1 } }], { updatePipeline: true }); + const user = await User.findById(createdUser._id); + + // Assert + assert.equal(user.counter, 1); + }); + + it('works with findOneAndUpdate', async function() { + // Arrange + const { User } = createTestContext({ globalUpdatePipeline: false }); + const createdUser = await User.create({ name: 'Hafez', counter: 0 }); + + // Act + const user = await User.findOneAndUpdate({ _id: createdUser._id }, [{ $set: { counter: 2, name: 'Hafez2' } }], { updatePipeline: true, new: true }); + + // Assert + assert.equal(user.counter, 2); + assert.equal(user.name, 'Hafez2'); + }); + }); + + function createTestContext({ globalUpdatePipeline }) { + mongoose.set('updatePipeline', globalUpdatePipeline); + const userSchema = new Schema({ + name: { type: String }, + counter: { type: Number, default: 0 } + }); + + const User = db.model('User', userSchema); + return { User }; + } + }); + describe('buildBulkWriteOperations() (gh-9673)', () => { it('builds write operations', async() => { From 834feeab273f52fb0543cfe04c76c132b8206f8f Mon Sep 17 00:00:00 2001 From: Hafez Date: Tue, 18 Nov 2025 14:55:55 +0100 Subject: [PATCH 203/209] feat: implement global `updatePipeline` option re #15756 --- lib/mongoose.js | 5 +++-- lib/query.js | 29 +++++++++++++++++++---------- lib/validOptions.js | 5 +++-- types/mongooseoptions.d.ts | 9 +++++++++ 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/lib/mongoose.js b/lib/mongoose.js index f6f93aa32cd..864b686dd55 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -230,7 +230,6 @@ Mongoose.prototype.setDriver = function setDriver(driver) { * - `cloneSchemas`: `false` by default. Set to `true` to `clone()` all schemas before compiling into a model. * - `debug`: If `true`, prints the operations mongoose sends to MongoDB to the console. If a writable stream is passed, it will log to that stream, without colorization. If a callback function is passed, it will receive the collection name, the method name, then all arguments passed to the method. For example, if you wanted to replicate the default logging, you could output from the callback `Mongoose: ${collectionName}.${methodName}(${methodArgs.join(', ')})`. * - `id`: If `true`, adds a `id` virtual to all schemas unless overwritten on a per-schema basis. - * - `timestamps.createdAt.immutable`: `true` by default. If `false`, it will change the `createdAt` field to be [`immutable: false`](https://mongoosejs.com/docs/api/schematype.html#SchemaType.prototype.immutable) which means you can update the `createdAt` * - `maxTimeMS`: If set, attaches [maxTimeMS](https://www.mongodb.com/docs/manual/reference/operator/meta/maxTimeMS/) to every query * - `objectIdGetter`: `true` by default. Mongoose adds a getter to MongoDB ObjectId's called `_id` that returns `this` for convenience with populate. Set this to false to remove the getter. * - `overwriteModels`: Set to `true` to default to overwriting models with the same name when calling `mongoose.model()`, as opposed to throwing an `OverwriteModelError`. @@ -238,10 +237,12 @@ Mongoose.prototype.setDriver = function setDriver(driver) { * - `runValidators`: `false` by default. Set to true to enable [update validators](https://mongoosejs.com/docs/validation.html#update-validators) for all validators by default. * - `sanitizeFilter`: `false` by default. Set to true to enable the [sanitization of the query filters](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.sanitizeFilter()) against query selector injection attacks by wrapping any nested objects that have a property whose name starts with `$` in a `$eq`. * - `selectPopulatedPaths`: `true` by default. Set to false to opt out of Mongoose adding all fields that you `populate()` to your `select()`. The schema-level option `selectPopulatedPaths` overwrites this one. - * - `strict`: `true` by default, may be `false`, `true`, or `'throw'`. Sets the default strict mode for schemas. * - `strictQuery`: `false` by default. May be `false`, `true`, or `'throw'`. Sets the default [strictQuery](https://mongoosejs.com/docs/guide.html#strictQuery) mode for schemas. + * - `strict`: `true` by default, may be `false`, `true`, or `'throw'`. Sets the default strict mode for schemas. + * - `timestamps.createdAt.immutable`: `true` by default. If `false`, it will change the `createdAt` field to be [`immutable: false`](https://mongoosejs.com/docs/api/schematype.html#SchemaType.prototype.immutable) which means you can update the `createdAt` * - `toJSON`: `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toJSON()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.toJSON()), for determining how Mongoose documents get serialized by `JSON.stringify()` * - `toObject`: `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toObject()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject()) + * - `updatePipeline`: `false` by default. If `true`, allows passing update pipelines (arrays) to update operations by default without explicitly setting `updatePipeline: true` in each query. * * @param {String|Object} key The name of the option or a object of multiple key-value pairs * @param {String|Function|Boolean} value The value of the option, unused if "key" is a object diff --git a/lib/query.js b/lib/query.js index 567b62eb55f..dfab11dea9a 100644 --- a/lib/query.js +++ b/lib/query.js @@ -3451,15 +3451,16 @@ Query.prototype.findOneAndUpdate = function(filter, update, options) { delete options.fields; } - const returnOriginal = this && - this.model && - this.model.base && - this.model.base.options && - this.model.base.options.returnOriginal; + const returnOriginal = this?.model?.base?.options?.returnOriginal; if (options.new == null && options.returnDocument == null && options.returnOriginal == null && returnOriginal != null) { options.returnOriginal = returnOriginal; } + const updatePipeline = this?.model?.base?.options?.updatePipeline; + if (options.updatePipeline == null && updatePipeline != null) { + options.updatePipeline = updatePipeline; + } + this.setOptions(options); // apply doc @@ -3715,14 +3716,16 @@ Query.prototype.findOneAndReplace = function(filter, replacement, options) { options = options || {}; - const returnOriginal = this && - this.model && - this.model.base && - this.model.base.options && - this.model.base.options.returnOriginal; + const returnOriginal = this?.model?.base?.options?.returnOriginal; if (options.new == null && options.returnDocument == null && options.returnOriginal == null && returnOriginal != null) { options.returnOriginal = returnOriginal; } + + const updatePipeline = this?.model?.base?.options?.updatePipeline; + if (options.updatePipeline == null && updatePipeline != null) { + options.updatePipeline = updatePipeline; + } + this.setOptions(options); return this; @@ -4407,6 +4410,12 @@ function _update(query, op, filter, doc, options, callback) { query.merge(filter); } + const updatePipeline = query?.model?.base?.options?.updatePipeline; + if (updatePipeline != null && (options == null || options.updatePipeline == null)) { + options = options || {}; + options.updatePipeline = updatePipeline; + } + if (utils.isObject(options)) { query.setOptions(options); } diff --git a/lib/validOptions.js b/lib/validOptions.js index 6c09480def1..9c66659d13f 100644 --- a/lib/validOptions.js +++ b/lib/validOptions.js @@ -19,7 +19,6 @@ const VALID_OPTIONS = Object.freeze([ 'debug', 'forceRepopulate', 'id', - 'timestamps.createdAt.immutable', 'maxTimeMS', 'objectIdGetter', 'overwriteModels', @@ -32,10 +31,12 @@ const VALID_OPTIONS = Object.freeze([ 'strict', 'strictPopulate', 'strictQuery', + 'timestamps.createdAt.immutable', 'toJSON', 'toObject', 'transactionAsyncLocalStorage', - 'translateAliases' + 'translateAliases', + 'updatePipeline' ]); module.exports = VALID_OPTIONS; diff --git a/types/mongooseoptions.d.ts b/types/mongooseoptions.d.ts index 9c35ab8222b..31019fd1542 100644 --- a/types/mongooseoptions.d.ts +++ b/types/mongooseoptions.d.ts @@ -215,5 +215,14 @@ declare module 'mongoose' { * to their database property names. Defaults to false. */ translateAliases?: boolean; + + /** + * If `true`, allows passing update pipelines (arrays) to update operations by default + * without explicitly setting `updatePipeline: true` in each query. This is the global + * default for the `updatePipeline` query option. + * + * @default false + */ + updatePipeline?: boolean; } } From d8c1ab7c711f5a3b8ac062a7c19455c96c15a917 Mon Sep 17 00:00:00 2001 From: Hafez Date: Tue, 18 Nov 2025 15:53:05 +0100 Subject: [PATCH 204/209] docs: add docs for global updatePipeline options --- docs/migrating_to_9.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index c67a4efbdd9..757e9e38d53 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -85,6 +85,19 @@ Set `updatePipeline: true` to enable update pipelines. await Model.updateOne({}, [{ $set: { newProp: 'test2' } }], { updatePipeline: true }); ``` +You can also set `updatePipeline` globally to enable update pipelines for all update operations by default. + +```javascript +// Enable update pipelines globally +mongoose.set('updatePipeline', true); + +// Now update pipelines work without needing to specify the option on each query +await Model.updateOne({}, [{ $set: { newProp: 'test2' } }]); + +// You can still override the global setting per query +await Model.updateOne({}, [{ $set: { newProp: 'test2' } }], { updatePipeline: false }); // throws +``` + ## Removed background option for indexes [MongoDB no longer supports the `background` option for indexes as of MongoDB 4.2](https://www.mongodb.com/docs/manual/core/index-creation/#index-operations). Mongoose 9 will no longer set the background option by default and Mongoose 9 no longer supports setting the `background` option on `Schema.prototype.index()`. From 04c7ba5ea1a3327d416a1fdacb8ba581ad8158c4 Mon Sep 17 00:00:00 2001 From: Hafez Date: Tue, 18 Nov 2025 15:54:32 +0100 Subject: [PATCH 205/209] test: add type tests re #15756 --- test/types/base.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/types/base.test.ts b/test/types/base.test.ts index d13c10ea17d..1c1d57d359d 100644 --- a/test/types/base.test.ts +++ b/test/types/base.test.ts @@ -56,6 +56,11 @@ function gh10139() { mongoose.set('timestamps.createdAt.immutable', false); } +function gh15756() { + mongoose.set('updatePipeline', false); + mongoose.set('updatePipeline', true); +} + function gh12100() { mongoose.syncIndexes({ continueOnError: true, sparse: true }); mongoose.syncIndexes({ continueOnError: false, sparse: true }); @@ -64,7 +69,8 @@ function gh12100() { function setAsObject() { mongoose.set({ debug: true, - autoIndex: false + autoIndex: false, + updatePipeline: true }); expectError(mongoose.set({ invalid: true })); From ed70ce1b2c6936d8842b5b063e0970744f31ccc1 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 18 Nov 2025 15:09:08 -0500 Subject: [PATCH 206/209] bump kareem and mquery --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index eebe2a8db4c..70453c928f8 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "type": "commonjs", "license": "MIT", "dependencies": { - "kareem": "git+https://github.com/mongoosejs/kareem.git#vkarpov15/overwrite-arguments", + "kareem": "3.0.0", "mongodb": "~7.0", "mpath": "0.9.0", - "mquery": "5.0.0", + "mquery": "6.0.0", "ms": "2.1.3", "sift": "17.1.3" }, @@ -135,7 +135,7 @@ "noImplicitAny": false, "strictNullChecks": true, "module": "commonjs", - "target": "ES2017" + "target": "ES2022" } } } From e7962c47c25f16fb911d7cb916802a83ab383500 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 18 Nov 2025 15:12:33 -0500 Subject: [PATCH 207/209] test: add coverage for schemas defined with type: Types.ObjectId --- test/types/schema.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index f3c724997d5..75e90f50268 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -2074,3 +2074,15 @@ function autoInferredNestedMaps() { const doc = new TestModel({ nestedMap: new Map([['1', new Map([['2', 'value']])]]) }); expectType>>(doc.nestedMap); } + +function gh15751() { + const schema = new Schema({ + myId: { + type: Types.ObjectId, + required: true + } + }); + const TestModel = model('Test', schema); + const doc = new TestModel(); + expectType(doc.myId); +} From 9c39f4552440e843c6d171870e606d90d1fb6616 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 18 Nov 2025 17:13:28 -0500 Subject: [PATCH 208/209] fix: avoid pulling in updatePipeline for findOneAndReplace() --- lib/query.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/query.js b/lib/query.js index dfab11dea9a..e2d3258492a 100644 --- a/lib/query.js +++ b/lib/query.js @@ -3710,10 +3710,6 @@ Query.prototype.findOneAndReplace = function(filter, replacement, options) { ); } - if (replacement != null) { - this._mergeUpdate(replacement); - } - options = options || {}; const returnOriginal = this?.model?.base?.options?.returnOriginal; @@ -3721,11 +3717,6 @@ Query.prototype.findOneAndReplace = function(filter, replacement, options) { options.returnOriginal = returnOriginal; } - const updatePipeline = this?.model?.base?.options?.updatePipeline; - if (options.updatePipeline == null && updatePipeline != null) { - options.updatePipeline = updatePipeline; - } - this.setOptions(options); return this; From b3a74291c30cdd19e4a1b03880ac0cad637ea4be Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 18 Nov 2025 17:17:58 -0500 Subject: [PATCH 209/209] fix tests --- lib/query.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/query.js b/lib/query.js index e2d3258492a..57bc291340f 100644 --- a/lib/query.js +++ b/lib/query.js @@ -3710,6 +3710,10 @@ Query.prototype.findOneAndReplace = function(filter, replacement, options) { ); } + if (replacement != null) { + this._mergeUpdate(replacement); + } + options = options || {}; const returnOriginal = this?.model?.base?.options?.returnOriginal;