Directory Structure
/assets: all static files (FE) stored here
/models: all DB schemas stored here
/routes: all router middlewares stored here
File Details
app.js
const express = require("express");
const db = require('./models/index.js');
const todosRouter = require("./routes/todos.router.js");
const app = express();
app.use("/api", express.json(), todosRouter);
app.use(express.static("./assets")); // middleware for static files
app.listen(8080, () => {
console.log("Server is running!");
});
models/index.js
const mongoose = require("mongoose");
mongoose
.connect("mongodb://localhost:27017/todo-demo", {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then((value) => console.log("MongoDB connection succeeded."))
.catch((reason) => console.log("MongoDB connection failed."));
const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error:"));
module.exports = db;
models/todo.js
const mongoose = require("mongoose");
const TodoSchema = new mongoose.Schema({
value: String,
doneAt: Date,
order: Number,
});
// virtual item (todoId) can be only viewed on the mongoose
// the actual mongoDB (on Studio 3T) doesn't store this item
TodoSchema.virtual("todoId").get(function () {
return this._id.toHexString();
});
TodoSchema.set("toJSON", { virtuals: true });
module.exports = mongoose.model("Todo", TodoSchema);
Virtuals
In the mongoose
library, virtuals are used to add a "virtual" column to the data when querying MongoDB, even though the actual column does not exist in the database. This allows for easier data manipulation or usage by adding additional computed properties.
The Schema.set("toJSON", {virtuals: true})
configuration sets mongoose
to return virtual values as part of the JSON representation of the Schema when converting data to JSON.
When comparing data from Studio 3T and mongoose
, you can observe that the virtual todoId
value, which is not actually present in MongoDB, is returned when querying with mongoose
. The todoId
virtual is configured to return this._id
, which is the ObjectId of the Todo Schema, allowing it to be accessed as if it were an actual column in the data.
- Studio 3T
mongoose
routes/todos.router.js
router.post("/todos", async (req, res) => {
const { value } = req.body;
const maxOrderByUserId = await Todo.findOne({}).sort("-order").exec(); // sort descending by order key
// increment order by 1 if exists, otherwise, set as 1
const order = maxOrderByUserId ? maxOrderByUserId.order + 1 : 1;
// save(): create instance first, do something with the value, and save
const todo = new Todo({ value, order });
await todo.save();
res.send({ todo });
});
exec()
It's generally recommended to use .exec()
at the end of a Mongoose query in Node.js when you want to execute the query and retrieve the results. The .exec()
method is used to return a Promise that resolves with the query result, which allows you to handle any errors that may occur during the query execution.
In the example you provided, const maxOrderByUserId = await Todo.findOne({}).sort("-order").exec()
, the .exec()
method is used after chaining the .sort()
method, which sorts the result of the findOne()
query by the "order"
field in descending order. The await
keyword is used to wait for the query to complete and return the result, which is then stored in the maxOrderByUserId
variable.
router.patch("/todos/:todoId", async (req, res) => {
const { todoId } = req.params;
const { order, done, value } = req.body; // three events on FE
// validation: check if id exists
const currentTodo = await Todo.findById(todoId);
if (!currentTodo) {
return res
.status(400)
.json({ errorMessage: "data doesn't exist." });
}
// UPDATE
// 1. change order (FE: user clicks up/down button)
if (order) {
const targetTodo = await Todo.findOne({ order }).exec();
if (targetTodo) {
targetTodo.order = currentTodo.order;
await targetTodo.save();
}
currentTodo.order = order;
}
// 2. change todo content (FE: user change the value in field)
else if(value) {
currentTodo.value = value;
}
// 3. change if todo is completed (FE: user clicks the checkbox)
else if(done !== undefined) {
currentTodo.doneAt = done ? new Date() : null;
}
await currentTodo.save();
res.send({});
});
Methods for MongoDB
save()
https://stackoverflow.com/questions/38290684/mongoose-save-vs-insert-vs-create
The .save()
is an instance method of the model, while the .create()
is called directly from the Model
as a method call, being static in nature, and takes the object as a first parameter.
// method 1 create()
const newTour = await Tour.create(req.body);
// method 2 save()
const newTour = new Tour(req.body);
await newTour.save(); // for POST
targetTodo.order = currentTodo.order;
await targetTodo.save(); // for PATCH
remove()
DELETE is idempotent, so we do not need to check if the key/id exists for the input key/id.