Menu
CoddyTech

Upsert

Fa parte della sezione Database con MongoDB del percorso Node.js di Coddy. Lezione 18 di 29.

Ogni lettore valuta un libro una sola volta. Valutarlo di nuovo dovrebbe modificare le stelle, non aggiungere una seconda valutazione.

Con { upsert: true }, updateOne aggiorna il documento che corrisponde al filtro e, quando non ne corrisponde nessuno, ne inserisce uno creato dai campi del filtro più l’aggiornamento. Una sola chiamata copre la prima volta e tutte le volte successive.

// un documento delle impostazioni per ogni utente
await settings.updateOne(
    { user: 'ada' },
    { $set: { theme: 'dark' } },
    { upsert: true }
);

Il filtro specifica cosa rende un documento univoco; in questo caso, un documento per utente. upsertedId del risultato viene impostato quando la chiamata ha inserito un documento. Un semplice insertOne non può farlo: aggiunge un documento ogni volta.

challenge icon

Sfida

Facile

I lettori assegnano ai libri da 1 a 5 stelle. Ogni lettore valuta un libro una sola volta: valutarlo di nuovo modifica il numero di stelle invece di aggiungere una seconda valutazione.

  1. In server.js, accanto a const books = db.collection('books');, apri una seconda raccolta chiamata ratings. Ogni valutazione è un documento:
    { "bookId": "6720a1f4c2b9e81d3a5f0c11", "user": "ada", "stars": 4 }
    Memorizza bookId come ObjectId restituito da parseId, non come testo proveniente dall'URL. In JSON hanno lo stesso aspetto.
  2. Aggiungi PUT /books/:id/ratings. Il corpo ha questo aspetto: { "user": "ada", "stars": 4 }. Se questo utente ha già valutato il libro, modifica il numero di stelle. In caso contrario, aggiungi una nuova valutazione. Esegui entrambe le operazioni con una sola chiamata. Rispondi con lo stato 200 e la valutazione: { "user": "ada", "stars": 4 }.
  3. Aggiungi GET /books/:id/ratings. Risponde con lo stato 200 e un array contenente le valutazioni di quel libro così come sono memorizzate. Le valutazioni di altri libri non devono comparire:
    [ { "_id": "6720a1f4c2b9e81d3a5f0c21", "bookId": "6720a1f4c2b9e81d3a5f0c11", "user": "ada", "stars": 2 } ]
  4. Entrambi gli endpoint usano parseId in caso di ID non valido, che restituisce 400. Prova: invia GET /books e copia l'_id di Dune. Invia PUT /books/<Dune's _id>/ratings con { "user": "ada", "stars": 4 }: ottieni 200 e { "user": "ada", "stars": 4 }. Invialo di nuovo con { "user": "ada", "stars": 2 }: ottieni 200 e { "user": "ada", "stars": 2 }. Invia GET /books/<Dune's _id>/ratings: ottieni una valutazione, quella di Ada, con 2 stelle. Valuta Dune con { "user": "bo", "stars": 5 } e richiedi di nuovo l'elenco: ottieni due valutazioni. La scheda Data ora mostra una raccolta ratings. GET /books/<Kindred's _id>/ratings restituisce [].

Provalo tu

import express from 'express';
import { MongoClient, ObjectId } from 'mongodb';

const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('shelf');
const books = db.collection('books');

await books.createIndex({ isbn: 1 }, { unique: true });
await books.insertMany([
    { title: 'Dune', author: 'Frank Herbert', year: 1965, isbn: '9780441013593', genre: 'scifi' },
    { title: 'Kindred', author: 'Octavia E. Butler', year: 1979, isbn: '9780807083697', genre: 'scifi' },
    { title: 'Emma', author: 'Jane Austen', year: 1815, isbn: '9780141439587', genre: 'classic' },
]);

const app = express();
app.use(express.json());

// Ogni errore restituisce { error: message }.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// Restituisce :id come ObjectId, oppure null dopo aver risposto con 400 per un id non valido.
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 list = await books.find(filter).toArray();
    res.json(list);
});

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.listen(3000, () => {
    console.log('Bookshelf is running on port 3000');
});

Tutte le lezioni di Database con MongoDB

Esercitati da solo: Playground Express