diff --git a/lib/model.js b/lib/model.js index 74d990b68c..1d0a120296 100644 --- a/lib/model.js +++ b/lib/model.js @@ -409,7 +409,8 @@ Model.prototype.$__save = async function $__save(options) { for (const key in delta[1]['$set']) { if (options.pathsToSave.includes(key)) { continue; - } else if (options.pathsToSave.some(pathToSave => key.slice(0, pathToSave.length) === pathToSave && key.charAt(pathToSave.length) === '.')) { + } else if (options.pathsToSave.some(pathToSave => key.slice(0, pathToSave.length) === pathToSave && key.charAt(pathToSave.length) === '.') || + options.pathsToSave.some(pathToSave => pathToSave.slice(0, key.length) === key && pathToSave.charAt(key.length) === '.')) { continue; } else { delete delta[1]['$set'][key]; @@ -631,7 +632,7 @@ Model.prototype.$save = Model.prototype.save; * @instance */ -Model.prototype.$__version = function(where, delta) { +Model.prototype.$__version = function (where, delta) { const key = this.$__schema.options.versionKey; if (where === true) { // this is an insert @@ -740,7 +741,7 @@ Model.prototype.$__where = function _where(where) { Model.prototype.deleteOne = function deleteOne(options) { if (typeof options === 'function' || - typeof arguments[1] === 'function') { + typeof arguments[1] === 'function') { throw new MongooseError('Model.prototype.deleteOne() no longer accepts a callback'); } @@ -911,7 +912,7 @@ Model.exists = function exists(filter, options) { * @api public */ -Model.discriminator = function(name, schema, options) { +Model.discriminator = function (name, schema, options) { let model; if (typeof name === 'function') { model = name; @@ -1042,7 +1043,7 @@ Model.init = function init() { } const conn = this.db; - const _ensureIndexes = async() => { + const _ensureIndexes = async () => { const autoIndex = utils.getOption( 'autoIndex', this.schema.options, @@ -1054,7 +1055,7 @@ Model.init = function init() { } return await this.ensureIndexes({ _automatic: true }); }; - const _createSearchIndexes = async() => { + const _createSearchIndexes = async () => { const autoSearchIndex = utils.getOption( 'autoSearchIndex', this.schema.options, @@ -1067,7 +1068,7 @@ Model.init = function init() { return await this.createSearchIndexes(); }; - const _createCollection = async() => { + const _createCollection = async () => { let autoCreate = utils.getOption( 'autoCreate', this.schema.options, @@ -1099,7 +1100,7 @@ Model.init = function init() { const _catch = this.$init.catch; const _this = this; - this.$init.catch = function() { + this.$init.catch = function () { _this.$caught = true; return _catch.apply(_this.$init, arguments); }; @@ -1620,7 +1621,7 @@ function _ensureIndexes(model, options, callback) { let indexError; options = options || {}; - const done = function(err) { + const done = function (err) { if (err && !model.$caught) { model.emit('error', err); } @@ -1638,24 +1639,24 @@ function _ensureIndexes(model, options, callback) { } if (!indexes.length) { - immediate(function() { + immediate(function () { done(); }); return; } // Indexes are created one-by-one - const indexSingleDone = function(err, fields, options, name) { + const indexSingleDone = function (err, fields, options, name) { model.emit('index-single-done', err, fields, options, name); }; - const indexSingleStart = function(fields, options) { + const indexSingleStart = function (fields, options) { model.emit('index-single-start', fields, options); }; const baseSchema = model.schema._baseSchema; const baseSchemaIndexes = baseSchema ? baseSchema.indexes() : []; - immediate(function() { + immediate(function () { // If buffering is off, do this manually. if (options._automatic && !model.collection.collection) { model.collection.addQueue(create, []); @@ -1668,7 +1669,7 @@ function _ensureIndexes(model, options, callback) { function create() { if (options._automatic) { if (model.schema.options.autoIndex === false || - (model.schema.options.autoIndex == null && model.db.config.autoIndex === false)) { + (model.schema.options.autoIndex == null && model.db.config.autoIndex === false)) { return done(); } } @@ -2326,7 +2327,7 @@ Model.$where = function $where() { * @api public */ -Model.findOneAndUpdate = function(conditions, update, options) { +Model.findOneAndUpdate = function (conditions, update, options) { _checkContext(this, 'findOneAndUpdate'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') { throw new MongooseError('Model.findOneAndUpdate() no longer accepts a callback'); @@ -2413,7 +2414,7 @@ Model.findOneAndUpdate = function(conditions, update, options) { * @api public */ -Model.findByIdAndUpdate = function(id, update, options) { +Model.findByIdAndUpdate = function (id, update, options) { _checkContext(this, 'findByIdAndUpdate'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') { throw new MongooseError('Model.findByIdAndUpdate() no longer accepts a callback'); @@ -2466,7 +2467,7 @@ Model.findByIdAndUpdate = function(id, update, options) { * @api public */ -Model.findOneAndDelete = function(conditions, options) { +Model.findOneAndDelete = function (conditions, options) { _checkContext(this, 'findOneAndDelete'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') { @@ -2503,7 +2504,7 @@ Model.findOneAndDelete = function(conditions, options) { * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ */ -Model.findByIdAndDelete = function(id, options) { +Model.findByIdAndDelete = function (id, options) { _checkContext(this, 'findByIdAndDelete'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') { @@ -2546,7 +2547,7 @@ Model.findByIdAndDelete = function(id, options) { * @api public */ -Model.findOneAndReplace = function(filter, replacement, options) { +Model.findOneAndReplace = function (filter, replacement, options) { _checkContext(this, 'findOneAndReplace'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') { @@ -2597,7 +2598,7 @@ Model.findOneAndReplace = function(filter, replacement, options) { Model.create = async function create(doc, options) { if (typeof options === 'function' || - typeof arguments[2] === 'function') { + typeof arguments[2] === 'function') { throw new MongooseError('Model.create() no longer accepts a callback'); } @@ -2629,12 +2630,12 @@ Model.create = async function create(doc, options) { } if (args.length === 2 && - args[0] != null && - args[1] != null && - args[0].session == null && - last && - getConstructorName(last.session) === 'ClientSession' && - !this.schema.path('session')) { + args[0] != null && + args[1] != null && + args[0].session == null && + last && + getConstructorName(last.session) === 'ClientSession' && + !this.schema.path('session')) { // Probably means the user is running into the common mistake of trying // to use a spread to specify options, see gh-7535 utils.warn('WARNING: to pass a `session` to `Model.create()` in ' + @@ -2664,7 +2665,7 @@ Model.create = async function create(doc, options) { this; if (Model == null) { throw new MongooseError(`Discriminator "${doc[discriminatorKey]}" not ` + - `found for model "${this.modelName}"`); + `found for model "${this.modelName}"`); } let toSave = doc; if (!(toSave instanceof Model)) { @@ -2689,7 +2690,7 @@ Model.create = async function create(doc, options) { this; if (Model == null) { throw new MongooseError(`Discriminator "${doc[discriminatorKey]}" not ` + - `found for model "${this.modelName}"`); + `found for model "${this.modelName}"`); } let toSave = doc; @@ -2710,7 +2711,7 @@ Model.create = async function create(doc, options) { this; if (Model == null) { throw new MongooseError(`Discriminator "${doc[discriminatorKey]}" not ` + - `found for model "${this.modelName}"`); + `found for model "${this.modelName}"`); } try { let toSave = doc; @@ -2817,7 +2818,7 @@ Model.insertOne = async function insertOne(doc, options) { * @api public */ -Model.watch = function(pipeline, options) { +Model.watch = function (pipeline, options) { _checkContext(this, 'watch'); options = options || {}; @@ -2873,7 +2874,7 @@ Model.watch = function(pipeline, options) { * @api public */ -Model.startSession = function() { +Model.startSession = function () { _checkContext(this, 'startSession'); return this.db.startSession.apply(this.db, arguments); @@ -2995,7 +2996,7 @@ Model.insertMany = async function insertMany(arr, options) { } // We filter all failed pre-validations by removing nulls - const docAttributes = docs.filter(function(doc) { + const docAttributes = docs.filter(function (doc) { return doc != null; }); for (let i = 0; i < docAttributes.length; ++i) { @@ -3033,7 +3034,7 @@ Model.insertMany = async function insertMany(arr, options) { } return []; } - const docObjects = lean ? docAttributes : docAttributes.map(function(doc) { + const docObjects = lean ? docAttributes : docAttributes.map(function (doc) { if (doc.$__schema.options.versionKey) { doc[doc.$__schema.options.versionKey] = 0; } @@ -3054,7 +3055,7 @@ Model.insertMany = async function insertMany(arr, options) { // `writeErrors` is a property reported by the MongoDB driver, // just not if there's only 1 error. if (error.writeErrors == null && - (error.result && error.result.result && error.result.result.writeErrors) != null) { + (error.result && error.result.result && error.result.result.writeErrors) != null) { error.writeErrors = error.result.result.writeErrors; } @@ -3281,7 +3282,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) { _checkContext(this, 'bulkWrite'); if (typeof options === 'function' || - typeof arguments[2] === 'function') { + typeof arguments[2] === 'function') { throw new MongooseError('Model.bulkWrite() no longer accepts a callback'); } options = options || {}; @@ -3830,7 +3831,7 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op * @api public */ -Model.hydrate = function(obj, projection, options) { +Model.hydrate = function (obj, projection, options) { _checkContext(this, 'hydrate'); if (options?.virtuals && options?.hydratedPopulatedDocs === false) { @@ -4004,9 +4005,9 @@ function _update(model, op, conditions, doc, options) { options = typeof options === 'function' ? options : clone(options); const versionKey = model && - model.schema && - model.schema.options && - model.schema.options.versionKey || null; + model.schema && + model.schema.options && + model.schema.options.versionKey || null; decorateUpdateWithVersionKey(doc, options, versionKey); return mq[op](conditions, doc, options); @@ -4341,9 +4342,9 @@ async function _populatePath(model, docs, populateOptions) { // to fail. So delay running lean transform until _after_ // `_assign()` if (mod.options && - mod.options.options && - mod.options.options.lean && - mod.options.options.lean.transform) { + mod.options.options && + mod.options.options.lean && + mod.options.options.lean.transform) { mod.options.options._leanTransform = mod.options.options.lean.transform; mod.options.options.lean = true; } @@ -4475,8 +4476,8 @@ function _execPopulateQuery(mod, match, select) { // field, that's the client's fault. for (const foreignField of mod.foreignField) { if (foreignField !== '_id' && - query.selectedInclusively() && - !isPathSelectedInclusive(query._fields, foreignField)) { + query.selectedInclusively() && + !isPathSelectedInclusive(query._fields, foreignField)) { query.select(foreignField); } } @@ -4741,7 +4742,7 @@ Model.compile = function compile(name, schema, collectionName, connection, base) model.$__collection = collection; // Create custom query constructor - model.Query = function() { + model.Query = function () { Query.apply(this, arguments); }; Object.setPrototypeOf(model.Query.prototype, Query.prototype); @@ -4891,7 +4892,7 @@ Model.__subclass = function subclass(conn, schema, collection) { Model.collection = Model.prototype.collection; Model.$__collection = Model.collection; // Errors handled internally, so ignore - Model.init().catch(() => {}); + Model.init().catch(() => { }); return Model; }; @@ -4947,7 +4948,7 @@ Model.recompileSchema = function recompileSchema() { * @api public */ -Model.inspect = function() { +Model.inspect = function () { return `Model { ${this.modelName} }`; }; diff --git a/test/gh-paths-to-save.test.js b/test/gh-paths-to-save.test.js new file mode 100644 index 0000000000..7922e7bd18 --- /dev/null +++ b/test/gh-paths-to-save.test.js @@ -0,0 +1,48 @@ +'use strict'; + +const start = require('./common'); +const assert = require('assert'); +const mongoose = start.mongoose; +const Schema = mongoose.Schema; + +describe('gh-paths-to-save', function () { + let db; + let Model; + + before(function () { + db = start(); + }); + + after(async function () { + await db.close(); + }); + + beforeEach(() => db.deleteModel(/.*/)); + afterEach(() => db.deleteModel(/.*/)); + afterEach(() => db.dropDatabase()); + + it('should allow saving parent paths of whitelisted paths (gh-issue)', async function () { + const schema = new Schema({ + nested: { + a: Number, + b: Number + } + }); + + Model = db.model('Test', schema); + + const doc = new Model({ nested: { a: 1, b: 1 } }); + await doc.save(); + + // Modify the parent path + doc.nested = { a: 2, b: 2 }; + + // Try to save ONLY nested.a + // This should allow the update to nested.a to go through, even if it means saving the whole nested object + await doc.save({ pathsToSave: ['nested.a'] }); + + const found = await Model.findById(doc._id); + assert.strictEqual(found.nested.a, 2); + assert.strictEqual(found.nested.b, 2); + }); +});