If you remember, Lodash provides a few really great utility functions and we'll be taking advantage of a couple of those inside of our update route. Right in the Terminal, I'm going to use npm i with the --save flag to install it; the module name itself is called lodash, and we'll be using the most recent version @4.15.0:
npm i --save lodash@4.17.5
Now, once this is installed, we can require it up top and then we can go ahead and add our route. At the very top of the server.js file we can make a constant; we'll use underscore as the name for the variable that stores the Lodash library, then we'll go ahead and require it, require('lodash'). Now, I've used regular variables instead of constants for my other imports, so I can go ahead and switch these variables to constants as well:
const _ = require('lodash');
const express = require('express');
const bodyParser = require('body-parser');
const {ObjectID} = require('mongodb');
Now that we have this in place we are ready to move to the bottom of the file and start adding the new route. This route is going to use the HTTP patch method; patch is what you use when you want to update a resource. Now remember, none of this is really set in stone. I could have a delete route that creates new Todos and I could have a post route that deletes todos, but these are just the general guidelines and best practices for API development. We're going to set up a patch method route by calling app.patch. This is what is going to allow us to update Todo items. Now, the URL is going to be the exact same URL as it has been when we're managing an individual Todo item, /todos/:id. Then we can set up our callback with our request and response arguments. Inside of the callback, one of the first things that we're going to need to do is grab that id just like we do for all our other routes. I'm going to make a variable called id and set it equal to req.params.id. Now, on the next line we're going to go ahead and create a variable called body and this is the reason I loaded in Lodash. The body, the request body, that's where the updates are going to be stored. If I want to set a Todos text to something else, I would make a patch request. I would set the text property equal to whatever I wanted the Todo text to be. The problem here is that someone can send any property along; they could send along properties that aren't on the Todo items or they could send along properties we don't want them to update, for example, completedAt. The completedAt property is going to be a property that gets updated, but it's not going to be updated by the user, it's going to be updated by us when the user updates the completed property. completedAt is going to be generated by the program, which means we do not want a user to be able to update it.
In order to pull off just the properties we want users to update, we're going to be using the pick method, _.pick. The pick method is fantastic; it takes an object, we're going to pass in req.body, then it takes an array of properties that you want to pull off, if they exist. For example, if the text property exists, we want to pull that off of req.body, adding it to body. This is something that users should be able to update, and we'll do the same thing for completed. These are the only two properties a user is going to be able to update; we don't need users updating IDs or adding any other properties that aren't specified in the Mongoose model:
app.patch('/todos/:id',(req, res) => {
var id = req.params.id;
var body = _.pick(req.body, ['text', 'completed']);
});
Now that we have this in place, we can get started down the usual path, kicking things off by validating our ID. There's no need to rewrite the code since we've written it before and we know what it does; we can simply copy it from app.delete block and paste it in app.patch:
if(!ObjectID.isValid(id)){
return res.status(404).send();
}
And now we can go ahead and move onto the slightly complex part of patch, which is going to be checking the completed value and using that value to set completedAt. If a user is setting a Todos completed property to true, we want to set completedAt to a timestamp. If they're setting it to false, we want to clear that timestamp because the Todo won't be completed. We're going to add an if statement checking if the completed property is a Boolean, and it's on body. We're going to use the _.isBoolean utility method to get that done. We want to check if body.completed is a Boolean; if it is a Boolean and that Boolean is true, body.completed, then we're going to go ahead and run some code. This code is going to run if it's a Boolean and it's true, otherwise we're going to run some code if it's not a Boolean or it's not true.
If it is a Boolean and it is true, we're going to set body.completedAt. Everything we set on body is eventually going to be updated in the model. Now, we don't want the user to update everything, so we've picked off certain ones from the req.body, but we can make some modifications of our own. We're going to set body.completedAt equal to the current timestamp. We're going to create a new date, which we've done before, but instead of calling toString, which is the method we used in the previous section, we'll be using a method called getTime. The getTime method returns a JavaScript timestamp; this is the number of milliseconds since midnight on January 1st of the year 1970. It's just a regular number. Values greater than zero are milliseconds from that moment forward, and values less than zero are in the past, so if I had a number of -1000, it would be 1000 milliseconds before that Unix epoch, which is the name for that date, that January 1st at midnight on 1970:
if(_.isBoolean(body.completed) && body.completed) {
body.completedAt = new Date().getTime();
} else {
}
Now that we have that in place we can go ahead and fill out the else clause. Inside of the else clause, if it is not a Boolean or it's not true, we're going to go ahead and set body.completed = false and we're also going to clear completedAt. body.completedAt is going to get set equal to null. When you want to remove a value from the database you can simply set it to null:
if(_.isBoolean(body.completed) && body.completed) {
body.completedAt = new Date().getTime();
} else {
body.completed = false;
body.completedAt = null;
}
Now we're going to go ahead and follow a usual pattern: we're going to be making a query to actually update the database. The query that we're going to be making is going to be really similar to the one we made in our mongodb-update file. Inside of mongodb-update we used a method called findOneAndUpdate. It took a query, the update object, and a set of options. We're going to be using a method called findByIdAndUpdate which takes a really similar set of arguments. Right here in server, we will call Todo.findByIdAndUpdate. The first argument for findByIdAndUpdate is going to be id itself; since we're using a findById method, we can simply pass in id as opposed to passing in a query. Now we can go ahead and set the values on our object, which is the second argument. Remember, you can't just set key value pairs—you have to use those MongoDB operators, things like increment or, in our case, $set. Now, $set, as we explored, takes a set of key value pairs, and these are going to get set. In this case, we've already generated the object, as shown in the following code:
$set: {
completed:true
}
We just happen to generate it in the app.patch block and it happens to be called body. So I will set the $set operator equal to the body variable. Now we can go ahead and move onto the final options. These are just some options that let you tweak how the function works. If you remember, in mongodb-update, we set returnOriginal to false; this meant we got the new object back, the updated one. We're going to use a similar option with a different name; it's called new. It has similar functionality, it just has a different name because that's what the Mongoose developers chose. With a query in place though, we are done, and we can tack on a then callback and a catch callback, and add our success and error code. If things go well, we're going to get our todo doc back, and if things don't go well we are going to get an error argument, and we can go ahead and send back a 400 status code, res.status(400).send():
Todo.findByIdAndUpdate(id, {$set: body}, {new: true}).then((todo) => {
}).catch((e) => {
res.status(400).send();
})
Now, we are going to need to check if the todo object exists. If it doesn't, if there is no todo, then we're going to go ahead and respond with a 404 status code, return res.status(404).send(). If todo does exist, that means we were able to find it and it was updated, so we can simply send it back, res.send, and we're going to send it back as the todo property where todo equals the todo variable using the ES6 syntax:
Todo.findByIdAndUpdate(id, {$set: body}, {new: true}).then((todo) => {
if(!todo)
{
return res.status(404).send();
}
res.send({todo});
}).catch((e) => {
res.status(400).send();
})
With this in place, we are now done. It's not too bad but it was a little more complex than any of the routes we've done before, so I wanted to walk you through it step by step. Let's take just a quick moment to recap what we did and why we did it. First up, the first unusual thing we did is we created the body variable; this has a subset of the things the user passed to us. We didn't want the user to be able to update anything they chose. Next up, we updated the completedAt property based off of the completed property, and finally we made our call to findByIdAndUpdate. With these three steps we were able to successfully update our Todos when we make the patch call.