migration, adding categories, small fixes

This commit is contained in:
2023-08-29 21:33:59 +02:00
parent f2414baf9a
commit a3939e972d
6 changed files with 333 additions and 115 deletions

2
rust/.gitignore vendored
View File

@@ -1,2 +1,4 @@
/target/ /target/
*.sqlite *.sqlite
*.sqlite-wal
*.sqlite-shm

View File

@@ -0,0 +1,57 @@
CREATE TABLE "inventory_items" (
id TEXT,
name TEXT,
description TEXT,
weight INT,
category_id TEXT,
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" (
id VARCHAR(36) NOT NULL,
name TEXT NOT NULL,
description TEXT,
PRIMARY KEY (id),
UNIQUE (name)
);
CREATE TABLE "trips" (
id VARCHAR(36) NOT NULL,
name TEXT NOT NULL,
date_start DATE NOT NULL,
date_end DATE NOT NULL,
location TEXT,
state VARCHAR(8) NOT NULL DEFAULT "Planning",
comment TEXT,
temp_min INTEGER,
temp_max INTEGER,
PRIMARY KEY (id),
UNIQUE (name)
);
CREATE TABLE "trips_types" (
id VARCHAR(36) NOT NULL,
name TEXT NOT NULL,
PRIMARY KEY (id),
UNIQUE (name)
);
CREATE TABLE "trips_to_trips_types" (
trip_id VARCHAR(36) NOT NULL,
trip_type_id VARCHAR(36) NOT NULL,
PRIMARY KEY (trip_id, trip_type_id),
FOREIGN KEY(trip_id) REFERENCES "trips" (id),
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,
pick BOOLEAN NOT NULL,
pack BOOLEAN NOT NULL,
PRIMARY KEY (item_id, trip_id),
FOREIGN KEY(item_id) REFERENCES "inventory_items" (id),
FOREIGN KEY(trip_id) REFERENCES "trips" (id)
);

View File

@@ -13,6 +13,7 @@ impl Inventory {
div ."p-8" ."grid" ."grid-cols-4" ."gap-3" { div ."p-8" ."grid" ."grid-cols-4" ."gap-3" {
div ."col-span-2" { div ."col-span-2" {
(InventoryCategoryList::build(&state, &categories)) (InventoryCategoryList::build(&state, &categories))
(InventoryNewCategoryForm::build())
} }
div ."col-span-2" { div ."col-span-2" {
h1 ."text-2xl" ."mb-5" ."text-center" { "Items" } h1 ."text-2xl" ."mb-5" ."text-center" { "Items" }
@@ -409,3 +410,53 @@ impl InventoryNewItemForm {
) )
} }
} }
pub struct InventoryNewCategoryForm;
impl InventoryNewCategoryForm {
pub fn build() -> Markup {
html!(
form
name="new-category"
id="new-category"
action="/inventory/category/"
target="_self"
method="post"
."mt-8" ."p-5" ."border-2" ."border-gray-200" {
div ."mb-5" ."flex" ."flex-row" ."items-center" {
span ."mdi" ."mdi-playlist-plus" ."text-2xl" ."mr-4" {}
p ."inline" ."text-xl" { "Add new category" }
}
div ."w-11/12" ."mx-auto" {
div ."pb-8" {
div ."flex" ."flex-row" ."justify-center" ."items-start"{
label for="name" .font-bold ."w-1/2" ."p-2" ."text-center" { "Name" }
span ."w-1/2" {
input type="text" id="new-category-name" name="new-category-name"
."block"
."w-full"
."p-2"
."bg-gray-50"
."border-2"
."rounded"
."focus:outline-none"
."focus:bg-white"
."focus:border-purple-500"
{
}
}
}
}
input type="submit" value="Add"
."py-2"
."border-2"
."rounded"
."border-gray-300"
."mx-auto"
."w-full" {
}
}
}
)
}
}

View File

@@ -411,61 +411,73 @@ impl TripInfo {
."flex-wrap" ."flex-wrap"
."gap-2" ."gap-2"
."justify-between" ."justify-between"
// as we have a gap between the elements, we have
// to completely skip an element when there are no
// active or inactive items, otherwise we get the gap
// between the empty (invisible) item, throwing off
// the margins
{ {
@let types = trip.types(); @let types = trip.types();
div @let active_triptypes = types.iter().filter(|t| t.active).collect::<Vec<&TripType>>();
."flex" @let inactive_triptypes = types.iter().filter(|t| !t.active).collect::<Vec<&TripType>>();
."flex-row"
."flex-wrap" @if !active_triptypes.is_empty() {
."gap-2" div
."justify-start" ."flex"
{ ."flex-row"
@for triptype in types.iter().filter(|t| t.active) { ."flex-wrap"
a href=(format!("type/{}/remove", triptype.id)) { ."gap-2"
li ."justify-start"
."border" {
."rounded-2xl" @for triptype in active_triptypes {
."py-0.5" a href=(format!("type/{}/remove", triptype.id)) {
."px-2" li
."bg-green-100" ."border"
."cursor-pointer" ."rounded-2xl"
."flex" ."py-0.5"
."flex-column" ."px-2"
."items-center" ."bg-green-100"
."hover:bg-red-200" ."cursor-pointer"
."gap-1" ."flex"
{ ."flex-column"
span { (triptype.name) } ."items-center"
span ."mdi" ."mdi-delete" ."text-sm" {} ."hover:bg-red-200"
."gap-1"
{
span { (triptype.name) }
span ."mdi" ."mdi-delete" ."text-sm" {}
}
} }
} }
} }
} }
div @if !inactive_triptypes.is_empty() {
."flex" div
."flex-row" ."flex"
."flex-wrap" ."flex-row"
."gap-2" ."flex-wrap"
."justify-start" ."gap-2"
{ ."justify-start"
@for triptype in types.iter().filter(|t| !t.active) { {
a href=(format!("type/{}/add", triptype.id)) { @for triptype in inactive_triptypes {
li a href=(format!("type/{}/add", triptype.id)) {
."border" li
."rounded-2xl" ."border"
."py-0.5" ."rounded-2xl"
."px-2" ."py-0.5"
."bg-gray-100" ."px-2"
."cursor-pointer" ."bg-gray-100"
."flex" ."cursor-pointer"
."flex-column" ."flex"
."items-center" ."flex-column"
."hover:bg-green-200" ."items-center"
."gap-1" ."hover:bg-green-200"
."opacity-60" ."gap-1"
{ ."opacity-60"
span { (triptype.name) } {
span ."mdi" ."mdi-plus" ."text-sm" {} span { (triptype.name) }
span ."mdi" ."mdi-plus" ."text-sm" {}
}
} }
} }
} }

View File

@@ -9,6 +9,8 @@ use axum::{
Form, Router, Form, Router,
}; };
use std::str::FromStr;
use serde_variant::to_variant_name; use serde_variant::to_variant_name;
use sqlx::{ use sqlx::{
@@ -72,13 +74,16 @@ async fn main() -> Result<(), sqlx::Error> {
let database_pool = SqlitePoolOptions::new() let database_pool = SqlitePoolOptions::new()
.max_connections(5) .max_connections(5)
.connect_with( .connect_with(
SqliteConnectOptions::new() SqliteConnectOptions::from_str(
.filename(std::env::var("SQLITE_DATABASE").expect("env SQLITE_DATABASE not found")) &std::env::var("DATABASE_URL").expect("env DATABASE_URL not found"),
.pragma("foreign_keys", "1"), )?
.pragma("foreign_keys", "1"),
) )
.await .await
.unwrap(); .unwrap();
sqlx::migrate!().run(&database_pool).await?;
let state = AppState { let state = AppState {
database_pool, database_pool,
client_state: ClientState::new(), client_state: ClientState::new(),
@@ -102,6 +107,7 @@ async fn main() -> Result<(), sqlx::Error> {
.route("/trip/:id/items/:id/pack", get(trip_item_set_pack)) .route("/trip/:id/items/:id/pack", get(trip_item_set_pack))
.route("/trip/:id/items/:id/unpack", get(trip_item_set_unpack)) .route("/trip/:id/items/:id/unpack", get(trip_item_set_unpack))
.route("/inventory/", get(inventory_inactive)) .route("/inventory/", get(inventory_inactive))
.route("/inventory/category/", post(inventory_category_create))
.route("/inventory/item/", post(inventory_item_create)) .route("/inventory/item/", post(inventory_item_create))
.route("/inventory/category/:id/", get(inventory_active)) .route("/inventory/category/:id/", get(inventory_active))
.route("/inventory/item/:id/delete", get(inventory_item_delete)) .route("/inventory/item/:id/delete", get(inventory_item_delete))
@@ -142,8 +148,8 @@ impl Default for InventoryQuery {
} }
async fn inventory_active( async fn inventory_active(
Path(id): Path<Uuid>,
State(mut state): State<AppState>, State(mut state): State<AppState>,
Path(id): Path<Uuid>,
Query(inventory_query): Query<InventoryQuery>, Query(inventory_query): Query<InventoryQuery>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
state.client_state.edit_item = inventory_query.edit_item; state.client_state.edit_item = inventory_query.edit_item;
@@ -164,7 +170,7 @@ 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 inventoryitemcategories") let mut categories = query("SELECT id,name,description FROM inventory_items_categories")
.fetch(&state.database_pool) .fetch(&state.database_pool)
.map_ok(std::convert::TryInto::try_into) .map_ok(std::convert::TryInto::try_into)
.try_collect::<Vec<Result<Category, models::Error>>>() .try_collect::<Vec<Result<Category, models::Error>>>()
@@ -236,7 +242,7 @@ async fn inventory_item_create(
Form(new_item): Form<NewItem>, Form(new_item): Form<NewItem>,
) -> Result<Redirect, (StatusCode, String)> { ) -> Result<Redirect, (StatusCode, String)> {
query( query(
"INSERT INTO inventoryitems "INSERT INTO inventory_items
(id, name, description, weight, category_id) (id, name, description, weight, category_id)
VALUES VALUES
(?, ?, ?, ?, ?)", (?, ?, ?, ?, ?)",
@@ -301,7 +307,7 @@ async fn inventory_item_delete(
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<Redirect, (StatusCode, String)> { ) -> Result<Redirect, (StatusCode, String)> {
let results = query( let results = query(
"DELETE FROM inventoryitems "DELETE FROM inventory_items
WHERE id = ?", WHERE id = ?",
) )
.bind(id.to_string()) .bind(id.to_string())
@@ -346,7 +352,7 @@ async fn inventory_item_delete(
// //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
// FROM inventoryitemcategories AS c // FROM inventory_items_categories AS c
// INNER JOIN inventoryitems AS i // INNER JOIN inventoryitems AS i
// ON i.category_id = c.id WHERE c.id = '{id}';", // ON i.category_id = c.id WHERE c.id = '{id}';",
// id = id, // id = id,
@@ -476,7 +482,7 @@ async fn trip_create(
), ),
})?; })?;
Ok(Redirect::to(&format!("/trips/{id}/", id = id.to_string()))) Ok(Redirect::to(&format!("/trip/{id}/", id = id.to_string())))
} }
async fn trips( async fn trips(
@@ -519,8 +525,8 @@ struct TripQuery {
} }
async fn trip( async fn trip(
Path(id): Path<Uuid>,
State(mut state): State<AppState>, State(mut state): State<AppState>,
Path(id): Path<Uuid>,
Query(trip_query): Query<TripQuery>, Query(trip_query): Query<TripQuery>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
state.client_state.trip_edit_attribute = trip_query.edit; state.client_state.trip_edit_attribute = trip_query.edit;
@@ -541,7 +547,7 @@ async fn trip(
})? })?
.map_err(|e: Error| (StatusCode::INTERNAL_SERVER_ERROR, ErrorPage::build(&e.to_string())))?; .map_err(|e: Error| (StatusCode::INTERNAL_SERVER_ERROR, ErrorPage::build(&e.to_string())))?;
trip.load_triptypes(&state.database_pool) trip.load_trips_types(&state.database_pool)
.await .await
.map_err(|e| { .map_err(|e| {
( (
@@ -577,11 +583,11 @@ async fn trip(
} }
async fn trip_type_remove( async fn trip_type_remove(
Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, (StatusCode, Markup)> {
let results = query( let results = query(
"DELETE FROM trips_to_triptypes 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 = ?
", ",
@@ -603,11 +609,11 @@ async fn trip_type_remove(
} }
async fn trip_type_add( async fn trip_type_add(
Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, (StatusCode, Markup)> {
query( query(
"INSERT INTO trips_to_triptypes "INSERT INTO trips_to_trips_types
(trip_id, trip_type_id) VALUES (?, ?)", (trip_id, trip_type_id) VALUES (?, ?)",
) )
.bind(trip_id.to_string()) .bind(trip_id.to_string())
@@ -672,8 +678,8 @@ struct CommentUpdate {
} }
async fn trip_comment_set( async fn trip_comment_set(
Path(trip_id): Path<Uuid>,
State(state): State<AppState>, State(state): State<AppState>,
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 result = query(
@@ -704,8 +710,8 @@ struct TripUpdate {
} }
async fn trip_edit_attribute( async fn trip_edit_attribute(
Path((trip_id, attribute)): Path<(Uuid, TripAttribute)>,
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, attribute)): Path<(Uuid, TripAttribute)>,
Form(trip_update): Form<TripUpdate>, Form(trip_update): Form<TripUpdate>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, (StatusCode, Markup)> {
let result = query(&format!( let result = query(&format!(
@@ -738,7 +744,7 @@ async fn trip_item_set_state(
value: bool, value: bool,
) -> Result<(), (StatusCode, Markup)> { ) -> Result<(), (StatusCode, Markup)> {
let result = query(&format!( let result = query(&format!(
"UPDATE tripitems "UPDATE trips_items
SET {key} = ? SET {key} = ?
WHERE trip_id = ? WHERE trip_id = ?
AND item_id = ?", AND item_id = ?",
@@ -764,9 +770,9 @@ async fn trip_item_set_state(
} }
async fn trip_item_set_pick( async fn trip_item_set_pick(
State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>, Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
headers: HeaderMap, headers: HeaderMap,
State(state): State<AppState>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, (StatusCode, Markup)> {
Ok(trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pick, true).await?).map( Ok(trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pick, true).await?).map(
|_| -> Result<Redirect, (StatusCode, Markup)> { |_| -> Result<Redirect, (StatusCode, Markup)> {
@@ -790,9 +796,9 @@ async fn trip_item_set_pick(
} }
async fn trip_item_set_unpick( async fn trip_item_set_unpick(
State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>, Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
headers: HeaderMap, headers: HeaderMap,
State(state): State<AppState>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, (StatusCode, Markup)> {
Ok(trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pick, false).await?).map( Ok(trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pick, false).await?).map(
|_| -> Result<Redirect, (StatusCode, Markup)> { |_| -> Result<Redirect, (StatusCode, Markup)> {
@@ -816,9 +822,9 @@ async fn trip_item_set_unpick(
} }
async fn trip_item_set_pack( async fn trip_item_set_pack(
State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>, Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
headers: HeaderMap, headers: HeaderMap,
State(state): State<AppState>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, (StatusCode, Markup)> {
Ok(trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pack, true).await?).map( Ok(trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pack, true).await?).map(
|_| -> Result<Redirect, (StatusCode, Markup)> { |_| -> Result<Redirect, (StatusCode, Markup)> {
@@ -842,9 +848,9 @@ async fn trip_item_set_pack(
} }
async fn trip_item_set_unpack( async fn trip_item_set_unpack(
State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>, Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
headers: HeaderMap, headers: HeaderMap,
State(state): State<AppState>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, (StatusCode, Markup)> {
Ok(trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pack, false).await?).map( Ok(trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pack, false).await?).map(
|_| -> Result<Redirect, (StatusCode, Markup)> { |_| -> Result<Redirect, (StatusCode, Markup)> {
@@ -866,3 +872,66 @@ async fn trip_item_set_unpack(
}, },
)? )?
} }
#[derive(Deserialize)]
struct NewCategory {
#[serde(rename = "new-category-name")]
name: String,
}
async fn inventory_category_create(
State(state): State<AppState>,
Form(new_category): Form<NewCategory>,
) -> Result<Redirect, (StatusCode, Markup)> {
let id = Uuid::new_v4();
query(
"INSERT INTO inventory_items_categories
(id, name)
VALUES
(?, ?)",
)
.bind(id.to_string())
.bind(&new_category.name)
.execute(&state.database_pool)
.map_err(|e| match e {
sqlx::Error::Database(ref error) => {
let sqlite_error = error.downcast_ref::<SqliteError>();
if let Some(code) = sqlite_error.code() {
match &*code {
"2067" => {
// SQLITE_CONSTRAINT_UNIQUE
(
StatusCode::BAD_REQUEST,
ErrorPage::build(&format!(
"category with name \"{name}\" already exists",
name = new_category.name
)),
)
}
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
ErrorPage::build(&format!(
"got error with unknown code: {}",
sqlite_error.to_string()
)),
),
}
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
ErrorPage::build(&format!(
"got error without code: {}",
sqlite_error.to_string()
)),
)
}
}
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
ErrorPage::build(&format!("got unknown error: {}", e.to_string())),
),
})
.await?;
Ok(Redirect::to("/inventory/"))
}

View File

@@ -229,18 +229,18 @@ impl<'a> Trip {
pub fn types(&'a self) -> &Vec<TripType> { pub fn types(&'a self) -> &Vec<TripType> {
self.types self.types
.as_ref() .as_ref()
.expect("you need to call load_triptypes()") .expect("you need to call load_trips_types()")
} }
pub fn categories(&'a self) -> &Vec<TripCategory> { pub fn categories(&'a self) -> &Vec<TripCategory> {
self.categories self.categories
.as_ref() .as_ref()
.expect("you need to call load_triptypes()") .expect("you need to call load_trips_types()")
} }
} }
impl<'a> Trip { impl<'a> Trip {
pub async fn load_triptypes( pub async fn load_trips_types(
&'a mut self, &'a mut self,
pool: &sqlx::Pool<sqlx::Sqlite>, pool: &sqlx::Pool<sqlx::Sqlite>,
) -> Result<(), Error> { ) -> Result<(), Error> {
@@ -250,13 +250,13 @@ impl<'a> Trip {
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 CASE WHEN inner.id IS NOT NULL THEN true ELSE false END AS active
FROM triptypes 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
FROM trips as trip FROM trips as trip
INNER JOIN trips_to_triptypes as ttt INNER JOIN trips_to_trips_types as ttt
ON ttt.trip_id = trip.id ON ttt.trip_id = trip.id
INNER JOIN triptypes AS type INNER JOIN trips_types AS type
ON type.id == ttt.trip_type_id ON type.id == ttt.trip_type_id
WHERE trip.id = ? WHERE trip.id = ?
) AS inner ) AS inner
@@ -287,19 +287,36 @@ impl<'a> Trip {
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,
item.id as item_id, inner.trip_id AS trip_id,
item.name as item_name, inner.category_description AS category_description,
item.description as item_description, inner.item_id AS item_id,
item.weight as item_weight, inner.item_name AS item_name,
trip.pick as item_is_picked, inner.item_description AS item_description,
trip.pack as item_is_packed inner.item_weight AS item_weight,
FROM tripitems as trip inner.item_is_picked AS item_is_picked,
INNER JOIN inventoryitems as item inner.item_is_packed AS item_is_packed
ON item.id = trip.item_id FROM inventory_items_categories AS category
INNER JOIN inventoryitemcategories as category LEFT JOIN (
ON category.id = item.category_id SELECT
WHERE trip.trip_id = ?; trip.trip_id AS trip_id,
category.id as category_id,
category.name as category_name,
category.description as category_description,
item.id as item_id,
item.name as item_name,
item.description as item_description,
item.weight as item_weight,
trip.pick as item_is_picked,
trip.pack as item_is_packed
FROM trips_items as trip
INNER JOIN inventory_items as item
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'
) AS inner
ON inner.category_id = category.id
", ",
) )
.bind(self.id.to_string()) .bind(self.id.to_string())
@@ -315,28 +332,38 @@ impl<'a> Trip {
items: None, items: None,
}; };
let item = TripItem { match row.try_get("item_id")? {
item: Item { None => {
id: Uuid::try_parse(row.try_get("item_id")?)?, // we have an empty (unused) category which has NULL values
name: row.try_get("item_name")?, // for the item_id column
description: row.try_get("item_description")?, category.items = Some(vec![]);
weight: row.try_get("item_weight")?, categories.push(category);
category_id: category.category.id, }
}, Some(item_id) => {
picked: row.try_get("item_is_picked")?, let item = TripItem {
packed: row.try_get("item_is_packed")?, 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")?,
category_id: category.category.id,
},
picked: row.try_get("item_is_picked")?,
packed: row.try_get("item_is_packed")?,
};
if let Some(&mut ref mut c) = categories if let Some(&mut ref mut c) = categories
.iter_mut() .iter_mut()
.find(|c| c.category.id == category.category.id) .find(|c| c.category.id == category.category.id)
{ {
// we always populate c.items when we add a new category, so // we always populate c.items when we add a new category, so
// it's safe to unwrap here // it's safe to unwrap here
c.items.as_mut().unwrap().push(item); c.items.as_mut().unwrap().push(item);
} else { } else {
category.items = Some(vec![item]); category.items = Some(vec![item]);
categories.push(category); categories.push(category);
}
}
} }
Ok(()) Ok(())
@@ -413,7 +440,7 @@ impl<'a> Category {
let items = sqlx::query(&format!( let items = sqlx::query(&format!(
"SELECT "SELECT
id,name,weight,description,category_id id,name,weight,description,category_id
FROM inventoryitems FROM inventory_items
WHERE category_id = '{id}'", WHERE category_id = '{id}'",
id = self.id id = self.id
)) ))
@@ -461,7 +488,7 @@ impl TryFrom<SqliteRow> for Item {
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 item: Result<Result<Item, Error>, sqlx::Error> = sqlx::query(
"SELECT * FROM inventoryitems AS item "SELECT * FROM inventory_items AS item
WHERE item.id = ?", WHERE item.id = ?",
) )
.bind(id.to_string()) .bind(id.to_string())
@@ -485,12 +512,12 @@ impl Item {
weight: u32, weight: u32,
) -> Result<Option<Uuid>, Error> { ) -> Result<Option<Uuid>, Error> {
let id: Result<Result<Uuid, Error>, sqlx::Error> = sqlx::query( let id: Result<Result<Uuid, Error>, sqlx::Error> = sqlx::query(
"UPDATE inventoryitems AS item "UPDATE inventory_items AS item
SET SET
name = ?, name = ?,
weight = ? weight = ?
WHERE item.id = ? WHERE item.id = ?
RETURNING inventoryitems.category_id AS id RETURNING inventory_items.category_id AS id
", ",
) )
.bind(name) .bind(name)