diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aef27baae5..b2e9fabefb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +6.10.5 / 2023-04-06 +=================== + * fix(model): execute valid write operations if calling bulkWrite() with ordered: false #13218 #13176 + * fix(array): pass-through all parameters #13202 #13201 [hasezoey](https://github.com/hasezoey) + * fix: improve error message when sorting by empty string #13249 #10182 + * docs: add version support and check version docs #13251 #13193 + 5.13.17 / 2023-04-04 ==================== * fix: backport fix for array filters handling $or and $and #13195 #13192 #10696 [raj-goguardian](https://github.com/raj-goguardian) diff --git a/benchmarks/get.js b/benchmarks/get.js index c0c7dae58be..765569194f3 100644 --- a/benchmarks/get.js +++ b/benchmarks/get.js @@ -1,40 +1,72 @@ 'use strict'; -const get = require('../lib/helpers/get'); - -let obj; - -// Single string -obj = {}; - -let start = Date.now(); -for (let i = 0; i < 10000000; ++i) { - get(obj, 'test', null); -} -console.log('Single string', Date.now() - start); - -// Array of length 1 -obj = {}; -start = Date.now(); -let arr = ['test']; -for (let i = 0; i < 10000000; ++i) { - get(obj, arr, null); -} -console.log('Array of length 1', Date.now() - start); - -// String with dots -obj = { a: { b: 1 } }; -start = Date.now(); -for (let i = 0; i < 10000000; ++i) { - get(obj, 'a.b', null); -} -console.log('String with dots', Date.now() - start); - -// Multi element array -obj = { a: { b: 1 } }; -start = Date.now(); -arr = ['a', 'b']; -for (let i = 0; i < 10000000; ++i) { - get(obj, arr, null); -} -console.log('Multi element array', Date.now() - start); \ No newline at end of file +const mongoose = require('../'); + +run().catch(err => { + console.error(err); + process.exit(-1); +}); + +async function run() { + const schema = new mongoose.Schema({ + field1: {type: String, default: Math.random().toString(36).slice(2, 16)}, + field2: {type: String, default: Math.random().toString(36).slice(2, 16)}, + field3: {type: String, default: Math.random().toString(36).slice(2, 16)}, + field4: {type: String, default: Math.random().toString(36).slice(2, 16)}, + field5: {type: String, default: Math.random().toString(36).slice(2, 16)}, + field6: {type: String, default: Math.random().toString(36).slice(2, 16)}, + field7: {type: String, default: Math.random().toString(36).slice(2, 16)}, + field8: {type: String, default: Math.random().toString(36).slice(2, 16)}, + }, { versionKey: false }); + const TestModel = mongoose.model('Test', schema); + + let tests = []; + for (let i = 0; i < 10000; i++) { + tests.push(new TestModel()); + } + + let loopStart = Date.now(); + + // run loop with mongoose objects + for (let k = 0; k < 100; k++) { + for (let test of tests) { + test.field1; + test.field2; + test.field3; + test.field4; + test.field5; + test.field6; + test.field7; + test.field8; + } + } + + const results = { + 'Model loop ms': Date.now() - loopStart + }; + + const plainTests = []; + for (let test of tests) { + plainTests.push(test.toObject()); + } + + loopStart = Date.now(); + + // run loop with plain objects + for (let k = 0; k < 100; k++) { + for (let test of plainTests) { + test.field1; + test.field2; + test.field3; + test.field4; + test.field5; + test.field6; + test.field7; + test.field8; + } + } + + results['POJO loop ms'] = Date.now() - loopStart; + + console.log(JSON.stringify(results, null, ' ')); +} \ No newline at end of file diff --git a/docs/check-version.md b/docs/check-version.md new file mode 100644 index 00000000000..59c97b8c2bc --- /dev/null +++ b/docs/check-version.md @@ -0,0 +1,34 @@ +# How to Check Your Mongoose Version + +To check what version of Mongoose you are using in Node.js, print out the [`mongoose.version` property](./api/mongoose.html#Mongoose.prototype.version) as follows. + +```javascript +const mongoose = require('mongoose'); + +console.log(mongoose.version); // '7.x.x' +``` + +We recommend printing the Mongoose version from Node.js, because that better handles cases where you have multiple versions of Mongoose installed. +You can also execute the above logic from your terminal using Node.js' `-e` flag as follows. + +``` +# Prints current Mongoose version, e.g. 7.0.3 +node -e "console.log(require('mongoose').version)" +``` + +## Using `npm list` + +You can also [get the installed version of the Mongoose npm package](https://masteringjs.io/tutorials/npm/version) using `npm list`. + +``` +$ npm list mongoose +test@ /path/to/test +└── mongoose@7.0.3 +``` + +`npm list` is helpful because it can identify if you have multiple versions of Mongoose installed. + +Other package managers also support similar functions: + +- [`yarn list --pattern "mongoose"`](https://classic.yarnpkg.com/lang/en/docs/cli/list/) +- [`pnpm list "mongoose"`](https://pnpm.io/cli/list) diff --git a/docs/guides.md b/docs/guides.md index 5362d0b4213..d24aff91797 100644 --- a/docs/guides.md +++ b/docs/guides.md @@ -42,6 +42,10 @@ integrating Mongoose with external tools and frameworks. * [Testing with Jest](jest.html) * [SSL Connections](tutorials/ssl.html) +## Other Guides + +* [How to Check Your Mongoose Version](check-version.html) + ## Migration Guides * [Mongoose 6.x to 7.x](migrating_to_7.html) diff --git a/docs/layout.pug b/docs/layout.pug index 8cd065d7315..3b09f3472fe 100644 --- a/docs/layout.pug +++ b/docs/layout.pug @@ -129,6 +129,8 @@ html(lang='en') a.pure-menu-link(href=`${versions.versionedPath}/docs/migrating_to_7.html`, class=outputUrl === `${versions.versionedPath}/docs/migrating_to_7.html` ? 'selected' : '') Migration Guide li.pure-menu-item a.pure-menu-link(href=`${versions.versionedPath}/docs/compatibility.html`, class=outputUrl === `${versions.versionedPath}/docs/compatibility.html` ? 'selected' : '') Version Compatibility + li.pure-menu-item + a.pure-menu-link(href=`${versions.versionedPath}/docs/version-support.html`, class=outputUrl === `${versions.versionedPath}/docs/version-support.html` ? 'selected' : '') Version Support li.pure-menu-item a.pure-menu-link(href=`${versions.versionedPath}/docs/faq.html`, class=outputUrl === `${versions.versionedPath}/docs/faq.html` ? 'selected' : '') FAQ li.pure-menu-item diff --git a/docs/migrating_to_5.md b/docs/migrating_to_5.md index 37006720568..aa9a3f60748 100644 --- a/docs/migrating_to_5.md +++ b/docs/migrating_to_5.md @@ -1,5 +1,8 @@ # Migrating from 4.x to 5.x +Please note: we plan to discontinue Mongoose 5 support on March 1, 2024. +Please see our [version support guide](./version-support.html). + There are several [backwards-breaking changes](https://github.com/Automattic/mongoose/blob/master/History.md) you should be aware of when migrating from Mongoose 4.x to Mongoose 5.x. diff --git a/docs/migrating_to_6.md b/docs/migrating_to_6.md index ba3c291d037..fe2067fc15d 100644 --- a/docs/migrating_to_6.md +++ b/docs/migrating_to_6.md @@ -6,6 +6,9 @@ } +Please note: we plan to discontinue Mongoose 5 support on March 1, 2024. +Please see our [version support guide](./version-support.html). + There are several [backwards-breaking changes](https://github.com/Automattic/mongoose/blob/master/CHANGELOG.md) you should be aware of when migrating from Mongoose 5.x to Mongoose 6.x. diff --git a/docs/source/index.js b/docs/source/index.js index dff61cb4269..19a75d16aaf 100644 --- a/docs/source/index.js +++ b/docs/source/index.js @@ -94,6 +94,8 @@ docs['docs/jobs.pug'] = { docs['docs/change-streams.md'] = { title: 'MongoDB Change Streams in NodeJS with Mongoose', markdown: true }; docs['docs/lodash.md'] = { title: 'Using Mongoose with Lodash', markdown: true }; docs['docs/incompatible_packages.md'] = { title: 'Known Incompatible npm Packages', markdown: true }; +docs['docs/check-version.md'] = { title: 'How to Check Your Mongoose Version', markdown: true }; +docs['docs/version-support.md'] = { title: 'Version Support', markdown: true }; for (const props of Object.values(docs)) { props.jobs = jobs; diff --git a/docs/version-support.md b/docs/version-support.md new file mode 100644 index 00000000000..037e5b1847a --- /dev/null +++ b/docs/version-support.md @@ -0,0 +1,27 @@ +# Version Support + +Mongoose 7.x (released February 27, 2023) is the current Mongoose major version. +We ship all new bug fixes and features to 7.x. + +## Mongoose 6 + +Mongoose 6.x (released August 24, 2021) is currently in legacy support. +We will continue to ship bug fixes to Mongoose 6 until August 24, 2023. +After August 24, 2023, we will only ship security fixes, and backport requested fixes to Mongoose 6. +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. + +We do not currently have a formal end of life (EOL) date for Mongoose 6. +However, we will not end support for Mongoose 6 until at least January 1, 2024. + +## 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. + +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. \ No newline at end of file diff --git a/lib/document.js b/lib/document.js index aec1402ca56..616104107a3 100644 --- a/lib/document.js +++ b/lib/document.js @@ -15,7 +15,6 @@ const Schema = require('./schema'); const StrictModeError = require('./error/strict'); const ValidationError = require('./error/validation'); const ValidatorError = require('./error/validator'); -const VirtualType = require('./virtualtype'); const $__hasIncludedChildren = require('./helpers/projection/hasIncludedChildren'); const applyDefaults = require('./helpers/document/applyDefaults'); const cleanModifiedSubpaths = require('./helpers/document/cleanModifiedSubpaths'); @@ -722,6 +721,9 @@ Document.prototype.$__init = function(doc, opts) { function init(self, obj, doc, opts, prefix) { prefix = prefix || ''; + if (obj.$__ != null) { + obj = obj._doc; + } const keys = Object.keys(obj); const len = keys.length; let schemaType; @@ -1806,28 +1808,47 @@ Document.prototype.$__setValue = function(path, val) { Document.prototype.get = function(path, type, options) { let adhoc; - options = options || {}; + if (options == null) { + options = {}; + } if (type) { adhoc = this.$__schema.interpretAsType(path, type, this.$__schema.options); } + const noDottedPath = options.noDottedPath; - let schema = this.$__path(path); + // Fast path if we know we're just accessing top-level path on the document: + // just get the schema path, avoid `$__path()` because that does string manipulation + let schema = noDottedPath ? this.$__schema.paths[path] : this.$__path(path); if (schema == null) { schema = this.$__schema.virtualpath(path); + + if (schema != null) { + return schema.applyGetters(void 0, this); + } } - if (schema instanceof MixedSchema) { + + if (noDottedPath) { + let obj = this._doc[path]; + if (adhoc) { + obj = adhoc.cast(obj); + } + if (schema != null && options.getters !== false) { + return schema.applyGetters(obj, this); + } + return obj; + } + + if (schema != null && schema.instance === 'Mixed') { const virtual = this.$__schema.virtualpath(path); if (virtual != null) { schema = virtual; } } - const pieces = path.indexOf('.') === -1 ? [path] : path.split('.'); - let obj = this._doc; - if (schema instanceof VirtualType) { - return schema.applyGetters(void 0, this); - } + const hasDot = path.indexOf('.') !== -1; + let obj = this._doc; + const pieces = hasDot ? path.split('.') : [path]; // Might need to change path for top-level alias if (typeof this.$__schema.aliases[pieces[0]] === 'string') { pieces[0] = this.$__schema.aliases[pieces[0]]; diff --git a/lib/helpers/document/compile.js b/lib/helpers/document/compile.js index fd790295cf0..51721e7a490 100644 --- a/lib/helpers/document/compile.js +++ b/lib/helpers/document/compile.js @@ -25,6 +25,10 @@ const _isEmptyOptions = Object.freeze({ transform: false }); +const noDottedPathGetOptions = Object.freeze({ + noDottedPath: true +}); + /** * Compiles schemas. * @param {Object} tree @@ -65,6 +69,7 @@ function defineKey({ prop, subprops, prototype, prefix, options }) { Document = Document || require('../../document'); const path = (prefix ? prefix + '.' : '') + prop; prefix = prefix || ''; + const useGetOptions = prefix ? Object.freeze({}) : noDottedPathGetOptions; if (subprops) { Object.defineProperty(prototype, prop, { @@ -189,7 +194,12 @@ function defineKey({ prop, subprops, prototype, prefix, options }) { enumerable: true, configurable: true, get: function() { - return this[getSymbol].call(this.$__[scopeSymbol] || this, path); + return this[getSymbol].call( + this.$__[scopeSymbol] || this, + path, + null, + useGetOptions + ); }, set: function(v) { this.$set.call(this.$__[scopeSymbol] || this, path, v); diff --git a/lib/model.js b/lib/model.js index fab3daab5ce..9d8f690fe1b 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3314,25 +3314,78 @@ Model.bulkWrite = async function bulkWrite(ops, options) { throw new MongooseError('Model.bulkWrite() no longer accepts a callback'); } options = options || {}; + const ordered = options.ordered == null ? true : options.ordered; const validations = ops.map(op => castBulkWrite(this, op, options)); return new Promise((resolve, reject) => { - each(validations, (fn, cb) => fn(cb), error => { - if (error) { - return reject(error); - } + if (ordered) { + each(validations, (fn, cb) => fn(cb), error => { + if (error) { + return reject(error); + } - if (ops.length === 0) { - return resolve(getDefaultBulkwriteResult()); - } + if (ops.length === 0) { + return resolve(getDefaultBulkwriteResult()); + } - try { - this.$__collection.bulkWrite(ops, options).then(resolve, reject); - } catch (err) { - return reject(err); - } - }); + try { + this.$__collection.bulkWrite(ops, options, (error, res) => { + if (error) { + return reject(error); + } + + resolve(res); + }); + } catch (err) { + return reject(err); + } + }); + + return; + } + + let remaining = validations.length; + let validOps = []; + let validationErrors = []; + for (let i = 0; i < validations.length; ++i) { + validations[i]((err) => { + if (err == null) { + validOps.push(i); + } else { + validationErrors.push({ index: i, error: err }); + } + if (--remaining <= 0) { + completeUnorderedValidation.call(this); + } + }); + } + + validationErrors = validationErrors. + sort((v1, v2) => v1.index - v2.index). + map(v => v.error); + + function completeUnorderedValidation() { + validOps = validOps.sort().map(index => ops[index]); + + this.$__collection.bulkWrite(validOps, options, (error, res) => { + if (error) { + if (validationErrors.length > 0) { + error.mongoose = error.mongoose || {}; + error.mongoose.validationErrors = validationErrors; + } + + return reject(error); + } + + if (validationErrors.length > 0) { + res.mongoose = res.mongoose || {}; + res.mongoose.validationErrors = validationErrors; + } + + resolve(res); + }); + } }); }; diff --git a/lib/schema.js b/lib/schema.js index 4dae13406e8..8a7a7150e67 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -962,9 +962,9 @@ reserved.collection = 1; */ Schema.prototype.path = function(path, obj) { - // Convert to '.$' to check subpaths re: gh-6405 - const cleanPath = _pathToPositionalSyntax(path); if (obj === undefined) { + // Convert to '.$' to check subpaths re: gh-6405 + const cleanPath = _pathToPositionalSyntax(path); let schematype = _getPath(this, path, cleanPath); if (schematype != null) { return schematype; diff --git a/test/deno.js b/test/deno.js index 6515f93f86a..b41eeaa9df7 100644 --- a/test/deno.js +++ b/test/deno.js @@ -12,6 +12,8 @@ Object.defineProperty(process.stdout, 'getWindowSize', { import { parse } from "https://deno.land/std/flags/mod.ts" const args = parse(Deno.args); +Error.stackTraceLimit = 100; + const require = createRequire(import.meta.url); const Mocha = require('mocha'); diff --git a/test/model.test.js b/test/model.test.js index 8dd0ebace36..bd7e8f86dde 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -4126,6 +4126,69 @@ describe('Model', function() { } }]); }); + + it('sends valid ops if ordered = false (gh-13176)', async function() { + const testSchema = new mongoose.Schema({ + num: Number + }); + const Test = db.model('Test', testSchema); + + const res = await Test.bulkWrite([ + { + updateOne: { + filter: {}, + update: { $set: { num: 'not a number' } }, + upsert: true + } + }, + { + updateOne: { + filter: {}, + update: { $set: { num: 42 } } + } + } + ], { ordered: false }); + assert.ok(res.mongoose); + assert.equal(res.mongoose.validationErrors.length, 1); + assert.strictEqual(res.result.nUpserted, 0); + }); + + it('decorates write error with validation errors if unordered fails (gh-13176)', async function() { + const testSchema = new mongoose.Schema({ + num: Number + }); + const Test = db.model('Test', testSchema); + + await Test.deleteMany({}); + const { _id } = await Test.create({ num: 42 }); + + const err = await Test.bulkWrite([ + { + updateOne: { + filter: {}, + update: { $set: { num: 'not a number' } }, + upsert: true + } + }, + { + updateOne: { + filter: {}, + update: { $push: { num: 42 } } + } + }, + { + updateOne: { + filter: {}, + update: { $inc: { num: 57 } } + } + } + ], { ordered: false }).then(() => null, err => err); + assert.ok(err); + assert.equal(err.mongoose.validationErrors.length, 1); + + const { num } = await Test.findById(_id); + assert.equal(num, 99); + }); }); it('deleteOne with cast error (gh-5323)', async function() { diff --git a/test/query.test.js b/test/query.test.js index 270304f585f..4f04eee5afb 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -4001,4 +4001,14 @@ describe('Query', function() { await Test.findOneAndUpdate({}, { name: 'bar' }); assert.ok(!('projection' in lastOptions)); }); + it('should provide a clearer error message when sorting with empty string', async function() { + const testSchema = new Schema({ + name: { type: String } + }); + + const Error = db.model('error', testSchema); + await assert.rejects(async() => { + await Error.find().sort('-'); + }, { message: 'Invalid field "" passed to sort()' }); + }); }); diff --git a/test/virtualtype.test.js b/test/virtualtype.test.js index 81e43269851..f39dfd6b2ef 100644 --- a/test/virtualtype.test.js +++ b/test/virtualtype.test.js @@ -1,13 +1,13 @@ 'use strict'; -const VirtualType = require('../lib/virtualtype'); const assert = require('assert'); +const start = require('./common'); describe('VirtualType', function() { describe('clone', function() { it('copies path and options correctly (gh-8587)', function() { const opts = { ref: 'User', localField: 'userId', foreignField: '_id' }; - const virtual = new VirtualType(Object.assign({}, opts), 'users'); + const virtual = new start.mongoose.VirtualType(Object.assign({}, opts), 'users'); const clone = virtual.clone(); assert.equal(clone.path, 'users'); diff --git a/types/models.d.ts b/types/models.d.ts index c6bdcd7c19a..11647e7d90a 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -173,7 +173,14 @@ declare module 'mongoose' { * if you use `create()`) because with `bulkWrite()` there is only one network * round trip to the MongoDB server. */ - bulkWrite(writes: Array>, options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions): Promise; + bulkWrite( + writes: Array>, + options: mongodb.BulkWriteOptions & MongooseBulkWriteOptions & { ordered: false } + ): Promise; + bulkWrite( + writes: Array>, + options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions + ): Promise; /** * Sends multiple `save()` calls in a single `bulkWrite()`. This is faster than