From 0718ad2ce85707dd5a86de72f5c68a017e5eb6e4 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sun, 30 Jan 2022 08:09:01 +0100 Subject: [PATCH 1/4] modify benchmarks --- benchmarks/benchjs/insert.js | 10 +++--- benchmarks/clone.js | 46 +++++++++++++++--------- benchmarks/create.js | 46 +++++++++++++++--------- benchmarks/mem.js | 8 ++--- benchmarks/populate.js | 2 +- benchmarks/validate.js | 70 ++++++++++++++++++++++++++++++++++++ 6 files changed, 139 insertions(+), 43 deletions(-) create mode 100644 benchmarks/validate.js diff --git a/benchmarks/benchjs/insert.js b/benchmarks/benchjs/insert.js index 026e76bb9bb..49b66a039cf 100644 --- a/benchmarks/benchjs/insert.js +++ b/benchmarks/benchjs/insert.js @@ -5,7 +5,7 @@ const Benchmark = require('benchmark'); const suite = new Benchmark.Suite(); const Schema = mongoose.Schema; -const mongo = require('mongodb'); +const mongoClient = require('mongodb').MongoClient; const utils = require('../../lib/utils.js'); const ObjectId = Schema.Types.ObjectId; @@ -20,11 +20,11 @@ const ObjectId = Schema.Types.ObjectId; */ -mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { + mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { if (err) { throw err; } - mongo.connect('mongodb://localhost', function(err, client) { + mongoClient.connect('mongodb://localhost/mongoose-bench', function(err, client) { if (err) { throw err; } @@ -127,7 +127,7 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { defer: true, fn: function(deferred) { const nData = utils.clone(data); - user.insert(nData, function(err) { + user.insertOne(nData, function(err) { if (err) { throw err; } @@ -149,7 +149,7 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { defer: true, fn: function(deferred) { const bp = utils.clone(blogData); - blogpost.insert(bp, function(err) { + blogpost.insertOne(bp, function(err) { if (err) { throw err; } diff --git a/benchmarks/clone.js b/benchmarks/clone.js index 63cf2370fb0..a58e15c0f26 100644 --- a/benchmarks/clone.js +++ b/benchmarks/clone.js @@ -2,14 +2,20 @@ const mongoose = require('../'); const Schema = mongoose.Schema; +const Benchmark = require('benchmark'); const DocSchema = new Schema({ title: String }); +const SimpleSchema = new Schema({ + string: { type: String, required: true }, + number: { type: Number, min: 10 }, +}); + const AllSchema = new Schema({ - string: {type: String, required: true}, - number: {type: Number, min: 10}, + string: { type: String, required: true }, + number: { type: Number, min: 10 }, date: Date, bool: Boolean, buffer: Buffer, @@ -22,11 +28,11 @@ const AllSchema = new Schema({ buffers: [Buffer], objectids: [Schema.ObjectId], docs: { - type: [DocSchema], validate: function() { + type: [DocSchema], validate: function () { return true; } }, - s: {nest: String} + s: { nest: String } }); const A = mongoose.model('A', AllSchema); @@ -44,21 +50,29 @@ const a = new A({ bools: [true, false, false, true, true], buffers: [Buffer.from([33]), Buffer.from([12])], objectids: [new mongoose.Types.ObjectId], - docs: [{title: 'yo'}, {title: 'nowafasdi0fas asjkdfla fa'}], - s: {nest: 'hello there everyone!'} + docs: [{ title: 'yo' }, { title: 'nowafasdi0fas asjkdfla fa' }], + s: { nest: 'hello there everyone!' } }); -const start = new Date; -const total = 100000; -let i = total; -let len; - -for (i = 0, len = total; i < len; ++i) { - a.toObject({depopulate: true}); -} +const Simple = mongoose.model('Simple', SimpleSchema); +const simple = new Simple({ + string: 'hello world', + number: 444848484, +}); -const time = (new Date - start) / 1000; -console.error('took %d seconds for %d docs (%d dps)', time, total, total / time); +new Benchmark.Suite() + .add('Simple', function () { + simple.toObject({ depopulate: true }); + }) + .add('AllSchema', function () { + a.toObject({ depopulate: true }); + }) + .on('cycle', function (evt) { + if (process.env.MONGOOSE_DEV || process.env.PULL_REQUEST) { + console.log(String(evt.target)); + } + }) + .run(); process.memoryUsage(); // --trace-opt --trace-deopt --trace-bailout diff --git a/benchmarks/create.js b/benchmarks/create.js index 3c69c0228c3..b1b534b9bbe 100644 --- a/benchmarks/create.js +++ b/benchmarks/create.js @@ -3,36 +3,48 @@ 'use strict'; const mongoose = require('../../mongoose'); -const fs = require('fs'); +const Benchmark = require('benchmark'); const Schema = mongoose.Schema; -const CheckItem = new Schema({ - name: {type: String}, - type: {type: String}, - pos: {type: Number} +let CheckItem = new Schema({ + name: { type: String }, + type: { type: String }, + pos: { type: Number } }); const Checklist = new Schema({ - name: {type: String}, - checkItems: {type: [CheckItem]} + name: { type: String }, + checkItems: { type: [CheckItem] } }); let Board = new Schema({ - checklists: {type: [Checklist]} + checklists: { type: [Checklist] } }); // const start1 = new Date(); -Board = mongoose.model('Board', Board); +const BoardModel = mongoose.model('Board', Board); +const CheckItemModel = mongoose.model('CheckItem', CheckItem); // const Cl = mongoose.model('Checklist', Checklist); -const doc = JSON.parse(fs.readFileSync(__dirname + '/bigboard.json')); +const doc = require('./bigboard.json'); // const time1 = (new Date - start1); // console.error('reading from disk and parsing JSON took %d ms', time1); -const start2 = new Date(); -const iterations = 1000; -for (let i = 0; i < iterations; ++i) { - new Board(doc); -} -const time2 = (new Date - start2); -console.error('creation of large object took %d ms, %d ms per object', time2, time2 / iterations); +new Benchmark.Suite() + .add('CheckItem', function () { + const test = new CheckItemModel({ + "_id": "4daee8a2aae47fe55305eabf", + "pos": 32768, + "type": "check", + "name": "delete checklists" + }); + }) + .add('Board', function () { + const test = new BoardModel(doc); + }) + .on('cycle', function (evt) { + if (process.env.MONGOOSE_DEV || process.env.PULL_REQUEST) { + console.log(String(evt.target)); + } + }) + .run(); diff --git a/benchmarks/mem.js b/benchmarks/mem.js index 673675d4035..48b425a9534 100644 --- a/benchmarks/mem.js +++ b/benchmarks/mem.js @@ -4,7 +4,7 @@ const mongoose = require('../'); const Schema = mongoose.Schema; -mongoose.connect('localhost', 'testing_bench'); +mongoose.connect('mongodb://localhost/mongoose-bench'); const DocSchema = new Schema({ title: String @@ -57,10 +57,10 @@ methods.push(function(a, cb) { A.where('date', a.date).where('array').in(3).limit(10).exec(cb); }); // 1.82 MB methods.push(function(a, cb) { - A.update({_id: a._id}, {$addToset: {array: 'heeeeello'}}, cb); + A.updateOne({_id: a._id}, {$addToset: {array: 'heeeeello'}}, cb); }); // 3.32 MB methods.push(function(a, cb) { - A.remove({_id: a._id}, cb); + A.deleteOne({_id: a._id}, cb); }); // 3.32 MB methods.push(function(a, cb) { A.find().where('objectids').exists().select('dates').limit(10).exec(cb); @@ -75,7 +75,7 @@ methods.push(function(a, cb) { a.bool = false; a.array.push(3); a.dates.push(new Date); - a.bools.push([true, false]); + // a.bools.push([true, false]); a.docs.addToSet({title: 'woot'}); a.strings.remove('three'); a.numbers.pull(72); diff --git a/benchmarks/populate.js b/benchmarks/populate.js index f4460209dd4..97fedfa17dd 100644 --- a/benchmarks/populate.js +++ b/benchmarks/populate.js @@ -18,7 +18,7 @@ const B = mongoose.model('B', Schema({ let start; let count = 0; -mongoose.connect('localhost', 'benchmark-populate', function(err) { +mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { if (err) { return done(err); } diff --git a/benchmarks/validate.js b/benchmarks/validate.js new file mode 100644 index 00000000000..0aceacfb831 --- /dev/null +++ b/benchmarks/validate.js @@ -0,0 +1,70 @@ +// require('nodetime').profile(); + +'use strict'; + +const mongoose = require('../../mongoose'); +const Benchmark = require('benchmark'); + +const Schema = mongoose.Schema; +const breakfastSchema = new Schema({ + eggs: { + type: Number, + min: [6, 'Too few eggs'], + max: 12 + }, + bacon: { + type: Number, + required: [true, 'Why no bacon?'] + }, + drink: { + type: String, + enum: ['Coffee', 'Tea'], + required: function () { + return this.bacon > 3; + } + } +}); +const Breakfast = mongoose.model('Breakfast', breakfastSchema); +// const time1 = (new Date - start1); +// console.error('reading from disk and parsing JSON took %d ms', time1); +const badBreakfast = new Breakfast({ + eggs: 2, + bacon: 0, + drink: 'Milk' +}); + +const goodBreakfast = new Breakfast({ + eggs: 6, + bacon: 1, + drink: 'Tea' +}) +// const start = new Date; +// const total = 10000000; +// let i = total; +// let len; + +// for (i = 0, len = total; i < len; ++i) { + +// const goodBreakfast = new Breakfast({ +// eggs: 6, +// bacon: 1, +// drink: 'Tea' +// }) +// goodBreakfast.validateSync(); +// } + +// const time = (new Date - start) / 1000; +// console.error('took %d seconds for %d docs (%d dps)', time, total, total / time); +new Benchmark.Suite() +.add('invalid', function () { + badBreakfast.validateSync(); +}) +.add('valid', function () { + goodBreakfast.validateSync(); +}) + .on('cycle', function (evt) { + if (process.env.MONGOOSE_DEV || process.env.PULL_REQUEST) { + console.log(String(evt.target)); + } + }) + .run(); \ No newline at end of file From 090fe4e3618c43321625e726df059ce47077a17f Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sun, 30 Jan 2022 08:29:57 +0100 Subject: [PATCH 2/4] improve performance of validateSync and clone --- lib/document.js | 122 ++++++++++-------- lib/helpers/clone.js | 54 ++++---- lib/helpers/common.js | 4 +- lib/helpers/getFunctionName.js | 10 +- lib/helpers/isMongooseObject.js | 17 +-- lib/helpers/isObject.js | 8 +- lib/helpers/path/parentPaths.js | 15 ++- .../populate/assignRawDocsToIdStructure.js | 6 +- lib/helpers/populate/assignVals.js | 12 +- .../populate/getModelsMapForPopulate.js | 8 +- .../populate/markArraySubdocsPopulated.js | 4 +- lib/helpers/populate/modelNamesFromRefPath.js | 7 +- lib/helpers/schema/getPath.js | 6 +- lib/model.js | 8 +- lib/plugins/trackTransaction.js | 6 +- lib/queryhelpers.js | 2 +- lib/schema/array.js | 18 ++- lib/schema/documentarray.js | 13 +- lib/schematype.js | 57 ++++---- lib/types/ArraySubdocument.js | 3 +- lib/types/DocumentArray/index.js | 35 ++--- .../DocumentArray/isMongooseDocumentArray.js | 3 + lib/types/DocumentArray/methods/index.js | 18 ++- lib/types/array/index.js | 41 +++--- lib/types/array/isMongooseArray.js | 3 + lib/types/array/methods/index.js | 24 ++-- lib/utils.js | 7 + test/helpers/isMongooseObject.test.js | 6 +- 28 files changed, 284 insertions(+), 233 deletions(-) create mode 100644 lib/types/DocumentArray/isMongooseDocumentArray.js create mode 100644 lib/types/array/isMongooseArray.js diff --git a/lib/document.js b/lib/document.js index bde713cc63c..9ba06f3be44 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1135,7 +1135,7 @@ Document.prototype.$set = function $set(path, val, type, options) { // them to ensure we keep the user's key order. if (type === true && !prefix && - path[key] != null && + valForKey != null && pathtype === 'nested' && this._doc[key] != null) { delete this._doc[key]; @@ -1313,7 +1313,7 @@ Document.prototype.$set = function $set(path, val, type, options) { let curPath = ''; for (i = 0; i < parts.length - 1; ++i) { cur = cur[parts[i]]; - curPath += (curPath.length > 0 ? '.' : '') + parts[i]; + curPath += (curPath.length !== 0 ? '.' : '') + parts[i]; if (!cur) { this.$set(curPath, {}); // Hack re: gh-5800. If nested field is not selected, it probably exists @@ -1333,10 +1333,11 @@ Document.prototype.$set = function $set(path, val, type, options) { // When using the $set operator the path to the field must already exist. // Else mongodb throws: "LEFT_SUBFIELD only supports Object" - if (parts.length <= 1) { + if (parts.length < 2) { pathToMark = path; } else { - for (i = 0; i < parts.length; ++i) { + const len = parts.length; + for (i = 0; i < len; ++i) { const subpath = parts.slice(0, i + 1).join('.'); if (this.$get(subpath, null, { getters: false }) === null) { pathToMark = subpath; @@ -1361,7 +1362,7 @@ Document.prototype.$set = function $set(path, val, type, options) { _markValidSubpaths(this, path); } - if (schema.$isSingleNested && val != null && merge) { + if (val != null && merge && schema.$isSingleNested) { if (val instanceof Document) { val = val.toObject({ virtuals: false, transform: false }); } @@ -1431,9 +1432,10 @@ Document.prototype.$set = function $set(path, val, type, options) { val = schema.applySetters(val, this, false, priorVal); } - if (schema.$isMongooseDocumentArray && - Array.isArray(val) && - val.length > 0 && + if (Array.isArray(val) && + !Array.isArray(schema) && + schema.$isMongooseDocumentArray && + val.length !== 0 && val[0] != null && val[0].$__ != null && val[0].$__.populated != null) { @@ -1459,7 +1461,7 @@ Document.prototype.$set = function $set(path, val, type, options) { delete this.$__.populated[path]; } - if (schema.$isSingleNested && val != null) { + if (val != null && schema.$isSingleNested) { _checkImmutableSubpaths(val, schema, priorVal); } @@ -1652,11 +1654,11 @@ Document.prototype.$__set = function(pathToMark, path, options, constructing, pa // handle directly setting arrays (gh-1126) MongooseArray || (MongooseArray = require('./types/array')); - if (val && val.isMongooseArray) { + if (val && utils.isMongooseArray(val)) { val._registerAtomic('$set', val); // Update embedded document parent references (gh-5189) - if (val.isMongooseDocumentArray) { + if (utils.isMongooseDocumentArray(val)) { val.forEach(function(item) { item && item.__parentArray && (item.__parentArray = val); }); @@ -1670,10 +1672,10 @@ Document.prototype.$__set = function(pathToMark, path, options, constructing, pa } }); } - } else if (Array.isArray(val) && val.isMongooseArray && Array.isArray(priorVal) && priorVal.isMongooseArray) { + } else if (Array.isArray(val) && Array.isArray(priorVal) && utils.isMongooseArray(val) && utils.isMongooseArray(priorVal)) { val[arrayAtomicsSymbol] = priorVal[arrayAtomicsSymbol]; val[arrayAtomicsBackupSymbol] = priorVal[arrayAtomicsBackupSymbol]; - if (val.isMongooseDocumentArray) { + if (utils.isMongooseDocumentArray(val)) { val.forEach(doc => { doc.isNew = false; }); } } @@ -1702,7 +1704,7 @@ Document.prototype.$__set = function(pathToMark, path, options, constructing, pa obj = obj[parts[i]]; } else if (obj[parts[i]] && obj[parts[i]] instanceof Embedded) { obj = obj[parts[i]]; - } else if (obj[parts[i]] && obj[parts[i]].$isSingleNested) { + } else if (obj[parts[i]] && !Array.isArray(obj[parts[i]]) && obj[parts[i]].$isSingleNested) { obj = obj[parts[i]]; } else if (obj[parts[i]] && Array.isArray(obj[parts[i]])) { obj = obj[parts[i]]; @@ -1963,7 +1965,7 @@ Document.prototype.$isEmpty = function(path) { transform: false }; - if (arguments.length > 0) { + if (arguments.length !== 0) { const v = this.$get(path); if (v == null) { return true; @@ -2006,51 +2008,60 @@ function _isEmpty(v) { Document.prototype.modifiedPaths = function(options) { options = options || {}; + const directModifiedPaths = Object.keys(this.$__.activePaths.states.modify); - const _this = this; - return directModifiedPaths.reduce(function(list, path) { - const parts = path.split('.'); - list = list.concat(parts.reduce(function(chains, part, i) { - return chains.concat(parts.slice(0, i).concat(part).join('.')); - }, []).filter(function(chain) { - return (list.indexOf(chain) === -1); - })); + const result = new Set(); + + let i = 0; + let j = 0; + const len = directModifiedPaths.length; + + for (i = 0; i < len; ++i) { + const path = directModifiedPaths[i]; + const parts = parentPaths(path); + const pLen = parts.length; + + for (j = 0; j < pLen; ++j) { + result.add(parts[j]); + } if (!options.includeChildren) { - return list; + continue; } - let cur = _this.$get(path); - if (cur != null && typeof cur === 'object') { + let ii = 0; + let cur = this.$get(path); + if (typeof cur === 'object' && cur !== null) { if (cur._doc) { cur = cur._doc; } + const len = cur.length; if (Array.isArray(cur)) { - const len = cur.length; - for (let i = 0; i < len; ++i) { - if (list.indexOf(path + '.' + i) === -1) { - list.push(path + '.' + i); - if (cur[i] != null && cur[i].$__) { - const modified = cur[i].modifiedPaths(); - for (const childPath of modified) { - list.push(path + '.' + i + '.' + childPath); + for (ii = 0; ii < len; ++ii) { + const subPath = path + '.' + ii; + if (!result.has(subPath)) { + result.add(subPath); + if (cur[ii] != null && cur[ii].$__) { + const modified = cur[ii].modifiedPaths(); + let iii = 0; + const iiiLen = modified.length; + for (iii = 0; iii < iiiLen; ++iii) { + result.add(subPath + '.' + modified[iii]); } } } } } else { - Object.keys(cur). - filter(function(key) { - return list.indexOf(path + '.' + key) === -1; - }). - forEach(function(key) { - list.push(path + '.' + key); - }); + const keys = Object.keys(cur); + let ii = 0; + const len = keys.length; + for (ii = 0; ii < len; ++ii) { + result.add(path + '.' + keys[ii]); + } } } - - return list; - }, []); + } + return Array.from(result); }; Document.prototype[documentModifiedPaths] = Document.prototype.modifiedPaths; @@ -2448,7 +2459,12 @@ Document.prototype.$validate = Document.prototype.validate; */ function _evaluateRequiredFunctions(doc) { - Object.keys(doc.$__.activePaths.states.require).forEach(path => { + const requiredFields = Object.keys(doc.$__.activePaths.states.require); + let i = 0; + const len = requiredFields.length; + for (i = 0; i < len; ++i) { + const path = requiredFields[i]; + const p = doc.$__schema.path(path); if (p != null && typeof p.originalRequiredValue === 'function') { @@ -2459,7 +2475,7 @@ function _evaluateRequiredFunctions(doc) { doc.invalidate(path, err); } } - }); + } } /*! @@ -2520,7 +2536,7 @@ function _getPathsToValidate(doc) { // To avoid potential performance issues, skip doc arrays whose children // are not required. `getPositionalPathType()` may be slow, so avoid // it unless we have a case of #6364 - (_pathType.$isMongooseDocumentArray && !get(_pathType, 'schemaOptions.required'))) { + (!Array.isArray(_pathType) && _pathType.$isMongooseDocumentArray && !get(_pathType, 'schemaOptions.required'))) { continue; } @@ -3112,7 +3128,7 @@ Document.prototype.$__reset = function reset() { return _this.$__getValue(i); }) .filter(function(val) { - return val && val instanceof Array && val.isMongooseDocumentArray && val.length; + return val && val instanceof Array && utils.isMongooseDocumentArray(val) && val.length; }) .forEach(function(array) { let i = array.length; @@ -3135,7 +3151,7 @@ Document.prototype.$__reset = function reset() { return _this.$__getValue(i); }). filter(function(val) { - return val && val.$isSingleNested; + return val && !Array.isArray(val) && val.$isSingleNested; }). forEach(function(doc) { doc.$__reset(); @@ -3315,7 +3331,7 @@ Document.prototype.$__getArrayPathsToValidate = function() { return this.$__getValue(i); }.bind(this)) .filter(function(val) { - return val && val instanceof Array && val.isMongooseDocumentArray && val.length; + return val && val instanceof Array && utils.isMongooseDocumentArray(val) && val.length; }).reduce(function(seed, array) { return seed.concat(array); }, []) @@ -3357,12 +3373,12 @@ Document.prototype.$getAllSubdocs = function() { seed = Array.from(val.keys()).reduce(function(seed, path) { return docReducer(val.get(path), seed, null); }, seed); - } else if (val && val.$isSingleNested) { + } else if (val && !Array.isArray(val) && val.$isSingleNested) { seed = Object.keys(val._doc).reduce(function(seed, path) { return docReducer(val._doc, seed, path); }, seed); seed.push(val); - } else if (val && val.isMongooseDocumentArray) { + } else if (val && utils.isMongooseDocumentArray(val)) { val.forEach(function _docReduce(doc) { if (!doc || !doc._doc) { return; @@ -4139,7 +4155,7 @@ Document.prototype.populate = function populate() { const args = [...arguments]; let fn; - if (args.length > 0) { + if (args.length !== 0) { if (typeof args[args.length - 1] === 'function') { fn = args.pop(); } diff --git a/lib/helpers/clone.js b/lib/helpers/clone.js index 4cdd9ecac5f..83b9c2da531 100644 --- a/lib/helpers/clone.js +++ b/lib/helpers/clone.js @@ -34,7 +34,7 @@ function clone(obj, options, isArrayChild) { } if (Array.isArray(obj)) { - return cloneArray(obj, options); + return cloneArray(utils.isMongooseArray(obj) ? obj.__array : obj, options); } if (isMongooseObject(obj)) { @@ -54,12 +54,14 @@ function clone(obj, options, isArrayChild) { return obj.toObject(options); } - if (obj.constructor) { - switch (getFunctionName(obj.constructor)) { + const objConstructor = obj.constructor; + + if (objConstructor) { + switch (getFunctionName(objConstructor)) { case 'Object': return cloneObject(obj, options, isArrayChild); case 'Date': - return new obj.constructor(+obj); + return new objConstructor(+obj); case 'RegExp': return cloneRegExp(obj); default: @@ -79,12 +81,12 @@ function clone(obj, options, isArrayChild) { return Decimal.fromString(obj.toString()); } - if (!obj.constructor && isObject(obj)) { - // object created with Object.create(null) + // object created with Object.create(null) + if (!objConstructor && isObject(obj)) { return cloneObject(obj, options, isArrayChild); } - if (obj[symbols.schemaTypeSymbol]) { + if (typeof obj === 'object' && obj[symbols.schemaTypeSymbol]) { return obj.clone(); } @@ -95,7 +97,7 @@ function clone(obj, options, isArrayChild) { return obj; } - if (obj.valueOf != null) { + if (typeof obj.valueOf === 'function') { return obj.valueOf(); } @@ -112,25 +114,28 @@ function cloneObject(obj, options, isArrayChild) { const ret = {}; let hasKeys; - if (obj[trustedSymbol]) { + if (trustedSymbol in obj) { ret[trustedSymbol] = obj[trustedSymbol]; } - for (const k of Object.keys(obj)) { - if (specialProperties.has(k)) { + let i = 0; + let key = ''; + const keys = Object.keys(obj); + const len = keys.length; + + for (i = 0; i < len; ++i) { + if (specialProperties.has(key = keys[i])) { continue; } // Don't pass `isArrayChild` down - const val = clone(obj[k], options); - - if (!minimize || (typeof val !== 'undefined')) { - if (minimize === false && typeof val === 'undefined') { - delete ret[k]; - } else { - hasKeys || (hasKeys = true); - ret[k] = val; - } + const val = clone(obj[key], options, false); + + if (minimize === false && typeof val === 'undefined') { + delete ret[key]; + } else if (minimize !== true || (typeof val !== 'undefined')) { + hasKeys || (hasKeys = true); + ret[key] = val; } } @@ -138,10 +143,11 @@ function cloneObject(obj, options, isArrayChild) { } function cloneArray(arr, options) { - const ret = []; - - for (const item of arr) { - ret.push(clone(item, options, true)); + let i = 0; + const len = arr.length; + const ret = new Array(len); + for (i = 0; i < len; ++i) { + ret[i] = clone(arr[i], options, true); } return ret; diff --git a/lib/helpers/common.js b/lib/helpers/common.js index 20aafad44dc..98e66aa0a93 100644 --- a/lib/helpers/common.js +++ b/lib/helpers/common.js @@ -88,7 +88,7 @@ function modifiedPaths(update, path, result) { cur += '.' + sp[i]; } } - if (isMongooseObject(val) && !Buffer.isBuffer(val)) { + if (!Buffer.isBuffer(val) && isMongooseObject(val)) { val = val.toObject({ transform: false, virtuals: false }); } if (shouldFlatten(val)) { @@ -108,7 +108,7 @@ function shouldFlatten(val) { typeof val === 'object' && !(val instanceof Date) && !(val instanceof ObjectId) && - (!Array.isArray(val) || val.length > 0) && + (!Array.isArray(val) || val.length !== 0) && !(val instanceof Buffer) && !(val instanceof Decimal128) && !(val instanceof Binary); diff --git a/lib/helpers/getFunctionName.js b/lib/helpers/getFunctionName.js index 87a2c690ab1..d1f3a5a6afe 100644 --- a/lib/helpers/getFunctionName.js +++ b/lib/helpers/getFunctionName.js @@ -1,8 +1,10 @@ 'use strict'; +const functionNameRE = /^function\s*([^\s(]+)/; + module.exports = function(fn) { - if (fn.name) { - return fn.name; - } - return (fn.toString().trim().match(/^function\s*([^\s(]+)/) || [])[1]; + return ( + fn.name || + (fn.toString().trim().match(functionNameRE) || [])[1] + ); }; diff --git a/lib/helpers/isMongooseObject.js b/lib/helpers/isMongooseObject.js index 016f9e65a86..906ef2d6567 100644 --- a/lib/helpers/isMongooseObject.js +++ b/lib/helpers/isMongooseObject.js @@ -1,5 +1,6 @@ 'use strict'; +const isMongooseArray = require('../types/array/isMongooseArray').isMongooseArray /*! * Returns if `v` is a mongoose object that has a `toObject()` method we can use. * @@ -10,12 +11,12 @@ */ module.exports = function(v) { - if (v == null) { - return false; - } - - return v.$__ != null || // Document - v.isMongooseArray || // Array or Document Array - v.isMongooseBuffer || // Buffer - v.$isMongooseMap; // Map + return ( + v != null && ( + isMongooseArray(v) || // Array or Document Array + v.$__ != null || // Document + v.isMongooseBuffer || // Buffer + v.$isMongooseMap // Map + ) + ); }; \ No newline at end of file diff --git a/lib/helpers/isObject.js b/lib/helpers/isObject.js index f8ac31326cb..436aa9c0fc3 100644 --- a/lib/helpers/isObject.js +++ b/lib/helpers/isObject.js @@ -9,8 +9,8 @@ */ module.exports = function(arg) { - if (Buffer.isBuffer(arg)) { - return true; - } - return Object.prototype.toString.call(arg) === '[object Object]'; + return ( + Buffer.isBuffer(arg) || + Object.prototype.toString.call(arg) === '[object Object]' + ); }; \ No newline at end of file diff --git a/lib/helpers/path/parentPaths.js b/lib/helpers/path/parentPaths.js index 77d00ae89bb..24d1bc51990 100644 --- a/lib/helpers/path/parentPaths.js +++ b/lib/helpers/path/parentPaths.js @@ -1,12 +1,17 @@ 'use strict'; +const dotRE = /\./g; module.exports = function parentPaths(path) { - const pieces = path.split('.'); + if (path.indexOf('.') === -1) { + return [path]; + } + const pieces = path.split(dotRE); + const len = pieces.length; + const ret = new Array(len); let cur = ''; - const ret = []; - for (let i = 0; i < pieces.length; ++i) { - cur += (cur.length > 0 ? '.' : '') + pieces[i]; - ret.push(cur); + for (let i = 0; i < len; ++i) { + cur += (cur.length !== 0) ? '.' + pieces[i] : pieces[i]; + ret[i] = cur; } return ret; diff --git a/lib/helpers/populate/assignRawDocsToIdStructure.js b/lib/helpers/populate/assignRawDocsToIdStructure.js index 562b15cf535..ea79808c18b 100644 --- a/lib/helpers/populate/assignRawDocsToIdStructure.js +++ b/lib/helpers/populate/assignRawDocsToIdStructure.js @@ -35,11 +35,13 @@ function assignRawDocsToIdStructure(rawIds, resultDocs, resultOrder, options, re let sid; let id; - if (rawIds.isMongooseArrayProxy) { + if (utils.isMongooseArray(rawIds)) { rawIds = rawIds.__array; } - for (let i = 0; i < rawIds.length; ++i) { + let i = 0; + const len = rawIds.length; + for (i = 0; i < len; ++i) { id = rawIds[i]; if (Array.isArray(id)) { diff --git a/lib/helpers/populate/assignVals.js b/lib/helpers/populate/assignVals.js index f71785a4bd7..e2e5b9cd1df 100644 --- a/lib/helpers/populate/assignVals.js +++ b/lib/helpers/populate/assignVals.js @@ -241,15 +241,19 @@ function valueFilter(val, assignmentOpts, populateOptions, allIds) { } } + const rLen = ret.length; // Since we don't want to have to create a new mongoosearray, make sure to // modify the array in place - while (val.length > ret.length) { + while (val.length > rLen) { Array.prototype.pop.apply(val, []); } - for (let i = 0; i < ret.length; ++i) { - if (val.isMongooseArrayProxy) { + let i = 0; + if (utils.isMongooseArray(val)) { + for (i = 0; i < rLen; ++i) { val.set(i, ret[i], true); - } else { + } + } else { + for (i = 0; i < rLen; ++i) { val[i] = ret[i]; } } diff --git a/lib/helpers/populate/getModelsMapForPopulate.js b/lib/helpers/populate/getModelsMapForPopulate.js index 09bba68313d..c130f8dd4fc 100644 --- a/lib/helpers/populate/getModelsMapForPopulate.js +++ b/lib/helpers/populate/getModelsMapForPopulate.js @@ -43,7 +43,7 @@ module.exports = function getModelsMapForPopulate(model, docs, options) { let allSchemaTypes = getSchemaTypes(model, modelSchema, null, options.path); allSchemaTypes = Array.isArray(allSchemaTypes) ? allSchemaTypes : [allSchemaTypes].filter(v => v != null); - if (allSchemaTypes.length <= 0 && options.strictPopulate !== false && options._localModel != null) { + if (allSchemaTypes.length === 0 && options.strictPopulate !== false && options._localModel != null) { return new MongooseError('Cannot populate path `' + (options._fullPath || options.path) + '` because it is not in your schema. Set the `strictPopulate` option ' + 'to false to override.'); @@ -607,7 +607,7 @@ function _getLocalFieldValues(doc, localField, model, options, virtual, schema) const getters = 'getters' in _populateOptions ? _populateOptions.getters : get(virtual, 'options.getters', false); - if (localFieldGetters.length > 0 && getters) { + if (localFieldGetters.length !== 0 && getters) { const hydratedDoc = (doc.$__ != null) ? doc : model.hydrate(doc); const localFieldValue = utils.getValue(localField, doc); if (Array.isArray(localFieldValue)) { @@ -644,7 +644,7 @@ function convertTo_id(val, schema) { rawVal[i] = rawVal[i]._id; } } - if (val.isMongooseArray && val.$schema()) { + if (utils.isMongooseArray(val) && val.$schema()) { return val.$schema()._castForPopulate(val, val.$parent()); } @@ -694,7 +694,7 @@ function _findRefPathForDiscriminators(doc, modelSchema, data, options, normaliz if (schematype != null && schematype.$isMongooseArray && schematype.caster.discriminators != null && - Object.keys(schematype.caster.discriminators).length > 0) { + Object.keys(schematype.caster.discriminators).length !== 0) { const subdocs = utils.getValue(cur, doc); const remnant = options.path.substring(cur.length + 1); const discriminatorKey = schematype.caster.schema.options.discriminatorKey; diff --git a/lib/helpers/populate/markArraySubdocsPopulated.js b/lib/helpers/populate/markArraySubdocsPopulated.js index fd7a2dd8349..ddc593cd6e4 100644 --- a/lib/helpers/populate/markArraySubdocsPopulated.js +++ b/lib/helpers/populate/markArraySubdocsPopulated.js @@ -1,5 +1,7 @@ 'use strict'; +const utils = require('../../utils'); + /*! * If populating a path within a document array, make sure each * subdoc within the array knows its subpaths are populated. @@ -29,7 +31,7 @@ module.exports = function markArraySubdocsPopulated(doc, populated) { continue; } - if (val.isMongooseDocumentArray) { + if (utils.isMongooseDocumentArray(val)) { for (let j = 0; j < val.length; ++j) { val[j].populated(rest, item._docs[id] == null ? void 0 : item._docs[id][j], item); } diff --git a/lib/helpers/populate/modelNamesFromRefPath.js b/lib/helpers/populate/modelNamesFromRefPath.js index 3770e6a5167..79f0bb6f19e 100644 --- a/lib/helpers/populate/modelNamesFromRefPath.js +++ b/lib/helpers/populate/modelNamesFromRefPath.js @@ -7,6 +7,8 @@ const mpath = require('mpath'); const util = require('util'); const utils = require('../../utils'); +const hasNumericPropRE = /(\.\d+$|\.\d+\.)/g; + module.exports = function modelNamesFromRefPath(refPath, doc, populatedPath, modelSchema, queryProjection) { if (refPath == null) { return []; @@ -20,10 +22,9 @@ module.exports = function modelNamesFromRefPath(refPath, doc, populatedPath, mod // If populated path has numerics, the end `refPath` should too. For example, // if populating `a.0.b` instead of `a.b` and `b` has `refPath = a.c`, we // should return `a.0.c` for the refPath. - const hasNumericProp = /(\.\d+$|\.\d+\.)/g; - if (hasNumericProp.test(populatedPath)) { - const chunks = populatedPath.split(hasNumericProp); + if (hasNumericPropRE.test(populatedPath)) { + const chunks = populatedPath.split(hasNumericPropRE); if (chunks[chunks.length - 1] === '') { throw new Error('Can\'t populate individual element in an array'); diff --git a/lib/helpers/schema/getPath.js b/lib/helpers/schema/getPath.js index ccbc67c81e2..d0f120224d8 100644 --- a/lib/helpers/schema/getPath.js +++ b/lib/helpers/schema/getPath.js @@ -5,6 +5,8 @@ * needing to put `.0.`, so `getPath(schema, 'docArr.elProp')` works. */ +const numberRE = /^\d+$/; + module.exports = function getPath(schema, path) { let schematype = schema.path(path); if (schematype != null) { @@ -16,7 +18,7 @@ module.exports = function getPath(schema, path) { let isArray = false; for (const piece of pieces) { - if (/^\d+$/.test(piece) && isArray) { + if (isArray && numberRE.test(piece)) { continue; } cur = cur.length === 0 ? piece : cur + '.' + piece; @@ -25,7 +27,7 @@ module.exports = function getPath(schema, path) { if (schematype != null && schematype.schema) { schema = schematype.schema; cur = ''; - if (schematype.$isMongooseDocumentArray) { + if (!isArray && schematype.$isMongooseDocumentArray) { isArray = true; } } diff --git a/lib/model.js b/lib/model.js index 3934b9b9f93..538b4e4ebaf 100644 --- a/lib/model.js +++ b/lib/model.js @@ -740,7 +740,7 @@ Model.prototype.$__delta = function() { operand(this, where, delta, data, 1, '$unset'); } else if (value === null) { operand(this, where, delta, data, null); - } else if (value.isMongooseArray && value.$path() && value[arrayAtomicsSymbol]) { + } else if (utils.isMongooseArray(value) && value.$path() && value[arrayAtomicsSymbol]) { // arrays and other custom types (support plugins etc) handleAtomics(this, where, delta, data, value); } else if (value[MongooseBuffer.pathSymbol] && Buffer.isBuffer(value)) { @@ -792,7 +792,7 @@ function checkDivergentArray(doc, path, array) { } } - if (!(pop && array && array.isMongooseArray)) return; + if (!(pop && utils.isMongooseArray(array))) return; // If the array was populated using options that prevented all // documents from being returned (match, skip, limit) or they @@ -3374,7 +3374,7 @@ Model.$__insertMany = function(arr, options, callback) { return doc != null; }); // Quickly escape while there aren't any valid docAttributes - if (docAttributes.length < 1) { + if (docAttributes.length === 0) { if (rawResult) { const res = { mongoose: { @@ -4524,7 +4524,7 @@ function populate(model, docs, options, callback) { } if (!hasOne) { // If models but no docs, skip further deep populate. - if (modelsMap.length > 0) { + if (modelsMap.length !== 0) { return callback(); } // If no models to populate but we have a nested populate, diff --git a/lib/plugins/trackTransaction.js b/lib/plugins/trackTransaction.js index 30ded8785f2..0c0f7d8305d 100644 --- a/lib/plugins/trackTransaction.js +++ b/lib/plugins/trackTransaction.js @@ -47,10 +47,10 @@ function _getAtomics(doc, previous) { const val = doc.$__getValue(path); if (val != null && val instanceof Array && - val.isMongooseDocumentArray && + utils.isMongooseDocumentArray(val) && val.length && val[arrayAtomicsSymbol] != null && - Object.keys(val[arrayAtomicsSymbol]).length > 0) { + Object.keys(val[arrayAtomicsSymbol]).length !== 0) { const existing = previous.get(path) || {}; pathToAtomics.set(path, mergeAtomics(existing, val[arrayAtomicsSymbol])); } @@ -61,7 +61,7 @@ function _getAtomics(doc, previous) { const path = dirt.path; const val = dirt.value; - if (val != null && val[arrayAtomicsSymbol] != null && Object.keys(val[arrayAtomicsSymbol]).length > 0) { + if (val != null && val[arrayAtomicsSymbol] != null && Object.keys(val[arrayAtomicsSymbol]).length !== 0) { const existing = previous.get(path) || {}; pathToAtomics.set(path, mergeAtomics(existing, val[arrayAtomicsSymbol])); } diff --git a/lib/queryhelpers.js b/lib/queryhelpers.js index 8b495aca8eb..0bda6593b65 100644 --- a/lib/queryhelpers.js +++ b/lib/queryhelpers.js @@ -212,7 +212,7 @@ exports.applyPaths = function applyPaths(fields, schema) { let addedPath = analyzePath(path, type); // arrays - if (addedPath == null && type.$isMongooseArray && !type.$isMongooseDocumentArray) { + if (addedPath == null && !Array.isArray(type) && type.$isMongooseArray && !type.$isMongooseDocumentArray) { addedPath = analyzePath(path, type.caster); } if (addedPath != null) { diff --git a/lib/schema/array.js b/lib/schema/array.js index 4114dd37812..15c65035775 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -112,14 +112,12 @@ function SchemaArray(key, cast, options, schemaOptions) { if (!('defaultValue' in this) || this.defaultValue !== void 0) { const defaultFn = function() { - let arr = []; - if (fn) { - arr = defaultArr.call(this); - } else if (defaultArr != null) { - arr = arr.concat(defaultArr); - } // Leave it up to `cast()` to convert the array - return arr; + return fn + ? defaultArr.call(this) + : defaultArr != null + ? [].concat(defaultArr) + : []; }; defaultFn.$runBeforeSetters = !fn; this.default(defaultFn); @@ -275,7 +273,7 @@ SchemaArray.prototype.applyGetters = function(value, scope) { const ret = SchemaType.prototype.applyGetters.call(this, value, scope); if (Array.isArray(ret)) { - const rawValue = ret.isMongooseArrayProxy ? ret.__array : ret; + const rawValue = utils.isMongooseArray(ret) ? ret.__array : ret; const len = rawValue.length; for (let i = 0; i < len; ++i) { rawValue[i] = this.caster.applyGetters(rawValue[i], scope); @@ -299,7 +297,7 @@ SchemaArray.prototype._applySetters = function(value, scope, init, priorVal) { } // No need to wrap empty arrays - if (value != null && value.length > 0) { + if (value != null && value.length !== 0) { const valueDepth = arrayDepth(value); if (valueDepth.min === valueDepth.max && valueDepth.max < depth && valueDepth.containsNonArrayItem) { for (let i = valueDepth.max; i < depth; ++i) { @@ -357,7 +355,7 @@ SchemaArray.prototype.cast = function(value, doc, init, prev, options) { options = options || emptyOpts; - let rawValue = value.isMongooseArrayProxy ? value.__array : value; + let rawValue = utils.isMongooseArray(value) ? value.__array : value; value = MongooseArray(rawValue, options.path || this._arrayPath || this.path, doc, this); rawValue = value.__array; diff --git a/lib/schema/documentarray.js b/lib/schema/documentarray.js index 48fd33b8082..8b6bdbb1151 100644 --- a/lib/schema/documentarray.js +++ b/lib/schema/documentarray.js @@ -228,7 +228,7 @@ DocumentArrayPath.prototype.doValidate = function(array, fn, scope, options) { if (options && options.updateValidator) { return fn(); } - if (!array.isMongooseDocumentArray) { + if (!utils.isMongooseDocumentArray(array)) { array = new MongooseDocumentArray(array, _this.path, scope); } @@ -398,12 +398,9 @@ DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) { return this.cast([value], doc, init, prev, options); } - if (!(value && value.isMongooseDocumentArray) && - !options.skipDocumentArrayCast) { - value = new MongooseDocumentArray(value, this.path, doc); - } else if (value && value.isMongooseDocumentArray) { - // We need to create a new array, otherwise change tracking will - // update the old doc (gh-4449) + // We need to create a new array, otherwise change tracking will + // update the old doc (gh-4449) + if (!options.skipDocumentArrayCast || utils.isMongooseDocumentArray(value)) { value = new MongooseDocumentArray(value, this.path, doc); } @@ -415,7 +412,7 @@ DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) { value[arrayPathSymbol] = this.path + '.' + options.arrayPathIndex; } - const rawArray = value.isMongooseDocumentArrayProxy ? value.__array : value; + const rawArray = utils.isMongooseDocumentArray(value) ? value.__array : value; const len = rawArray.length; const initDocumentOptions = { skipId: true, willInit: true }; diff --git a/lib/schematype.js b/lib/schematype.js index 6cd76ecea7b..86e6d9cfb35 100644 --- a/lib/schematype.js +++ b/lib/schematype.js @@ -1080,7 +1080,7 @@ SchemaType.prototype.getDefault = function(scope, init) { } const casted = this.applySetters(ret, scope, init); - if (casted && casted.$isSingleNested) { + if (casted && !Array.isArray(casted) && casted.$isSingleNested) { casted.$__parent = scope; } return casted; @@ -1283,6 +1283,16 @@ SchemaType.prototype.doValidate = function(value, fn, scope, options) { } }; + +function _validate(ok, validatorProperties) { + if (ok !== undefined && !ok) { + const ErrorConstructor = validatorProperties.ErrorConstructor || ValidatorError; + const err = new ErrorConstructor(validatorProperties); + err[validatorErrorSymbol] = true; + return err; + } +} + /** * Performs a validation of `value` using the validators declared for this SchemaType. * @@ -1306,7 +1316,7 @@ SchemaType.prototype.doValidateSync = function(value, scope, options) { let validators = this.validators; if (value === void 0) { - if (this.validators.length > 0 && this.validators[0].type === 'required') { + if (this.validators.length !== 0 && this.validators[0].type === 'required') { validators = [this.validators[0]]; } else { return null; @@ -1314,34 +1324,35 @@ SchemaType.prototype.doValidateSync = function(value, scope, options) { } let err = null; - validators.forEach(function(v) { - if (err) { - return; - } + let i = 0; + const len = validators.length; + for (i = 0; i < len; ++i ) { + + const v = validators[i]; if (v == null || typeof v !== 'object') { - return; + continue; } const validator = v.validator; - const validatorProperties = utils.clone(v); + const validatorProperties = Object.assign({}, v); validatorProperties.path = options && options.path ? options.path : path; validatorProperties.value = value; - let ok; + let ok = false; // Skip any explicit async validators. Validators that return a promise // will still run, but won't trigger any errors. if (isAsyncFunction(validator)) { - return; + continue; } if (validator instanceof RegExp) { - validate(validator.test(value), validatorProperties); - return; + err = _validate(validator.test(value), validatorProperties); + continue; } if (typeof validator !== 'function') { - return; + continue; } try { @@ -1358,23 +1369,15 @@ SchemaType.prototype.doValidateSync = function(value, scope, options) { // Skip any validators that return a promise, we can't handle those // synchronously if (ok != null && typeof ok.then === 'function') { - return; + continue; } - validate(ok, validatorProperties); - }); - - return err; - - function validate(ok, validatorProperties) { + err = _validate(ok, validatorProperties); if (err) { - return; - } - if (ok !== undefined && !ok) { - const ErrorConstructor = validatorProperties.ErrorConstructor || ValidatorError; - err = new ErrorConstructor(validatorProperties); - err[validatorErrorSymbol] = true; + break; } } + + return err; }; /** @@ -1585,7 +1588,7 @@ SchemaType.prototype._castForQuery = function(val) { */ SchemaType.checkRequired = function(fn) { - if (arguments.length > 0) { + if (arguments.length !== 0) { this._checkRequired = fn; } diff --git a/lib/types/ArraySubdocument.js b/lib/types/ArraySubdocument.js index ee852406d78..70e05fb1187 100644 --- a/lib/types/ArraySubdocument.js +++ b/lib/types/ArraySubdocument.js @@ -6,6 +6,7 @@ const EventEmitter = require('events').EventEmitter; const Subdocument = require('./subdocument'); +const utils = require('../utils'); const documentArrayParent = require('../helpers/symbols').documentArrayParent; @@ -20,7 +21,7 @@ const documentArrayParent = require('../helpers/symbols').documentArrayParent; */ function ArraySubdocument(obj, parentArr, skipId, fields, index) { - if (parentArr != null && parentArr.isMongooseDocumentArray) { + if (utils.isMongooseDocumentArray(parentArr)) { this.__parentArray = parentArr; this[documentArrayParent] = parentArr.$parent(); } else { diff --git a/lib/types/DocumentArray/index.js b/lib/types/DocumentArray/index.js index 6f428ee7d95..95ca9e725a0 100644 --- a/lib/types/DocumentArray/index.js +++ b/lib/types/DocumentArray/index.js @@ -15,7 +15,7 @@ const arrayPathSymbol = require('../../helpers/symbols').arrayPathSymbol; const arraySchemaSymbol = require('../../helpers/symbols').arraySchemaSymbol; const _basePush = Array.prototype.push; - +const numberRE = /^\d+$/; /** * DocumentArray constructor * @@ -29,7 +29,7 @@ const _basePush = Array.prototype.push; */ function MongooseDocumentArray(values, path, doc) { - const arr = []; + const __array = []; const internals = { [arrayAtomicsSymbol]: {}, @@ -45,10 +45,11 @@ function MongooseDocumentArray(values, path, doc) { internals[arrayAtomicsSymbol] = Object.assign({}, values[arrayAtomicsSymbol]); } values.forEach(v => { - _basePush.call(arr, v); + _basePush.call(__array, v); }); } internals[arrayPathSymbol] = path; + internals.__array = __array; // Because doc comes from the context of another function, doc === global // can happen if there was a null somewhere up the chain (see #3020 && #3034) @@ -69,7 +70,7 @@ function MongooseDocumentArray(values, path, doc) { } } - const proxy = new Proxy(arr, { + const proxy = new Proxy(__array, { get: function(target, prop) { if (prop === 'isMongooseArray' || prop === 'isMongooseArrayProxy' || @@ -77,12 +78,6 @@ function MongooseDocumentArray(values, path, doc) { prop === 'isMongooseDocumentArrayProxy') { return true; } - if (prop === '__array') { - return arr; - } - if (prop === 'set') { - return set; - } if (internals.hasOwnProperty(prop)) { return internals[prop]; } @@ -93,15 +88,15 @@ function MongooseDocumentArray(values, path, doc) { return ArrayMethods[prop]; } - return arr[prop]; + return __array[prop]; }, set: function(target, prop, value) { - if (typeof prop === 'string' && /^\d+$/.test(prop)) { - set.call(proxy, prop, value); + if (typeof prop === 'string' && numberRE.test(prop)) { + DocumentArrayMethods.set.call(proxy, prop, value, false); } else if (internals.hasOwnProperty(prop)) { internals[prop] = value; } else { - arr[prop] = value; + __array[prop] = value; } return true; @@ -111,18 +106,6 @@ function MongooseDocumentArray(values, path, doc) { return proxy; } -function set(i, val, skipModified) { - const arr = this.__array; - if (skipModified) { - arr[i] = val; - return arr; - } - const value = DocumentArrayMethods._cast.call(this, val, i); - arr[i] = value; - DocumentArrayMethods._markModified.call(this, i); - return arr; -} - /*! * Module exports. */ diff --git a/lib/types/DocumentArray/isMongooseDocumentArray.js b/lib/types/DocumentArray/isMongooseDocumentArray.js new file mode 100644 index 00000000000..1ad3a917fcb --- /dev/null +++ b/lib/types/DocumentArray/isMongooseDocumentArray.js @@ -0,0 +1,3 @@ +exports.isMongooseDocumentArray = function (mongooseDocumentArray) { + return Array.isArray(mongooseDocumentArray) && mongooseDocumentArray.isMongooseDocumentArray; +} diff --git a/lib/types/DocumentArray/methods/index.js b/lib/types/DocumentArray/methods/index.js index 41061f3cab1..6f5c64b098d 100644 --- a/lib/types/DocumentArray/methods/index.js +++ b/lib/types/DocumentArray/methods/index.js @@ -36,7 +36,7 @@ const methods = { } let Constructor = this[arraySchemaSymbol].casterConstructor; const isInstance = Constructor.$isMongooseDocumentArray ? - value && value.isMongooseDocumentArray : + utils.isMongooseDocumentArray(value) : value instanceof Constructor; if (isInstance || // Hack re: #5001, see #5005 @@ -295,7 +295,7 @@ const methods = { break; } - if (_arr[i].isMongooseArray) { + if (utils.isMongooseArray(_arr[i])) { notify(val, _arr[i]); } else if (_arr[i]) { _arr[i].emit(event, val); @@ -304,6 +304,18 @@ const methods = { }; }, + set(i, val, skipModified) { + const arr = this.__array; + if (skipModified) { + arr[i] = val; + return this; + } + const value = methods._cast.call(this, val, i); + arr[i] = value; + methods._markModified.call(this, i); + return this; + }, + _markModified(elem, embeddedPath) { const parent = this[arrayParentSymbol]; let dirtyPath; @@ -326,7 +338,7 @@ const methods = { return this; } - parent.markModified(dirtyPath, arguments.length > 0 ? elem : parent); + parent.markModified(dirtyPath, arguments.length !== 0 ? elem : parent); } return this; diff --git a/lib/types/array/index.js b/lib/types/array/index.js index 9b71bf400ac..bc4f166c249 100644 --- a/lib/types/array/index.js +++ b/lib/types/array/index.js @@ -27,9 +27,12 @@ const arraySchemaSymbol = require('../../helpers/symbols').arraySchemaSymbol; * @inherits Array * @see http://bit.ly/f6CnZU */ +const _basePush = Array.prototype.push; +const numberRE = /^\d+$/; function MongooseArray(values, path, doc, schematype) { - let arr; + let __array; + if (Array.isArray(values)) { const len = values.length; @@ -38,21 +41,21 @@ function MongooseArray(values, path, doc, schematype) { // modifying the array is faster. Seems small, but adds up when you have a document // with thousands of nested arrays. if (len === 0) { - arr = new Array(); + __array = new Array(); } else if (len === 1) { - arr = new Array(1); - arr[0] = values[0]; + __array = new Array(1); + __array[0] = values[0]; } else if (len < 10000) { - arr = new Array(); - arr.push.apply(arr, values); + __array = new Array(); + _basePush.apply(__array, values); } else { - arr = new Array(); + __array = new Array(); for (let i = 0; i < len; ++i) { - arr.push(values[i]); + _basePush.apply(__array, values[i]); } } } else { - arr = []; + __array = []; } const internals = { @@ -63,10 +66,10 @@ function MongooseArray(values, path, doc, schematype) { [arrayParentSymbol]: void 0, isMongooseArray: true, isMongooseArrayProxy: true, - __array: arr + __array: __array }; - if (values[arrayAtomicsSymbol] != null) { + if (values && values[arrayAtomicsSymbol] != null) { internals[arrayAtomicsSymbol] = values[arrayAtomicsSymbol]; } @@ -79,7 +82,7 @@ function MongooseArray(values, path, doc, schematype) { internals[arraySchemaSymbol] = schematype || doc.schema.path(path); } - const proxy = new Proxy(arr, { + const proxy = new Proxy(__array, { get: function(target, prop) { if (internals.hasOwnProperty(prop)) { return internals[prop]; @@ -88,17 +91,15 @@ function MongooseArray(values, path, doc, schematype) { return mongooseArrayMethods[prop]; } - return arr[prop]; + return __array[prop]; }, - set: function(target, prop, val) { - if (typeof prop === 'string' && /^\d+$/.test(prop)) { - const value = mongooseArrayMethods._cast.call(proxy, val, prop); - arr[prop] = value; - mongooseArrayMethods._markModified.call(proxy, prop); + set: function(target, prop, value) { + if (typeof prop === 'string' && numberRE.test(prop)) { + mongooseArrayMethods.set.call(proxy, prop, value, false); } else if (internals.hasOwnProperty(prop)) { - internals[prop] = val; + internals[prop] = value; } else { - arr[prop] = val; + __array[prop] = value; } return true; diff --git a/lib/types/array/isMongooseArray.js b/lib/types/array/isMongooseArray.js new file mode 100644 index 00000000000..073cc2bb3e1 --- /dev/null +++ b/lib/types/array/isMongooseArray.js @@ -0,0 +1,3 @@ +exports.isMongooseArray = function (mongooseArray) { + return Array.isArray(mongooseArray) && mongooseArray.isMongooseArray; +} diff --git a/lib/types/array/methods/index.js b/lib/types/array/methods/index.js index 154f578d6ab..f699e56bb1f 100644 --- a/lib/types/array/methods/index.js +++ b/lib/types/array/methods/index.js @@ -285,7 +285,7 @@ const methods = { return this; } - parent.markModified(dirtyPath, arguments.length > 0 ? elem : parent); + parent.markModified(dirtyPath, arguments.length !== 0 ? elem : parent); } return this; @@ -393,8 +393,8 @@ const methods = { type = 'date'; } - const rawValues = values.isMongooseArrayProxy ? values.__array : this; - const rawArray = this.isMongooseArrayProxy ? this.__array : this; + const rawValues = utils.isMongooseArray(values) ? values.__array : this; + const rawArray = utils.isMongooseArray(this) ? this.__array : this; rawValues.forEach(function(v) { let found; @@ -520,7 +520,7 @@ const methods = { * * ####Note: * - * _marks the entire array as modified which will pass the entire thing to $set potentially overwritting any changes that happen between when you retrieved the object and when you save it._ + * _marks the entire array as modified which will pass the entire thing to $set potentially overwriting any changes that happen between when you retrieved the object and when you save it._ * * @see MongooseArray#$pop #types_array_MongooseArray-%24pop * @api public @@ -638,7 +638,7 @@ const methods = { let atomic = values; const isOverwrite = values[0] != null && utils.hasUserDefinedProperty(values[0], '$each'); - const arr = this.isMongooseArrayProxy ? this.__array : this; + const arr = utils.isMongooseArray(this) ? this.__array : this; if (isOverwrite) { atomic = values[0]; values = values[0].$each; @@ -761,7 +761,7 @@ const methods = { */ shift() { - const arr = this.isMongooseArrayProxy ? this.__array : this; + const arr = utils.isMongooseArray(this) ? this.__array : this; const ret = [].shift.call(arr); this._registerAtomic('$set', this); this._markModified(); @@ -782,7 +782,7 @@ const methods = { */ sort() { - const arr = this.isMongooseArrayProxy ? this.__array : this; + const arr = utils.isMongooseArray(this) ? this.__array : this; const ret = [].sort.apply(arr, arguments); this._registerAtomic('$set', this); return ret; @@ -803,7 +803,7 @@ const methods = { splice() { let ret; - const arr = this.isMongooseArrayProxy ? this.__array : this; + const arr = utils.isMongooseArray(this) ? this.__array : this; _checkManualPopulation(this, Array.prototype.slice.call(arguments, 2)); @@ -846,7 +846,7 @@ const methods = { */ toObject(options) { - const arr = this.isMongooseArrayProxy ? this.__array : this; + const arr = utils.isMongooseArray(this) ? this.__array : this; if (options && options.depopulate) { options = utils.clone(options); options._isNested = true; @@ -888,7 +888,7 @@ const methods = { values = this[arraySchemaSymbol].applySetters(values, this[arrayParentSymbol]); } - const arr = this.isMongooseArrayProxy ? this.__array : this; + const arr = utils.isMongooseArray(this) ? this.__array : this; [].unshift.apply(arr, values); this._registerAtomic('$set', this); this._markModified(); @@ -928,7 +928,7 @@ function _checkManualPopulation(arr, docs) { null : get(arr[arraySchemaSymbol], 'caster.options.ref', null); if (arr.length === 0 && - docs.length > 0) { + docs.length !== 0) { if (_isAllSubdocs(docs, ref)) { arr[arrayParentSymbol].$populated(arr[arrayPathSymbol], [], { [populateModelSymbol]: docs[0].constructor @@ -950,7 +950,7 @@ for (const method of returnVanillaArrayMethods) { } methods[method] = function() { - const _arr = this.isMongooseArrayProxy ? this.__array : this; + const _arr = utils.isMongooseArray(this) ? this.__array : this; const arr = [].concat(_arr); return arr[method].apply(arr, arguments); diff --git a/lib/utils.js b/lib/utils.js index e1c72d1b1c0..e0e8aa56a36 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -12,6 +12,8 @@ const PopulateOptions = require('./options/PopulateOptions'); const clone = require('./helpers/clone'); const immediate = require('./helpers/immediate'); const isObject = require('./helpers/isObject'); +const isMongooseArray = require('./types/array/isMongooseArray'); +const isMongooseDocumentArray = require('./types/DocumentArray/isMongooseDocumentArray'); const isBsonType = require('./helpers/isBsonType'); const getFunctionName = require('./helpers/getFunctionName'); const isMongooseObject = require('./helpers/isMongooseObject'); @@ -24,6 +26,11 @@ let Document; exports.specialProperties = specialProperties; +exports.isMongooseArray = isMongooseArray.isMongooseArray; +exports.isMongooseDocumentArray = isMongooseDocumentArray.isMongooseDocumentArray; +exports.registerMongooseArray = isMongooseArray.registerMongooseArray; +exports.registerMongooseDocumentArray = isMongooseDocumentArray.registerMongooseDocumentArray; + /*! * Produces a collection name from model `name`. By default, just returns * the model name diff --git a/test/helpers/isMongooseObject.test.js b/test/helpers/isMongooseObject.test.js index db8babd3af8..d442b59af2e 100644 --- a/test/helpers/isMongooseObject.test.js +++ b/test/helpers/isMongooseObject.test.js @@ -2,14 +2,16 @@ const assert = require('assert'); const isMongooseObject = require('../../lib/helpers/isMongooseObject'); +const MongooseArray = require('../../lib/types/array'); describe('isMongooseObject', () => { it('is when value.$__ != null', () => { assert.ok(isMongooseObject({ $__: !null })); }); - it('is when value.isMongooseArray is truthy', () => { - assert.ok(isMongooseObject({ isMongooseArray: true })); + it('is when value is a MongooseArray', () => { + const mongooseArray = new MongooseArray() + assert.ok(isMongooseObject(mongooseArray)); }); it('is when value.isMongooseBuffer is truthy', () => { From 49b5cc226563040e00e27e8ae94aacef70f896ab Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sun, 30 Jan 2022 08:39:05 +0100 Subject: [PATCH 3/4] fix linting --- lib/helpers/isMongooseObject.js | 2 +- lib/plugins/trackTransaction.js | 1 + lib/schema/array.js | 8 ++++---- lib/schematype.js | 2 +- lib/types/DocumentArray/isMongooseDocumentArray.js | 8 +++++--- lib/types/array/isMongooseArray.js | 8 +++++--- test/helpers/isMongooseObject.test.js | 2 +- 7 files changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/helpers/isMongooseObject.js b/lib/helpers/isMongooseObject.js index 906ef2d6567..d82866c6bb6 100644 --- a/lib/helpers/isMongooseObject.js +++ b/lib/helpers/isMongooseObject.js @@ -1,6 +1,6 @@ 'use strict'; -const isMongooseArray = require('../types/array/isMongooseArray').isMongooseArray +const isMongooseArray = require('../types/array/isMongooseArray').isMongooseArray; /*! * Returns if `v` is a mongoose object that has a `toObject()` method we can use. * diff --git a/lib/plugins/trackTransaction.js b/lib/plugins/trackTransaction.js index 0c0f7d8305d..5a4b13f85f5 100644 --- a/lib/plugins/trackTransaction.js +++ b/lib/plugins/trackTransaction.js @@ -2,6 +2,7 @@ const arrayAtomicsSymbol = require('../helpers/symbols').arrayAtomicsSymbol; const sessionNewDocuments = require('../helpers/symbols').sessionNewDocuments; +const utils = require('../utils'); module.exports = function trackTransaction(schema) { schema.pre('save', function() { diff --git a/lib/schema/array.js b/lib/schema/array.js index 15c65035775..36a11239dec 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -114,10 +114,10 @@ function SchemaArray(key, cast, options, schemaOptions) { const defaultFn = function() { // Leave it up to `cast()` to convert the array return fn - ? defaultArr.call(this) - : defaultArr != null - ? [].concat(defaultArr) - : []; + ? defaultArr.call(this) + : defaultArr != null + ? [].concat(defaultArr) + : []; }; defaultFn.$runBeforeSetters = !fn; this.default(defaultFn); diff --git a/lib/schematype.js b/lib/schematype.js index 86e6d9cfb35..10a391f46a5 100644 --- a/lib/schematype.js +++ b/lib/schematype.js @@ -1326,7 +1326,7 @@ SchemaType.prototype.doValidateSync = function(value, scope, options) { let err = null; let i = 0; const len = validators.length; - for (i = 0; i < len; ++i ) { + for (i = 0; i < len; ++i) { const v = validators[i]; diff --git a/lib/types/DocumentArray/isMongooseDocumentArray.js b/lib/types/DocumentArray/isMongooseDocumentArray.js index 1ad3a917fcb..6e6a1690712 100644 --- a/lib/types/DocumentArray/isMongooseDocumentArray.js +++ b/lib/types/DocumentArray/isMongooseDocumentArray.js @@ -1,3 +1,5 @@ -exports.isMongooseDocumentArray = function (mongooseDocumentArray) { - return Array.isArray(mongooseDocumentArray) && mongooseDocumentArray.isMongooseDocumentArray; -} +'use strict'; + +exports.isMongooseDocumentArray = function(mongooseDocumentArray) { + return Array.isArray(mongooseDocumentArray) && mongooseDocumentArray.isMongooseDocumentArray; +}; diff --git a/lib/types/array/isMongooseArray.js b/lib/types/array/isMongooseArray.js index 073cc2bb3e1..89326136b42 100644 --- a/lib/types/array/isMongooseArray.js +++ b/lib/types/array/isMongooseArray.js @@ -1,3 +1,5 @@ -exports.isMongooseArray = function (mongooseArray) { - return Array.isArray(mongooseArray) && mongooseArray.isMongooseArray; -} +'use strict'; + +exports.isMongooseArray = function(mongooseArray) { + return Array.isArray(mongooseArray) && mongooseArray.isMongooseArray; +}; diff --git a/test/helpers/isMongooseObject.test.js b/test/helpers/isMongooseObject.test.js index d442b59af2e..1fc42676ff5 100644 --- a/test/helpers/isMongooseObject.test.js +++ b/test/helpers/isMongooseObject.test.js @@ -10,7 +10,7 @@ describe('isMongooseObject', () => { }); it('is when value is a MongooseArray', () => { - const mongooseArray = new MongooseArray() + const mongooseArray = new MongooseArray(); assert.ok(isMongooseObject(mongooseArray)); }); From 9a4b58b8ecd9899a5dce24107c1c38d98ce67bdb Mon Sep 17 00:00:00 2001 From: Uzlopak Date: Fri, 4 Feb 2022 03:43:19 +0100 Subject: [PATCH 4/4] Update lib/document.js Co-authored-by: Valeri Karpov --- lib/document.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/document.js b/lib/document.js index 9ba06f3be44..86c0f6a76e1 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1333,7 +1333,7 @@ Document.prototype.$set = function $set(path, val, type, options) { // When using the $set operator the path to the field must already exist. // Else mongodb throws: "LEFT_SUBFIELD only supports Object" - if (parts.length < 2) { + if (parts.length <= 1) { pathToMark = path; } else { const len = parts.length;