Menu
CoddyTech

Update Operators

Part of the Databases with MongoDB section of Coddy's Node.js journey. Lesson 15 of 29.

Adding a copy or a tag should change a book in place, without reading it, changing it in JavaScript and writing it back.

$inc adds a number to a field, starting from 0 when the field is missing. $push appends a value to an array field, creating the array when it is missing. One update can hold several operators:

await players.updateOne(
    { name: 'Ada' },
    { $inc: { wins: 1 }, $push: { badges: 'streak' } }
);

The database applies the change itself, so two requests at the same moment both count. $set does not fit here: it replaces the whole badges array with the value you give it.

challenge icon

Challenge

Easy

The shelf counts how many copies of each book you own, and readers can tag books. Both changes happen inside the database, in one update each, without reading the book first.

  1. In server.js, add POST /books/:id/copies. It needs no body. It adds 1 to the book's copies field. A book with no copies yet ends up with "copies": 1.
  2. Add POST /books/:id/tags. The body names one tag, for example { "tag": "classic" }. The route adds the tag to the end of the book's tags list and keeps the tags already there. A book with no tags yet ends up with "tags": ["classic"].
  3. Both routes use parseId for a bad id, which gets 400. Both respond with 404 when no book has that _id. Otherwise both respond with status 200 and the whole updated book. After two copies and the tags classic then favorite, Dune looks like this:
    { "_id": "6720a1f4c2b9e81d3a5f0c11", "title": "Dune", "author": "Frank Herbert", "year": 1965, "isbn": "9780441013593", "genre": "scifi", "copies": 2, "tags": ["classic", "favorite"] }
    Try it: Send GET /books and copy Dune's _id. Send POST /books/<Dune's _id>/copies with an empty body: you get Dune with "copies": 1. Send it again: "copies": 2. Send POST /books/<Dune's _id>/tags with { "tag": "classic" }: "tags": ["classic"]. Send it again with { "tag": "favorite" }: "tags": ["classic", "favorite"]. Send POST /books/000000000000000000000000/copies: you get 404.

Try it yourself

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.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());

// Every error answers { error: message }.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// The :id as an ObjectId, or null after answering 400 for a bad id.
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;
    await books.insertOne(book);
    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.listen(3000, () => {
    console.log('Bookshelf is running on port 3000');
});

All lessons in Databases with MongoDB

Practice on your own: Express playground