Retrieving Tasks

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.