From 92175732bab462528f284313e9853a43f22eb08f Mon Sep 17 00:00:00 2001 From: Brady Davis Date: Wed, 10 Jan 2024 07:19:56 -0600 Subject: [PATCH 001/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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/199] 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"