mongoosejs / mongoose-lean-virtuals

Attach virtuals to the results of mongoose lean() queries
Apache License 2.0
45 stars 24 forks source link

Virtuals fail to access child schema virtuals after retrieval #28

Closed javamatte closed 5 years ago

javamatte commented 5 years ago

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.

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.

(Edited for brevity)

vkarpov15 commented 5 years ago

Fix is released in 0.5.0 :+1: