Skip to content

Commit

Permalink
Merge pull request #14385 from Automattic/IslandRhythms/gh-9583
Browse files Browse the repository at this point in the history
feat: `pathsToSave` option to `save()` function
  • Loading branch information
vkarpov15 committed Mar 20, 2024
2 parents 1f53635 + aabddf4 commit 276765d
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 5 deletions.
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;
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

0 comments on commit 276765d

Please sign in to comment.