DEV Community

Chiranjib
Chiranjib

Posted on

Working with Node.js Entities and Mongoose Models - III

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];
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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 Array ratings, and also calculate the $avg of those ratings as the field averageRating.
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;

Enter fullscreen mode Exit fullscreen mode

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 array records as an object having attributes movie and rating.

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')];
    }
Enter fullscreen mode Exit fullscreen mode

Define the function getExtentions in file ./entities/user/index.js as:

getExtensions() {
        return [require('./extendedRouters/getRatingsByUserId')];
    }
Enter fullscreen mode Exit fullscreen mode

And, that's that. We have the entity interactions defined and fully functional.

Next: Typing...

Top comments (0)