schema stuff works

This commit is contained in:
2023-08-29 21:33:59 +02:00
parent a3939e972d
commit 7f80b83809
11 changed files with 922 additions and 227 deletions

7
rust/Cargo.lock generated
View File

@@ -220,6 +220,9 @@ name = "either"
version = "1.8.1" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "event-listener" name = "event-listener"
@@ -1154,6 +1157,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"rustls", "rustls",
"rustls-pemfile", "rustls-pemfile",
"serde",
"sha2", "sha2",
"smallvec", "smallvec",
"sqlformat", "sqlformat",
@@ -1175,9 +1179,12 @@ dependencies = [
"dotenvy", "dotenvy",
"either", "either",
"heck", "heck",
"hex",
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde",
"serde_json",
"sha2", "sha2",
"sqlx-core", "sqlx-core",
"sqlx-rt", "sqlx-rt",

View File

@@ -39,7 +39,14 @@ features = [
[dependencies.sqlx] [dependencies.sqlx]
version = "0.6.3" version = "0.6.3"
features = ["runtime-tokio-rustls", "sqlite", "macros", "time"] features = [
"runtime-tokio-rustls",
"sqlite",
"macros",
"time",
"offline",
"migrate",
]
[dependencies.futures] [dependencies.futures]
version = "0.3.28" version = "0.3.28"

5
rust/build.rs Normal file
View File

@@ -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");
}

View File

@@ -1,10 +1,12 @@
CREATE TABLE "inventory_items" ( CREATE TABLE "inventory_items" (
id TEXT, id VARCHAR(36) NOT NULL,
name TEXT, name TEXT NOT NULL,
description TEXT, description TEXT,
weight INT, weight INTEGER NOT NULL,
category_id TEXT, category_id VARCHAR(36) NOT NULL,
FOREIGN KEY (category_id) REFERENCES inventory_items_categories(id)); PRIMARY KEY (id),
FOREIGN KEY (category_id) REFERENCES inventory_items_categories(id)
);
CREATE UNIQUE INDEX ux_unique ON inventory_items(name, category_id); CREATE UNIQUE INDEX ux_unique ON inventory_items(name, category_id);
CREATE TABLE "inventory_items_categories" ( CREATE TABLE "inventory_items_categories" (
@@ -21,7 +23,7 @@ CREATE TABLE "trips" (
date_start DATE NOT NULL, date_start DATE NOT NULL,
date_end DATE NOT NULL, date_end DATE NOT NULL,
location TEXT, location TEXT,
state VARCHAR(8) NOT NULL DEFAULT "Planning", state VARCHAR(8) NOT NULL,
comment TEXT, comment TEXT,
temp_min INTEGER, temp_min INTEGER,
temp_max INTEGER, temp_max INTEGER,
@@ -29,7 +31,6 @@ CREATE TABLE "trips" (
UNIQUE (name) UNIQUE (name)
); );
CREATE TABLE "trips_types" ( CREATE TABLE "trips_types" (
id VARCHAR(36) NOT NULL, id VARCHAR(36) NOT NULL,
name TEXT 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) FOREIGN KEY(trip_type_id) REFERENCES "trips_types" (id)
); );
CREATE TABLE trips_items ( CREATE TABLE trips_items (
item_id VARCHAR(36) NOT NULL, item_id VARCHAR(36) NOT NULL,
trip_id VARCHAR(36) NOT NULL, trip_id VARCHAR(36) NOT NULL,

7
rust/rebuild-schema-data.sh Executable file
View File

@@ -0,0 +1,7 @@
db="$(mktemp)"
export DATABASE_URL="sqlite://${db}"
cargo sqlx database create
cargo sqlx migrate run
cargo sqlx prepare

439
rust/sqlx-data.json Normal file
View File

@@ -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 = ?"
}
}

View File

@@ -37,7 +37,7 @@ pub struct InventoryCategoryList;
impl InventoryCategoryList { impl InventoryCategoryList {
pub fn build(state: &ClientState, categories: &Vec<Category>) -> Markup { pub fn build(state: &ClientState, categories: &Vec<Category>) -> Markup {
let biggest_category_weight: u32 = categories let biggest_category_weight: i64 = categories
.iter() .iter()
.map(Category::total_weight) .map(Category::total_weight)
.max() .max()
@@ -115,8 +115,8 @@ impl InventoryCategoryList {
format!( format!(
"width: {width}%;position:absolute;left:0;bottom:0;right:0;", "width: {width}%;position:absolute;left:0;bottom:0;right:0;",
width=( width=(
f64::from(category.total_weight()) (category.total_weight() as f64)
/ f64::from(biggest_category_weight) / (biggest_category_weight as f64)
* 100.0 * 100.0
) )
) )
@@ -130,7 +130,7 @@ impl InventoryCategoryList {
} }
td ."border" ."p-0" ."m-0" { td ."border" ."p-0" ."m-0" {
p ."p-2" ."m-2" { p ."p-2" ."m-2" {
(categories.iter().map(Category::total_weight).sum::<u32>().to_string()) (categories.iter().map(Category::total_weight).sum::<i64>().to_string())
} }
} }
} }
@@ -145,7 +145,7 @@ pub struct InventoryItemList;
impl InventoryItemList { impl InventoryItemList {
pub fn build(state: &ClientState, items: &Vec<Item>) -> Markup { pub fn build(state: &ClientState, items: &Vec<Item>) -> 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!( html!(
div #items { div #items {
@if items.is_empty() { @if items.is_empty() {
@@ -267,7 +267,7 @@ impl InventoryItemList {
position:absolute; position:absolute;
left:0; left:0;
bottom: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 td
."border-none" ."border-none"

View File

@@ -29,7 +29,9 @@ impl Root {
link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"; 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"))) } script { (include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js"))) }
} }
body hx-boost="true" { body
hx-boost="true"
{
header header
."bg-gray-200" ."bg-gray-200"
."p-5" ."p-5"

View File

@@ -276,7 +276,7 @@ pub struct TripInfoRow;
impl TripInfoRow { impl TripInfoRow {
pub fn build( pub fn build(
name: &str, name: &str,
value: impl std::fmt::Display, value: Option<impl std::fmt::Display>,
attribute_key: TripAttribute, attribute_key: TripAttribute,
edit_attribute: Option<&TripAttribute>, edit_attribute: Option<&TripAttribute>,
input_type: InputType, input_type: InputType,
@@ -303,7 +303,7 @@ impl TripInfoRow {
id="new-value" id="new-value"
name="new-value" name="new-value"
form="edit-trip" form="edit-trip"
value=(value) value=(value.map_or("".to_string(), |v| v.to_string()))
; ;
} }
} }
@@ -355,7 +355,7 @@ impl TripInfoRow {
} }
} @else { } @else {
td ."border" ."p-2" { (name) } td ."border" ."p-2" { (name) }
td ."border" ."p-2" { (value) } td ."border" ."p-2" { (value.map_or("".to_string(), |v| v.to_string())) }
td td
."border-none" ."border-none"
."bg-blue-100" ."bg-blue-100"
@@ -397,9 +397,9 @@ impl TripInfo {
."w-full" ."w-full"
{ {
tbody { tbody {
(TripInfoRow::build("Location", &trip.location, TripAttribute::Location, state.trip_edit_attribute.as_ref(), InputType::Text)) (TripInfoRow::build("Location", trip.location.as_ref(), 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("Start date", Some(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("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 (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)) (TripInfoRow::build("Temp (max)", trip.temp_max, TripAttribute::TempMax, state.trip_edit_attribute.as_ref(), InputType::Number))
tr .h-full { tr .h-full {
@@ -587,7 +587,7 @@ impl TripCategoryList {
pub fn build(state: &ClientState, trip: &models::Trip) -> Markup { pub fn build(state: &ClientState, trip: &models::Trip) -> Markup {
let categories = trip.categories(); let categories = trip.categories();
let biggest_category_weight: u32 = categories let biggest_category_weight: i64 = categories
.iter() .iter()
.map(TripCategory::total_picked_weight) .map(TripCategory::total_picked_weight)
.max() .max()
@@ -655,8 +655,8 @@ impl TripCategoryList {
format!( format!(
"width: {width}%;position:absolute;left:0;bottom:0;right:0;", "width: {width}%;position:absolute;left:0;bottom:0;right:0;",
width=( width=(
f64::from(category.total_picked_weight()) (category.total_picked_weight() as f64)
/ f64::from(biggest_category_weight) / (biggest_category_weight as f64)
* 100.0 * 100.0
) )
) )
@@ -670,7 +670,7 @@ impl TripCategoryList {
} }
td ."border" ."p-0" ."m-0" { td ."border" ."p-0" ."m-0" {
p ."p-2" ."m-2" { p ."p-2" ."m-2" {
(categories.iter().map(TripCategory::total_picked_weight).sum::<u32>().to_string()) (categories.iter().map(TripCategory::total_picked_weight).sum::<i64>().to_string())
} }
} }
} }
@@ -684,7 +684,7 @@ pub struct TripItemList;
impl TripItemList { impl TripItemList {
pub fn build(state: &ClientState, trip: &models::Trip, items: &Vec<TripItem>) -> Markup { pub fn build(state: &ClientState, trip: &models::Trip, items: &Vec<TripItem>) -> 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!( html!(
@if items.is_empty() { @if items.is_empty() {
@@ -778,7 +778,7 @@ impl TripItemList {
position:absolute; position:absolute;
left:0; left:0;
bottom: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))) {}
} }
} }
} }

View File

@@ -15,8 +15,8 @@ use serde_variant::to_variant_name;
use sqlx::{ use sqlx::{
error::DatabaseError, error::DatabaseError,
query, query, query_as,
sqlite::{SqliteConnectOptions, SqliteError, SqlitePoolOptions}, sqlite::{SqliteConnectOptions, SqliteError, SqlitePoolOptions, SqliteRow},
Pool, Row, Sqlite, Pool, Row, Sqlite,
}; };
@@ -170,29 +170,32 @@ async fn inventory(
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
state.client_state.active_category_id = active_id; state.client_state.active_category_id = active_id;
let mut categories = query("SELECT id,name,description FROM inventory_items_categories") let mut categories = query_as!(
.fetch(&state.database_pool) DbCategoryRow,
.map_ok(std::convert::TryInto::try_into) "SELECT id,name,description FROM inventory_items_categories"
.try_collect::<Vec<Result<Category, models::Error>>>() )
.await .fetch(&state.database_pool)
// we have two error handling lines here. these are distinct errors .map_ok(|row: DbCategoryRow| row.try_into())
// this one is the SQL error that may arise during the query .try_collect::<Vec<Result<Category, models::Error>>>()
.map_err(|e| { .await
( // we have two error handling lines here. these are distinct errors
StatusCode::INTERNAL_SERVER_ERROR, // this one is the SQL error that may arise during the query
ErrorPage::build(&e.to_string()), .map_err(|e| {
) (
})? StatusCode::INTERNAL_SERVER_ERROR,
.into_iter() ErrorPage::build(&e.to_string()),
.collect::<Result<Vec<Category>, models::Error>>() )
// and this one is the model mapping error that may arise e.g. during })?
// reading of the rows .into_iter()
.map_err(|e| { .collect::<Result<Vec<Category>, models::Error>>()
( // and this one is the model mapping error that may arise e.g. during
StatusCode::INTERNAL_SERVER_ERROR, // reading of the rows
ErrorPage::build(&e.to_string()), .map_err(|e| {
) (
})?; StatusCode::INTERNAL_SERVER_ERROR,
ErrorPage::build(&e.to_string()),
)
})?;
for category in &mut categories { for category in &mut categories {
category category
@@ -241,17 +244,21 @@ async fn inventory_item_create(
State(state): State<AppState>, State(state): State<AppState>,
Form(new_item): Form<NewItem>, Form(new_item): Form<NewItem>,
) -> Result<Redirect, (StatusCode, String)> { ) -> Result<Redirect, (StatusCode, String)> {
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 "INSERT INTO inventory_items
(id, name, description, weight, category_id) (id, name, description, weight, category_id)
VALUES 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) .execute(&state.database_pool)
.await .await
.map_err(|e| match e { .map_err(|e| match e {
@@ -306,11 +313,12 @@ async fn inventory_item_delete(
headers: HeaderMap, headers: HeaderMap,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<Redirect, (StatusCode, String)> { ) -> Result<Redirect, (StatusCode, String)> {
let results = query( let id_param = id.to_string();
let results = query!(
"DELETE FROM inventory_items "DELETE FROM inventory_items
WHERE id = ?", WHERE id = ?",
id_param,
) )
.bind(id.to_string())
.execute(&state.database_pool) .execute(&state.database_pool)
.await .await
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?; .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
@@ -348,7 +356,7 @@ async fn inventory_item_delete(
// .await // .await
// .unwrap(); // .unwrap();
// let items = query(&format!( // let items = query!(&format!(
// //TODO bind this stuff!!!!!!! no sql injection pls // //TODO bind this stuff!!!!!!! no sql injection pls
// "SELECT // "SELECT
// i.id, i.name, i.description, i.weight, i.category_id // i.id, i.name, i.description, i.weight, i.category_id
@@ -392,14 +400,29 @@ async fn inventory_item_edit(
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
Form(edit_item): Form<EditItem>, Form(edit_item): Form<EditItem>,
) -> Result<Redirect, (StatusCode, String)> { ) -> Result<Redirect, (StatusCode, Markup)> {
let id = Item::update(&state.database_pool, id, &edit_item.name, edit_item.weight) let id = Item::update(
.await &state.database_pool,
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? id,
.ok_or(( &edit_item.name,
StatusCode::NOT_FOUND, i64::try_from(edit_item.weight).map_err(|e| {
format!("item with id {id} not found", id = id), (
))?; 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))) Ok(Redirect::to(&format!("/inventory/category/{id}/", id = id)))
} }
@@ -437,16 +460,26 @@ async fn trip_create(
Form(new_trip): Form<NewTrip>, Form(new_trip): Form<NewTrip>,
) -> Result<Redirect, (StatusCode, String)> { ) -> Result<Redirect, (StatusCode, String)> {
let id = Uuid::new_v4(); 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 "INSERT INTO trips
(id, name, date_start, date_end) (id, name, date_start, date_end, state)
VALUES 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) .execute(&state.database_pool)
.await .await
.map_err(|e| match e { .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( async fn trips(
State(state): State<AppState>, State(state): State<AppState>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
let trips: Vec<models::Trip> = query("SELECT * FROM trips") tracing::info!("receiving trips");
.fetch(&state.database_pool)
.map_ok(std::convert::TryInto::try_into) let trips: Vec<models::Trip> = query_as!(
.try_collect::<Vec<Result<models::Trip, models::Error>>>() DbTripRow,
.await "SELECT
// we have two error handling lines here. these are distinct errors id,
// this one is the SQL error that may arise during the query name,
.map_err(|e| { CAST (date_start AS TEXT) date_start,
( CAST (date_end AS TEXT) date_end,
StatusCode::INTERNAL_SERVER_ERROR, state,
ErrorPage::build(&e.to_string()), location,
) temp_min,
})? temp_max,
.into_iter() comment
.collect::<Result<Vec<models::Trip>, models::Error>>() FROM trips",
// and this one is the model mapping error that may arise e.g. during )
// reading of the rows .fetch(&state.database_pool)
.map_err(|e| { .map_ok(|row| row.try_into())
( .try_collect::<Vec<Result<models::Trip, models::Error>>>()
StatusCode::INTERNAL_SERVER_ERROR, .await
ErrorPage::build(&e.to_string()), // 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::<Result<Vec<models::Trip>, 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(( Ok((
StatusCode::OK, StatusCode::OK,
@@ -532,20 +582,42 @@ async fn trip(
state.client_state.trip_edit_attribute = trip_query.edit; state.client_state.trip_edit_attribute = trip_query.edit;
state.client_state.active_category_id = trip_query.category; state.client_state.active_category_id = trip_query.category;
let mut trip: models::Trip = let id_param = id.to_string();
query("SELECT id,name,date_start,date_end,state,location,temp_min,temp_max,comment FROM trips WHERE id = ?") let mut trip: models::Trip = query_as!(
.bind(id.to_string()) DbTripRow,
.fetch_one(&state.database_pool) "SELECT
.map_ok(std::convert::TryInto::try_into) id,
.await name,
.map_err(|e: sqlx::Error| match e { CAST (date_start AS TEXT) AS date_start,
sqlx::Error::RowNotFound => ( CAST (date_end AS TEXT) AS date_end,
StatusCode::NOT_FOUND, state,
ErrorPage::build(&format!("trip with id {} not found", id)), location,
), temp_min,
_ => (StatusCode::INTERNAL_SERVER_ERROR, ErrorPage::build(&e.to_string())), temp_max,
})? comment
.map_err(|e: Error| (StatusCode::INTERNAL_SERVER_ERROR, ErrorPage::build(&e.to_string())))?; 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) trip.load_trips_types(&state.database_pool)
.await .await
@@ -586,14 +658,16 @@ async fn trip_type_remove(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, type_id)): Path<(Uuid, Uuid)>, Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, (StatusCode, Markup)> {
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 "DELETE FROM trips_to_trips_types AS ttt
WHERE ttt.trip_id = ? WHERE ttt.trip_id = ?
AND ttt.trip_type_id = ? AND ttt.trip_type_id = ?
", ",
trip_id,
type_id
) )
.bind(trip_id.to_string())
.bind(type_id.to_string())
.execute(&state.database_pool) .execute(&state.database_pool)
.await .await
.map_err(|e| (StatusCode::BAD_REQUEST, ErrorPage::build(&e.to_string())))?; .map_err(|e| (StatusCode::BAD_REQUEST, ErrorPage::build(&e.to_string())))?;
@@ -612,12 +686,14 @@ async fn trip_type_add(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, type_id)): Path<(Uuid, Uuid)>, Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, (StatusCode, Markup)> {
query( let trip_id = trip_id.to_string();
let type_id = type_id.to_string();
query!(
"INSERT INTO trips_to_trips_types "INSERT INTO trips_to_trips_types
(trip_id, trip_type_id) VALUES (?, ?)", (trip_id, trip_type_id) VALUES (?, ?)",
trip_id,
type_id
) )
.bind(trip_id.to_string())
.bind(type_id.to_string())
.execute(&state.database_pool) .execute(&state.database_pool)
.await .await
.map_err(|e| match e { .map_err(|e| match e {
@@ -682,13 +758,14 @@ async fn trip_comment_set(
Path(trip_id): Path<Uuid>, Path(trip_id): Path<Uuid>,
Form(comment_update): Form<CommentUpdate>, Form(comment_update): Form<CommentUpdate>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, (StatusCode, Markup)> {
let result = query( let trip_id = trip_id.to_string();
let result = query!(
"UPDATE trips "UPDATE trips
SET comment = ? SET comment = ?
WHERE id = ?", WHERE id = ?",
comment_update.new_comment,
trip_id,
) )
.bind(comment_update.new_comment)
.bind(trip_id.to_string())
.execute(&state.database_pool) .execute(&state.database_pool)
.await .await
.map_err(|e| (StatusCode::BAD_REQUEST, ErrorPage::build(&e.to_string())))?; .map_err(|e| (StatusCode::BAD_REQUEST, ErrorPage::build(&e.to_string())))?;
@@ -884,14 +961,15 @@ async fn inventory_category_create(
Form(new_category): Form<NewCategory>, Form(new_category): Form<NewCategory>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, (StatusCode, Markup)> {
let id = Uuid::new_v4(); let id = Uuid::new_v4();
query( let id_param = id.to_string();
query!(
"INSERT INTO inventory_items_categories "INSERT INTO inventory_items_categories
(id, name) (id, name)
VALUES VALUES
(?, ?)", (?, ?)",
id_param,
new_category.name
) )
.bind(id.to_string())
.bind(&new_category.name)
.execute(&state.database_pool) .execute(&state.database_pool)
.map_err(|e| match e { .map_err(|e| match e {
sqlx::Error::Database(ref error) => { sqlx::Error::Database(ref error) => {

View File

@@ -8,6 +8,7 @@ use sqlx::{
use std::convert; use std::convert;
use std::error; use std::error;
use std::fmt; use std::fmt;
use std::num::TryFromIntError;
use std::str::FromStr; use std::str::FromStr;
use uuid::Uuid; use uuid::Uuid;
@@ -16,10 +17,19 @@ use sqlx::sqlite::SqlitePoolOptions;
use futures::TryFutureExt; use futures::TryFutureExt;
use futures::TryStreamExt; 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 { pub enum Error {
SqlError { description: String }, SqlError { description: String },
UuidError { description: String }, UuidError { description: String },
EnumError { description: String },
NotFoundError { description: String }, NotFoundError { description: String },
IntError { description: String },
TimeParseError { description: String },
} }
impl fmt::Display for Error { impl fmt::Display for Error {
@@ -34,6 +44,15 @@ impl fmt::Display for Error {
Self::NotFoundError { description } => { Self::NotFoundError { description } => {
write!(f, "Not found: {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<sqlx::Error> for Error {
} }
} }
impl convert::From<TryFromIntError> for Error {
fn from(value: TryFromIntError) -> Self {
Error::IntError {
description: value.to_string(),
}
}
}
impl convert::From<TimeParseError> for Error {
fn from(value: TimeParseError) -> Self {
Error::TimeParseError {
description: value.to_string(),
}
}
}
impl error::Error for Error {} impl error::Error for Error {}
#[derive(sqlx::Type)] #[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<Self, Self::Error> {
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)] #[derive(Serialize, Debug)]
pub enum TripItemStateKey { pub enum TripItemStateKey {
Pick, Pick,
@@ -114,7 +168,7 @@ pub struct TripCategory {
} }
impl TripCategory { impl TripCategory {
pub fn total_picked_weight(&self) -> u32 { pub fn total_picked_weight(&self) -> i64 {
self.items self.items
.as_ref() .as_ref()
.unwrap() .unwrap()
@@ -132,15 +186,47 @@ pub struct TripItem {
pub packed: bool, 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<String>,
pub temp_min: Option<i64>,
pub temp_max: Option<i64>,
pub comment: Option<String>,
}
impl TryFrom<DbTripRow> for Trip {
type Error = Error;
fn try_from(row: DbTripRow) -> Result<Self, Self::Error> {
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 struct Trip {
pub id: Uuid, pub id: Uuid,
pub name: String, pub name: String,
pub date_start: time::Date, pub date_start: time::Date,
pub date_end: time::Date, pub date_end: time::Date,
pub state: TripState, pub state: TripState,
pub location: String, pub location: Option<String>,
pub temp_min: i32, pub temp_min: Option<i64>,
pub temp_max: i32, pub temp_max: Option<i64>,
pub comment: Option<String>, pub comment: Option<String>,
types: Option<Vec<TripType>>, types: Option<Vec<TripType>>,
categories: Option<Vec<TripCategory>>, categories: Option<Vec<TripCategory>>,
@@ -193,37 +279,37 @@ pub enum TripAttribute {
// } // }
// } // }
impl TryFrom<SqliteRow> for Trip { // impl TryFrom<SqliteRow> for Trip {
type Error = Error; // type Error = Error;
fn try_from(row: SqliteRow) -> Result<Self, Self::Error> { // fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
let name: &str = row.try_get("name")?; // let name: &str = row.try_get("name")?;
let id: &str = row.try_get("id")?; // let id: &str = row.try_get("id")?;
let date_start: time::Date = row.try_get("date_start")?; // let date_start: time::Date = row.try_get("date_start")?;
let date_end: time::Date = row.try_get("date_end")?; // let date_end: time::Date = row.try_get("date_end")?;
let state: TripState = row.try_get("state")?; // let state: TripState = row.try_get("state")?;
let location = row.try_get("location")?; // let location = row.try_get("location")?;
let temp_min = row.try_get("temp_min")?; // let temp_min = row.try_get("temp_min")?;
let temp_max = row.try_get("temp_max")?; // let temp_max = row.try_get("temp_max")?;
let comment = row.try_get("comment")?; // let comment = row.try_get("comment")?;
let id: Uuid = Uuid::try_parse(id)?; // let id: Uuid = Uuid::try_parse(id)?;
Ok(Trip { // Ok(Trip {
id, // id,
name: name.to_string(), // name: name.to_string(),
date_start, // date_start,
date_end, // date_end,
state, // state,
location, // location,
temp_min, // temp_min,
temp_max, // temp_max,
comment, // comment,
types: None, // types: None,
categories: None, // categories: None,
}) // })
} // }
} // }
impl<'a> Trip { impl<'a> Trip {
pub fn types(&'a self) -> &Vec<TripType> { pub fn types(&'a self) -> &Vec<TripType> {
@@ -244,12 +330,13 @@ impl<'a> Trip {
&'a mut self, &'a mut self,
pool: &sqlx::Pool<sqlx::Sqlite>, pool: &sqlx::Pool<sqlx::Sqlite>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let types = sqlx::query( let id = self.id.to_string();
let types = sqlx::query!(
" "
SELECT SELECT
type.id as id, type.id as id,
type.name as name, 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 FROM trips_types AS type
LEFT JOIN ( LEFT JOIN (
SELECT type.id as id, type.name as name SELECT type.id as id, type.name as name
@@ -262,10 +349,20 @@ impl<'a> Trip {
) AS inner ) AS inner
ON inner.id = type.id ON inner.id = type.id
", ",
id
) )
.bind(self.id.to_string())
.fetch(pool) .fetch(pool)
.map_ok(std::convert::TryInto::try_into) .map_ok(|row| -> Result<TripType, Error> {
Ok(TripType {
id: Uuid::try_parse(&row.id)?,
name: row.name,
active: match row.active {
0 => false,
1 => true,
_ => unreachable!(),
},
})
})
.try_collect::<Vec<Result<TripType, Error>>>() .try_collect::<Vec<Result<TripType, Error>>>()
.await? .await?
.into_iter() .into_iter()
@@ -282,14 +379,14 @@ impl<'a> Trip {
let mut categories: Vec<TripCategory> = vec![]; let mut categories: Vec<TripCategory> = vec![];
// we can ignore the return type as we collect into `categories` // we can ignore the return type as we collect into `categories`
// in the `map_ok()` closure // in the `map_ok()` closure
sqlx::query( let id = self.id.to_string();
sqlx::query!(
" "
SELECT SELECT
category.id as category_id, category.id as category_id,
category.name as category_name, category.name as category_name,
category.description AS category_description, category.description AS category_description,
inner.trip_id AS trip_id, inner.trip_id AS trip_id,
inner.category_description AS category_description,
inner.item_id AS item_id, inner.item_id AS item_id,
inner.item_name AS item_name, inner.item_name AS item_name,
inner.item_description AS item_description, inner.item_description AS item_description,
@@ -314,25 +411,28 @@ impl<'a> Trip {
ON item.id = trip.item_id ON item.id = trip.item_id
INNER JOIN inventory_items_categories as category INNER JOIN inventory_items_categories as category
ON category.id = item.category_id ON category.id = item.category_id
WHERE trip.trip_id = 'a8b181d6-3b16-4a41-99fa-0713b94a34d9' WHERE trip.trip_id = ?
) AS inner ) AS inner
ON inner.category_id = category.id ON inner.category_id = category.id
", ",
id
) )
.bind(self.id.to_string())
.fetch(pool) .fetch(pool)
.map_ok(|row| -> Result<(), Error> { .map_ok(|row| -> Result<(), Error> {
let mut category = TripCategory { let mut category = TripCategory {
category: Category { category: Category {
id: Uuid::try_parse(row.try_get("category_id")?)?, id: Uuid::try_parse(&row.category_id)?,
name: row.try_get("category_name")?, name: row.category_name,
description: row.try_get("category_description")?, // TODO align optionality between code and database
// idea: make description nullable
description: row.category_description,
items: None, items: None,
}, },
items: None, items: None,
}; };
match row.try_get("item_id")? { match row.item_id {
None => { None => {
// we have an empty (unused) category which has NULL values // we have an empty (unused) category which has NULL values
// for the item_id column // for the item_id column
@@ -342,14 +442,14 @@ impl<'a> Trip {
Some(item_id) => { Some(item_id) => {
let item = TripItem { let item = TripItem {
item: Item { item: Item {
id: Uuid::try_parse(item_id)?, id: Uuid::try_parse(&item_id)?,
name: row.try_get("item_name")?, name: row.item_name.unwrap(),
description: row.try_get("item_description")?, description: row.item_description,
weight: row.try_get("item_weight")?, weight: row.item_weight.unwrap(),
category_id: category.category.id, category_id: category.category.id,
}, },
picked: row.try_get("item_is_picked")?, picked: row.item_is_picked.unwrap(),
packed: row.try_get("item_is_packed")?, packed: row.item_is_packed.unwrap(),
}; };
if let Some(&mut ref mut c) = categories if let Some(&mut ref mut c) = categories
@@ -385,43 +485,70 @@ pub struct TripType {
pub active: bool, pub active: bool,
} }
impl TryFrom<SqliteRow> for TripType { // impl TryFrom<SqliteRow> for TripType {
type Error = Error; // type Error = Error;
fn try_from(row: SqliteRow) -> Result<Self, Self::Error> { // fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
let id: Uuid = Uuid::try_parse(row.try_get("id")?)?; // let id: Uuid = Uuid::try_parse(row.try_get("id")?)?;
let name: String = row.try_get::<&str, _>("name")?.to_string(); // let name: String = row.try_get::<&str, _>("name")?.to_string();
let active: bool = row.try_get("active")?; // 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<String>,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Category { pub struct Category {
pub id: Uuid, pub id: Uuid,
pub name: String, pub name: String,
pub description: String, pub description: Option<String>,
items: Option<Vec<Item>>, items: Option<Vec<Item>>,
} }
impl TryFrom<SqliteRow> for Category { impl TryFrom<DbCategoryRow> for Category {
type Error = Error; type Error = Error;
fn try_from(row: SqliteRow) -> Result<Self, Self::Error> { fn try_from(row: DbCategoryRow) -> Result<Self, Self::Error> {
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 { Ok(Category {
id, id: Uuid::try_parse(&row.id)?,
name: name.to_string(), name: row.name,
description: description.to_string(), description: row.description,
items: None, items: None,
}) })
} }
} }
// impl TryFrom<SqliteRow> for Category {
// type Error = Error;
// fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
// 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<String>,
category_id: String,
}
impl<'a> Category { impl<'a> Category {
pub fn items(&'a self) -> &'a Vec<Item> { pub fn items(&'a self) -> &'a Vec<Item> {
self.items self.items
@@ -429,7 +556,7 @@ impl<'a> Category {
.expect("you need to call populate_items()") .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() self.items().iter().map(|item| item.weight).sum()
} }
@@ -437,15 +564,21 @@ impl<'a> Category {
&'a mut self, &'a mut self,
pool: &sqlx::Pool<sqlx::Sqlite>, pool: &sqlx::Pool<sqlx::Sqlite>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let items = sqlx::query(&format!( let id = self.id.to_string();
let items = sqlx::query_as!(
DbInventoryItemsRow,
"SELECT "SELECT
id,name,weight,description,category_id id,
name,
weight,
description,
category_id
FROM inventory_items FROM inventory_items
WHERE category_id = '{id}'", WHERE category_id = ?",
id = self.id id
)) )
.fetch(pool) .fetch(pool)
.map_ok(std::convert::TryInto::try_into) .map_ok(|row| row.try_into())
.try_collect::<Vec<Result<Item, Error>>>() .try_collect::<Vec<Result<Item, Error>>>()
.await? .await?
.into_iter() .into_iter()
@@ -460,40 +593,56 @@ impl<'a> Category {
pub struct Item { pub struct Item {
pub id: Uuid, pub id: Uuid,
pub name: String, pub name: String,
pub description: String, pub description: Option<String>,
pub weight: u32, pub weight: i64,
pub category_id: Uuid, pub category_id: Uuid,
} }
impl TryFrom<SqliteRow> for Item { impl TryFrom<DbInventoryItemsRow> for Item {
type Error = Error; type Error = Error;
fn try_from(row: SqliteRow) -> Result<Self, Self::Error> { fn try_from(row: DbInventoryItemsRow) -> Result<Self, Self::Error> {
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")?)?;
Ok(Item { Ok(Item {
id, id: Uuid::try_parse(&row.id)?,
name: name.to_string(), name: row.name,
weight, description: row.description, // TODO
description: description.to_string(), weight: row.weight,
category_id, category_id: Uuid::try_parse(&row.category_id)?,
}) })
} }
} }
// impl TryFrom<SqliteRow> for Item {
// type Error = Error;
// fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
// 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 { impl Item {
pub async fn find(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<Option<Item>, Error> { pub async fn find(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<Option<Item>, Error> {
let item: Result<Result<Item, Error>, sqlx::Error> = sqlx::query( let id_param = id.to_string();
let item: Result<Result<Item, Error>, sqlx::Error> = sqlx::query_as!(
DbInventoryItemsRow,
"SELECT * FROM inventory_items AS item "SELECT * FROM inventory_items AS item
WHERE item.id = ?", WHERE item.id = ?",
id_param,
) )
.bind(id.to_string())
.fetch_one(pool) .fetch_one(pool)
.map_ok(std::convert::TryInto::try_into) .map_ok(|row| row.try_into())
.await; .await;
match item { match item {
@@ -509,9 +658,10 @@ impl Item {
pool: &sqlx::Pool<sqlx::Sqlite>, pool: &sqlx::Pool<sqlx::Sqlite>,
id: Uuid, id: Uuid,
name: &str, name: &str,
weight: u32, weight: i64,
) -> Result<Option<Uuid>, Error> { ) -> Result<Option<Uuid>, Error> {
let id: Result<Result<Uuid, Error>, sqlx::Error> = sqlx::query( let id_param = id.to_string();
let id: Result<Result<Uuid, Error>, sqlx::Error> = sqlx::query!(
"UPDATE inventory_items AS item "UPDATE inventory_items AS item
SET SET
name = ?, name = ?,
@@ -519,13 +669,13 @@ impl Item {
WHERE item.id = ? WHERE item.id = ?
RETURNING inventory_items.category_id AS id RETURNING inventory_items.category_id AS id
", ",
name,
weight,
id_param,
) )
.bind(name)
.bind(weight)
.bind(id.to_string())
.fetch_one(pool) .fetch_one(pool)
.map_ok(|row| { .map_ok(|row| {
let id: &str = row.try_get("id")?; let id: &str = &row.id.unwrap(); // TODO
let uuid: Result<Uuid, uuid::Error> = Uuid::try_parse(id); let uuid: Result<Uuid, uuid::Error> = Uuid::try_parse(id);
let uuid: Result<Uuid, Error> = uuid.map_err(|e| e.into()); let uuid: Result<Uuid, Error> = uuid.map_err(|e| e.into());
uuid uuid