In this chapter we’ll add an additional endpoint to the tasks router to allow us to
- get all tasks
- get all tasks that are not completed or all tasks that are completed
- get a subset of tasks (e.g. tasks 10-20)
- get tasks in sorted order (e.g. according to the last modified timestamp)
To implement the last three types of requests we’re going to require the client to pass instructions (completed, skip, limit, and sortBy options) to to endpoint via the query string.
We’re also going to create a tasks virtual entity in the user schema which will allow us to add to a user instance a set of the tasks in the tasks collection that belong to the user.
Let’s get started.
Add a Virtual Property to the User Schema
When designing the Task model we had a choice to make. Since a task belongs to an individual user and there is a one-to-many relationships between users and tasks, we need to somehow store the relationship between users and tasks. We chose to include in the Task model an owner property whose value is the _id
property of the user who created the task. Alternatively, we could have stored the ids of the tasks in an array in the users model.
There are times where the later solution would be helpful, for instance when a client wants all of the tasks associated with a user. In order to collect the tasks associated with a given user we will create a virtual property, which we’ll name tasks, in the user schema and populate it when needed.
Below is a statement that creates a virtual property named tasks in the user schema. The second argument to the virtual()
method is an object which defines the relationship (or constraint) between an instance of the User
model and the Task
documents that are able to be added to the virtual tasks
property. That is, the tasks
property of an instance of the User
model can only contain Task
documents that have owner
property that are equal to the _id
property of the user.
Copy the code below after your userSchema definition in src/models/user.js.
userSchema.virtual('tasks', { localField: '_id', foreignField: 'owner', ref: 'Task' })
In the next section, we’ll see that we can populate the virtual tasks
property by calling user.populate()
.
Add a New Tasks Endpoint
Below is a router handler that allows clients to retrieve all or a subset of tasks associated with the user identified in the authorization token. We allow the client to send key/value pairs in the query string of the URL which will specify which tasks and the order of the tasks that the client is requesting. The query string can have zero or more of the following key=value pairs. #
refers to any integer and task-property
refers to any one of the properties in the task object.
completed=false completed=true skip=# limit=# sortBy=task-property:asc sortBy=task-property:desc
Please copy this code into src/routers/tasks.js and read the description of the code below.
router.get('/tasks', auth, async(req, res) => { const match = {} const sort = {} if (req.query.sortBy) { const parts = req.query.sortBy.split(':') sort[parts[0]] = (parts[1] == 'asc') ? 1 : -1 } if (req.query.completed) { match.completed = (req.query.completed === 'true') } try { await req.user.populate({ path: 'tasks', match, options: { limit: parseInt(req.query.limit), skip: parseInt(req.query.skip), sort } }) res.send(req.user.tasks) } catch (e) { res.status(500).send() } })
Description of the Code
Since we want restrict users from retrieving other user’s tasks, we will use the auth
middleware (line 1) on this endpoint requiring the client to send an authorization token containing the user’s id.
On lines 2 and 3 we create empty objects that may or not be populated in the code below.
The query string sent by the client may or may not include a sortBy
key. If so, on lines 5-8 we add the task-property (in parts[0]) as a property of the sort
object and set the task-property
in the sort
object equal to either 1 if the value passed by the client is asc
or -1 otherwise.
If the query string contains a key named completed, then on lines 10-12 we add a property named completed
to the match
object and set the property to true if the completed is equal to true in the query string.
On lines 13-24 we try to populate the user’s task
property by calling populate()
, passing to it an object that specifies the name of the user property to populate (path
), a match property which filters the tasks based on the values in the tasks’ completed
properties, and an options object that provides pagination and sorting.
Once the user’s tasks
property is populated the tasks are sent to the client on line 23.
If an error occurs we return a status code of 500 on line 25.
Test Endpoint with Postman
We can test this endpoint with a single Postman request. In the /tasks folder in your Web App API collection, add a new request named Get Tasks. Set the URL to {{url}}/tasks
. In the params tab, we can add multiple key/value pairs and select the check boxes for the key/value pairs that we want to add to the query string when testing. Add the following key/value pairs
- completed/false
- skip/2
- limit/2
- sortBy/completed:asc
- sortBy/updatedAt:desc
Populate your tasks collection using the Create Task request. Add at least 5 tasks, with 2 of them not completed.
Run a series of tests with various combinations (including none) of the params being checked. Verify that the set of tasks that are returned are correct.
Deploy to Heroku
When confident that the changes to your API server work on localhost, push your code to heroku and retest the API server with Postman.
© 2022 – 2024, Eric McGregor. All rights reserved. Unauthorized use, distribution and/or duplication of this material without express and written permission from the author is strictly prohibited.