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"
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",

View File

@@ -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"

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" (
id TEXT,
name TEXT,
id VARCHAR(36) NOT NULL,
name TEXT NOT NULL,
description TEXT,
weight INT,
category_id TEXT,
FOREIGN KEY (category_id) REFERENCES inventory_items_categories(id));
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,

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 {
pub fn build(state: &ClientState, categories: &Vec<Category>) -> 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::<u32>().to_string())
(categories.iter().map(Category::total_weight).sum::<i64>().to_string())
}
}
}
@@ -145,7 +145,7 @@ pub struct InventoryItemList;
impl InventoryItemList {
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!(
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"

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";
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"

View File

@@ -276,7 +276,7 @@ pub struct TripInfoRow;
impl TripInfoRow {
pub fn build(
name: &str,
value: impl std::fmt::Display,
value: Option<impl std::fmt::Display>,
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::<u32>().to_string())
(categories.iter().map(TripCategory::total_picked_weight).sum::<i64>().to_string())
}
}
}
@@ -684,7 +684,7 @@ pub struct TripItemList;
impl TripItemList {
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!(
@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))) {}
}
}
}

View File

@@ -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,9 +170,12 @@ 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")
let mut categories = query_as!(
DbCategoryRow,
"SELECT id,name,description FROM inventory_items_categories"
)
.fetch(&state.database_pool)
.map_ok(std::convert::TryInto::try_into)
.map_ok(|row: DbCategoryRow| row.try_into())
.try_collect::<Vec<Result<Category, models::Error>>>()
.await
// we have two error handling lines here. these are distinct errors
@@ -241,17 +244,21 @@ async fn inventory_item_create(
State(state): State<AppState>,
Form(new_item): Form<NewItem>,
) -> 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
(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<Uuid>,
) -> Result<Redirect, (StatusCode, String)> {
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,13 +400,28 @@ async fn inventory_item_edit(
State(state): State<AppState>,
Path(id): Path<Uuid>,
Form(edit_item): Form<EditItem>,
) -> Result<Redirect, (StatusCode, String)> {
let id = Item::update(&state.database_pool, id, &edit_item.name, edit_item.weight)
) -> Result<Redirect, (StatusCode, Markup)> {
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, e.to_string()))?
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
ErrorPage::build(&e.to_string()),
)
})?
.ok_or((
StatusCode::NOT_FOUND,
format!("item with id {id} not found", id = id),
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<NewTrip>,
) -> Result<Redirect, (StatusCode, String)> {
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,15 +515,30 @@ 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<AppState>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
let trips: Vec<models::Trip> = query("SELECT * FROM trips")
tracing::info!("receiving trips");
let trips: Vec<models::Trip> = 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(std::convert::TryInto::try_into)
.map_ok(|row| row.try_into())
.try_collect::<Vec<Result<models::Trip, models::Error>>>()
.await
// we have two error handling lines here. these are distinct errors
@@ -512,6 +560,8 @@ async fn trips(
)
})?;
tracing::info!("received trips");
Ok((
StatusCode::OK,
Root::build(TripManager::build(trips), &TopLevelPage::Trips),
@@ -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())
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(std::convert::TryInto::try_into)
.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())),
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
ErrorPage::build(&e.to_string()),
),
})?
.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_trips_types(&state.database_pool)
.await
@@ -586,14 +658,16 @@ async fn trip_type_remove(
State(state): State<AppState>,
Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
) -> 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
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<AppState>,
Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
) -> 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
(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<Uuid>,
Form(comment_update): Form<CommentUpdate>,
) -> Result<Redirect, (StatusCode, Markup)> {
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<NewCategory>,
) -> Result<Redirect, (StatusCode, Markup)> {
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) => {

View File

@@ -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<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 {}
#[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)]
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<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 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<String>,
pub temp_min: Option<i64>,
pub temp_max: Option<i64>,
pub comment: Option<String>,
types: Option<Vec<TripType>>,
categories: Option<Vec<TripCategory>>,
@@ -193,37 +279,37 @@ pub enum TripAttribute {
// }
// }
impl TryFrom<SqliteRow> for Trip {
type Error = Error;
// impl TryFrom<SqliteRow> for Trip {
// type Error = Error;
fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
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<Self, Self::Error> {
// 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<TripType> {
@@ -244,12 +330,13 @@ impl<'a> Trip {
&'a mut self,
pool: &sqlx::Pool<sqlx::Sqlite>,
) -> 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<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>>>()
.await?
.into_iter()
@@ -282,14 +379,14 @@ impl<'a> Trip {
let mut categories: Vec<TripCategory> = 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<SqliteRow> for TripType {
type Error = Error;
// impl TryFrom<SqliteRow> for TripType {
// type Error = Error;
fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
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<Self, Self::Error> {
// 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<String>,
}
#[derive(Debug)]
pub struct Category {
pub id: Uuid,
pub name: String,
pub description: String,
pub description: Option<String>,
items: Option<Vec<Item>>,
}
impl TryFrom<SqliteRow> for Category {
impl TryFrom<DbCategoryRow> 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")?)?;
fn try_from(row: DbCategoryRow) -> Result<Self, Self::Error> {
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<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 {
pub fn items(&'a self) -> &'a Vec<Item> {
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<sqlx::Sqlite>,
) -> 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::<Vec<Result<Item, Error>>>()
.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<String>,
pub weight: i64,
pub category_id: Uuid,
}
impl TryFrom<SqliteRow> for Item {
impl TryFrom<DbInventoryItemsRow> 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: 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<Self, Self::Error> {
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<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 {
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
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<sqlx::Sqlite>,
id: Uuid,
name: &str,
weight: u32,
weight: i64,
) -> 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
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, uuid::Error> = Uuid::try_parse(id);
let uuid: Result<Uuid, Error> = uuid.map_err(|e| e.into());
uuid