Mongoose
| Created | |
|---|---|
| Type | Library |
| Language | Javascript |
| Last Edit |
Basics
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 mongooseConnect
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
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
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
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. 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
toJSONis inbuilt function: This function get called whenever an object is stringifiedJSON.stringify(object)
// Hide private data
userSchema.methods.toJSON = function () {
const user = this;
const userObject = user.toObject();
delete userObject.password;
delete userObject.tokens;
return userObject;
};