Kaydetmeden Önce Doğrulama
Coddy'nin Node.js Journey'sinin MongoDB ile Veritabanları bölümünün bir parçası. Ders 28 / 29.
MongoDB, kendisine verilen her nesneyi saklar. req.body doğrudan insertOne içine aktarıldığında, istemci "soon" değerini yıl olarak veya "admin": true gibi fazladan alanları kaydedebilir.
Eklemeden önce zod ile doğrulama yap ve req.body yerine zod'un döndürdüğü veriyi ekle. Bir z.object şeması, listelenmeyen her anahtarı kaldırır; böylece ayrıştırılmış veri yalnızca izin verdiğin alanları içerir.
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 }Bir koleksiyonun sabit bir yapısı yoktur; bu nedenle belgelerinin aynı biçimde kalmasını sağlayan şey kodundaki şemadır.
Görev
KolayŞu anda bir istemci, yıl alanına "soon" değerini kaydedebilir veya "admin": true gibi bir alan ekleyebilir. Veritabanına ulaşmadan önce yeni kitapların her birini zod ile doğrula.
server.jsdosyasında,'zod'içindenzöğesini içe aktar. Yeni bir kitap için şema yaz:title,author,isbnvegenreboş olmayan dizeler;yearise bir tam sayı olmalı. Beş alanın tümü zorunludur.POST /booksiçinde, ekleme işleminden öncereq.bodydeğerini şemanınsafeParseyöntemiyle doğrula. Doğrulama başarısız olduğundasendErrorkullanarak 400 durum kodu ve bir hata yanıtı gönder; hiçbir şey kaydetme. Eksik bir alan, boş bir dize veya"soon"ya da1965.5gibi tam sayı olmayan bir yıl olduğunda doğrulama başarısız olur. Hata mesajı sana kalmış; örneğin{ "error": "year: Invalid input: expected number, received string" }.- Ek alanlar hata sayılmaz. İstek yine başarılı olur, ancak ek alanlar atılır:
req.bodyyerinesafeParseyönteminin döndürdüğü veriyi ekle. Düz birz.object, listesinde olmayan tüm anahtarları atar. Buraya.strict()ekleme; çünkü bu, isteği 400 durum koduyla reddeder. - Geçerli bir kitap yine 201 durum kodunu ve kaydedilen kitabı döndürür. Yinelenen bir ISBN yine 409 durum kodunu döndürür. Dene:
POST /booksisteğini{ "title": "Beloved", "author": "Toni Morrison", "year": "soon", "isbn": "9781400033416", "genre": "literary" }ile gönder: 400 yanıtı alırsın. İsteği bu kez"year": 1987ve ek olarak"admin": trueile gönder: 201 yanıtı alırsın ve yanıttaki kitaptaadminalanı bulunmaz. Data sekmesinde Beloved kitabıadminalanı olmadan görünür.titlealanı olmayan bir kitap gönder: 400 yanıtı alırsın.
Kendin dene
import express from 'express';
import { ObjectId } from 'mongodb';
import { books, ratings } from './db.js';
const app = express();
app.use(express.json());
// Her hata, { error: message } biçiminde yanıt verir.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// :id, bir ObjectId olarak döndürülür; geçersiz bir id için 400 yanıtı verildikten sonra ise null döndürülür.
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 ile Veritabanları bölümündeki tüm dersler
1Belgeler
Bir Belge Olarak KitapKoleksiyonlar ve VeritabanlarıMongoDB'ye BağlanmaSaklanan Belgeleri OkumaBelge EklemeTek Bir Belge BulmaObjectId ile BulmaSayıları KarşılaştırmaSorgu FiltreleriDöndürülen Alanları SeçmeSorgu Sonuçlarını Sıralamaİlk Sonuçları TutmaProjection, Sort ve Limit4Yapı
Bir Veritabanı ModülüVeritabanında SayfalamaKendi başına pratik yap: Express playground