Search code examples
node.jsmongodbmongoosemongoose-schema

Node JS - Mongoose - How to properly join the results from two collections by ID


I have the following Schemas for tracking Habits, and a user's progress

HABIT SCHEMA

const HabitSchema = new mongoose.Schema({
  user: {
    type: mongoose.Schema.Types.ObjectId,
    ref: "User",
  },
  title: {
    type: String,
    required: true,
  },
  type: {
    type: String,
    enum: ["Goal", "Limit"],
    default: "Goal",
    required: true,
  },
  dateStarted: {
    type: Date,
    default: Date.now,
    required: true,
  },
});

PROGRESS SCHEMA: each time a user clicks that they completed a habit for a given period (day, week, month etc) a document will be created for that period, to indicate that it was done.

const ProgressSchema = new mongoose.Schema({
  habit: {
    type: mongoose.Schema.Types.ObjectId,
    ref: "Habit",
  },
  period: {
    type: Date,
    default: Date.now,
    required: true,
  },
  value: {
    type: Number,
    default: 1,
    required: true,
  },
});

When I get all of a user's habits, I want to also get all progress documents per habit. I want to join these objects before returning.

Here's what I have attempted:

let habits = await Habit.find({ user: req.user.id }).sort({
  dateStarted: -1,
});

habits = await Promise.all(
  habits.map(async (h, i) => {
    try {
      const progress = await Progress.find({ habit: h._id });

      return { ...h, progress };
    } catch (e) {
      res.status(500).send("Server Error");
    }
  })
);
res.json({ habits });

This gives the error: CastError: Cast to ObjectId failed for value "undefined" at path "_id" for model "Habit"

What would be the correct way to accomplish this?


Solution

  • I still don't understand why the error came up considering that I used await/async so the line should not have been running before an id was retrieved (the error was saying ._id is undefined when it clearly wasn't).

    After trying lots of things, here is what finally worked:

    try {
      await Promise.all(
        habits.map(async (h, i) => {
          let progress = await Progress.find({ habit: h._id });
          let newHabit = h;
          newHabit._doc.progresses = progress;
          return newHabit;
        })
      );
    } catch (error) {
      console.log(error);
      res.status(500).send("Server Error");
    }
    

    Once I finally got the await Progress.find({ habit: h._id }); to work, I also found out that I can't just add to a mongoose retrieved object with a spread operator. The object has many properties, and the fields from the database are actually contained within the ._doc property of the object: newHabit._doc.progresses = progress (but this has nothing to do with the error I was getting). This makes me think there is a better way for adding fields to a document found by mongoose .find().