אימות לפני שמירה
חלק מהיחידה מסדי נתונים עם MongoDB במסלול ה-Node.js של Coddy. שיעור 28 מתוך 29.
MongoDB שומר כל אובייקט שנמסר לו. כאשר req.body מועבר ישירות אל insertOne, לקוח יכול לשמור שנה עם הערך "soon" או שדות נוספים כמו "admin": true.
יש לבצע אימות באמצעות zod לפני ההוספה, ולהוסיף את מה ש-zod מחזירה, ולא את req.body. סכמת z.object מסירה כל מפתח שלא צוין בה, כך שהנתונים שעברו ניתוח מכילים רק את השדות שהתירה.
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 }למסמך באוסף אין מבנה קבוע, ולכן הסכמה שבקוד שלך היא זו ששומרת על אחידות המסמכים שבו.
אתגר
קלכרגע לקוח יכול לשמור שנה בתור "soon", או להגניב שדה כמו "admin": true. צריך לבדוק כל ספר חדש באמצעות zod לפני שהוא מגיע למסד הנתונים.
- ב־
server.js, ייבא אתzמתוך'zod'. כתוב סכימה לספר חדש:title,author,isbnו־genreהם מחרוזות לא ריקות, ו־yearהוא מספר שלם. כל חמשת השדות נדרשים. - ב־
POST /books, בדוק אתreq.bodyבאמצעותsafeParseשל הסכימה לפני ההכנסה. אם הבדיקה נכשלת, החזר שגיאה עם סטטוס 400 באמצעותsendError, ואל תשמור דבר. הבדיקה נכשלת כשחסר שדה, כשיש מחרוזת ריקה, או כשהשנה אינה מספר שלם, כמו"soon"או1965.5. בחירת הודעת השגיאה נתונה לך, לדוגמה{ "error": "year: Invalid input: expected number, received string" }. - שדות נוספים אינם נחשבים לשגיאה. הבקשה עדיין מצליחה, אבל השדות הנוספים מוסרים: הכנס את הנתונים ש־
safeParseמחזיר, ולא אתreq.body.z.objectרגיל מסיר כל מפתח שאינו מופיע בו. אל תוסיף כאן.strict(), כי הוא ידחה את הבקשה עם 400 במקום זאת. - ספר תקין עדיין מקבל 201 ואת הספר שנשמר. ISBN חוזר עדיין מקבל 409. נסה זאת: שלח
POST /booksעם{ "title": "Beloved", "author": "Toni Morrison", "year": "soon", "isbn": "9781400033416", "genre": "literary" }: תקבל 400. שלח אותו שוב עם"year": 1987ועם"admin": trueנוסף: תקבל 201, והספר בתגובה לא יכלולadmin. בכרטיסייה Data מופיע Beloved ללאadmin. שלח ספר ללאtitle: תקבל 400.
נסו בעצמכם
import express from 'express';
import { ObjectId } from 'mongodb';
import { books, ratings } from './db.js';
const app = express();
app.use(express.json());
// כל שגיאה מחזירה { error: message }.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// מחזירה את :id בתור ObjectId, או null לאחר החזרת תשובה עם קוד 400 עבור id לא תקין.
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');
});
כל השיעורים ביחידה מסדי נתונים עם MongoDB
תרגלו בעצמכם: Express אונליין