diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 78de8a0..37d69c9 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -347,20 +347,7 @@ dependencies = [ name = "backend" version = "0.1.0" dependencies = [ - "axum", - "axum-extra", - "clap", - "error-chain", - "log", - "reqwest", - "sea-orm", - "serde", - "serde_json", - "tokio", - "tower", - "tower-http", - "tracing", - "tracing-subscriber", + "booksman-api", ] [[package]] @@ -426,6 +413,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "booksman-api" +version = "0.1.0" +dependencies = [ + "axum", + "axum-extra", + "booksman-orm", + "clap", + "error-chain", + "log", + "reqwest", + "sea-orm", + "serde", + "serde_json", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "booksman-orm" +version = "0.1.0" +dependencies = [ + "chrono", + "entity", + "sea-orm", + "serde", +] + [[package]] name = "brotli" version = "3.3.4" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index d60059a..cf9d1cd 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -5,29 +5,8 @@ edition = "2021" [workspace] -members = [".", "entity", "migration"] +members = [".", "entity", "migration", "orm", "api"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -axum = "^0.5" -axum-extra = { version = "^0.3", features = ["spa"] } -clap = { version = "^3", features = ["derive"] } -error-chain = "0.12.4" -log = "^0.4" -reqwest = {version = "0.11.11", features = ["json"]} -serde = { version = "^1.0", features = ["derive"] } -serde_json = "^1.0" -tokio = { version = "^1", features = ["full"] } -tower = "^0.4" -tower-http = { version = "^0.3", features = ["full"] } -tracing = "^0.1" -tracing-subscriber = "^0.3" - -[dependencies.sea-orm] -version = "^0.9.2" # sea-orm version -features = [ - "debug-print", - "runtime-tokio-native-tls", - "sqlx-sqlite", -] - +booksman-api = { path = "api" } diff --git a/backend/api/Cargo.toml b/backend/api/Cargo.toml new file mode 100644 index 0000000..874615b --- /dev/null +++ b/backend/api/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "booksman-api" +version = "0.1.0" +edition = "2021" + + +[dependencies] +booksman-orm = { path = "../orm" } +axum = "^0.5" +axum-extra = { version = "^0.3", features = ["spa"] } +clap = { version = "^3", features = ["derive"] } +error-chain = "0.12.4" +log = "^0.4" +reqwest = {version = "0.11.11", features = ["json"]} +serde = { version = "^1.0", features = ["derive"] } +serde_json = "^1.0" +tokio = { version = "^1", features = ["full"] } +tower = "^0.4" +tower-http = { version = "^0.3", features = ["full"] } +tracing = "^0.1" +tracing-subscriber = "^0.3" + +[dependencies.sea-orm] +version = "^0.9.2" # sea-orm version +features = [ + "debug-print", + "runtime-tokio-native-tls", + "sqlx-sqlite", +] + + diff --git a/backend/api/src/lib.rs b/backend/api/src/lib.rs new file mode 100644 index 0000000..729938e --- /dev/null +++ b/backend/api/src/lib.rs @@ -0,0 +1,142 @@ +use axum::{ + http::{HeaderValue, Method}, + response::IntoResponse, + routing::get, + Json, Router, +}; +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::CorsLayer; +use tower_http::trace::TraceLayer; + +#[derive(Deserialize, Serialize, Debug)] +struct Docs { + key: String, + title: String, + first_publish_year: Option, + number_of_pages_median: Option, + isbn: Option>, + cover_i: Option, + cover_url: Option, + author_name: Option>, + person: Option>, + place: Option>, + subject: Option>, + time: Option>, +} +/* +impl Docs { + fn set_cover_url(&mut self) { + match self.cover_i { + Some(cover_i) => { + self.cover_url = Some(format!( + "https://covers.openlibrary.org/b/id/{}-L.jpg", + (cover_i.unwrap()) + )) + } + None => (), + } + } +} +*/ +#[derive(Deserialize, Serialize, Debug)] +struct Books { + num_found: u32, + docs: Vec, +} + +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(); + + let app = Router::new() + .route("/api/hello", get(hello)) + .merge(SpaRouter::new("/assets", opt.static_dir)) + .layer(ServiceBuilder::new().layer(TraceLayer::new_for_http())) + .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_origin("http://localhost:8080".parse::().unwrap()) + .allow_methods([Method::GET]), + ); + + 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 hello( + axum::extract::Query(params): axum::extract::Query>, +) -> 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::().await.expect("Unable to return value"); + resjson.docs.truncate(10); + resjson.set_all_cover_urls(); + print!("Search token {:?}\n", search); + return Json(resjson); +} diff --git a/backend/entity/src/entities/book.rs b/backend/entity/src/entities/book.rs index 6d9b4ba..0f94df2 100644 --- a/backend/entity/src/entities/book.rs +++ b/backend/entity/src/entities/book.rs @@ -1,4 +1,4 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2 +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 use sea_orm::entity::prelude::*; @@ -33,6 +33,8 @@ pub enum Relation { BookSubject, #[sea_orm(has_many = "super::book_time::Entity")] BookTime, + #[sea_orm(has_many = "super::book_isbn::Entity")] + BookIsbn, } impl Related for Entity { @@ -65,4 +67,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::BookIsbn.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/backend/entity/src/entities/book_author.rs b/backend/entity/src/entities/book_author.rs index 1983217..ce4e23e 100644 --- a/backend/entity/src/entities/book_author.rs +++ b/backend/entity/src/entities/book_author.rs @@ -1,4 +1,4 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2 +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 use sea_orm::entity::prelude::*; diff --git a/backend/entity/src/entities/book_isbn.rs b/backend/entity/src/entities/book_isbn.rs new file mode 100644 index 0000000..86ccdb5 --- /dev/null +++ b/backend/entity/src/entities/book_isbn.rs @@ -0,0 +1,32 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "book_isbn")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub isbn: String, + pub book_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::book::Entity", + from = "Column::BookId", + to = "super::book::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Book, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Book.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/backend/entity/src/entities/book_person.rs b/backend/entity/src/entities/book_person.rs index cf589e6..09a55eb 100644 --- a/backend/entity/src/entities/book_person.rs +++ b/backend/entity/src/entities/book_person.rs @@ -1,4 +1,4 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2 +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 use sea_orm::entity::prelude::*; diff --git a/backend/entity/src/entities/book_place.rs b/backend/entity/src/entities/book_place.rs index bb5a5d0..5a254fb 100644 --- a/backend/entity/src/entities/book_place.rs +++ b/backend/entity/src/entities/book_place.rs @@ -1,4 +1,4 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2 +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 use sea_orm::entity::prelude::*; diff --git a/backend/entity/src/entities/book_subject.rs b/backend/entity/src/entities/book_subject.rs index fa9d114..69e70e7 100644 --- a/backend/entity/src/entities/book_subject.rs +++ b/backend/entity/src/entities/book_subject.rs @@ -1,4 +1,4 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2 +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 use sea_orm::entity::prelude::*; diff --git a/backend/entity/src/entities/book_time.rs b/backend/entity/src/entities/book_time.rs index 9151198..a9d2038 100644 --- a/backend/entity/src/entities/book_time.rs +++ b/backend/entity/src/entities/book_time.rs @@ -1,4 +1,4 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2 +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 use sea_orm::entity::prelude::*; diff --git a/backend/entity/src/entities/mod.rs b/backend/entity/src/entities/mod.rs index 97bb3c0..78abcbd 100644 --- a/backend/entity/src/entities/mod.rs +++ b/backend/entity/src/entities/mod.rs @@ -1,9 +1,10 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2 +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 pub mod prelude; pub mod book; pub mod book_author; +pub mod book_isbn; pub mod book_person; pub mod book_place; pub mod book_subject; diff --git a/backend/entity/src/entities/prelude.rs b/backend/entity/src/entities/prelude.rs index aa79f6a..acb1b5f 100644 --- a/backend/entity/src/entities/prelude.rs +++ b/backend/entity/src/entities/prelude.rs @@ -1,7 +1,8 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2 +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 pub use super::book::Entity as Book; pub use super::book_author::Entity as BookAuthor; +pub use super::book_isbn::Entity as BookIsbn; pub use super::book_person::Entity as BookPerson; pub use super::book_place::Entity as BookPlace; pub use super::book_subject::Entity as BookSubject; diff --git a/backend/entity/src/lib.rs b/backend/entity/src/lib.rs index 5bdcb1c..9b60289 100644 --- a/backend/entity/src/lib.rs +++ b/backend/entity/src/lib.rs @@ -1,3 +1,3 @@ -mod entities; -pub use entities::*; +pub mod entities; + diff --git a/backend/migration/src/m20220101_000001_create_table.rs b/backend/migration/src/m20220101_000001_create_table.rs index 0b9db09..f0e83f6 100644 --- a/backend/migration/src/m20220101_000001_create_table.rs +++ b/backend/migration/src/m20220101_000001_create_table.rs @@ -38,32 +38,6 @@ impl MigrationTrait for Migration { ) .await; - manager - .create_table( - Table::create() - .table(BookAuthor::Table) - .if_not_exists() - .col( - ColumnDef::new(BookAuthor::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(BookAuthor::AuthorName).string().not_null()) - .col(ColumnDef::new(BookAuthor::BookId).integer().not_null()) - .foreign_key( - ForeignKey::create() - .name("FK_2e303c3a712662f1fc2a4d0aad6") - .from(BookAuthor::Table, BookAuthor::BookId) - .to(Book::Table, Book::Id) - .on_delete(ForeignKeyAction::Cascade) - .on_update(ForeignKeyAction::Cascade), - ) - .to_owned(), - ) - .await; - manager .create_table( Table::create() @@ -192,6 +166,32 @@ impl MigrationTrait for Migration { ) .to_owned(), ) + .await; + + manager + .create_table( + Table::create() + .table(BookISBN::Table) + .if_not_exists() + .col( + ColumnDef::new(BookISBN::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(BookISBN::ISBN).string().not_null()) + .col(ColumnDef::new(BookISBN::BookId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("FK_2e303c3a712662f1fc2a4d0aae0") + .from(BookISBN::Table, BookISBN::BookId) + .to(Book::Table, Book::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) .await } @@ -216,6 +216,9 @@ impl MigrationTrait for Migration { .await; manager .drop_table(Table::drop().table(BookTime::Table).to_owned()) + .await; + manager + .drop_table(Table::drop().table(BookISBN::Table).to_owned()) .await } } @@ -235,6 +238,7 @@ enum Book { //Place, //Subject, //Time, + //ISBN, GoodreadId, Description, Cover, @@ -283,3 +287,11 @@ enum BookTime { Time, BookId, } + +#[derive(Iden)] +enum BookISBN { + Table, + Id, + ISBN, + BookId, +} diff --git a/backend/orm/Cargo.toml b/backend/orm/Cargo.toml new file mode 100644 index 0000000..9fb2c1c --- /dev/null +++ b/backend/orm/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "booksman-orm" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +serde = { version = "1", features = ["derive"] } +entity = { path = "../entity" } +chrono = "0.4" + +[dependencies.sea-orm] +version = "^0.9.2" # sea-orm version +features = [ + "debug-print", + "runtime-tokio-native-tls", + "sqlx-sqlite", +] diff --git a/backend/orm/src/lib.rs b/backend/orm/src/lib.rs new file mode 100644 index 0000000..4a80f23 --- /dev/null +++ b/backend/orm/src/lib.rs @@ -0,0 +1,7 @@ +mod mutation; +mod query; + +pub use mutation::*; +pub use query::*; + +pub use sea_orm; diff --git a/backend/orm/src/mutation.rs b/backend/orm/src/mutation.rs new file mode 100644 index 0000000..d7bdf19 --- /dev/null +++ b/backend/orm/src/mutation.rs @@ -0,0 +1,55 @@ +//use ::entity::entities::prelude::Book; +//use ::entity::entities::{prelude::*, *}; +use sea_orm::*; +//use ::entity::entities::prelude::Book; + +pub struct Mutation; + +impl Mutation { +/* pub async fn create_post( + db: &DbConn, + form_data: post::Model, + ) -> Result { + post::ActiveModel { + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + ..Default::default() + } + .save(db) + .await + } + + pub async fn update_post_by_id( + db: &DbConn, + id: i32, + form_data: post::Model, + ) -> Result { + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; + + post::ActiveModel { + id: post.id, + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + } + .update(db) + .await + } + + pub async fn delete_post(db: &DbConn, id: i32) -> Result { + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; + + post.delete(db).await + } + */ + /* pub async fn delete_all_books(db: &DbConn) -> Result { + Book::delete_many().exec(db).await + } */ +} diff --git a/backend/orm/src/query.rs b/backend/orm/src/query.rs new file mode 100644 index 0000000..120d156 --- /dev/null +++ b/backend/orm/src/query.rs @@ -0,0 +1,30 @@ +use ::entity::entities::book::Entity as Book; +use ::entity::entities::book; +//use ::entity::entities::{prelude::*, *}; +use sea_orm::*; +//use ::entity::entities::prelude::Book; + +pub struct Query; + +impl Query { + pub async fn find_book_by_id(db: &DbConn, id: i32) -> Result, DbErr> { + Book::find_by_id(id).one(db).await + } + + /// If ok, returns (post models, num pages). + pub async fn find_posts_in_page( + db: &DbConn, + page: usize, + posts_per_page: usize, + ) -> Result<(Vec, usize), DbErr> { + // Setup paginator + let paginator = Book::find() + .order_by_asc(book::Column::Id) + .paginate(db, posts_per_page); + let num_pages = paginator.num_pages().await?; + + // Fetch paginated posts + paginator.fetch_page(page - 1).await.map(|p| (p, num_pages)) + } +} + diff --git a/backend/src/main.rs b/backend/src/main.rs index 140bcc0..c127fe6 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,158 +1,4 @@ -use axum::{ - http::{HeaderValue, Method}, - response::IntoResponse, - routing::get, - Json, Router, -}; -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::CorsLayer; -use tower_http::trace::TraceLayer; - -#[derive(Deserialize, Serialize, Debug)] -struct Docs { - key: String, - title: String, - first_publish_year: Option, - number_of_pages_median: Option, - isbn: Option>, - cover_i: Option, - cover_url: Option, - author_name: Option>, - person: Option>, - place: Option>, - subject: Option>, - time: Option>, -} -/* -impl Docs { - fn set_cover_url(&mut self) { - match self.cover_i { - Some(cover_i) => { - self.cover_url = Some(format!( - "https://covers.openlibrary.org/b/id/{}-L.jpg", - (cover_i.unwrap()) - )) - } - None => (), - } - } -} -*/ -#[derive(Deserialize, Serialize, Debug)] -struct Books { - num_found: u32, - docs: Vec, +fn main() { + booksman_api::main(); } -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] -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(); - - let app = Router::new() - .route("/api/hello", get(hello)) - .merge(SpaRouter::new("/assets", opt.static_dir)) - .layer(ServiceBuilder::new().layer(TraceLayer::new_for_http())) - .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_origin("http://localhost:8080".parse::().unwrap()) - .allow_methods([Method::GET]), - ); - - 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 hello( - axum::extract::Query(params): axum::extract::Query>, -) -> impl IntoResponse { - //"hello from server!" - //let res = reqwest::get("https://openlibrary.org/search.json?q=the+lord+of+the+rings") - // .await - // .expect("Unable to request"); - //let resjson = res.json::().await.expect("Unable to return value"); - print!("Get items with query params: {:?}\n", params); - //let search = params.get("search"); - //let search = ""; - let search = params.get("search").unwrap(); - //match search { - // Some(token) => (), - // None => return "None", - //} - //let search = match params.get("search") { - // Some(&token) => token, - // _ => String(""), - //}; - 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::().await.expect("Unable to return value"); - resjson.docs.truncate(10); - resjson.set_all_cover_urls(); - print!("Search token {:?}\n", search); - return Json(resjson); - //return "Hello"; -}