Build a Post activity log using React & AWS Amplify

June 03, 2019

Post Cover

This is a series of posts where I document my learnings & findings while building two webapps for two family businesses

TL;DR. checkout this example repo on how to achieve this using AWS Amplify & React. Any feedback is welcome!


At Hamerlin, as in any other company, data is crucial. having a goto place to find all the information about clients & reports is essential to the business. Not only that, but knowing who and when update, create and delete data.

We've seen this in many of the services we use in our daily lives, eg: Trello or Google Drive. Because is something usually users expects, I naively thought that it was something ready to plugin in place and start using it, but it doesn't. In fact, It's more complex that you might think. To illustrate a real world example, let's take a look at how Trello approach this:

How Trello stores User's activity data

they call them Actions, and as in there website, here's their definition:

Actions are generated whenever an action occurs in Trello. For instance, when a user deletes a card, a deleteCard action is generated and includes information about the deleted card, the list the card was in, the board the card was on, the user that deleted the card, and the idObject of the action.

Trello Actions 2 Trello Actions in the activity feed in the board sidebar

Trello Actions 1 Trello Actions in a card's activity feed

Basically they not only store all the changes that happened, but they identify changes by type and store it accordingly, including a bunch of data related to the modified resource. This is important for them because they not only list the generic changes, but they also include links to the resource, related data like the board was in, the column. the user, etc.

Trello Actions API Action Object

To be honest, this approach is amazing but is TOO complex to what (I think?) we can achieve in just a couple of hours. I want to build this app incrementally, so I will present you my fisrt approach and then we can talk about the ideal escenario.


Let's build our Activity log

First of all, make sure you have an Amplify + React project setup ready to start. you can use whatever entity you want, in this cas I will use a Post type for the sake of simplicity.

Let's define our schema

As I said before, this is the simplest approach to get the job done. it may be better ways to do this, but we are Lean developers and we can improve this in another iteration :)

The goal is to store the history of changes of each post.

the fields I want to store per-change are:

  • the post ID
  • the author of the change
  • a timestamp of the change
  • the action type
  • and the resulted post after the change

after running amplify add api, we can edit our schema.graphql. It should look something like this:

type Post @model {
  id: ID!
  title: String!
  slug: String!
  content: String!
}

type PostHistory @model @searchable {
  id: ID!
  postId: ID!
  creator: String!
  createdAt: String
  action: PostAction
  payload: HistoryPayload
}

enum PostAction {
  CREATED
  UPDATED
  DELETED
}

type HistoryPayload {
  title: String
  slug: String
  content: String
}

Then we are ready to run amplify push and start testing our implementation. But first let me explain my approach here:

The idea is that everytime a Post is being either CREATED, UPDATED OR DELETED, I also create a new PostHistory that stores data from the previews operation. The cool thing is that because we are using GraphQL, we know before hand which operation the user wants to execute, so it's easier to populate the PostAction enum from it in our code. Also we have all the possible queries & mutations generated by the Amplify CLI 😉

Another thing to point here is the HistoryPayload type. I use a simple type here instead of using the special @connection directive from AWS Amplify, because I'm interested in storing the exact post's data at that moment when the operation happened. Using the @connection will store an actual reference to the Post, and that's not the goal of that payload.

Create a post using React

First we need to create our CreatePost component with a simple form. Let's focus on the handleSubmit method:

const handleSubmit = async e => {
  e.preventDefault();
  const date = new Date();
  const input = { title, content, slug };

  const { data } = await API.graphql(graphqlOperation(createPost, { input }));
  if (data.createPost.id) {
    const postHistory = await API.graphql(
      graphqlOperation(createPostHistory, {
        input: {
          postId: data.createPost.id,
          creator: "horacio",
          createdAt: date,
          action: "CREATED",
          payload: {
            title: data.createPost.title,
            slug: data.createPost.slug,
            content: data.createPost.content
          }
        }
      })
    );
    history.push(`/post/${data.createPost.id}`);
  }
}

As you can see, I'm creating a post as you normally will do with the Amplify's API module. After the creation, I take the result and create a new PostHistory using the generated createPostHistory mutation. You should see something like this:

You can also checkout how the UpdatePost component works, it's pretty similar to the creation.


Some considerations

Of course this is a really simplistic example, and it has a couple of limitations that I should consider for future iterations:

  • Currently the creator attribute is a string, but eventually I would like this to be a connection to the actual user that's logged in for each operation, this will let me not only get access to more data from the user, but also flexible for changes on the user's data.
  • Now I now this is a more REST approach, we are making 2 round trips to the server for one single user's operation. In the future, I would like this to be automatic, and maybe call a Lambda function from any DynamoDB event? or maybe a pipeline resolver could work?
  • if you see the Post.js component, you see that I create aldo 2 queries to get the data for the frontend. This is OK, but maybe I would like to change my schema to something like this:
type Post @model {
  id: ID!
  title: String!
  slug: String!
  content: String!
  actions: [PostHistory] # <== THIS!!
}

I think having the actions inside the Post type it's more ellegant. I don't know how difficult this may be, but it looks cool right? This may be possible using the new @function directive, I can execute a Lambda function and access the dynamoDB database directly from here. This is someting I will try very soon :)


You can checkout the final result on this repo and let me know any feedback!. I'm learning a lot doing this examples and hope you can take some knowledge from it too!. If you thought any other way to solve this problem or know how to make this example better please let me know!

I would like to thank Kurt & Nader for helping me reviewing this piece of content. They both work as developer advocates at AWS Amplify and they are doing an amazing work helping people like me in the community. kudos!!

I'm going to continue sharing content like this, having your support would be awesome!