Mongoose returns undefined for subdocument

57 views Asked by At

I have the Express application example from Getting MEAN book. I am connecting to Mongodb with Mongoose. Here's the document


{
  "_id": {
    "$oid": "65537eb149dd64103a4b7950"
  },
  "name": "Starcups",
  "address": "125 High Street, Reading, RG6 1PS",
  "rating": 3,
  "facilities": [
    "Hot drinks",
    "Food",
    "Premium wifi"
  ],
  "coords": [
    -0.9690884,
    51.455041
  ],
  "openingTimes": [
    {
      "days": "Monday - Friday",
      "opening": "7:00am",
      "closing": "7:00pm",
      "closed": false
    },
    {
      "days": "Saturday",
      "opening": "8:00am",
      "closing": "5:00pm",
      "closed": false
    },
    {
      "days": "Sunday",
      "closed": true
    }
  ],
  "reviews": [
    {
      "author": "Lekan Yusuf",
      "id": {
        "$oid": "6560c0dfadc556388f6b8e93"
      },
      "rating": 4,
      "timestamp": {
        "$date": "1992-06-11T23:00:00Z"
      },
      "reviewText": "Good wifi, moderate coffee"
    },
    {
      "author": "Lekan Yusuf",
      "id": {
        "$oid": "6563c6c0d8b148207dcc9817"
      },
      "rating": 4,
      "timestamp": {
        "$date": "1992-06-11T23:00:00Z"
      },
      "reviewText": "Good wifi, moderate coffee"
    }
  ]
}



I have this routes:

var express = require('express');
var router = express.Router();
var ctrlReviews =require('../controllers/reviews');


//reviews routes
router.get('/locations/:locationid/reviews/:reviewid', ctrlReviews.reviewsReadOne);
router.post('locations/:locationid/reviews', ctrlReviews.createReview);
router.put('locations/:locationid/reviews/reviewid', ctrlReviews.reviewsUpdateOne);
router.delete('locations/:locationid/reviews/reviewid', ctrlReviews.reviewsDeleteOne);

This is the controller code:

module.exports.reviewsReadOne = function(req, res){
    console.log('in ReviewsReadOne');
        
    if(req.params && req.params.locationid && req.params.reviewid){
        console.log('locationid: '+ req.query.locationid + ' reviewid: '+ req.query.reviewid);
        Loc.findById(req.query.locationid, 'name reviews').exec(
            function(err,location){
                var response, review;
                if(!location){
                    sendJsonResponse(res, 404, {"message": "locationid not found"});
                    return;   
                } else if (err){
                    sendJsonResponse(res, 404, err);
                    return;
                }
                console.log('location just before "if(location.reviews && location.reviews.length >0)"\n' + location);
                console.log('\n reviews: '+ location.reviews);
                
                if(location.reviews && location.reviews.length >0){

                    review = location.reviews.id(req.params.reviewid);
                    
                    if(!review){
                        sendJsonResponse(res, 400, {"message":"Review id not found"});
                    } else {
                        console.log('review ' + review)
                        response ={
                            location:{
                                name: location.name,
                                id: req.query.id
                            },
                            review:review
                        };
                        sendJsonResponse(res, 200, response);

                    }
                } else {
                    sendJsonResponse(res, 400, {message:"Not found, locationid and reviewid are both required"});
                }

            }
        );

    } else{
        sendJsonResponse(res, 400, {"message": "No location id and or review id"});
        return;
    }
    
}

When I test the API from Postman with existing locationid and reviewid, it gives:

{
    "message": "Not found, locationid and reviewid are both required"
}

VSCode terminal gives the following output:

in ReviewsReadOne
locationid: 65538583fc3b3ac2c0b83a8d reviewid: 6563c74141216ec9d9ec7c38
location just before "if(location.reviews && location.reviews.length >0)"
{
  _id: new ObjectId("65538583fc3b3ac2c0b83a8d"),
  name: 'Starfi',
  reviews: [
    {
      author: 'Subomi ogunderu',
      id: new ObjectId("6563c74141216ec9d9ec7c38"),
      rating: 3,
      timestamp: 1995-08-11T23:00:00.000Z,
      reviewText: 'Excellent wifi and the food is great'
    }
  ]
}

 reviews: undefined

GET /api/locations/locationid/reviews/reviewid?locationid=65538583fc3b3ac2c0b83a8d&reviewid=6563c74141216ec9d9ec7c38 400 566.482 ms - 66


Obviously, the location document is retrieved but the test,

if(location.reviews && location.reviews.length >0)

fails and that seems to be the reason I get

reviews: undefined

in the terminal.

Pls why is reviews undefined and how do I solve this.

1

There are 1 answers

0
jQueeny On

On this line:

review = location.reviews.id(req.params.reviewid)

You seem to be trying to use id() as if it was a method of the location.reviews array. Unfortunately, mongoose if not designed that way as location.reviews is of type MongooseArray which has a limited number of methods.

Thankfully, mongodb has a good selection of operators that you can use to find and select specific properties of documents which will make your code much easier to read. For example, the $elemMatch operator can allow you to match an element from an array and if used in combination with the $ positional operator means you can chose to just project that element.

You can use this code which adopts a modern async/await pattern utilising try/catch blocks for cleaner error handling:

module.exports.reviewsReadOne = async function(req, res){ //< Mark function as async
    console.log('in ReviewsReadOne');
    if(req.params && req.params.locationid && req.params.reviewid){
        console.log('locationid: '+ req.query.locationid + ' reviewid: '+ req.query.reviewid);
        try{
            const location = await Loc.findOne({ //< await the async query
                _id: req.query.locationid,
                reviews: {
                    $elemMatch : {
                        _id: req.query.reviewid
                    }
                }
            }, {
                name: 1, //< Output location name
                'reviews.$': 1 //< Output the matching review
            });
            if(!location){ //< location will be null if no matches
                sendJsonResponse(res, 404, {"message": "Document not found"});
                return;
            }
            sendJsonResponse(res, 200, location);
        } catch (err) {
            console.log(err);
            sendJsonResponse(res, 500, {"message": "Error on server. Check log."});
            return;
        }
    } else{
        sendJsonResponse(res, 400, {"message": "No location id and or review id"});
        return;
    }
}