Introduction

This is a revision of my Todolist app, which uses:

AWS lambda
DynamoDB
IAM
API Gateway
MUI

This project is the first time I build a serverless app on cloud and utilize a noSQL database into production. A doc that record the problems and features of the app might be helpful.

![[Pasted image 20230120223602.png]]

Basic AWS setup

Create Role

  1. Go into DynamoDB, create a table first
  2. copy the tabel ARN![[Pasted image 20230120203223.png]]
  3. Create Role ![[Pasted image 20230120203745.png]]
  4. On the next page, Add AWSLambdaBasicExecutionRole permission
  5. Create role
  6. Click into the role and select inline policy![[Pasted image 20230120204354.png]]
  7. Select DynamoDB for service, set up actions and put in the table ARN![[Pasted image 20230120204521.png]]

    Functions (Lambda)

    Creating functions are less complicated. Remember to use the execution role we just created ![[Pasted image 20230120205437.png]]

API

Create a REST API with Edge optimized endpoint![[Pasted image 20230120210531.png]]
Set up the method as above.

The POST method need to specify a model ![[Pasted image 20230120211023.png]]

![[Pasted image 20230120211144.png]]

Create New Task

set up appropriate useState variables to store values and store error state.

1
2
3
4
5
6
7
8
<TextField
onChange={(e) => setQuantity(e.target.value)}
label="quantity (in total)"
variant="outlined"
color="secondary"
error={quantityError}
/>

When clicking submit button, error check is performed

1
2
3
4
5
6
7
if (title == "") {

      setTitleError(true);

      setAlert("set title");

    } else if ...

Posting data

There’s not too much worth mentioning on the frontend side. One thing when we want to find the difference in two dates in days, use:

1
2
3
4
5
(differenceInCalendarDays(
new Date(endValue),
new Date(startValue)
) +
1)

In this way, two same date value will have a difference of 0.

where end and startValue is set using Datepicker

1
2
3
4
5
6
7
<DatePicker
label="End Date"
value={endValue}
error={endError}
onChange={(newValue) => {
setEnd(newValue.toLocaleDateString());
}}

![[Pasted image 20230121205321.png]]
This can put the date into readable format, and mm/dd/yyyy and be passed into Date() as well.

AWS putItem

1
2
3
4
5
6
7
8
9
10
11
12
const crypto = require('node:crypto');

......

const params={
TableName:"Tasks",
Item:{
PK:crypto.randomUUID(),
SK:"task",
...
}
}

DynamoDB is noSQL so we will need a sort key to separate different items, in this case, set tasks’ SK to “task”. PK is basically an ID, hence the above UUID method would work.
https://stackoverflow.com/questions/11721308/how-to-make-a-uuid-in-dynamodb

View name and details of the task

1
2
3
4
5
6
7
8
9
const params={
TableName:"Tasks",
FilterExpression:"contains(SK,:sk)",
ExpressionAttributeValues:{
":sk": "task"
}
}
try{
const data = await documentClient.scan(params).promise();

Use FilterExpression:"contains(SK,:sk)", and documentClient.scan(params) to get all objects with specific condition (eg. sort key)

Fetch items as usual, we can put a setLoading(false); when we recieved the response to achieve a progress bar function

Sort

In this case, we also want to sort items by some value.

response.data.forEach((note) => sortByTime(note));

1
2
3
4
5
6
7
8
9
const sortByTime = (note) => {

    if (note.timeSection == "Morning") {

      setMorning((morning) => [...morning, note]);

    } else if ...

  };

setTheArray(oldArray => [...oldArray, newElement]) can set a new array in useState hook without “appending” new object

then

1
2
3
4
5
6
7
8
9
10
11
12
13
<Grid container>

        <Grid item style={{ margin: "auto", padding: "20px" }} xs={10}>

          <Typography variant="h3">Morning</Typography>

          {loading ? <CircularProgress /> : <></>}

        </Grid>



        {morning.map((note) => renderTask(note))}

make a note of the {loading ? <CircularProgress /> : <></>} to achieve progress bar function.

The child component will then render the task one by one.

View progress

The thing worth mentioning here is the calculation of current date and end date.
(differenceInCalendarDays(new Date(note.endDate), new Date()) + 1);

react-bootstrap progress bar:

1
2
3
4
5
6
7
<ProgressBar
striped
variant="success"
now={note.currentProgress}
key={1}
label={`${note.currentProgress.toFixed(1)}%`}
/>

to calculate progress today:

1
2
3
const progressToday =
(100 - note.currentProgress) /
(differenceInCalendarDays(new Date(note.endDate), new Date()) + 1);

This value will be added to the current progress and pushed to database when updating.

Finish task

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<CompleteButton
variant="contained"
color={color}
disabled={disabled}
onClick={() => handleUpdate(progressToday)}
>
{buttonText}
</CompleteButton>

const handleUpdate = (progress) => {
setDisabled(true);
axios
.patch(`${config.api.invokeUrl}/tasks/${note.PK}`, {
id: note.PK,
taskName: note.taskName,
taskDetail: note.taskDetail,
startDate: note.startDate,
endDate: note.endDate,
totalHours: note.totalHours,
timeSection: note.timeSection,
currentProgress: note.currentProgress + progress,
lastFinishDate: date,
quantity: note.quantity,
quantifier: note.quantifier,
})
.then(setColor("success"));
};

From the AWS end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let {currentProgress, lastFinishDate, id,taskName,taskDetail,startDate, endDate,totalHours, timeSection,quantity,quantifier} = JSON.parse(event.body)

const params={
TableName:"Tasks",
Key:{
PK:id,
SK:"task"
},
UpdateExpression:"SET lastFinishDate = :l, currentProgress = :cp, taskName = :tn, taskDetail = :td, startDate = :sd, endDate = :ed, totalHours = :h, timeSection = :ts, quantity = :q, quantifier = :qf",
ExpressionAttributeValues:{
":l": lastFinishDate,
":cp": currentProgress,
":tn": taskName,
":td":taskDetail,
":sd": startDate,
":ed": endDate,
":h": totalHours,
":ts": timeSection,
":q":quantity,
":qf":quantifier,

},
ReturnValues: "UPDATED_NEW"
};

The updated attributes must all be there.
Or just update single attribute, but some helper method is needed, havent tried yet.:
https://stackoverflow.com/questions/55825544/how-to-dynamically-update-an-attribute-in-a-dynamodb-item

Deleting task

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<IconButton>
<AlertDialog action={() => handleDelete(note.PK)}></AlertDialog>
</IconButton>

const handleDelete = (id) => {
axios
.delete(`${config.api.invokeUrl}/tasks/${id}`)
.then(() => {
setNotes(
notes.filter((note) => {
console.log(note.PK);
return note.PK != id;
})
);

// notes.forEach((note) => sortByTime(note))
})
.catch((err) => console.log(err));
};

The setNotes doesn’t seem to be working

The AlertDialog component:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
return (
<div>
<DeleteOutlined onClick={()=>handleClickOpen()} />
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{"Alert"}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Do you want to delete this task?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={()=>{action();setOpen(false)}}>Yes</Button>
<Button onClick={handleClose} autoFocus>
No
</Button>
</DialogActions>
</Dialog>
</div>
);
}

Make a note on how to pass in the function <AlertDialog action={() => handleDelete(note.PK)}></AlertDialog> and <Button onClick={()=>{action();setOpen(false)}}>Yes</Button>

Milestones

Create

milestones are separated from the task object which are added in the TaskDetail pop up page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Button
onClick={()=>handleSubmit()}
color="secondary"
variant="contained"
disabled={disabled}
endIcon={<KeyboardArrowRightIcon />}
>
Add
</Button></>}

const handleSubmit=()=>{
setDisabled(true);
axios
.post(`${config.api.invokeUrl}/milestone`, {
"taskID":note.PK, "milestoneName":details, "deadline":startValue
})
.then(() => {navigate("/");setOpen(false);setDisabled(false);setMilestone([...milestone, {"milestoneName":details, "deadline":startValue}])})
.catch((err) => console.log(err));
}

.then(() => {navigate("/");setOpen(false);setDisabled(false);setMilestone([...milestone, {"milestoneName":details, "deadline":startValue}])})

The setMilestone function and milestone is passed from te parent component to achieve a real time update effect.

The lambda function uses milestone+name of the milestone as the SK (this is not safe), PK as the taskID.

1
2
3
4
5
6
7
8
9
Item:{
PK:taskID,
SK:"milestone_"+milestoneName,
milestoneName: milestoneName,
milestoneDetail:milestoneDetail,
deadline:deadline,
expectedProgress:expectedProgress

}

Get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const {id} = event.pathParameters

const params={
TableName:"Tasks",
KeyConditionExpression:"PK = :pk AND begins_with(SK, :sk)",
ExpressionAttributeValues:{
":pk":id,
":sk": "milestone"
}
}
try{
const data = await documentClient.query(params).promise();
responseBody = JSON.stringify(data.Items)
statusCode=200

}

fetch milestones of one task at a time, pass in the task ID and narrow the result down to milestone objects by using begins_with(SK, :sk) which :sk is "milestone",return as a list

Display

1
2
3
4
5
6
7
8
9
10
11
12
function closestDate(d) {
return differenceInCalendarDays(new Date(d.deadline), new Date()) >= 0;
}
<=====================>
{milestone.filter(closestDate)[0] != null ? (
"Upcoming milestone: " +
milestone.filter(closestDate)[0].milestoneName +
" " +
milestone.filter(closestDate)[0].deadline
) : (
<></>
)}

use milestone.filter(closestDate)[0].milestoneName to get the lowest date difference between deadline date and current date (if not null) and display on task card.

1
2
3
4
5
function custom_sort(a, b) {
return new Date(a.deadline).getTime() - new Date(b.deadline).getTime();
}

{milestone.sort(custom_sort).map(milestone=>(<Typography variant="body">{milestone.milestoneName}, deadline {milestone.deadline}<br></br></Typography>))}

Sort by date

TaskDetail

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<IconButton>
<TaskDetail
note={note}
milestone={milestone}
setMilestone={setMilestone}
></TaskDetail>
</IconButton>

<MoreHorizOutlinedIcon variant="outlined" onClick={handleClickOpen}>
Open Task Detail
</MoreHorizOutlinedIcon>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
1
2
3
4
<IconButton>
<MoreHorizOutlinedIcon>
<Dialog/>
</IconButton>

👆is the structure of creating pop up dialogue.

Editing

<EditIcon onClick={()=>{setEdit(!edit)}}></EditIcon>
sets the edit state
{edit ? <></>:<></>}
decides what to display based on edit state.

Bugs

Module not found: Error: Can’t resolve ‘date-fns/addDays’

https://stackoverflow.com/questions/71037974/module-not-found-error-cant-resolve-date-fns-adddays-in-c-users

Solution:

DatePicker requiresdate-fns package from NPM using npm install --save date-fns.

How do I include a file over 2 directories back?

https://stackoverflow.com/questions/162873/how-do-i-include-a-file-over-2-directories-back

Solution:

.. selects the parent directory from the current. Of course, this can be chained:
../../index.php This would be two directories up.

npm install goes to dead in China

https://stackoverflow.com/questions/22764407/npm-install-goes-to-dead-in-china

Solution:

There is a Chinese registry now too:

1
2
$ npm config set registry http://r.cnpmjs.org
$ npm install
React Hooks: useEffect() is called twice even if an empty array is used as an argument
solution

React.Strict mode is on

StrictMode renders components twice (on dev but not production) in order to detect any problems with your code and warn you about them (which can be quite useful).

Remove Json object from json array element (eg delete task from list of task and display dynamically)

solution: not tested https://stackoverflow.com/questions/48163429/remove-json-object-from-json-array-element

passing props

https://stackoverflow.com/questions/71959559/nextjs-cant-render-a-component-while-using-map-over-a-array-of-objects-objects

You are passing the props in a wrong way. Either use it as a single object in props or have all the props it inside {} using destructuring method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default function List({email, phone, nick}) {}


OR

export default function List(props) {
return (
<div align="center">
<hr />
<strong>Email: </strong>
<p>{props.email}</p>
<strong>Nick: </strong>
<p>{props.phone}</p>
<strong>Phone: </strong>
<p>{props.nick}</p>
</div>
Uncaught Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop

https://stackoverflow.com/questions/55265604/uncaught-invariant-violation-too-many-re-renders-react-limits-the-number-of-re

Solution:

use

1
const [open, setSnackBarState] = useState(variant ? true : false); 

instead of:

1
2
3
4
const [open, setSnackBarState] = useState(false);
if (variant) {
setSnackBarState(true); // HERE BE DRAGONS
}