Connecting Svelte front-end to AWS back-end

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>
https://stackoverflow.com/questions/58262380/how-to-pass-parameters-to-onclick-in-svelte
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>.
https://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.
http://www.css-spinners.com/
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,




