From cb6d61f219b495f47428f8a45eb22c0d02c1152d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 26 Sep 2023 15:59:28 -0400 Subject: [PATCH 01/27] wip: inferRawDocType re: #13772 --- test/types/schema.test.ts | 11 +++ types/index.d.ts | 1 + types/inferrawdoctype.d.ts | 169 +++++++++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 types/inferrawdoctype.d.ts diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index fc0204e2a71..0b5a823a1a5 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -4,6 +4,7 @@ import { HydratedDocument, IndexDefinition, IndexOptions, + InferRawDocType, InferSchemaType, InsertManyOptions, ObtainDocumentType, @@ -1230,3 +1231,13 @@ async function gh13797() { expectType(this); return ''; } } }); } + +function gh13772() { + const schemaDefinition = { + name: String, + docArr: [{ name: String }] + }; + const schema = new Schema(schemaDefinition); + type RawDocType = InferRawDocType; + expectAssignable<{ name?: string, docArr?: Array<{ name?: string }> }>({} as RawDocType); +} diff --git a/types/index.d.ts b/types/index.d.ts index 1bd4b0a7085..ce84f9100ad 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -20,6 +20,7 @@ /// /// /// +/// /// /// /// diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts new file mode 100644 index 00000000000..cb4e2ea3c5a --- /dev/null +++ b/types/inferrawdoctype.d.ts @@ -0,0 +1,169 @@ +declare module 'mongoose' { + export type InferRawDocType< + DocDefinition, + TSchemaOptions extends Record = DefaultSchemaOptions + > = { + [ + K in keyof (RequiredPaths & + OptionalPaths) + ]: ObtainRawDocumentPathType; + }; + +/** + * @summary Obtains schema Path type. + * @description Obtains Path type by separating path type from other options and calling {@link ResolvePathType} + * @param {PathValueType} PathValueType Document definition path type. + * @param {TypeKey} TypeKey A generic refers to document definition. + */ +type ObtainRawDocumentPathType< + PathValueType, + TypeKey extends string = DefaultTypeKey +> = ResolveRawPathType< + PathValueType extends PathWithTypePropertyBaseType ? PathValueType[TypeKey] : PathValueType, + PathValueType extends PathWithTypePropertyBaseType ? Omit : {}, + TypeKey +>; + +type IsPathDefaultUndefined = PathType extends { default: undefined } ? + true : + PathType extends { default: (...args: any[]) => undefined } ? + true : + false; + +/** + * @summary Checks if a document path is required or optional. + * @param {P} P Document path. + * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". + */ +type IsPathRequired = + P extends { required: true | [true, string | undefined] | { isRequired: true } } | ArrayConstructor | any[] + ? true + : P extends { required: boolean } + ? P extends { required: false } + ? false + : true + : P extends (Record) + ? IsPathDefaultUndefined

extends true + ? false + : true + : P extends (Record) + ? P extends { default: any } + ? IfEquals + : false + : false; + +/** + * @summary Path base type defined by using TypeKey + * @description It helps to check if a path is defined by TypeKey OR not. + * @param {TypeKey} TypeKey A literal string refers to path type property key. + */ +type PathWithTypePropertyBaseType = { [k in TypeKey]: any }; + +/** + * @summary A Utility to obtain schema's required path keys. + * @param {T} T A generic refers to document definition. + * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". + * @returns required paths keys of document definition. + */ +type RequiredPathKeys = { + [K in keyof T]: IsPathRequired extends true ? IfEquals : never; +}[keyof T]; + +/** + * @summary A Utility to obtain schema's required paths. + * @param {T} T A generic refers to document definition. + * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". + * @returns a record contains required paths with the corresponding type. + */ +type RequiredPaths = { + [K in RequiredPathKeys]: T[K]; +}; + +/** + * @summary A Utility to obtain schema's optional path keys. + * @param {T} T A generic refers to document definition. + * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". + * @returns optional paths keys of document definition. + */ +type OptionalPathKeys = { + [K in keyof T]: IsPathRequired extends true ? never : K; +}[keyof T]; + +/** + * @param {T} T A generic refers to string path enums. + * @returns Path enum values type as literal strings or string. + */ +type PathEnumOrString['enum']> = T extends ReadonlyArray ? E : T extends { values: any } ? PathEnumOrString : string; + +/** + * @summary A Utility to obtain schema's optional paths. + * @param {T} T A generic refers to document definition. + * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". + * @returns a record contains optional paths with the corresponding type. + */ +type OptionalPaths = { + [K in OptionalPathKeys]?: T[K]; +}; + +/** + * @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 ResolveRawPathType = {}, TypeKey extends string = DefaultSchemaOptions['typeKey']> = + PathValueType extends Schema ? InferRawDocType : + PathValueType extends (infer Item)[] ? + IfEquals> : + 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 ObtainDocumentType to correctly infer its type. + InferRawDocType[]: + ObtainRawDocumentPathType[] + >: + PathValueType extends ReadonlyArray ? + IfEquals> : + Item extends Record ? + Item[TypeKey] extends Function | String ? + ObtainRawDocumentPathType[] : + InferRawDocType[]: + 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 ? Date : + IfEquals extends true ? Date : + 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 : + PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt ? bigint : + PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? Buffer : + IfEquals extends true ? Buffer : + PathValueType extends MapConstructor ? 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 ? ObtainDocumentType : + unknown; +} \ No newline at end of file From 7f9d520b913d5cfe68ec4b98319b940f688f5f6c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 28 Sep 2023 16:08:30 -0400 Subject: [PATCH 02/27] refactor(inferrawdoctype): import helpers from inferschematype --- types/inferrawdoctype.d.ts | 234 +++++++++++++------------------------ 1 file changed, 80 insertions(+), 154 deletions(-) diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index cb4e2ea3c5a..49b05d2ce9f 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -1,169 +1,95 @@ +import { + RequiredPaths, + OptionalPaths, + PathWithTypePropertyBaseType, + PathEnumOrString +} from './inferschematype'; + declare module 'mongoose' { export type InferRawDocType< DocDefinition, TSchemaOptions extends Record = DefaultSchemaOptions > = { [ - K in keyof (RequiredPaths & - OptionalPaths) + K in keyof (RequiredPaths & + OptionalPaths) ]: ObtainRawDocumentPathType; }; -/** - * @summary Obtains schema Path type. - * @description Obtains Path type by separating path type from other options and calling {@link ResolvePathType} - * @param {PathValueType} PathValueType Document definition path type. - * @param {TypeKey} TypeKey A generic refers to document definition. - */ -type ObtainRawDocumentPathType< - PathValueType, - TypeKey extends string = DefaultTypeKey -> = ResolveRawPathType< - PathValueType extends PathWithTypePropertyBaseType ? PathValueType[TypeKey] : PathValueType, - PathValueType extends PathWithTypePropertyBaseType ? Omit : {}, - TypeKey ->; - -type IsPathDefaultUndefined = PathType extends { default: undefined } ? - true : - PathType extends { default: (...args: any[]) => undefined } ? - true : - false; - -/** - * @summary Checks if a document path is required or optional. - * @param {P} P Document path. - * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". - */ -type IsPathRequired = - P extends { required: true | [true, string | undefined] | { isRequired: true } } | ArrayConstructor | any[] - ? true - : P extends { required: boolean } - ? P extends { required: false } - ? false - : true - : P extends (Record) - ? IsPathDefaultUndefined

extends true - ? false - : true - : P extends (Record) - ? P extends { default: any } - ? IfEquals - : false - : false; - -/** - * @summary Path base type defined by using TypeKey - * @description It helps to check if a path is defined by TypeKey OR not. - * @param {TypeKey} TypeKey A literal string refers to path type property key. - */ -type PathWithTypePropertyBaseType = { [k in TypeKey]: any }; - -/** - * @summary A Utility to obtain schema's required path keys. - * @param {T} T A generic refers to document definition. - * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". - * @returns required paths keys of document definition. - */ -type RequiredPathKeys = { - [K in keyof T]: IsPathRequired extends true ? IfEquals : never; -}[keyof T]; - -/** - * @summary A Utility to obtain schema's required paths. - * @param {T} T A generic refers to document definition. - * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". - * @returns a record contains required paths with the corresponding type. - */ -type RequiredPaths = { - [K in RequiredPathKeys]: T[K]; -}; + /** + * @summary Obtains schema Path type. + * @description Obtains Path type by separating path type from other options and calling {@link ResolvePathType} + * @param {PathValueType} PathValueType Document definition path type. + * @param {TypeKey} TypeKey A generic refers to document definition. + */ + type ObtainRawDocumentPathType< + PathValueType, + TypeKey extends string = DefaultTypeKey + > = ResolveRawPathType< + PathValueType extends PathWithTypePropertyBaseType ? PathValueType[TypeKey] : PathValueType, + PathValueType extends PathWithTypePropertyBaseType ? Omit : {}, + TypeKey + >; -/** - * @summary A Utility to obtain schema's optional path keys. - * @param {T} T A generic refers to document definition. - * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". - * @returns optional paths keys of document definition. - */ -type OptionalPathKeys = { - [K in keyof T]: IsPathRequired extends true ? never : K; -}[keyof T]; - -/** - * @param {T} T A generic refers to string path enums. - * @returns Path enum values type as literal strings or string. - */ -type PathEnumOrString['enum']> = T extends ReadonlyArray ? E : T extends { values: any } ? PathEnumOrString : string; - -/** - * @summary A Utility to obtain schema's optional paths. - * @param {T} T A generic refers to document definition. - * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". - * @returns a record contains optional paths with the corresponding type. - */ -type OptionalPaths = { - [K in OptionalPathKeys]?: T[K]; -}; - -/** - * @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 ResolveRawPathType = {}, TypeKey extends string = DefaultSchemaOptions['typeKey']> = - PathValueType extends Schema ? InferRawDocType : - PathValueType extends (infer Item)[] ? - IfEquals> : - 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 ObtainDocumentType to correctly infer its type. - InferRawDocType[]: - ObtainRawDocumentPathType[] - >: - PathValueType extends ReadonlyArray ? + /** + * @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 ResolveRawPathType = {}, TypeKey extends string = DefaultSchemaOptions['typeKey']> = + PathValueType extends Schema ? InferRawDocType : + PathValueType extends (infer Item)[] ? IfEquals> : - Item extends Record ? + Item extends Record? Item[TypeKey] extends Function | String ? - ObtainRawDocumentPathType[] : + // 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 ObtainDocumentType to correctly infer its type. InferRawDocType[]: - ObtainRawDocumentPathType[] + 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 ? Date : - IfEquals extends true ? Date : - 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 : - PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt ? bigint : - PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? Buffer : - IfEquals extends true ? Buffer : - PathValueType extends MapConstructor ? 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 ? ObtainDocumentType : - unknown; -} \ No newline at end of file + PathValueType extends ReadonlyArray ? + IfEquals> : + Item extends Record ? + Item[TypeKey] extends Function | String ? + ObtainRawDocumentPathType[] : + InferRawDocType[]: + 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 ? Date : + IfEquals extends true ? Date : + 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 : + PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt ? bigint : + PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? Buffer : + IfEquals extends true ? Buffer : + PathValueType extends MapConstructor ? 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 ? ObtainDocumentType : + unknown; +} From ccfefd83664f0580a903a2f5800e98d56002a045 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:25:23 -0400 Subject: [PATCH 03/27] feat: `listDatabases()` function --- lib/connection.js | 8 ++++++++ test/connection.test.js | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/connection.js b/lib/connection.js index 05ff52461b0..51b85ceaec2 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -647,6 +647,14 @@ Connection.prototype.listCollections = async function listCollections() { return await cursor.toArray(); }; +Connection.prototype.listDatabases = async function listDatabases() { + if (this.client) { + return this.client.db().admin().listDatabases(); + } else { + throw new MongooseError('No client could be found on the given connection'); + } +}; + /** * Helper for `dropDatabase()`. Deletes the given database, including all * collections, documents, and indexes. diff --git a/test/connection.test.js b/test/connection.test.js index 3d1170838cf..6a151e1321c 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -1580,6 +1580,20 @@ describe('connections:', function() { }); assert.ok(session); }); + it('should demonstrate the listDatabases() function (gh-9048)', async function() { + const client = await mongodb.MongoClient.connect(start.uri); + const db = mongoose.createConnection().setClient(client); + db.useDb(start.databases[1]); + const { databases } = await db.listDatabases(); + assert.ok(databases); + }); + it('should throw an error when no client is present (gh-9048)', async function() { + const db = mongoose.createConnection(); + const db2 = db.useDb(start.databases[1]); + assert.rejects(async() => { + await db.listDatabases(); + }, { message: 'No client could be found on the given connection' }); + }); describe('createCollections()', function() { it('should create collections for all models on the connection with the createCollections() function (gh-13300)', async function() { const m = new mongoose.Mongoose(); From f1667e0ffaafffb4f124f7896cc5eacef05e1aa8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 8 Apr 2024 14:07:24 -0400 Subject: [PATCH 04/27] fix: make listDatabases() consistent with other connection methods and add typescript support + docs --- lib/connection.js | 24 ++++++++++++++++++++---- test/connection.test.js | 23 ++++++++++------------- test/types/connection.test.ts | 4 ++++ types/connection.d.ts | 6 ++++++ 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 51b85ceaec2..48ea6bb2450 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -647,12 +647,28 @@ Connection.prototype.listCollections = async function listCollections() { return await cursor.toArray(); }; +/** + * Helper for MongoDB Node driver's `listDatabases()`. + * Returns an object with a `databases` property that contains an + * array of database objects. + * + * #### Example: + * const { databases } = await mongoose.connection.listDatabases(); + * databases; // [{ name: 'mongoose_test', sizeOnDisk: 0, empty: false }] + * + * @method listCollections + * @return {Promise<{ databases: Array<{ name: string }> }>} + * @api public + */ + Connection.prototype.listDatabases = async function listDatabases() { - if (this.client) { - return this.client.db().admin().listDatabases(); - } else { - throw new MongooseError('No client could be found on the given connection'); + if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) { + await new Promise(resolve => { + this._queue.push({ fn: resolve }); + }); } + + return await this.db.admin().listDatabases(); }; /** diff --git a/test/connection.test.js b/test/connection.test.js index 6a151e1321c..82df2ee4c3a 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -1580,19 +1580,16 @@ describe('connections:', function() { }); assert.ok(session); }); - it('should demonstrate the listDatabases() function (gh-9048)', async function() { - const client = await mongodb.MongoClient.connect(start.uri); - const db = mongoose.createConnection().setClient(client); - db.useDb(start.databases[1]); - const { databases } = await db.listDatabases(); - assert.ok(databases); - }); - it('should throw an error when no client is present (gh-9048)', async function() { - const db = mongoose.createConnection(); - const db2 = db.useDb(start.databases[1]); - assert.rejects(async() => { - await db.listDatabases(); - }, { message: 'No client could be found on the given connection' }); + it('listDatabases() should return a list of database objects with a name property (gh-9048)', async function() { + const connection = await mongoose.createConnection(start.uri).asPromise(); + // If this test is running in isolation, then the `start.uri` db might not + // exist yet, so create this collection (and the associated db) just in case + await connection.createCollection('tests').catch(() => {}); + + const { databases } = await connection.listDatabases(); + assert.ok(connection.name); + console.log(databases); + assert.ok(databases.map(database => database.name).includes(connection.name)); }); describe('createCollections()', function() { it('should create collections for all models on the connection with the createCollections() function (gh-13300)', async function() { diff --git a/test/types/connection.test.ts b/test/types/connection.test.ts index 93c3fc6c0d8..c5979663a20 100644 --- a/test/types/connection.test.ts +++ b/test/types/connection.test.ts @@ -78,6 +78,10 @@ expectType>( conn.listCollections().then(collections => collections.map(coll => coll.name)) ); +expectType>( + conn.listDatabases().then(dbs => dbs.databases.map(db => db.name)) +); + export function autoTypedModelConnection() { const AutoTypedSchema = autoTypedSchema(); const AutoTypedModel = connection.model('AutoTypeModelConnection', AutoTypedSchema); diff --git a/types/connection.d.ts b/types/connection.d.ts index b2812d01cf6..2f47bdc84e5 100644 --- a/types/connection.d.ts +++ b/types/connection.d.ts @@ -132,6 +132,12 @@ declare module 'mongoose' { */ listCollections(): Promise[]>; + /** + * Helper for MongoDB Node driver's `listDatabases()`. + * Returns an array of database names. + */ + listDatabases(): Promise; + /** * A [POJO](https://masteringjs.io/tutorials/fundamentals/pojo) containing * a map from model names to models. Contains all models that have been From 2d4b8c2995251b8ece99da72a0fe1ba4481c8d18 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 9 Apr 2024 15:16:37 -0400 Subject: [PATCH 05/27] refactor(connection): move listDatabases implementation to node-mongodb-native driver Re: #14506 Re: #9048 --- lib/connection.js | 48 ++++++++----------- lib/drivers/node-mongodb-native/connection.js | 13 +++++ test/connection.test.js | 1 - 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 48ea6bb2450..44c5cef0e63 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -398,11 +398,7 @@ Connection.prototype.createCollection = async function createCollection(collecti throw new MongooseError('Connection.prototype.createCollection() no longer accepts a callback'); } - if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) { - await new Promise(resolve => { - this._queue.push({ fn: resolve }); - }); - } + await this._waitForConnect(); return this.db.createCollection(collection, options); }; @@ -494,11 +490,7 @@ Connection.prototype.startSession = async function startSession(options) { throw new MongooseError('Connection.prototype.startSession() no longer accepts a callback'); } - if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) { - await new Promise(resolve => { - this._queue.push({ fn: resolve }); - }); - } + await this._waitForConnect(); const session = this.client.startSession(options); return session; @@ -618,13 +610,24 @@ Connection.prototype.dropCollection = async function dropCollection(collection) throw new MongooseError('Connection.prototype.dropCollection() no longer accepts a callback'); } + await this._waitForConnect(); + + return this.db.dropCollection(collection); +}; + +/** + * Waits for connection to be established, so the connection has a `client` + * + * @return Promise + * @api private + */ + +Connection.prototype._waitForConnect = async function _waitForConnect() { if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) { await new Promise(resolve => { this._queue.push({ fn: resolve }); }); } - - return this.db.dropCollection(collection); }; /** @@ -637,11 +640,7 @@ Connection.prototype.dropCollection = async function dropCollection(collection) */ Connection.prototype.listCollections = async function listCollections() { - if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) { - await new Promise(resolve => { - this._queue.push({ fn: resolve }); - }); - } + await this._waitForConnect(); const cursor = this.db.listCollections(); return await cursor.toArray(); @@ -662,13 +661,8 @@ Connection.prototype.listCollections = async function listCollections() { */ Connection.prototype.listDatabases = async function listDatabases() { - if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) { - await new Promise(resolve => { - this._queue.push({ fn: resolve }); - }); - } - - return await this.db.admin().listDatabases(); + // Implemented in `lib/drivers/node-mongodb-native/connection.js` + throw new MongooseError('listDatabases() not implemented by driver'); }; /** @@ -691,11 +685,7 @@ Connection.prototype.dropDatabase = async function dropDatabase() { throw new MongooseError('Connection.prototype.dropDatabase() no longer accepts a callback'); } - if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) { - await new Promise(resolve => { - this._queue.push({ fn: resolve }); - }); - } + await this._waitForConnect(); // If `dropDatabase()` is called, this model's collection will not be // init-ed. It is sufficiently common to call `dropDatabase()` after diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js index c0f3261595f..3c64ff2216f 100644 --- a/lib/drivers/node-mongodb-native/connection.js +++ b/lib/drivers/node-mongodb-native/connection.js @@ -197,6 +197,19 @@ NativeConnection.prototype.doClose = async function doClose(force) { return this; }; +/** + * Implementation of `listDatabases()` for MongoDB driver + * + * @return Promise + * @api public + */ + +NativeConnection.prototype.listDatabases = async function listDatabases() { + await this._waitForConnect(); + + return await this.db.admin().listDatabases(); +}; + /*! * ignore */ diff --git a/test/connection.test.js b/test/connection.test.js index 82df2ee4c3a..77ec8ac64d1 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -1588,7 +1588,6 @@ describe('connections:', function() { const { databases } = await connection.listDatabases(); assert.ok(connection.name); - console.log(databases); assert.ok(databases.map(database => database.name).includes(connection.name)); }); describe('createCollections()', function() { From d5d1ad8f77cf71f819288b188bd5c0b6b6731f1d Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:08:52 -0400 Subject: [PATCH 06/27] Update model.js --- lib/model.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/model.js b/lib/model.js index 1e2729320d7..04b10ba700e 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1655,6 +1655,15 @@ Model.dropSearchIndex = async function dropSearchIndex(name) { return await this.$__collection.dropSearchIndex(name); }; + +Model.listSearchIndexes = async function listSearchIndexes() { + _checkContext(this, 'listSearchIndexes'); + + const cursor = await this.$__collection.listSearchIndexes(); + + return await cursor.toArray(); +} + /** * Does a dry-run of `Model.syncIndexes()`, returning the indexes that `syncIndexes()` would drop and create if you were to run `syncIndexes()`. * From 660562903422f48815f8edcde6c07168d3f01a61 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:20:45 -0400 Subject: [PATCH 07/27] fix: lint --- lib/model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model.js b/lib/model.js index 04b10ba700e..ba3f7e36492 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1662,7 +1662,7 @@ Model.listSearchIndexes = async function listSearchIndexes() { const cursor = await this.$__collection.listSearchIndexes(); return await cursor.toArray(); -} +}; /** * Does a dry-run of `Model.syncIndexes()`, returning the indexes that `syncIndexes()` would drop and create if you were to run `syncIndexes()`. From ec518edbcb396b5558162602120d81e9e140c23d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 16 Apr 2024 15:33:18 -0400 Subject: [PATCH 08/27] add TypeScript types, jsdoc, and options support for Model.listSearchIndexes() --- lib/model.js | 20 ++++++++++++++++++-- types/models.d.ts | 6 ++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/model.js b/lib/model.js index ba3f7e36492..477a14c76b9 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1655,11 +1655,27 @@ Model.dropSearchIndex = async function dropSearchIndex(name) { return await this.$__collection.dropSearchIndex(name); }; +/** + * List all [Atlas search indexes](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) on this model's collection. + * This function only works when connected to MongoDB Atlas. + * + * #### Example: + * + * const schema = new Schema({ name: { type: String, unique: true } }); + * const Customer = mongoose.model('Customer', schema); + * + * await Customer.createSearchIndex({ name: 'test', definition: { mappings: { dynamic: true } } }); + * const res = await Customer.listSearchIndexes(); // Includes `[{ name: 'test' }]` + * + * @param {Object} [options] + * @return {Promise} + * @api public + */ -Model.listSearchIndexes = async function listSearchIndexes() { +Model.listSearchIndexes = async function listSearchIndexes(options) { _checkContext(this, 'listSearchIndexes'); - const cursor = await this.$__collection.listSearchIndexes(); + const cursor = await this.$__collection.listSearchIndexes(options); return await cursor.toArray(); }; diff --git a/types/models.d.ts b/types/models.d.ts index a56f1ee92e4..aa207f6b45b 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -544,6 +544,12 @@ declare module 'mongoose' { Array>> >; + /** + * List all [Atlas search indexes](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) on this model's collection. + * This function only works when connected to MongoDB Atlas. + */ + listSearchIndexes(options?: mongodb.ListSearchIndexesOptions): Promise>; + /** The name of the model */ modelName: string; From 03b7031045aa46e1e974b1bee1ce4747aabbb6ca Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 26 Apr 2024 17:25:17 -0400 Subject: [PATCH 09/27] types(document): make document _id type default to unknown instead of any Fix #14520 --- types/document.d.ts | 2 +- types/types.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/types/document.d.ts b/types/document.d.ts index 2bb82e3c677..c0723e883bf 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -16,7 +16,7 @@ declare module 'mongoose' { * * TQueryHelpers - Object with any helpers that should be mixed into the Query type * * DocType - the type of the actual Document created */ - class Document { + class Document { constructor(doc?: any); /** This documents _id. */ diff --git a/types/types.d.ts b/types/types.d.ts index f63b1934907..08f90c6184c 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -83,7 +83,7 @@ declare module 'mongoose' { class ObjectId extends mongodb.ObjectId { } - class Subdocument extends Document { + class Subdocument extends Document { $isSingleNested: true; /** Returns the top level document of this sub-document. */ From 23869fb10cd3e81c1bb2b51878a206ea30bb935e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 27 Apr 2024 11:20:42 -0400 Subject: [PATCH 10/27] fix(model): make `recompileSchema()` overwrite existing document array discriminators Fix #14520 --- lib/schema/documentArray.js | 2 +- test/model.test.js | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/schema/documentArray.js b/lib/schema/documentArray.js index 2af276500ff..d35c5dfbf10 100644 --- a/lib/schema/documentArray.js +++ b/lib/schema/documentArray.js @@ -195,7 +195,7 @@ SchemaDocumentArray.prototype.discriminator = function(name, schema, options) { schema = schema.clone(); } - schema = discriminator(this.casterConstructor, name, schema, tiedValue); + schema = discriminator(this.casterConstructor, name, schema, tiedValue, null, null, options?.overwriteExisting); const EmbeddedDocument = _createConstructor(schema, null, this.casterConstructor); EmbeddedDocument.baseCasterConstructor = this.casterConstructor; diff --git a/test/model.test.js b/test/model.test.js index 840b098a86a..9d35f207000 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -7463,6 +7463,23 @@ describe('Model', function() { assert.equal(instance.item.whoAmI(), 'I am Test2'); }); + it('overwrites existing discriminators when calling recompileSchema (gh-14527) (gh-14444)', async function() { + const shopItemSchema = new mongoose.Schema({}, { discriminatorKey: 'type' }); + const shopSchema = new mongoose.Schema({ + items: { type: [shopItemSchema], required: true } + }); + + const shopItemSubType = new mongoose.Schema({ prop: Number }); + shopItemSchema.discriminator(2, shopItemSubType); + const shopModel = db.model('shop', shopSchema); + + shopModel.recompileSchema(); + const doc = new shopModel({ + items: [{ type: 2, prop: 42 }] + }); + assert.equal(doc.items[0].prop, 42); + }); + it('inserts versionKey even if schema has `toObject.versionKey` set to false (gh-14344)', async function() { const schema = new mongoose.Schema( { name: String }, From cecbc44ae2dcb4c9cb538d2438e78310779c48c8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 5 May 2024 15:17:12 -0400 Subject: [PATCH 11/27] fix(error): remove model property from CastError to avoid printing all model properties to console Fix #14529 --- lib/error/cast.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/error/cast.js b/lib/error/cast.js index f7df49b8c7e..115927117f7 100644 --- a/lib/error/cast.js +++ b/lib/error/cast.js @@ -75,7 +75,6 @@ class CastError extends MongooseError { * ignore */ setModel(model) { - this.model = model; this.message = formatMessage(model, this.kind, this.value, this.path, this.messageFormat, this.valueType); } From 6ca92be66ab93128a42d5380a61634539ea2d77a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 5 May 2024 15:26:54 -0400 Subject: [PATCH 12/27] types: remove model from CastError --- types/error.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/types/error.d.ts b/types/error.d.ts index 226fad31931..3fec7c41399 100644 --- a/types/error.d.ts +++ b/types/error.d.ts @@ -28,7 +28,6 @@ declare module 'mongoose' { value: any; path: string; reason?: NativeError | null; - model?: any; constructor(type: string, value: any, path: string, reason?: NativeError, schemaType?: SchemaType); } From a928cee0f105a530b7bb3c30c97a0be669ce232b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 7 May 2024 12:22:45 -0400 Subject: [PATCH 13/27] fix(mongoose): handle initially null driver when instantiating Mongoose for rollup Fix #12335 --- lib/index.js | 6 +++++- lib/mongoose.js | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/index.js b/lib/index.js index 6247d6a4d5c..67534e9e793 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,10 +4,14 @@ * Module dependencies. */ -require('./driver').set(require('./drivers/node-mongodb-native')); +const mongodbDriver = require('./drivers/node-mongodb-native'); + +require('./driver').set(mongodbDriver); const mongoose = require('./mongoose'); +mongoose.setDriver(mongodbDriver); + mongoose.Mongoose.prototype.mongo = require('mongodb'); module.exports = mongoose; diff --git a/lib/mongoose.js b/lib/mongoose.js index 915720b59f7..49392bfd70f 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -68,8 +68,8 @@ function Mongoose(options) { autoCreate: true, autoSearchIndex: false }, options); - const createInitialConnection = utils.getOption('createInitialConnection', this.options); - if (createInitialConnection == null || createInitialConnection) { + const createInitialConnection = utils.getOption('createInitialConnection', this.options) ?? true; + if (createInitialConnection && this.__driver != null) { const conn = this.createConnection(); // default connection conn.models = this.models; } @@ -169,6 +169,9 @@ Mongoose.prototype.setDriver = function setDriver(driver) { const oldDefaultConnection = _mongoose.connections[0]; _mongoose.connections = [new Connection(_mongoose)]; _mongoose.connections[0].models = _mongoose.models; + if (oldDefaultConnection == null) { + return _mongoose; + } // Update all models that pointed to the old default connection to // the new default connection, including collections From 19e744a4699ba8ffbdbc1c6162fc6d02eb0d217c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 7 May 2024 12:22:58 -0400 Subject: [PATCH 14/27] fix(error): clean up some circular imports in errors Re: #12335 --- lib/error/browserMissingSchema.js | 2 +- lib/error/divergentArray.js | 2 +- lib/error/eachAsyncMultiError.js | 2 +- lib/error/invalidSchemaOption.js | 2 +- lib/error/missingSchema.js | 2 +- lib/error/notFound.js | 2 +- lib/error/objectExpected.js | 2 +- lib/error/objectParameter.js | 2 +- lib/error/overwriteModel.js | 2 +- lib/error/parallelSave.js | 2 +- lib/error/strict.js | 2 +- lib/error/strictPopulate.js | 2 +- lib/error/validator.js | 2 +- lib/error/version.js | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/error/browserMissingSchema.js b/lib/error/browserMissingSchema.js index 3f271499d4d..608cfd983e4 100644 --- a/lib/error/browserMissingSchema.js +++ b/lib/error/browserMissingSchema.js @@ -4,7 +4,7 @@ 'use strict'; -const MongooseError = require('./'); +const MongooseError = require('./mongooseError'); class MissingSchemaError extends MongooseError { diff --git a/lib/error/divergentArray.js b/lib/error/divergentArray.js index 6bb527d0205..f266dbde449 100644 --- a/lib/error/divergentArray.js +++ b/lib/error/divergentArray.js @@ -5,7 +5,7 @@ 'use strict'; -const MongooseError = require('./'); +const MongooseError = require('./mongooseError'); class DivergentArrayError extends MongooseError { /** diff --git a/lib/error/eachAsyncMultiError.js b/lib/error/eachAsyncMultiError.js index 9c04020312b..b14156a09a3 100644 --- a/lib/error/eachAsyncMultiError.js +++ b/lib/error/eachAsyncMultiError.js @@ -4,7 +4,7 @@ 'use strict'; -const MongooseError = require('./'); +const MongooseError = require('./mongooseError'); /** diff --git a/lib/error/invalidSchemaOption.js b/lib/error/invalidSchemaOption.js index 2ab1aa9497e..089dc6a03ef 100644 --- a/lib/error/invalidSchemaOption.js +++ b/lib/error/invalidSchemaOption.js @@ -5,7 +5,7 @@ 'use strict'; -const MongooseError = require('./'); +const MongooseError = require('./mongooseError'); class InvalidSchemaOptionError extends MongooseError { /** diff --git a/lib/error/missingSchema.js b/lib/error/missingSchema.js index 50c81054a90..2b3bf242526 100644 --- a/lib/error/missingSchema.js +++ b/lib/error/missingSchema.js @@ -5,7 +5,7 @@ 'use strict'; -const MongooseError = require('./'); +const MongooseError = require('./mongooseError'); class MissingSchemaError extends MongooseError { /** diff --git a/lib/error/notFound.js b/lib/error/notFound.js index e1064bb89d9..19a22f3a101 100644 --- a/lib/error/notFound.js +++ b/lib/error/notFound.js @@ -4,7 +4,7 @@ * Module dependencies. */ -const MongooseError = require('./'); +const MongooseError = require('./mongooseError'); const util = require('util'); class DocumentNotFoundError extends MongooseError { diff --git a/lib/error/objectExpected.js b/lib/error/objectExpected.js index 6506f60656a..9f7a8116618 100644 --- a/lib/error/objectExpected.js +++ b/lib/error/objectExpected.js @@ -4,7 +4,7 @@ 'use strict'; -const MongooseError = require('./'); +const MongooseError = require('./mongooseError'); class ObjectExpectedError extends MongooseError { diff --git a/lib/error/objectParameter.js b/lib/error/objectParameter.js index 295582e2484..b3f5b80849d 100644 --- a/lib/error/objectParameter.js +++ b/lib/error/objectParameter.js @@ -4,7 +4,7 @@ 'use strict'; -const MongooseError = require('./'); +const MongooseError = require('./mongooseError'); class ObjectParameterError extends MongooseError { /** diff --git a/lib/error/overwriteModel.js b/lib/error/overwriteModel.js index 1ff180b0498..8904e4e74b3 100644 --- a/lib/error/overwriteModel.js +++ b/lib/error/overwriteModel.js @@ -5,7 +5,7 @@ 'use strict'; -const MongooseError = require('./'); +const MongooseError = require('./mongooseError'); class OverwriteModelError extends MongooseError { diff --git a/lib/error/parallelSave.js b/lib/error/parallelSave.js index e0628576de7..57ac458238d 100644 --- a/lib/error/parallelSave.js +++ b/lib/error/parallelSave.js @@ -4,7 +4,7 @@ * Module dependencies. */ -const MongooseError = require('./'); +const MongooseError = require('./mongooseError'); class ParallelSaveError extends MongooseError { /** diff --git a/lib/error/strict.js b/lib/error/strict.js index 393ca6e1fc7..6cf4cf91141 100644 --- a/lib/error/strict.js +++ b/lib/error/strict.js @@ -4,7 +4,7 @@ 'use strict'; -const MongooseError = require('./'); +const MongooseError = require('./mongooseError'); class StrictModeError extends MongooseError { diff --git a/lib/error/strictPopulate.js b/lib/error/strictPopulate.js index f7addfa5287..288799897bc 100644 --- a/lib/error/strictPopulate.js +++ b/lib/error/strictPopulate.js @@ -4,7 +4,7 @@ 'use strict'; -const MongooseError = require('./'); +const MongooseError = require('./mongooseError'); class StrictPopulateError extends MongooseError { /** diff --git a/lib/error/validator.js b/lib/error/validator.js index 4ca7316d7bf..f7ee2ef4761 100644 --- a/lib/error/validator.js +++ b/lib/error/validator.js @@ -4,7 +4,7 @@ 'use strict'; -const MongooseError = require('./'); +const MongooseError = require('./mongooseError'); class ValidatorError extends MongooseError { diff --git a/lib/error/version.js b/lib/error/version.js index b357fb16ca3..85f2921a517 100644 --- a/lib/error/version.js +++ b/lib/error/version.js @@ -4,7 +4,7 @@ * Module dependencies. */ -const MongooseError = require('./'); +const MongooseError = require('./mongooseError'); class VersionError extends MongooseError { /** From df1c7208b5fa0a2d95f57c96fabb1a521ea1b74d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 8 May 2024 15:03:16 -0400 Subject: [PATCH 15/27] feat(schema): add schema-level readConcern option to apply default readConcern for all queries Fix #14511 --- docs/guide.md | 19 +++++++++++++++++++ lib/model.js | 3 +++ lib/query.js | 2 ++ lib/schema.js | 1 + test/schema.test.js | 21 +++++++++++++++++++++ types/schemaoptions.d.ts | 4 ++++ 6 files changed, 50 insertions(+) diff --git a/docs/guide.md b/docs/guide.md index 2ed451b08c3..9c2766c311b 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -559,6 +559,7 @@ Valid options: * [methods](#methods) * [query](#query-helpers) * [autoSearchIndex](#autoSearchIndex) +* [readConcern](#readConcern)

option: autoIndex

@@ -1473,6 +1474,24 @@ schema.searchIndex({ const Test = mongoose.model('Test', schema); ``` +

+ + option: readConcern + +

+ +[Read concerns](https://www.mongodb.com/docs/manual/reference/read-concern/) are similar to [`writeConcern`](#writeConcern), but for read operations like `find()` and `findOne()`. +To set a default `readConcern`, pass the `readConcern` option to the schema constructor as follows. + +```javascript +const eventSchema = new mongoose.Schema( + { name: String }, + { + readConcern: { level: 'available' } // <-- set default readConcern for all queries + } +); +``` +

With ES6 Classes

Schemas have a [`loadClass()` method](api/schema.html#schema_Schema-loadClass) diff --git a/lib/model.js b/lib/model.js index eaadf894b91..3e28d4f18c6 100644 --- a/lib/model.js +++ b/lib/model.js @@ -27,6 +27,7 @@ const applyEmbeddedDiscriminators = require('./helpers/discriminator/applyEmbedd const applyHooks = require('./helpers/model/applyHooks'); const applyMethods = require('./helpers/model/applyMethods'); const applyProjection = require('./helpers/projection/applyProjection'); +const applyReadConcern = require('./helpers/schema/applyReadConcern'); const applySchemaCollation = require('./helpers/indexes/applySchemaCollation'); const applyStaticHooks = require('./helpers/model/applyStaticHooks'); const applyStatics = require('./helpers/model/applyStatics'); @@ -417,6 +418,8 @@ Model.prototype.$__handleSave = function(options, callback) { where[key] = val; } } + + applyReadConcern(this.$__schema, optionsWithCustomValues); this.constructor.collection.findOne(where, optionsWithCustomValues) .then(documentExists => { const matchedCount = !documentExists ? 0 : 1; diff --git a/lib/query.js b/lib/query.js index 22956fb818f..dbf022ebf54 100644 --- a/lib/query.js +++ b/lib/query.js @@ -13,6 +13,7 @@ const QueryCursor = require('./cursor/queryCursor'); const ValidationError = require('./error/validation'); const { applyGlobalMaxTimeMS, applyGlobalDiskUse } = require('./helpers/query/applyGlobalOption'); const handleReadPreferenceAliases = require('./helpers/query/handleReadPreferenceAliases'); +const applyReadConcern = require('./helpers/schema/applyReadConcern'); const applyWriteConcern = require('./helpers/schema/applyWriteConcern'); const cast = require('./cast'); const castArrayFilters = require('./helpers/update/castArrayFilters'); @@ -1944,6 +1945,7 @@ Query.prototype._optionsForExec = function(model) { if (!model) { return options; } + applyReadConcern(model.schema, options); // Apply schema-level `writeConcern` option applyWriteConcern(model.schema, options); diff --git a/lib/schema.js b/lib/schema.js index 04c631eb799..97c64a38e1c 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -66,6 +66,7 @@ const numberRE = /^\d+$/; * - [_id](https://mongoosejs.com/docs/guide.html#_id): bool - defaults to true * - [minimize](https://mongoosejs.com/docs/guide.html#minimize): bool - controls [document#toObject](https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject()) behavior when called manually - defaults to true * - [read](https://mongoosejs.com/docs/guide.html#read): string + * - [readConcern](https://mongoosejs.com/docs/guide.html#readConcern): object - defaults to null, use to set a default [read concern](https://www.mongodb.com/docs/manual/reference/read-concern/) for all queries. * - [writeConcern](https://mongoosejs.com/docs/guide.html#writeConcern): object - defaults to null, use to override [the MongoDB server's default write concern settings](https://www.mongodb.com/docs/manual/reference/write-concern/) * - [shardKey](https://mongoosejs.com/docs/guide.html#shardKey): object - defaults to `null` * - [strict](https://mongoosejs.com/docs/guide.html#strict): bool - defaults to true diff --git a/test/schema.test.js b/test/schema.test.js index 0092a44a4ee..8cd58ba7b9f 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -3237,4 +3237,25 @@ describe('schema', function() { assert.equal(doc.element, '#hero'); assert.ok(doc instanceof ClickedModel); }); + + it('supports schema-level readConcern (gh-14511)', async function() { + const eventSchema = new mongoose.Schema({ + name: String + }, { readConcern: { level: 'available' } }); + const Event = db.model('Test', eventSchema); + + let q = Event.find(); + let options = q._optionsForExec(); + assert.deepStrictEqual(options.readConcern, { level: 'available' }); + + q = Event.find().setOptions({ readConcern: { level: 'local' } }); + options = q._optionsForExec(); + assert.deepStrictEqual(options.readConcern, { level: 'local' }); + + q = Event.find().setOptions({ readConcern: null }); + options = q._optionsForExec(); + assert.deepStrictEqual(options.readConcern, null); + + await q; + }); }); diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index 31795187cf0..4df87a806ea 100644 --- a/types/schemaoptions.d.ts +++ b/types/schemaoptions.d.ts @@ -120,6 +120,10 @@ declare module 'mongoose' { * to all queries derived from a model. */ read?: string; + /** + * Set a default readConcern for all queries at the schema level + */ + readConcern?: { level: 'local' | 'available' | 'majority' | 'snapshot' | 'linearizable' } /** Allows setting write concern at the schema level. */ writeConcern?: WriteConcern; /** defaults to true. */ From 0596f558bf444eab48ca0fb232ac43794a69e9c2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 8 May 2024 15:05:30 -0400 Subject: [PATCH 16/27] fix: add missing file --- lib/helpers/schema/applyReadConcern.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 lib/helpers/schema/applyReadConcern.js diff --git a/lib/helpers/schema/applyReadConcern.js b/lib/helpers/schema/applyReadConcern.js new file mode 100644 index 00000000000..80d4da6eb20 --- /dev/null +++ b/lib/helpers/schema/applyReadConcern.js @@ -0,0 +1,22 @@ +'use strict'; + +const get = require('../get'); + +module.exports = function applyReadConcern(schema, options) { + if (options.readConcern !== undefined) { + return; + } + + // Don't apply default read concern to operations in transactions, + // because you shouldn't set read concern on individual operations + // within a transaction. + // See: https://www.mongodb.com/docs/manual/reference/read-concern/ + if (options && options.session && options.session.transaction) { + return; + } + + const level = get(schema, 'options.readConcern.level', null); + if (level != null) { + options.readConcern = { level }; + } +}; From 9e75b3789f006910fc4cd89d5b315c43ead0d758 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 9 May 2024 13:17:02 -0400 Subject: [PATCH 17/27] feat(mongoose): export omitUndefined() helper Fix #14569 --- lib/helpers/omitUndefined.js | 20 ++++++++++++++++++++ lib/helpers/query/cast$expr.js | 10 ++-------- lib/mongoose.js | 22 ++++++++++++++++++++++ test/types/base.test.ts | 2 ++ types/index.d.ts | 2 ++ 5 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 lib/helpers/omitUndefined.js diff --git a/lib/helpers/omitUndefined.js b/lib/helpers/omitUndefined.js new file mode 100644 index 00000000000..5c9eb88564a --- /dev/null +++ b/lib/helpers/omitUndefined.js @@ -0,0 +1,20 @@ +'use strict'; + +module.exports = function omitUndefined(val) { + if (val == null || typeof val !== 'object') { + return val; + } + if (Array.isArray(val)) { + for (let i = val.length - 1; i >= 0; --i) { + if (val[i] === undefined) { + val.splice(i, 1); + } + } + } + for (const key of Object.keys(val)) { + if (val[key] === void 0) { + delete val[key]; + } + } + return val; +}; diff --git a/lib/helpers/query/cast$expr.js b/lib/helpers/query/cast$expr.js index a13190b1c41..9889d47ada1 100644 --- a/lib/helpers/query/cast$expr.js +++ b/lib/helpers/query/cast$expr.js @@ -3,6 +3,7 @@ const CastError = require('../../error/cast'); const StrictModeError = require('../../error/strict'); const castNumber = require('../../cast/number'); +const omitUndefined = require('../omitUndefined'); const booleanComparison = new Set(['$and', '$or']); const comparisonOperator = new Set(['$cmp', '$eq', '$lt', '$lte', '$gt', '$gte']); @@ -125,18 +126,11 @@ function _castExpression(val, schema, strictQuery) { val.$round = $round.map(v => castNumberOperator(v, schema, strictQuery)); } - _omitUndefined(val); + omitUndefined(val); return val; } -function _omitUndefined(val) { - const keys = Object.keys(val); - for (let i = 0, len = keys.length; i < len; ++i) { - (val[keys[i]] === void 0) && delete val[keys[i]]; - } -} - // { $op: } function castNumberOperator(val) { if (!isLiteral(val)) { diff --git a/lib/mongoose.js b/lib/mongoose.js index 49392bfd70f..89cb4348c2b 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -1281,6 +1281,28 @@ Mongoose.prototype.skipMiddlewareFunction = Kareem.skipWrappedFunction; Mongoose.prototype.overwriteMiddlewareResult = Kareem.overwriteResult; +/** + * Takes in an object and deletes any keys from the object whose values + * are strictly equal to `undefined`. + * This function is useful for query filters because Mongoose treats + * `TestModel.find({ name: undefined })` as `TestModel.find({ name: null })`. + * + * #### Example: + * + * const filter = { name: 'John', age: undefined, status: 'active' }; + * mongoose.omitUndefined(filter); // { name: 'John', status: 'active' } + * filter; // { name: 'John', status: 'active' } + * + * await UserModel.findOne(mongoose.omitUndefined(filter)); + * + * @method omitUndefined + * @param {Object} [val] the object to remove undefined keys from + * @returns {Object} the object passed in + * @api public + */ + +Mongoose.prototype.omitUndefined = require('./helpers/omitUndefined'); + /** * The exports object is an instance of Mongoose. * diff --git a/test/types/base.test.ts b/test/types/base.test.ts index fba2acf37b0..9d25cfe30f3 100644 --- a/test/types/base.test.ts +++ b/test/types/base.test.ts @@ -69,3 +69,5 @@ function setAsObject() { expectError(mongoose.set({ invalid: true })); } + +const x: { name: string } = mongoose.omitUndefined({ name: 'foo' }); diff --git a/types/index.d.ts b/types/index.d.ts index cd5695b35f2..6f851d23dfa 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -68,6 +68,8 @@ declare module 'mongoose' { /** Gets mongoose options */ export function get(key: K): MongooseOptions[K]; + export function omitUndefined>(val: T): T; + /* ! ignore */ export type CompileModelOptions = { overwriteModels?: boolean, From 819fde3fa01f278d2c315939a80ce1e999958ecc Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 10 May 2024 17:54:33 -0400 Subject: [PATCH 18/27] feat: add `transactionAsyncLocalStorage` option to opt in to automatically setting `session` on all transactions Fix #13889 --- docs/transactions.md | 31 ++++++++++++++++++-- lib/aggregate.js | 5 ++++ lib/connection.js | 13 ++++++-- lib/model.js | 7 +++++ lib/mongoose.js | 16 ++++++++-- lib/query.js | 5 ++++ lib/validOptions.js | 1 + test/docs/transactions.test.js | 44 ++++++++++++++++++++++++++++ test/model.findByIdAndUpdate.test.js | 3 -- types/mongooseoptions.d.ts | 7 +++++ 10 files changed, 121 insertions(+), 11 deletions(-) diff --git a/docs/transactions.md b/docs/transactions.md index 901282dac44..dfe9610529b 100644 --- a/docs/transactions.md +++ b/docs/transactions.md @@ -1,8 +1,6 @@ # Transactions in Mongoose -[Transactions](https://www.mongodb.com/transactions) are new in MongoDB -4.0 and Mongoose 5.2.0. Transactions let you execute multiple operations -in isolation and potentially undo all the operations if one of them fails. +[Transactions](https://www.mongodb.com/transactions) let you execute multiple operations in isolation and potentially undo all the operations if one of them fails. This guide will get you started using transactions with Mongoose.

Getting Started with Transactions

@@ -86,6 +84,33 @@ Below is an example of executing an aggregation within a transaction. [require:transactions.*aggregate] ``` +

Using AsyncLocalStorage

+ +One major pain point with transactions in Mongoose is that you need to remember to set the `session` option on every operation. +If you don't, your operation will execute outside of the transaction. +Mongoose 8.4 is able to set the `session` operation on all operations within a `Connection.prototype.transaction()` executor function using Node's [AsyncLocalStorage API](https://nodejs.org/api/async_context.html#class-asynclocalstorage). +Set the `transactionAsyncLocalStorage` option using `mongoose.set('transactionAsyncLocalStorage', true)` to enable this feature. + +```javascript +mongoose.set('transactionAsyncLocalStorage', true); + +const Test = mongoose.model('Test', mongoose.Schema({ name: String })); + +const doc = new Test({ name: 'test' }); + +// Save a new doc in a transaction that aborts +await connection.transaction(async() => { + await doc.save(); // Notice no session here + throw new Error('Oops'); +}); + +// false, `save()` was rolled back +await Test.exists({ _id: doc._id }); +``` + +With `transactionAsyncLocalStorage`, you no longer need to pass sessions to every operation. +Mongoose will add the session by default under the hood. +

Advanced Usage

Advanced users who want more fine-grained control over when they commit or abort transactions diff --git a/lib/aggregate.js b/lib/aggregate.js index 827f1642a60..0bdf65b995a 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -1022,6 +1022,11 @@ Aggregate.prototype.exec = async function exec() { applyGlobalMaxTimeMS(this.options, model.db.options, model.base.options); applyGlobalDiskUse(this.options, model.db.options, model.base.options); + const asyncLocalStorage = this.model?.db?.base.transactionAsyncLocalStorage?.getStore(); + if (!this.options.hasOwnProperty('session') && asyncLocalStorage?.session != null) { + this.options.session = asyncLocalStorage.session; + } + if (this.options && this.options.cursor) { return new AggregationCursor(this); } diff --git a/lib/connection.js b/lib/connection.js index 05ff52461b0..b3e224702a6 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -539,7 +539,7 @@ Connection.prototype.startSession = async function startSession(options) { Connection.prototype.transaction = function transaction(fn, options) { return this.startSession().then(session => { session[sessionNewDocuments] = new Map(); - return session.withTransaction(() => _wrapUserTransaction(fn, session), options). + return session.withTransaction(() => _wrapUserTransaction(fn, session, this.base), options). then(res => { delete session[sessionNewDocuments]; return res; @@ -558,9 +558,16 @@ Connection.prototype.transaction = function transaction(fn, options) { * Reset document state in between transaction retries re: gh-13698 */ -async function _wrapUserTransaction(fn, session) { +async function _wrapUserTransaction(fn, session, mongoose) { try { - const res = await fn(session); + const res = mongoose.transactionAsyncLocalStorage == null + ? await fn(session) + : await new Promise(resolve => { + mongoose.transactionAsyncLocalStorage.run( + { session }, + () => resolve(fn(session)) + ); + }); return res; } catch (err) { _resetSessionDocuments(session); diff --git a/lib/model.js b/lib/model.js index 5e0a105c479..d2a5db1f495 100644 --- a/lib/model.js +++ b/lib/model.js @@ -296,8 +296,11 @@ Model.prototype.$__handleSave = function(options, callback) { } const session = this.$session(); + const asyncLocalStorage = this.db.base.transactionAsyncLocalStorage?.getStore(); if (!saveOptions.hasOwnProperty('session') && session != null) { saveOptions.session = session; + } else if (asyncLocalStorage?.session != null) { + saveOptions.session = asyncLocalStorage.session; } if (this.$isNew) { // send entire doc @@ -3533,6 +3536,10 @@ Model.bulkWrite = async function bulkWrite(ops, options) { } const validations = ops.map(op => castBulkWrite(this, op, options)); + const asyncLocalStorage = this.db.base.transactionAsyncLocalStorage?.getStore(); + if (!options.hasOwnProperty('session') && asyncLocalStorage.session != null) { + options = { ...options, session: asyncLocalStorage.session }; + } let res = null; if (ordered) { diff --git a/lib/mongoose.js b/lib/mongoose.js index 915720b59f7..687f5162685 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -38,6 +38,8 @@ require('./helpers/printJestWarning'); const objectIdHexRegexp = /^[0-9A-Fa-f]{24}$/; +const { AsyncLocalStorage } = require('node:async_hooks'); + /** * Mongoose constructor. * @@ -101,6 +103,10 @@ function Mongoose(options) { } this.Schema.prototype.base = this; + if (options?.transactionAsyncLocalStorage) { + this.transactionAsyncLocalStorage = new AsyncLocalStorage(); + } + Object.defineProperty(this, 'plugins', { configurable: false, enumerable: true, @@ -267,7 +273,7 @@ Mongoose.prototype.set = function(key, value) { if (optionKey === 'objectIdGetter') { if (optionValue) { - Object.defineProperty(mongoose.Types.ObjectId.prototype, '_id', { + Object.defineProperty(_mongoose.Types.ObjectId.prototype, '_id', { enumerable: false, configurable: true, get: function() { @@ -275,7 +281,13 @@ Mongoose.prototype.set = function(key, value) { } }); } else { - delete mongoose.Types.ObjectId.prototype._id; + delete _mongoose.Types.ObjectId.prototype._id; + } + } else if (optionKey === 'transactionAsyncLocalStorage') { + if (optionValue && !_mongoose.transactionAsyncLocalStorage) { + _mongoose.transactionAsyncLocalStorage = new AsyncLocalStorage(); + } else if (!optionValue && _mongoose.transactionAsyncLocalStorage) { + delete _mongoose.transactionAsyncLocalStorage; } } } diff --git a/lib/query.js b/lib/query.js index 22956fb818f..e5ec16618be 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1947,6 +1947,11 @@ Query.prototype._optionsForExec = function(model) { // Apply schema-level `writeConcern` option applyWriteConcern(model.schema, options); + const asyncLocalStorage = this.model.db.base.transactionAsyncLocalStorage?.getStore(); + if (!this.options.hasOwnProperty('session') && asyncLocalStorage?.session != null) { + options.session = asyncLocalStorage.session; + } + const readPreference = model && model.schema && model.schema.options && diff --git a/lib/validOptions.js b/lib/validOptions.js index c9968237595..2654a7521ed 100644 --- a/lib/validOptions.js +++ b/lib/validOptions.js @@ -32,6 +32,7 @@ const VALID_OPTIONS = Object.freeze([ 'strictQuery', 'toJSON', 'toObject', + 'transactionAsyncLocalStorage', 'translateAliases' ]); diff --git a/test/docs/transactions.test.js b/test/docs/transactions.test.js index 8b883e3388c..6302daee0e4 100644 --- a/test/docs/transactions.test.js +++ b/test/docs/transactions.test.js @@ -351,6 +351,50 @@ describe('transactions', function() { await session.endSession(); }); + describe('transactionAsyncLocalStorage option', function() { + let m; + before(async function() { + m = new mongoose.Mongoose(); + m.set('transactionAsyncLocalStorage', true); + + await m.connect(start.uri); + }); + + after(async function() { + await m.disconnect(); + }); + + it('transaction() sets `session` by default if transactionAsyncLocalStorage option is set', async function() { + const Test = m.model('Test', m.Schema({ name: String })); + + await Test.createCollection(); + await Test.deleteMany({}); + + const doc = new Test({ name: 'test' }); + await assert.rejects( + () => m.connection.transaction(async() => { + await doc.save(); + + await Test.updateOne({ name: 'foo' }, { name: 'foo' }, { upsert: true }); + + let docs = await Test.aggregate([{ $match: { _id: doc._id } }]); + assert.equal(docs.length, 1); + + const docs = await Test.find({ _id: doc._id }); + assert.equal(docs.length, 1); + + throw new Error('Oops!'); + }), + /Oops!/ + ); + let exists = await Test.exists({ _id: doc._id }); + assert.ok(!exists); + + exists = await Test.exists({ name: 'foo' }); + assert.ok(!exists); + }); + }); + it('transaction() resets $isNew on error', async function() { db.deleteModel(/Test/); const Test = db.model('Test', Schema({ name: String })); diff --git a/test/model.findByIdAndUpdate.test.js b/test/model.findByIdAndUpdate.test.js index cbd953f5606..9db1b39d228 100644 --- a/test/model.findByIdAndUpdate.test.js +++ b/test/model.findByIdAndUpdate.test.js @@ -53,9 +53,6 @@ describe('model: findByIdAndUpdate:', function() { 'shape.side': 4, 'shape.color': 'white' }, { new: true }); - console.log('doc'); - console.log(doc); - console.log('doc'); assert.equal(doc.shape.kind, 'gh8378_Square'); assert.equal(doc.shape.name, 'after'); diff --git a/types/mongooseoptions.d.ts b/types/mongooseoptions.d.ts index 7fec10b208f..9c35ab8222b 100644 --- a/types/mongooseoptions.d.ts +++ b/types/mongooseoptions.d.ts @@ -203,6 +203,13 @@ declare module 'mongoose' { */ toObject?: ToObjectOptions; + /** + * Set to true to make Mongoose use Node.js' built-in AsyncLocalStorage (Node >= 16.0.0) + * to set `session` option on all operations within a `connection.transaction(fn)` call + * by default. Defaults to false. + */ + transactionAsyncLocalStorage?: boolean; + /** * If `true`, convert any aliases in filter, projection, update, and distinct * to their database property names. Defaults to false. From 2df8f37b67af817f4c1425d24172f473c5cd8028 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 11 May 2024 11:19:38 -0400 Subject: [PATCH 19/27] fix lint and add extra test showing session propagates down to async functions --- test/docs/transactions.test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/docs/transactions.test.js b/test/docs/transactions.test.js index 6302daee0e4..29be35d0b56 100644 --- a/test/docs/transactions.test.js +++ b/test/docs/transactions.test.js @@ -380,7 +380,12 @@ describe('transactions', function() { let docs = await Test.aggregate([{ $match: { _id: doc._id } }]); assert.equal(docs.length, 1); - const docs = await Test.find({ _id: doc._id }); + docs = await Test.find({ _id: doc._id }); + assert.equal(docs.length, 1); + + docs = await Promise.all([async() => { + return await Test.findOne({ _id: doc._id }); + }]).then(res => res[0]); assert.equal(docs.length, 1); throw new Error('Oops!'); From f498cc558b959b954566fbda04645ae0b0ce5edc Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 11 May 2024 11:29:33 -0400 Subject: [PATCH 20/27] fix tests --- lib/model.js | 4 ++-- lib/query.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/model.js b/lib/model.js index d2a5db1f495..5cd6179adcc 100644 --- a/lib/model.js +++ b/lib/model.js @@ -296,7 +296,7 @@ Model.prototype.$__handleSave = function(options, callback) { } const session = this.$session(); - const asyncLocalStorage = this.db.base.transactionAsyncLocalStorage?.getStore(); + const asyncLocalStorage = this[modelDbSymbol].base.transactionAsyncLocalStorage?.getStore(); if (!saveOptions.hasOwnProperty('session') && session != null) { saveOptions.session = session; } else if (asyncLocalStorage?.session != null) { @@ -3537,7 +3537,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) { const validations = ops.map(op => castBulkWrite(this, op, options)); const asyncLocalStorage = this.db.base.transactionAsyncLocalStorage?.getStore(); - if (!options.hasOwnProperty('session') && asyncLocalStorage.session != null) { + if ((!options || !options.hasOwnProperty('session')) && asyncLocalStorage?.session != null) { options = { ...options, session: asyncLocalStorage.session }; } diff --git a/lib/query.js b/lib/query.js index e5ec16618be..6ebd3d1eeb1 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1947,7 +1947,7 @@ Query.prototype._optionsForExec = function(model) { // Apply schema-level `writeConcern` option applyWriteConcern(model.schema, options); - const asyncLocalStorage = this.model.db.base.transactionAsyncLocalStorage?.getStore(); + const asyncLocalStorage = this.model?.db?.base.transactionAsyncLocalStorage?.getStore(); if (!this.options.hasOwnProperty('session') && asyncLocalStorage?.session != null) { options.session = asyncLocalStorage.session; } From eb62a769272ba741b95f55d329f73334aa8a9d41 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 11 May 2024 11:31:15 -0400 Subject: [PATCH 21/27] feat: upgrade mongodb -> 6.6.1 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e543bd33e0c..8a0afcba1fa 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ ], "license": "MIT", "dependencies": { - "bson": "^6.5.0", + "bson": "^6.7.0", "kareem": "2.6.3", - "mongodb": "6.5.0", + "mongodb": "6.6.1", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", From 3c74f46c19839496a7c89a5ae1d68a06db6f9d78 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 May 2024 10:30:11 -0400 Subject: [PATCH 22/27] fix tests --- lib/aggregate.js | 2 +- test/docs/transactions.test.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/aggregate.js b/lib/aggregate.js index 0bdf65b995a..35c32c480a9 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -1022,7 +1022,7 @@ Aggregate.prototype.exec = async function exec() { applyGlobalMaxTimeMS(this.options, model.db.options, model.base.options); applyGlobalDiskUse(this.options, model.db.options, model.base.options); - const asyncLocalStorage = this.model?.db?.base.transactionAsyncLocalStorage?.getStore(); + const asyncLocalStorage = this.model()?.db?.base.transactionAsyncLocalStorage?.getStore(); if (!this.options.hasOwnProperty('session') && asyncLocalStorage?.session != null) { this.options.session = asyncLocalStorage.session; } diff --git a/test/docs/transactions.test.js b/test/docs/transactions.test.js index 29be35d0b56..e21639331b9 100644 --- a/test/docs/transactions.test.js +++ b/test/docs/transactions.test.js @@ -370,7 +370,7 @@ describe('transactions', function() { await Test.createCollection(); await Test.deleteMany({}); - const doc = new Test({ name: 'test' }); + const doc = new Test({ name: 'test_transactionAsyncLocalStorage' }); await assert.rejects( () => m.connection.transaction(async() => { await doc.save(); @@ -383,10 +383,10 @@ describe('transactions', function() { docs = await Test.find({ _id: doc._id }); assert.equal(docs.length, 1); - docs = await Promise.all([async() => { + docs = await async function test() { return await Test.findOne({ _id: doc._id }); - }]).then(res => res[0]); - assert.equal(docs.length, 1); + }(); + assert.equal(doc.name, 'test_transactionAsyncLocalStorage'); throw new Error('Oops!'); }), From 5e9977bd6c12f0e39c8be8934afb4fb7d36514c5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 May 2024 10:34:01 -0400 Subject: [PATCH 23/27] Update docs/transactions.md Co-authored-by: hasezoey --- docs/transactions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/transactions.md b/docs/transactions.md index dfe9610529b..4251cd5d017 100644 --- a/docs/transactions.md +++ b/docs/transactions.md @@ -102,7 +102,7 @@ const doc = new Test({ name: 'test' }); await connection.transaction(async() => { await doc.save(); // Notice no session here throw new Error('Oops'); -}); +}).catch(() => {}); // false, `save()` was rolled back await Test.exists({ _id: doc._id }); From 713c9ba2ec153d7df8a4bb1429255d9ffb057a26 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 May 2024 12:18:16 -0400 Subject: [PATCH 24/27] test: fix tests --- test/types/schema.test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index c5ee82c0c10..bbbcdd5d71f 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1503,5 +1503,11 @@ function gh13772() { }; const schema = new Schema(schemaDefinition); type RawDocType = InferRawDocType; - expectAssignable<{ name?: string, docArr?: Array<{ name?: string }> }>({} as RawDocType); -} \ No newline at end of file + expectAssignable< + { name?: string | null, docArr?: Array<{ name?: string | null }> } + >({} as RawDocType); + + const TestModel = model('User', schema); + const doc = new TestModel(); + expectAssignable(doc.toObject()); +} From 792bcac7f34a637e4ce308e3ab8abbbdff47c685 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 May 2024 12:57:29 -0400 Subject: [PATCH 25/27] adapt changes from inferSchemaType --- scripts/tsc-diagnostics-check.js | 2 +- types/inferrawdoctype.d.ts | 99 +++++++++++++++++++------------- 2 files changed, 61 insertions(+), 40 deletions(-) diff --git a/scripts/tsc-diagnostics-check.js b/scripts/tsc-diagnostics-check.js index 2f74bf39b92..3e1f6c66282 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]) ? 125000 : parseInt(process.argv[2], 10); +const maxInstantiations = isNaN(process.argv[2]) ? 127500 : parseInt(process.argv[2], 10); console.log(stdin); diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index 49b05d2ce9f..95a912ff240 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -1,4 +1,5 @@ import { + IsSchemaTypeFromBuiltinClass, RequiredPaths, OptionalPaths, PathWithTypePropertyBaseType, @@ -32,6 +33,12 @@ declare module 'mongoose' { >; /** + * Same as inferSchemaType, except: + * + * 1. Replace `Types.DocumentArray` and `Types.Array` with vanilla `Array` + * 2. Replace `ObtainDocumentPathType` with `ObtainRawDocumentPathType` + * 3. Replace `ResolvePathType` with `ResolveRawPathType` + * * @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. @@ -39,51 +46,65 @@ declare module 'mongoose' { * @returns Number, "Number" or "number" will be resolved to number type. */ type ResolveRawPathType = {}, TypeKey extends string = DefaultSchemaOptions['typeKey']> = - PathValueType extends Schema ? InferRawDocType : - PathValueType extends (infer Item)[] ? + PathValueType extends Schema ? + InferSchemaType : + PathValueType extends (infer Item)[] ? + IfEquals> : + 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 ObtainDocumentType to correctly infer its type. + Array> : + IsSchemaTypeFromBuiltinClass extends true ? + ObtainRawDocumentPathType[] : + IsItRecordAndNotAny extends true ? + Item extends Record ? + ObtainRawDocumentPathType[] : + Array> : + ObtainRawDocumentPathType[] + >: + PathValueType extends ReadonlyArray ? IfEquals> : - Item extends Record? + 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 ObtainDocumentType to correctly infer its type. - InferRawDocType[]: - ObtainRawDocumentPathType[] + ObtainDocumentType[]: + IsSchemaTypeFromBuiltinClass extends true ? + ObtainRawDocumentPathType[] : + IsItRecordAndNotAny extends true ? + Item extends Record ? + ObtainRawDocumentPathType[] : + Array> : + ObtainRawDocumentPathType[] >: - PathValueType extends ReadonlyArray ? - IfEquals> : - Item extends Record ? - Item[TypeKey] extends Function | String ? - ObtainRawDocumentPathType[] : - InferRawDocType[]: - 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 ? Date : - IfEquals extends true ? Date : - 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 : - PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt ? bigint : + 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 ? Date : + IfEquals extends true ? Date : + 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 ? Buffer : IfEquals extends true ? Buffer : - PathValueType extends MapConstructor ? Map> : + PathValueType extends MapConstructor | 'Map' ? Map> : IfEquals extends true ? Map> : PathValueType extends ArrayConstructor ? any[] : PathValueType extends typeof Schema.Types.Mixed ? any: From 366004148dc0c7bd95d81bee28a19267042aed02 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 15 May 2024 17:59:25 -0400 Subject: [PATCH 26/27] docs(typescript): add section on InferRawDocType to TypeScript docs --- docs/typescript/schemas.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/typescript/schemas.md b/docs/typescript/schemas.md index e4127ce1621..8dfa5310556 100644 --- a/docs/typescript/schemas.md +++ b/docs/typescript/schemas.md @@ -9,7 +9,7 @@ Mongoose can automatically infer the document type from your schema definition a We recommend relying on automatic type inference when defining schemas and models. ```typescript -import { Schema } from 'mongoose'; +import { Schema, model } from 'mongoose'; // Schema const schema = new Schema({ name: { type: String, required: true }, @@ -32,6 +32,31 @@ There are a few caveats for using automatic type inference: 2. You need to define your schema in the `new Schema()` call. Don't assign your schema definition to a temporary variable. Doing something like `const schemaDefinition = { name: String }; const schema = new Schema(schemaDefinition);` will not work. 3. Mongoose adds `createdAt` and `updatedAt` to your schema if you specify the `timestamps` option in your schema, *except* if you also specify `methods`, `virtuals`, or `statics`. There is a [known issue](https://github.com/Automattic/mongoose/issues/12807) with type inference with timestamps and methods/virtuals/statics options. If you use methods, virtuals, and statics, you're responsible for adding `createdAt` and `updatedAt` to your schema definition. +If you need to explicitly get the raw document type (the value returned from `doc.toObject()`, `await Model.findOne().lean()`, etc.) from your schema definition, you can use Mongoose's `inferRawDocType` helper as follows: + +```ts +import { Schema, InferRawDocType, model } from 'mongoose'; + +const schemaDefinition = { + name: { type: String, required: true }, + email: { type: String, required: true }, + avatar: String +} as const; +const schema = new Schema(schemaDefinition); + +const UserModel = model('User', schema); +const doc = new UserModel({ name: 'test', email: 'test' }); + +type RawUserDocument = InferRawDocType; + +useRawDoc(doc.toObject()); + +function useRawDoc(doc: RawUserDocument) { + // ... +} + +``` + If automatic type inference doesn't work for you, you can always fall back to document interface definitions. ## Separate document interface definition From 72d877d555cbaa071e18e46a537fdf9a3a0378e0 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 15 May 2024 18:02:50 -0400 Subject: [PATCH 27/27] fix: use mongodb@6.6.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4b4adb594b8..ef6ec8a6b85 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "bson": "^6.7.0", "kareem": "2.6.3", - "mongodb": "6.6.1", + "mongodb": "6.6.2", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3",