376 lines
12 KiB
Rust
376 lines
12 KiB
Rust
use axum::{
|
|
http::{HeaderValue, Method},
|
|
response::IntoResponse,
|
|
routing::{get,post},
|
|
Json, Router,
|
|
extract::{Extension, Path},
|
|
};
|
|
use axum_extra::routing::SpaRouter;
|
|
use clap::Parser;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
|
|
use std::str::FromStr;
|
|
use tower::ServiceBuilder;
|
|
use tower_http::cors::{Any,CorsLayer};
|
|
use tower_http::trace::TraceLayer;
|
|
use booksman_orm::{
|
|
sea_orm::{Database, DatabaseConnection},
|
|
Mutation as MutationCore, Query as QueryCore,
|
|
BookAndMeta,
|
|
};
|
|
use itertools::Itertools;
|
|
use ::entity::entities::{book,book_author,book_person,book_place,book_subject,book_time,book_isbn};
|
|
use std::env;
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
struct BookUI {
|
|
id: i32,
|
|
open_library_key: Option<String>,
|
|
title: String,
|
|
edition_count: Option<i32>,
|
|
first_publish_year: Option<i32>,
|
|
median_page_count: Option<i32>,
|
|
goodread_id: Option<String>,
|
|
description: Option<String>,
|
|
cover: Option<String>,
|
|
location: Option<String>,
|
|
time_added: Option<String>,
|
|
rating: Option<i32>,
|
|
comments: Option<String>,
|
|
author_name: Option<Vec<String>>,
|
|
person: Option<Vec<String>>,
|
|
place: Option<Vec<String>>,
|
|
subject: Option<Vec<String>>,
|
|
time: Option<Vec<String>>,
|
|
isbn: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
struct Docs {
|
|
key: String,
|
|
title: String,
|
|
edition_count: Option<i32>,
|
|
first_publish_year: Option<i32>,
|
|
number_of_pages_median: Option<i32>,
|
|
goodread_id: Option<String>,
|
|
description: Option<String>,
|
|
isbn: Option<Vec<String>>,
|
|
cover_i: Option<i32>,
|
|
cover_url: Option<String>,
|
|
author_name: Option<Vec<String>>,
|
|
person: Option<Vec<String>>,
|
|
place: Option<Vec<String>>,
|
|
subject: Option<Vec<String>>,
|
|
time: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Debug)]
|
|
struct Books {
|
|
num_found: u32,
|
|
docs: Vec<Docs>,
|
|
}
|
|
|
|
impl Books {
|
|
fn set_all_cover_urls(&mut self) {
|
|
for book in self.docs.iter_mut() {
|
|
match book.cover_i {
|
|
Some(cover_i) => {
|
|
book.cover_url = Some(format!(
|
|
"https://covers.openlibrary.org/b/id/{}-L.jpg",
|
|
cover_i
|
|
))
|
|
}
|
|
None => (),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setup the command line interface with clap.
|
|
#[derive(Parser, Debug)]
|
|
#[clap(name = "server", about = "A server for our wasm project!")]
|
|
struct Opt {
|
|
/// set the log level
|
|
#[clap(short = 'l', long = "log", default_value = "debug")]
|
|
log_level: String,
|
|
|
|
/// set the listen addr
|
|
#[clap(short = 'a', long = "addr", default_value = "::1")]
|
|
addr: String,
|
|
|
|
/// set the listen port
|
|
#[clap(short = 'p', long = "port", default_value = "8081")]
|
|
port: u16,
|
|
|
|
/// set the directory where static files are to be found
|
|
#[clap(long = "static-dir", default_value = "../dist")]
|
|
static_dir: String,
|
|
}
|
|
|
|
#[tokio::main]
|
|
pub async fn main() {
|
|
let opt = Opt::parse();
|
|
|
|
// Setup logging & RUST_LOG from args
|
|
if std::env::var("RUST_LOG").is_err() {
|
|
std::env::set_var("RUST_LOG", format!("{},hyper=info,mio=info", opt.log_level))
|
|
}
|
|
// enable console logging
|
|
tracing_subscriber::fmt::init();
|
|
|
|
|
|
dotenvy::dotenv().ok();
|
|
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file");
|
|
|
|
let conn = Database::connect(db_url)
|
|
.await
|
|
.expect("Database connection failed");
|
|
//Migrator::up(&conn, None).await.unwrap();
|
|
|
|
let app = Router::new()
|
|
.route("/api/search_openlibrary", get(search_openlibrary))
|
|
.route("/api/delete/:id", post(delete_book))
|
|
.route("/api/list", post(list_book))
|
|
.route("/api/create", post(create_book))
|
|
.merge(SpaRouter::new("/assets", opt.static_dir))
|
|
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
|
|
.layer(Extension(conn))
|
|
.layer(
|
|
// see https://docs.rs/tower-http/latest/tower_http/cors/index.html
|
|
// for more details
|
|
//
|
|
// pay attention that for some request types like posting content-type: application/json
|
|
// it is required to add ".allow_headers([http::header::CONTENT_TYPE])"
|
|
// or see this issue https://github.com/tokio-rs/axum/issues/849
|
|
CorsLayer::new()
|
|
.allow_methods([Method::GET, Method::POST])
|
|
.allow_origin("http://localhost:8080".parse::<HeaderValue>().unwrap())
|
|
.allow_headers(Any),
|
|
);
|
|
|
|
let sock_addr = SocketAddr::from((
|
|
IpAddr::from_str(opt.addr.as_str()).unwrap_or(IpAddr::V6(Ipv6Addr::LOCALHOST)),
|
|
opt.port,
|
|
));
|
|
|
|
log::info!("listening on http://{}", sock_addr);
|
|
|
|
axum::Server::bind(&sock_addr)
|
|
.serve(app.into_make_service())
|
|
.await
|
|
.expect("Unable to start server");
|
|
}
|
|
|
|
async fn search_openlibrary(
|
|
axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
|
|
) -> impl IntoResponse {
|
|
print!("Get items with query params: {:?}\n", params);
|
|
let search = params.get("search").unwrap();
|
|
let query = format!("https://openlibrary.org/search.json?q={}", search);
|
|
let res = reqwest::get(query).await.expect("Unable to request");
|
|
let mut resjson = res.json::<Books>().await.expect("Unable to return value");
|
|
resjson.docs.truncate(10);
|
|
resjson.set_all_cover_urls();
|
|
print!("Search token {:?}\n", search);
|
|
let mut vec = Vec::with_capacity(10);
|
|
for i in 0..10 as usize {
|
|
let doc = resjson.docs[i].clone();
|
|
vec.push(
|
|
BookUI{
|
|
id: (i+1) as i32,
|
|
open_library_key: Some(doc.key),
|
|
title: doc.title,
|
|
edition_count: doc.edition_count,
|
|
first_publish_year: doc.first_publish_year,
|
|
median_page_count: doc.number_of_pages_median,
|
|
goodread_id: doc.goodread_id,
|
|
description: doc.description,
|
|
cover: doc.cover_url,
|
|
location: None,
|
|
time_added: None,
|
|
rating: None,
|
|
comments: None,
|
|
author_name: doc.author_name,
|
|
person: doc.person,
|
|
place: doc.place,
|
|
subject: doc.subject,
|
|
time: doc.time,
|
|
isbn: doc.isbn
|
|
}
|
|
);
|
|
}
|
|
return Json(vec);
|
|
}
|
|
|
|
async fn delete_book(
|
|
Extension(ref conn): Extension<DatabaseConnection>,
|
|
Path(id): Path<i32>,
|
|
) -> impl IntoResponse {
|
|
MutationCore::delete_book(conn, id)
|
|
.await
|
|
.expect("could not delete book");
|
|
"success"
|
|
}
|
|
|
|
async fn list_book(
|
|
Extension(ref conn): Extension<DatabaseConnection>,
|
|
) -> impl IntoResponse {
|
|
let books = QueryCore::find_books_plus_meta_in_page(conn,1,5)
|
|
.await
|
|
.expect("could not list books");
|
|
|
|
let mut data_grouped: Vec<(i32,Vec<BookAndMeta>)> = Vec::with_capacity(5);
|
|
for (key, group) in &books.0.into_iter().group_by(|x| x.id) {
|
|
data_grouped.push((key, group.collect()));
|
|
}
|
|
|
|
let mut res: Vec<BookUI> = Vec::with_capacity(5);
|
|
for (id,group) in data_grouped.into_iter() {
|
|
let bookui = BookUI{
|
|
id,
|
|
title: group.clone().into_iter().map( |u| u.title).next().unwrap(),
|
|
open_library_key: group.clone().into_iter().map( |u| u.open_library_key).next(),
|
|
edition_count: group.clone().into_iter().clone().map(|u| u.edition_count).next(),
|
|
first_publish_year: group.clone().into_iter().clone().map(|u| u.first_publish_year).next(),
|
|
median_page_count: group.clone().into_iter().clone().map(|u| u.median_page_count).next(),
|
|
goodread_id: group.clone().into_iter().clone().map(|u| u.goodread_id).next(),
|
|
description: group.clone().into_iter().clone().map(|u| u.description).next(),
|
|
cover: group.clone().into_iter().clone().map(|u| u.cover).next(),
|
|
location: group.clone().into_iter().clone().map(|u| u.location).next(),
|
|
time_added: group.clone().into_iter().clone().map(|u| u.time_added).next(),
|
|
rating: group.clone().into_iter().clone().map(|u| u.rating).next(),
|
|
comments: group.clone().into_iter().clone().map(|u| u.comments).next(),
|
|
author_name: Some(group.clone().into_iter().map(|u| u.author_name).collect()),
|
|
person: Some(group.clone().into_iter().map(|u| u.person).collect()),
|
|
place: Some(group.clone().into_iter().map(|u| u.place).collect()),
|
|
subject: Some(group.clone().into_iter().map(|u| u.subject).collect()),
|
|
time: Some(group.clone().into_iter().map(|u| u.time).collect()),
|
|
isbn: Some(group.clone().into_iter().map(|u| u.isbn).collect()),
|
|
|
|
};
|
|
res.push(bookui);
|
|
}
|
|
/* let mut res: Vec<BookUI> = (books.0).iter()
|
|
.group_by(|x| (x.id, x.title.clone()) ).into_iter()
|
|
.map(|((id,title), group )| BookUI{
|
|
id,
|
|
title,
|
|
open_library_key: group.map( |u| u.open_library_key.clone()).next(),
|
|
edition_count: group.map(|u| u.edition_count).next(),
|
|
first_publish_year: group.map(|u| u.first_publish_year).next(),
|
|
median_page_count: group.map(|u| u.median_page_count).next(),
|
|
goodread_id: group.map(|u| u.goodread_id.clone()).next(),
|
|
description: group.map(|u| u.description.clone()).next(),
|
|
cover: group.map(|u| u.cover.clone()).next(),
|
|
location: group.map(|u| u.location.clone()).next(),
|
|
time_added: group.map(|u| u.time_added.clone()).next(),
|
|
rating: group.map(|u| u.rating).next(),
|
|
comments: group.map(|u| u.comments.clone()).next(),
|
|
author_name: Some(group.map(|u| u.author_name.clone()).collect()),
|
|
person: Some(group.map(|u| u.person.clone()).collect()),
|
|
place: Some(group.map(|u| u.place.clone()).collect()),
|
|
subject: Some(group.map(|u| u.subject.clone()).collect()),
|
|
time: Some(group.map(|u| u.time.clone()).collect()),
|
|
isbn: Some(group.map(|u| u.isbn.clone()).collect()),
|
|
}).collect();
|
|
*/
|
|
return Json(res);
|
|
// "success"
|
|
}
|
|
|
|
|
|
async fn create_book(
|
|
Extension(ref conn): Extension<DatabaseConnection>,
|
|
Json(doc_sent): Json<BookUI>,
|
|
) -> impl IntoResponse {
|
|
println!("Creating book");
|
|
let book: book::Model = book::Model{
|
|
open_library_key: doc_sent.open_library_key.to_owned(),
|
|
title: (doc_sent.title.to_owned()),
|
|
edition_count: doc_sent.edition_count.to_owned(),
|
|
first_publish_year: (doc_sent.first_publish_year.to_owned()),
|
|
median_page_count: (doc_sent.median_page_count.to_owned()),
|
|
goodread_id: doc_sent.goodread_id.to_owned(),
|
|
description: doc_sent.description.to_owned(),
|
|
comments: doc_sent.comments.to_owned(),
|
|
cover: doc_sent.cover.to_owned(),
|
|
rating: doc_sent.rating.to_owned(),
|
|
time_added: doc_sent.time_added.to_owned(),
|
|
id: 1,
|
|
location: doc_sent.location.to_owned(),
|
|
};
|
|
let created_book = MutationCore::create_book(conn, book)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
for author_name in doc_sent.author_name.as_ref().unwrap().iter() {
|
|
let record : book_author::Model = book_author::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
author_name: author_name.to_owned(),
|
|
};
|
|
MutationCore::create_book_author(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
}
|
|
for person in doc_sent.person.as_ref().unwrap().iter() {
|
|
let record : book_person::Model = book_person::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
person: person.to_owned(),
|
|
};
|
|
MutationCore::create_book_person(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for place in doc_sent.place.as_ref().unwrap().iter() {
|
|
let record : book_place::Model = book_place::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
place: place.to_owned(),
|
|
};
|
|
MutationCore::create_book_place(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for subject in doc_sent.subject.as_ref().unwrap().iter() {
|
|
let record : book_subject::Model = book_subject::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
subject: subject.to_owned(),
|
|
};
|
|
MutationCore::create_book_subject(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for time in doc_sent.time.as_ref().unwrap().iter() {
|
|
let record : book_time::Model = book_time::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
time: time.to_owned(),
|
|
};
|
|
MutationCore::create_book_time(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for isbn in doc_sent.isbn.as_ref().unwrap().iter() {
|
|
let record : book_isbn::Model = book_isbn::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
isbn: isbn.to_owned(),
|
|
};
|
|
MutationCore::create_book_isbn(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
|
|
|
|
"success"
|
|
}
|