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
$matchstage of the pipeline simply matches movie id of documents. - the '$group' pipeline collects all the documents that pass the
$matchstage, and$pushthem into an Arrayratings, and also calculate the$avgof 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
$matchstage matches the user id in question with the available documents - the
$groupstage again collects all documents that pass the$matchstage, and$pushthem into an arrayrecordsas an object having attributesmovieandrating.
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...
Top comments (0)