1

I've tried to use virtual populate between two models I created: in this case to get all the reviews with the tour id and show them with the tour. (when using query findById() to show only this tour)

my virtual is set to true in the Schema (I've tried to set them to true after using the virtual populate but it doesn't work - by this soultion)

after checking the mongoose documentation its seems to be right but it doesn't work.

my tourSchema:

const tourSchema = new mongoose.Schema({
    name: {
        type: String,
        required: [true, 'A tour must have a name'], //validator
        unique: true,
        trim: true,
        maxlength: [40, 'A tour name must have less or equal then 40 characters'],
        minlength: [10, 'A tour name must have at least 10 character']
        //validate: [validator.isAlpha, 'A tour name must have only alphabetic characters']
    },

    etc...
    etc...
    etc...

    //guides: Array --- array of user id's || embedding
    guides: [ 
        //Reference to the user data model without saving the guides in the tour data model
        //Child referencing
        {
            type: mongoose.Schema.ObjectId,
            ref: 'User'
        }
    ]
},
    {
        //passing options, getting the virual properties to the document/object
        toJSON: { virtuals: true },
        toObject: { virtuals: true }
    }
);

//Define virtual properties
tourSchema.virtual('durationWeeks').get(function () {
    console.log('Virtual 1');
    //using function declaration => using this keyword
    return this.duration / 7;
});

//Virtual populate
tourSchema.virtual('reviews', {
    ref: 'review',
    localField: '_id', // Find tour where `localField`
    foreignField: 'tour', // is equal to `foreignField`
    //look for the _id of the tour in the tour field in review
});

my reviewSchema: ** I used in the review schema in for the tour and user populate for the tour id **

const reviewSchema = new mongoose.Schema({
    review: {
        type: String,
        required: [true, 'Review can not be empty!']
    },
    rating: {
        type: Number,
        min: 1,
        max: 5
    },
    createdAt: {
        type: Date,
        default: Date.now(),
    },
    tour: [
        {
            type: mongoose.Schema.ObjectId,
            ref: 'Tour',
            required: [true, 'Review must be belong to a tour.']
        }
    ],
    user: [
        {
            type: mongoose.Schema.ObjectId,
            ref: 'User',
            required: [true, 'Review must be belong to a user.']
        }
    ]
},
    {
        //passing options, getting the virual properties to the document/object
        toJSON: { virtuals: true },
        toObject: { virtuals: true },
    }
);


//Query middleware
reviewSchema.pre(/^find/, function (next) {
    this.populate({
        path: 'tour',
        select: 'name'
    })
        .populate({
            path: 'user',
            select: 'name'
        });

    next();
});

My output: get all reviews (review model data):

{
    "status": "success",
    "data": {
        "review": [
            {
                "_id": "5f6ba5b45624454efca7e0b1",
                "review": "What an amzing tour",
                "tour": {
                    "guides": [],
                    "_id": "5c88fa8cf4afda39709c2955",
                    "name": "The Sea Explorer",
                    "durationWeeks": null,
                    "id": "5c88fa8cf4afda39709c2955"
                },
                "user": {
                    "_id": "5f69f736e6eb324decbc3a52",
                    "name": "Liav"
                },
                "createdAt": "2020-09-23T19:44:52.519Z",
                "id": "5f6ba5b45624454efca7e0b1"
            }
        ]
    }
}

and the get tour by id:

{
    "status": "success",
    "data": {
        "tour": {
            "startLocation": {
                "type": "Point",
                "coordinates": [
                    -80.185942,
                    25.774772
                ],
                "description": "Miami, USA",
                "address": "301 Biscayne Blvd, Miami, FL 33132, USA"
            },
            "ratingsAverage": 4.8,
            "ratingsQuantaity": 0,
            "images": [
                "tour-2-1.jpg",
                "tour-2-2.jpg",
                "tour-2-3.jpg"
            ],
            "startDates": [
                "2021-06-19T09:00:00.000Z",
                "2021-07-20T09:00:00.000Z",
                "2021-08-18T09:00:00.000Z"
            ],
            "secretTour": false,
            "guides": [],
            "_id": "5c88fa8cf4afda39709c2955",
            .
            .
            .
            .
            "slug": "the-sea-explorer",
            "__v": 0,
            "durationWeeks": 1,
            "id": "5c88fa8cf4afda39709c2955"
        }
    }
}

as you can see the review has the tour as an arr and the id is inside the arr of the tour is there an option that the populate is not targeting the right field?

leonheess
  • 16,068
  • 14
  • 77
  • 112
Adi
  • 11
  • 1
  • 6

2 Answers2

0

You need an option virtuals: true passed into the schema creation:

const tourSchema = new mongoose.Schema({
    ...
}, {
  virtuals: true
}

In addition, we use the mongoose-lean-virtuals module to help with .lean and virtuals. e.g.

const mongooseLeanVirtuals = require('mongoose-lean-virtuals');
...
tourSchema.plugin(mongooseLeanVirtuals);
tourSchema.set('toJSON', { virtuals: true });
tourSchema.set('toObject', { virtuals: true });

though I'm guessing that's not strictly necessary.

Joe
  • 41,484
  • 20
  • 104
  • 125
  • Hey well, that didn't work, also in my tourSchema as you can see after defining the model have the virtuals set to true. also, I updated the question and post the JSON that I get a response. in the reviewSchema i get the id of the tour but inside the arr of the tour object, i might not do the right populate to get the id? – Adi Sep 23 '20 at 20:46
0

So I figure it out. First i asked in github - mongoose repo and got answerd:

reviewSchema.pre(/^find/, function (next) {
    this.populate({
        path: 'tour',
        options: { select: 'name' } // <-- wrap `select` in `options` here...
    }).populate({
        path: 'user',
        options: { select: 'name photo' } // <-- and here
    });

    next();
});

We should improve this: nested options are very confusing and it's hard to remember whether something should be options.select or select

The second issue was to add populate after using the FindById method in the tour controller, using the populate without using the wrap 'select' didn't work for me.

exports.getTour = catchAsync(async (req, res, next) => { //parameter => :id || optinal parameter => :id?
    //populate reference to the guides in the user data model
    const tour = await Tour.findById(req.params.id).populate('reviews');

    if (!tour) {
        return next(new AppError('No tour found with that id', 404));
    }

    res.status(200).json({
        status: 'success',
        data: {
            tour
        }
    });
})

and in the tour model, I changed the foreign key from "tour_id" (as I saw in other questions to "tour").

//Virtual populate
tourSchema.virtual('reviews', {
    ref: 'Review',
    localField: '_id', // Find tour where `localField`
    foreignField: 'tour' // is equal to `foreignField`
    //look for the _id of the tour in the tour field in review
});

Now i do have reviews in my tour data and it does virtual populate to the tour by id

Adi
  • 11
  • 1
  • 6