Working with Node.js Entities and Mongoose Models - III
Published: 2023-02-20 07:32:14

**<u>_Previous:_</u>** [Working with Node.js Entities and Mongoose Models - II](https://dev.to/chiranjib_b/working-with-nodejs-entities-and-mongoose-models-ii-4145)

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 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;

```
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')];
    }
```

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.

**<u>_Next:_</u>** Typing...