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
EasyThe 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.
- In
server.js, addPOST /books/:id/copies. It needs no body. It adds 1 to the book'scopiesfield. A book with nocopiesyet ends up with"copies": 1. - 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'stagslist and keeps the tags already there. A book with notagsyet ends up with"tags": ["classic"]. - Both routes use
parseIdfor 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 tagsclassicthenfavorite, Dune looks like this:Try it: Send{ "_id": "6720a1f4c2b9e81d3a5f0c11", "title": "Dune", "author": "Frank Herbert", "year": 1965, "isbn": "9780441013593", "genre": "scifi", "copies": 2, "tags": ["classic", "favorite"] }GET /booksand copy Dune's_id. SendPOST /books/<Dune's _id>/copieswith an empty body: you get Dune with"copies": 1. Send it again:"copies": 2. SendPOST /books/<Dune's _id>/tagswith{ "tag": "classic" }:"tags": ["classic"]. Send it again with{ "tag": "favorite" }:"tags": ["classic", "favorite"]. SendPOST /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