From 7f80b83809d3edefd07a078fbee3dc4600efe02b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Tue, 29 Aug 2023 21:33:59 +0200 Subject: [PATCH] schema stuff works --- rust/Cargo.lock | 7 + rust/Cargo.toml | 9 +- rust/build.rs | 5 + rust/migrations/20230519222555_initial.sql | 18 +- rust/rebuild-schema-data.sh | 7 + rust/sqlx-data.json | 439 +++++++++++++++++++++ rust/src/components/inventory.rs | 12 +- rust/src/components/mod.rs | 4 +- rust/src/components/trip.rs | 24 +- rust/src/main.rs | 276 ++++++++----- rust/src/models.rs | 348 +++++++++++----- 11 files changed, 922 insertions(+), 227 deletions(-) create mode 100644 rust/build.rs create mode 100755 rust/rebuild-schema-data.sh create mode 100644 rust/sqlx-data.json diff --git a/rust/Cargo.lock b/rust/Cargo.lock index f572eb4..fa0f1a4 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -220,6 +220,9 @@ name = "either" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +dependencies = [ + "serde", +] [[package]] name = "event-listener" @@ -1154,6 +1157,7 @@ dependencies = [ "percent-encoding", "rustls", "rustls-pemfile", + "serde", "sha2", "smallvec", "sqlformat", @@ -1175,9 +1179,12 @@ dependencies = [ "dotenvy", "either", "heck", + "hex", "once_cell", "proc-macro2", "quote", + "serde", + "serde_json", "sha2", "sqlx-core", "sqlx-rt", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9b5d5c5..856094d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -39,7 +39,14 @@ features = [ [dependencies.sqlx] version = "0.6.3" -features = ["runtime-tokio-rustls", "sqlite", "macros", "time"] +features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "time", + "offline", + "migrate", +] [dependencies.futures] version = "0.3.28" diff --git a/rust/build.rs b/rust/build.rs new file mode 100644 index 0000000..7609593 --- /dev/null +++ b/rust/build.rs @@ -0,0 +1,5 @@ +// generated by `sqlx migrate build-script` +fn main() { + // trigger recompilation when a new migration is added + println!("cargo:rerun-if-changed=migrations"); +} \ No newline at end of file diff --git a/rust/migrations/20230519222555_initial.sql b/rust/migrations/20230519222555_initial.sql index 8a5f88c..0d70051 100644 --- a/rust/migrations/20230519222555_initial.sql +++ b/rust/migrations/20230519222555_initial.sql @@ -1,10 +1,12 @@ CREATE TABLE "inventory_items" ( - id TEXT, - name TEXT, - description TEXT, - weight INT, -category_id TEXT, -FOREIGN KEY (category_id) REFERENCES inventory_items_categories(id)); + id VARCHAR(36) NOT NULL, + name TEXT NOT NULL, + description TEXT, + weight INTEGER NOT NULL, + category_id VARCHAR(36) NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (category_id) REFERENCES inventory_items_categories(id) +); CREATE UNIQUE INDEX ux_unique ON inventory_items(name, category_id); CREATE TABLE "inventory_items_categories" ( @@ -21,7 +23,7 @@ CREATE TABLE "trips" ( date_start DATE NOT NULL, date_end DATE NOT NULL, location TEXT, - state VARCHAR(8) NOT NULL DEFAULT "Planning", + state VARCHAR(8) NOT NULL, comment TEXT, temp_min INTEGER, temp_max INTEGER, @@ -29,7 +31,6 @@ CREATE TABLE "trips" ( UNIQUE (name) ); - CREATE TABLE "trips_types" ( id VARCHAR(36) NOT NULL, name TEXT NOT NULL, @@ -45,7 +46,6 @@ CREATE TABLE "trips_to_trips_types" ( FOREIGN KEY(trip_type_id) REFERENCES "trips_types" (id) ); - CREATE TABLE trips_items ( item_id VARCHAR(36) NOT NULL, trip_id VARCHAR(36) NOT NULL, diff --git a/rust/rebuild-schema-data.sh b/rust/rebuild-schema-data.sh new file mode 100755 index 0000000..072ccee --- /dev/null +++ b/rust/rebuild-schema-data.sh @@ -0,0 +1,7 @@ +db="$(mktemp)" + +export DATABASE_URL="sqlite://${db}" + +cargo sqlx database create +cargo sqlx migrate run +cargo sqlx prepare diff --git a/rust/sqlx-data.json b/rust/sqlx-data.json new file mode 100644 index 0000000..a45787b --- /dev/null +++ b/rust/sqlx-data.json @@ -0,0 +1,439 @@ +{ + "db": "SQLite", + "0d341935886c28710302aec9d5d085b535ad54949b87793e98cbf3bd5d828a41": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 3 + } + }, + "query": "UPDATE inventory_items AS item\n SET\n name = ?,\n weight = ?\n WHERE item.id = ?\n RETURNING inventory_items.category_id AS id\n " + }, + "10886f1ddebc2a11bd2f2cbd41bd5220cde17405e1210c792dda29ca100c01cb": { + "describe": { + "columns": [ + { + "name": "category_id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "category_name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "category_description", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "trip_id", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "item_id", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "item_name", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "item_description", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "item_weight", + "ordinal": 7, + "type_info": "Int64" + }, + { + "name": "item_is_picked", + "ordinal": 8, + "type_info": "Bool" + }, + { + "name": "item_is_packed", + "ordinal": 9, + "type_info": "Bool" + } + ], + "nullable": [ + false, + false, + true, + true, + true, + true, + true, + true, + true, + true + ], + "parameters": { + "Right": 1 + } + }, + "query": "\n SELECT\n category.id as category_id,\n category.name as category_name,\n category.description AS category_description,\n inner.trip_id AS trip_id,\n inner.item_id AS item_id,\n inner.item_name AS item_name,\n inner.item_description AS item_description,\n inner.item_weight AS item_weight,\n inner.item_is_picked AS item_is_picked,\n inner.item_is_packed AS item_is_packed\n FROM inventory_items_categories AS category\n LEFT JOIN (\n SELECT\n trip.trip_id AS trip_id,\n category.id as category_id,\n category.name as category_name,\n category.description as category_description,\n item.id as item_id,\n item.name as item_name,\n item.description as item_description,\n item.weight as item_weight,\n trip.pick as item_is_picked,\n trip.pack as item_is_packed\n FROM trips_items as trip\n INNER JOIN inventory_items as item\n ON item.id = trip.item_id\n INNER JOIN inventory_items_categories as category\n ON category.id = item.category_id\n WHERE trip.trip_id = ?\n ) AS inner\n ON inner.category_id = category.id\n " + }, + "18cbb2893df033f5f81f42097fcae7ee036405749a5d93f2ea1d79ba280dfd20": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "DELETE FROM trips_to_trips_types AS ttt\n WHERE ttt.trip_id = ?\n AND ttt.trip_type_id = ?\n " + }, + "1f08e9bebf51aab9cabff2a5c79211233a686e9ef9f96ea5c036fbba8f6b06d5": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "active", + "ordinal": 2, + "type_info": "Int" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "\n SELECT\n type.id as id,\n type.name as name,\n inner.id IS NOT NULL AS active\n FROM trips_types AS type\n LEFT JOIN (\n SELECT type.id as id, type.name as name\n FROM trips as trip\n INNER JOIN trips_to_trips_types as ttt\n ON ttt.trip_id = trip.id\n INNER JOIN trips_types AS type\n ON type.id == ttt.trip_type_id\n WHERE trip.id = ?\n ) AS inner\n ON inner.id = type.id\n " + }, + "4d377bb01af6bbbca637d8c61326c84e8b05b1e570199c464b593bdc81b3dba6": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "INSERT INTO trips_to_trips_types\n (trip_id, trip_type_id) VALUES (?, ?)" + }, + "6973cceeb5499216475136b320b25e1355974e1213829d931abdd6b7a1448a87": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "weight", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "description", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "category_id", + "ordinal": 4, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false, + true, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT\n id,\n name,\n weight,\n description,\n category_id\n FROM inventory_items\n WHERE category_id = ?" + }, + "6e2928c8c2e66b15fc3f6f0ae4e8e0d4616b714fccbf273306d5135df31f4c19": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "INSERT INTO inventory_items_categories\n (id, name)\n VALUES\n (?, ?)" + }, + "7746dbbd63e69f7ec8ba5c1036e9ac03b83021be3189bb38ff31131cfbe99534": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 5 + } + }, + "query": "INSERT INTO inventory_items\n (id, name, description, weight, category_id)\n VALUES\n (?, ?, ?, ?, ?)" + }, + "7f23d9e4bb088de4123e93e5287c9743a417aab218d1d9484c0c6f3ac763f772": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "date_start", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "date_end", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "state", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "location", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "temp_min", + "ordinal": 6, + "type_info": "Int64" + }, + { + "name": "temp_max", + "ordinal": 7, + "type_info": "Int64" + }, + { + "name": "comment", + "ordinal": 8, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + true, + true + ], + "parameters": { + "Right": 0 + } + }, + "query": "SELECT\n id,\n name,\n CAST (date_start AS TEXT) date_start,\n CAST (date_end AS TEXT) date_end,\n state,\n location,\n temp_min,\n temp_max,\n comment\n FROM trips" + }, + "88293d85c61e1eeaf9e46ada4154736b127c3bf305e92130de87b89ce7c6edab": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "UPDATE trips\n SET comment = ?\n WHERE id = ?" + }, + "8f2499b0b98e3aa7d8c5925d7898406928572312b034d23ec96acbf19315a74e": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "date_start", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "date_end", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "state", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "location", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "temp_min", + "ordinal": 6, + "type_info": "Int64" + }, + { + "name": "temp_max", + "ordinal": 7, + "type_info": "Int64" + }, + { + "name": "comment", + "ordinal": 8, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + true, + true + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT\n id,\n name,\n CAST (date_start AS TEXT) AS date_start,\n CAST (date_end AS TEXT) AS date_end,\n state,\n location,\n temp_min,\n temp_max,\n comment\n FROM trips\n WHERE id = ?" + }, + "982720cc8c246f8cae25106b253394e5492ac968bd604a6fa7f848eee4174696": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 5 + } + }, + "query": "INSERT INTO trips\n (id, name, date_start, date_end, state)\n VALUES\n (?, ?, ?, ?, ?)" + }, + "a81bcbeb11260e3b4363e19c26b71b489e326b08bfacb6e11b4c4fc068dc7806": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "description", + "ordinal": 2, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + true + ], + "parameters": { + "Right": 0 + } + }, + "query": "SELECT id,name,description FROM inventory_items_categories" + }, + "ab7c1b44121defb6c55291ef68958acfb9ba36a63cd7dd1286101d2e6c7065e0": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 1 + } + }, + "query": "DELETE FROM inventory_items\n WHERE id = ?" + }, + "b916db63913aa222cef4552dffdcda0f26f16612fbb4c1e839bfd0162888fdc3": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "description", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "weight", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "category_id", + "ordinal": 4, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + true, + false, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT * FROM inventory_items AS item\n WHERE item.id = ?" + } +} \ No newline at end of file diff --git a/rust/src/components/inventory.rs b/rust/src/components/inventory.rs index 6816f08..dac1141 100644 --- a/rust/src/components/inventory.rs +++ b/rust/src/components/inventory.rs @@ -37,7 +37,7 @@ pub struct InventoryCategoryList; impl InventoryCategoryList { pub fn build(state: &ClientState, categories: &Vec) -> Markup { - let biggest_category_weight: u32 = categories + let biggest_category_weight: i64 = categories .iter() .map(Category::total_weight) .max() @@ -115,8 +115,8 @@ impl InventoryCategoryList { format!( "width: {width}%;position:absolute;left:0;bottom:0;right:0;", width=( - f64::from(category.total_weight()) - / f64::from(biggest_category_weight) + (category.total_weight() as f64) + / (biggest_category_weight as f64) * 100.0 ) ) @@ -130,7 +130,7 @@ impl InventoryCategoryList { } td ."border" ."p-0" ."m-0" { p ."p-2" ."m-2" { - (categories.iter().map(Category::total_weight).sum::().to_string()) + (categories.iter().map(Category::total_weight).sum::().to_string()) } } } @@ -145,7 +145,7 @@ pub struct InventoryItemList; impl InventoryItemList { pub fn build(state: &ClientState, items: &Vec) -> Markup { - let biggest_item_weight: u32 = items.iter().map(|item| item.weight).max().unwrap_or(1); + let biggest_item_weight: i64 = items.iter().map(|item| item.weight).max().unwrap_or(1); html!( div #items { @if items.is_empty() { @@ -267,7 +267,7 @@ impl InventoryItemList { position:absolute; left:0; bottom:0; - right:0;", width=(f64::from(item.weight) / f64::from(biggest_item_weight) * 100.0))) {} + right:0;", width=((item.weight as f64) / (biggest_item_weight as f64) * 100.0))) {} } td ."border-none" diff --git a/rust/src/components/mod.rs b/rust/src/components/mod.rs index 47735be..a405af4 100644 --- a/rust/src/components/mod.rs +++ b/rust/src/components/mod.rs @@ -29,7 +29,9 @@ impl Root { link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"; script { (include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js"))) } } - body hx-boost="true" { + body + hx-boost="true" + { header ."bg-gray-200" ."p-5" diff --git a/rust/src/components/trip.rs b/rust/src/components/trip.rs index 59fb922..a4f16f7 100644 --- a/rust/src/components/trip.rs +++ b/rust/src/components/trip.rs @@ -276,7 +276,7 @@ pub struct TripInfoRow; impl TripInfoRow { pub fn build( name: &str, - value: impl std::fmt::Display, + value: Option, attribute_key: TripAttribute, edit_attribute: Option<&TripAttribute>, input_type: InputType, @@ -303,7 +303,7 @@ impl TripInfoRow { id="new-value" name="new-value" form="edit-trip" - value=(value) + value=(value.map_or("".to_string(), |v| v.to_string())) ; } } @@ -355,7 +355,7 @@ impl TripInfoRow { } } @else { td ."border" ."p-2" { (name) } - td ."border" ."p-2" { (value) } + td ."border" ."p-2" { (value.map_or("".to_string(), |v| v.to_string())) } td ."border-none" ."bg-blue-100" @@ -397,9 +397,9 @@ impl TripInfo { ."w-full" { tbody { - (TripInfoRow::build("Location", &trip.location, TripAttribute::Location, state.trip_edit_attribute.as_ref(), InputType::Text)) - (TripInfoRow::build("Start date", trip.date_start, TripAttribute::DateStart, state.trip_edit_attribute.as_ref(), InputType::Date)) - (TripInfoRow::build("End date", trip.date_end, TripAttribute::DateEnd, state.trip_edit_attribute.as_ref(), InputType::Date)) + (TripInfoRow::build("Location", trip.location.as_ref(), TripAttribute::Location, state.trip_edit_attribute.as_ref(), InputType::Text)) + (TripInfoRow::build("Start date", Some(trip.date_start), TripAttribute::DateStart, state.trip_edit_attribute.as_ref(), InputType::Date)) + (TripInfoRow::build("End date", Some(trip.date_end), TripAttribute::DateEnd, state.trip_edit_attribute.as_ref(), InputType::Date)) (TripInfoRow::build("Temp (min)", trip.temp_min, TripAttribute::TempMin, state.trip_edit_attribute.as_ref(), InputType::Number)) (TripInfoRow::build("Temp (max)", trip.temp_max, TripAttribute::TempMax, state.trip_edit_attribute.as_ref(), InputType::Number)) tr .h-full { @@ -587,7 +587,7 @@ impl TripCategoryList { pub fn build(state: &ClientState, trip: &models::Trip) -> Markup { let categories = trip.categories(); - let biggest_category_weight: u32 = categories + let biggest_category_weight: i64 = categories .iter() .map(TripCategory::total_picked_weight) .max() @@ -655,8 +655,8 @@ impl TripCategoryList { format!( "width: {width}%;position:absolute;left:0;bottom:0;right:0;", width=( - f64::from(category.total_picked_weight()) - / f64::from(biggest_category_weight) + (category.total_picked_weight() as f64) + / (biggest_category_weight as f64) * 100.0 ) ) @@ -670,7 +670,7 @@ impl TripCategoryList { } td ."border" ."p-0" ."m-0" { p ."p-2" ."m-2" { - (categories.iter().map(TripCategory::total_picked_weight).sum::().to_string()) + (categories.iter().map(TripCategory::total_picked_weight).sum::().to_string()) } } } @@ -684,7 +684,7 @@ pub struct TripItemList; impl TripItemList { pub fn build(state: &ClientState, trip: &models::Trip, items: &Vec) -> Markup { - let biggest_item_weight: u32 = items.iter().map(|item| item.item.weight).max().unwrap_or(1); + let biggest_item_weight: i64 = items.iter().map(|item| item.item.weight).max().unwrap_or(1); html!( @if items.is_empty() { @@ -778,7 +778,7 @@ impl TripItemList { position:absolute; left:0; bottom:0; - right:0;", width=(f64::from(item.item.weight) / f64::from(biggest_item_weight) * 100.0))) {} + right:0;", width=((item.item.weight as f64) / (biggest_item_weight as f64) * 100.0))) {} } } } diff --git a/rust/src/main.rs b/rust/src/main.rs index 8b4433c..7c24ac5 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -15,8 +15,8 @@ use serde_variant::to_variant_name; use sqlx::{ error::DatabaseError, - query, - sqlite::{SqliteConnectOptions, SqliteError, SqlitePoolOptions}, + query, query_as, + sqlite::{SqliteConnectOptions, SqliteError, SqlitePoolOptions, SqliteRow}, Pool, Row, Sqlite, }; @@ -170,29 +170,32 @@ async fn inventory( ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { state.client_state.active_category_id = active_id; - let mut categories = query("SELECT id,name,description FROM inventory_items_categories") - .fetch(&state.database_pool) - .map_ok(std::convert::TryInto::try_into) - .try_collect::>>() - .await - // we have two error handling lines here. these are distinct errors - // this one is the SQL error that may arise during the query - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - ErrorPage::build(&e.to_string()), - ) - })? - .into_iter() - .collect::, models::Error>>() - // and this one is the model mapping error that may arise e.g. during - // reading of the rows - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - ErrorPage::build(&e.to_string()), - ) - })?; + let mut categories = query_as!( + DbCategoryRow, + "SELECT id,name,description FROM inventory_items_categories" + ) + .fetch(&state.database_pool) + .map_ok(|row: DbCategoryRow| row.try_into()) + .try_collect::>>() + .await + // we have two error handling lines here. these are distinct errors + // this one is the SQL error that may arise during the query + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })? + .into_iter() + .collect::, models::Error>>() + // and this one is the model mapping error that may arise e.g. during + // reading of the rows + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })?; for category in &mut categories { category @@ -241,17 +244,21 @@ async fn inventory_item_create( State(state): State, Form(new_item): Form, ) -> Result { - query( + let id = Uuid::new_v4(); + let id_param = id.to_string(); + let name = &new_item.name; + let category_id = new_item.category_id.to_string(); + query!( "INSERT INTO inventory_items (id, name, description, weight, category_id) VALUES (?, ?, ?, ?, ?)", + id_param, + name, + "", + new_item.weight, + category_id, ) - .bind(Uuid::new_v4().to_string()) - .bind(&new_item.name) - .bind("") - .bind(new_item.weight) - .bind(new_item.category_id.to_string()) .execute(&state.database_pool) .await .map_err(|e| match e { @@ -306,11 +313,12 @@ async fn inventory_item_delete( headers: HeaderMap, Path(id): Path, ) -> Result { - let results = query( + let id_param = id.to_string(); + let results = query!( "DELETE FROM inventory_items WHERE id = ?", + id_param, ) - .bind(id.to_string()) .execute(&state.database_pool) .await .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?; @@ -348,7 +356,7 @@ async fn inventory_item_delete( // .await // .unwrap(); -// let items = query(&format!( +// let items = query!(&format!( // //TODO bind this stuff!!!!!!! no sql injection pls // "SELECT // i.id, i.name, i.description, i.weight, i.category_id @@ -392,14 +400,29 @@ async fn inventory_item_edit( State(state): State, Path(id): Path, Form(edit_item): Form, -) -> Result { - let id = Item::update(&state.database_pool, id, &edit_item.name, edit_item.weight) - .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? - .ok_or(( - StatusCode::NOT_FOUND, - format!("item with id {id} not found", id = id), - ))?; +) -> Result { + let id = Item::update( + &state.database_pool, + id, + &edit_item.name, + i64::try_from(edit_item.weight).map_err(|e| { + ( + StatusCode::UNPROCESSABLE_ENTITY, + ErrorPage::build(&e.to_string()), + ) + })?, + ) + .await + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })? + .ok_or(( + StatusCode::NOT_FOUND, + ErrorPage::build(&format!("item with id {id} not found", id = id)), + ))?; Ok(Redirect::to(&format!("/inventory/category/{id}/", id = id))) } @@ -437,16 +460,26 @@ async fn trip_create( Form(new_trip): Form, ) -> Result { let id = Uuid::new_v4(); - query( + let id_param = id.to_string(); + let date_start = new_trip + .date_start + .format(DATE_FORMAT) + .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?; + let date_end = new_trip + .date_end + .format(DATE_FORMAT) + .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?; + query!( "INSERT INTO trips - (id, name, date_start, date_end) + (id, name, date_start, date_end, state) VALUES - (?, ?, ?, ?)", + (?, ?, ?, ?, ?)", + id_param, + new_trip.name, + date_start, + date_end, + TripState::Planning, ) - .bind(id.to_string()) - .bind(&new_trip.name) - .bind(new_trip.date_start) - .bind(new_trip.date_end) .execute(&state.database_pool) .await .map_err(|e| match e { @@ -482,35 +515,52 @@ async fn trip_create( ), })?; - Ok(Redirect::to(&format!("/trip/{id}/", id = id.to_string()))) + Ok(Redirect::to(&format!("/trip/{id}/", id = id))) } async fn trips( State(state): State, ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { - let trips: Vec = query("SELECT * FROM trips") - .fetch(&state.database_pool) - .map_ok(std::convert::TryInto::try_into) - .try_collect::>>() - .await - // we have two error handling lines here. these are distinct errors - // this one is the SQL error that may arise during the query - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - ErrorPage::build(&e.to_string()), - ) - })? - .into_iter() - .collect::, models::Error>>() - // and this one is the model mapping error that may arise e.g. during - // reading of the rows - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - ErrorPage::build(&e.to_string()), - ) - })?; + tracing::info!("receiving trips"); + + let trips: Vec = query_as!( + DbTripRow, + "SELECT + id, + name, + CAST (date_start AS TEXT) date_start, + CAST (date_end AS TEXT) date_end, + state, + location, + temp_min, + temp_max, + comment + FROM trips", + ) + .fetch(&state.database_pool) + .map_ok(|row| row.try_into()) + .try_collect::>>() + .await + // we have two error handling lines here. these are distinct errors + // this one is the SQL error that may arise during the query + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })? + .into_iter() + .collect::, models::Error>>() + // and this one is the model mapping error that may arise e.g. during + // reading of the rows + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })?; + + tracing::info!("received trips"); Ok(( StatusCode::OK, @@ -532,20 +582,42 @@ async fn trip( state.client_state.trip_edit_attribute = trip_query.edit; state.client_state.active_category_id = trip_query.category; - let mut trip: models::Trip = - query("SELECT id,name,date_start,date_end,state,location,temp_min,temp_max,comment FROM trips WHERE id = ?") - .bind(id.to_string()) - .fetch_one(&state.database_pool) - .map_ok(std::convert::TryInto::try_into) - .await - .map_err(|e: sqlx::Error| match e { - sqlx::Error::RowNotFound => ( - StatusCode::NOT_FOUND, - ErrorPage::build(&format!("trip with id {} not found", id)), - ), - _ => (StatusCode::INTERNAL_SERVER_ERROR, ErrorPage::build(&e.to_string())), - })? - .map_err(|e: Error| (StatusCode::INTERNAL_SERVER_ERROR, ErrorPage::build(&e.to_string())))?; + let id_param = id.to_string(); + let mut trip: models::Trip = query_as!( + DbTripRow, + "SELECT + id, + name, + CAST (date_start AS TEXT) AS date_start, + CAST (date_end AS TEXT) AS date_end, + state, + location, + temp_min, + temp_max, + comment + FROM trips + WHERE id = ?", + id_param, + ) + .fetch_one(&state.database_pool) + .map_ok(|row| row.try_into()) + .await + .map_err(|e: sqlx::Error| match e { + sqlx::Error::RowNotFound => ( + StatusCode::NOT_FOUND, + ErrorPage::build(&format!("trip with id {} not found", id)), + ), + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ), + })? + .map_err(|e: Error| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })?; trip.load_trips_types(&state.database_pool) .await @@ -586,14 +658,16 @@ async fn trip_type_remove( State(state): State, Path((trip_id, type_id)): Path<(Uuid, Uuid)>, ) -> Result { - let results = query( + let trip_id = trip_id.to_string(); + let type_id = type_id.to_string(); + let results = query!( "DELETE FROM trips_to_trips_types AS ttt WHERE ttt.trip_id = ? AND ttt.trip_type_id = ? ", + trip_id, + type_id ) - .bind(trip_id.to_string()) - .bind(type_id.to_string()) .execute(&state.database_pool) .await .map_err(|e| (StatusCode::BAD_REQUEST, ErrorPage::build(&e.to_string())))?; @@ -612,12 +686,14 @@ async fn trip_type_add( State(state): State, Path((trip_id, type_id)): Path<(Uuid, Uuid)>, ) -> Result { - query( + let trip_id = trip_id.to_string(); + let type_id = type_id.to_string(); + query!( "INSERT INTO trips_to_trips_types (trip_id, trip_type_id) VALUES (?, ?)", + trip_id, + type_id ) - .bind(trip_id.to_string()) - .bind(type_id.to_string()) .execute(&state.database_pool) .await .map_err(|e| match e { @@ -682,13 +758,14 @@ async fn trip_comment_set( Path(trip_id): Path, Form(comment_update): Form, ) -> Result { - let result = query( + let trip_id = trip_id.to_string(); + let result = query!( "UPDATE trips SET comment = ? WHERE id = ?", + comment_update.new_comment, + trip_id, ) - .bind(comment_update.new_comment) - .bind(trip_id.to_string()) .execute(&state.database_pool) .await .map_err(|e| (StatusCode::BAD_REQUEST, ErrorPage::build(&e.to_string())))?; @@ -884,14 +961,15 @@ async fn inventory_category_create( Form(new_category): Form, ) -> Result { let id = Uuid::new_v4(); - query( + let id_param = id.to_string(); + query!( "INSERT INTO inventory_items_categories (id, name) VALUES (?, ?)", + id_param, + new_category.name ) - .bind(id.to_string()) - .bind(&new_category.name) .execute(&state.database_pool) .map_err(|e| match e { sqlx::Error::Database(ref error) => { diff --git a/rust/src/models.rs b/rust/src/models.rs index abe9d02..c123eb9 100644 --- a/rust/src/models.rs +++ b/rust/src/models.rs @@ -8,6 +8,7 @@ use sqlx::{ use std::convert; use std::error; use std::fmt; +use std::num::TryFromIntError; use std::str::FromStr; use uuid::Uuid; @@ -16,10 +17,19 @@ use sqlx::sqlite::SqlitePoolOptions; use futures::TryFutureExt; use futures::TryStreamExt; +use time::{ + error::Parse as TimeParseError, format_description::FormatItem, macros::format_description, +}; + +pub const DATE_FORMAT: &[FormatItem<'static>] = format_description!("[year]-[month]-[day]"); + pub enum Error { SqlError { description: String }, UuidError { description: String }, + EnumError { description: String }, NotFoundError { description: String }, + IntError { description: String }, + TimeParseError { description: String }, } impl fmt::Display for Error { @@ -34,6 +44,15 @@ impl fmt::Display for Error { Self::NotFoundError { description } => { write!(f, "Not found: {description}") } + Self::IntError { description } => { + write!(f, "Integer error: {description}") + } + Self::EnumError { description } => { + write!(f, "Enum error: {description}") + } + Self::TimeParseError { description } => { + write!(f, "Date parse error: {description}") + } } } } @@ -61,6 +80,22 @@ impl convert::From for Error { } } +impl convert::From for Error { + fn from(value: TryFromIntError) -> Self { + Error::IntError { + description: value.to_string(), + } + } +} + +impl convert::From for Error { + fn from(value: TimeParseError) -> Self { + Error::TimeParseError { + description: value.to_string(), + } + } +} + impl error::Error for Error {} #[derive(sqlx::Type)] @@ -88,6 +123,25 @@ impl fmt::Display for TripState { } } +impl std::convert::TryFrom<&str> for TripState { + type Error = Error; + + fn try_from(value: &str) -> Result { + Ok(match value { + "Planning" => Self::Planning, + "Planned" => Self::Planned, + "Active" => Self::Active, + "Review" => Self::Review, + "Done" => Self::Done, + _ => { + return Err(Error::EnumError { + description: format!("{} is not a valid value for TripState", value), + }) + } + }) + } +} + #[derive(Serialize, Debug)] pub enum TripItemStateKey { Pick, @@ -114,7 +168,7 @@ pub struct TripCategory { } impl TripCategory { - pub fn total_picked_weight(&self) -> u32 { + pub fn total_picked_weight(&self) -> i64 { self.items .as_ref() .unwrap() @@ -132,15 +186,47 @@ pub struct TripItem { pub packed: bool, } +pub struct DbTripRow { + pub id: String, + pub name: String, + pub date_start: String, + pub date_end: String, + pub state: String, + pub location: Option, + pub temp_min: Option, + pub temp_max: Option, + pub comment: Option, +} + +impl TryFrom for Trip { + type Error = Error; + + fn try_from(row: DbTripRow) -> Result { + Ok(Trip { + id: Uuid::try_parse(&row.id)?, + name: row.name, + date_start: time::Date::parse(&row.date_start, DATE_FORMAT)?, + date_end: time::Date::parse(&row.date_end, DATE_FORMAT)?, + state: row.state.as_str().try_into()?, + location: row.location, + temp_min: row.temp_min, + temp_max: row.temp_max, + comment: row.comment, + types: None, + categories: None, + }) + } +} + pub struct Trip { pub id: Uuid, pub name: String, pub date_start: time::Date, pub date_end: time::Date, pub state: TripState, - pub location: String, - pub temp_min: i32, - pub temp_max: i32, + pub location: Option, + pub temp_min: Option, + pub temp_max: Option, pub comment: Option, types: Option>, categories: Option>, @@ -193,37 +279,37 @@ pub enum TripAttribute { // } // } -impl TryFrom for Trip { - type Error = Error; +// impl TryFrom for Trip { +// type Error = Error; - fn try_from(row: SqliteRow) -> Result { - let name: &str = row.try_get("name")?; - let id: &str = row.try_get("id")?; - let date_start: time::Date = row.try_get("date_start")?; - let date_end: time::Date = row.try_get("date_end")?; - let state: TripState = row.try_get("state")?; - let location = row.try_get("location")?; - let temp_min = row.try_get("temp_min")?; - let temp_max = row.try_get("temp_max")?; - let comment = row.try_get("comment")?; +// fn try_from(row: SqliteRow) -> Result { +// let name: &str = row.try_get("name")?; +// let id: &str = row.try_get("id")?; +// let date_start: time::Date = row.try_get("date_start")?; +// let date_end: time::Date = row.try_get("date_end")?; +// let state: TripState = row.try_get("state")?; +// let location = row.try_get("location")?; +// let temp_min = row.try_get("temp_min")?; +// let temp_max = row.try_get("temp_max")?; +// let comment = row.try_get("comment")?; - let id: Uuid = Uuid::try_parse(id)?; +// let id: Uuid = Uuid::try_parse(id)?; - Ok(Trip { - id, - name: name.to_string(), - date_start, - date_end, - state, - location, - temp_min, - temp_max, - comment, - types: None, - categories: None, - }) - } -} +// Ok(Trip { +// id, +// name: name.to_string(), +// date_start, +// date_end, +// state, +// location, +// temp_min, +// temp_max, +// comment, +// types: None, +// categories: None, +// }) +// } +// } impl<'a> Trip { pub fn types(&'a self) -> &Vec { @@ -244,12 +330,13 @@ impl<'a> Trip { &'a mut self, pool: &sqlx::Pool, ) -> Result<(), Error> { - let types = sqlx::query( + let id = self.id.to_string(); + let types = sqlx::query!( " SELECT type.id as id, type.name as name, - CASE WHEN inner.id IS NOT NULL THEN true ELSE false END AS active + inner.id IS NOT NULL AS active FROM trips_types AS type LEFT JOIN ( SELECT type.id as id, type.name as name @@ -262,10 +349,20 @@ impl<'a> Trip { ) AS inner ON inner.id = type.id ", + id ) - .bind(self.id.to_string()) .fetch(pool) - .map_ok(std::convert::TryInto::try_into) + .map_ok(|row| -> Result { + Ok(TripType { + id: Uuid::try_parse(&row.id)?, + name: row.name, + active: match row.active { + 0 => false, + 1 => true, + _ => unreachable!(), + }, + }) + }) .try_collect::>>() .await? .into_iter() @@ -282,14 +379,14 @@ impl<'a> Trip { let mut categories: Vec = vec![]; // we can ignore the return type as we collect into `categories` // in the `map_ok()` closure - sqlx::query( + let id = self.id.to_string(); + sqlx::query!( " SELECT category.id as category_id, category.name as category_name, category.description AS category_description, inner.trip_id AS trip_id, - inner.category_description AS category_description, inner.item_id AS item_id, inner.item_name AS item_name, inner.item_description AS item_description, @@ -314,25 +411,28 @@ impl<'a> Trip { ON item.id = trip.item_id INNER JOIN inventory_items_categories as category ON category.id = item.category_id - WHERE trip.trip_id = 'a8b181d6-3b16-4a41-99fa-0713b94a34d9' + WHERE trip.trip_id = ? ) AS inner ON inner.category_id = category.id ", + id ) - .bind(self.id.to_string()) .fetch(pool) .map_ok(|row| -> Result<(), Error> { let mut category = TripCategory { category: Category { - id: Uuid::try_parse(row.try_get("category_id")?)?, - name: row.try_get("category_name")?, - description: row.try_get("category_description")?, + id: Uuid::try_parse(&row.category_id)?, + name: row.category_name, + // TODO align optionality between code and database + // idea: make description nullable + description: row.category_description, + items: None, }, items: None, }; - match row.try_get("item_id")? { + match row.item_id { None => { // we have an empty (unused) category which has NULL values // for the item_id column @@ -342,14 +442,14 @@ impl<'a> Trip { Some(item_id) => { let item = TripItem { item: Item { - id: Uuid::try_parse(item_id)?, - name: row.try_get("item_name")?, - description: row.try_get("item_description")?, - weight: row.try_get("item_weight")?, + id: Uuid::try_parse(&item_id)?, + name: row.item_name.unwrap(), + description: row.item_description, + weight: row.item_weight.unwrap(), category_id: category.category.id, }, - picked: row.try_get("item_is_picked")?, - packed: row.try_get("item_is_packed")?, + picked: row.item_is_picked.unwrap(), + packed: row.item_is_packed.unwrap(), }; if let Some(&mut ref mut c) = categories @@ -385,43 +485,70 @@ pub struct TripType { pub active: bool, } -impl TryFrom for TripType { - type Error = Error; +// impl TryFrom for TripType { +// type Error = Error; - fn try_from(row: SqliteRow) -> Result { - let id: Uuid = Uuid::try_parse(row.try_get("id")?)?; - let name: String = row.try_get::<&str, _>("name")?.to_string(); - let active: bool = row.try_get("active")?; +// fn try_from(row: SqliteRow) -> Result { +// let id: Uuid = Uuid::try_parse(row.try_get("id")?)?; +// let name: String = row.try_get::<&str, _>("name")?.to_string(); +// let active: bool = row.try_get("active")?; - Ok(Self { id, name, active }) - } +// Ok(Self { id, name, active }) +// } +// } + +pub struct DbCategoryRow { + pub id: String, + pub name: String, + pub description: Option, } #[derive(Debug)] pub struct Category { pub id: Uuid, pub name: String, - pub description: String, + pub description: Option, items: Option>, } -impl TryFrom for Category { +impl TryFrom for Category { type Error = Error; - fn try_from(row: SqliteRow) -> Result { - let name: &str = row.try_get("name")?; - let description: &str = row.try_get("description")?; - let id: Uuid = Uuid::try_parse(row.try_get("id")?)?; - + fn try_from(row: DbCategoryRow) -> Result { Ok(Category { - id, - name: name.to_string(), - description: description.to_string(), + id: Uuid::try_parse(&row.id)?, + name: row.name, + description: row.description, items: None, }) } } +// impl TryFrom for Category { +// type Error = Error; + +// fn try_from(row: SqliteRow) -> Result { +// let name: &str = row.try_get("name")?; +// let description: &str = row.try_get("description")?; +// let id: Uuid = Uuid::try_parse(row.try_get("id")?)?; + +// Ok(Category { +// id, +// name: name.to_string(), +// description: description.to_string(), +// items: None, +// }) +// } +// } + +pub struct DbInventoryItemsRow { + id: String, + name: String, + weight: i64, + description: Option, + category_id: String, +} + impl<'a> Category { pub fn items(&'a self) -> &'a Vec { self.items @@ -429,7 +556,7 @@ impl<'a> Category { .expect("you need to call populate_items()") } - pub fn total_weight(&self) -> u32 { + pub fn total_weight(&self) -> i64 { self.items().iter().map(|item| item.weight).sum() } @@ -437,15 +564,21 @@ impl<'a> Category { &'a mut self, pool: &sqlx::Pool, ) -> Result<(), Error> { - let items = sqlx::query(&format!( + let id = self.id.to_string(); + let items = sqlx::query_as!( + DbInventoryItemsRow, "SELECT - id,name,weight,description,category_id + id, + name, + weight, + description, + category_id FROM inventory_items - WHERE category_id = '{id}'", - id = self.id - )) + WHERE category_id = ?", + id + ) .fetch(pool) - .map_ok(std::convert::TryInto::try_into) + .map_ok(|row| row.try_into()) .try_collect::>>() .await? .into_iter() @@ -460,40 +593,56 @@ impl<'a> Category { pub struct Item { pub id: Uuid, pub name: String, - pub description: String, - pub weight: u32, + pub description: Option, + pub weight: i64, pub category_id: Uuid, } -impl TryFrom for Item { +impl TryFrom for Item { type Error = Error; - fn try_from(row: SqliteRow) -> Result { - let name: &str = row.try_get("name")?; - let description: &str = row.try_get("description")?; - let weight: u32 = row.try_get("weight")?; - let id: Uuid = Uuid::try_parse(row.try_get("id")?)?; - let category_id: Uuid = Uuid::try_parse(row.try_get("category_id")?)?; - + fn try_from(row: DbInventoryItemsRow) -> Result { Ok(Item { - id, - name: name.to_string(), - weight, - description: description.to_string(), - category_id, + id: Uuid::try_parse(&row.id)?, + name: row.name, + description: row.description, // TODO + weight: row.weight, + category_id: Uuid::try_parse(&row.category_id)?, }) } } +// impl TryFrom for Item { +// type Error = Error; + +// fn try_from(row: SqliteRow) -> Result { +// let name: &str = row.try_get("name")?; +// let description: &str = row.try_get("description")?; +// let weight: i64 = row.try_get("weight")?; +// let id: Uuid = Uuid::try_parse(row.try_get("id")?)?; +// let category_id: Uuid = Uuid::try_parse(row.try_get("category_id")?)?; + +// Ok(Item { +// id, +// name: name.to_string(), +// weight, +// description: description.to_string(), +// category_id, +// }) +// } +// } + impl Item { pub async fn find(pool: &sqlx::Pool, id: Uuid) -> Result, Error> { - let item: Result, sqlx::Error> = sqlx::query( + let id_param = id.to_string(); + let item: Result, sqlx::Error> = sqlx::query_as!( + DbInventoryItemsRow, "SELECT * FROM inventory_items AS item WHERE item.id = ?", + id_param, ) - .bind(id.to_string()) .fetch_one(pool) - .map_ok(std::convert::TryInto::try_into) + .map_ok(|row| row.try_into()) .await; match item { @@ -509,9 +658,10 @@ impl Item { pool: &sqlx::Pool, id: Uuid, name: &str, - weight: u32, + weight: i64, ) -> Result, Error> { - let id: Result, sqlx::Error> = sqlx::query( + let id_param = id.to_string(); + let id: Result, sqlx::Error> = sqlx::query!( "UPDATE inventory_items AS item SET name = ?, @@ -519,13 +669,13 @@ impl Item { WHERE item.id = ? RETURNING inventory_items.category_id AS id ", + name, + weight, + id_param, ) - .bind(name) - .bind(weight) - .bind(id.to_string()) .fetch_one(pool) .map_ok(|row| { - let id: &str = row.try_get("id")?; + let id: &str = &row.id.unwrap(); // TODO let uuid: Result = Uuid::try_parse(id); let uuid: Result = uuid.map_err(|e| e.into()); uuid