Skip to content

Commit

Permalink
Get books from DDB
Browse files Browse the repository at this point in the history
  • Loading branch information
rimutaka committed Aug 23, 2024
1 parent 5dba2ab commit 2d81fcb
Show file tree
Hide file tree
Showing 20 changed files with 419 additions and 61 deletions.
8 changes: 4 additions & 4 deletions build/asset-manifest.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
{
"files": {
"main.css": "./static/css/main.71786728.css",
"main.js": "./static/js/main.3726aeb7.js",
"static/media/isbn_mod_bg.wasm": "./static/media/isbn_mod_bg.4f74f3097d9ed7af09bd.wasm",
"main.js": "./static/js/main.267c14ac.js",
"static/media/isbn_mod_bg.wasm": "./static/media/isbn_mod_bg.c7f2ea4a54633e615edd.wasm",
"static/media/buy.svg": "./static/media/buy.a0ebbd4b83f7c8afd5d9.svg",
"static/media/icomoon.woff": "./static/media/icomoon.25908258b2a9c6e6da52.woff",
"index.html": "./index.html",
"static/media/about.svg": "./static/media/about.472d9c94914ce88e8d8f.svg",
"static/media/borrow.svg": "./static/media/borrow.f8356d8f6c1fc40fa23b.svg",
"main.71786728.css.map": "./static/css/main.71786728.css.map",
"main.3726aeb7.js.map": "./static/js/main.3726aeb7.js.map"
"main.267c14ac.js.map": "./static/js/main.267c14ac.js.map"
},
"entrypoints": [
"static/css/main.71786728.css",
"static/js/main.3726aeb7.js"
"static/js/main.267c14ac.js"
]
}
2 changes: 1 addition & 1 deletion build/index.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype html><html lang="en"><head prefix="og: http://ogp.me/ns#"><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="referrer" content="no-referrer"/><meta name="theme-color" content="#ffffff"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>📖📚📚</title><meta name="description" content="A pocket assistant for keen readers: find more information about the book or the author online, borrow it from your local library, buy, sell or share."><meta property="og:title" content="Scan ISBN to record or share a book"/><meta property="og:description" content="A pocket assistant for keen readers: find more information about the book or the author online, borrow it from your local library, buy, sell or share."><meta property="og:site_name" content="Bookworm Food"><meta property="og:type" content="website"><meta property="og:url" content="https://bookwormfood.com"><meta property="og:image" itemprop="image" content="http://bookwormfood.com/img/og-image-400.png"/><meta property="og:image:secure_url" itemprop="image" content="https://bookwormfood.com/img/og-image-400.png"><meta property="og:image:width" content="400"><meta property="og:image:height" content="400"><meta name="twitter:card" content="summary"/><meta name="twitter:title" content="Scan ISBN to record or share a book"/><meta name="twitter:description" content="A pocket assistant for keen readers: find more information about the book or the author online, borrow it from your local library, buy, sell or share."/><meta name="twitter:image" content="https://bookwormfood.com/img/og-image-400.png"/><link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Amatic+SC:wght@400;700&display=swap" rel="stylesheet"><link href="https://fonts.googleapis.com/css2?family=News+Cycle:wght@400;700&display=swap" rel="stylesheet"><script defer="defer" src="/static/js/main.3726aeb7.js"></script><link href="/static/css/main.71786728.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="app"></div></body></html>
<!doctype html><html lang="en"><head prefix="og: http://ogp.me/ns#"><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="referrer" content="no-referrer"/><meta name="theme-color" content="#ffffff"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>📖📚📚</title><meta name="description" content="A pocket assistant for keen readers: find more information about the book or the author online, borrow it from your local library, buy, sell or share."><meta property="og:title" content="Scan ISBN to record or share a book"/><meta property="og:description" content="A pocket assistant for keen readers: find more information about the book or the author online, borrow it from your local library, buy, sell or share."><meta property="og:site_name" content="Bookworm Food"><meta property="og:type" content="website"><meta property="og:url" content="https://bookwormfood.com"><meta property="og:image" itemprop="image" content="http://bookwormfood.com/img/og-image-400.png"/><meta property="og:image:secure_url" itemprop="image" content="https://bookwormfood.com/img/og-image-400.png"><meta property="og:image:width" content="400"><meta property="og:image:height" content="400"><meta name="twitter:card" content="summary"/><meta name="twitter:title" content="Scan ISBN to record or share a book"/><meta name="twitter:description" content="A pocket assistant for keen readers: find more information about the book or the author online, borrow it from your local library, buy, sell or share."/><meta name="twitter:image" content="https://bookwormfood.com/img/og-image-400.png"/><link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Amatic+SC:wght@400;700&display=swap" rel="stylesheet"><link href="https://fonts.googleapis.com/css2?family=News+Cycle:wght@400;700&display=swap" rel="stylesheet"><script defer="defer" src="/static/js/main.267c14ac.js"></script><link href="/static/css/main.71786728.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="app"></div></body></html>

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions build/static/js/main.267c14ac.js.map

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion build/static/js/main.3726aeb7.js.map

This file was deleted.

Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion build/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ if ("function" === typeof importScripts) {
self.addEventListener("install", event => {
self.skipWaiting();
});
workbox.precaching.precacheAndRoute([{"revision":"18334dfcd779de04c14446e5a61bb278","url":"asset-manifest.json"},{"revision":"4020571efe44dc33d271798e6a18e0c1","url":"favicon.ico"},{"revision":"f98923403f5c78d689d56b18f947c520","url":"img/apple-touch-icon.png"},{"revision":"a62aa63bb4d0a3dd08820787bd7e118f","url":"img/favicon-16x16.png"},{"revision":"a5229a03fcfe584a3031846fe3c19ccf","url":"img/favicon-32x32.png"},{"revision":"37ff8dc0d50cd7705fc65fc84837c2aa","url":"img/og-image-400.png"},{"revision":"e3e1abfea0da26ad437cea4144d29290","url":"index.html"},{"revision":"04e972588e780145b7edd5c11b0902e8","url":"static/css/main.71786728.css"},{"revision":"08cc7b373bfc42f96ed026e44ca2a635","url":"static/js/main.3726aeb7.js"},{"revision":"3311ddb0ad85b9240262a753f5a667b7","url":"static/media/about.472d9c94914ce88e8d8f.svg"},{"revision":"8d00e8b89883db0a8a11d9f91b427d10","url":"static/media/borrow.f8356d8f6c1fc40fa23b.svg"},{"revision":"fb577b765b6c263191ff525af5f4f175","url":"static/media/buy.a0ebbd4b83f7c8afd5d9.svg"},{"revision":"be1c5fee23d7aa9c5b7598defa293dac","url":"static/media/isbn_mod_bg.4f74f3097d9ed7af09bd.wasm"},{"revision":"24f2b115d3964c9f977462cdd38b066a","url":"wasm/koder.js"},{"revision":"6f11e7db4fe9aca82cac7150bfc33769","url":"wasm/zbar.js"},{"revision":"e8789bf03df9c2c85e9c59ab0a0cd0c6","url":"wasm/zbar.wasm"},{"revision":"bb1c649a95ffa80369254cc3e51b9a41","url":"wasmWorker.js"}]);
workbox.precaching.precacheAndRoute([{"revision":"87add44d5bbd6500366aa7acec73c075","url":"asset-manifest.json"},{"revision":"4020571efe44dc33d271798e6a18e0c1","url":"favicon.ico"},{"revision":"f98923403f5c78d689d56b18f947c520","url":"img/apple-touch-icon.png"},{"revision":"a62aa63bb4d0a3dd08820787bd7e118f","url":"img/favicon-16x16.png"},{"revision":"a5229a03fcfe584a3031846fe3c19ccf","url":"img/favicon-32x32.png"},{"revision":"37ff8dc0d50cd7705fc65fc84837c2aa","url":"img/og-image-400.png"},{"revision":"1b643c029cc2999f9b0cb13b4473d0e3","url":"index.html"},{"revision":"04e972588e780145b7edd5c11b0902e8","url":"static/css/main.71786728.css"},{"revision":"c3e099fec758fe0793909fffdd5f0280","url":"static/js/main.267c14ac.js"},{"revision":"3311ddb0ad85b9240262a753f5a667b7","url":"static/media/about.472d9c94914ce88e8d8f.svg"},{"revision":"8d00e8b89883db0a8a11d9f91b427d10","url":"static/media/borrow.f8356d8f6c1fc40fa23b.svg"},{"revision":"fb577b765b6c263191ff525af5f4f175","url":"static/media/buy.a0ebbd4b83f7c8afd5d9.svg"},{"revision":"88569f0509b3727aa58d086aa3e5889f","url":"static/media/isbn_mod_bg.c7f2ea4a54633e615edd.wasm"},{"revision":"24f2b115d3964c9f977462cdd38b066a","url":"wasm/koder.js"},{"revision":"6f11e7db4fe9aca82cac7150bfc33769","url":"wasm/zbar.js"},{"revision":"e8789bf03df9c2c85e9c59ab0a0cd0c6","url":"wasm/zbar.wasm"},{"revision":"bb1c649a95ffa80369254cc3e51b9a41","url":"wasmWorker.js"}]);
workbox.routing.registerRoute(
new RegExp("https://fonts.(?:.googlepis|gstatic).com/(.*)"),
new workbox.strategies.CacheFirst({
Expand Down
122 changes: 115 additions & 7 deletions rust/lambdas/client-sync/src/book.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
use std::str::FromStr;

use crate::USER_BOOKS_TABLE_NAME;
use anyhow::Error;
use aws_sdk_dynamodb::{types::AttributeValue, Client};
use bookwormfood_types::Book;
use bookwormfood_types::{Book, Books, ReadStatus};
use chrono::{DateTime, Utc};
use tracing::info;

const FIELD_UID: &str = "uid"; // used in a query
const FIELD_ISBN: &str = "isbn";
const FIELD_TITLE: &str = "title";
const FIELD_AUTHORS: &str = "authors";
const FIELD_UPDATED: &str = "updated";
const FIELD_READ_STATUS: &str = "read_status";

/// Save a book in the user_books table.
/// Replaces existing records unconditionally.
pub(crate) async fn save(book: &Book, client: &Client, uid: &str) -> Result<(), Error> {
match client
.put_item()
.table_name(USER_BOOKS_TABLE_NAME)
.item("uid", AttributeValue::S(uid.to_string()))
.item("isbn", AttributeValue::N(book.isbn.clone()))
.item("title", attr_val_s(&book.title))
.item("authors", attr_val_ss(&book.authors))
.item("updated", AttributeValue::S(book.timestamp_update.to_rfc3339()))
.item(FIELD_UID, AttributeValue::S(uid.to_string()))
.item(FIELD_ISBN, AttributeValue::S(book.isbn.clone()))
.item(FIELD_TITLE, attr_val_s(&book.title))
.item(FIELD_AUTHORS, attr_val_ss(&book.authors))
.item(FIELD_UPDATED, AttributeValue::S(book.timestamp_update.to_rfc3339()))
.item(
"read_status",
FIELD_READ_STATUS,
book.read_status
.map_or_else(|| AttributeValue::Null(true), |v| AttributeValue::S(v.to_string())),
)
Expand All @@ -32,12 +44,108 @@ pub(crate) async fn save(book: &Book, client: &Client, uid: &str) -> Result<(),
}
}

/// Returns all book records for the given user.
/// Returns an empty list if no records found.
pub(crate) async fn get_by_user(client: &Client, uid: &str) -> Result<Books, Error> {
let books = match client
.query()
.table_name(USER_BOOKS_TABLE_NAME)
.key_condition_expression("#uid = :uid")
.expression_attribute_names("#uid", FIELD_UID)
.expression_attribute_values(":uid", AttributeValue::S(uid.to_string()))
.send()
.await
{
Ok(v) => match v.items {
// convert the items into books
Some(items) => {
let mut books = Vec::with_capacity(items.len());
// loop thru the records
for item in items {
let mut book = Book {
isbn: "".to_string(),
title: None,
authors: None,
timestamp_update: DateTime::<Utc>::MIN_UTC,
read_status: None,
cover: None,
timestamp_sync: None,
volume_info: None,
};

// iterate through the list of attributes for the record
// instead of looking them up by name
for attr in item {
match attr.0.as_str() {
FIELD_ISBN => book.isbn = attr_to_string(attr.1),
FIELD_TITLE => book.title = attr_to_option(attr.1),
FIELD_AUTHORS => {
book.authors = match attr.1 {
AttributeValue::Ss(v) => Some(v),
_ => None,
}
}
FIELD_UPDATED => {
book.timestamp_update = match DateTime::parse_from_rfc3339(&attr_to_string(attr.1)) {
Ok(v) => v.into(),
Err(_) => DateTime::<Utc>::MIN_UTC,
}
}
FIELD_READ_STATUS => book.read_status = ReadStatus::from_str(&attr_to_string(attr.1)).ok(),
_ => {}
}
}

// skip the book if invalid
if !book.is_valid_isbn() {
info!("Invalid ISBN {} / {:?}", uid, book);
continue;
}

books.push(book);
}

Books { books }
}
None => {
info!("No books found for user {}", uid);
Books { books: Vec::new() }
}
},
Err(e) => {
info!("Failed to get books for {}: {:?}", uid, e);
return Err(Error::msg("Failed to save book".to_string()));
}
};

info!("Returning {} books for {}", books.books.len(), uid);
Ok(books)
}

///Converts the value into an AttributeValue
fn attr_val_s(v: &Option<String>) -> AttributeValue {
v.as_ref()
.map_or_else(|| AttributeValue::Null(true), |v| AttributeValue::S(v.clone()))
}

///Converts the value into an AttributeValue
fn attr_val_ss(v: &Option<Vec<String>>) -> AttributeValue {
v.as_ref()
.map_or_else(|| AttributeValue::Null(true), |v| AttributeValue::Ss(v.clone()))
}

/// Converts the AttributeValue into a string
fn attr_to_string(v: AttributeValue) -> String {
match v {
AttributeValue::S(v) => v,
_ => "".to_string(),
}
}

/// Converts the AttributeValue into an option-string
fn attr_to_option(v: AttributeValue) -> Option<String> {
match v {
AttributeValue::S(v) => Some(v),
_ => None,
}
}
76 changes: 55 additions & 21 deletions rust/lambdas/client-sync/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use aws_lambda_events::{
http::{HeaderMap, HeaderValue},
http::{method::Method, HeaderMap, HeaderValue},
lambda_function_urls::{LambdaFunctionUrlRequest, LambdaFunctionUrlResponse},
};
use aws_sdk_dynamodb::Client;
Expand Down Expand Up @@ -42,42 +42,76 @@ pub(crate) async fn my_handler(
None => String::new(),
};

info!("{:?}", event.payload.body);

// try to deser the body into a book
let book = match &event.payload.body {
Some(v) => match serde_json::from_str::<Book>(v) {
Ok(v) => v,
Err(e) => {
info!("Failed to parse payload: {:?}", e);
return handler_response(Some("Invalid payload. Expected DdbBook".to_string()), 400);
}
},
None => {
info!("Empty input");
return handler_response(Some("Missing payload. Expected DdbBook".to_string()), 400);
}
};
info!("Body: {:?}", event.payload.body);

// info!("Auth: {authorization}");
// info!("Headers: {:?}", event.payload.headers);

// exit if no valid email is provided
let email = match jwt::get_email(&authorization) {
Ok(v) => v,
Ok(v) => v.to_lowercase(),
Err(e) => {
info!("Unauthorized via JWT: {:?}", e);
return handler_response(Some("Unauthorized via JWT".to_string()), 403);
}
};
info!("Email: {:?}", email);

// decide on the action depending on the HTTP method
let method = match event.payload.request_context.http.method {
Some(v) => {
if let Ok(method) = Method::from_bytes(v.as_bytes()) {
method
} else {
info!("Invalid HTTP method: {v}");
return handler_response(Some("Invalid HTTP method".to_string()), 400);
}
}
None => {
info!("Missing HTTP method");
return handler_response(Some("Missing HTTP method. It's a bug.".to_string()), 400);
}
};

// save the book to the database
let client = Client::new(&aws_config::load_from_env().await);

match book::save(&book, &client, &email).await {
Ok(_) => handler_response(Some("Book saved".to_string()), 200),
Err(e) => handler_response(Some(e.to_string()), 400),
match method {
// save the book to the database
Method::POST => {
// try to deser the body into a book
let book = match &event.payload.body {
Some(v) => match serde_json::from_str::<Book>(v) {
Ok(v) => v,
Err(e) => {
info!("Failed to parse payload: {:?}", e);
return handler_response(Some("Invalid payload. Expected DdbBook".to_string()), 400);
}
},
None => {
info!("Empty input");
return handler_response(Some("Missing payload. Expected DdbBook".to_string()), 400);
}
};

match book::save(&book, &client, &email).await {
Ok(_) => handler_response(None, 204),
Err(e) => handler_response(Some(e.to_string()), 400),
}
}
// return the list of all books
Method::GET => match book::get_by_user(&client, &email).await {
Ok(v) => match serde_json::to_string(&v) {
Ok(v) => handler_response(Some(v), 200),
Err(e) => {
info!("Failed to serialize books for {email}: {:?}", e);
handler_response(Some(e.to_string()), 400)
}
},
Err(e) => handler_response(Some(e.to_string()), 400),
},
// unsupported method
_ => handler_response(Some("Unsupported HTTP method".to_string()), 400),
}
}

Expand Down
1 change: 1 addition & 0 deletions rust/types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ license = "GPL 3.0"
[dependencies]
wasm-bindgen = "0.2"
serde = { workspace = true }
serde_json = { workspace = true }
chrono = { workspace = true }
6 changes: 3 additions & 3 deletions rust/types/src/google.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use serde::{Deserialize, Serialize};
// }

/// Part of GoogleBooks API response
#[derive(Deserialize, Serialize, Debug, Default)]
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ImageLinks {
/// ~80 pixels wide
Expand Down Expand Up @@ -54,7 +54,7 @@ pub struct ImageLinks {
// }

/// Part of GoogleBooks API response
#[derive(Deserialize, Serialize, Debug, Default)]
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
pub struct VolumeInfo {
pub title: String,
Expand All @@ -73,7 +73,7 @@ pub struct VolumeInfo {
}

/// Part of GoogleBooks API response
#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Volume {
pub id: String,
Expand Down
Loading

0 comments on commit 2d81fcb

Please sign in to comment.