Bootcamp Notes – Day 12 (Thu) – NodeJS: Week 3: Mongoose Population

Mongoose Population

What is Mongoose Population? How is it useful to us?

SQL/RELATIONAL DATABASES

You learned about databases earlier this week. Relational databases are designed with relationships between databases in mind, hence the name relational. Records are able to link to/reference other records, that can reference other records. This is a defining aspect of how SQL-based relational databases work.

NOSQL DATABASES

This kind of support doesn’t exist natively in NOSQL databases at least not very much of it. No SQL databases are non-relational. In a document based database, individual documents are self-contained, no references to other documents. Everything that you need in a document is stored completely within the document. MongoDB has taken a few steps in adding some support to work in a relational manner. But in general, NOSQL document databases like MONGODB expect that all documents are self-contained units without references to other documents. All the information for that document is within itself. However it is common to have situations where you have one document that needs to have some information in it and another document already contains the same information. So, you may want to be able to reference that information instead of duplicating it. This is where a feature of Mongoose called population will be useful to us.

Mongoose Population lets us store references in documents to other documents.

Let us say we have a document, document A and we want this document to store in one of its fields a reference to another document in another collection document B. So in document A’s field, it will store two pieces of data about document B. One will be the ID of document B, which should be encoded as an object ID. The other will be the model of document B.

DocumentA, in one of its fields, can refer to DocumentB with DoucmentB’s _id (as an ObjectID) and Model

Then by using a mongoose method called .populate() we can graph the information from document B and replace the content of that field in document A.

Coming up, we will update the comment subdocument’s Author field to reference a user document.

Currently the comment subdocument stores its data in a simple string. We are going to update the author field with a reference to the user document of the user who submitted the comment. That means we will store in the author field, the ID of the user, plus the model named user which will look like this below:

Then, whenever we need to. we can use .populate() to get data from user document and that will go to the user document and grab the data stored there for this user and bring it back to this comments sub-document and populate the author field with that data. Also, you will no longer have to ask the user for their name when they submit a comment. It’s added automatically, so they can’t just put any name in for the author and you can track who is making mod comments

Mongoose .populate() method

In our project, campsiteRouter services GET requests with Campsite.find() or Campsite.findbyID

.find() returns array of documents

.findbyId() returns single document

We can use populate on either of these,:

Campsite.find()

.populate(‘comments.author’)

.then(…)

When this populate method is called the comment subdocuments author field will retrieve the data from the reference user document and populates comments.author field with it.

Until then, only holds ObjectID and Model reference, so stored data isn’t unnecessarily duplicated, only retrieved as needed.

The populate method is an intensive task for the server, so use minimally. Be careful!


  • Learn about using Mongoose population to reference one document in MongoDB from another.
  • Use Mongoose population to cross-reference users with a comment and populate comments with referenced users’ information.

Update the Mongoose userSchema and commentSchema to support Mongoose population

  • Open user.js in the models folder and update the code for the schema as follows:
const mongoose = require(‘mongoose’);
const passportLocalMongoose = require(‘passport-local-mongoose’);
const Schema = mongoose.Schema;
const userSchema = new Schema({
    firstname: {
        type: String,
        default: ”
    },
    lastname: {
        type: String,
        default: ”
    },
    admin: {
        type: Boolean,
        default: false
    }
});
userSchema.plugin(passportLocalMongoose);
module.exports = mongoose.model(‘User’, userSchema);
  • Open campsite.js in the models folder and update the comment schema as follows:
const mongoose = require(‘mongoose’);
const Schema = mongoose.Schema;
require(‘mongoose-currency’).loadType(mongoose);
const Currency = mongoose.Types.Currency;
const commentSchema = new Schema({
    rating: {
        type: Number,
        min: 1,
        max: 5,
        required: true
    },
    text: {
        type: String,
        required: true
    },
    author: {
        type: mongoose.Schema.Types.ObjectId,
        ref: ‘User’
    }
}, {
    timestamps: true
});
const campsiteSchema = new Schema({
    name: {
        type: String,
        required: true,
        unique: true
    },
    description: {
        type: String,
        required: true
    },
    image: {
        type: String,
        required: true
    },
    elevation: {
        type: Number,
        required: true
    },
    cost: {
        type: Currency,
        required: true,
        min: 0
    },
    featured: {
        type: Boolean,
        default: false
    },
    comments: [commentSchema]
}, {
    timestamps: true
});
const Campsite = mongoose.model(‘Campsite’, campsiteSchema);
module.exports = Campsite;

Update the campsiteRouter to populate comment authors on GET requests, and save the user._id to author field on comments POST request

  • Open campsiteRouter.js in the routes folder and update the routers and the methods as shown below:
    const express = require(‘express’);
    const Campsite = require(‘../models/campsite’);
    const authenticate = require(‘../authenticate’);
    const campsiteRouter = express.Router();
    campsiteRouter.route(‘/’)
    .get((req, res, next) => {
        Campsite.find()
        .populate(‘comments.author’)
        .then(campsites => {
            res.statusCode = 200;
            res.setHeader(‘Content-Type’, ‘application/json’);
            res.json(campsites);
        })
        .catch(err => next(err));
    })
    .post(authenticate.verifyUser, (req, res, next) => {
        Campsite.create(req.body)
        .then(campsite => {
            console.log(‘Campsite Created ‘, campsite);
            res.statusCode = 200;
            res.setHeader(‘Content-Type’, ‘application/json’);
            res.json(campsite);
        })
        .catch(err => next(err));
    })
    .put(authenticate.verifyUser, (req, res) => {
        res.statusCode = 403;
        res.end(‘PUT operation not supported on /campsites’);
    })
    .delete(authenticate.verifyUser, (req, res, next) => {
        Campsite.deleteMany()
        .then(response => {
            res.statusCode = 200;
            res.setHeader(‘Content-Type’, ‘application/json’);
            res.json(response);
        })
        .catch(err => next(err));
    });
    campsiteRouter.route(‘/:campsiteId’)
    .get((req, res, next) => {
        Campsite.findById(req.params.campsiteId)
        .populate(‘comments.author’)
        .then(campsite => {
            res.statusCode = 200;
            res.setHeader(‘Content-Type’, ‘application/json’);
            res.json(campsite);
        })
        .catch(err => next(err));
    })
    .post(authenticate.verifyUser, (req, res) => {
        res.statusCode = 403;
        res.end(`POST operation not supported on /campsites/${req.params.campsiteId}`);
    })
    .put(authenticate.verifyUser, (req, res, next) => {
        Campsite.findByIdAndUpdate(req.params.campsiteId, {
            $set: req.body
        }, { new: true })
        .then(campsite => {
            res.statusCode = 200;
            res.setHeader(‘Content-Type’, ‘application/json’);
            res.json(campsite);
        })
        .catch(err => next(err));
    })
    .delete(authenticate.verifyUser, (req, res, next) => {
        Campsite.findByIdAndDelete(req.params.campsiteId)
        .then(response => {
            res.statusCode = 200;
            res.setHeader(‘Content-Type’, ‘application/json’);
            res.json(response);
        })
        .catch(err => next(err));
    });
    campsiteRouter.route(‘/:campsiteId/comments’)
    .get((req, res, next) => {
        Campsite.findById(req.params.campsiteId)
        .populate(‘comments.author’)
        .then(campsite => {
            if (campsite) {
                res.statusCode = 200;
                res.setHeader(‘Content-Type’, ‘application/json’);
                res.json(campsite.comments);
            } else {
                err = new Error(`Campsite ${req.params.campsiteId} not found`);
                err.status = 404;
                return next(err);
            }
        })
        .catch(err => next(err));
    })
    .post(authenticate.verifyUser, (req, res, next) => {
        Campsite.findById(req.params.campsiteId)
        .then(campsite => {
            if (campsite) {
                campsite.comments.push(req.body);
                campsite.save()
                .then(campsite => {
                    res.statusCode = 200;
                    res.setHeader(‘Content-Type’, ‘application/json’);
                    res.json(campsite);
                })
                .catch(err => next(err));
            } else {
                err = new Error(`Campsite ${req.params.campsiteId} not found`);
                err.status = 404;
                return next(err);
            }
        })
        .catch(err => next(err));
    })
    .put(authenticate.verifyUser, (req, res) => {
        res.statusCode = 403;
        res.end(`PUT operation not supported on /campsites/${req.params.campsiteId}/comments`);
    })
    .delete(authenticate.verifyUser, (req, res, next) => {
        Campsite.findById(req.params.campsiteId)
        .then(campsite => {
            if (campsite) {
                for (let i = (campsite.comments.length-1); i >= 0; i–) {
                    campsite.comments.id(campsite.comments[i]._id).remove();
                }
                campsite.save()
                .then(campsite => {
                    res.statusCode = 200;
                    res.setHeader(‘Content-Type’, ‘application/json’);
                    res.json(campsite);
                })
                .catch(err => next(err));
            } else {
                err = new Error(`Campsite ${req.params.campsiteId} not found`);
                err.status = 404;
                return next(err);
            }
        })
        .catch(err => next(err));
    });
    campsiteRouter.route(‘/:campsiteId/comments/:commentId’)
    .get((req, res, next) => {
        Campsite.findById(req.params.campsiteId)
        .populate(‘comments.author’)
        .then(campsite => {
            if (campsite && campsite.comments.id(req.params.commentId)) {
                res.statusCode = 200;
                res.setHeader(‘Content-Type’, ‘application/json’);
                res.json(campsite.comments.id(req.params.commentId));
            } else if (!campsite) {
                err = new Error(`Campsite ${req.params.campsiteId} not found`);
                err.status = 404;
                return next(err);
            } else {
                err = new Error(`Comment ${req.params.commentId} not found`);
                err.status = 404;
                return next(err);
            }
        })
        .catch(err => next(err));
    })
    .post(authenticate.verifyUser, (req, res) => {
        res.statusCode = 403;
        res.end(`POST operation not supported on /campsites/${req.params.campsiteId}/comments/${req.params.commentId}`);
    })
    .put(authenticate.verifyUser, (req, res, next) => {
        Campsite.findById(req.params.campsiteId)
        .then(campsite => {
            if (campsite && campsite.comments.id(req.params.commentId)) {
                if (req.body.rating) {
                    campsite.comments.id(req.params.commentId).rating = req.body.rating;
                }
                if (req.body.text) {
                    campsite.comments.id(req.params.commentId).text = req.body.text;
                }
                campsite.save()
                .then(campsite => {
                    res.statusCode = 200;
                    res.setHeader(‘Content-Type’, ‘application/json’);
                    res.json(campsite);
                })
                .catch(err => next(err));
            } else if (!campsite) {
                err = new Error(`Campsite ${req.params.campsiteId} not found`);
                err.status = 404;
                return next(err);
            } else {
                err = new Error(`Comment ${req.params.commentId} not found`);
                err.status = 404;
                return next(err);
            }
        })
        .catch(err => next(err));
    })
    .delete(authenticate.verifyUser, (req, res, next) => {
        Campsite.findById(req.params.campsiteId)
        .then(campsite => {
            if (campsite && campsite.comments.id(req.params.commentId)) {
                campsite.comments.id(req.params.commentId).remove();
                campsite.save()
                .then(campsite => {
                    res.statusCode = 200;
                    res.setHeader(‘Content-Type’, ‘application/json’);
                    res.json(campsite);
                })
                .catch(err => next(err));
            } else if (!campsite) {
                err = new Error(`Campsite ${req.params.campsiteId} not found`);
                err.status = 404;
                return next(err);
            } else {
                err = new Error(`Comment ${req.params.commentId} not found`);
                err.status = 404;
                return next(err);
            }
        })
        .catch(err => next(err));
    });
    module.exports = campsiteRouter;

Update the users router to parse firstname and lastname fields from signup requests and save them to the user document

  • Open users.js in the routes folder and update the register function as follows: 
const express = require(‘express’);
const User = require(‘../models/user’);
const passport = require(‘passport’);
const authenticate = require(‘../authenticate’);
const router = express.Router();
/* GET users listing. */
router.get(‘/’, function(req, res, next) {
    res.send(‘respond with a resource’);
});
router.post(‘/signup’, (req, res) => {
    User.register(
        new User({username: req.body.username}),
        req.body.password,
        (err, user) => {
            if (err) {
                res.statusCode = 500;
                res.setHeader(‘Content-Type’, ‘application/json’);
                res.json({err: err});
            } else {
                if (req.body.firstname) {
                    user.firstname = req.body.firstname;
                }
                if (req.body.lastname) {
                    user.lastname = req.body.lastname;
                }
                user.save(err => {
                    if (err) {
                        res.statusCode = 500;
                        res.setHeader(‘Content-Type’, ‘application/json’);
                        res.json({err: err});
                        return;
                    }
                    passport.authenticate(‘local’)(req, res, () => {
                        res.statusCode = 200;
                        res.setHeader(‘Content-Type’, ‘application/json’);
                        res.json({success: true, status: ‘Registration Successful!’});
                    });
                });
            }
        }
    );
});
router.post(‘/login’, passport.authenticate(‘local’), (req, res) => {
    const token = authenticate.getToken({_id: req.user._id});
    res.statusCode = 200;
    res.setHeader(‘Content-Type’, ‘application/json’);
    res.json({success: true, token: token, status: ‘You are successfully logged in!’});
});
router.get(‘/logout’, (req, res, next) => {
    if (req.session) {
        req.session.destroy();
        res.clearCookie(‘session-id’);
        res.redirect(‘/’);
    } else {
        const err = new Error(‘You are not logged in!’);
        err.status = 401;
        return next(err);
    }
});
module.exports = router;
  • Save all the changes and start the server and test according to the video instructions.
  • If you happen to have a registered user in your MongoDB already, then use the Mongo REPL to delete the user before you begin testing.
  • During testing, you will be asked to POST a campsite with no comments. You can use this as your campsite:
{
    "name": "Redux Woods Campground",
    "image": "images/redux-woods.jpg",
    "elevation": 42,
    "featured": true,
    "cost": 55,
    "description": "You'll never want to leave this hidden gem, deep within the lush Redux Woods."
}
  • Optional: Do a Git commit with the message “Mongoose Population”.

 


Additional Resources:

You May Also Like