Introduction
When you see these two posts I wrote before, it's natural to think about connecting this front-end and back-end. That's what I'm going to do this time.
What I am talking about are these two:
The outcome
At any rate, here's the finished demo screen.
The architecture
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Frontend │ │ APIGW │ │ Lambda │ │ │
│ (Svelte) ├─────┤► (REST) ├──────► with ├─────► DynamoDB │
│ │ │ │ │ RubySDK │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
The code
Svelte
This is a modified version of the previous version ... (1), which was running only on the frontend. It makes API calls now.
Todo.svelte
<script>
const baseURL = 'https://<api-id>.execute-api.<region>.amazonaws.com/dev/tasks'
let taskName = ""
let checked = false
let modalOn = false
// sort
function compare( a, b ) {
console.log('compare called')
if ( a['updated-at'] > b['updated-at'] ){ return -1; }
if ( a['updated-at'] < b['updated-at'] ){ return 1; }
return 0;
}
function sortByCreatedAt(todos){
console.log('sortBy called')
todos.then(data => {
data = data.sort(compare);
});
return todos
}
function statusText(taskStatus){ return taskStatus ? "Active" : "Done" }
const listTasks = async () => {
console.log('listTasks called')
const res = await fetch(baseURL);
return await sortByCreatedAt(res.json())
}
const getTask = async (taskId) => {
console.log('getTask called')
const task = await fetch(baseURL + '/' +taskId);
return await task.json()
}
const addTask = async (taskName) => {
console.log('addTask called!')
let taskData = {
"task-id": Date.now().toString(36),
"is-active": true,
"task-name": "",
"updated-at": Date.now().toString(),
"created-at": Date.now().toString(),
"user-id": "100"
}
taskData['task-name'] = taskName;
const res = await fetch(baseURL, {
method: 'POST',
body: JSON.stringify(taskData)
});
todos = listTasks()
}
const deleteTask = async (taskId) => {
console.log('deleteTask called!')
const res = await fetch(baseURL + '/' + taskId, {
method: 'DELETE',
});
todos = listTasks() // assignment to ignite the reactivity
}
const completeTask = async (taskId, status) => {
const res = await fetch(baseURL + '/' + taskId, {
method: 'PUT',
body: JSON.stringify({ 'task-id': taskId, 'is-active': !status })
});
todos = listTasks()
}
let taskDetail = ''
function showDetail(taskId) {
console.log('showDetail called')
modalOn = true
//return getTask(taskId)
taskDetail = getTask(taskId)
}
// format date
function fd(date) {
console.log('fd called')
let d = new Date(parseInt(date))
return d.getFullYear() + '/' + d.getMonth() + '/' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes()
}
// todos
let todos = listTasks(); // Initialized as Promise
$: activeTodos = checked ? todos : todos.then(data => data.filter(todo => todo['is-active']))
</script>
<main>
<h1>HELLO TODO!</h1>
<div class="container is-fluid">
<div class="columns is-centered">
<div class="field has-addons">
<div class="control" style="margin-bottom: 5em;">
<input class="input" type="text" placeholder="Add a task" bind:value={taskName}>
</div>
<div class="control">
<a class="button is-danger" on:click={() => addTask(taskName)}><i class="fas fa-plus"></i></a>
</div>
</div>
</div>
<div class="columns" style="width: 200px; margin-left: 60%; margin-bottom: 3em">
<div class="field">
<input id="switchRoundedDanger" type="checkbox" name="switchRoundedDanger" class="switch is-rounded is-danger is-rtl" bind:checked={checked}>
<label for="switchRoundedDanger">Show completed</label>
</div>
</div>
<div class="columns is-centered">
{#await activeTodos}
<span class="spinner-loader" style="margin-top: 10em">Loading…</span>
{:then tasks}
<table class="table is-hoverable">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Created at</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{#each tasks as task}
<tr>
<td>{task['task-id']}</td>
<td>{task['task-name']}</td>
<td>{fd(task['created-at'])}</td>
<td>{statusText(task['is-active'])}</td>
<td>
<i class="fas fa-check" on:click={() => completeTask(task['task-id'], task['is-active'])}></i>
<i class="fas fa-info" on:click={() => showDetail(task['task-id'])}></i>
<i class="far fa-trash-alt" on:click={() => deleteTask(task['task-id'])}></i>
</td>
</tr>
{/each}
</tbody>
</table>
{/await}
</div>
<div class="modal { modalOn ? 'is-active' : ''}">
<div class="modal-background"></div>
<div class="modal-content">
<dev class="box">
{#await taskDetail}
<span class="spinner-loader" style="margin-top: 10em">Loading…</span>
{:then td}
<table class="table is-fullwidth">
<thead>
<tr><td>key</td><td>value</td></tr>
</thead>
<tbody>
<tr><td>task-id</td><td><span class="tag">{td['task-id']}</span></td></tr>
<tr><td>task-name</td><td><span class="tag">{td['task-name']}</span></td></tr>
<tr><td>created-at</td><td><span class="tag">{fd(td['created-at'])}</span></td></tr>
<tr><td>updated-at</td><td><span class="tag">{fd(td['updated-at'])}</span></td></tr>
<tr><td>is-active</td><td><span class="tag">{td['is-active']}</span></td></tr>
<tr><td>user-id</td><td><span class="tag">{td['user-id']}</span></td></tr>
</tbody>
</table>
{/await}
</dev>
</div>
<button class="modal-close is-large" aria-label="close" on:click="{() => modalOn = false}"></button>
</div>
</div>
</main>
<style>
main {
text-align: center;
padding: 1em;
max-width: 240px;
}
h1 {
color: #ff3e00;
text-transform: uppercase;
font-size: 4em;
font-weight: 100;
margin-bottom: 1.5em;
}
td i {
padding: 5px;
cursor: pointer;
color: gray;
}
label { font-family: monospace; }
table { font-family: monospace; }
@media (min-width: 640px) {
main {
max-width: none;
}
}
</style>
Lambda
It is pretty much the same thing as my previous post ... (2)
lambda_handler.rb
require 'json'
require 'json/add/exception'
require 'aws-sdk-dynamodb'
CORS_HEADER = {
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Origin": '*',
"Access-Control-Allow-Methods": "OPTIONS,POST,PUT,GET"
}
def add_task(table, body)
begin
table.put_item({ item: body })
{ statusCode: 200, headers: CORS_HEADER, body: JSON.generate(body) }
rescue => e
{ statusCode: 500, body: e.to_json }
end
end
def delete_task(table, task_id)
begin
params = { table_name: table, key: { 'task-id': task_id } }
table.delete_item(params)
list_task(table)
rescue => e
{ statusCode: 500, body: e.to_json }
end
end
def update_task(table, body)
begin
params = {
table_name: table,
key: { 'task-id': body['task-id'] },
attribute_updates: {
'is-active': { value: body['is-active'], action: "PUT" },
}
}
table.update_item(params)
{ statusCode: 200, headers: CORS_HEADER, body: JSON.generate(body) }
rescue => e
{ statusCode: 500, headers: CORS_HEADER, body: e.to_json }
end
end
def list_task(table)
begin
scan_output = table.scan({ limit: 50, select: "ALL_ATTRIBUTES" })
{ statusCode: 200, headers: CORS_HEADER, body: JSON.generate(scan_output['items']) }
rescue => e
{ statusCode: 500, body: e.to_json }
end
end
def get_task(table, task_id)
begin
params = { key: { 'task-id': task_id } }
task = table.get_item(params)
{ statusCode: 200, headers: CORS_HEADER, body: JSON.generate(task['item']) }
rescue => e
{ statusCode: 500, body: e.to_json }
end
end
def lambda_handler(event:, context:)
begin
http_method = event['httpMethod']
dynamodb = Aws::DynamoDB::Resource.new(region: 'us-east-2')
table = dynamodb.table('todoTable-dev')
case http_method
when 'GET'
path_param = event.dig('pathParameters', 'proxy')
if path_param.nil?
list_task(table)
else
get_task(table, path_param)
end
when 'PUT' then update_task(table, JSON.parse(event['body']))
when 'POST' then result = add_task(table, JSON.parse(event['body']))
when 'DELETE' then delete_task(table, event['pathParameters']['proxy'])
else 0
end
rescue => e
{ statusCode: 500, body: e.to_json }
end
end
Findings
Don't be fooled by mere TODO apps. Especially if you're learning a new framework.
Svelte
1. Await block is kinky but nice.
The first time you see it, you might think, "Wow, you're going to await
in a template!" But it's nice. I like it.
<h1>Task list</h1>.
{#await todos}
<p>Loading... </p> <p>Loading...
{:then tasks}
{#each tasks as task}
<p>{task['task-id']}, {task['task-name']}</p>
{/each}
{/await}
2. How to pass arguments to functions fired by on:click
.
This is fine for calling functions with no arguments, but
<button on:click="{listTask}"></button>
If there is an argument, this will cause the function to execute immediately instead of on click
<button on:click="{getTask(taskId)}"></button>
Instead, write
<button on:click="{() => getTask(taskId)}"></button>
stackoverflow.com/questions/58262380/how-to..
3. You can't call two functions in one action.
I thought I could do this like Vue
, but no luck. Well, you can call the second function inside the first one.
<button on:click="{listTask; getTask}"></button>.
github.com/sveltejs/svelte/issues/2109
4. selected
and other shorthands are good.
When opening a checkbox or modal, you may want to manipulate class
such as selected
.
<style> ...
/* ... . .other CSS... */ ...
span.cell.selected {
outline-color: lightblue;
outline-style: dotted;
}
</style>
<span class="cell {selected === true ? 'selected' : ''"}>
{value}
</span>
which can be written as follows, but
<span class="cell" class="selected" class="{selected}">
{value}
</span>
And further shorthand for this
<span class="cell" class:selected>
{value}
</span>
That's good. However, if you use hyphens in the class name, it won't work.
Source is here
CORS
1. no-cors
in fetch
is not a panacea. It doesn't work with PUT
.
It's sad when you get stuck around CORS. It's not always possible to escape with no-cors
.
2. Adding CORS headers in an error response make the debug easier.
I think this can be done by APIGW Web console like "all 500
has this headers" or something.
JavaScript
1. Date in JS is still painful.
I don't even want to write it.
2. You can't escape Promise.
No matter how hard I try to cut out only the content, it always comes back to me as Promise
. You have to get along with it.
Use this for the CSS of Loading
It's the little things like this that change the atmosphere and keep you motivated to create a little bit longer.
DynamoDB
1. Sorting is not done in the query, but in a table with a sort key...
I should have used Firebase...
2. If you forget to add a sort key, you have to recreate the table.
Your only option is to create a new table with redefined key attributes and then copy Your only option is to create a new table with redefined key attributes and then copy the data from the existing table to the newly created table.
Seriously... I sorted in the front-end.
3. :return_values
is not so good.
The :return_values
function is not very useful, because it returns the values before the update (like ALL_OLD
). What kind of applications are you expecting?
Lambda
At first, I thought it would be easier for the client, so when Lambda accepts a POST
or PUT
, it returns the updated task list as a GET
, but you shouldn't do that. It's better to make each request Atomic and handle it from the front side to avoid complications.
Cheers,