Valider avant d’enregistrer
Fait partie de la section Bases de données avec MongoDB du Journey Node.js de Coddy. Leçon 28 sur 29.
MongoDB stocke tout objet qui lui est fourni. Lorsque req.body est transmis directement à insertOne, un client peut enregistrer une année égale à "soon" ou des champs supplémentaires comme "admin": true.
Valide les données avec zod avant l'insertion, puis insère ce que zod renvoie, et non req.body. Un schéma z.object supprime toutes les clés qu'il ne répertorie pas, de sorte que les données analysées ne contiennent que les champs que tu autorises.
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 }Une collection n'a pas de forme fixe, donc le schéma dans ton code est ce qui garantit que ses documents ont une structure similaire.
Défi
FacileActuellement, un client peut enregistrer une année égale à "soon", ou insérer discrètement un champ comme "admin": true. Vérifiez chaque nouveau livre avec zod avant qu’il n’atteigne la base de données.
- Dans
server.js, importezzdepuis'zod'. Écrivez un schéma pour un nouveau livre :title,author,isbnetgenresont des chaînes non vides, etyearest un nombre entier. Les cinq champs sont obligatoires. - Dans
POST /books, vérifiezreq.bodyavec la méthodesafeParsedu schéma avant l’insertion. Si la vérification échoue, répondez avec le statut 400 et une erreur, en utilisantsendError, et n’enregistrez rien. La vérification échoue si un champ est manquant, si une chaîne est vide ou si l’année n’est pas un nombre entier, par exemple"soon"ou1965.5. Le message est à votre choix, par exemple{ "error": "year: Invalid input: expected number, received string" }. - Les champs supplémentaires ne constituent pas une erreur. La requête aboutit quand même, mais les champs supplémentaires sont supprimés : insérez les données renvoyées par
safeParse, et nonreq.body. Unz.objectsimple supprime toutes les clés qui ne figurent pas dans sa liste. N’ajoutez pas.strict()ici, car cela rejetterait la requête avec le statut 400. - Un livre valide reçoit toujours le statut 201 et est enregistré. Un ISBN déjà utilisé entraîne toujours le statut 409. Essayez : envoyez
POST /booksavec{ "title": "Beloved", "author": "Toni Morrison", "year": "soon", "isbn": "9781400033416", "genre": "literary" }: vous obtenez le statut 400. Envoyez-le à nouveau avec"year": 1987et un champ supplémentaire"admin": true: vous obtenez le statut 201, et le livre dans la réponse ne contient pasadmin. L’onglet Data affiche Beloved sansadmin. Envoyez un livre sanstitle: vous obtenez le statut 400.
Essayez vous-même
import express from 'express';
import { ObjectId } from 'mongodb';
import { books, ratings } from './db.js';
const app = express();
app.use(express.json());
// Chaque erreur renvoie { error: message }.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// Le :id sous forme d’ObjectId, ou null après avoir renvoyé 400 si l’identifiant est invalide.
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');
});
Toutes les leçons de Bases de données avec MongoDB
1Documents
Un livre comme documentCollections et bases de donnéesSe connecter à MongoDBLire des documents stockésInsérer des documentsTrouver un documentRechercher par ObjectIdComparer des nombresFiltres de requêteChoisir les champs renvoyésTrier les résultats de requêteConserver les premiers résultatsProjection, tri et limite4Structure
Un module de base de donnéesLa pagination dans la base de donnéesEntraînez-vous par vous-même : Playground Express