Skip to content

Commit

Permalink
BREAKING CHANGE: overwrite instead of merging when setting nested paths
Browse files Browse the repository at this point in the history
Fix #9121
  • Loading branch information
vkarpov15 committed Jul 26, 2021
1 parent 45fed75 commit 98150ac
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 26 deletions.
122 changes: 97 additions & 25 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const cleanModifiedSubpaths = require('./helpers/document/cleanModifiedSubpaths'
const compile = require('./helpers/document/compile').compile;
const defineKey = require('./helpers/document/compile').defineKey;
const flatten = require('./helpers/common').flatten;
const flattenObjectWithDottedPaths = require('./helpers/path/flattenObjectWithDottedPaths');
const get = require('./helpers/get');
const getEmbeddedDiscriminatorPath = require('./helpers/document/getEmbeddedDiscriminatorPath');
const handleSpreadDoc = require('./helpers/document/handleSpreadDoc');
Expand Down Expand Up @@ -453,6 +454,73 @@ function $__applyDefaults(doc, fields, skipId, exclude, hasIncludedChildren, isB
}
}

/*!
* ignore
*/

function $applyDefaultsToNested(val, path, doc) {
if (val == null) {
return;
}

flattenObjectWithDottedPaths(val);

const paths = Object.keys(doc.$__schema.paths);
const plen = paths.length;

const pathPieces = path.indexOf('.') === -1 ? [path] : path.split('.');

for (let i = 0; i < plen; ++i) {
let curPath = '';
const p = paths[i];

if (!p.startsWith(path + '.')) {
continue;
}

const type = doc.$__schema.paths[p];
const pieces = type.splitPath().slice(pathPieces.length);
const len = pieces.length;

if (type.defaultValue === void 0) {
//continue;
}

let cur = val;

for (let j = 0; j < len; ++j) {
if (cur == null) {
break;
}

const piece = pieces[j];

if (j === len - 1) {
if (cur[piece] !== void 0) {
break;
}

try {
const def = type.getDefault(doc, false);
if (def !== void 0) {
cur[piece] = def;
}
} catch (err) {
doc.invalidate(path + '.' + curPath, err);
break;
}

break;
}

curPath += (!curPath.length ? '' : '.') + piece;

cur[piece] = cur[piece] || {};
cur = cur[piece];
}
}
}

/**
* Builds the default doc structure
*
Expand Down Expand Up @@ -972,7 +1040,9 @@ Document.prototype.$set = function $set(path, val, type, options) {
this.$__schema.paths[pathName].options.ref);

if (someCondition) {
this.$set(path[key], prefix + key, constructing, options);
$applyDefaultsToNested(path[key], prefix + key, this);
this.$set(prefix + key, path[key], constructing, { ...options, _skipMarkModified: true });
continue;
} else if (strict) {
// Don't overwrite defaults with undefined keys (gh-3981) (gh-9039)
if (constructing && path[key] === void 0 &&
Expand Down Expand Up @@ -1020,16 +1090,27 @@ Document.prototype.$set = function $set(path, val, type, options) {
// `Object.assign()` or `{...doc}`
val = handleSpreadDoc(val);

// if this doc is being constructed we should not trigger getters
const priorVal = (() => {
if (this.$__.$options.priorDoc != null) {
return this.$__.$options.priorDoc.$__getValue(path);
}
if (constructing) {
return void 0;
}
return this.$__getValue(path);
})();

if (pathType === 'nested' && val) {
if (typeof val === 'object' && val != null) {
const hasPriorVal = this.$__.savedState != null && this.$__.savedState.hasOwnProperty(path);
const hasInitialVal = this.$__.savedState != null && this.$__.savedState.hasOwnProperty(path);
if (this.$__.savedState != null && !this.isNew && !this.$__.savedState.hasOwnProperty(path)) {
const priorVal = this.$__getValue(path);
this.$__.savedState[path] = priorVal;
const initialVal = this.$__getValue(path);
this.$__.savedState[path] = initialVal;

const keys = Object.keys(priorVal || {});
const keys = Object.keys(initialVal || {});
for (const key of keys) {
this.$__.savedState[path + '.' + key] = priorVal[key];
this.$__.savedState[path + '.' + key] = initialVal[key];
}
}

Expand All @@ -1043,10 +1124,9 @@ Document.prototype.$set = function $set(path, val, type, options) {
const keys = Object.keys(val);
this.$__setValue(path, {});
for (const key of keys) {
this.$set(path + '.' + key, val[key], constructing);
this.$set(path + '.' + key, val[key], constructing, options);
}

if (hasPriorVal && utils.deepEqual(this.$__.savedState[path], val)) {
if (priorVal != null && utils.deepEqual(hasInitialVal ? this.$__.savedState[path] : priorVal, val)) {
this.unmarkModified(path);
} else {
this.markModified(path);
Expand Down Expand Up @@ -1151,19 +1231,8 @@ Document.prototype.$set = function $set(path, val, type, options) {
}
}

// if this doc is being constructed we should not trigger getters
const priorVal = (() => {
if (this.$__.$options.priorDoc != null) {
return this.$__.$options.priorDoc.$__getValue(path);
}
if (constructing) {
return void 0;
}
return this.$__getValue(path);
})();

if (!schema) {
this.$__set(pathToMark, path, constructing, parts, schema, val, priorVal);
this.$__set(pathToMark, path, options, constructing, parts, schema, val, priorVal);
return this;
}

Expand Down Expand Up @@ -1294,7 +1363,7 @@ Document.prototype.$set = function $set(path, val, type, options) {
}

if (shouldSet) {
this.$__set(pathToMark, path, constructing, parts, schema, val, priorVal);
this.$__set(pathToMark, path, options, constructing, parts, schema, val, priorVal);

if (this.$__.savedState != null) {
if (!this.isNew && !this.$__.savedState.hasOwnProperty(path)) {
Expand Down Expand Up @@ -1387,7 +1456,10 @@ Document.prototype.set = Document.prototype.$set;
* @instance
*/

Document.prototype.$__shouldModify = function(pathToMark, path, constructing, parts, schema, val, priorVal) {
Document.prototype.$__shouldModify = function(pathToMark, path, options, constructing, parts, schema, val, priorVal) {
if (options._skipMarkModified) {
return false;
}
if (this.isNew) {
return true;
}
Expand Down Expand Up @@ -1443,10 +1515,10 @@ Document.prototype.$__shouldModify = function(pathToMark, path, constructing, pa
* @instance
*/

Document.prototype.$__set = function(pathToMark, path, constructing, parts, schema, val, priorVal) {
Document.prototype.$__set = function(pathToMark, path, options, constructing, parts, schema, val, priorVal) {
Embedded = Embedded || require('./types/ArraySubdocument');

const shouldModify = this.$__shouldModify(pathToMark, path, constructing, parts,
const shouldModify = this.$__shouldModify(pathToMark, path, options, constructing, parts,
schema, val, priorVal);
const _this = this;

Expand Down
25 changes: 25 additions & 0 deletions lib/helpers/path/flattenObjectWithDottedPaths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use strict';

const setDottedPath = require('../path/setDottedPath');

/**
* Given an object that may contain dotted paths, flatten the paths out.
* For example: `flattenObjectWithDottedPaths({ a: { 'b.c': 42 } })` => `{ a: { b: { c: 42 } } }`
*/

module.exports = function flattenObjectWithDottedPaths(obj) {
if (obj == null || typeof obj !== 'object' || Array.isArray(obj)) {
return;
}
const keys = Object.keys(obj);
for (const key of keys) {
const val = obj[key];
if (key.indexOf('.') !== -1) {
delete obj[key];
setDottedPath(obj, key, val);
continue;
}

flattenObjectWithDottedPaths(obj[key]);
}
};
2 changes: 1 addition & 1 deletion lib/helpers/path/setDottedPath.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

module.exports = function setDottedPath(obj, path, val) {
const parts = path.split('.');
const parts = path.indexOf('.') === -1 ? [path] : path.split('.');
let cur = obj;
for (const part of parts.slice(0, -1)) {
if (cur[part] == null) {
Expand Down

0 comments on commit 98150ac

Please sign in to comment.