Step 5

There is a tutorial on Mongoose's website titled Faster Mongoose Queries With Lean. According to this tutorial, by default, Mongoose queries return an instance of the Mongoose Document class. Documents are much heavier than vanilla JavaScript objects because they have a lot of internal states for change tracking.

If you're executing a query and sending the results without modification to, say, an Express response, you should use "lean." The lean option tells Mongoose to skip hydrating the result documents. This makes queries faster and less memory intensive. Still, the result documents are plain old JavaScript objects (POJOs), not Mongoose documents.

For example, consider the following operation in UserDao:

  // returns empty array if no user matches the username
  async readOne(username) {
-   const user = await User.find({ username });
+   const user = await User.find({ username }).lean();
    return user;
  }

Enabling the lean option tells Mongoose to skip instantiating a full Mongoose document and just give you the POJO.

While at it, we can also use Mongoose attribute selector to dump the attributes we don't need:

  // returns empty array if no user matches the username
  async readOne(username) {
-   const user = await User.find({ username }).lean();
+   const user = await User.find({ username }).lean().select("-__v");
    return user;
  }

Finally, since we simply return the user object, we can rewrite the function body as follows:

  // returns empty array if no user matches the username
  async readOne(username) {
    return User.find({ username }).lean().select("-__v");
  }

Make a similar update to all operations on UserDao. Notice, however, there is no "lean" method on the result of Mongoose's create operation. In that case, we can selectively choose the attributes we care to return!

The final UserDao.js file should look like this:
const User = require("../model/User");
const ApiError = require("../model/ApiError");
const { hashPassword } = require("../util/hashing");

class UserDao {
  async create({ username, password, role }) {
    if (username === undefined || username === "") {
      throw new ApiError(400, "Every user must have a username!");
    }

    if (password === undefined || password === "") {
      throw new ApiError(400, "Every user must have a password!");
    }

    if (role !== "ADMIN" && role !== "CLIENT") {
      throw new ApiError(400, "Every user must have a valid role!");
    }

    const hash = await hashPassword(password);
    const user = await User.create({ username, password: hash, role });
    return {
      _id: user._id.toString(),
      username: user.username,
      password: user.password,
      role: user.role,
    };
  }

  // to update or reset password, or to change role.
  async update(id, { password, role }) {
    await this.read(id);
    return User.findByIdAndUpdate(
      id,
      { password, role },
      { new: true, runValidators: true }
    )
      .lean()
      .select("-__v");
  }

  async delete(id) {
    await this.read(id);
    return User.findByIdAndDelete(id).lean().select("-__v");
  }

  async read(id) {
    const user = await User.findById(id).lean().select("-__v");

    if (user === null) {
      throw new ApiError(404, "There is no user with the given ID!");
    }

    return user;
  }

  // returns empty array if no user matches the username
  async readOne(username) {
    return User.find({ username }).lean().select("-__v");
  }

  // returns an empty array if there is no user in the database
  //  or no user matches the user role query
  async readAll(role = "") {
    const filter = role === "" ? {} : { role };
    return User.find(filter).lean().select("-__v");
  }
}

module.exports = UserDao;

Notice I have also refactored some of the methods to use the read operation.

Save the changes and rerun the tests. Now, all tests must pass except for one!