Create a Task System with Unread Filtering and User Assignment in Hasura

Introduction

Task systems can be tricky to build, but with Hasura's permission system, subscriptions, and event triggers you can spin one up in an afternoon. You'll have real time tasks, read, unread, as well as permissions for reading, and creating tasks.

We'll walk through what the database structure will look like, building the queries, mutations, and permissions for the task system.

Ensure you have docker and the hasura cli installed. You can find the documentation for installing the

Due to Hasura and it's super flexible querying system we are able to accomplish many common filtering requests that would apply to a task system without using any custom code. This task system could also be used as a notification system or really anything

Setup

To setup your hasura project run the command below. This will create the necessary folder, with all the empty metadata necessary.

hasura init --admin-secret myadminsecretkey

Next grab the docker-compose.yaml provided by Hasura. You can get it here or copy from the code below.

version: "3.6"
services:
  postgres:
    image: postgres:12
    restart: always
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: postgrespassword
  graphql-engine:
    image: hasura/graphql-engine:v1.3.3
    ports:
      - "8080:8080"
    depends_on:
      - "postgres"
    restart: always
    environment:
      HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
      HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
      HASURA_GRAPHQL_DEV_MODE: "true"
      HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
      HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
volumes:
  db_data:

Now that you have it saved in the root. Run the commands below to get the hasura console open. Docker compose will create the DB, and Hasura, then expose hasura on port 8080. Running hasura console will open up a browser at http://localhost:9695/ this will allow us to make changes and have them saved to the migrations and metadata so you'll be able to apply the stuff to your production environment.

docker-compose up -d
cd hasura
hausra console

Our Tasks Data Structure

  • users
  • tasks
  • tasks_assignments
  • tasks_read

We'll flush out what our tables will look like, for the sake of this article we won't make a real users table, or login that will be left up to you.

users
====
- id
- name
tasks
=====
id
created_at
created_by
title
due
tasks_assignments
=====
id
task_id
user_id
tasks_read
=====
id
task_id
user_id
created_at

Creating Tables In Hasura

Once your console is open visit the Data tab.

Click Create Table.

Then start filling out the tables.

This portion is easier to watch in the video.

Setup Relationships and Data Tracking

There are 2 options. You can setup foreign key relationships or just relationships between fields inside of Hasura. I lean a little bit towards foreign key relationships to ensure data integrity but foreign keys can be limiting.

Some of the power ofs of relationships with in Hasura is a field could have a mix of values, and point to other tables from a singular column.

For example: If the user_id was a UUID it could point to an internal user. If then you wanted to support exteranl users you could allow for email to be inserted into the same column.

This tutorial will leverage foreign key relationships.

Permissions

Hasura permissions can be tricky to explain via text. I recommend watching the video to see how all of the permissions get setup.

Build our Mutations and Queries

Before getting started insert a few users so that we can work with some user ids. These are required because we setup foreign key relationships. This means that if we insert a value it must appear in the other table.

We aren't running an authentication system so we must set our values in the header. This is allowed because we are passing in the admin secret. This allows us to pretend to be any role, and any user.

One such query you might run is querying for all the tasks. This will load all task info, who it was created by, who has read it and also who it is assigned to.

query LoadAllTasks {
  tasks {
    id
    due
    created_at
    user {
      id
      name
    }
    tasks_reads {
      user {
        name
        id
      }
    }
    task_assignments {
      user {
        id
        name
      }
    }
  }
}

There aren't any variables here for the graphql query like there would be a normal insert. I am showing that Hasura allows you to do a creation of a specific thing and if the lower tables have a relationship to the primary key it will be automatically inserted.

So we can insert a task, and assign it to a specific user and when the task_assignment is created it will be pointing to the newly created task in the task_id column. This is all done in a transaction so if something breaks we won't have a task, or any assignment created.

mutation InserTask {
  insert_tasks_one(
    object: {
      title: "Test Task"
      due: "now()"
      task_assignments: {
        data: { user_id: "3938e4f3-25cd-4410-b702-59fab96790e0" }
      }
    }
  ) {
    id
    due
    title
    user {
      id
      name
    }
  }
}

Some other general queries might be querying for unread tasks. We can use Hasuras _not query to find where values don't exist. In our case we are querying for where tasks don't have a task_read user equally a specific user_id. You could query for tasks about yourself, or even tasks that another person has not read.

query GetUnreadTasks($userId: uuid!) {
  tasks(where: { _not: { tasks_reads: { user_id: { _eq: $userId } } } }) {
    id
    due
    created_by
    created_at
    title
    updated_at
    user {
      id
      name
    }
  }
}

The opposite then is also the case, querying for tasks that you have read. We remove the _not.

query GetReadTasks($userId: uuid!) {
  tasks(where: { tasks_reads: { user_id: { _eq: $userId } } }) {
    id
    due
    created_by
    created_at
    title
    updated_at
    user {
      id
      name
    }
  }
}

To insert a task read because we have our column preset we only need to supply the task_id and the user_id will be pulled from our hasura claims and inserted in so that we aren't allowing users to say that another user read a task.

mutation InsertTaskRead($task_id: uuid!) {
  insert_tasks_read_one(object: { task_id: $task_id }) {
    id
  }
}

The same queries for task read can also be done for task_assignments, or search for things on specific due dates, etc.

Next Steps

The next step after this would to be leverage Hasura's event trigger system so that when a task is created, or a task is assigned to you a user they can be notified. Event triggers fired when you select to be notified for inserts, updates, and even deletions.

This would allow you to track if a user needs to be notified they were assigned, or that they were unassigned to any task.

Ending

Hasura has allowed us to build a flexible task system that allows for tasks to be assigned to multiple people, and the ability to track who has read tasks all without a single line of backend code.

We were able to leverage the role based access control, and permission system to manage what users can access and automatically track who has created a particular task.

Code Daily Discord

Join our community and get help with React, React Native, and all web technologies. Even recommend tutorials, and content you want to see.