🦆

Mongoose

Created
TypeLibrary
LanguageJavascript
Last Edit

Basics

Mongoose
Let's face it, writing MongoDB validation, casting and business logic boilerplate is a drag. That's why we wrote Mongoose. Mongoose provides a straight-forward, schema-based solution to model your application data. It includes built-in type casting, validation, query building, business logic hooks and more, out of the box.
https://mongoosejs.com/

ODM

Mongoose is a Object Document Mapper

Configure

Folder Structure

const express = require("express");
require("./db/mongoose");

const User = require("./models/user");const express = require("express");

const userRouter = require("./routers/user");
const taskRouter = require("./routers/task");

Setup

npm i mongoose

Connect

const mongoose = require("mongoose");

const uri = "mongodb://localhost:27017/";
const database = "task-manager-db";

mongoose.connect(uri + database);

module.exports = mongoose;

Models

Create

const User = mongoose.model("User", {
  name: { type: String },
  age: { type: Number },
});

This will add basic validation when adding data to tables.

Table created in mongodb will be named users even though we have given the model “User” name.

Mongoose converts model names to lowercase and also plurizes it.

Refer Another Model

Here Task model will refer User model.

const Task = mongoose.model("Task", {
  description: { type: String, trim: true, required: true },
  completed: { type: Boolean, default: false },
  owner: {
    type: mongoose.Schema.Types.ObjectId,
    required: true,
    ref: "User",
  },
});

Following code will get the user who is the author of the task:

const Task = require("./models/task");

const main = async () => {
  const task = await Task.findById("636a2f341bdf902dacef69c9");
  await task.populate("owner");
  console.log(task.owner);
};
main();

Define a virtual relation between user and task:

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

This will not create any field in database, only describes relation between models.

const user = await User.findById('5c2e4dcb5eac678725b5b')
await user.populate('tasks')
console.log(user.tasks)

Schema

const userSchema = new mongoose.Schema({
  name: { type: String, required: true, trim: true },
  //...{} all other fields
});

//pass schema to model
const User = mongoose.model("User", userSchema);

module.exports = User;
Schema allows to extend model by including middlewares and custom functions.

Timestamps

const taskSchema = new mongoose.Schema(
  {
    description: { type: String },
    //...
  },
  {
    timestamps: true,
  }
);

This will create updatedAt and createdAt fields in database.

File Upload

const userSchema = new mongoose.Schema(
  {
	//....
	avatar: {
	      type: Buffer,
	    },
	});

Save

const me = new User({
  name: "Andrew",
  age: 27,
});

me.save()
  .then(() => {
    console.log(me);
  })
  .catch((error) => {
    console.log(error);
  });

Data Validation

Mongoose v6.7.0: Validation
Before we get into the specifics of validation syntax, please keep the following rules in mind: Validation is defined in the SchemaType Validation is middleware. Mongoose registers validation as a pre('save') hook on every schema by default.
https://mongoosejs.com/docs/validation.html

Built In Validation

const User = mongoose.model("User", {
  name: { type: String, required: true },
  age: { type: Number },
});

Setting name field as required will through an error when we try to save a new user data without name field.

Custom Validation

const User = mongoose.model("User", {
  name: { type: String, required: true },
  age: {
    type: Number,
    validate(value) {
      if (value < 0) throw new Error("Age must be a positive number");
    },
  },
});

Validator Package

const validator = require("validator");
...
email: {
    type: String,
    required: true,
    validate(value) {
      if (!validator.isEmail(value)) throw new Error("Email is invalid");
    },
  },
...

Data Sanitisation

Trim to remove white space before and after strings.

Lowercase to convert string to lowercase.

Default to set value when no value is available.

name: { type: String, required: true, trim: true },
email: {
	  type: String,
    required: true,
    lowercase: true,
    trim: true,
		unique: true,
},
password: {
    type: String,
    required: true,
    trim: true,
    minlength: 6,
    validate(value) {
      if (value.toLowerCase().includes("password"))
        throw new Error("Password cannot contain 'password'");
    },
  },
age: {
    type: Number,
    default: 0,
}

Queries

Mongoose v6.7.0: Queries
Mongoose models provide several static helper functions for CRUD operations. Each of these functions returns a mongoose Query object. A mongoose query can be executed in one of two ways. First, if you pass in a callback function, Mongoose will execute the query asynchronously and pass the results to the callback.
https://mongoosejs.com/docs/queries.html

Get Data

app.get("/users", async (req, res) => {
  try {
    const users = await User.find({});
    res.send(users);
  } catch (e) {
    res.status(500).send(e.message);
  }
});

Get Single Data

app.get("/users/:id", async (req, res) => {
  const _id = req.params.id;
  try {
    if (mongoose.Types.ObjectId.isValid(_id)) {
      const user = await User.findById(_id);
      if (!user) return res.status(404).send();
      res.send(user);
    } else {
      throw new Error("Invalid ID");
    }
  } catch (error) {
    res.status(500).send(error.message);
  }
});

Add Data

app.post("/users", async (req, res) => {
  const user = new User(req.body);
  try {
    await user.save();
    res.status(201).send(user);
  } catch (e) {
    res.status(400).send(e);
  }
});

Update Data

app.patch("/users/:id", async (req, res) => {
  const _id = req.params.id;
  try {
    if (!mongoose.Types.ObjectId.isValid(_id)) throw new Error("Invalid ID");

    const user = await User.findByIdAndUpdate(_id, req.body, {
      new: true,
      runValidators: true,
    });
    if (!user) return res.status(404).send();  //404: Not found status code
    res.send(user);
  } catch (error) {
    res.status(400).send(error.message);
  }
});

Update Data With Validation

Only allow updates of certain fields.

const updates = Object.keys(req.body);
const allowedUpdated = ["name", "email", "password", "age"];
const isValidOperation = updates.every((update) =>
    allowedUpdated.includes(update)
);

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

Delete Single Data

app.delete("/users/:id", async (req, res) => {
  const _id = req.params.id;
  try {
    if (!mongoose.Types.ObjectId.isValid(_id)) throw new Error("Invalid ID");
    const user = await User.findByIdAndDelete(_id);
    if (!user) return res.status(404).send();
    res.send(user);
  } catch (error) {
    res.status(500).send(error.message);
  }
});

Middleware

Mongoose v6.7.1: Middleware
Middleware (also called pre and post hooks) are functions which are passed control during execution of asynchronous functions. Middleware is specified on the schema level and is useful for writing plugins. Mongoose has 4 types of middleware: document middleware, model middleware, aggregate middleware, and query middleware.
https://mongoosejs.com/docs/middleware.html
Pre middleware functions are executed one after another, when each middleware calls next.
const mongoose = require("mongoose");
const validator = require("validator");

const userSchema = new mongoose.Schema({
  name: { type: String, required: true, trim: true },
  //...{} all other fields
});

userSchema.pre("save", async function (next) {
  const user = this;
  console.log("Just before saving!", user);
  next();
});

//pass schema to model
const User = mongoose.model("User", userSchema);

module.exports = User;
Post middleware are executed after the hooked method and all of its pre middleware have completed.
⚠️
Middlewares are bypassed by certain function in Mongoose like findByIdAndUpdate, so they have to be converted to properly work with middlewares.

// OLD CODE:
// const user = await User.findByIdAndUpdate(_id, req.body, {
//   new: true,
//   runValidators: true,
// });
// NEW CODE:
const user = await User.findById(_id);
updates.forEach((update) => (user[update] = req.body[update]));
await user.save();

Authentication: Hash Password

userSchema.pre("save", async function (next) {
  const user = this;
  if(user.isModified("password")) {
    user.password = await bcrypt.hash(user.password, 8);
  }
  next();
});

Cascade Delete

// Delete user tasks when user is removed
userSchema.pre("remove", async function (next) {
  const user = this;

  await Task.deleteMany({ owner: user._id });
  next();
});

Custom Functions

All Models

schema.statics.yourFunction = async ()⇒{}

Login Function

// Find user by email and password
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;
};

findByCredentials will the name of the custom function and it can be called like so:

const user = await User.findByCredentials(
      req.body.email,
      req.body.password
    );

Particular Instance of Model

schema.methods.yourFunction = async function () {};

JWT Auth Token Get

// JWT Token generate
userSchema.methods.generateAuthToken = async function () {
  const user = this;
  const token = jwt.sign({ _id: user._id.toString() }, "thisismynewcourse");
  user.tokens = user.tokens.concat({ token });
  await user.save();
  return token;
};

Remove Private Data

toJSON is inbuilt function: This function get called whenever an object is stringified JSON.stringify(object)
// Hide private data
userSchema.methods.toJSON = function () {
  const user = this;
  const userObject = user.toObject();

  delete userObject.password;
  delete userObject.tokens;

  return userObject;
};