Menu
CoddyTech

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.

challenge icon

Défi

Facile

Actuellement, 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.

  1. Dans server.js, importez z depuis 'zod'. Écrivez un schéma pour un nouveau livre : title, author, isbn et genre sont des chaînes non vides, et year est un nombre entier. Les cinq champs sont obligatoires.
  2. Dans POST /books, vérifiez req.body avec la méthode safeParse du schéma avant l’insertion. Si la vérification échoue, répondez avec le statut 400 et une erreur, en utilisant sendError, 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" ou 1965.5. Le message est à votre choix, par exemple { "error": "year: Invalid input: expected number, received string" }.
  3. 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 non req.body. Un z.object simple 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.
  4. 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 /books avec { "title": "Beloved", "author": "Toni Morrison", "year": "soon", "isbn": "9781400033416", "genre": "literary" } : vous obtenez le statut 400. Envoyez-le à nouveau avec "year": 1987 et un champ supplémentaire "admin": true : vous obtenez le statut 201, et le livre dans la réponse ne contient pas admin. L’onglet Data affiche Beloved sans admin. Envoyez un livre sans title : 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

Entraînez-vous par vous-même : Playground Express