1

I have User and Training model. Both hold likes property set to be an array. The idea is that I would like to create some kind of "add-to-favorite" functionality.

User model

likes: [
    {
      type: mongoose.Schema.ObjectId,
      ref: 'Training',
    },
  ],

Training model

likes: [
    {
      type: mongoose.Schema.ObjectId,
      ref: 'Training',
    },
  ],

In my controller I created a function which is responsible for populating user id in Training and vice versa.

exports.favoriteTraining = catchAsync(async (req, res, next) => {
  const user = await User.findById(req.user.id);
  const training = await Training.findById(req.params.id);

  const trainingLikes = training.likes.filter((like) => {
    return like.toString() === req.user.id;
  });

  if (trainingLikes.length > 0) {
    return next(new AppError('Training already liked', 400));
  }

  user.likes.unshift(req.params.id);
  training.likes.unshift(req.user.id);

  await user.save({ validateBeforeSave: false });
  await training.save({ validateBeforeSave: false });

  res.status(201).json({
    status: 'success',
    data: {
      user,
      training,
    },
  });
});

Current solution works, but I was wondering if there is any other way where I do not need to query both databases separately.

0

1 Answer 1

3

You can simplify your user schema, and the logic to favorite a training by removing the likes from user model.

Here are the steps:

1-) Remove the likes from user model

const mongoose = require("mongoose");

const UserSchema = new mongoose.Schema({
  name: String,
});

module.exports = mongoose.model("User", UserSchema);

2-) Now we only need to modify the likes array in the training model.

exports.favoriteTraining = catchAsync(async (req, res, next) => {
  const loggedUserId = req.user.id;

  let training = await Training.findById(req.params.id);

  const alreadyLiked = training.likes.find((like) => like.toString() === loggedUserId) !== undefined;

  if (alreadyLiked) return next(new AppError("Training already liked", 400));

  training.likes.push(loggedUserId);

  training = await training.save({ validateBeforeSave: false });

  res.status(201).json({
    status: "success",
    data: {
      training,
    },
  });
});

As you see we only made 2 db operations here (it was 4 ). Also I advise to use push instead of unshift, unshift modifies the index of all items in array, push only add the new item to the end.

3-) Since we removed the likes from user, we need to find a way to reference the trainings from user.

Let's say we want to find a user by id and get the trainings he/she favorited. We can use mongodb $lookup aggregation to achieve this. (We could also use virtual populate feature of mongoose, but lookup is better.)

exports.getUserAndFavoritedTrainings = catchAsync(async (req, res, next) => {
  const loggedUserId = req.user.id;

  const result = await User.aggregate([
    {
      $match: {
        _id: mongoose.Types.ObjectId(loggedUserId),
      },
    },
    {
      $lookup: {
        from: "trainings", //must be physcial name of the collection
        localField: "_id",
        foreignField: "likes",
        as: "favorites",
      },
    },
    {
      $project: {
        __v: 0,
        "favorites.likes": 0,
        "favorites.__v": 0,
      },
    },
  ]);

  if (result.length > 0) {
    return res.send(result[0]);
  } else {
    return next(new AppError("User not found", 400));
  }
});

TEST:

Let's say we have these 2 users:

{
    "_id" : ObjectId("5ea7fb904c166d2cc42fd862"),
    "name" : "Krzysztof"
},
{
    "_id" : ObjectId("5ea808988c6f2207c8289191"),
    "name" : "SuleymanSah"
}

And these two trainings:

{
    "_id" : ObjectId("5ea7fbc34c166d2cc42fd863"),
    "likes" : [
        ObjectId("5ea7fb904c166d2cc42fd862"), // Swimming favorited by Krzysztof
        ObjectId("5ea808988c6f2207c8289191") // Swimming favorited by SuleymanSah
    ],
    "name" : "Swimming Training",
},
{
    "_id" : ObjectId("5ea8090d6191c60a00fe9d87"),
    "likes" : [
        ObjectId("5ea7fb904c166d2cc42fd862") // Running favorited by Krzysztof
    ],
    "name" : "Running Training"
}

If the logged in user is Krzysztof, the result will be like this:

{
    "_id": "5ea7fb904c166d2cc42fd862",
    "name": "Krzysztof",
    "favorites": [
        {
            "_id": "5ea7fbc34c166d2cc42fd863",
            "name": "Swimming Training"
        },
        {
            "_id": "5ea8090d6191c60a00fe9d87",
            "name": "Running Training"
        }
    ]
}

You can play with this aggregation in this playground

Sign up to request clarification or add additional context in comments.

1 Comment

Wow! Thank you very much, this is exactly what I needed. I will definitely look into mongodb $lookup cause it looks like a powerful tool for these kind of operations. Thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.