Memvalidasi Sebelum Menyimpan
Termasuk bagian Database dengan MongoDB dalam perjalanan Node.js di Coddy. Pelajaran 28 dari 29.
MongoDB menyimpan objek apa pun yang diberikan kepadanya. Saat req.body langsung diteruskan ke insertOne, klien bisa menyimpan tahun dengan nilai "soon" atau kolom tambahan seperti "admin": true.
Validasi dengan zod sebelum penyisipan, lalu sisipkan hasil yang dikembalikan zod, bukan req.body. Skema z.object menghapus setiap kunci yang tidak tercantum di dalamnya, jadi data yang sudah diuraikan hanya berisi kolom yang kamu izinkan.
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 }Collection tidak memiliki bentuk tetap, jadi skema dalam kodemu yang menjaga agar dokumennya tetap seragam.
Tantangan
MudahSaat ini, client dapat menyimpan tahun berupa "soon", atau menyisipkan field seperti "admin": true. Periksa setiap buku baru dengan zod sebelum buku itu masuk ke database.
- Di
server.js, imporzdari'zod'. Tulis skema untuk buku baru:title,author,isbn, dangenreharus berupa string yang tidak kosong, sedangkanyearharus berupa bilangan bulat. Kelima field wajib diisi. - Di
POST /books, periksareq.bodydengansafeParsedari skema sebelum melakukan penyisipan. Jika pemeriksaan gagal, kirim respons dengan status 400 dan error menggunakansendError, lalu jangan simpan apa pun. Pemeriksaan gagal jika ada field yang tidak diisi, string kosong, atau tahun yang bukan bilangan bulat, seperti"soon"atau1965.5. Kamu bebas menentukan pesannya, misalnya{ "error": "year: Invalid input: expected number, received string" }. - Field tambahan bukan error. Permintaan tetap berhasil, tetapi field tambahan akan dihapus: sisipkan data yang dikembalikan oleh
safeParse, bukanreq.body.z.objectbiasa akan menghapus setiap key yang tidak tercantum di dalamnya. Jangan tambahkan.strict()di sini, karena itu akan menolak permintaan dengan status 400. - Buku yang valid tetap mendapat status 201 dan buku yang tersimpan. ISBN yang sama tetap mendapat status 409. Coba: Kirim
POST /booksdengan{ "title": "Beloved", "author": "Toni Morrison", "year": "soon", "isbn": "9781400033416", "genre": "literary" }: kamu mendapat status 400. Kirim lagi dengan"year": 1987dan field tambahan"admin": true: kamu mendapat status 201, dan buku dalam respons tidak memilikiadmin. Tab Data menampilkan Beloved tanpaadmin. Kirim buku tanpatitle: kamu mendapat status 400.
Coba sendiri
import express from 'express';
import { ObjectId } from 'mongodb';
import { books, ratings } from './db.js';
const app = express();
app.use(express.json());
// Setiap error menghasilkan respons { error: message }.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// Mengembalikan :id sebagai ObjectId, atau null setelah mengirim respons 400 jika id tidak valid.
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');
});
Semua pelajaran di Database dengan MongoDB
1Dokumen
Buku sebagai DokumenKoleksi dan DatabaseMenghubungkan ke MongoDBMembaca Dokumen TersimpanMenyisipkan DokumenMenemukan Satu DokumenMenemukan Dokumen berdasarkan ObjectIdMembandingkan AngkaFilter QueryMemilih Field yang DikembalikanMengurutkan Hasil QueryMempertahankan Hasil PertamaProyeksi, Pengurutan, dan Batas4Struktur
Sebuah module databasePaginasi di databaseLatihan sendiri: Playground Express