From 5548e3723466b3dae17deec61b4b92b280a49e5f Mon Sep 17 00:00:00 2001 From: Milim Date: Fri, 6 Dec 2024 18:08:24 +0100 Subject: [PATCH 01/10] basic SQLite database setup --- .gitignore | 3 +++ Cargo.toml | 2 +- src/config.rs | 2 +- src/db/mod.rs | 31 +++++++++++++++++++++++++++++++ src/main.rs | 15 +++++++++++---- 5 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 src/db/mod.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..d9811f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /target +gunnhildr.db +gunnhildr.db-shm +gunnhildr.db-wal \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 30d497e..2fbb05f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,5 +9,5 @@ env_logger = "0.11.5" figment = {version="0.10.19", features=["env"]} log = "0.4.22" serde = {version="1.0.215", features=["derive"]} -sqlx = {version="0.8.2", features=["runtime-tokio"]} +sqlx = {version="0.8.2", features=["runtime-tokio","postgres", "sqlite"]} tokio = "1.42.0" diff --git a/src/config.rs b/src/config.rs index 88637bd..4e74963 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,7 +5,7 @@ use figment::{ use serde::{Deserialize, Serialize}; use std::net::{IpAddr, Ipv4Addr}; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Clone, Copy)] pub struct Config { pub binding_ip: IpAddr, pub port: u16, diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..6c120ee --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,31 @@ +use log::{info, warn}; +use sqlx::{migrate::MigrateDatabase, Sqlite, SqlitePool}; + +#[derive(Clone)] +pub enum DataBase { + Sqlite(sqlx::Pool), + //Postgres(sqlx::Pool), +} + +impl DataBase { + pub async fn sqlite() -> Self { + if !sqlx::Sqlite::database_exists("sqlite:gunnhildr.db") + .await + .expect("failed to connect to db") + { + warn!("No SQLite database found, if this is the first time you are starting Gunnhildr, you can safely ignore this."); + sqlx::Sqlite::create_database("sqlite:gunnhildr.db") + .await + .expect("failed to create SQLite Database"); + info!("Created new SQLite Database"); + } + + let pool = SqlitePool::connect("sqlite:gunnhildr.db").await.unwrap(); + sqlx::migrate!("migrations/sqlite") + .run(&pool) + .await + .expect("Failed to apply migration!"); + + Self::Sqlite(pool) + } +} diff --git a/src/main.rs b/src/main.rs index 5a8fc1e..e95217b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,18 +2,25 @@ use actix_web::{get, App, HttpServer}; use log::info; mod config; +mod db; #[actix_web::main] async fn main() -> Result<(), std::io::Error> { env_logger::init(); let config = config::parse_config(); + let db = db::DataBase::sqlite().await; info!("Server starting..."); - HttpServer::new(|| App::new().service(hello)) - .bind((config.binding_ip, config.port))? - .run() - .await + HttpServer::new(move || { + App::new() + .app_data(config) + .app_data(db.clone()) + .service(hello) + }) + .bind((config.binding_ip, config.port))? + .run() + .await } #[get("/hello")] From f5e7e0a36e4bdd72ececbef9e185fa32af4e8161 Mon Sep 17 00:00:00 2001 From: Milim Date: Fri, 6 Dec 2024 20:51:45 +0100 Subject: [PATCH 02/10] add postgress boilerplate --- src/db/mod.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/db/mod.rs b/src/db/mod.rs index 6c120ee..0c93318 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,10 +1,11 @@ +#![allow(unused)] use log::{info, warn}; -use sqlx::{migrate::MigrateDatabase, Sqlite, SqlitePool}; +use sqlx::{migrate::MigrateDatabase, PgPool, Postgres, Sqlite, SqlitePool}; #[derive(Clone)] pub enum DataBase { Sqlite(sqlx::Pool), - //Postgres(sqlx::Pool), + Postgres(sqlx::Pool), } impl DataBase { @@ -28,4 +29,25 @@ impl DataBase { Self::Sqlite(pool) } + + pub async fn postgres(url: &str) -> Self { + if !sqlx::Postgres::database_exists(url) + .await + .expect("failed to connect to db") + { + warn!("No Postgres database found, if this is the first time you are starting Gunnhildr, you can safely ignore this."); + sqlx::Postgres::create_database(url) + .await + .expect("failed to create Postgres Database!"); + info!("Created new Postgres Database"); + } + + let pool = PgPool::connect("url").await.unwrap(); + sqlx::migrate!("migrations/postgres") + .run(&pool) + .await + .expect("Failed to apply migration!"); + + Self::Postgres(pool) + } } From 1b98cbac1a761c37ea223a783d754f430777191c Mon Sep 17 00:00:00 2001 From: Milim Date: Sat, 7 Dec 2024 09:16:01 +0100 Subject: [PATCH 03/10] add basic book/chapter --- src/books/mod.rs | 15 +++++++++++++++ src/main.rs | 2 ++ 2 files changed, 17 insertions(+) create mode 100644 src/books/mod.rs diff --git a/src/books/mod.rs b/src/books/mod.rs new file mode 100644 index 0000000..c333300 --- /dev/null +++ b/src/books/mod.rs @@ -0,0 +1,15 @@ +use actix_web::{get, web, Scope}; + +pub fn book_scope() -> Scope { + web::scope("/b").service(book_view).service(chapter_view) +} + +#[get("/{book}")] +async fn book_view(book: web::Path) -> String { + format!("This is the info for {book}") +} + +#[get("/{book}/{chapter})")] +async fn chapter_view(path: web::Path<(String, String)>) -> String { + format!("This is {} of {}", path.0, path.1) +} diff --git a/src/main.rs b/src/main.rs index e95217b..41b0bce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use actix_web::{get, App, HttpServer}; use log::info; +mod books; mod config; mod db; @@ -16,6 +17,7 @@ async fn main() -> Result<(), std::io::Error> { App::new() .app_data(config) .app_data(db.clone()) + .service(books::book_scope()) .service(hello) }) .bind((config.binding_ip, config.port))? From ee9e682d9eee00c48bb8d4b5f0513fedd7218343 Mon Sep 17 00:00:00 2001 From: Milim Date: Sat, 7 Dec 2024 09:25:08 +0100 Subject: [PATCH 04/10] set default log level to info --- src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 41b0bce..39eafd1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,9 @@ mod db; #[actix_web::main] async fn main() -> Result<(), std::io::Error> { - env_logger::init(); + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .init(); let config = config::parse_config(); let db = db::DataBase::sqlite().await; From 307419d37f73feea9429cad5313ec177172a0e9d Mon Sep 17 00:00:00 2001 From: Milim Date: Sat, 7 Dec 2024 10:04:34 +0100 Subject: [PATCH 05/10] add sqlite database creation migrations --- .../20241207082654_initial_table_setup.sql | 25 +++++++++++++++++++ src/db/mod.rs | 2 ++ 2 files changed, 27 insertions(+) create mode 100644 migrations/sqlite/20241207082654_initial_table_setup.sql diff --git a/migrations/sqlite/20241207082654_initial_table_setup.sql b/migrations/sqlite/20241207082654_initial_table_setup.sql new file mode 100644 index 0000000..e6842a0 --- /dev/null +++ b/migrations/sqlite/20241207082654_initial_table_setup.sql @@ -0,0 +1,25 @@ +CREATE TABLE IF NOT EXISTS users( + user_id INTEGER PRIMARY KEY, + name TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS books( + book_id INTEGER PRIMARY KEY, + book_title TEXT NOT NULL, + book_description TEXT, + book_creation_date INTEGER NOT NULL, + author_id INTEGER NOT NULL, + FOREIGN KEY (author_id) REFERENCES users(user_id) ON DELETE CASCADE + +); + +CREATE TABLE IF NOT EXISTS chapters( + chapter_id INTEGER PRIMARY KEY, + chapter_title TEXT NOT NULL, + chapter_text TEXT NOT NULL, + chapter_creation_date INT NOT NULL, + book_id INTEGER NOT NULL, + author_id INTEGER NOT NULL, + FOREIGN KEY (book_id) REFERENCES books(book_id) ON DELETE CASCADE, + FOREIGN KEY (author_id) REFERENCES users(user_id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/src/db/mod.rs b/src/db/mod.rs index 0c93318..e4c4b70 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -22,10 +22,12 @@ impl DataBase { } let pool = SqlitePool::connect("sqlite:gunnhildr.db").await.unwrap(); + sqlx::migrate!("migrations/sqlite") .run(&pool) .await .expect("Failed to apply migration!"); + info!("Applied migrations."); Self::Sqlite(pool) } From 0e50b214ed8b20b766762d9b2c86615ee0b1f53f Mon Sep 17 00:00:00 2001 From: Milim Date: Sat, 7 Dec 2024 10:38:16 +0100 Subject: [PATCH 06/10] add simple documentation --- src/books/mod.rs | 5 ++++- src/config.rs | 3 +++ src/db/mod.rs | 10 ++++++++++ src/main.rs | 3 ++- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/books/mod.rs b/src/books/mod.rs index c333300..8fbd08c 100644 --- a/src/books/mod.rs +++ b/src/books/mod.rs @@ -1,14 +1,17 @@ use actix_web::{get, web, Scope}; -pub fn book_scope() -> Scope { +/// scope to handle all reading related pages +pub fn reading_scope() -> Scope { web::scope("/b").service(book_view).service(chapter_view) } +/// route to view info for a specific book #[get("/{book}")] async fn book_view(book: web::Path) -> String { format!("This is the info for {book}") } +/// view for reading a chapter #[get("/{book}/{chapter})")] async fn chapter_view(path: web::Path<(String, String)>) -> String { format!("This is {} of {}", path.0, path.1) diff --git a/src/config.rs b/src/config.rs index 4e74963..f5dc496 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,7 +7,9 @@ use std::net::{IpAddr, Ipv4Addr}; #[derive(Deserialize, Serialize, Clone, Copy)] pub struct Config { + /// Ip address that the gunnhildr should bind to pub binding_ip: IpAddr, + /// Port that gunnhildr should listen on pub port: u16, } @@ -20,6 +22,7 @@ impl Default for Config { } } +/// Parse and merge all config sources pub fn parse_config() -> Config { Figment::from(Serialized::defaults(Config::default())) .merge(Env::prefixed("HILDR")) diff --git a/src/db/mod.rs b/src/db/mod.rs index e4c4b70..44214b8 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -2,14 +2,19 @@ use log::{info, warn}; use sqlx::{migrate::MigrateDatabase, PgPool, Postgres, Sqlite, SqlitePool}; +/// Utility for interacting with the database #[derive(Clone)] pub enum DataBase { + /// Used for Sqlite database Sqlite(sqlx::Pool), + /// Used for Postgres database Postgres(sqlx::Pool), } impl DataBase { + /// Database backed by SQLite pub async fn sqlite() -> Self { + // Check if db exists, if not create it. if !sqlx::Sqlite::database_exists("sqlite:gunnhildr.db") .await .expect("failed to connect to db") @@ -23,6 +28,7 @@ impl DataBase { let pool = SqlitePool::connect("sqlite:gunnhildr.db").await.unwrap(); + // run migrations sqlx::migrate!("migrations/sqlite") .run(&pool) .await @@ -32,7 +38,9 @@ impl DataBase { Self::Sqlite(pool) } + /// Database backed by Postgres pub async fn postgres(url: &str) -> Self { + // check if database exists and create one if not if !sqlx::Postgres::database_exists(url) .await .expect("failed to connect to db") @@ -45,6 +53,8 @@ impl DataBase { } let pool = PgPool::connect("url").await.unwrap(); + + // run migrations sqlx::migrate!("migrations/postgres") .run(&pool) .await diff --git a/src/main.rs b/src/main.rs index 39eafd1..6019417 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod db; #[actix_web::main] async fn main() -> Result<(), std::io::Error> { + // init env logger env_logger::builder() .filter_level(log::LevelFilter::Info) .init(); @@ -19,7 +20,7 @@ async fn main() -> Result<(), std::io::Error> { App::new() .app_data(config) .app_data(db.clone()) - .service(books::book_scope()) + .service(books::reading_scope()) .service(hello) }) .bind((config.binding_ip, config.port))? From 28f571e02babc91b814ed026901445e8d74ce4df Mon Sep 17 00:00:00 2001 From: Milim Date: Sat, 7 Dec 2024 10:58:50 +0100 Subject: [PATCH 07/10] add database model types and a few other db things --- .../20241207082654_initial_table_setup.sql | 4 ++-- src/db/mod.rs | 11 ++++++++++ src/db/models.rs | 21 +++++++++++++++++++ src/db/postgres.rs | 1 + src/db/sqlite.rs | 5 +++++ 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 src/db/models.rs create mode 100644 src/db/postgres.rs create mode 100644 src/db/sqlite.rs diff --git a/migrations/sqlite/20241207082654_initial_table_setup.sql b/migrations/sqlite/20241207082654_initial_table_setup.sql index e6842a0..3fb9ee3 100644 --- a/migrations/sqlite/20241207082654_initial_table_setup.sql +++ b/migrations/sqlite/20241207082654_initial_table_setup.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS books( book_id INTEGER PRIMARY KEY, book_title TEXT NOT NULL, book_description TEXT, - book_creation_date INTEGER NOT NULL, + book_creation_date TEXT NOT NULL, author_id INTEGER NOT NULL, FOREIGN KEY (author_id) REFERENCES users(user_id) ON DELETE CASCADE @@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS chapters( chapter_id INTEGER PRIMARY KEY, chapter_title TEXT NOT NULL, chapter_text TEXT NOT NULL, - chapter_creation_date INT NOT NULL, + chapter_creation_date TEXT NOT NULL, book_id INTEGER NOT NULL, author_id INTEGER NOT NULL, FOREIGN KEY (book_id) REFERENCES books(book_id) ON DELETE CASCADE, diff --git a/src/db/mod.rs b/src/db/mod.rs index 44214b8..a894e29 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -2,6 +2,10 @@ use log::{info, warn}; use sqlx::{migrate::MigrateDatabase, PgPool, Postgres, Sqlite, SqlitePool}; +pub mod models; +mod postgres; +mod sqlite; + /// Utility for interacting with the database #[derive(Clone)] pub enum DataBase { @@ -62,4 +66,11 @@ impl DataBase { Self::Postgres(pool) } + + pub fn get_chapter(&self, id: u32) -> models::Chapter { + match self { + DataBase::Sqlite(pool) => sqlite::sqlite_chapter(pool, id), + DataBase::Postgres(pool) => todo!(), + } + } } diff --git a/src/db/models.rs b/src/db/models.rs new file mode 100644 index 0000000..ac2fd84 --- /dev/null +++ b/src/db/models.rs @@ -0,0 +1,21 @@ +pub struct Chapter { + pub id: u32, + pub title: String, + pub text: String, + pub creation_data: String, + pub book_id: u32, + pub author_id: u32, +} + +pub struct Book { + pub id: u32, + pub title: String, + pub description: String, + pub creation_date: String, + pub author_id: u32, +} + +pub struct User { + pub id: u32, + pub name: String, +} diff --git a/src/db/postgres.rs b/src/db/postgres.rs new file mode 100644 index 0000000..624925e --- /dev/null +++ b/src/db/postgres.rs @@ -0,0 +1 @@ +//! Module containing database code for Postgres diff --git a/src/db/sqlite.rs b/src/db/sqlite.rs new file mode 100644 index 0000000..f0a727d --- /dev/null +++ b/src/db/sqlite.rs @@ -0,0 +1,5 @@ +//! Module containing database code for SQLite + +pub fn sqlite_chapter(pool: &sqlx::Pool, id: u32) -> super::models::Chapter { + todo!() +} From be6b86ee288a885fff6e1ffedae167f62d8942c5 Mon Sep 17 00:00:00 2001 From: Milim Date: Sat, 7 Dec 2024 12:27:48 +0100 Subject: [PATCH 08/10] add sqlite database stuff --- Cargo.lock | 138 ++++++++++++++++++ Cargo.toml | 1 + .../20241207082654_initial_table_setup.sql | 4 +- src/db/mod.rs | 78 +++++++++- src/db/models.rs | 2 +- src/db/sqlite.rs | 98 ++++++++++++- 6 files changed, 314 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 13414d2..0833cd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,6 +243,21 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.18" @@ -382,6 +397,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "bytemuck" version = "1.20.0" @@ -426,6 +447,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -464,6 +499,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.16" @@ -822,6 +863,7 @@ name = "gunnhildr" version = "0.1.0" dependencies = [ "actix-web", + "chrono", "env_logger", "figment", "log", @@ -942,6 +984,29 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1124,6 +1189,16 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -2282,6 +2357,60 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + [[package]] name = "whoami" version = "1.5.2" @@ -2292,6 +2421,15 @@ dependencies = [ "wasite", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 2fbb05f..bfc9501 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] actix-web = {version="4.9.0"} +chrono = "0.4.38" env_logger = "0.11.5" figment = {version="0.10.19", features=["env"]} log = "0.4.22" diff --git a/migrations/sqlite/20241207082654_initial_table_setup.sql b/migrations/sqlite/20241207082654_initial_table_setup.sql index 3fb9ee3..4f90b11 100644 --- a/migrations/sqlite/20241207082654_initial_table_setup.sql +++ b/migrations/sqlite/20241207082654_initial_table_setup.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS books( book_id INTEGER PRIMARY KEY, book_title TEXT NOT NULL, book_description TEXT, - book_creation_date TEXT NOT NULL, + book_creation_date INT NOT NULL, author_id INTEGER NOT NULL, FOREIGN KEY (author_id) REFERENCES users(user_id) ON DELETE CASCADE @@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS chapters( chapter_id INTEGER PRIMARY KEY, chapter_title TEXT NOT NULL, chapter_text TEXT NOT NULL, - chapter_creation_date TEXT NOT NULL, + chapter_creation_date INT NOT NULL, book_id INTEGER NOT NULL, author_id INTEGER NOT NULL, FOREIGN KEY (book_id) REFERENCES books(book_id) ON DELETE CASCADE, diff --git a/src/db/mod.rs b/src/db/mod.rs index a894e29..4cd2f49 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -15,6 +15,24 @@ pub enum DataBase { Postgres(sqlx::Pool), } +/// Error type for handling DB related errors +pub enum DbError { + /// No such entry found + NotFound, + /// Database related error + SqlxError(sqlx::Error), +} + +impl From for DbError { + fn from(value: sqlx::Error) -> Self { + match value { + sqlx::Error::RowNotFound => DbError::NotFound, + _ => DbError::SqlxError(value), + } + } +} +type DbResult = Result; + impl DataBase { /// Database backed by SQLite pub async fn sqlite() -> Self { @@ -67,9 +85,65 @@ impl DataBase { Self::Postgres(pool) } - pub fn get_chapter(&self, id: u32) -> models::Chapter { + /// Tries to fetch a book from the database + pub async fn get_book(&self, id: u32) -> DbResult { match self { - DataBase::Sqlite(pool) => sqlite::sqlite_chapter(pool, id), + DataBase::Sqlite(pool) => sqlite::sqlite_book(pool, id).await, + DataBase::Postgres(pool) => todo!(), + } + } + + /// Tries to create a book and returns the book's id if successful + pub async fn create_book( + &self, + title: String, + description: String, + author_id: u32, + ) -> DbResult { + match self { + DataBase::Sqlite(pool) => { + sqlite::sqlite_book_create(pool, title, description, author_id).await + } + DataBase::Postgres(pool) => todo!(), + } + } + + /// Tries to fetch a chapter from the database + pub async fn get_chapter(&self, id: u32) -> DbResult { + match self { + DataBase::Sqlite(pool) => sqlite::sqlite_chapter(pool, id).await, + DataBase::Postgres(pool) => todo!(), + } + } + + /// Tries to create a chapter and returns the chapter's id if successfu + pub async fn create_chapter( + &self, + title: String, + text: String, + book_id: u32, + author_id: u32, + ) -> DbResult { + match self { + DataBase::Sqlite(pool) => { + sqlite::sqlite_chapter_create(pool, title, text, book_id, author_id).await + } + DataBase::Postgres(pool) => todo!(), + } + } + + /// Tries to fetch a user from the database + pub async fn get_user(&self, id: u32) -> DbResult { + match self { + DataBase::Sqlite(pool) => sqlite::sqlite_user(pool, id).await, + DataBase::Postgres(pool) => todo!(), + } + } + + /// Tries to create a user and returns the user's id if successful + pub async fn create_user(&self, name: String) -> DbResult { + match self { + DataBase::Sqlite(pool) => sqlite::sqlite_user_create(pool, name).await, DataBase::Postgres(pool) => todo!(), } } diff --git a/src/db/models.rs b/src/db/models.rs index ac2fd84..470c4d1 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -2,7 +2,7 @@ pub struct Chapter { pub id: u32, pub title: String, pub text: String, - pub creation_data: String, + pub creation_date: String, pub book_id: u32, pub author_id: u32, } diff --git a/src/db/sqlite.rs b/src/db/sqlite.rs index f0a727d..201fabf 100644 --- a/src/db/sqlite.rs +++ b/src/db/sqlite.rs @@ -1,5 +1,99 @@ //! Module containing database code for SQLite +use super::models::{Book, Chapter, User}; +use super::{DbError, DbResult}; +use serde::de; +use sqlx::{Pool, Row, Sqlite}; -pub fn sqlite_chapter(pool: &sqlx::Pool, id: u32) -> super::models::Chapter { - todo!() +pub async fn sqlite_book(pool: &Pool, id: u32) -> DbResult { + let row = sqlx::query("SELECT * FROM users WHERE user_id = ?") + .bind(id) + .fetch_one(pool) + .await?; + + let book = Book { + id, + title: row.get("book_title"), + description: row.get("book_description"), + creation_date: row.get("book_creation_date"), + author_id: row.get("author_id"), + }; + + Ok(book) +} + +pub async fn sqlite_book_create( + pool: &Pool, + title: String, + description: String, + author_id: u32, +) -> DbResult { + let id = sqlx::query("INSERT INTO books (title, description, author_id, book_creation_date) VALUES ( ?1, ?2, ?3, ?4 )") + .bind(title) + .bind(description) + .bind(author_id) + .bind(chrono::Local::now().timestamp()) + .execute(pool) + .await? + .last_insert_rowid() as u32; + Ok(id) +} + +pub async fn sqlite_chapter(pool: &Pool, id: u32) -> DbResult { + let row = sqlx::query("SELECT * FROM chapters WHERE chapter_id = ?") + .bind(id) + .fetch_one(pool) + .await?; + + let chapter = Chapter { + id, + title: row.get("chapter_title"), + text: row.get("chapter_text"), + creation_date: row.get("chapter_creation_date"), + book_id: row.get("book_id"), + author_id: row.get("author_id"), + }; + Ok(chapter) +} + +pub async fn sqlite_chapter_create( + pool: &Pool, + title: String, + text: String, + book_id: u32, + author_id: u32, +) -> DbResult { + let id = sqlx::query("INSERT INTO chapters (chapter_title, chapter_text, book_id, author_id, chapter_creation_date) VALUES ( ?1, ?2, ?3, ?4, ?5 )") + .bind(title) + .bind(text) + .bind(book_id) + .bind(author_id) + .bind(chrono::Local::now().timestamp()) + .execute(pool) + .await? + .last_insert_rowid() as u32; + + Ok(id) +} + +pub async fn sqlite_user(pool: &Pool, id: u32) -> DbResult { + let row = sqlx::query("SELECT * FROM users WHERE user_id = ?") + .bind(id) + .fetch_one(pool) + .await?; + + let user = User { + id, + name: row.get("name"), + }; + + Ok(user) +} + +pub async fn sqlite_user_create(pool: &Pool, name: String) -> DbResult { + let id = sqlx::query("INSERT INTO users (name) VALUES ( ? )") + .bind(name) + .execute(pool) + .await? + .last_insert_rowid() as u32; + Ok(id) } From 107ad5dfa52b1d8d2602efc689b1d620671bb158 Mon Sep 17 00:00:00 2001 From: Milim Date: Sat, 7 Dec 2024 18:01:11 +0100 Subject: [PATCH 09/10] add basic chapter content view --- src/books/mod.rs | 18 ------------------ src/db/mod.rs | 38 +++++++++++++++++++------------------- src/db/sqlite.rs | 10 +++++----- src/main.rs | 12 ++++++------ src/reading/mod.rs | 24 ++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 48 deletions(-) delete mode 100644 src/books/mod.rs create mode 100644 src/reading/mod.rs diff --git a/src/books/mod.rs b/src/books/mod.rs deleted file mode 100644 index 8fbd08c..0000000 --- a/src/books/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -use actix_web::{get, web, Scope}; - -/// scope to handle all reading related pages -pub fn reading_scope() -> Scope { - web::scope("/b").service(book_view).service(chapter_view) -} - -/// route to view info for a specific book -#[get("/{book}")] -async fn book_view(book: web::Path) -> String { - format!("This is the info for {book}") -} - -/// view for reading a chapter -#[get("/{book}/{chapter})")] -async fn chapter_view(path: web::Path<(String, String)>) -> String { - format!("This is {} of {}", path.0, path.1) -} diff --git a/src/db/mod.rs b/src/db/mod.rs index 4cd2f49..69236fc 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -8,7 +8,7 @@ mod sqlite; /// Utility for interacting with the database #[derive(Clone)] -pub enum DataBase { +pub enum DbInterface { /// Used for Sqlite database Sqlite(sqlx::Pool), /// Used for Postgres database @@ -33,7 +33,7 @@ impl From for DbError { } type DbResult = Result; -impl DataBase { +impl DbInterface { /// Database backed by SQLite pub async fn sqlite() -> Self { // Check if db exists, if not create it. @@ -88,63 +88,63 @@ impl DataBase { /// Tries to fetch a book from the database pub async fn get_book(&self, id: u32) -> DbResult { match self { - DataBase::Sqlite(pool) => sqlite::sqlite_book(pool, id).await, - DataBase::Postgres(pool) => todo!(), + DbInterface::Sqlite(pool) => sqlite::sqlite_book(pool, id).await, + DbInterface::Postgres(pool) => todo!(), } } /// Tries to create a book and returns the book's id if successful pub async fn create_book( &self, - title: String, - description: String, + title: &String, + description: &String, author_id: u32, ) -> DbResult { match self { - DataBase::Sqlite(pool) => { + DbInterface::Sqlite(pool) => { sqlite::sqlite_book_create(pool, title, description, author_id).await } - DataBase::Postgres(pool) => todo!(), + DbInterface::Postgres(pool) => todo!(), } } /// Tries to fetch a chapter from the database pub async fn get_chapter(&self, id: u32) -> DbResult { match self { - DataBase::Sqlite(pool) => sqlite::sqlite_chapter(pool, id).await, - DataBase::Postgres(pool) => todo!(), + DbInterface::Sqlite(pool) => sqlite::sqlite_chapter(pool, id).await, + DbInterface::Postgres(pool) => todo!(), } } /// Tries to create a chapter and returns the chapter's id if successfu pub async fn create_chapter( &self, - title: String, - text: String, + title: &String, + text: &String, book_id: u32, author_id: u32, ) -> DbResult { match self { - DataBase::Sqlite(pool) => { + DbInterface::Sqlite(pool) => { sqlite::sqlite_chapter_create(pool, title, text, book_id, author_id).await } - DataBase::Postgres(pool) => todo!(), + DbInterface::Postgres(pool) => todo!(), } } /// Tries to fetch a user from the database pub async fn get_user(&self, id: u32) -> DbResult { match self { - DataBase::Sqlite(pool) => sqlite::sqlite_user(pool, id).await, - DataBase::Postgres(pool) => todo!(), + DbInterface::Sqlite(pool) => sqlite::sqlite_user(pool, id).await, + DbInterface::Postgres(pool) => todo!(), } } /// Tries to create a user and returns the user's id if successful - pub async fn create_user(&self, name: String) -> DbResult { + pub async fn create_user(&self, name: &String) -> DbResult { match self { - DataBase::Sqlite(pool) => sqlite::sqlite_user_create(pool, name).await, - DataBase::Postgres(pool) => todo!(), + DbInterface::Sqlite(pool) => sqlite::sqlite_user_create(pool, name).await, + DbInterface::Postgres(pool) => todo!(), } } } diff --git a/src/db/sqlite.rs b/src/db/sqlite.rs index 201fabf..1f52e46 100644 --- a/src/db/sqlite.rs +++ b/src/db/sqlite.rs @@ -23,8 +23,8 @@ pub async fn sqlite_book(pool: &Pool, id: u32) -> DbResult { pub async fn sqlite_book_create( pool: &Pool, - title: String, - description: String, + title: &String, + description: &String, author_id: u32, ) -> DbResult { let id = sqlx::query("INSERT INTO books (title, description, author_id, book_creation_date) VALUES ( ?1, ?2, ?3, ?4 )") @@ -57,8 +57,8 @@ pub async fn sqlite_chapter(pool: &Pool, id: u32) -> DbResult { pub async fn sqlite_chapter_create( pool: &Pool, - title: String, - text: String, + title: &String, + text: &String, book_id: u32, author_id: u32, ) -> DbResult { @@ -89,7 +89,7 @@ pub async fn sqlite_user(pool: &Pool, id: u32) -> DbResult { Ok(user) } -pub async fn sqlite_user_create(pool: &Pool, name: String) -> DbResult { +pub async fn sqlite_user_create(pool: &Pool, name: &String) -> DbResult { let id = sqlx::query("INSERT INTO users (name) VALUES ( ? )") .bind(name) .execute(pool) diff --git a/src/main.rs b/src/main.rs index 6019417..b7c77d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ -use actix_web::{get, App, HttpServer}; +use actix_web::{get, web, App, HttpServer}; use log::info; -mod books; mod config; mod db; +mod reading; #[actix_web::main] async fn main() -> Result<(), std::io::Error> { @@ -13,14 +13,14 @@ async fn main() -> Result<(), std::io::Error> { .init(); let config = config::parse_config(); - let db = db::DataBase::sqlite().await; + let db = db::DbInterface::sqlite().await; info!("Server starting..."); HttpServer::new(move || { App::new() - .app_data(config) - .app_data(db.clone()) - .service(books::reading_scope()) + .app_data(web::Data::new(config)) + .app_data(web::Data::new(db.clone())) + .service(reading::reading_scope()) .service(hello) }) .bind((config.binding_ip, config.port))? diff --git a/src/reading/mod.rs b/src/reading/mod.rs new file mode 100644 index 0000000..f6bdc3c --- /dev/null +++ b/src/reading/mod.rs @@ -0,0 +1,24 @@ +use actix_web::{get, web, Scope}; + +use crate::db::DbInterface; + +/// scope to handle all reading related pages +pub fn reading_scope() -> Scope { + web::scope("/r").service(book_view).service(chapter_view) +} + +/// route to view info for a specific book +#[get("/b/{book}")] +async fn book_view(book: web::Path, db: web::Data) -> Option { + Some(format!( + "This is the info for {}", + db.get_book(*book).await.ok()?.title + )) +} + +/// view for reading a chapter +#[get("/c/{chapter})")] +async fn chapter_view(id: web::Path, db: web::Data) -> Option { + let chapter = db.get_chapter(*id).await.ok()?; + Some(format!("Text: {}", chapter.text)) +} From 1ae83668ee97a39788819174171ac5d15e81a552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morr=C3=ADgan?= <81323548+TheMorrigna@users.noreply.github.com> Date: Sun, 15 Dec 2024 11:07:51 +0100 Subject: [PATCH 10/10] stupid shit, lets clean up --- src/api.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ src/db/mod.rs | 3 ++- src/db/models.rs | 16 ++++++++-------- src/main.rs | 1 + 4 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 src/api.rs diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..2d07234 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,46 @@ +use actix_web::{ + get, + web::{self, Json, Redirect}, + Scope, +}; +use serde::Deserialize; + +use crate::db::DbInterface; + +pub fn api_scope() -> Scope { + web::scope("/api") + .service(create_book) + .service(create_chapter) + .service(create_user) +} + +#[derive(Deserialize)] +struct BookForm { + title: String, + description: String, +} + +#[get("/create/book")] +async fn create_book(Json(form): Json, db: web::Data) -> Redirect { + let id = db + .create_book(&form.title, &form.description, todo!()) + .await + .unwrap(); + + Redirect::to(format!("r/b/{}", id)).permanent() +} + +#[get("/create/chapter")] +async fn create_chapter() -> String { + todo!() +} + +#[derive(Deserialize)] +struct UserForm { + name: String, +} + +#[get("/create/user")] +async fn create_user(web::Form(form): web::Form, db: web::Data) -> String { + todo!() +} diff --git a/src/db/mod.rs b/src/db/mod.rs index 69236fc..c058993 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -16,6 +16,7 @@ pub enum DbInterface { } /// Error type for handling DB related errors +#[derive(Debug)] pub enum DbError { /// No such entry found NotFound, @@ -116,7 +117,7 @@ impl DbInterface { } } - /// Tries to create a chapter and returns the chapter's id if successfu + /// Tries to create a chapter and returns the chapter's id if successful pub async fn create_chapter( &self, title: &String, diff --git a/src/db/models.rs b/src/db/models.rs index 470c4d1..51feae0 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -1,3 +1,11 @@ +pub struct Book { + pub id: u32, + pub title: String, + pub description: String, + pub creation_date: String, + pub author_id: u32, +} + pub struct Chapter { pub id: u32, pub title: String, @@ -7,14 +15,6 @@ pub struct Chapter { pub author_id: u32, } -pub struct Book { - pub id: u32, - pub title: String, - pub description: String, - pub creation_date: String, - pub author_id: u32, -} - pub struct User { pub id: u32, pub name: String, diff --git a/src/main.rs b/src/main.rs index b7c77d8..3470a57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use actix_web::{get, web, App, HttpServer}; use log::info; +mod api; mod config; mod db; mod reading;