Search code examples
mongodbexpressmongooseaggregation-framework

How to join collections in mongoose and retrieve specific fields


I'm working on an eCommerce platform in MERN stack. In my project, I have three schemas: User, Product, and Cart. The Cart schema references both User and Product schemas.

Below is my simplified schema structures:

cartModel.js

const mongoose = require("mongoose");
const { Schema, model } = mongoose;

const cartSchema = new Schema(
  {
    user: { type: Schema.Types.ObjectId, ref: "User", required: true },
    product: { type: Schema.Types.ObjectId, ref: "Product", required: true },
    unit: { type: Number, required: true },
  },
  { timestamps: true, versionKey: false }
);

const cartModel = model("Cart", cartSchema);
module.exports = cartModel;

productModel.js

const mongoose = require("mongoose");
const { Schema, model } = mongoose;

const productSchema = new Schema(
  {
    title: String,
    short_des: String,
    price: Number,
    discount_price: Number,
    image: String,
    stock: String,
    star: String,
  },
  { timestamps: true, versionKey: false }
);

const productModel = model("Product", productSchema);

module.exports = productModel;

userModel.js

const mongoose = require("mongoose");
const { Schema, model } = mongoose;

const userSchema = new Schema(
  {
    name: { type: String, required: true },
    email: { type: String, unique: true, required: true },
    password: { type: String, required: true },
  },
  { timestamps: true, versionKey: false }
);

const userModel = model("User", userSchema);

module.exports = userModel;

My problem is, In my cart controller, I am to trying to define a query to join the Cart, User, and Product models, and trying to extract specific information but my query isn't working as expected.

I need to extract below information for each product grouped by user:

  • cart object ID and unit
  • user name and email
  • product title and price

And I trying to return the result in an array so that I can use map method it in my react frontend to display data.

cartController.js

exports.cartList = async (req, res) => {
  try {
    let userId = new mongoose.Types.ObjectId(req.user._id);

    let result = await cartModel.aggregate([
      { $match: { user: userId } },
      {
        $lookup: {
          from: "users", // Name of the User model collection
          localField: "user",
          foreignField: "_id",
          as: "userDetails",
        },
      },
      {
        $lookup: {
          from: "products", // Name of the Product model collection
          localField: "product",
          foreignField: "_id",
          as: "productDetails",
        },
      },
      {
        $group: {
          _id: "$user",
        },
      },
    ]);
    res.status(200).json({ status: true, data: result });
  } catch (error) {
    res.status(200).json({ status: false, data: error.message });
  }
};

A sample cart model data is below

{
  "_id": {
    "$oid": "6592be024fc8de3f1771a9e1"
  },
  "product": {
    "$oid": "657fab1586f102e3657bf034"
  },
  "user": {
    "$oid": "6559696bfa9b7df357161a81"
  },
  "createdAt": {
    "$date": "2024-01-01T13:28:34.452Z"
  },
  "unit": 4,
  "updatedAt": {
    "$date": "2024-01-01T13:28:46.865Z"
  }
},
{
  "_id": {
    "$oid": "6592dc014fc8de3f1771bc3f"
  },
  "product": {
    "$oid": "65801d5e2c366e8773ada21f"
  },
  "user": {
    "$oid": "6559696bfa9b7df357161a81"
  },
  "createdAt": {
    "$date": "2024-01-01T15:36:33.420Z"
  },
  "unit": 3,
  "updatedAt": {
    "$date": "2024-01-01T15:36:37.161Z"
  }
}

Could someone provide guidance or an example of an aggregation query that retrieves the specified information from multiple collections (Cart, User, and Product) in a format suitable for mapping in React? Any help or suggestions would be greatly appreciated. Thank you!


Solution

  • You could do the following:

    Option 1:

    This would give you an array of 1 object grouped by the user which would have a name, email and carts property. The carts property would be all the carts the user has, each with the specific fields you need. That way you can map the carts.

    const result = await cartModel.aggregate([
      {
        $match: {
          user: userId
        }
      },
      {
        $lookup: {
          from: "users",
          localField: "user",
          foreignField: "_id",
          as: "userDetails"
        }
      },
      {
        $unwind: "$userDetails"
      },
      {
        $lookup: {
          from: "products",
          localField: "product",
          foreignField: "_id",
          as: "productDetails"
        }
      },
      {
        $unwind: "$productDetails"
      },
      {
        $group: {
          _id: "$userDetails._id",
          name: {
            $first: "$userDetails.name"
          },
          email: {
            $first: "$userDetails.email"
          },
          carts: {
            $push: {
              cart_id: "$_id",
              product: "$productDetails",
              unit: "$unit"
            }
          }
        }
      },
      {
        $project: {
          _id: 0,
          name: 1,
          email: 1,
          unit: 1,
          "carts.cart_id": 1,
          "carts.product": {
            price: 1,
            title: 1
          }
        }
      }
    ])
    

    See HERE for a working example.

    Option 2:

    This would give you an array of 1 object grouped by the user but has a single carts property. This would be an array of all the carts the user has but this time each cart would contain the user details in case you need them while looping on the front-end.

    const result = await cartModel.aggregate([
      {
        $match: {
          user: userId
        }
      },
      {
        $lookup: {
          from: "users",
          localField: "user",
          foreignField: "_id",
          as: "userDetails"
        }
      },
      {
        $unwind: "$userDetails"
      },
      {
        $lookup: {
          from: "products",
          localField: "product",
          foreignField: "_id",
          as: "productDetails"
        }
      },
      {
        $unwind: "$productDetails"
      },
      {
        $group: {
          _id: "$userDetails._id",
          carts: {
            $push: {
              cart_id: "$_id",
              product: "$productDetails",
              unit: "$unit",
              user: "$userDetails"
            }
          }
        }
      },
      {
        $project: {
          _id: 0,
          unit: 1,
          "carts.cart_id": 1,
          "carts.product": {
            price: 1,
            title: 1
          },
          "carts.user": {
            email: 1,
            name: 1
          }
        }
      }
    ])
    

    See HERE for a working example.

    Option 3:

    Since you are using reference docs for user and product properties you can just use Model.populate() like so:

    const result = await cartModel.find({user: userId})
    .populate('user', 'name email -_id')
    .populate('product', 'title price -_id')
    .select('unit');