Skip to content

Commit

Permalink
Merge pull request #12057 from Automattic/vkarpov15/gh-11945
Browse files Browse the repository at this point in the history
feat(model): add `applyDefaults()` helper that allows applying defaults to document or POJO
  • Loading branch information
vkarpov15 committed Jul 18, 2022
2 parents 871a121 + b165936 commit 1ff55dd
Show file tree
Hide file tree
Showing 17 changed files with 609 additions and 329 deletions.
124 changes: 5 additions & 119 deletions lib/document.js
Expand Up @@ -18,6 +18,7 @@ const ValidatorError = require('./error/validator');
const VirtualType = require('./virtualtype');
const $__hasIncludedChildren = require('./helpers/projection/hasIncludedChildren');
const promiseOrCallback = require('./helpers/promiseOrCallback');
const applyDefaults = require('./helpers/document/applyDefaults');
const cleanModifiedSubpaths = require('./helpers/document/cleanModifiedSubpaths');
const compile = require('./helpers/document/compile').compile;
const defineKey = require('./helpers/document/compile').defineKey;
Expand Down Expand Up @@ -153,7 +154,7 @@ function Document(obj, fields, skipId, options) {
// By default, defaults get applied **before** setting initial values
// Re: gh-6155
if (defaults) {
$__applyDefaults(this, fields, exclude, hasIncludedChildren, true, null);
applyDefaults(this, fields, exclude, hasIncludedChildren, true, null);
}
}
if (obj) {
Expand All @@ -177,7 +178,7 @@ function Document(obj, fields, skipId, options) {
this.$__.skipDefaults = options.skipDefaults;
}
} else if (defaults) {
$__applyDefaults(this, fields, exclude, hasIncludedChildren, false, options.skipDefaults);
applyDefaults(this, fields, exclude, hasIncludedChildren, false, options.skipDefaults);
}

if (!this.$__.strictMode && obj) {
Expand Down Expand Up @@ -451,122 +452,6 @@ Object.defineProperty(Document.prototype, '$op', {
}
});

/*!
* ignore
*/

function $__applyDefaults(doc, fields, exclude, hasIncludedChildren, isBeforeSetters, pathsToSkip) {
const paths = Object.keys(doc.$__schema.paths);
const plen = paths.length;

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

if (p === '_id' && doc.$__.skipId) {
continue;
}

const type = doc.$__schema.paths[p];
const path = type.splitPath();
const len = path.length;
let included = false;
let doc_ = doc._doc;
for (let j = 0; j < len; ++j) {
if (doc_ == null) {
break;
}

const piece = path[j];
curPath += (!curPath.length ? '' : '.') + piece;

if (exclude === true) {
if (curPath in fields) {
break;
}
} else if (exclude === false && fields && !included) {
const hasSubpaths = type.$isSingleNested || type.$isMongooseDocumentArray;
if (curPath in fields || (hasSubpaths && hasIncludedChildren != null && hasIncludedChildren[curPath])) {
included = true;
} else if (hasIncludedChildren != null && !hasIncludedChildren[curPath]) {
break;
}
}

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

if (typeof type.defaultValue === 'function') {
if (!type.defaultValue.$runBeforeSetters && isBeforeSetters) {
break;
}
if (type.defaultValue.$runBeforeSetters && !isBeforeSetters) {
break;
}
} else if (!isBeforeSetters) {
// Non-function defaults should always run **before** setters
continue;
}

if (pathsToSkip && pathsToSkip[curPath]) {
break;
}

if (fields && exclude !== null) {
if (exclude === true) {
// apply defaults to all non-excluded fields
if (p in fields) {
continue;
}

try {
def = type.getDefault(doc, false);
} catch (err) {
doc.invalidate(p, err);
break;
}

if (typeof def !== 'undefined') {
doc_[piece] = def;
doc.$__.activePaths.default(p);
}
} else if (included) {
// selected field
try {
def = type.getDefault(doc, false);
} catch (err) {
doc.invalidate(p, err);
break;
}

if (typeof def !== 'undefined') {
doc_[piece] = def;
doc.$__.activePaths.default(p);
}
}
} else {
try {
def = type.getDefault(doc, false);
} catch (err) {
doc.invalidate(p, err);
break;
}

if (typeof def !== 'undefined') {
doc_[piece] = def;
doc.$__.activePaths.default(p);
}
}
} else {
doc_ = doc_[piece];
}
}
}
}

/*!
* ignore
*/
Expand Down Expand Up @@ -786,7 +671,8 @@ Document.prototype.$__init = function(doc, opts) {
const hasIncludedChildren = this.$__.exclude === false && this.$__.selected ?
$__hasIncludedChildren(this.$__.selected) :
null;
$__applyDefaults(this, this.$__.selected, this.$__.exclude, hasIncludedChildren, false, this.$__.skipDefaults);

applyDefaults(this, this.$__.selected, this.$__.exclude, hasIncludedChildren, false, this.$__.skipDefaults);

return this;
};
Expand Down
115 changes: 115 additions & 0 deletions lib/helpers/document/applyDefaults.js
@@ -0,0 +1,115 @@
'use strict';

module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildren, isBeforeSetters, pathsToSkip) {
const paths = Object.keys(doc.$__schema.paths);
const plen = paths.length;

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

if (p === '_id' && doc.$__.skipId) {
continue;
}

const type = doc.$__schema.paths[p];
const path = type.splitPath();
const len = path.length;
let included = false;
let doc_ = doc._doc;
for (let j = 0; j < len; ++j) {
if (doc_ == null) {
break;
}

const piece = path[j];
curPath += (!curPath.length ? '' : '.') + piece;

if (exclude === true) {
if (curPath in fields) {
break;
}
} else if (exclude === false && fields && !included) {
const hasSubpaths = type.$isSingleNested || type.$isMongooseDocumentArray;
if (curPath in fields || (hasSubpaths && hasIncludedChildren != null && hasIncludedChildren[curPath])) {
included = true;
} else if (hasIncludedChildren != null && !hasIncludedChildren[curPath]) {
break;
}
}

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

if (isBeforeSetters != null) {
if (typeof type.defaultValue === 'function') {
if (!type.defaultValue.$runBeforeSetters && isBeforeSetters) {
break;
}
if (type.defaultValue.$runBeforeSetters && !isBeforeSetters) {
break;
}
} else if (!isBeforeSetters) {
// Non-function defaults should always run **before** setters
continue;
}
}

if (pathsToSkip && pathsToSkip[curPath]) {
break;
}

if (fields && exclude !== null) {
if (exclude === true) {
// apply defaults to all non-excluded fields
if (p in fields) {
continue;
}

try {
def = type.getDefault(doc, false);
} catch (err) {
doc.invalidate(p, err);
break;
}

if (typeof def !== 'undefined') {
doc_[piece] = def;
doc.$__.activePaths.default(p);
}
} else if (included) {
// selected field
try {
def = type.getDefault(doc, false);
} catch (err) {
doc.invalidate(p, err);
break;
}

if (typeof def !== 'undefined') {
doc_[piece] = def;
doc.$__.activePaths.default(p);
}
}
} else {
try {
def = type.getDefault(doc, false);
} catch (err) {
doc.invalidate(p, err);
break;
}

if (typeof def !== 'undefined') {
doc_[piece] = def;
doc.$__.activePaths.default(p);
}
}
} else {
doc_ = doc_[piece];
}
}
}
};
52 changes: 52 additions & 0 deletions lib/helpers/model/applyDefaultsToPOJO.js
@@ -0,0 +1,52 @@
'use strict';

module.exports = function applyDefaultsToPOJO(doc, schema) {
const paths = Object.keys(schema.paths);
const plen = paths.length;

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

const type = schema.paths[p];
const path = type.splitPath();
const len = path.length;
let doc_ = doc;
for (let j = 0; j < len; ++j) {
if (doc_ == null) {
break;
}

const piece = path[j];
curPath += (!curPath.length ? '' : '.') + piece;

if (j === len - 1) {
if (typeof doc_[piece] !== 'undefined') {
if (type.$isSingleNested) {
applyDefaultsToPOJO(doc_[piece], type.caster.schema);
} else if (type.$isMongooseDocumentArray && Array.isArray(doc_[piece])) {
doc_[piece].forEach(el => applyDefaultsToPOJO(el, type.schema));
}

break;
}

const def = type.getDefault(doc, false, { skipCast: true });
if (typeof def !== 'undefined') {
doc_[piece] = def;

if (type.$isSingleNested) {
applyDefaultsToPOJO(def, type.caster.schema);
} else if (type.$isMongooseDocumentArray && Array.isArray(def)) {
def.forEach(el => applyDefaultsToPOJO(el, type.schema));
}
}
} else {
if (doc_[piece] == null) {
doc_[piece] = {};
}
doc_ = doc_[piece];
}
}
}
};
26 changes: 26 additions & 0 deletions lib/helpers/timestamps/setDocumentTimestamps.js
@@ -0,0 +1,26 @@
'use strict';

module.exports = function setDocumentTimestamps(doc, timestampOption, currentTime, createdAt, updatedAt) {
const skipUpdatedAt = timestampOption != null && timestampOption.updatedAt === false;
const skipCreatedAt = timestampOption != null && timestampOption.createdAt === false;

const defaultTimestamp = currentTime != null ?
currentTime() :
doc.ownerDocument().constructor.base.now();

if (!skipCreatedAt &&
(doc.isNew || doc.$isSubdocument) &&
createdAt &&
!doc.$__getValue(createdAt) &&
doc.$__isSelected(createdAt)) {
doc.$set(createdAt, defaultTimestamp, undefined, { overwriteImmutable: true });
}

if (!skipUpdatedAt && updatedAt && (doc.isNew || doc.$isModified())) {
let ts = defaultTimestamp;
if (doc.isNew && createdAt != null) {
ts = doc.$__getValue(createdAt);
}
doc.$set(updatedAt, ts);
}
};

0 comments on commit 1ff55dd

Please sign in to comment.