Kiểm tra tính hợp lệ trước khi lưu
Thuộc phần Cơ sở dữ liệu với MongoDB trong hành trình Node.js của Coddy. Bài học 28 trên 29.
MongoDB lưu bất kỳ đối tượng nào được truyền vào. Khi req.body được đưa thẳng vào insertOne, client có thể lưu một năm dưới dạng "soon" hoặc các trường bổ sung như "admin": true.
Hãy xác thực bằng zod trước khi chèn, rồi chèn dữ liệu zod trả về, không phải req.body. Lược đồ z.object loại bỏ mọi khóa không được liệt kê, vì vậy dữ liệu đã phân tích chỉ chứa những trường bạn cho phép.
const Player = z.object({
name: z.string().min(1),
level: z.number().int().min(1),
});
const parsed = Player.safeParse({ name: 'Ada', level: 2, admin: true });
parsed.data; // { name: 'Ada', level: 2 }Bộ sưu tập không có cấu trúc cố định, vì vậy lược đồ trong code giúp các tài liệu trong đó có cùng cấu trúc.
Thử thách
DễHiện tại, client có thể lưu năm xuất bản là "soon", hoặc lén thêm một trường như "admin": true. Hãy kiểm tra mọi sách mới bằng zod trước khi sách được lưu vào cơ sở dữ liệu.
- Trong
server.js, importztừ'zod'. Viết schema cho sách mới:title,author,isbnvàgenrelà các chuỗi không rỗng, cònyearlà số nguyên. Cả năm trường đều bắt buộc. - Trong
POST /books, kiểm trareq.bodybằngsafeParsecủa schema trước khi thêm dữ liệu. Nếu kiểm tra không đạt, hãy phản hồi với mã trạng thái 400 và một lỗi bằng cách dùngsendError, đồng thời không lưu gì cả. Kiểm tra không đạt nếu thiếu trường, chuỗi rỗng hoặc năm không phải số nguyên, chẳng hạn như"soon"hoặc1965.5. Bạn có thể tự chọn thông báo, ví dụ{ "error": "year: Invalid input: expected number, received string" }. - Các trường bổ sung không phải là lỗi. Yêu cầu vẫn thành công, nhưng những trường bổ sung sẽ bị loại bỏ: hãy thêm dữ liệu mà
safeParsetrả về, không phảireq.body. Mộtz.objectthông thường sẽ loại bỏ mọi khóa không được liệt kê. Đừng thêm.strict()ở đây, vì như vậy yêu cầu sẽ bị từ chối với mã 400. - Sách hợp lệ vẫn nhận mã 201 và sách được lưu. ISBN trùng lặp vẫn nhận mã 409. Thử ngay: Gửi
POST /booksvới{ "title": "Beloved", "author": "Toni Morrison", "year": "soon", "isbn": "9781400033416", "genre": "literary" }: bạn nhận được mã 400. Gửi lại với"year": 1987và trường bổ sung"admin": true: bạn nhận được mã 201, và sách trong phản hồi không cóadmin. Tab Data hiển thị Beloved mà không cóadmin. Gửi một sách không cótitle: bạn nhận được mã 400.
Tự làm thử
import express from 'express';
import { ObjectId } from 'mongodb';
import { books, ratings } from './db.js';
const app = express();
app.use(express.json());
// Mỗi lỗi trả về { error: message }.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// Trả về :id dưới dạng ObjectId, hoặc null sau khi trả về 400 nếu id không hợp lệ.
function parseId(req, res) {
if (ObjectId.isValid(req.params.id)) {
return new ObjectId(req.params.id);
}
sendError(res, 400, 'Not a valid id');
return null;
}
app.get('/', (req, res) => {
res.send('Bookshelf API');
});
app.get('/books', async (req, res) => {
const filter = {};
if (req.query.author) {
filter.author = req.query.author;
}
if (req.query.from || req.query.to) {
filter.year = {};
if (req.query.from) {
filter.year.$gte = Number(req.query.from);
}
if (req.query.to) {
filter.year.$lte = Number(req.query.to);
}
}
const page = Number(req.query.page) || 1;
const limit = Number(req.query.limit) || 10;
const items = await books
.find(filter)
.skip((page - 1) * limit)
.limit(limit)
.toArray();
const total = await books.countDocuments(filter);
res.json({ items, total });
});
app.post('/books', async (req, res) => {
const book = req.body;
try {
await books.insertOne(book);
} catch (err) {
if (err.code === 11000) {
return sendError(res, 409, 'That ISBN is already on the shelf');
}
throw err;
}
res.status(201).json(book);
});
app.get('/books/newest', async (req, res) => {
const n = Number(req.query.n) || 3;
const list = await books
.find({}, { projection: { title: 1, year: 1, _id: 0 } })
.sort({ year: -1 })
.limit(n)
.toArray();
res.json(list);
});
app.get('/books/:id', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const book = await books.findOne({ _id: id });
if (!book) {
return sendError(res, 404, 'No such book');
}
res.json(book);
});
app.patch('/books/:id', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.updateOne({ _id: id }, { $set: req.body });
if (result.matchedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.json(await books.findOne({ _id: id }));
});
app.post('/books/:id/copies', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.updateOne({ _id: id }, { $inc: { copies: 1 } });
if (result.matchedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.json(await books.findOne({ _id: id }));
});
app.post('/books/:id/tags', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.updateOne({ _id: id }, { $push: { tags: req.body.tag } });
if (result.matchedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.json(await books.findOne({ _id: id }));
});
app.delete('/books/:id', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.deleteOne({ _id: id });
if (result.deletedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.status(204).end();
});
app.put('/books/:id/ratings', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const { user, stars } = req.body;
await ratings.updateOne({ bookId: id, user }, { $set: { stars } }, { upsert: true });
res.json({ user, stars });
});
app.get('/books/:id/ratings', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
res.json(await ratings.find({ bookId: id }).toArray());
});
app.get('/stats', async (req, res) => {
const filter = req.query.genre ? { genre: req.query.genre } : {};
res.json({
books: await books.countDocuments(filter),
ratings: await ratings.countDocuments(),
});
});
app.get('/stats/genres', async (req, res) => {
const genres = await books
.aggregate([{ $group: { _id: '$genre', count: { $sum: 1 } } }])
.toArray();
res.json(genres);
});
app.get('/stats/top', async (req, res) => {
const top = await ratings
.aggregate([
{ $group: { _id: '$bookId', avg: { $avg: '$stars' }, count: { $sum: 1 } } },
{ $match: { count: { $gte: 2 } } },
{ $sort: { avg: -1 } },
{ $limit: 3 },
])
.toArray();
res.json(top);
});
app.get('/books/:id/full', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const [book] = await books
.aggregate([
{ $match: { _id: id } },
{
$lookup: {
from: 'authors',
localField: 'author',
foreignField: 'name',
as: 'authorInfo',
},
},
])
.toArray();
if (!book) {
return sendError(res, 404, 'No such book');
}
res.json(book);
});
app.listen(3000, () => {
console.log('Bookshelf is running on port 3000');
});
Tất cả bài học trong Cơ sở dữ liệu với MongoDB
1Tài liệu
Một cuốn sách dưới dạng tài liệuBộ sưu tập và cơ sở dữ liệuKết nối với MongoDBĐọc tài liệu đã lưuChèn tài liệuTìm một tài liệuTìm theo ObjectIdSo sánh các sốBộ lọc truy vấnChọn các trường được trả vềSắp xếp kết quả truy vấnGiữ lại các kết quả đầu tiênPhép chiếu, sắp xếp và giới hạn4Cấu trúc
Một module cơ sở dữ liệuPhân trang trong cơ sở dữ liệuTự luyện tập: Playground Express