How to track change to any content with FaunaDB's Temporality feature
Tags: jamstack, fauna, mvp
Published: 2020-08-19 18:36:48

---
title: How to track change to any content with FaunaDB's Temporality feature
published: true
description: 
tags: Jamstack, FaunaDB, MVP
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/i8erfc9qtf4vsuwn4tce.png
---
This is the third article from the series of articles on how I build an MVP with Gatsby and [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky). This time we will look at yet another FaunaDB’s built-in feature called [Temporality](https://docs.fauna.com/fauna/current/tutorials/temporality.html). In simple terms, it helps you track how exactly data has changed over time.

The simplest use-cases are content moderation or versioning. In our MVP, authors can create and update courses and we want to see what changed and approve course content before it goes live. Luckily, we don't need to build our own implementation because [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) offers this feature on a database level out of the box.

This article is based on the previous two articles. If you missed them, it’s best to start with:
- the [first article](https://dev.to/sandorturanszky/how-to-launch-an-mvp-with-jamstack-faunadb-and-graphql-with-zero-running-costs-lfm) where we connect our starter project to [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) and fetch data at build time
- and the [second article](https://dev.to/sandorturanszky/how-to-add-user-authentication-to-your-mvp-using-faunadb-g67) where we implement user authentication with [FaunaDB's](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) built-in Login function, ABAC and also explore FQL together with user-defined functions (UDF).

### Schema update
Our schema has changed again. Here it is:

```graphql
type Course {
 title: String!
 description: String
 visible: Boolean!
 author: User!
 bookmarks: [Bookmark!] @relation
}

type Bookmark {
 title: String
 private: Boolean!
 user: User!
 course: Course!
}

type User {
 name: String!
 email: String!
 role: Role!
 courses: [Course!] @relation
 bookmarks: [Bookmark!] @relation
}

input CreateUserInput {
 name: String!
 email: String!
 password: String!
 role: Role!
}

input LoginUserInput {
 email: String!
 password: String!
}

input LogoutInput {
 allTokens: Boolean
}

type AuthPayload {
 token: String!
 user: User!
}

type CourseUpdates @embedded {
 title: String
 description: String
 visible: Boolean
}

type HistoryUpdate @embedded {
 ts: Long!
 action: String!
 data: CourseUpdates
}

type HistoryPage @embedded {
 data: [HistoryUpdate]
}

type Query {
 allCourses: [Course!]
 allBookmarks: [Bookmark!]
 allUsers(role: Role): [User!]
 allCoursesInReview(visible: Boolean = false): [Course!]
 courseUpdateHistory(id: ID!): HistoryPage
   @resolver(name: "course_update_history")
}

type Mutation {
 createUser(data: CreateUserInput): User! @resolver(name: "create_user")
 loginUser(data: LoginUserInput): AuthPayload! @resolver(name: "login_user")
 logoutUser(data: LogoutInput): Boolean! @resolver(name: "logout_user")
}

enum Role {
 AUTHOR
 DEVELOPER
 MANAGER
}
```
We added:
1. the `visible` property to `Course` type to make sure that only visible (approved) courses will be listed;
2. the `allCoursesInReview` query to list all courses that require approval based on the value of the `visible` property;
3. the `courseUpdateHistory` query to list all changes made to a particular course that we can look up by its `_id`;
4. The `MANAGER` role

We also use the `@embedded` directive on the return type for the `courseUpdateHistory` query so that [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) does not create collections for those types.

Copy the above schema into a file named `schema.gql` and apply it with [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) as shown on the screenshot below:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/8cbaagng6x055izje4mp.png)

Since we added the `visible` property as non-nullable (`visible: Boolean!` - the exclamation mark means that the field is non-nullable, meaning that there must be always a value) and none of our courses has it, we need to set it for all courses to avoid getting the `"Cannot return null for non-nullable type"` GraphQL error. We need to make a small change to our existing data to accommodate the schema update which we can do in pure FQL. Copy and paste the following code into the Shell and run it:

```javascript
Map(
  Paginate(
    Match(Index("allCourses"))
  ),
  Lambda("X", 
    Update(
      Select("ref",  Get(Var("X"))),
      { data: { visible: true }})
    )
)
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2acee5frwwdyen08fib9.png)

### Author role
Before we see how FaunaB’s temporal feature works, we need a way to create and update courses as authors would. While we could do it via the GraphQL playground, it’s always better to see a real-life example.

Authors can log in, but we do not have an Author role yet. Let’s create it and define what privileges we will give to authors.

Here is our FQL for the AUTHOR role:
```javascript
CreateRole({
  name: "Author",
  membership: [
    {
      resource: Collection("User"),
      predicate: Query(
        Lambda(
          "userRef",
          Equals(Select(["data", "role"], Get(Var("userRef"))), "AUTHOR")
        )
      )
    }
  ],
  privileges: [
    {
      resource: Collection("User"),
      actions: {
        read: true
      }
    },
        {
          resource: Collection("Bookmark"),
          actions: {
            read: Query(
              Lambda(
                "bookmarkRef",
                Let(
                  {
                    bookmark: Get(Var("bookmarkRef")),
                    private: Select(["data", "private"], Var("bookmark"))
                  },
                  Equals(Var("private"), false)
                )
              )
            )
          }
        },
    {
      resource: Collection("Course"),
      actions: {
        read: true,
        write: Query(
          Lambda(
            ["oldData", "newData"],
            And(
              Equals(Identity(), Select(["data", "author"], Var("oldData"))),
              Equals(Select(["data", "visible"], Var("newData")), false),
              Equals(
                Select(["data", "author"], Var("oldData")),
                Select(["data", "author"], Var("newData"))
              )
            )
          )
        ),
        create: Query(
          Lambda(
            "data",
            And(
              Equals(Identity(), Select(["data", "author"], Var("data"))),
              Equals(Select(["data", "visible"], Var("data")), false)
            )
          )
        )
      }
    },
    {
      resource: Index("allUsers"),
      actions: {
        read: true
      }
    },
    {
      resource: Function("logout_user"),
      actions: {
        call: true
      }
    },
    {
      resource: Index("allCourses"),
      actions: {
        read: true
      }
    },
    {
      resource: Index("course_author_by_user"),
      actions: {
        read: true
      }
    },
    {
      resource: Index("bookmark_user_by_user"),
      actions: {
        read: true
      }
    }
  ]
})
```
We allow the AUTHOR role to create and edit own courses only, see all courses and public bookmarks and log out. 

What’s interesting to note is that the `visible` property in the `Course` type is `false` by default for new and updated courses which means that the course is “In review”. The author can’t override this value. We are checking for it in the predicate function in these lines:
- for “Create” action:
`Equals(Select(["data", "visible"], Var("data")), false)`
- and for the “Write” action:
`Equals(Select(["data", "visible"], Var("newData")), false)`

We can safely set the `visible` property to `false` on the client-side because we know that even if the value is changed intentionally, it will not pass the ABAC. This means that we do not need to create a custom resolver or a backend function to set this value manually. 

We will test it in a minute.

Copy and paste the above FQL into the Shell and run it.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/k81z72ha1s09gh2k52g6.png)

Now we need to clone the repository with the latest changes for this article.
Note that you will need the `.env.development` and `.env.production` files containing the bootstrap key and other variables that we added in the [previous article](https://dev.to/sandorturanszky/how-to-add-user-authentication-to-your-mvp-using-faunadb-g67)

Follow these steps:
- `git clone --single-branch --branch article-3/temporality git@github.com:sandorTuranszky/Gatsby-FaunaDB-GraphQL.git gatsby-fauna-db`
- `cd gatsby-fauna-db`
- copy the `.env.development` and `.env.production` files
- `npm install`
- `gatsby develop` to start the project in development mode

You should see the MVP up and running.
 
> If you see an error, make sure that the `token` cookie and the `user_data` in local storage have been removed or remove them manually.

Log in as an author using the following credentials:
Email: `johns.austin@email.com`
Password: `password`

And you will be redirected to the following page `/app/courses`:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/t5k5doqygq60loyzc0z6.png)

Now we can create new courses or update an existing one. 

Let’s create a new course:
Title:  “Node.js Masterclass”
Description: “We’ll cover some of the topics including integrating Node.js with Express and asynchronous code”
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/8e22e93h22890ikz7oco.png)

We can see that the newly created course is marked “In review”. As it was mentioned before, all new or updated courses have a default `false` value for the `visible` property. This means that the course needs to be reviewed. 

The default `false` value is set on the client and is controlled by ABAC. You can test it by changing the default value to `true` in the mutation in `/src/components/updateCourse.js file`:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/b0mcq59gekpuxkt1xa0p.png)

and try to update the “React for beginners” course. You will get an error:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/os0ylo42qvlv29x4vo8j.png)

Now, revert the default value of the `visible` prop to `false` and click “Update”
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/w3jfes3rbltgebweqz2b.png)

You can see that the updated course is now marked as (In review) too.

Great, we have created and updated courses using the author account. This is what we needed to be able to test out the [temporal](https://docs.fauna.com/fauna/current/tutorials/temporality.html) feature.

### Temporality
FaunaDB’s [Temporality](https://docs.fauna.com/fauna/current/tutorials/temporality.html) has two features: Snapshots and Events.

#### Snapshots
Snapshots allow us to see the state of our data at a particular point in time. We have just added two new courses and updated one course above. If you run the following FQL in the Shell, you will see all the 7 courses in their latest state including the newly created “Node.js Masterclass” course:
```javascript
Map(
  Paginate(
    Match(Index("allCourses"))
  ),
  Lambda("X", 
    Let({
      ts: Select("ts", Get(Var("X"))),
      data: Select("data", Get(Var("X")))
    },
    {
      ts: Var("ts"),
      data: Var("data")
    }
    )
  )
)
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/aii48wbqr286lq79ipq0.png)

How can we learn which articles we just created and updated? We will use the Snapshots feature to travel back in time to see our data before the updates that we made.

To use the [At](https://docs.fauna.com/fauna/current/api/fql/functions/at) function we need a timestamp. We can get it from the above query which returns a `ts` property for each course (see the first arrow on the screenshot above). 

We know that we did not update the course with the title “NodeJS Tips & Tricks” (we added it when following the previous article) hence we can assume that its timestamp represents a state before the changes that we just made. Let’s check. 

Run the following FQL in the Shell.

Note that you need to copy the timestamp from your list in the Shell above (`ts` property is above the `data` property for each course)

```javascript
At(
  1558455784100000,
  Map(
    Paginate(
      Match(Index("allCourses"))
    ),
    Lambda("X", Get(Var("X")))
  )
)
```
And you will see a list of courses without the newly added  “Node.js Masterclass” course.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4wz5bq9m8w3dyyhff9f1.png)

We remember that we created the “Node.js Masterclass” course first and then updated the “React for beginners” course. This means that with the timestamp of the  “Node.js Masterclass” course, we can get the state for the “React for beginners” course, where the `visible` property is `true` and without the `description` property. 

Copy the timestamp for the “Node.js Masterclass” course from the query result that we made to list all courses and run the following FQL in the Shell:
```javascript
At(
  1594566013530000,
  Map(
    Paginate(
      Match(Index("allCourses"))
    ),
    Lambda("X", Get(Var("X")))
  )
)
```
You will see that the `visible` property is `true` and the `description` property is nowhere to be seen for the “React for beginners” course. The “Node.js Masterclass” course is also listed.

Cool, right? 

#### Events
FaunaDB creates a new copy of the document containing all the changes we’ve made. The original document is never changed. It means that FaunaDB has at least two copies of the “React for beginners” course, one where the `visible` property is `true` and one where it’s `false` and the `description` property is available.

Since we ran a script to update the `visible` property for all courses, we will have more copies. To see them for the “React for beginners” course, run the following FQL:
```javascript
Paginate(
  Events(
    Select(
      "ref",
      Get(Match(Index("course_by_title"), "React for beginners"))
    )
  )
)
```
The above query will list all copies created as a result of changes that were made to the above-mentioned course. I have four copies. The first change has an action “create” when the document was created and the other three have action “update”. 

You should see the first item in the list without the `visible` property. Then, one copy with the `visible` property set to `true` and the last copy with the `visible` property set to `false` and the `description` property as well. This is exactly how we changed it. 

I have one more copy which does not represent any meaningful change and the timing of the change tells me that the copy was created when we tried to manually set the default value for the `visible` property to `true` and got an error response. 

Note that here we looked up the course by the name for simplicity. In our UDF we will look up courses by their `_id` field.

Now, when we’ve seen how the temporal features work, let’s use it for our MVP.

### UDF for listing changes for a particular course
We need a custom resolver to return all changes that have been made to any given course. Copy and paste the following FQL and run it in the Shell:
```javascript
Update(
  Function("course_update_history"),
  {
    "body": Query(
      Lambda(["id"],
        Let({
          page: Paginate(
            Events(
              Select(
                "ref",
                Get(Ref(Collection("Course"), Var("id")))
              )
            )
          )
        },
          Var("page")
        )
      )
     )
  }
)
```
Now the `courseUpdateHistory` mutation has a revolver. To test it out in our MVP, we need to create a MANAGER role and a few manager users.

The MANAGER role will have the same rights as the GUEST role with a few extra privileges, namely to read the history for courses, update courses and call the `logout_user` UDF. Run the following FQL in the Shell:
```javascript
CreateRole({
  name: "Manager",
  privileges: [
    {
      resource: Collection("Course"),
      actions: {
        read: true,
        write: true,
        history_read: true
      }
    },
    {
      resource: Index("allCourses"),
      actions: {
        read: true
      }
    },
    {
      resource: Function("course_update_history"),
      actions: {
        call: true
      }
    },
    {
      resource: Function("logout_user"),
      actions: {
        call: true
      }
    },
    {
      resource: Index("allCoursesInReview"),
      actions: {
        read: true
      }
    },
    {
      resource: Collection("Bookmark"),
      actions: {
        read: Query(
          Lambda(
            "bookmarkRef",
            Let(
              {
                bookmark: Get(Var("bookmarkRef")),
                private: Select(["data", "private"], Var("bookmark"))
              },
              Equals(Var("private"), false)
            )
          )
        )
      }
    },
    {
      resource: Index("allBookmarks"),
      actions: {
        read: true
      }
    },
    {
      resource: Index("bookmark_user_by_user"),
      actions: {
        read: true
      }
    },
    {
      resource: Collection("User"),
      actions: {
        read: true,
      }
    },
    {
      resource: Index("allUsers"),
      actions: {
        read: true
      }
    }
  ],
  membership: [
    {
      resource: Collection("User"),
      predicate: Query(
        Lambda(
          "userRef",
          Equals(Select(["data", "role"], Get(Var("userRef"))), "MANAGER")
        )
      )
    }
  ]
})
```
Run the following FQL in the Shell to create a couple of manager users:
```javascript
Map(
  [
    {
      name: "Manager 1",
      email: "manager1@email.com",
      password: "password",
      role: "MANAGER"
    },
    {
      name: "Manager 2",
      email: "manager2@email.com",
      password: "password",
      role: "MANAGER"
    },
  ],
  Lambda(
    "data",
      Let(
      {
        userRef: Select(
          "ref",
          Call(Function("create_user"), [
            {
              name: Select("name", Var("data")),
              email: Select("email", Var("data")),
              password: Select("password", Var("data")),
              role: Select("role", Var("data"))
            }
          ])
        )
      },
      {
        result: "Success"
      }
    )
    )
)
```
Now we are ready to test how a course review process might look like in real life. This is simply an example showing what data is available.

> Note that mixing manager (admin) functionality with the client-facing app is a bad idea and we do it for simplicity reasons only. It’s much better to have a dedicated app for administration purposes.

Head over to our MVP and log in using the following credentials:
Email: `manager1@email.com`
Password: `password`

You will be redirected to the following page `/app/courses/review` where you will see courses for review.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/mm3dmd6xuxikv9g685a8.png)
Click on the ”See details” link for the first course.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/vbtituj62bkpqoxmy5sm.png)

In the first box, you can see the current state of the course. 
In the second box - how the course was updated. First, it was created, then all other changes are listed (you may see a different list of changes depending on what you changed and in what order).

If you like the changes, you can click ”Approve”

### Conclusion
This is how you can leverage the [FaunaDB's](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) [Temporality](https://docs.fauna.com/fauna/current/tutorials/temporality.html) feature to track changes to any document.

We could create a fancy UI to allow an interactive time travel but this sounds like another project!

With this article finished, we have addressed all the challenges I listed in the [first](https://dev.to/sandorturanszky/how-to-launch-an-mvp-with-jamstack-faunadb-and-graphql-with-zero-running-costs-lfm) one. We have proved that using [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) built-in features combined with Gatsby removes the need to reinvent the wheel and allows us to concentrate on the idea.

This is the approach I am taking with my project. I am using [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) and will share more insights once I release it. Stay tuned.