diff --git a/lib/cursor/changeStream.js b/lib/cursor/changeStream.js index ec5cac5705f..55cdecfcdc2 100644 --- a/lib/cursor/changeStream.js +++ b/lib/cursor/changeStream.js @@ -33,15 +33,18 @@ class ChangeStream extends EventEmitter { ); } - // This wrapper is necessary because of buffering. - changeStreamThunk((err, driverChangeStream) => { - if (err != null) { - this.emit('error', err); - return; - } + this.$driverChangeStreamPromise = new Promise((resolve, reject) => { + // This wrapper is necessary because of buffering. + changeStreamThunk((err, driverChangeStream) => { + if (err != null) { + this.emit('error', err); + return reject(err); + } - this.driverChangeStream = driverChangeStream; - this.emit('ready'); + this.driverChangeStream = driverChangeStream; + this.emit('ready'); + resolve(); + }); }); } @@ -53,20 +56,23 @@ class ChangeStream extends EventEmitter { this.bindedEvents = true; if (this.driverChangeStream == null) { - this.once('ready', () => { - this.driverChangeStream.on('close', () => { - this.closed = true; - }); + this.$driverChangeStreamPromise.then( + () => { + this.driverChangeStream.on('close', () => { + this.closed = true; + }); - driverChangeStreamEvents.forEach(ev => { - this.driverChangeStream.on(ev, data => { - if (data != null && data.fullDocument != null && this.options && this.options.hydrate) { - data.fullDocument = this.options.model.hydrate(data.fullDocument); - } - this.emit(ev, data); + driverChangeStreamEvents.forEach(ev => { + this.driverChangeStream.on(ev, data => { + if (data != null && data.fullDocument != null && this.options && this.options.hydrate) { + data.fullDocument = this.options.model.hydrate(data.fullDocument); + } + this.emit(ev, data); + }); }); - }); - }); + }, + () => {} // No need to register events if opening change stream failed + ); return; } @@ -142,8 +148,12 @@ class ChangeStream extends EventEmitter { this.closed = true; if (this.driverChangeStream) { return this.driverChangeStream.close(); + } else { + return this.$driverChangeStreamPromise.then( + () => this.driverChangeStream.close(), + () => {} // No need to close if opening the change stream failed + ); } - return Promise.resolve(); } } diff --git a/lib/cursor/queryCursor.js b/lib/cursor/queryCursor.js index ff59f6aeba8..6f00a316794 100644 --- a/lib/cursor/queryCursor.js +++ b/lib/cursor/queryCursor.js @@ -10,6 +10,7 @@ const eachAsync = require('../helpers/cursor/eachAsync'); const helpers = require('../queryHelpers'); const kareem = require('kareem'); const immediate = require('../helpers/immediate'); +const { once } = require('node:events'); const util = require('util'); /** @@ -42,6 +43,7 @@ function QueryCursor(query) { this.cursor = null; this.skipped = false; this.query = query; + this._closed = false; const model = query.model; this._mongooseOptions = {}; this._transforms = []; @@ -135,6 +137,25 @@ QueryCursor.prototype._read = function() { }); }; +/** + * Returns the underlying cursor from the MongoDB Node driver that this cursor uses. + * + * @method getDriverCursor + * @memberOf QueryCursor + * @returns {Cursor} MongoDB Node driver cursor instance + * @instance + * @api public + */ + +QueryCursor.prototype.getDriverCursor = async function getDriverCursor() { + if (this.cursor) { + return this.cursor; + } + + await once(this, 'cursor'); + return this.cursor; +}; + /** * Registers a transform function which subsequently maps documents retrieved * via the streams interface or `.next()` @@ -209,6 +230,7 @@ QueryCursor.prototype.close = async function close() { } try { await this.cursor.close(); + this._closed = true; this.emit('close'); } catch (error) { this.listeners('error').length > 0 && this.emit('error', error); @@ -246,6 +268,9 @@ QueryCursor.prototype.next = async function next() { if (typeof arguments[0] === 'function') { throw new MongooseError('QueryCursor.prototype.next() no longer accepts a callback'); } + if (this._closed) { + throw new MongooseError('Cannot call `next()` on a closed cursor'); + } return new Promise((resolve, reject) => { _next(this, function(error, doc) { if (error) { diff --git a/lib/model.js b/lib/model.js index e0270872155..e83a61be4ff 100644 --- a/lib/model.js +++ b/lib/model.js @@ -2115,17 +2115,21 @@ Model.countDocuments = function countDocuments(conditions, options) { * * @param {String} field * @param {Object} [conditions] optional + * @param {Object} [options] optional * @return {Query} * @api public */ -Model.distinct = function distinct(field, conditions) { +Model.distinct = function distinct(field, conditions, options) { _checkContext(this, 'distinct'); - if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') { + if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') { throw new MongooseError('Model.distinct() no longer accepts a callback'); } const mq = new this.Query({}, {}, this, this.$__collection); + if (options != null) { + mq.setOptions(options); + } return mq.distinct(field, conditions); }; diff --git a/lib/query.js b/lib/query.js index f928ca75375..dec846a2dcb 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2777,7 +2777,7 @@ Query.prototype.estimatedDocumentCount = function(options) { this.op = 'estimatedDocumentCount'; this._validateOp(); - if (typeof options === 'object' && options != null) { + if (options != null) { this.setOptions(options); } @@ -2836,7 +2836,7 @@ Query.prototype.countDocuments = function(conditions, options) { this.merge(conditions); } - if (typeof options === 'object' && options != null) { + if (options != null) { this.setOptions(options); } @@ -2874,21 +2874,24 @@ Query.prototype.__distinct = async function __distinct() { * * #### Example: * + * distinct(field, conditions, options) * distinct(field, conditions) * distinct(field) * distinct() * * @param {String} [field] * @param {Object|Query} [filter] + * @param {Object} [options] * @return {Query} this * @see distinct https://www.mongodb.com/docs/manual/reference/method/db.collection.distinct/ * @api public */ -Query.prototype.distinct = function(field, conditions) { +Query.prototype.distinct = function(field, conditions, options) { if (typeof field === 'function' || typeof conditions === 'function' || - typeof arguments[2] === 'function') { + typeof options === 'function' || + typeof arguments[3] === 'function') { throw new MongooseError('Query.prototype.distinct() no longer accepts a callback'); } @@ -2907,6 +2910,10 @@ Query.prototype.distinct = function(field, conditions) { this._distinct = field; } + if (options != null) { + this.setOptions(options); + } + return this; }; diff --git a/package.json b/package.json index 53551cff5c0..0453893604b 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "bson": "^6.7.0", "kareem": "2.6.3", - "mongodb": "6.7.0", + "mongodb": "6.8.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", diff --git a/test/connection.test.js b/test/connection.test.js index d395be5511b..03f87b40f3d 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -1032,6 +1032,8 @@ describe('connections:', function() { await nextChange; assert.equal(changes.length, 1); assert.equal(changes[0].operationType, 'insert'); + + await changeStream.close(); await conn.close(); }); diff --git a/test/docs/transactions.test.js b/test/docs/transactions.test.js index 2b19aef9ee3..de2ecfc9952 100644 --- a/test/docs/transactions.test.js +++ b/test/docs/transactions.test.js @@ -338,6 +338,25 @@ describe('transactions', function() { assert.deepEqual(fromDb, { name: 'Tyrion Lannister' }); }); + it('distinct (gh-8006)', async function() { + const Character = db.model('gh8006_Character', new Schema({ name: String, rank: String }, { versionKey: false })); + + const session = await db.startSession(); + + session.startTransaction(); + await Character.create([{ name: 'Will Riker', rank: 'Commander' }, { name: 'Jean-Luc Picard', rank: 'Captain' }], { session }); + + let names = await Character.distinct('name', {}, { session }); + assert.deepStrictEqual(names.sort(), ['Jean-Luc Picard', 'Will Riker']); + + names = await Character.distinct('name', { rank: 'Captain' }, { session }); + assert.deepStrictEqual(names.sort(), ['Jean-Luc Picard']); + + // Undo both update and delete since doc should pull from `$session()` + await session.abortTransaction(); + session.endSession(); + }); + it('save() with no changes (gh-8571)', async function() { db.deleteModel(/Test/); const Test = db.model('Test', Schema({ name: String })); diff --git a/test/model.test.js b/test/model.test.js index 855f1eecb8f..f6943d96fc7 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -7,6 +7,7 @@ const sinon = require('sinon'); const start = require('./common'); const assert = require('assert'); +const { once } = require('events'); const random = require('./util').random; const util = require('./util'); @@ -3508,6 +3509,9 @@ describe('Model', function() { } changeStream.removeListener('change', listener); listener = null; + // Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream + // may still poll after close. + changeStream.on('error', () => {}); changeStream.close(); changeStream = null; }); @@ -3560,14 +3564,21 @@ describe('Model', function() { it('fullDocument (gh-11936)', async function() { const MyModel = db.model('Test', new Schema({ name: String })); + const doc = await MyModel.create({ name: 'Ned Stark' }); const changeStream = await MyModel.watch([], { fullDocument: 'updateLookup', hydrate: true }); + await changeStream.$driverChangeStreamPromise; - const doc = await MyModel.create({ name: 'Ned Stark' }); - - const p = changeStream.next(); + const p = new Promise((resolve) => { + changeStream.once('change', change => { + resolve(change); + }); + }); + // Need to wait for resume token to be set after the event listener, + // otherwise change stream might not pick up the update. + await once(changeStream.driverChangeStream, 'resumeTokenChanged'); await MyModel.updateOne({ _id: doc._id }, { name: 'Tony Stark' }); const changeData = await p; @@ -3576,6 +3587,8 @@ describe('Model', function() { doc._id.toHexString()); assert.ok(changeData.fullDocument.$__); assert.equal(changeData.fullDocument.get('name'), 'Tony Stark'); + + await changeStream.close(); }); it('fullDocument with immediate watcher and hydrate (gh-14049)', async function() { @@ -3583,15 +3596,22 @@ describe('Model', function() { const doc = await MyModel.create({ name: 'Ned Stark' }); + let changeStream = null; const p = new Promise((resolve) => { - MyModel.watch([], { + changeStream = MyModel.watch([], { fullDocument: 'updateLookup', hydrate: true - }).on('change', change => { + }); + + changeStream.on('change', change => { resolve(change); }); }); + // Need to wait for cursor to be initialized and for resume token to + // be set, otherwise change stream might not pick up the update. + await changeStream.$driverChangeStreamPromise; + await once(changeStream.driverChangeStream, 'resumeTokenChanged'); await MyModel.updateOne({ _id: doc._id }, { name: 'Tony Stark' }); const changeData = await p; @@ -3600,6 +3620,8 @@ describe('Model', function() { doc._id.toHexString()); assert.ok(changeData.fullDocument.$__); assert.equal(changeData.fullDocument.get('name'), 'Tony Stark'); + + await changeStream.close(); }); it('respects discriminators (gh-11007)', async function() { @@ -3639,6 +3661,9 @@ describe('Model', function() { assert.equal(changeData.operationType, 'insert'); assert.equal(changeData.fullDocument.name, 'Ned Stark'); + // Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream + // may still poll after close. + changeStream.on('error', () => {}); await changeStream.close(); await db.close(); }); @@ -3654,11 +3679,16 @@ describe('Model', function() { setTimeout(resolve, 500, false); }); - changeStream.close(); - await db; + // Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream + // may still poll after close. + changeStream.on('error', () => {}); + + const close = changeStream.close(); + await db.asPromise(); const readyCalled = await ready; assert.strictEqual(readyCalled, false); + await close; await db.close(); }); @@ -3675,6 +3705,10 @@ describe('Model', function() { await MyModel.create({ name: 'Hodor' }); + // Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream + // may still poll after close. + changeStream.on('error', () => {}); + changeStream.close(); const closedData = await closed; assert.strictEqual(closedData, true); diff --git a/test/model.watch.test.js b/test/model.watch.test.js index 84d41dc5b14..3e2ad733a24 100644 --- a/test/model.watch.test.js +++ b/test/model.watch.test.js @@ -37,18 +37,22 @@ describe('model: watch: ', function() { const changeData = await changed; assert.equal(changeData.operationType, 'insert'); assert.equal(changeData.fullDocument.name, 'Ned Stark'); + await changeStream.close(); }); it('watch() close() prevents buffered watch op from running (gh-7022)', async function() { const MyModel = db.model('Test', new Schema({})); const changeStream = MyModel.watch(); - const ready = new global.Promise(resolve => { + const ready = new Promise(resolve => { changeStream.once('data', () => { resolve(true); }); setTimeout(resolve, 500, false); }); + // Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream + // may still poll after close. + changeStream.on('error', () => {}); const close = changeStream.close(); await db.asPromise(); const readyCalled = await ready; @@ -64,12 +68,16 @@ describe('model: watch: ', function() { await MyModel.init(); const changeStream = MyModel.watch(); - const closed = new global.Promise(resolve => { + const closed = new Promise(resolve => { changeStream.once('close', () => resolve(true)); }); await MyModel.create({ name: 'Hodor' }); + // Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream + // may still poll after close. + changeStream.on('error', () => {}); + await changeStream.close(); const closedData = await closed; diff --git a/test/query.cursor.test.js b/test/query.cursor.test.js index d835135a0b2..d80264c5f2d 100644 --- a/test/query.cursor.test.js +++ b/test/query.cursor.test.js @@ -415,7 +415,8 @@ describe('QueryCursor', function() { await cursor.next(); assert.ok(false); } catch (error) { - assert.equal(error.name, 'MongoCursorExhaustedError'); + assert.equal(error.name, 'MongooseError'); + assert.ok(error.message.includes('closed cursor'), error.message); } }); }); @@ -900,6 +901,25 @@ describe('QueryCursor', function() { assert.ok(err); assert.ok(err.message.includes('skipMiddlewareFunction'), err.message); }); + + it('returns the underlying Node driver cursor with getDriverCursor()', async function() { + const schema = new mongoose.Schema({ name: String }); + + const Movie = db.model('Movie', schema); + + await Movie.deleteMany({}); + await Movie.create([ + { name: 'Kickboxer' }, + { name: 'Ip Man' }, + { name: 'Enter the Dragon' } + ]); + + const cursor = await Movie.find({}).cursor(); + assert.ok(!cursor.cursor); + const driverCursor = await cursor.getDriverCursor(); + assert.ok(cursor.cursor); + assert.equal(driverCursor, cursor.cursor); + }); }); async function delay(ms) { diff --git a/test/query.test.js b/test/query.test.js index bf6175e0688..a3de50044e3 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -1088,6 +1088,16 @@ describe('Query', function() { assert.equal(q.op, 'distinct'); }); + + it('using options parameter for distinct', function() { + const q = new Query({}); + const options = { collation: { locale: 'en', strength: 2 } }; + + q.distinct('blah', {}, options); + + assert.equal(q.op, 'distinct'); + assert.deepEqual(q.options.collation, options.collation); + }); }); describe('findOne', function() { diff --git a/test/types/docArray.test.ts b/test/types/docArray.test.ts index c296ce6fea7..78650845ba2 100644 --- a/test/types/docArray.test.ts +++ b/test/types/docArray.test.ts @@ -91,7 +91,7 @@ async function gh13424() { const TestModel = model('Test', testSchema); const doc = new TestModel(); - expectType(doc.subDocArray[0]._id); + expectType(doc.subDocArray[0]._id); } async function gh14367() { diff --git a/test/types/document.test.ts b/test/types/document.test.ts index d492cebd6e7..84451edf0f2 100644 --- a/test/types/document.test.ts +++ b/test/types/document.test.ts @@ -10,7 +10,7 @@ import { DefaultSchemaOptions } from 'mongoose'; import { DeleteResult } from 'mongodb'; -import { expectAssignable, expectError, expectType } from 'tsd'; +import { expectAssignable, expectError, expectNotAssignable, expectType } from 'tsd'; import { autoTypedModel } from './models.test'; import { autoTypedModelConnection } from './connection.test'; import { AutoTypedSchemaType } from './schema.test'; @@ -42,7 +42,8 @@ void async function main() { expectType(await doc.deleteOne()); expectType(await doc.deleteOne().findOne()); - expectType<{ _id: Types.ObjectId, name?: string } | null>(await doc.deleteOne().findOne().lean()); + expectAssignable<{ _id: Types.ObjectId, name?: string } | null>(await doc.deleteOne().findOne().lean()); + expectNotAssignable(await doc.deleteOne().findOne().lean()); }(); diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 218c4c90569..fbe411225f0 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -215,8 +215,8 @@ function find() { Project.find({}); Project.find({ name: 'Hello' }); - // just callback - Project.find((error: CallbackError, result: IProject[]) => console.log(error, result)); + // just callback; this is no longer supported on .find() + expectError(Project.find((error: CallbackError, result: IProject[]) => console.log(error, result))); // filter + projection Project.find({}, undefined); @@ -977,3 +977,29 @@ function testWithLevel1NestedPaths() { 'foo.one': string | null | undefined }>({} as Test2); } + +function gh14764TestFilterQueryRestrictions() { + const TestModel = model<{ validKey: number }>('Test', new Schema({})); + // A key not in the schema should be invalid + expectError(TestModel.find({ invalidKey: 0 })); + // A key not in the schema should be invalid for simple root operators + expectError(TestModel.find({ $and: [{ invalidKey: 0 }] })); + + // Any "nested" keys should be valid + TestModel.find({ 'validKey.subkey': 0 }); + + // And deeply "nested" keys should be valid + TestModel.find({ 'validKey.deep.nested.key': 0 }); + TestModel.find({ validKey: { deep: { nested: { key: 0 } } } }); + + // Any Query should be accepted as the root argument (due to merge support) + TestModel.find(TestModel.find()); + // A Query should not be a valid type for a FilterQuery within an op like $and + expectError(TestModel.find({ $and: [TestModel.find()] })); + + const id = new Types.ObjectId(); + // Any ObjectId should be accepted as the root argument + TestModel.find(id); + // A ObjectId should not be a valid type for a FilterQuery within an op like $and + expectError(TestModel.find({ $and: [id] })); +} diff --git a/test/types/plugin.test.ts b/test/types/plugin.test.ts index 0b579ccc6b9..fc1deddf900 100644 --- a/test/types/plugin.test.ts +++ b/test/types/plugin.test.ts @@ -49,7 +49,7 @@ interface TestStaticMethods { findSomething(this: TestModel): Promise; } type TestDocument = HydratedDocument; -type TestQuery = Query & TestQueryHelpers; +type TestQuery = Query & TestQueryHelpers; interface TestQueryHelpers { whereSomething(this: TestQuery): this } diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index f10f26b3ad2..5396f384cad 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -38,11 +38,11 @@ const schema: Schema, {}, QueryHelpers> = new endDate: Date }); -schema.query._byName = function(name: string): QueryWithHelpers { +schema.query._byName = function(name: string): QueryWithHelpers { return this.find({ name }); }; -schema.query.byName = function(name: string): QueryWithHelpers { +schema.query.byName = function(name: string): QueryWithHelpers { expectError(this.notAQueryHelper()); return this._byName(name); }; @@ -109,6 +109,7 @@ Test.find({ name: { $gte: 'Test' } }, null, { collation: { locale: 'en-us' } }). Test.findOne().orFail(new Error('bar')).then((doc: ITest | null) => console.log('Found! ' + doc)); Test.distinct('name').exec().then((res: Array) => console.log(res[0])); +Test.distinct('name', {}, { collation: { locale: 'en', strength: 2 } }).exec().then((res: Array) => console.log(res[0])); Test.findOneAndUpdate({ name: 'test' }, { name: 'test2' }).exec().then((res: ITest | null) => console.log(res)); Test.findOneAndUpdate({ name: 'test' }, { name: 'test2' }).then((res: ITest | null) => console.log(res)); diff --git a/test/types/queryhelpers.test.ts b/test/types/queryhelpers.test.ts index c96aaffbaa2..34a55b6bd22 100644 --- a/test/types/queryhelpers.test.ts +++ b/test/types/queryhelpers.test.ts @@ -8,7 +8,7 @@ interface Project { type ProjectModelType = Model; // Query helpers should return `Query> & ProjectQueryHelpers` // to enable chaining. -type ProjectModelQuery = Query, ProjectQueryHelpers> & ProjectQueryHelpers; +type ProjectModelQuery = Query, ProjectQueryHelpers, any> & ProjectQueryHelpers; interface ProjectQueryHelpers { byName(this: ProjectModelQuery, name: string): ProjectModelQuery; } diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 8c9d1a8f3d5..04828bc4f17 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -22,6 +22,7 @@ import { model, ValidateOpts } from 'mongoose'; +import { IsPathRequired } from '../../types/inferschematype'; import { expectType, expectError, expectAssignable } from 'tsd'; import { ObtainDocumentPathType, ResolvePathType } from '../../types/inferschematype'; @@ -1387,7 +1388,7 @@ function gh13424() { const TestModel = model('TestModel', new Schema(testSchema)); const doc = new TestModel({}); - expectType(doc.subDocArray[0]._id); + expectType(doc.subDocArray[0]._id); } function gh14147() { @@ -1502,16 +1503,18 @@ function gh13772() { const schemaDefinition = { name: String, docArr: [{ name: String }] - }; + } as const; const schema = new Schema(schemaDefinition); + + const TestModel = model('User', schema); type RawDocType = InferRawDocType; expectAssignable< - { name?: string | null, docArr?: Array<{ name?: string | null }> } + { name?: string | null, docArr?: Array<{ name?: string | null }> | null } >({} as RawDocType); - const TestModel = model('User', schema); const doc = new TestModel(); expectAssignable(doc.toObject()); + expectAssignable(doc.toJSON()); } function gh14696() { diff --git a/types/document.d.ts b/types/document.d.ts index c0fb5589240..5557269783f 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -22,7 +22,7 @@ declare module 'mongoose' { constructor(doc?: any); /** This documents _id. */ - _id?: T; + _id: T; /** This documents __v. */ __v?: any; @@ -259,11 +259,14 @@ 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 }): FlattenMaps>; + toJSON(options: ToObjectOptions & { flattenMaps: false }): Require_id; toJSON>(options?: ToObjectOptions & { flattenMaps?: true }): FlattenMaps; toJSON>(options: ToObjectOptions & { flattenMaps: false }): T; /** Converts this document into a plain-old JavaScript object ([POJO](https://masteringjs.io/tutorials/fundamentals/pojo)). */ - toObject>(options?: ToObjectOptions): Require_id; + toObject(options?: ToObjectOptions): Require_id; + toObject(options?: ToObjectOptions): Require_id; /** Clears the modified state on the specified path. */ unmarkModified(path: T): void; diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index e2b1d52b6b9..5ef52e13251 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -1,4 +1,5 @@ import { + IsPathRequired, IsSchemaTypeFromBuiltinClass, RequiredPaths, OptionalPaths, @@ -14,7 +15,9 @@ declare module 'mongoose' { [ K in keyof (RequiredPaths & OptionalPaths) - ]: ObtainRawDocumentPathType; + ]: IsPathRequired extends true + ? ObtainRawDocumentPathType + : ObtainRawDocumentPathType | null; }, TSchemaOptions>; /** diff --git a/types/models.d.ts b/types/models.d.ts index 27c43612ac2..4c2403fd51b 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -188,7 +188,7 @@ declare module 'mongoose' { export interface ReplaceOneModel { /** The filter to limit the replaced document. */ - filter: FilterQuery; + filter: RootFilterQuery; /** The document with which to replace the matched document. */ replacement: mongodb.WithoutId; /** Specifies a collation. */ @@ -203,7 +203,7 @@ declare module 'mongoose' { export interface UpdateOneModel { /** The filter to limit the updated documents. */ - filter: FilterQuery; + filter: RootFilterQuery; /** A document or pipeline containing update operators. */ update: UpdateQuery; /** A set of filters specifying to which array elements an update should apply. */ @@ -220,7 +220,7 @@ declare module 'mongoose' { export interface UpdateManyModel { /** The filter to limit the updated documents. */ - filter: FilterQuery; + filter: RootFilterQuery; /** A document or pipeline containing update operators. */ update: UpdateQuery; /** A set of filters specifying to which array elements an update should apply. */ @@ -237,7 +237,7 @@ declare module 'mongoose' { export interface DeleteOneModel { /** The filter to limit the deleted documents. */ - filter: FilterQuery; + filter: RootFilterQuery; /** Specifies a collation. */ collation?: mongodb.CollationOptions; /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ @@ -246,7 +246,7 @@ declare module 'mongoose' { export interface DeleteManyModel { /** The filter to limit the deleted documents. */ - filter: FilterQuery; + filter: RootFilterQuery; /** Specifies a collation. */ collation?: mongodb.CollationOptions; /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ @@ -318,7 +318,7 @@ declare module 'mongoose' { /** Creates a `countDocuments` query: counts the number of documents that match `filter`. */ countDocuments( - filter?: FilterQuery, + filter?: RootFilterQuery, options?: (mongodb.CountOptions & MongooseBaseQueryOptions) | null ): QueryWithHelpers< number, @@ -357,7 +357,7 @@ declare module 'mongoose' { * regardless of the `single` option. */ deleteMany( - filter?: FilterQuery, + filter?: RootFilterQuery, options?: (mongodb.DeleteOptions & MongooseBaseQueryOptions) | null ): QueryWithHelpers< mongodb.DeleteResult, @@ -368,7 +368,7 @@ declare module 'mongoose' { TInstanceMethods >; deleteMany( - filter: FilterQuery + filter: RootFilterQuery ): QueryWithHelpers< mongodb.DeleteResult, THydratedDocumentType, @@ -384,7 +384,7 @@ declare module 'mongoose' { * `single` option. */ deleteOne( - filter?: FilterQuery, + filter?: RootFilterQuery, options?: (mongodb.DeleteOptions & MongooseBaseQueryOptions) | null ): QueryWithHelpers< mongodb.DeleteResult, @@ -395,7 +395,7 @@ declare module 'mongoose' { TInstanceMethods >; deleteOne( - filter: FilterQuery + filter: RootFilterQuery ): QueryWithHelpers< mongodb.DeleteResult, THydratedDocumentType, @@ -446,7 +446,7 @@ declare module 'mongoose' { /** Finds one document. */ findOne( - filter: FilterQuery, + filter: RootFilterQuery, projection: ProjectionType | null | undefined, options: QueryOptions & { lean: true } ): QueryWithHelpers< @@ -458,16 +458,16 @@ declare module 'mongoose' { TInstanceMethods >; findOne( - filter?: FilterQuery, + filter?: RootFilterQuery, projection?: ProjectionType | null, options?: QueryOptions | null ): QueryWithHelpers; findOne( - filter?: FilterQuery, + filter?: RootFilterQuery, projection?: ProjectionType | null ): QueryWithHelpers; findOne( - filter?: FilterQuery + filter?: RootFilterQuery ): QueryWithHelpers; /** @@ -621,7 +621,8 @@ declare module 'mongoose' { /** Creates a `distinct` query: returns the distinct values of the given `field` that match `filter`. */ distinct( field: DocKey, - filter?: FilterQuery + filter?: RootFilterQuery, + options?: QueryOptions ): QueryWithHelpers< Array< DocKey extends keyof WithLevel1NestedPaths @@ -650,7 +651,7 @@ declare module 'mongoose' { * the given `filter`, and `null` otherwise. */ exists( - filter: FilterQuery + filter: RootFilterQuery ): QueryWithHelpers< { _id: InferId } | null, THydratedDocumentType, @@ -662,7 +663,7 @@ declare module 'mongoose' { /** Creates a `find` query: gets a list of documents that match `filter`. */ find( - filter: FilterQuery, + filter: RootFilterQuery, projection: ProjectionType | null | undefined, options: QueryOptions & { lean: true } ): QueryWithHelpers< @@ -674,16 +675,16 @@ declare module 'mongoose' { TInstanceMethods >; find( - filter: FilterQuery, + filter: RootFilterQuery, projection?: ProjectionType | null | undefined, options?: QueryOptions | null | undefined ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'find', TInstanceMethods>; find( - filter: FilterQuery, + filter: RootFilterQuery, projection?: ProjectionType | null | undefined ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'find', TInstanceMethods>; find( - filter: FilterQuery + filter: RootFilterQuery ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'find', TInstanceMethods>; find( ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'find', TInstanceMethods>; @@ -711,7 +712,7 @@ declare module 'mongoose' { /** Creates a `findOneAndUpdate` query, filtering by the given `_id`. */ findByIdAndUpdate( - filter: FilterQuery, + filter: RootFilterQuery, update: UpdateQuery, options: QueryOptions & { includeResultMetadata: true, lean: true } ): QueryWithHelpers< @@ -756,7 +757,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: FilterQuery, + filter: RootFilterQuery, options: QueryOptions & { lean: true } ): QueryWithHelpers< GetLeanResultType | null, @@ -767,17 +768,17 @@ declare module 'mongoose' { TInstanceMethods >; findOneAndDelete( - filter: FilterQuery, + filter: RootFilterQuery, options: QueryOptions & { includeResultMetadata: true } ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndDelete', TInstanceMethods>; findOneAndDelete( - filter?: FilterQuery | null, + filter?: RootFilterQuery | null, options?: QueryOptions | null ): QueryWithHelpers; /** Creates a `findOneAndReplace` query: atomically finds the given document and replaces it with `replacement`. */ findOneAndReplace( - filter: FilterQuery, + filter: RootFilterQuery, replacement: TRawDocType | AnyObject, options: QueryOptions & { lean: true } ): QueryWithHelpers< @@ -789,24 +790,24 @@ declare module 'mongoose' { TInstanceMethods >; findOneAndReplace( - filter: FilterQuery, + filter: RootFilterQuery, replacement: TRawDocType | AnyObject, options: QueryOptions & { includeResultMetadata: true } ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndReplace', TInstanceMethods>; findOneAndReplace( - filter: FilterQuery, + filter: RootFilterQuery, replacement: TRawDocType | AnyObject, options: QueryOptions & { upsert: true } & ReturnsNewDoc ): QueryWithHelpers; findOneAndReplace( - filter?: FilterQuery, + filter?: RootFilterQuery, replacement?: TRawDocType | AnyObject, options?: QueryOptions | null ): QueryWithHelpers; /** Creates a `findOneAndUpdate` query: atomically find the first document that matches `filter` and apply `update`. */ findOneAndUpdate( - filter: FilterQuery, + filter: RootFilterQuery, update: UpdateQuery, options: QueryOptions & { includeResultMetadata: true, lean: true } ): QueryWithHelpers< @@ -818,7 +819,7 @@ declare module 'mongoose' { TInstanceMethods >; findOneAndUpdate( - filter: FilterQuery, + filter: RootFilterQuery, update: UpdateQuery, options: QueryOptions & { lean: true } ): QueryWithHelpers< @@ -830,24 +831,24 @@ declare module 'mongoose' { TInstanceMethods >; findOneAndUpdate( - filter: FilterQuery, + filter: RootFilterQuery, update: UpdateQuery, options: QueryOptions & { includeResultMetadata: true } ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndUpdate', TInstanceMethods>; findOneAndUpdate( - filter: FilterQuery, + filter: RootFilterQuery, update: UpdateQuery, options: QueryOptions & { upsert: true } & ReturnsNewDoc ): QueryWithHelpers; findOneAndUpdate( - filter?: FilterQuery, + filter?: RootFilterQuery, update?: UpdateQuery, options?: QueryOptions | null ): QueryWithHelpers; /** Creates a `replaceOne` query: finds the first document that matches `filter` and replaces it with `replacement`. */ replaceOne( - filter?: FilterQuery, + filter?: RootFilterQuery, replacement?: TRawDocType | AnyObject, options?: (mongodb.ReplaceOptions & MongooseQueryOptions) | null ): QueryWithHelpers; @@ -860,14 +861,14 @@ declare module 'mongoose' { /** Creates a `updateMany` query: updates all documents that match `filter` with `update`. */ updateMany( - filter?: FilterQuery, + filter?: RootFilterQuery, update?: UpdateQuery | UpdateWithAggregationPipeline, options?: (mongodb.UpdateOptions & MongooseUpdateQueryOptions) | null ): QueryWithHelpers; /** Creates a `updateOne` query: updates the first document that matches `filter` with `update`. */ updateOne( - filter?: FilterQuery, + filter?: RootFilterQuery, update?: UpdateQuery | UpdateWithAggregationPipeline, options?: (mongodb.UpdateOptions & MongooseUpdateQueryOptions) | null ): QueryWithHelpers; diff --git a/types/query.d.ts b/types/query.d.ts index 58cfe517ce6..572ec33e1df 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -10,9 +10,11 @@ declare module 'mongoose' { * { age: { $gte: 30 } } * ``` */ - type FilterQuery = { + type RootFilterQuery = FilterQuery | Query | Types.ObjectId; + + type FilterQuery ={ [P in keyof T]?: Condition; - } & RootQuerySelector; + } & RootQuerySelector & { _id?: Condition; }; type MongooseBaseQueryOptionKeys = | 'context' @@ -115,8 +117,9 @@ declare module 'mongoose' { /** @see https://www.mongodb.com/docs/manual/reference/operator/query/comment/#op._S_comment */ $comment?: string; // we could not find a proper TypeScript generic to support nested queries e.g. 'user.friends.name' - // this will mark all unrecognized properties as any (including nested queries) - [key: string]: any; + // this will mark all unrecognized properties as any (including nested queries) only if + // they include a "." (to avoid generically allowing any unexpected keys) + [nestedSelector: `${string}.${string}`]: any; }; interface QueryTimestampsConfig { @@ -224,7 +227,7 @@ declare module 'mongoose' { : MergeType : MergeType; - class Query> implements SessionOperation { + class Query> implements SessionOperation { _mongooseOptions: MongooseQueryOptions; /** @@ -303,7 +306,7 @@ declare module 'mongoose' { /** Specifies this query as a `countDocuments` query. */ countDocuments( - criteria?: FilterQuery, + criteria?: RootFilterQuery, options?: QueryOptions ): QueryWithHelpers; @@ -319,10 +322,10 @@ declare module 'mongoose' { * collection, regardless of the value of `single`. */ deleteMany( - filter?: FilterQuery, + filter?: RootFilterQuery, options?: QueryOptions ): QueryWithHelpers; - deleteMany(filter: FilterQuery): QueryWithHelpers< + deleteMany(filter: RootFilterQuery): QueryWithHelpers< any, DocType, THelpers, @@ -338,10 +341,10 @@ declare module 'mongoose' { * option. */ deleteOne( - filter?: FilterQuery, + filter?: RootFilterQuery, options?: QueryOptions ): QueryWithHelpers; - deleteOne(filter: FilterQuery): QueryWithHelpers< + deleteOne(filter: RootFilterQuery): QueryWithHelpers< any, DocType, THelpers, @@ -354,7 +357,8 @@ declare module 'mongoose' { /** Creates a `distinct` query: returns the distinct values of the given `field` that match `filter`. */ distinct( field: DocKey, - filter?: FilterQuery + filter?: RootFilterQuery, + options?: QueryOptions ): QueryWithHelpers< Array< DocKey extends keyof WithLevel1NestedPaths @@ -406,52 +410,52 @@ declare module 'mongoose' { /** Creates a `find` query: gets a list of documents that match `filter`. */ find( - filter: FilterQuery, + filter: RootFilterQuery, projection?: ProjectionType | null, options?: QueryOptions | null ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find', TInstanceMethods>; find( - filter: FilterQuery, + filter: RootFilterQuery, projection?: ProjectionType | null ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find', TInstanceMethods>; find( - filter: FilterQuery + filter: RootFilterQuery ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find', TInstanceMethods>; find(): QueryWithHelpers, DocType, THelpers, RawDocType, 'find', TInstanceMethods>; /** Declares the query a findOne operation. When executed, returns the first found document. */ findOne( - filter?: FilterQuery, + filter?: RootFilterQuery, projection?: ProjectionType | null, options?: QueryOptions | null ): QueryWithHelpers; findOne( - filter?: FilterQuery, + filter?: RootFilterQuery, projection?: ProjectionType | null ): QueryWithHelpers; findOne( - filter?: FilterQuery + filter?: RootFilterQuery ): QueryWithHelpers; /** Creates a `findOneAndDelete` query: atomically finds the given document, deletes it, and returns the document as it was before deletion. */ findOneAndDelete( - filter?: FilterQuery, + filter?: RootFilterQuery, options?: QueryOptions | null ): QueryWithHelpers; /** Creates a `findOneAndUpdate` query: atomically find the first document that matches `filter` and apply `update`. */ findOneAndUpdate( - filter: FilterQuery, + filter: RootFilterQuery, update: UpdateQuery, options: QueryOptions & { includeResultMetadata: true } ): QueryWithHelpers, DocType, THelpers, RawDocType, 'findOneAndUpdate', TInstanceMethods>; findOneAndUpdate( - filter: FilterQuery, + filter: RootFilterQuery, update: UpdateQuery, options: QueryOptions & { upsert: true } & ReturnsNewDoc ): QueryWithHelpers; findOneAndUpdate( - filter?: FilterQuery, + filter?: RootFilterQuery, update?: UpdateQuery, options?: QueryOptions | null ): QueryWithHelpers; @@ -548,9 +552,19 @@ declare module 'mongoose' { j(val: boolean | null): this; /** Sets the lean option. */ - lean< - LeanResultType = GetLeanResultType - >( + lean( + val?: boolean | any + ): QueryWithHelpers< + ResultType extends null + ? GetLeanResultType | null + : GetLeanResultType, + DocType, + THelpers, + RawDocType, + QueryOp, + TInstanceMethods + >; + lean( val?: boolean | any ): QueryWithHelpers< ResultType extends null @@ -592,7 +606,7 @@ declare module 'mongoose' { maxTimeMS(ms: number): this; /** Merges another Query or conditions object into this one. */ - merge(source: Query | FilterQuery): this; + merge(source: RootFilterQuery): 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; @@ -711,7 +725,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?: FilterQuery, + filter?: RootFilterQuery, replacement?: DocType | AnyObject, options?: QueryOptions | null ): QueryWithHelpers; @@ -819,7 +833,7 @@ declare module 'mongoose' { * the `multi` option. */ updateMany( - filter?: FilterQuery, + filter?: RootFilterQuery, update?: UpdateQuery | UpdateWithAggregationPipeline, options?: QueryOptions | null ): QueryWithHelpers; @@ -829,7 +843,7 @@ declare module 'mongoose' { * `update()`, except it does not support the `multi` or `overwrite` options. */ updateOne( - filter?: FilterQuery, + filter?: RootFilterQuery, update?: UpdateQuery | UpdateWithAggregationPipeline, options?: QueryOptions | null ): QueryWithHelpers;