Skip to content

Commit

Permalink
feat(model): add Model.applyVirtuals() to apply virtuals to a POJO
Browse files Browse the repository at this point in the history
Fix #14818
  • Loading branch information
vkarpov15 committed Sep 22, 2024
1 parent 45bb194 commit 50b7bc9
Show file tree
Hide file tree
Showing 3 changed files with 381 additions and 0 deletions.
141 changes: 141 additions & 0 deletions lib/helpers/document/applyVirtuals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
'use strict';

const mpath = require('mpath');

module.exports = applyVirtuals;

function applyVirtuals(schema, doc, virtuals, parent) {
if (doc == null) {
return doc;
}

let virtualsForChildren = virtuals;
let toApply = null;

if (Array.isArray(virtuals)) {
virtualsForChildren = [];
toApply = [];
const len = virtuals.length;
for (let i = 0; i < len; ++i) {
const virtual = virtuals[i];
if (virtual.length === 1) {
toApply.push(virtual[0]);
} else {
virtualsForChildren.push(virtual);
}
}
}

applyVirtualsToChildren(this, schema, doc, virtualsForChildren, parent);
return applyVirtualsToDocs(schema, doc, toApply);
}

function applyVirtualsToDocs(schema, res, toApply) {
if (Array.isArray(res)) {
const len = res.length;
for (let i = 0; i < len; ++i) {
applyVirtualsToDoc(schema, res[i], toApply);
}
return res;
} else {
return applyVirtualsToDoc(schema, res, toApply);
}
}

function applyVirtualsToChildren(doc, schema, res, virtuals, parent) {
const len = schema.childSchemas.length;
let attachedVirtuals = false;
for (let i = 0; i < len; ++i) {
const _path = schema.childSchemas[i].model.path;
const _schema = schema.childSchemas[i].schema;
if (!_path) {
continue;
}
const _doc = mpath.get(_path, res);
if (_doc == null || (Array.isArray(_doc) && _doc.flat(Infinity).length === 0)) {
continue;
}

let virtualsForChild = null;
if (Array.isArray(virtuals)) {
virtualsForChild = [];
const len = virtuals.length;
for (let i = 0; i < len; ++i) {
const virtual = virtuals[i];
if (virtual[0] == _path) {
virtualsForChild.push(virtual.slice(1));
}
}

if (virtualsForChild.length === 0) {
continue;
}
}

applyVirtuals.call(doc, _schema, _doc, virtualsForChild, res);
attachedVirtuals = true;
}

if (virtuals && virtuals.length && !attachedVirtuals) {
applyVirtualsToDoc(schema, res, virtuals, parent);
}
}

function applyVirtualsToDoc(schema, doc, virtuals) {
if (doc == null || typeof doc !== 'object') {
return;
}
if (Array.isArray(doc)) {
for (let i = 0; i < doc.length; ++i) {
applyVirtualsToDoc(schema, doc[i], virtuals);
}
return;
}

if (schema.discriminators && Object.keys(schema.discriminators).length > 0) {
for (const discriminatorKey of Object.keys(schema.discriminators)) {
const discriminator = schema.discriminators[discriminatorKey];
const key = discriminator.discriminatorMapping.key;
const value = discriminator.discriminatorMapping.value;
if (doc[key] == value) {
schema = discriminator;
break;
}
}
}

if (virtuals == null) {
virtuals = Object.keys(schema.virtuals);
}
const numVirtuals = virtuals.length;
for (let i = 0; i < numVirtuals; ++i) {
const virtual = virtuals[i];
if (schema.virtuals[virtual] == null) {
continue;
}
const virtualType = schema.virtuals[virtual];
const sp = Array.isArray(virtual)
? virtual :
virtual.indexOf('.') === -1
? [virtual]
: virtual.split('.');
let cur = doc;
for (let j = 0; j < sp.length - 1; ++j) {
cur[sp[j]] = sp[j] in cur ? cur[sp[j]] : {};
cur = cur[sp[j]];
}
let val = virtualType.applyGetters(cur[sp[sp.length - 1]], doc);
if (isPopulateVirtual(virtualType) && val === undefined) {
if (virtualType.options.justOne) {
val = null;
} else {
val = [];
}
}
cur[sp[sp.length - 1]] = val;
}
}

function isPopulateVirtual(virtualType) {
return virtualType.options && (virtualType.options.ref || virtualType.options.refPath);
}
38 changes: 38 additions & 0 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const applySchemaCollation = require('./helpers/indexes/applySchemaCollation');
const applyStaticHooks = require('./helpers/model/applyStaticHooks');
const applyStatics = require('./helpers/model/applyStatics');
const applyWriteConcern = require('./helpers/schema/applyWriteConcern');
const applyVirtualsHelper = require('./helpers/document/applyVirtuals');
const assignVals = require('./helpers/populate/assignVals');
const castBulkWrite = require('./helpers/model/castBulkWrite');
const clone = require('./helpers/clone');
Expand Down Expand Up @@ -3488,6 +3489,9 @@ function handleSuccessfulWrite(document) {
*/

Model.applyDefaults = function applyDefaults(doc) {
if (doc == null) {
return doc;
}
if (doc.$__ != null) {
applyDefaultsHelper(doc, doc.$__.fields, doc.$__.exclude);

Expand All @@ -3503,6 +3507,40 @@ Model.applyDefaults = function applyDefaults(doc) {
return doc;
};

/**
* Apply this model's virtuals to a given POJO. Virtuals execute with the POJO as the context `this`.
*
* #### Example:
*
* const userSchema = new Schema({ name: String });
* userSchema.virtual('upper').get(function() { return this.name.toUpperCase(); });
* const User = mongoose.model('User', userSchema);
*
* const obj = { name: 'John' };
* User.applyVirtuals(obj);
* obj.name; // 'John'
* obj.upper; // 'JOHN', Mongoose applied the return value of the virtual to the given object
*
* @param {Object} obj object or document to apply virtuals on
* @param {Array<string>} [virtualsToApply] optional whitelist of virtuals to apply
* @returns {Object} obj
* @api public
*/

Model.applyVirtuals = function applyVirtuals(doc, virtualsToApply) {
if (doc == null) {
return doc;
}
// Nothing to do if this is already a hydrated document - it should already have virtuals
if (doc.$__ != null) {
return doc;
}

applyVirtualsHelper(this.schema, doc, virtualsToApply, null);

return doc;
};

/**
* Cast the given POJO to the model's schema
*
Expand Down
Loading

0 comments on commit 50b7bc9

Please sign in to comment.