Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 48 additions & 47 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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');
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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);
};
Expand Down Expand Up @@ -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);
}
Expand All @@ -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, []);
Expand All @@ -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();
}
}
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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');
}

Expand Down Expand Up @@ -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 ' +
Expand Down Expand Up @@ -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)) {
Expand All @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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 || {};
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}

Expand Down Expand Up @@ -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 || {};
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
};

Expand Down Expand Up @@ -4947,7 +4948,7 @@ Model.recompileSchema = function recompileSchema() {
* @api public
*/

Model.inspect = function() {
Model.inspect = function () {
return `Model { ${this.modelName} }`;
};

Expand Down
48 changes: 48 additions & 0 deletions test/gh-paths-to-save.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});