Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pathsToSave option to save() function #14385

Merged
merged 10 commits into from Mar 20, 2024
20 changes: 17 additions & 3 deletions lib/document.js
Expand Up @@ -2846,7 +2846,10 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip) {
*/

Document.prototype.$__validate = function(pathsToValidate, options, callback) {
if (typeof pathsToValidate === 'function') {

if (this.$__.saveOptions && this.$__.saveOptions.pathsToSave && !pathsToValidate) {
pathsToValidate = [...this.$__.saveOptions.pathsToSave];
} else if (typeof pathsToValidate === 'function') {
callback = pathsToValidate;
options = null;
pathsToValidate = null;
Expand Down Expand Up @@ -2929,8 +2932,19 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
const validated = {};
let total = 0;

for (const path of paths) {
validatePath(path);
let pathsToSave = this.$__.saveOptions?.pathsToSave;
if (Array.isArray(pathsToSave)) {
pathsToSave = new Set(pathsToSave);
for (const path of paths) {
if (!pathsToSave.has(path)) {
continue;
}
validatePath(path);
}
} else {
for (const path of paths) {
validatePath(path);
}
}

function validatePath(path) {
Expand Down
15 changes: 13 additions & 2 deletions lib/model.js
Expand Up @@ -298,7 +298,6 @@ Model.prototype.$__handleSave = function(options, callback) {
if (!saveOptions.hasOwnProperty('session') && session != null) {
saveOptions.session = session;
}

if (this.$isNew) {
// send entire doc
const obj = this.toObject(saveToObjectOptions);
Expand Down Expand Up @@ -335,6 +334,18 @@ Model.prototype.$__handleSave = function(options, callback) {
// since it already exists
this.$__.inserting = false;
const delta = this.$__delta();

if (options.pathsToSave) {
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) === '.')) {
continue;
} else {
delete delta[1]['$set'][key];
}
}
}
if (delta) {
if (delta instanceof MongooseError) {
callback(delta);
Expand Down Expand Up @@ -521,6 +532,7 @@ function generateVersionError(doc, modifiedPaths) {
* @param {Number} [options.wtimeout] sets a [timeout for the write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern).
* @param {Boolean} [options.checkKeys=true] the MongoDB driver prevents you from saving keys that start with '$' or contain '.' by default. Set this option to `false` to skip that check. See [restrictions on field names](https://docs.mongodb.com/manual/reference/limits/#mongodb-limit-Restrictions-on-Field-Names)
* @param {Boolean} [options.timestamps=true] if `false` and [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this `save()`.
* @param {Array} [options.pathsToSave] An array of paths that tell mongoose to only validate and save the paths in `pathsToSave`.
* @throws {DocumentNotFoundError} if this [save updates an existing document](https://mongoosejs.com/docs/api/document.html#Document.prototype.isNew) but the document doesn't exist in the database. For example, you will get this error if the document is [deleted between when you retrieved the document and when you saved it](documents.html#updating).
* @return {Promise}
* @api public
Expand Down Expand Up @@ -747,7 +759,6 @@ function handleAtomics(self, where, delta, data, value) {

Model.prototype.$__delta = function() {
const dirty = this.$__dirty();

const optimisticConcurrency = this.$__schema.options.optimisticConcurrency;
if (optimisticConcurrency) {
if (Array.isArray(optimisticConcurrency)) {
Expand Down
89 changes: 89 additions & 0 deletions test/model.test.js
Expand Up @@ -2537,6 +2537,95 @@ describe('Model', function() {
assert.ok(!doc.$__.$versionError);
assert.ok(!doc.$__.saveOptions);
});
it('should only save paths specificed in the `pathsToSave` array (gh-9583)', async function() {
const schema = new Schema({ name: String, age: Number, weight: { type: Number, validate: v => v == null || v >= 140 }, location: String });
const Test = db.model('Test', schema);
await Test.create({ name: 'Test Testerson', age: 1, weight: 180, location: 'Florida' });
const doc = await Test.findOne();
doc.name = 'Test';
doc.age = 100;
IslandRhythms marked this conversation as resolved.
Show resolved Hide resolved
doc.weight = 80;
await doc.save({ pathsToSave: ['name'] });
const check = await Test.findOne();
assert.equal(check.name, 'Test');
assert.equal(check.weight, 180);
assert.equal(check.age, 1);
});
it('should have `pathsToSave` work with subdocs (gh-9583)', async function() {
const locationSchema = new Schema({ state: String, city: String, zip: { type: Number, validate: v => v == null || v.toString().length == 5 } });
const schema = new Schema({
name: String,
nickname: String,
age: Number,
weight: { type: Number, validate: v => v == null || v >= 140 },
location: locationSchema
});
const Test = db.model('Test', schema);
await Test.create({ name: 'Test Testerson', nickname: 'test', age: 1, weight: 180, location: { state: 'FL', city: 'Miami', zip: 33330 } });
let doc = await Test.findOne();
doc.name = 'Test';
doc.nickname = 'Test2';
doc.age = 100;
doc.weight = 80;
doc.location.state = 'Ohio';
doc.location.zip = 0;
await doc.save({ pathsToSave: ['name', 'location.state'] });
let check = await Test.findOne();
assert.equal(check.name, 'Test');
assert.equal(check.nickname, 'test');
assert.equal(check.weight, 180);
assert.equal(check.age, 1);
assert.equal(check.location.state, 'Ohio');
assert.equal(check.location.zip, 33330);
check.location = { state: 'Georgia', city: 'Athens', zip: 34512 };
check.name = 'Quiz';
check.age = 50;
await check.save({ pathsToSave: ['name', 'location'] });
const nestedCheck = await Test.findOne();
assert.equal(nestedCheck.location.state, 'Georgia');
assert.equal(nestedCheck.location.city, 'Athens');
assert.equal(nestedCheck.location.zip, 34512);
assert.equal(nestedCheck.name, 'Quiz');

doc = await Test.findOne();
doc.name = 'foobar';
doc.location.city = 'Reynolds';
await doc.save({ pathsToSave: ['location'] });
check = await Test.findById(doc._id);
assert.equal(check.name, 'Quiz');
assert.equal(check.location.city, 'Reynolds');
assert.equal(check.location.state, 'Georgia');
});
it('should have `pathsToSave` work with doc arrays (gh-9583)', async function() {
const locationSchema = new Schema({ state: String, city: String, zip: { type: Number, validate: v => v == null || v.toString().length == 5 } });
const schema = new Schema({ name: String, age: Number, weight: { type: Number, validate: v => v == null || v >= 140 }, location: [locationSchema] });
const Test = db.model('Test', schema);
await Test.create({ name: 'Test Testerson', age: 1, weight: 180, location: [{ state: 'FL', city: 'Miami', zip: 33330 }, { state: 'New York', city: 'Albany', zip: 34567 }] });
const doc = await Test.findOne();
doc.name = 'Test';
doc.age = 100;
doc.weight = 80;
doc.location[0].state = 'Ohio';
doc.location[0].zip = 0;
doc.location[1].state = 'Ohio';
await doc.save({ pathsToSave: ['name', 'location.0.state'] });
const check = await Test.findOne();
assert.equal(check.name, 'Test');
assert.equal(check.weight, 180);
assert.equal(check.age, 1);
assert.equal(check.location[0].state, 'Ohio');
assert.equal(check.location[0].zip, 33330);
assert.equal(check.location[1].state, 'New York');
check.location[0] = { state: 'Georgia', city: 'Athens', zip: 34512 };
check.name = 'Quiz';
check.age = 50;
await check.save({ pathsToSave: ['name', 'location'] });
const nestedCheck = await Test.findOne();
assert.equal(nestedCheck.location[0].state, 'Georgia');
assert.equal(nestedCheck.location[0].city, 'Athens');
assert.equal(nestedCheck.location[0].zip, 34512);
assert.equal(nestedCheck.name, 'Quiz');
});
});


Expand Down