Search code examples
node.jsmongodbexpressmongoose

Virtual properties not populating in mongoose


User Model

const mongoose = require("mongoose");
const validator = require("validator");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const Task = require("./tasks");

const userSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: true,
      trim: true
    },
    age: {
      type: Number,
      default: 0,
      validate(val) {
        if (val < 0) throw new Error("age must be a positive number");
      }
    },
    email: {
      type: String,
      unique: true, //If you use this when you already have data in your db you need to re-populate the db or else it wont work
      required: true,
      trim: true,
      lowercase: true,
      validate(val) {
        if (!validator.isEmail(val)) throw new Error("Invalid Email");
      }
    },
    password: {
      type: String,
      minlength: 6,
      required: true,
      trim: true,
      validate(val) {
        if (val.includes("password"))
          throw new Error("password cant contain password");
      }
    },
    tokens: [
      {
        token: {
          type: String,
          required: true
        }
      }
    ]
  },
  {
    timestamps: true
  }
);

userSchema.virtual("tasks", {
  ref: "Task",
  localField: "_id",
  foreignField: "owner"
});

// Custom function to find user by email and password
// N.B :- we use ".statics for create model functions"
userSchema.statics.findByCredentials = async (email, password) => {
  const user = await User.findOne({ email });

  if (!user) {
    throw new Error("Unable to login");
  }

  const isMatch = await bcrypt.compare(password, user.password);

  if (!isMatch) {
    throw new Error("Unable to login");
  }

  return user;
};

// Hash plain text password before saving
userSchema.pre("save", async function(next) {
  const user = this;

  if (user.isModified("password")) {
    user.password = await bcrypt.hash(user.password, 8);
  }

  next();
});

//Middleware to delete all user tasks when the user is deleted
userSchema.pre("remove", async function(next) {
  const user = this;
  await Task.deleteMany({ owner: user._id });

  next();
});

//Custom function to create a jwt token for a specific user
// N.B:- we use ".methods" to create instance methods

userSchema.methods.generateAuthToken = async function() {
  const user = this;
  const token = jwt.sign({ _id: user._id.toString() }, "mysupersecurestring");

  user.tokens = user.tokens.concat({ token });
  await user.save();
  return token;
};

userSchema.methods.toJSON = function() {
  const user = this;
  const userObject = user.toObject();

  delete userObject.password;
  delete userObject.tokens;

  return userObject;
};

const User = mongoose.model("User", userSchema);

module.exports = User;

Task Model

const mongoose = require("mongoose");

const taskSchema = new mongoose.Schema(
  {
    description: {
      type: String,
      required: true,
      trim: true
    },
    completed: {
      type: Boolean,
      default: false
    },
    owner: {
      type: mongoose.Schema.Types.ObjectId, //use toHexString() to convert to string to avoid errors
      required: true,
      ref: "User"
    }
  },
  {
    timestamps: true //timestamps (plural)
  }
);

const Task = mongoose.model("Task", taskSchema);

module.exports = Task;

Task Controller

const express = require("express");
const Task = require("../models/tasks");
const auth = require("../middleware/auth");
const router = new express.Router();

router.post("/tasks", auth, async (req, res) => {
  //const task = new Task(req.body);
  const task = new Task({
    ...req.body,
    owner: req.user._id
  });

  try {
    await task.save();
    return res.status(201).send(task);
  } catch (e) {
    console.log(e);
    // return res.send(e);
  }
});

router.get("/tasks", auth, async (req, res) => {
  try {
    //const allTasks = await Task.find({ owner: req.user._id.toHexString() });
    await req.user
      .populate({
        path: "tasks",
        match: {
          completed: false
        }
      })
      .execPopulate();

    res.send(req.user.tasks);
    //res.send(allTasks);
  } catch (e) {
    res.status(500).send();
  }
});

router.get("/tasks/:id", auth, async (req, res) => {
  const _id = req.params.id;
  try {
    const task = await Task.findOne({ _id, owner: req.user._id.toHexString() });
    //console.log(_id, req.user._id.toHexString(), task);
    if (!task) {
      return res.status(404).send();
    }
    res.send(task);
  } catch (e) {
    res.status(500).send();
  }
});

router.patch("/tasks/:id", auth, async (req, res) => {
  const updates = Object.keys(req.body);
  const allowedUpdates = ["description", "completed"];
  const isValidOperation = updates.every(update =>
    allowedUpdates.includes(update)
  );

  if (!isValidOperation)
    return res.status(400).send({ error: "invalid updates!" });

  try {
    //const task = await Task.findByIdAndUpdate(req.params.id, req.body, {new: true,runValidators: true});
    const task = await Task.findOne({
      _id: req.params.id,
      owner: req.user._id
    });

    if (!task) return res.status(404).send();
    updates.forEach(update => {
      task[update] = req.body[update];
    });
    await task.save();
    return res.send(task);
  } catch (e) {
    return res.status(400).send(e);
  }
});

router.delete("/tasks/:id", auth, async (req, res) => {
  try {
    const task = await Task.findOneAndDelete({
      _id: req.params.id,
      owner: req.user._id
    });

    if (!task) return res.status(404).send();
    res.send(task);
  } catch (e) {
    res.status(500).send(e);
  }
});

module.exports = router;

In the task controller when I am trying to use the router.get(/tasks) it keeps showing me an error 500(Internal server error). I am trying to populate the virtual properties in the User model which is the tasks. The get task route handler is meant to return all the tasks that specific user created


Solution

  • If you mongoose version is >=6.0 then execPopulate() is no longer exists. Reference Removed execPopulate(). Try

    await req.user
      .populate({
        path: "tasks",
        match: {
          completed: false
        }
      })
    

    hopefully it will solve your problem.