When you have a schema that contains another schema with virtual properties, virtuals on the parent schema that access a child schema's virtuals do not behave properly.
For example, take two schemas, Foo and Bar.
Bar has a number property and a doubleNumber virtual property.
Foo has an array of Bar objects, and a virtual (barDoubleTotal) that sums it's bars' doubleNumber values
When you create them a Foo with a couple of child Bars, foo.bar[0].doubleNumber and foo.barDoubleTotal work as expected. After storage and retrieval foo.bar[0].doubleNumber works as expected, but foo.barDoubleTotal returns NaN instead of summing the array of bar's doubleNumber virtual properties.
Code below to (hopefully) clearly illustrate the issue...
Example:
const mongoose = require('mongoose');
const mongooseLeanVirtuals = require('mongoose-lean-virtuals');
// Bar Model
const barSchema = new mongoose.Schema({
number: { default: 2, type: Number }
});
barSchema.plugin(mongooseLeanVirtuals);
barSchema.virtual('doubleNumber').get(function() {
return this.number * 2;
});
const Bar = mongoose.model('Bar', barSchema);
// Foo Model
const fooSchema = new mongoose.Schema({
bars: [Bar.schema],
number: { type: Number }
});
fooSchema.plugin(mongooseLeanVirtuals);
// virtual that totals virtuals
fooSchema.virtual('barDoubleTotal').get(function() {
return this.bars.map(b => b.doubleNumber).reduce((a, c) => a + c);
});
fooSchema.virtual('barTotal').get(function() {
return this.bars.map(b => b.number).reduce((a, c) => a + c);
});
fooSchema.virtual('doubleNumber').get(function() {
return this.number * 2;
});
const Foo = mongoose.model('Foo', fooSchema);
var bar1 = new Bar({ number: 1 });
var bar2 = new Bar({ number: 3 });
var foo = new Foo({
bars: [ bar1, bar2 ],
number: 5
});
console.log('\nInitial state:\n***************');
console.log(`✅ plain property\tfoo.number: ${foo.number}`);
console.log(`✅ virtual property\tfoo.doubleNumber: ${foo.doubleNumber}`);
console.log(`✅ sum child properties\tfoo.barTotal: ${foo.barTotal}`);
console.log(`✅ sum child virtuals\tfoo.barDoubleTotal: ${foo.barDoubleTotal}`);
console.log(`✅ child virtual\t\tfoo.bars[0].doubleNumber: ${foo.bars[0].doubleNumber}`);
console.log(`✅ child virtual\t\tfoo.bars[1].doubleNumber: ${foo.bars[1].doubleNumber}`);
console.log('\nstoring...');
mongoose.connect('mongodb://127.0.0.1/nested-virtuals', function() {
foo.save(function(err, savedFoo) {
if (err) {
console.err(err);
process.exit(1);
}
console.log('retrieving...');
Foo.findOne({_id: savedFoo._id}).lean({ virtuals: true }).exec(function(err, retrievedFoo) {
if (err) {
console.err(err);
process.exit(1);
}
console.log('\nRetrieved state:\n***************');
console.log(`✅ retrievedFoo.number: ${retrievedFoo.number}`);
console.log(`✅ retrievedFoo.doubleNumber: ${retrievedFoo.doubleNumber}`);
console.log(`✅ retrievedFoo.barTotal: ${retrievedFoo.barTotal}`);
console.log(`❌ retrievedFoo.barDoubleTotal: ${retrievedFoo.barDoubleTotal}`);
console.log(`✅ retrievedFoo.bars[0].doubleNumber: ${retrievedFoo.bars[0].doubleNumber}`);
console.log(`✅ retrievedFoo.bars[1].doubleNumber: ${retrievedFoo.bars[1].doubleNumber}`);
console.log('\nDone.\n');
process.exit(0);
});
});
});
Output
Initial state:
***************
✅ plain property foo.number: 5
✅ virtual property foo.doubleNumber: 10
✅ sum child properties foo.barTotal: 4
✅ sum child virtuals foo.barDoubleTotal: 8
✅ child virtual foo.bars[0].doubleNumber: 2
✅ child virtual foo.bars[1].doubleNumber: 6
storing...
(node:39847) DeprecationWarning: `open()` is deprecated in mongoose >= 4.11.0, use `openUri()` instead, or set the `useMongoClient` option if using `connect()` or `createConnection()`. See http://mongoosejs.com/docs/4.x/docs/connections.html#use-mongo-client
(node:39847) DeprecationWarning: Mongoose: mpromise (mongoose's default promise library) is deprecated, plug in your own promise library instead: http://mongoosejs.com/docs/promises.html
retrieving...
Retrieved state:
***************
✅ retrievedFoo.number: 5
✅ retrievedFoo.doubleNumber: 10
✅ retrievedFoo.barTotal: 4
❌ retrievedFoo.barDoubleTotal: NaN
✅ retrievedFoo.bars[0].doubleNumber: 2
✅ retrievedFoo.bars[1].doubleNumber: 6
Done.
The problem:
When you have a schema that contains another schema with virtual properties, virtuals on the parent schema that access a child schema's virtuals do not behave properly.
For example, take two schemas, Foo and Bar.
number
property and adoubleNumber
virtual property.barDoubleTotal
) that sums it's bars' doubleNumber valuesWhen you create them a Foo with a couple of child Bars,
foo.bar[0].doubleNumber
andfoo.barDoubleTotal
work as expected. After storage and retrievalfoo.bar[0].doubleNumber
works as expected, butfoo.barDoubleTotal
returns NaN instead of summing the array of bar's doubleNumber virtual properties.Code below to (hopefully) clearly illustrate the issue...
Example:
Output
(Edited for brevity)