diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 762b9438818..eb32422b89b 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -26,7 +26,7 @@ jobs: with: fetch-depth: 0 - name: Setup node - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 16 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 3f35875a1f1..75f6b49d93a 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup node - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 16 @@ -52,7 +52,7 @@ jobs: - run: git fetch --depth=1 --tags # download all tags for documentation - name: Setup node - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 16 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4c3fa33aa5f..9d8858be7e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup node - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 18 @@ -61,7 +61,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup node - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: ${{ matrix.node }} @@ -96,7 +96,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup node - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 16 - name: Load MongoDB binary cache @@ -124,7 +124,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup node - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 16 - run: npm install diff --git a/.github/workflows/tidelift-alignment.yml b/.github/workflows/tidelift-alignment.yml index 54362980425..e2498c40e08 100644 --- a/.github/workflows/tidelift-alignment.yml +++ b/.github/workflows/tidelift-alignment.yml @@ -17,7 +17,7 @@ jobs: - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup node - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 16 - name: Alignment diff --git a/.github/workflows/tsd.yml b/.github/workflows/tsd.yml index ee33cc847fe..1eb30cf6862 100644 --- a/.github/workflows/tsd.yml +++ b/.github/workflows/tsd.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup node - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 18 @@ -41,7 +41,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup node - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 14 diff --git a/CHANGELOG.md b/CHANGELOG.md index bfdeaa2d5f4..a6cf1375736 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,42 @@ +8.2.2 / 2024-03-15 +================== + * fix(model): improve update minimizing to only minimize top-level properties in the update #14437 #14420 #13782 + * fix: add Null check in case schema.options['type'][0] is undefined #14431 [Atharv-Bobde](https://github.com/Atharv-Bobde) + * types: consistently infer array of objects in schema as a DocumentArray #14430 #14367 + * types: add TypeScript interface for the new PipelineStage - Vector Search - solving issue #14428 #14429 [jkorach](https://github.com/jkorach) + * types: add pre and post function types on Query class #14433 #14432 [IICarst](https://github.com/IICarst) + * types(model): make bulkWrite() types more flexible to account for casting #14423 + * docs: update version support documentation for mongoose 5 & 6 #14427 [hasezoey](https://github.com/hasezoey) + +7.6.10 / 2024-03-13 +=================== + * docs(model): add extra note about lean option for insertMany() skipping casting #14415 + * docs(mongoose): add options.overwriteModel details to mongoose.model() docs #14422 + +8.2.1 / 2024-03-04 +================== + * fix(document): make $clone avoid converting subdocs into POJOs #14395 #14353 + * fix(connection): avoid unhandled error on createConnection() if on('error') handler registered #14390 #14377 + * fix(schema): avoid applying default write concern to operations that are in a transaction #14391 #11382 + * types(querycursor): correct cursor async iterator type with populate() support #14384 #14374 + * types: missing typescript details on options params of updateMany, updateOne, etc. #14382 #14379 #14378 [FaizBShah](https://github.com/FaizBShah) [sderrow](https://github.com/sderrow) + * types: allow Record as valid query select argument #14371 [sderrow](https://github.com/sderrow) + +6.12.7 / 2024-03-01 +=================== + * perf(model): make insertMany() lean option skip hydrating Mongoose docs #14376 #14372 + * perf(document+schema): small optimizations to make init() faster #14383 #14113 + * fix(connection): don't modify passed options object to `openUri()` #14370 #13376 #13335 + * fix(ChangeStream): bubble up resumeTokenChanged changeStream event #14355 #14349 [3150](https://github.com/3150) + +7.6.9 / 2024-02-26 +================== + * fix(document): handle embedded recursive discriminators on nested path defined using Schema.prototype.discriminator #14256 #14245 + * types(model): correct return type for findByIdAndDelete() #14233 #14190 + * docs(connections): add note about using asPromise() with createConnection() for error handling #14364 #14266 + * docs(model+query+findoneandupdate): add more details about overwriteDiscriminatorKey option to docs #14264 #14246 + +<<<<<<< HEAD 8.2.0 / 2024-02-22 ================== * feat(model): add recompileSchema() function to models to allow applying schema changes after compiling #14306 #14296 @@ -55,6 +94,12 @@ * docs: update TLS/SSL guide for Mongoose v8 - MongoDB v6 driver deprecations #14170 [andylwelch](https://github.com/andylwelch) * docs: update findOneAndUpdate tutorial to use includeResultMetadata #14208 #14207 * docs: clarify disabling _id on subdocs #14195 #14194 +======= +6.12.6 / 2024-01-22 +=================== + * fix(collection): correctly handle buffer timeouts with find() #14277 + * fix(document): allow calling push() with different $position arguments #14254 +>>>>>>> 7.x 7.6.8 / 2024-01-08 ================== diff --git a/docs/connections.md b/docs/connections.md index cd95feb3ebb..d9fe9b62e30 100644 --- a/docs/connections.md +++ b/docs/connections.md @@ -426,16 +426,24 @@ The `mongoose.createConnection()` function takes the same arguments as const conn = mongoose.createConnection('mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]', options); ``` -This [connection](api/connection.html#connection_Connection) object is then used to -create and retrieve [models](api/model.html#model_Model). Models are -**always** scoped to a single connection. +This [connection](api/connection.html#connection_Connection) object is then used to create and retrieve [models](api/model.html#model_Model). +Models are **always** scoped to a single connection. ```javascript const UserModel = conn.model('User', userSchema); ``` -If you use multiple connections, you should make sure you export schemas, -**not** models. Exporting a model from a file is called the *export model pattern*. +The `createConnection()` function returns a connection instance, not a promise. +If you want to use `await` to make sure Mongoose successfully connects to MongoDB, use the [`asPromise()` function](api/connection.html#Connection.prototype.asPromise()): + +```javascript +// `asPromise()` returns a promise that resolves to the connection +// once the connection succeeds, or rejects if connection failed. +const conn = await mongoose.createConnection(connectionString).asPromise(); +``` + +If you use multiple connections, you should make sure you export schemas, **not** models. +Exporting a model from a file is called the *export model pattern*. The export model pattern is limited because you can only use one connection. ```javascript diff --git a/docs/tutorials/findoneandupdate.md b/docs/tutorials/findoneandupdate.md index 80f99fff4da..79fe5a2945e 100644 --- a/docs/tutorials/findoneandupdate.md +++ b/docs/tutorials/findoneandupdate.md @@ -7,10 +7,18 @@ However, there are some cases where you need to use [`findOneAndUpdate()`](https * [Atomic Updates](#atomic-updates) * [Upsert](#upsert) * [The `includeResultMetadata` Option](#includeresultmetadata) +* [Updating Discriminator Keys](#updating-discriminator-keys) ## Getting Started -As the name implies, `findOneAndUpdate()` finds the first document that matches a given `filter`, applies an `update`, and returns the document. By default, `findOneAndUpdate()` returns the document as it was **before** `update` was applied. +As the name implies, `findOneAndUpdate()` finds the first document that matches a given `filter`, applies an `update`, and returns the document. +The `findOneAndUpdate()` function has the following signature: + +```javascript +function findOneAndUpdate(filter, update, options) {} +``` + +By default, `findOneAndUpdate()` returns the document as it was **before** `update` was applied. ```acquit [require:Tutorial.*findOneAndUpdate.*basic case] @@ -78,3 +86,31 @@ Here's what the `res` object from the above example looks like: age: 29 }, ok: 1 } ``` + +## Updating Discriminator Keys + +Mongoose prevents updating the [discriminator key](../discriminators.html#discriminator-keys) using `findOneAndUpdate()` by default. +For example, suppose you have the following discriminator models. + +```javascript +const eventSchema = new mongoose.Schema({ time: Date }); +const Event = db.model('Event', eventSchema); + +const ClickedLinkEvent = Event.discriminator( + 'ClickedLink', + new mongoose.Schema({ url: String }) +); + +const SignedUpEvent = Event.discriminator( + 'SignedUp', + new mongoose.Schema({ username: String }) +); +``` + +Mongoose will remove `__t` (the default discriminator key) from the `update` parameter, if `__t` is set. +This is to prevent unintentional updates to the discriminator key; for example, if you're passing untrusted user input to the `update` parameter. +However, you can tell Mongoose to allow updating the discriminator key by setting the `overwriteDiscriminatorKey` option to `true` as shown below. + +```acquit +[require:use overwriteDiscriminatorKey to change discriminator key] +``` diff --git a/docs/version-support.md b/docs/version-support.md index 436759db80c..986b69e6115 100644 --- a/docs/version-support.md +++ b/docs/version-support.md @@ -13,19 +13,9 @@ We ship all new bug fixes and features to 7.x. Mongoose 6.x (released August 24, 2021) is currently only receiving security fixes and requested bug fixes as of August 24, 2023. Please open a [bug report on GitHub](https://github.com/Automattic/mongoose/issues/new?assignees=&labels=&template=bug.yml) to request backporting a fix to Mongoose 6. -We are **not** actively backporting any new features from Mongoose 7 into Mongoose 6. -Until August 24, 2023, we will backport requested features into Mongoose 6; please open a [feature request on GitHub](https://github.com/Automattic/mongoose/issues/new?assignees=&labels=enhancement%2Cnew+feature&template=feature.yml) to request backporting a feature into Mongoose 6. -After August 24, 2023, we will not backport any new features into Mongoose 6. - Mongoose 6.x end of life (EOL) is January 1, 2025. Mongoose 6.x will no longer receive any updates, security or otherwise, after that date. ## Mongoose 5 -Mongoose 5.x (released January 17, 2018) is currently only receiving security fixes and requested bug fixes. -Please open a [bug report on GitHub](https://github.com/Automattic/mongoose/issues/new?assignees=&labels=&template=bug.yml) to request backporting a fix to Mongoose 5. -We will **not** backport any new features from Mongoose 6 or Mongoose 7 into Mongoose 5. -This includes support for newer versions of MongoDB: we do not intend to add support for MongoDB 5 or higher to Mongoose 5.x. - -Mongoose 5.x end of life (EOL) is March 1, 2024. -Mongoose 5.x will no longer receive any updates, security or otherwise, after that date. +Mongoose 5.x (released January 17, 2018) is End-of-Life (EOL) since March 1, 2024. Mongoose 5.x will no longer receive any updates, security or otherwise. diff --git a/lib/connection.js b/lib/connection.js index 542bdf3c572..05ff52461b0 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -829,6 +829,30 @@ Connection.prototype.openUri = async function openUri(uri, options) { return this; }; +/*! + * Treat `on('error')` handlers as handling the initialConnection promise + * to avoid uncaught exceptions when using `on('error')`. See gh-14377. + */ + +Connection.prototype.on = function on(event, callback) { + if (event === 'error' && this.$initialConnection) { + this.$initialConnection.catch(() => {}); + } + return EventEmitter.prototype.on.call(this, event, callback); +}; + +/*! + * Treat `once('error')` handlers as handling the initialConnection promise + * to avoid uncaught exceptions when using `on('error')`. See gh-14377. + */ + +Connection.prototype.once = function on(event, callback) { + if (event === 'error' && this.$initialConnection) { + this.$initialConnection.catch(() => {}); + } + return EventEmitter.prototype.once.call(this, event, callback); +}; + /*! * ignore */ diff --git a/lib/document.js b/lib/document.js index 0dc1238bc2c..1031f8a8ad6 100644 --- a/lib/document.js +++ b/lib/document.js @@ -742,7 +742,7 @@ function init(self, obj, doc, opts, prefix) { if (i === '__proto__' || i === 'constructor') { return; } - path = prefix + i; + path = prefix ? prefix + i : i; schemaType = docSchema.path(path); // Should still work if not a model-level discriminator, but should not be // necessary. This is *only* to catch the case where we queried using the @@ -751,7 +751,8 @@ function init(self, obj, doc, opts, prefix) { return; } - if (!schemaType && utils.isPOJO(obj[i])) { + const value = obj[i]; + if (!schemaType && utils.isPOJO(value)) { // assume nested object if (!doc[i]) { doc[i] = {}; @@ -759,29 +760,30 @@ function init(self, obj, doc, opts, prefix) { self[i] = doc[i]; } } - init(self, obj[i], doc[i], opts, path + '.'); + init(self, value, doc[i], opts, path + '.'); } else if (!schemaType) { - doc[i] = obj[i]; + doc[i] = value; if (!strict && !prefix) { - self[i] = obj[i]; + self[i] = value; } } else { // Retain order when overwriting defaults - if (doc.hasOwnProperty(i) && obj[i] !== void 0 && !opts.hydratedPopulatedDocs) { + if (doc.hasOwnProperty(i) && value !== void 0 && !opts.hydratedPopulatedDocs) { delete doc[i]; } - if (obj[i] === null) { + if (value === null) { doc[i] = schemaType._castNullish(null); - } else if (obj[i] !== undefined) { - const wasPopulated = obj[i].$__ == null ? null : obj[i].$__.wasPopulated; - if ((schemaType && !wasPopulated) && !opts.hydratedPopulatedDocs) { + } else if (value !== undefined) { + const wasPopulated = value.$__ == null ? null : value.$__.wasPopulated; + + if (schemaType && !wasPopulated && !opts.hydratedPopulatedDocs) { try { if (opts && opts.setters) { // Call applySetters with `init = false` because otherwise setters are a noop const overrideInit = false; - doc[i] = schemaType.applySetters(obj[i], self, overrideInit); + doc[i] = schemaType.applySetters(value, self, overrideInit); } else { - doc[i] = schemaType.cast(obj[i], self, true); + doc[i] = schemaType.cast(value, self, true); } } catch (e) { self.invalidate(e.path, new ValidatorError({ @@ -793,7 +795,7 @@ function init(self, obj, doc, opts, prefix) { })); } } else { - doc[i] = obj[i]; + doc[i] = value; } } // mark as hydrated @@ -1388,6 +1390,7 @@ Document.prototype.$set = function $set(path, val, type, options) { if (schema.options && Array.isArray(schema.options[typeKey]) && schema.options[typeKey].length && + schema.options[typeKey][0] && schema.options[typeKey][0].ref && _isManuallyPopulatedArray(val, schema.options[typeKey][0].ref)) { popOpts = { [populateModelSymbol]: val[0].constructor }; @@ -4743,7 +4746,7 @@ Document.prototype.$clone = function() { const clonedDoc = new Model(); clonedDoc.$isNew = this.$isNew; if (this._doc) { - clonedDoc._doc = clone(this._doc); + clonedDoc._doc = clone(this._doc, { retainDocuments: true }); } if (this.$__) { const Cache = this.$__.constructor; diff --git a/lib/drivers/node-mongodb-native/collection.js b/lib/drivers/node-mongodb-native/collection.js index 784fe4be582..e0a74c368d3 100644 --- a/lib/drivers/node-mongodb-native/collection.js +++ b/lib/drivers/node-mongodb-native/collection.js @@ -136,11 +136,10 @@ function iter(i) { let promise = null; let timeout = null; if (syncCollectionMethods[i] && typeof lastArg === 'function') { - this.addQueue(() => { - lastArg.call(this, null, this[i].apply(this, _args.slice(0, _args.length - 1))); - }, []); + this.addQueue(i, _args); + callback = lastArg; } else if (syncCollectionMethods[i]) { - promise = new Promise((resolve, reject) => { + promise = new this.Promise((resolve, reject) => { callback = function collectionOperationCallback(err, res) { if (timeout != null) { clearTimeout(timeout); diff --git a/lib/helpers/clone.js b/lib/helpers/clone.js index b83858acdf7..a7b5f2f2fe5 100644 --- a/lib/helpers/clone.js +++ b/lib/helpers/clone.js @@ -36,10 +36,23 @@ function clone(obj, options, isArrayChild) { } if (isMongooseObject(obj)) { - // Single nested subdocs should apply getters later in `applyGetters()` - // when calling `toObject()`. See gh-7442, gh-8295 - if (options && options._skipSingleNestedGetters && obj.$isSingleNested) { - options = Object.assign({}, options, { getters: false }); + if (options) { + // Single nested subdocs should apply getters later in `applyGetters()` + // when calling `toObject()`. See gh-7442, gh-8295 + if (options._skipSingleNestedGetters && obj.$isSingleNested) { + options = Object.assign({}, options, { getters: false }); + } + if (options.retainDocuments && obj.$__ != null) { + const clonedDoc = obj.$clone(); + if (obj.__index != null) { + clonedDoc.__index = obj.__index; + } + if (obj.__parentArray != null) { + clonedDoc.__parentArray = obj.__parentArray; + } + clonedDoc.$__parent = obj.$__parent; + return clonedDoc; + } } const isSingleNested = obj.$isSingleNested; diff --git a/lib/helpers/discriminator/applyEmbeddedDiscriminators.js b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js index 840a4dc628e..9a04ecb072f 100644 --- a/lib/helpers/discriminator/applyEmbeddedDiscriminators.js +++ b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js @@ -19,11 +19,10 @@ function applyEmbeddedDiscriminators(schema, seen = new WeakSet()) { if (schemaType._appliedDiscriminators) { continue; } - for (const disc of schemaType.schema._applyDiscriminators.keys()) { - schemaType.discriminator( - disc, - schemaType.schema._applyDiscriminators.get(disc) - ); + for (const discriminatorKey of schemaType.schema._applyDiscriminators.keys()) { + const discriminatorSchema = schemaType.schema._applyDiscriminators.get(discriminatorKey); + applyEmbeddedDiscriminators(discriminatorSchema, seen); + schemaType.discriminator(discriminatorKey, discriminatorSchema); } schemaType._appliedDiscriminators = true; } diff --git a/lib/helpers/schema/applyWriteConcern.js b/lib/helpers/schema/applyWriteConcern.js index fcce08f8410..27098110872 100644 --- a/lib/helpers/schema/applyWriteConcern.js +++ b/lib/helpers/schema/applyWriteConcern.js @@ -6,6 +6,12 @@ module.exports = function applyWriteConcern(schema, options) { if (options.writeConcern != null) { return; } + // Don't apply default write concern to operations in transactions, + // because setting write concern on an operation in a transaction is an error + // See: https://www.mongodb.com/docs/manual/reference/write-concern/ + if (options && options.session && options.session.transaction) { + return; + } const writeConcern = get(schema, 'options.writeConcern', {}); if (Object.keys(writeConcern).length != 0) { options.writeConcern = {}; diff --git a/lib/model.js b/lib/model.js index f6540bced92..f5e4f12f967 100644 --- a/lib/model.js +++ b/lib/model.js @@ -362,12 +362,22 @@ Model.prototype.$__handleSave = function(options, callback) { const update = delta[1]; if (this.$__schema.options.minimize) { - minimize(update); - // minimize might leave us with an empty object, which would - // lead to MongoDB throwing a "Update document requires atomic operators" error - if (Object.keys(update).length === 0) { - handleEmptyUpdate.call(this); - return; + for (const updateOp of Object.values(update)) { + if (updateOp == null) { + continue; + } + for (const key of Object.keys(updateOp)) { + if (updateOp[key] == null || typeof updateOp[key] !== 'object') { + continue; + } + if (!utils.isPOJO(updateOp[key])) { + continue; + } + minimize(updateOp[key]); + if (Object.keys(updateOp[key]).length === 0) { + updateOp[key] = null; + } + } } } @@ -2543,6 +2553,7 @@ Model.$where = function $where() { * @param {Boolean} [options.setDefaultsOnInsert=true] If `setDefaultsOnInsert` and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created * @param {Boolean} [options.includeResultMetadata] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @return {Query} * @see Tutorial https://mongoosejs.com/docs/tutorials/findoneandupdate.html * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ @@ -2635,6 +2646,7 @@ Model.findOneAndUpdate = function(conditions, update, options) { * @param {Boolean} [options.new=false] if true, return the modified document rather than the original * @param {Object|String} [options.select] sets the document fields to return. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @return {Query} * @see Model.findOneAndUpdate https://mongoosejs.com/docs/api/model.html#Model.findOneAndUpdate() * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ @@ -3084,7 +3096,7 @@ Model.startSession = function() { * @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#insertMany) * @param {Boolean} [options.ordered=true] if true, will fail fast on the first error encountered. If false, will insert all the documents it can and report errors later. An `insertMany()` with `ordered = false` is called an "unordered" `insertMany()`. * @param {Boolean} [options.rawResult=false] if false, the returned promise resolves to the documents that passed mongoose document validation. If `true`, will return the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/InsertManyResult.html) with a `mongoose` property that contains `validationErrors` and `results` if this is an unordered `insertMany`. - * @param {Boolean} [options.lean=false] if `true`, skips hydrating and validating the documents. This option is useful if you need the extra performance, but Mongoose won't validate the documents before inserting. + * @param {Boolean} [options.lean=false] if `true`, skips hydrating the documents. This means Mongoose will **not** cast or validate any of the documents passed to `insertMany()`. This option is useful if you need the extra performance, but comes with data integrity risk. Consider using with [`castObject()`](https://mongoosejs.com/docs/api/model.html#Model.castObject()). * @param {Number} [options.limit=null] this limits the number of documents being processed (validation/casting) by mongoose in parallel, this does **NOT** send the documents in batches to MongoDB. Use this option if you're processing a large number of documents and your app is running out of memory. * @param {String|Object|Array} [options.populate=null] populates the result documents. This option is a no-op if `rawResult` is set. * @param {Boolean} [options.throwOnValidationError=false] If true and `ordered: false`, throw an error if one of the operations failed validation, but all valid operations completed successfully. @@ -3145,6 +3157,13 @@ Model.$__insertMany = function(arr, options, callback) { const results = ordered ? null : new Array(arr.length); const toExecute = arr.map((doc, index) => callback => { + // If option `lean` is set to true bypass validation and hydration + if (lean) { + // we have to execute callback at the nextTick to be compatible + // with parallelLimit, as `results` variable has TDZ issue if we + // execute the callback synchronously + return immediate(() => callback(null, doc)); + } if (!(doc instanceof _this)) { if (doc != null && typeof doc !== 'object') { return callback(new ObjectParameterError(doc, 'arr.' + index, 'insertMany')); @@ -3225,7 +3244,7 @@ Model.$__insertMany = function(arr, options, callback) { callback(null, []); return; } - const docObjects = docAttributes.map(function(doc) { + const docObjects = lean ? docAttributes : docAttributes.map(function(doc) { if (doc.$__schema.options.versionKey) { doc[doc.$__schema.options.versionKey] = 0; } @@ -3238,9 +3257,11 @@ Model.$__insertMany = function(arr, options, callback) { _this.$__collection.insertMany(docObjects, options).then( res => { - for (const attribute of docAttributes) { - attribute.$__reset(); - _setIsNew(attribute, false); + if (!lean) { + for (const attribute of docAttributes) { + attribute.$__reset(); + _setIsNew(attribute, false); + } } if (ordered === false && throwOnValidationError && validationErrors.length > 0) { @@ -3342,6 +3363,9 @@ Model.$__insertMany = function(arr, options, callback) { return !isErrored; }). map(function setIsNewForInsertedDoc(doc) { + if (lean) { + return doc; + } doc.$__reset(); _setIsNew(doc, false); return doc; @@ -3384,6 +3408,7 @@ function _setIsNew(doc, val) { * trip to MongoDB. * * Mongoose will perform casting on all operations you provide. + * The only exception is [setting the `update` operator for `updateOne` or `updateMany` to a pipeline](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#updateone-and-updatemany): Mongoose does **not** cast update pipelines. * * This function does **not** trigger any middleware, neither `save()`, nor `update()`. * If you need to trigger @@ -3419,6 +3444,15 @@ function _setIsNew(doc, val) { * console.log(res.insertedCount, res.modifiedCount, res.deletedCount); * }); * + * // Mongoose does **not** cast update pipelines, so no casting for the `update` option below. + * // Mongoose does still cast `filter` + * await Character.bulkWrite([{ + * updateOne: { + * filter: { name: 'Annika Hansen' }, + * update: [{ $set: { name: 7 } }] // Array means update pipeline, so Mongoose skips casting + * } + * }]); + * * The [supported operations](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#db.collection.bulkWrite) are: * * - `insertOne` @@ -3948,13 +3982,14 @@ Model.hydrate = function(obj, projection, options) { * - `updateMany()` * * @param {Object} filter - * @param {Object|Array} update + * @param {Object|Array} update. If array, this update will be treated as an update pipeline and not casted. * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @return {Query} * @see Query docs https://mongoosejs.com/docs/queries.html * @see MongoDB docs https://www.mongodb.com/docs/manual/reference/command/update/#update-command-output @@ -3987,13 +4022,14 @@ Model.updateMany = function updateMany(conditions, doc, options) { * - `updateOne()` * * @param {Object} filter - * @param {Object|Array} update + * @param {Object|Array} update. If array, this update will be treated as an update pipeline and not casted. * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @return {Query} * @see Query docs https://mongoosejs.com/docs/queries.html * @see MongoDB docs https://www.mongodb.com/docs/manual/reference/command/update/#update-command-output diff --git a/lib/mongoose.js b/lib/mongoose.js index 5bee65d3c85..fba35284838 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -506,12 +506,14 @@ Mongoose.prototype.pluralize = function(fn) { * * // or * - * const collectionName = 'actor' - * const M = mongoose.model('Actor', schema, collectionName) + * const collectionName = 'actor'; + * const M = mongoose.model('Actor', schema, collectionName); * * @param {String|Function} name model name or class extending Model * @param {Schema} [schema] the schema to use. * @param {String} [collection] name (optional, inferred from model name) + * @param {Object} [options] + * @param {Boolean} [options.overwriteModels=false] If true, overwrite existing models with the same name to avoid `OverwriteModelError` * @return {Model} The model associated with `name`. Mongoose will create the model if it doesn't already exist. * @api public */ diff --git a/lib/query.js b/lib/query.js index 8731f003204..c0d44e020c9 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2861,19 +2861,27 @@ Query.prototype.distinct = function(field, conditions) { * Cannot be used with `distinct()` * * @param {Object|String|Array>} arg + * @param {Object} [options] + * @param {Boolean} [options.override=false] If true, replace existing sort options with `arg` * @return {Query} this * @see cursor.sort https://www.mongodb.com/docs/manual/reference/method/cursor.sort/ * @api public */ -Query.prototype.sort = function(arg) { - if (arguments.length > 1) { - throw new Error('sort() only takes 1 Argument'); +Query.prototype.sort = function(arg, options) { + if (arguments.length > 2) { + throw new Error('sort() takes at most 2 arguments'); + } + if (options != null && typeof options !== 'object') { + throw new Error('sort() options argument must be an object or nullish'); } if (this.options.sort == null) { this.options.sort = {}; } + if (options && options.override) { + this.options.sort = {}; + } const sort = this.options.sort; if (typeof arg === 'string') { const properties = arg.indexOf(' ') === -1 ? [arg] : arg.split(' '); @@ -3191,6 +3199,7 @@ function prepareDiscriminatorCriteria(query) { * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.returnOriginal=null] An alias for the `new` option. `returnOriginal: false` is equivalent to `new: true`. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @see Tutorial https://mongoosejs.com/docs/tutorials/findoneandupdate.html * @see findAndModify command https://www.mongodb.com/docs/manual/reference/command/findAndModify/ * @see ModifyResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html @@ -3879,7 +3888,7 @@ Query.prototype._replaceOne = async function _replaceOne() { * - `updateMany()` * * @param {Object} [filter] - * @param {Object|Array} [update] the update command + * @param {Object|Array} [update] the update command. If array, this update will be treated as an update pipeline and not casted. * @param {Object} [options] * @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors. * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) @@ -3887,6 +3896,7 @@ Query.prototype._replaceOne = async function _replaceOne() { * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @param {Function} [callback] params are (error, writeOpResult) * @return {Query} this * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update() @@ -3948,14 +3958,15 @@ Query.prototype.updateMany = function(conditions, doc, options, callback) { * - `updateOne()` * * @param {Object} [filter] - * @param {Object|Array} [update] the update command + * @param {Object|Array} [update] the update command. If array, this update will be treated as an update pipeline and not casted. * @param {Object} [options] * @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors. * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set. - @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @param {Function} [callback] params are (error, writeOpResult) * @return {Query} this * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update() diff --git a/lib/schema.js b/lib/schema.js index 0f45ba4d322..0f829536f29 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1014,6 +1014,9 @@ reserved.collection = 1; Schema.prototype.path = function(path, obj) { if (obj === undefined) { + if (this.paths[path] != null) { + return this.paths[path]; + } // Convert to '.$' to check subpaths re: gh-6405 const cleanPath = _pathToPositionalSyntax(path); let schematype = _getPath(this, path, cleanPath); @@ -1289,6 +1292,7 @@ Schema.prototype.interpretAsType = function(path, obj, options) { return clone; } + // If this schema has an associated Mongoose object, use the Mongoose object's // copy of SchemaTypes re: gh-7158 gh-6933 const MongooseTypes = this.base != null ? this.base.Schema.Types : Schema.Types; @@ -1354,9 +1358,13 @@ Schema.prototype.interpretAsType = function(path, obj, options) { } return new MongooseTypes.DocumentArray(path, cast[options.typeKey], obj, cast); } - - if (Array.isArray(cast)) { - return new MongooseTypes.Array(path, this.interpretAsType(path, cast, options), obj); + if (typeof cast !== 'undefined') { + if (Array.isArray(cast) || cast.type === Array || cast.type == 'Array') { + if (cast && cast.type == 'Array') { + cast.type = Array; + } + return new MongooseTypes.Array(path, this.interpretAsType(path, cast, options), obj); + } } // Handle both `new Schema({ arr: [{ subpath: String }] })` and `new Schema({ arr: [{ type: { subpath: string } }] })` @@ -1407,7 +1415,6 @@ Schema.prototype.interpretAsType = function(path, obj, options) { type = cast[options.typeKey] && (options.typeKey !== 'type' || !cast.type.type) ? cast[options.typeKey] : cast; - if (Array.isArray(type)) { return new MongooseTypes.Array(path, this.interpretAsType(path, type, options), obj); } diff --git a/lib/schemaType.js b/lib/schemaType.js index e3caa4ff033..74d713795d8 100644 --- a/lib/schemaType.js +++ b/lib/schemaType.js @@ -803,6 +803,21 @@ SchemaType.prototype.get = function(fn) { return this; }; +/** + * Adds multiple validators for this document path. + * Calls `validate()` for every element in validators. + * + * @param {Array} validators + * @returns this + */ + +SchemaType.prototype.validateAll = function(validators) { + for (let i = 0; i < validators.length; i++) { + this.validate(validators[i]); + } + return this; +}; + /** * Adds validator(s) for this document path. * @@ -1285,6 +1300,9 @@ SchemaType.prototype.select = function select(val) { SchemaType.prototype.doValidate = function(value, fn, scope, options) { let err = false; const path = this.path; + if (typeof fn !== 'function') { + throw new TypeError(`Must pass callback function to doValidate(), got ${typeof fn}`); + } // Avoid non-object `validators` const validators = this.validators. @@ -1419,7 +1437,6 @@ SchemaType.prototype.doValidateSync = function(value, scope, options) { let i = 0; const len = validators.length; for (i = 0; i < len; ++i) { - const v = validators[i]; if (v === null || typeof v !== 'object') { diff --git a/lib/types/array/methods/index.js b/lib/types/array/methods/index.js index b6829e6993f..b9229e7f76f 100644 --- a/lib/types/array/methods/index.js +++ b/lib/types/array/methods/index.js @@ -698,22 +698,21 @@ const methods = { if ((atomics.$push && atomics.$push.$each && atomics.$push.$each.length || 0) !== 0 && atomics.$push.$position != atomic.$position) { - throw new MongooseError('Cannot call `Array#push()` multiple times ' + - 'with different `$position`'); - } + if (atomic.$position != null) { + [].splice.apply(arr, [atomic.$position, 0].concat(values)); + ret = arr.length; + } else { + ret = [].push.apply(arr, values); + } - if (atomic.$position != null) { + this._registerAtomic('$set', this); + } else if (atomic.$position != null) { [].splice.apply(arr, [atomic.$position, 0].concat(values)); ret = this.length; } else { ret = [].push.apply(arr, values); } } else { - if ((atomics.$push && atomics.$push.$each && atomics.$push.$each.length || 0) !== 0 && - atomics.$push.$position != null) { - throw new MongooseError('Cannot call `Array#push()` multiple times ' + - 'with different `$position`'); - } atomic = values; ret = _basePush.apply(arr, values); } diff --git a/lib/types/arraySubdocument.js b/lib/types/arraySubdocument.js index 94ce5bcc5b4..920088fae76 100644 --- a/lib/types/arraySubdocument.js +++ b/lib/types/arraySubdocument.js @@ -145,7 +145,7 @@ ArraySubdocument.prototype.$__fullPath = function(path, skipIndex) { */ ArraySubdocument.prototype.$__pathRelativeToParent = function(path, skipIndex) { - if (this.__index == null) { + if (this.__index == null || (!this.__parentArray || !this.__parentArray.$path)) { return null; } if (skipIndex) { diff --git a/package.json b/package.json index 19c9cc27aa6..8102651b9b6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "8.2.0", + "version": "8.2.2", "author": "Guillermo Rauch ", "keywords": [ "mongodb", @@ -20,7 +20,7 @@ "license": "MIT", "dependencies": { "bson": "^6.2.0", - "kareem": "2.5.1", + "kareem": "2.6.0", "mongodb": "6.3.0", "mpath": "0.9.0", "mquery": "5.0.0", @@ -28,8 +28,8 @@ "sift": "16.0.1" }, "devDependencies": { - "@babel/core": "7.23.9", - "@babel/preset-env": "7.23.9", + "@babel/core": "7.24.0", + "@babel/preset-env": "7.24.0", "@typescript-eslint/eslint-plugin": "^6.2.1", "@typescript-eslint/parser": "^6.2.1", "acquit": "1.3.0", @@ -43,9 +43,9 @@ "buffer": "^5.6.0", "cheerio": "1.0.0-rc.12", "crypto-browserify": "3.12.0", - "dotenv": "16.4.1", + "dotenv": "16.4.5", "dox": "1.0.0", - "eslint": "8.56.0", + "eslint": "8.57.0", "eslint-plugin-markdown": "^3.0.1", "eslint-plugin-mocha-no-only": "1.1.1", "express": "^4.18.1", @@ -56,7 +56,7 @@ "markdownlint-cli2": "^0.12.1", "marked": "4.3.0", "mkdirp": "^3.0.1", - "mocha": "10.2.0", + "mocha": "10.3.0", "moment": "2.x", "mongodb-memory-server": "8.15.1", "ncp": "^2.0.0", @@ -65,10 +65,10 @@ "q": "1.5.1", "sinon": "17.0.1", "stream-browserify": "3.0.0", - "tsd": "0.30.4", + "tsd": "0.30.7", "typescript": "5.3.3", "uuid": "9.0.1", - "webpack": "5.90.1" + "webpack": "5.90.3" }, "directories": { "lib": "./lib/mongoose" diff --git a/scripts/tsc-diagnostics-check.js b/scripts/tsc-diagnostics-check.js index b00bcbb2438..2f74bf39b92 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]) ? 120000 : parseInt(process.argv[2], 10); +const maxInstantiations = isNaN(process.argv[2]) ? 125000 : parseInt(process.argv[2], 10); console.log(stdin); diff --git a/test/collection.test.js b/test/collection.test.js index 794a41c91bd..755eccbe55a 100644 --- a/test/collection.test.js +++ b/test/collection.test.js @@ -51,6 +51,19 @@ describe('collections:', function() { await db.close(); }); + it('returns a promise if buffering and callback with find() (gh-14184)', function(done) { + db = mongoose.createConnection(); + const collection = db.collection('gh14184'); + collection.opts.bufferTimeoutMS = 100; + + collection.find({ foo: 'bar' }, {}, (err, docs) => { + assert.ok(err); + assert.ok(err.message.includes('buffering timed out after 100ms')); + assert.equal(docs, undefined); + done(); + }); + }); + it('methods should that throw (unimplemented)', function() { const collection = new Collection('test', mongoose.connection); let thrown = false; diff --git a/test/connection.test.js b/test/connection.test.js index 9ea81e356d0..3d1170838cf 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -916,6 +916,21 @@ describe('connections:', function() { assert.equal(err.name, 'MongooseServerSelectionError'); }); + it('avoids unhandled error on createConnection() if error handler registered (gh-14377)', async function() { + const opts = { + serverSelectionTimeoutMS: 100 + }; + const uri = 'mongodb://baddomain:27017/test'; + + const conn = mongoose.createConnection(uri, opts); + await new Promise(resolve => { + conn.on('error', err => { + assert.equal(err.name, 'MongoServerSelectionError'); + resolve(); + }); + }); + }); + it('`watch()` on a whole collection (gh-8425)', async function() { this.timeout(10000); if (!process.env.REPLICA_SET) { diff --git a/test/docs/lean.test.js b/test/docs/lean.test.js index 4512c7ced7f..e571987864b 100644 --- a/test/docs/lean.test.js +++ b/test/docs/lean.test.js @@ -42,7 +42,7 @@ describe('Lean Tutorial', function() { const leanDoc = await MyModel.findOne().lean(); v8Serialize(normalDoc).length; // approximately 180 - v8Serialize(leanDoc).length; // 32, about 5x smaller! + v8Serialize(leanDoc).length; // approximately 55, about 3x smaller! // In case you were wondering, the JSON form of a Mongoose doc is the same // as the POJO. This additional memory only affects how much memory your @@ -50,7 +50,7 @@ describe('Lean Tutorial', function() { JSON.stringify(normalDoc).length === JSON.stringify(leanDoc).length; // true // acquit:ignore:start assert.ok(v8Serialize(normalDoc).length >= 150 && v8Serialize(normalDoc).length <= 200, v8Serialize(normalDoc).length); - assert.equal(v8Serialize(leanDoc).length, 32); + assert.ok(v8Serialize(leanDoc).length === 55 || v8Serialize(leanDoc).length === 32, v8Serialize(leanDoc).length); assert.equal(JSON.stringify(normalDoc).length, JSON.stringify(leanDoc).length); // acquit:ignore:end }); diff --git a/test/docs/transactions.test.js b/test/docs/transactions.test.js index 5d0277a5aa4..8b883e3388c 100644 --- a/test/docs/transactions.test.js +++ b/test/docs/transactions.test.js @@ -477,4 +477,20 @@ describe('transactions', function() { assert.equal(i, 3); }); + + it('doesnt apply schema write concern to transaction operations (gh-11382)', async function() { + db.deleteModel(/Test/); + const Test = db.model('Test', Schema({ status: String }, { writeConcern: { w: 'majority' } })); + + await Test.createCollection(); + await Test.deleteMany({}); + + const session = await db.startSession(); + + await session.withTransaction(async function() { + await Test.findOneAndUpdate({}, { name: 'test' }, { session }); + }); + + await session.endSession(); + }); }); diff --git a/test/document.test.js b/test/document.test.js index 8f67cf1d74e..73361a49574 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -8172,7 +8172,7 @@ describe('document', function() { assert.deepEqual(Object.keys(err.errors), ['age']); }); - it('array push with $position (gh-4322)', async function() { + it('array push with $position (gh-14244) (gh-4322)', async function() { const schema = Schema({ nums: [Number] }); @@ -8196,12 +8196,13 @@ describe('document', function() { $each: [0], $position: 0 }); - assert.throws(() => { - doc.nums.push({ $each: [5] }); - }, /Cannot call.*multiple times/); - assert.throws(() => { - doc.nums.push(5); - }, /Cannot call.*multiple times/); + assert.deepStrictEqual(doc.nums.$__getAtomics(), [['$push', { $each: [0], $position: 0 }]]); + + doc.nums.push({ $each: [5] }); + assert.deepStrictEqual(doc.nums.$__getAtomics(), [['$set', [0, 1, 2, 3, 4, 5]]]); + + doc.nums.push({ $each: [0.5], $position: 1 }); + assert.deepStrictEqual(doc.nums.$__getAtomics(), [['$set', [0, 0.5, 1, 2, 3, 4, 5]]]); }); it('setting a path to a single nested document should update the single nested doc parent (gh-8400)', function() { @@ -12070,6 +12071,27 @@ describe('document', function() { assert.strictEqual(clonedDoc.$session(), session); }); + it('$clone() with single nested and doc array (gh-14353) (gh-11849)', async function() { + const schema = new mongoose.Schema({ + subdocArray: [{ + name: String + }], + subdoc: new mongoose.Schema({ name: String }) + }); + const Test = db.model('Test', schema); + + const item = await Test.create({ subdocArray: [{ name: 'test 1' }], subdoc: { name: 'test 2' } }); + + const doc = await Test.findById(item._id); + const clonedDoc = doc.$clone(); + + assert.ok(clonedDoc.subdocArray[0].$__); + assert.ok(clonedDoc.subdoc.$__); + + assert.deepEqual(doc.subdocArray[0], clonedDoc.subdocArray[0]); + assert.deepEqual(doc.subdoc, clonedDoc.subdoc); + }); + it('can create document with document array and top-level key named `schema` (gh-12480)', async function() { const AuthorSchema = new Schema({ fullName: { type: 'String', required: true } @@ -12791,6 +12813,54 @@ describe('document', function() { await doc2.save(); }); + it('handles embedded recursive discriminators on nested path defined using Schema.prototype.discriminator (gh-14245)', async function() { + const baseSchema = new Schema({ + type: { type: Number, required: true } + }, { discriminatorKey: 'type' }); + + class Base { + whoAmI() { return 'I am Base'; } + } + + baseSchema.loadClass(Base); + + class NumberTyped extends Base { + whoAmI() { return 'I am NumberTyped'; } + } + + class StringTyped extends Base { + whoAmI() { return 'I am StringTyped'; } + } + + const selfRefSchema = new Schema({ + self: { type: [baseSchema], required: true } + }); + + class SelfReferenceTyped extends Base { + whoAmI() { return 'I am SelfReferenceTyped'; } + } + + selfRefSchema.loadClass(SelfReferenceTyped); + baseSchema.discriminator(5, selfRefSchema); + + const numberTypedSchema = new Schema({}).loadClass(NumberTyped); + const stringTypedSchema = new Schema({}).loadClass(StringTyped); + baseSchema.discriminator(1, numberTypedSchema); + baseSchema.discriminator(3, stringTypedSchema); + const containerSchema = new Schema({ items: [baseSchema] }); + const containerModel = db.model('Test', containerSchema); + + const instance = await containerModel.create({ + items: [{ type: 5, self: [{ type: 1 }, { type: 3 }] }] + }); + + assert.equal(instance.items[0].whoAmI(), 'I am SelfReferenceTyped'); + assert.deepStrictEqual(instance.items[0].self.map(item => item.whoAmI()), [ + 'I am NumberTyped', + 'I am StringTyped' + ]); + }); + it('can use `collection` as schema name (gh-13956)', async function() { const schema = new mongoose.Schema({ name: String, collection: String }); const Test = db.model('Test', schema); @@ -12972,6 +13042,51 @@ describe('document', function() { assert.ok(doc.docArr.toString().includes('child'), doc.docArr.toString()); assert.ok(doc.docArr.toString().includes('test child'), doc.docArr.toString()); }); + + it('minimizes when updating existing documents (gh-13782)', async function() { + const schema = new Schema({ + metadata: { + type: {}, + default: {}, + required: true, + _id: false + } + }, { minimize: true }); + const Model = db.model('Test', schema); + const m = new Model({ metadata: {} }); + await m.save(); + + const x = await Model.findById(m._id).exec(); + x.metadata = {}; + await x.save(); + + const { metadata } = await Model.findById(m._id).orFail(); + assert.strictEqual(metadata, null); + }); + + it('saves when setting subdocument to empty object (gh-14420) (gh-13782)', async function() { + const SubSchema = new mongoose.Schema({ + name: { type: String }, + age: Number + }, { _id: false }); + + const MainSchema = new mongoose.Schema({ + sub: { + type: SubSchema + } + }); + + const MainModel = db.model('Test', MainSchema); + + const doc = new MainModel({ sub: { name: 'Hello World', age: 42 } }); + await doc.save(); + + doc.sub = {}; + await doc.save(); + + const savedDoc = await MainModel.findById(doc.id).orFail(); + assert.strictEqual(savedDoc.sub, null); + }); }); describe('Check if instance function that is supplied in schema option is availabe', function() { diff --git a/test/model.test.js b/test/model.test.js index 3f35166e461..76e8341ec1b 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -3681,6 +3681,20 @@ describe('Model', function() { await db.close(); }); + + it('bubbles up resumeTokenChanged events (gh-14349)', async function() { + const MyModel = db.model('Test', new Schema({ name: String })); + + const resumeTokenChangedEvent = new Promise(resolve => { + changeStream = MyModel.watch(); + listener = data => resolve(data); + changeStream.once('resumeTokenChanged', listener); + }); + + await MyModel.create({ name: 'test' }); + const { _data } = await resumeTokenChangedEvent; + assert.ok(_data); + }); }); describe('sessions (gh-6362)', function() { diff --git a/test/query.test.js b/test/query.test.js index 9d1cd201515..94f6911903a 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -795,7 +795,7 @@ describe('Query', function() { e = err; } assert.ok(e, 'uh oh. no error was thrown'); - assert.equal(e.message, 'sort() only takes 1 Argument'); + assert.equal(e.message, 'sort() takes at most 2 arguments'); }); }); @@ -4191,4 +4191,27 @@ describe('Query', function() { assert.strictEqual(doc.account.owner, undefined); assert.strictEqual(doc.account.taxIds, undefined); }); + + it('allows overriding sort (gh-14365)', function() { + const testSchema = new mongoose.Schema({ + name: String + }); + + const Test = db.model('Test', testSchema); + + const q = Test.find().select('name').sort({ name: -1, _id: -1 }); + assert.deepStrictEqual(q.getOptions().sort, { name: -1, _id: -1 }); + + q.sort({ name: 1 }, { override: true }); + assert.deepStrictEqual(q.getOptions().sort, { name: 1 }); + + q.sort(null, { override: true }); + assert.deepStrictEqual(q.getOptions().sort, {}); + + q.sort({ _id: 1 }, { override: true }); + assert.deepStrictEqual(q.getOptions().sort, { _id: 1 }); + + q.sort({}, { override: true }); + assert.deepStrictEqual(q.getOptions().sort, {}); + }); }); diff --git a/test/schema.test.js b/test/schema.test.js index ddf47565347..3efbaed5aa4 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -3202,4 +3202,14 @@ describe('schema', function() { const doc = new baseModel({ type: 1, self: [{ type: 1 }] }); assert.equal(doc.self[0].type, 1); }); + it('should have the correct schema definition with array schemas (gh-14416)', function() { + const schema = new Schema({ + nums: [{ type: Array, of: Number }], + tags: [{ type: 'Array', of: String }], + subdocs: [{ type: Array, of: Schema({ name: String }) }] + }); + assert.equal(schema.path('nums.$').caster.instance, 'Number'); // actually Mixed + assert.equal(schema.path('tags.$').caster.instance, 'String'); // actually Mixed + assert.equal(schema.path('subdocs.$').casterConstructor.schema.path('name').instance, 'String'); // actually Mixed + }); }); diff --git a/test/schematype.test.js b/test/schematype.test.js index 582a135c09c..ad8367d0f61 100644 --- a/test/schematype.test.js +++ b/test/schematype.test.js @@ -281,4 +281,38 @@ describe('schematype', function() { }); }); }); + it('demonstrates the `validateAll()` function (gh-6910)', function() { + const validateSchema = new Schema({ name: String, password: String }); + validateSchema.path('name').validate({ + validator: function(v) { + return v.length > 5; + }, + message: 'name must be longer than 5 characters' + }); + validateSchema.path('password').validateAll([ + { + validator: function(v) { + return this.name !== v; + }, + message: 'password must not equal name' + }, + { + validator: function(v) { + return v.length > 5; + }, + message: 'password must be at least six characters' + } + ]); + assert.equal(validateSchema.path('password').validators.length, 2); + + const passwordPath = validateSchema.path('password'); + assert.throws( + () => { throw passwordPath.doValidateSync('john', { name: 'john' }); }, + /password must not equal name/ + ); + assert.throws( + () => { throw passwordPath.doValidateSync('short', { name: 'john' }); }, + /password must be at least six characters/ + ); + }); }); diff --git a/test/types/PipelineStage.test.ts b/test/types/PipelineStage.test.ts index 967cf28b60d..b53a685cf21 100644 --- a/test/types/PipelineStage.test.ts +++ b/test/types/PipelineStage.test.ts @@ -520,3 +520,20 @@ function gh12269() { } }; } +const vectorSearchStages: PipelineStage[] = [ + { + $vectorSearch: { + index: 'title_vector_index', + path: 'embedding', + queryVector: [0.522, 0.123, 0.487], + limit: 5, + numCandidates: 100 + } + }, + { + $project: { + title: 1, + score: { $meta: 'searchScore' } + } + } +]; diff --git a/test/types/base.test.ts b/test/types/base.test.ts index 1dfaaa2ef70..fba2acf37b0 100644 --- a/test/types/base.test.ts +++ b/test/types/base.test.ts @@ -8,6 +8,15 @@ Object.values(mongoose.models).forEach(model => { mongoose.pluralize(null); +mongoose.overwriteMiddlewareResult('foo'); +const schema = new mongoose.Schema({ name: String }); +schema.pre('save', function() { + return mongoose.skipMiddlewareFunction('foobar'); +}); +schema.post('save', function() { + return mongoose.overwriteMiddlewareResult('foobar'); +}); + function gh10746() { type A = string extends Function ? never : string; diff --git a/test/types/docArray.test.ts b/test/types/docArray.test.ts index c5c9154cde7..cf41df0b82e 100644 --- a/test/types/docArray.test.ts +++ b/test/types/docArray.test.ts @@ -93,3 +93,37 @@ async function gh13424() { const doc = new TestModel(); expectType(doc.subDocArray[0]._id); } + +async function gh14367() { + const UserSchema = new Schema( + { + reminders: { + type: [ + { + type: { type: Schema.Types.String }, + date: { type: Schema.Types.Date }, + toggle: { type: Schema.Types.Boolean }, + notified: { type: Schema.Types.Boolean } + } + ], + default: [ + { type: 'vote', date: new Date(), toggle: false, notified: false }, + { type: 'daily', date: new Date(), toggle: false, notified: false }, + { type: 'drop', date: new Date(), toggle: false, notified: false }, + { type: 'claim', date: new Date(), toggle: false, notified: false }, + { type: 'work', date: new Date(), toggle: false, notified: false } + ] + }, + avatar: { + type: Schema.Types.String + } + }, + { timestamps: true } + ); + + type IUser = InferSchemaType; + expectType({} as IUser['reminders'][0]['type']); + expectType({} as IUser['reminders'][0]['date']); + expectType({} as IUser['reminders'][0]['toggle']); + expectType({} as IUser['avatar']); +} diff --git a/test/types/middleware.test.ts b/test/types/middleware.test.ts index 31e210eb26d..e127c3b683b 100644 --- a/test/types/middleware.test.ts +++ b/test/types/middleware.test.ts @@ -72,7 +72,7 @@ schema.pre>('insertMany', function() { return Promise.resolve(); }); -schema.pre>('insertMany', { document: false, query: false }, function() { +schema.pre>('insertMany', function() { console.log(this.name); }); diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 61298be836d..3e1bd32449f 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -897,6 +897,22 @@ function gh4727() { const company = { _id: new mongoose.Types.ObjectId(), name: 'Booster', users: [users[0]] }; return Company.hydrate(company, {}, { hydratedPopulatedDocs: true }); +} + +async function gh14440() { + const testSchema = new Schema({ + dateProperty: { type: Date } + }); + const TestModel = model('Test', testSchema); + const doc = new TestModel(); + await TestModel.bulkWrite([ + { + updateOne: { + filter: { _id: doc._id }, + update: { dateProperty: (new Date('2023-06-01')).toISOString() } + } + } + ]); } diff --git a/test/types/querycursor.test.ts b/test/types/querycursor.test.ts index dc87e0a669b..f63f38370e1 100644 --- a/test/types/querycursor.test.ts +++ b/test/types/querycursor.test.ts @@ -20,3 +20,29 @@ Test.find().cursor(). expectType(i); }). then(() => console.log('Done!')); + +async function gh14374() { + // `Parent` represents the object as it is stored in MongoDB + interface Parent { + child?: Types.ObjectId + name?: string + } + const ParentModel = model( + 'Parent', + new Schema({ + child: { type: Schema.Types.ObjectId, ref: 'Child' }, + name: String + }) + ); + + interface Child { + name: string + } + const childSchema: Schema = new Schema({ name: String }); + + const cursor = ParentModel.find({}).populate<{ child: Child }>('child').cursor(); + for await (const doc of cursor) { + expectType(doc.child); + } + +} diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index ec5c7ceb3e8..e7631229036 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1059,10 +1059,10 @@ function gh12882() { }); type tArrType = InferSchemaType; expectType<{ - fooArray: { + fooArray: Types.DocumentArray<{ type: string; foo: number; - }[] + }> }>({} as tArrType); // Readonly array of strings const rArrString = new Schema({ @@ -1110,10 +1110,10 @@ function gh12882() { }); type rTArrType = InferSchemaType; expectType<{ - fooArray: { + fooArray: Types.DocumentArray<{ type: string; foo: number; - }[] + }> }>({} as rTArrType); } diff --git a/types/index.d.ts b/types/index.d.ts index 6a5a926c654..92a05249e12 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -27,6 +27,7 @@ declare class NativeDate extends global.Date { } declare module 'mongoose' { + import Kareem = require('kareem'); import events = require('events'); import mongodb = require('mongodb'); import mongoose = require('mongoose'); @@ -390,7 +391,6 @@ declare module 'mongoose' { ): this; // this = Document pre(method: 'save', fn: PreSaveMiddlewareFunction): this; - pre(method: 'save', options: SchemaPreOptions, fn: PreSaveMiddlewareFunction): this; pre(method: MongooseDistinctDocumentMiddleware|MongooseDistinctDocumentMiddleware[], fn: PreMiddlewareFunction): this; pre(method: MongooseDistinctDocumentMiddleware|MongooseDistinctDocumentMiddleware[], options: SchemaPreOptions, fn: PreMiddlewareFunction): this; pre( @@ -408,7 +408,6 @@ declare module 'mongoose' { pre>(method: MongooseQueryOrDocumentMiddleware | MongooseQueryOrDocumentMiddleware[] | RegExp, fn: PreMiddlewareFunction): this; // method aggregate pre>(method: 'aggregate' | RegExp, fn: PreMiddlewareFunction): this; - pre>(method: 'aggregate' | RegExp, options: SchemaPreOptions, fn: PreMiddlewareFunction): this; /* method insertMany */ pre( method: 'insertMany' | RegExp, @@ -419,16 +418,6 @@ declare module 'mongoose' { options?: InsertManyOptions & { lean?: boolean } ) => void | Promise ): this; - pre( - method: 'insertMany' | RegExp, - options: SchemaPreOptions, - fn: ( - this: T, - next: (err?: CallbackError) => void, - docs: any | Array, - options?: InsertManyOptions & { lean?: boolean } - ) => void | Promise - ): this; /* method bulkWrite */ pre( method: 'bulkWrite' | RegExp, @@ -439,16 +428,6 @@ declare module 'mongoose' { options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions ) => void | Promise ): this; - pre( - method: 'bulkWrite' | RegExp, - options: SchemaPreOptions, - fn: ( - this: T, - next: (err?: CallbackError) => void, - ops: Array & MongooseBulkWritePerWriteOptions>, - options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions - ) => void | Promise - ): this; /* method createCollection */ pre( method: 'createCollection' | RegExp, @@ -458,15 +437,6 @@ declare module 'mongoose' { options?: mongodb.CreateCollectionOptions & Pick ) => void | Promise ): this; - pre( - method: 'createCollection' | RegExp, - options: SchemaPreOptions, - fn: ( - this: T, - next: (err?: CallbackError) => void, - options?: mongodb.CreateCollectionOptions & Pick - ) => void | Promise - ): this; /** Object of currently defined query helpers on this schema. */ query: TQueryHelpers; @@ -715,5 +685,9 @@ declare module 'mongoose' { /* for ts-mongoose */ export class mquery { } + export function overwriteMiddlewareResult(val: any): Kareem.OverwriteMiddlewareResult; + + export function skipMiddlewareFunction(val: any): Kareem.SkipWrappedFunction; + export default mongoose; } diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 8f878b312f4..bdd56790a3d 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -229,7 +229,7 @@ type ResolvePathType[] : // 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. - ObtainDocumentType[] : + Types.DocumentArray> : IsSchemaTypeFromBuiltinClass extends true ? ObtainDocumentPathType[] : IsItRecordAndNotAny extends true ? diff --git a/types/middlewares.d.ts b/types/middlewares.d.ts index 9302b9b7d48..8d380ed69d1 100644 --- a/types/middlewares.d.ts +++ b/types/middlewares.d.ts @@ -1,4 +1,5 @@ declare module 'mongoose' { + import Kareem = require('kareem'); type MongooseQueryAndDocumentMiddleware = 'updateOne' | 'deleteOne'; @@ -37,13 +38,13 @@ declare module 'mongoose' { this: ThisType, next: CallbackWithoutResultAndOptionalError, opts?: Record - ) => void | Promise; + ) => void | Promise | Kareem.SkipWrappedFunction; type PreSaveMiddlewareFunction = ( this: ThisType, next: CallbackWithoutResultAndOptionalError, opts: SaveOptions - ) => void | Promise; - type PostMiddlewareFunction = (this: ThisType, res: ResType, next: CallbackWithoutResultAndOptionalError) => void | Promise; + ) => void | Promise | Kareem.SkipWrappedFunction; + type PostMiddlewareFunction = (this: ThisType, res: ResType, next: CallbackWithoutResultAndOptionalError) => void | Promise | Kareem.OverwriteMiddlewareResult; type ErrorHandlingMiddlewareFunction = (this: ThisType, err: NativeError, res: ResType, next: CallbackWithoutResultAndOptionalError) => void; - type ErrorHandlingMiddlewareWithOption = (this: ThisType, err: NativeError, res: ResType | null, next: CallbackWithoutResultAndOptionalError) => void | Promise; + type ErrorHandlingMiddlewareWithOption = (this: ThisType, err: NativeError, res: ResType | null, next: CallbackWithoutResultAndOptionalError) => void | Promise | Kareem.OverwriteMiddlewareResult; } diff --git a/types/models.d.ts b/types/models.d.ts index a77aa25525e..7291e5ca79f 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -156,6 +156,85 @@ declare module 'mongoose' { const Model: Model; + export type AnyBulkWriteOperation = { + insertOne: InsertOneModel; + } | { + replaceOne: ReplaceOneModel; + } | { + updateOne: UpdateOneModel; + } | { + updateMany: UpdateManyModel; + } | { + deleteOne: DeleteOneModel; + } | { + deleteMany: DeleteManyModel; + }; + + export interface InsertOneModel { + document: mongodb.OptionalId + } + + export interface ReplaceOneModel { + /** The filter to limit the replaced document. */ + filter: FilterQuery; + /** The document with which to replace the matched document. */ + replacement: mongodb.WithoutId; + /** Specifies a collation. */ + collation?: mongodb.CollationOptions; + /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ + hint?: mongodb.Hint; + /** When true, creates a new document if no document matches the query. */ + upsert?: boolean; + } + + export interface UpdateOneModel { + /** The filter to limit the updated documents. */ + filter: FilterQuery; + /** A document or pipeline containing update operators. */ + update: UpdateQuery; + /** A set of filters specifying to which array elements an update should apply. */ + arrayFilters?: AnyObject[]; + /** Specifies a collation. */ + collation?: mongodb.CollationOptions; + /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ + hint?: mongodb.Hint; + /** When true, creates a new document if no document matches the query. */ + upsert?: boolean; + } + + export interface UpdateManyModel { + /** The filter to limit the updated documents. */ + filter: FilterQuery; + /** A document or pipeline containing update operators. */ + update: UpdateQuery; + /** A set of filters specifying to which array elements an update should apply. */ + arrayFilters?: AnyObject[]; + /** Specifies a collation. */ + collation?: mongodb.CollationOptions; + /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ + hint?: mongodb.Hint; + /** When true, creates a new document if no document matches the query. */ + upsert?: boolean; + } + + export interface DeleteOneModel { + /** The filter to limit the deleted documents. */ + filter: FilterQuery; + /** Specifies a collation. */ + collation?: mongodb.CollationOptions; + /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ + hint?: mongodb.Hint; + } + + export interface DeleteManyModel { + /** The filter to limit the deleted documents. */ + filter: FilterQuery; + /** Specifies a collation. */ + collation?: mongodb.CollationOptions; + /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ + hint?: mongodb.Hint; + } + /** * Models are fancy constructors compiled from `Schema` definitions. * An instance of a model is called a document. @@ -201,17 +280,11 @@ declare module 'mongoose' { * round trip to the MongoDB server. */ bulkWrite( - writes: Array< - mongodb.AnyBulkWriteOperation< - DocContents extends mongodb.Document ? DocContents : any - > & MongooseBulkWritePerWriteOptions>, + writes: Array>, options: mongodb.BulkWriteOptions & MongooseBulkWriteOptions & { ordered: false } ): Promise; bulkWrite( - writes: Array< - mongodb.AnyBulkWriteOperation< - DocContents extends mongodb.Document ? DocContents : any - > & MongooseBulkWritePerWriteOptions>, + writes: Array>, options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions ): Promise; @@ -228,7 +301,7 @@ declare module 'mongoose' { /** Creates a `countDocuments` query: counts the number of documents that match `filter`. */ countDocuments( filter?: FilterQuery, - options?: (mongodb.CountOptions & Omit, 'lean' | 'timestamps'>) | null + options?: (mongodb.CountOptions & MongooseBaseQueryOptions) | null ): QueryWithHelpers< number, THydratedDocumentType, @@ -266,7 +339,7 @@ declare module 'mongoose' { */ deleteMany( filter?: FilterQuery, - options?: (mongodb.DeleteOptions & Omit, 'lean' | 'timestamps'>) | null + options?: (mongodb.DeleteOptions & MongooseBaseQueryOptions) | null ): QueryWithHelpers< mongodb.DeleteResult, THydratedDocumentType, @@ -291,7 +364,7 @@ declare module 'mongoose' { */ deleteOne( filter?: FilterQuery, - options?: (mongodb.DeleteOptions & Omit, 'lean' | 'timestamps'>) | null + options?: (mongodb.DeleteOptions & MongooseBaseQueryOptions) | null ): QueryWithHelpers< mongodb.DeleteResult, THydratedDocumentType, @@ -743,14 +816,14 @@ declare module 'mongoose' { updateMany( filter?: FilterQuery, update?: UpdateQuery | UpdateWithAggregationPipeline, - options?: (mongodb.UpdateOptions & Omit, 'lean'>) | null + options?: (mongodb.UpdateOptions & MongooseUpdateQueryOptions) | null ): QueryWithHelpers; /** Creates a `updateOne` query: updates the first document that matches `filter` with `update`. */ updateOne( filter?: FilterQuery, update?: UpdateQuery | UpdateWithAggregationPipeline, - options?: (mongodb.UpdateOptions & Omit, 'lean'>) | null + options?: (mongodb.UpdateOptions & MongooseUpdateQueryOptions) | null ): QueryWithHelpers; /** Creates a Query, applies the passed conditions, and returns the Query. */ diff --git a/types/pipelinestage.d.ts b/types/pipelinestage.d.ts index 1e6007cd24e..a48d3d12b0a 100644 --- a/types/pipelinestage.d.ts +++ b/types/pipelinestage.d.ts @@ -36,7 +36,8 @@ declare module 'mongoose' { | PipelineStage.SortByCount | PipelineStage.UnionWith | PipelineStage.Unset - | PipelineStage.Unwind; + | PipelineStage.Unwind + | PipelineStage.VectorSearch; export namespace PipelineStage { export interface AddFields { @@ -308,5 +309,17 @@ declare module 'mongoose' { /** [`$unwind` reference](https://www.mongodb.com/docs/manual/reference/operator/aggregation/unwind/) */ $unwind: string | { path: string; includeArrayIndex?: string; preserveNullAndEmptyArrays?: boolean } } + export interface VectorSearch { + /** [`$vectorSearch` reference](https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/) */ + $vectorSearch: { + index: string, + path: string, + queryVector: number[], + numCandidates: number, + limit: number, + filter?: Expression, + } + } + } } diff --git a/types/query.d.ts b/types/query.d.ts index 0bdda904350..879eb934a93 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -17,25 +17,33 @@ declare module 'mongoose' { */ type FilterQuery = _FilterQuery; - type MongooseQueryOptions = Pick< - QueryOptions, - 'context' | - 'lean' | - 'multipleCastError' | - 'overwriteDiscriminatorKey' | - 'populate' | - 'runValidators' | - 'sanitizeProjection' | - 'sanitizeFilter' | - 'setDefaultsOnInsert' | - 'strict' | - 'strictQuery' | - 'timestamps' | - 'translateAliases' - > & { + type MongooseBaseQueryOptionKeys = + | 'context' + | 'multipleCastError' + | 'overwriteDiscriminatorKey' + | 'populate' + | 'runValidators' + | 'sanitizeProjection' + | 'sanitizeFilter' + | 'setDefaultsOnInsert' + | 'strict' + | 'strictQuery' + | 'translateAliases'; + + type MongooseQueryOptions< + DocType = unknown, + Keys extends keyof QueryOptions = MongooseBaseQueryOptionKeys | 'timestamps' | 'lean' + > = Pick, Keys> & { [other: string]: any; }; + type MongooseBaseQueryOptions = MongooseQueryOptions; + + type MongooseUpdateQueryOptions = MongooseQueryOptions< + DocType, + MongooseBaseQueryOptionKeys | 'timestamps' + >; + type ProjectionFields = { [Key in keyof DocType]?: any } & Record; type QueryWithHelpers = Query & THelpers; @@ -208,7 +216,7 @@ declare module 'mongoose' { * A QueryCursor exposes a Streams3 interface, as well as a `.next()` function. * This is equivalent to calling `.cursor()` with no arguments. */ - [Symbol.asyncIterator](): AsyncIterableIterator; + [Symbol.asyncIterator](): AsyncIterableIterator>; /** Executes the query */ exec(): Promise; @@ -286,7 +294,7 @@ declare module 'mongoose' { * Returns a wrapper around a [mongodb driver cursor](https://mongodb.github.io/node-mongodb-native/4.9/classes/FindCursor.html). * A QueryCursor exposes a Streams3 interface, as well as a `.next()` function. */ - cursor(options?: QueryOptions): Cursor>; + cursor(options?: QueryOptions): Cursor, QueryOptions>; /** * Declare and/or execute this query as a `deleteMany()` operation. Works like @@ -619,6 +627,12 @@ declare module 'mongoose' { QueryOp >; + /** Add pre middleware to this query instance. Doesn't affect other queries. */ + pre(fn: Function): this; + + /** Add post middleware to this query instance. Doesn't affect other queries. */ + post(fn: Function): this; + /** Get/set the current projection (AKA fields). Pass `null` to remove the current projection. */ projection(fields?: ProjectionFields | string): ProjectionFields; projection(fields: null): null; @@ -715,7 +729,10 @@ declare module 'mongoose' { slice(val: number | Array): this; /** Sets the sort order. If an object is passed, values allowed are `asc`, `desc`, `ascending`, `descending`, `1`, and `-1`. */ - sort(arg?: string | { [key: string]: SortOrder | { $meta: any } } | [string, SortOrder][] | undefined | null): this; + sort( + arg?: string | { [key: string]: SortOrder | { $meta: any } } | [string, SortOrder][] | undefined | null, + options?: { override?: boolean } + ): this; /** Sets the tailable option (for use with capped collections). */ tailable(bool?: boolean, opts?: { diff --git a/types/schematypes.d.ts b/types/schematypes.d.ts index 088bc27c598..9cb48343009 100644 --- a/types/schematypes.d.ts +++ b/types/schematypes.d.ts @@ -192,10 +192,14 @@ declare module 'mongoose' { [other: string]: any; } - interface Validator { - message?: string; type?: string; validator?: Function + interface Validator { + message?: string; + type?: string; + validator?: ValidatorFunction; } + type ValidatorFunction = (this: DocType, value: any, validatorProperties?: Validator) => any; + class SchemaType { /** SchemaType constructor */ constructor(path: string, options?: AnyObject, instance?: string); @@ -281,7 +285,10 @@ declare module 'mongoose' { validators: Validator[]; /** Adds validator(s) for this document path. */ - validate(obj: RegExp | ((this: DocType, value: any, validatorProperties?: Validator) => any), errorMsg?: string, type?: string): this; + validate(obj: RegExp | ValidatorFunction | Validator, errorMsg?: string, type?: string): this; + + /** Adds multiple validators for this document path. */ + validateAll(validators: Array | Validator>): this; /** Default options for this SchemaType */ defaultOptions?: Record; diff --git a/types/utility.d.ts b/types/utility.d.ts index 60005c2e8b7..016f2c48b07 100644 --- a/types/utility.d.ts +++ b/types/utility.d.ts @@ -2,6 +2,17 @@ declare module 'mongoose' { type IfAny = 0 extends (1 & IFTYPE) ? THENTYPE : ELSETYPE; type IfUnknown = unknown extends IFTYPE ? THENTYPE : IFTYPE; + /** + * @summary Removes keys from a type + * @description It helps to exclude keys from a type + * @param {T} T A generic type to be checked. + * @param {K} K Keys from T that are to be excluded from the generic type + * @returns T with the keys in K excluded + */ + type ExcludeKeys = { + [P in keyof T as P extends K ? never : P]: T[P]; + }; + type Unpacked = T extends (infer U)[] ? U : T extends ReadonlyArray ? U : T;