Working with Node.js Entities and Mongoose Models - III
Chiranjib
Posted on February 20, 2023
Previous: Working with Node.js Entities and Mongoose Models - II
Now that we have established the entities, controllers, models and data-access for elementary CRUD operations, let's go ahead and add a few interactions. We have movies, we have users, now we need to:
- enable our users to rate movies
- fetch ratings for a movie
- fetch all ratings by a user
Step 1 - edit our controller for picking up extensions as:
...
entityObject.getExtensions().forEach(function (extension) {
router.use('/', extension);
});
return [`/${entityObject.name}`, router];
Step 2 - create router extensions for entities
extension for rating movie
Let's create a file ./entities/movie/extendedRouters/postRateMovie.js
as:
const router = require('express').Router({ mergeParams: true });
const MovieRatingModel = require('_data-access/models/MovieRating');
router.post('/rate', async function (req, res, next) {
try {
const { movie, user, rating } = req.body;
const movieRating = await MovieRatingModel
.findOneAndUpdate({
movie, user
}, {
$set: {
rating
}
}, {
upsert: true,
new: true
});
res.json(movieRating);
} catch (error) {
next(error);
}
});
module.exports = router;
The code above simply takes in the request body and persists it to the required collection. You may notice how it uses findOneAndUpdate
, upsert: true
and new: true
. This ensures that mongoose creates the document for us upon first access and helps ensure that there's only ever one combination of a movie and a user (enabling a user to modify their rating if they fancy).
extension for fetching movie ratings
Let's create a file ./entities/movie/extendedRouters/getRatingsByMovieId.js
as:
const router = require('express').Router({ mergeParams: true });
const MovieRatingModel = require('_data-access/models/MovieRating');
const { Types: { ObjectId } } = require('mongoose');
router.get('/:movieId/ratings', async function (req, res, next) {
try {
const movieRatings = await MovieRatingModel.aggregate([
{
$match: { movie: new ObjectId(req.params.movieId) },
},
{
$group: {
_id: '$movie',
ratings: { $push: '$rating' },
averageRating: { $avg: '$rating' },
},
},
]);
res.json(movieRatings);
} catch (error) {
next(error);
}
});
module.exports = router;
Now, we're getting a little fancy with our query. Notice now we have used aggregate
pipelines. There are multiple aggregation stages in Mongo and it takes some getting used to if you are transitioning from being a relational database user. Let us spend a moment on the pipelines here:
- the
$match
stage of the pipeline simply matches movie id of documents. - the '$group' pipeline collects all the documents that pass the
$match
stage, and$push
them into an Arrayratings
, and also calculate the$avg
of those ratings as the fieldaverageRating
.
extension for fetching all ratings by a user
Let's create a file ./entities/user/extendedRouters/getRatingsByUserId.js
as:
const router = require('express').Router({ mergeParams: true });
const MovieRatingModel = require('_data-access/models/MovieRating');
const MovieModel = require('_data-access/models/Movie');
const {
Types: { ObjectId },
} = require('mongoose');
router.get('/:userId/ratings', async function (req, res, next) {
try {
const movieRatingsByUser = await MovieRatingModel.aggregate([
{ $match: { user: new ObjectId(req.params.userId) } },
{
$group: {
_id: '$user',
records: {
$push: {
movie: '$movie',
rating: '$rating',
},
},
},
},
]);
await MovieModel.populate(movieRatingsByUser, { path: 'records.movie', select: 'name' });
res.json(movieRatingsByUser);
} catch (error) {
next(error);
}
});
module.exports = router;
We have used the aggregate pipeline once again, let's understand what's happening once more:
- the
$match
stage matches the user id in question with the available documents - the
$group
stage again collects all documents that pass the$match
stage, and$push
them into an arrayrecords
as an object having attributesmovie
andrating
.
We decide to add a little more spice into our response, and decide that we will populate the Movie name in the response as well, because the person who processes the response would be able to make better sense of it. The line await MovieModel.populate(movieRatingsByUser, { path: 'records.movie', select: 'name' });
iterates all the documents that were received after the aggregate pipeline, and starts populating the records.movie
attribute for all of them with the 'Movie name' for relevance.
Step 3 - make the entities return the router extensions
Define the function getExtentions
in file ./entities/movie/index.js
as:
getExtensions() {
return [require('./extendedRouters/getRatingsByMovieId'), require('./extendedRouters/postRateMovie')];
}
Define the function getExtentions
in file ./entities/user/index.js
as:
getExtensions() {
return [require('./extendedRouters/getRatingsByUserId')];
}
And, that's that. We have the entity interactions defined and fully functional.
Next: Typing...
Posted on February 20, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.