— english, tutorial, React, Amplify, GraphQL — 4 min read
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:
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 in the activity feed in the board sidebar
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.
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.
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.
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:
after running amplify add api
, we can edit our schema.graphql
. It should look something like this:
1type Post @model {2 id: ID!3 title: String!4 slug: String!5 content: String!6}78type PostHistory @model @searchable {9 id: ID!10 postId: ID!11 creator: String!12 createdAt: String13 action: PostAction14 payload: HistoryPayload15}1617enum PostAction {18 CREATED19 UPDATED20 DELETED21}2223type HistoryPayload {24 title: String25 slug: String26 content: String27}
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.
First we need to create our CreatePost
component with a simple form. Let's focus on the handleSubmit
method:
1const handleSubmit = async (e) => {2 e.preventDefault()3 const date = new Date()4 const input = { title, content, slug }56 const { data } = await API.graphql(graphqlOperation(createPost, { input }))7 if (data.createPost.id) {8 const postHistory = await API.graphql(9 graphqlOperation(createPostHistory, {10 input: {11 postId: data.createPost.id,12 creator: "horacio",13 createdAt: date,14 action: "CREATED",15 payload: {16 title: data.createPost.title,17 slug: data.createPost.slug,18 content: data.createPost.content,19 },20 },21 })22 )23 history.push(`/post/${data.createPost.id}`)24 }25}
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.
Of course this is a really simplistic example, and it has a couple of limitations that I should consider for future iterations:
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.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?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:1type Post @model {2 id: ID!3 title: String!4 slug: String!5 content: String!6 actions: [PostHistory] # <== THIS!!7}
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!