more error refactoring
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
use maud::{html, Markup, PreEscaped};
|
use maud::{html, Markup};
|
||||||
|
|
||||||
use crate::models;
|
use crate::models;
|
||||||
use crate::ClientState;
|
use crate::ClientState;
|
||||||
use uuid::{uuid, Uuid};
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct Inventory;
|
pub struct Inventory;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use serde_variant::to_variant_name;
|
use serde_variant::to_variant_name;
|
||||||
|
|
||||||
use crate::ClientState;
|
|
||||||
pub struct TripManager;
|
pub struct TripManager;
|
||||||
|
|
||||||
pub mod packagelist;
|
pub mod packagelist;
|
||||||
|
|||||||
114
rust/src/error.rs
Normal file
114
rust/src/error.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::components;
|
||||||
|
use crate::models;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum RequestError {
|
||||||
|
EmptyFormElement { name: String },
|
||||||
|
RefererNotFound,
|
||||||
|
RefererInvalid { message: String },
|
||||||
|
NotFound { message: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for RequestError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::EmptyFormElement { name } => write!(f, "Form element {name} cannot be empty"),
|
||||||
|
Self::RefererNotFound => write!(f, "Referer header not found"),
|
||||||
|
Self::RefererInvalid { message } => write!(f, "Referer header invalid: {message}"),
|
||||||
|
Self::NotFound { message } => write!(f, "Not found: {message}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Error {
|
||||||
|
Model(models::Error),
|
||||||
|
Request(RequestError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum StartError {
|
||||||
|
DatabaseInitError { message: String },
|
||||||
|
DatabaseMigrationError { message: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sqlx::Error> for StartError {
|
||||||
|
fn from(value: sqlx::Error) -> Self {
|
||||||
|
Self::DatabaseInitError {
|
||||||
|
message: value.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sqlx::migrate::MigrateError> for StartError {
|
||||||
|
fn from(value: sqlx::migrate::MigrateError) -> Self {
|
||||||
|
Self::DatabaseMigrationError {
|
||||||
|
message: value.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Model(model_error) => write!(f, "Model error: {model_error}"),
|
||||||
|
Self::Request(request_error) => write!(f, "Request error: {request_error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<models::Error> for Error {
|
||||||
|
fn from(value: models::Error) -> Self {
|
||||||
|
Self::Model(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for Error {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
match self {
|
||||||
|
Self::Model(ref model_error) => match model_error {
|
||||||
|
models::Error::Database(_) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
components::ErrorPage::build(&format!("{}", self)),
|
||||||
|
),
|
||||||
|
models::Error::Query(error) => match error {
|
||||||
|
models::QueryError::NotFound { description } => (
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
components::ErrorPage::build(&description),
|
||||||
|
),
|
||||||
|
_ => (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
components::ErrorPage::build(&format!("{}", error)),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Self::Request(request_error) => match request_error {
|
||||||
|
RequestError::RefererNotFound => (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
components::ErrorPage::build("no referer header found"),
|
||||||
|
),
|
||||||
|
RequestError::RefererInvalid { message } => (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
components::ErrorPage::build(&format!(
|
||||||
|
"referer could not be converted: {}",
|
||||||
|
message
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
RequestError::EmptyFormElement { name } => (
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
components::ErrorPage::build(&format!("empty form element: {}", name)),
|
||||||
|
),
|
||||||
|
RequestError::NotFound { message } => (
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
components::ErrorPage::build(&format!("not found: {}", message)),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
503
rust/src/main.rs
503
rust/src/main.rs
@@ -1,45 +1,35 @@
|
|||||||
#![allow(unused_imports)]
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
headers,
|
|
||||||
headers::Header,
|
|
||||||
http::{
|
http::{
|
||||||
header,
|
header,
|
||||||
header::{HeaderMap, HeaderName, HeaderValue},
|
header::{HeaderMap, HeaderName, HeaderValue},
|
||||||
StatusCode,
|
StatusCode,
|
||||||
},
|
},
|
||||||
response::{Html, IntoResponse, Redirect, Response},
|
response::{IntoResponse, Redirect},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Form, Router,
|
Form, Router,
|
||||||
};
|
};
|
||||||
|
|
||||||
use maud::html;
|
use maud::html;
|
||||||
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use serde_variant::to_variant_name;
|
|
||||||
|
|
||||||
use sqlx::{
|
|
||||||
sqlite::{SqliteConnectOptions, SqlitePoolOptions},
|
|
||||||
Pool, Sqlite,
|
|
||||||
};
|
|
||||||
|
|
||||||
use maud::Markup;
|
use maud::Markup;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use futures::TryFutureExt;
|
use uuid::Uuid;
|
||||||
use futures::TryStreamExt;
|
|
||||||
use uuid::{uuid, Uuid};
|
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
mod components;
|
mod components;
|
||||||
|
mod error;
|
||||||
mod models;
|
mod models;
|
||||||
|
mod sqlite;
|
||||||
|
|
||||||
|
use error::{Error, RequestError, StartError};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
database_pool: Pool<Sqlite>,
|
database_pool: sqlite::Pool<sqlite::Sqlite>,
|
||||||
client_state: ClientState,
|
client_state: ClientState,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,22 +114,15 @@ impl From<HtmxRequestHeaders> for HeaderName {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), sqlx::Error> {
|
async fn main() -> Result<(), StartError> {
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_max_level(tracing::Level::DEBUG)
|
.with_max_level(tracing::Level::DEBUG)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
let database_pool = SqlitePoolOptions::new()
|
let database_pool = sqlite::init_database_pool(&args.database_url).await?;
|
||||||
.max_connections(5)
|
sqlite::migrate(&database_pool).await?;
|
||||||
.connect_with(
|
|
||||||
SqliteConnectOptions::from_str(&args.database_url)?.pragma("foreign_keys", "1"),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
sqlx::migrate!().run(&database_pool).await?;
|
|
||||||
|
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
database_pool,
|
database_pool,
|
||||||
@@ -158,6 +141,14 @@ async fn main() -> Result<(), sqlx::Error> {
|
|||||||
.route("/favicon.svg", get(icon_handler))
|
.route("/favicon.svg", get(icon_handler))
|
||||||
.route("/assets/luggage.svg", get(icon_handler))
|
.route("/assets/luggage.svg", get(icon_handler))
|
||||||
.route("/", get(root))
|
.route("/", get(root))
|
||||||
|
.route(
|
||||||
|
"/notfound",
|
||||||
|
get(|| async {
|
||||||
|
Error::Request(RequestError::NotFound {
|
||||||
|
message: "hi".to_string(),
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
.nest(
|
.nest(
|
||||||
"/trips/",
|
"/trips/",
|
||||||
Router::new()
|
Router::new()
|
||||||
@@ -212,7 +203,7 @@ async fn main() -> Result<(), sqlx::Error> {
|
|||||||
.route("/item/:id/edit", post(inventory_item_edit))
|
.route("/item/:id/edit", post(inventory_item_edit))
|
||||||
.route("/item/name/validate", post(inventory_item_validate_name)),
|
.route("/item/name/validate", post(inventory_item_validate_name)),
|
||||||
)
|
)
|
||||||
.fallback(|| async { (StatusCode::NOT_FOUND, "not found") })
|
.fallback(|| async { (StatusCode::NOT_FOUND, "no route found") })
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], args.port));
|
let addr = SocketAddr::from(([127, 0, 0, 1], args.port));
|
||||||
@@ -244,7 +235,7 @@ async fn inventory_active(
|
|||||||
State(mut state): State<AppState>,
|
State(mut state): State<AppState>,
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
Query(inventory_query): Query<InventoryQuery>,
|
Query(inventory_query): Query<InventoryQuery>,
|
||||||
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, Markup), Error> {
|
||||||
state.client_state.edit_item = inventory_query.edit_item;
|
state.client_state.edit_item = inventory_query.edit_item;
|
||||||
state.client_state.active_category_id = Some(id);
|
state.client_state.active_category_id = Some(id);
|
||||||
|
|
||||||
@@ -258,12 +249,9 @@ async fn inventory_active(
|
|||||||
.categories
|
.categories
|
||||||
.iter()
|
.iter()
|
||||||
.find(|category| category.id == id)
|
.find(|category| category.id == id)
|
||||||
.ok_or((
|
.ok_or(Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("a category with id {id} does not exist"),
|
||||||
components::ErrorPage::build(&format!(
|
}))
|
||||||
"a category with id {id} does not exist"
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
@@ -283,7 +271,7 @@ async fn inventory_active(
|
|||||||
async fn inventory_inactive(
|
async fn inventory_inactive(
|
||||||
State(mut state): State<AppState>,
|
State(mut state): State<AppState>,
|
||||||
Query(inventory_query): Query<InventoryQuery>,
|
Query(inventory_query): Query<InventoryQuery>,
|
||||||
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, Markup), Error> {
|
||||||
state.client_state.edit_item = inventory_query.edit_item;
|
state.client_state.edit_item = inventory_query.edit_item;
|
||||||
state.client_state.active_category_id = None;
|
state.client_state.active_category_id = None;
|
||||||
|
|
||||||
@@ -323,7 +311,7 @@ struct NewItemName {
|
|||||||
async fn inventory_item_validate_name(
|
async fn inventory_item_validate_name(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Form(new_item): Form<NewItemName>,
|
Form(new_item): Form<NewItemName>,
|
||||||
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, Markup), Error> {
|
||||||
let exists = models::InventoryItem::name_exists(&state.database_pool, &new_item.name).await?;
|
let exists = models::InventoryItem::name_exists(&state.database_pool, &new_item.name).await?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
@@ -332,40 +320,18 @@ async fn inventory_item_validate_name(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<models::Error> for (StatusCode, Markup) {
|
|
||||||
fn from(value: models::Error) -> (StatusCode, Markup) {
|
|
||||||
match value {
|
|
||||||
models::Error::Database(_) => (
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
components::ErrorPage::build(&value.to_string()),
|
|
||||||
),
|
|
||||||
models::Error::Query(error) => match error {
|
|
||||||
models::QueryError::NotFound { description } => (
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
components::ErrorPage::build(&description),
|
|
||||||
),
|
|
||||||
_ => (
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
components::ErrorPage::build(&error.to_string()),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn inventory_item_create(
|
async fn inventory_item_create(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Form(new_item): Form<NewItem>,
|
Form(new_item): Form<NewItem>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Markup)> {
|
) -> Result<impl IntoResponse, Error> {
|
||||||
if new_item.name.is_empty() {
|
if new_item.name.is_empty() {
|
||||||
return Err((
|
return Err(Error::Request(RequestError::EmptyFormElement {
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
name: "name".to_string(),
|
||||||
components::ErrorPage::build("name cannot be empty"),
|
}));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_id = models::InventoryItem::save(
|
let _new_id = models::InventoryItem::save(
|
||||||
&state.database_pool,
|
&state.database_pool,
|
||||||
&new_item.name,
|
&new_item.name,
|
||||||
new_item.category_id,
|
new_item.category_id,
|
||||||
@@ -377,18 +343,13 @@ async fn inventory_item_create(
|
|||||||
let inventory = models::Inventory::load(&state.database_pool).await?;
|
let inventory = models::Inventory::load(&state.database_pool).await?;
|
||||||
|
|
||||||
// it's impossible to NOT find the item here, as we literally just added
|
// it's impossible to NOT find the item here, as we literally just added
|
||||||
// it. but good error handling never hurts
|
// it.
|
||||||
let active_category: Option<&models::Category> = Some(
|
let active_category: Option<&models::Category> = Some(
|
||||||
inventory
|
inventory
|
||||||
.categories
|
.categories
|
||||||
.iter()
|
.iter()
|
||||||
.find(|category| category.id == new_item.category_id)
|
.find(|category| category.id == new_item.category_id)
|
||||||
.ok_or((
|
.unwrap(),
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
components::ErrorPage::build(&format!(
|
|
||||||
"a category with id {new_id} was inserted but does not exist, this is a bug"
|
|
||||||
)),
|
|
||||||
))?,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
@@ -409,37 +370,31 @@ async fn inventory_item_create(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_referer<'a>(headers: &'a HeaderMap) -> Result<&'a str, Error> {
|
||||||
|
headers
|
||||||
|
.get("referer")
|
||||||
|
.ok_or(Error::Request(RequestError::RefererNotFound))?
|
||||||
|
.to_str()
|
||||||
|
.map_err(|error| {
|
||||||
|
Error::Request(RequestError::RefererInvalid {
|
||||||
|
message: error.to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn inventory_item_delete(
|
async fn inventory_item_delete(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
) -> Result<Redirect, (StatusCode, Markup)> {
|
) -> Result<Redirect, Error> {
|
||||||
let deleted = models::InventoryItem::delete(&state.database_pool, id).await?;
|
let deleted = models::InventoryItem::delete(&state.database_pool, id).await?;
|
||||||
|
|
||||||
if !deleted {
|
if !deleted {
|
||||||
Err((
|
Err(Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("item with id {id} not found"),
|
||||||
components::ErrorPage::build(&format!("item with id {id} not found")),
|
}))
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(Redirect::to(
|
Ok(Redirect::to(get_referer(&headers)?))
|
||||||
headers
|
|
||||||
.get("referer")
|
|
||||||
.ok_or((
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
components::ErrorPage::build("no referer header found"),
|
|
||||||
))?
|
|
||||||
.to_str()
|
|
||||||
.map_err(|error| {
|
|
||||||
(
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
components::ErrorPage::build(&format!(
|
|
||||||
"referer could not be converted: {}",
|
|
||||||
error
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,30 +410,15 @@ 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, Markup)> {
|
) -> Result<Redirect, Error> {
|
||||||
if edit_item.name.is_empty() {
|
if edit_item.name.is_empty() {
|
||||||
return Err((
|
return Err(Error::Request(RequestError::EmptyFormElement {
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
name: "name".to_string(),
|
||||||
components::ErrorPage::build("name cannot be empty"),
|
}));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = models::Item::update(
|
let id =
|
||||||
&state.database_pool,
|
models::Item::update(&state.database_pool, id, &edit_item.name, edit_item.weight).await?;
|
||||||
id,
|
|
||||||
&edit_item.name,
|
|
||||||
i64::try_from(edit_item.weight).map_err(|error| {
|
|
||||||
(
|
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
|
||||||
components::ErrorPage::build(&error.to_string()),
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.ok_or((
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
components::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)))
|
||||||
}
|
}
|
||||||
@@ -486,11 +426,12 @@ async fn inventory_item_edit(
|
|||||||
async fn inventory_item_cancel(
|
async fn inventory_item_cancel(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
) -> Result<Redirect, (StatusCode, Markup)> {
|
) -> Result<Redirect, Error> {
|
||||||
let id = models::Item::find(&state.database_pool, id).await?.ok_or((
|
let id = models::Item::find(&state.database_pool, id)
|
||||||
StatusCode::NOT_FOUND,
|
.await?
|
||||||
components::ErrorPage::build(&format!("item with id {id} not found")),
|
.ok_or(Error::Request(RequestError::NotFound {
|
||||||
))?;
|
message: format!("item with id {id} not found"),
|
||||||
|
}))?;
|
||||||
|
|
||||||
Ok(Redirect::to(&format!(
|
Ok(Redirect::to(&format!(
|
||||||
"/inventory/category/{id}/",
|
"/inventory/category/{id}/",
|
||||||
@@ -511,12 +452,11 @@ struct NewTrip {
|
|||||||
async fn trip_create(
|
async fn trip_create(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Form(new_trip): Form<NewTrip>,
|
Form(new_trip): Form<NewTrip>,
|
||||||
) -> Result<Redirect, (StatusCode, Markup)> {
|
) -> Result<Redirect, Error> {
|
||||||
if new_trip.name.is_empty() {
|
if new_trip.name.is_empty() {
|
||||||
return Err((
|
return Err(Error::Request(RequestError::EmptyFormElement {
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
name: "name".to_string(),
|
||||||
components::ErrorPage::build("name cannot be empty"),
|
}));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_id = models::Trip::save(
|
let new_id = models::Trip::save(
|
||||||
@@ -530,9 +470,7 @@ async fn trip_create(
|
|||||||
Ok(Redirect::to(&format!("/trips/{new_id}/")))
|
Ok(Redirect::to(&format!("/trips/{new_id}/")))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn trips(
|
async fn trips(State(state): State<AppState>) -> Result<(StatusCode, Markup), Error> {
|
||||||
State(state): State<AppState>,
|
|
||||||
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
|
||||||
let trips = models::Trip::all(&state.database_pool).await?;
|
let trips = models::Trip::all(&state.database_pool).await?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
@@ -554,14 +492,16 @@ async fn trip(
|
|||||||
State(mut state): State<AppState>,
|
State(mut state): State<AppState>,
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
Query(trip_query): Query<TripQuery>,
|
Query(trip_query): Query<TripQuery>,
|
||||||
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, Markup), Error> {
|
||||||
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 = models::Trip::find(&state.database_pool, id).await?.ok_or((
|
let mut trip: models::Trip =
|
||||||
StatusCode::NOT_FOUND,
|
models::Trip::find(&state.database_pool, id)
|
||||||
components::ErrorPage::build(&format!("trip with id {} not found", id)),
|
.await?
|
||||||
))?;
|
.ok_or(Error::Request(RequestError::NotFound {
|
||||||
|
message: format!("trip with id {id} not found"),
|
||||||
|
}))?;
|
||||||
|
|
||||||
trip.load_trips_types(&state.database_pool).await?;
|
trip.load_trips_types(&state.database_pool).await?;
|
||||||
|
|
||||||
@@ -577,12 +517,9 @@ async fn trip(
|
|||||||
trip.categories()
|
trip.categories()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|category| category.category.id == id)
|
.find(|category| category.category.id == id)
|
||||||
.ok_or((
|
.ok_or(Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("an active category with id {id} does not exist"),
|
||||||
components::ErrorPage::build(&format!(
|
}))
|
||||||
"an active category with id {id} does not exist"
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
@@ -602,16 +539,13 @@ async fn trip(
|
|||||||
async fn trip_type_remove(
|
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, Error> {
|
||||||
let found = models::Trip::trip_type_remove(&state.database_pool, trip_id, type_id).await?;
|
let found = models::Trip::trip_type_remove(&state.database_pool, trip_id, type_id).await?;
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
Err((
|
Err(Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("type {type_id} is not active for trip {trip_id}"),
|
||||||
components::ErrorPage::build(&format!(
|
}))
|
||||||
"type {type_id} is not active for trip {trip_id}"
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(Redirect::to(&format!("/trips/{trip_id}/")))
|
Ok(Redirect::to(&format!("/trips/{trip_id}/")))
|
||||||
}
|
}
|
||||||
@@ -620,7 +554,7 @@ async fn trip_type_remove(
|
|||||||
async fn trip_type_add(
|
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, Error> {
|
||||||
models::Trip::trip_type_add(&state.database_pool, trip_id, type_id).await?;
|
models::Trip::trip_type_add(&state.database_pool, trip_id, type_id).await?;
|
||||||
|
|
||||||
Ok(Redirect::to(&format!("/trips/{trip_id}/")))
|
Ok(Redirect::to(&format!("/trips/{trip_id}/")))
|
||||||
@@ -636,16 +570,15 @@ async fn trip_comment_set(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
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, Error> {
|
||||||
let found =
|
let found =
|
||||||
models::Trip::set_comment(&state.database_pool, trip_id, &comment_update.new_comment)
|
models::Trip::set_comment(&state.database_pool, trip_id, &comment_update.new_comment)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
Err((
|
Err(Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("trip with id {trip_id} not found"),
|
||||||
components::ErrorPage::build(&format!("trip with id {id} not found", id = trip_id)),
|
}))
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(Redirect::to(&format!("/trips/{id}/", id = trip_id)))
|
Ok(Redirect::to(&format!("/trips/{id}/", id = trip_id)))
|
||||||
}
|
}
|
||||||
@@ -661,13 +594,12 @@ async fn trip_edit_attribute(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path((trip_id, attribute)): Path<(Uuid, models::TripAttribute)>,
|
Path((trip_id, attribute)): Path<(Uuid, models::TripAttribute)>,
|
||||||
Form(trip_update): Form<TripUpdate>,
|
Form(trip_update): Form<TripUpdate>,
|
||||||
) -> Result<Redirect, (StatusCode, Markup)> {
|
) -> Result<Redirect, Error> {
|
||||||
if attribute == models::TripAttribute::Name {
|
if attribute == models::TripAttribute::Name {
|
||||||
if trip_update.new_value.is_empty() {
|
if trip_update.new_value.is_empty() {
|
||||||
return Err((
|
return Err(Error::Request(RequestError::EmptyFormElement {
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
name: "name".to_string(),
|
||||||
components::ErrorPage::build("name cannot be empty"),
|
}));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
models::Trip::set_attribute(
|
models::Trip::set_attribute(
|
||||||
@@ -687,26 +619,18 @@ async fn trip_item_set_state(
|
|||||||
item_id: Uuid,
|
item_id: Uuid,
|
||||||
key: models::TripItemStateKey,
|
key: models::TripItemStateKey,
|
||||||
value: bool,
|
value: bool,
|
||||||
) -> Result<(), (StatusCode, Markup)> {
|
) -> Result<(), Error> {
|
||||||
models::TripItem::set_state(&state.database_pool, trip_id, item_id, key, value).await?;
|
models::TripItem::set_state(&state.database_pool, trip_id, item_id, key, value).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn trip_row(
|
async fn trip_row(state: &AppState, trip_id: Uuid, item_id: Uuid) -> Result<Markup, Error> {
|
||||||
state: &AppState,
|
|
||||||
trip_id: Uuid,
|
|
||||||
item_id: Uuid,
|
|
||||||
) -> Result<Markup, (StatusCode, Markup)> {
|
|
||||||
let item = models::TripItem::find(&state.database_pool, trip_id, item_id)
|
let item = models::TripItem::find(&state.database_pool, trip_id, item_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
(
|
Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("item with id {item_id} not found for trip {trip_id}"),
|
||||||
components::ErrorPage::build(&format!(
|
})
|
||||||
"item with id {} not found for trip {}",
|
|
||||||
item_id, trip_id
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let item_row = components::trip::TripItemListRow::build(
|
let item_row = components::trip::TripItemListRow::build(
|
||||||
@@ -718,13 +642,9 @@ async fn trip_row(
|
|||||||
let category = models::TripCategory::find(&state.database_pool, trip_id, item.item.category_id)
|
let category = models::TripCategory::find(&state.database_pool, trip_id, item.item.category_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
(
|
Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("category with id {} not found", item.item.category_id),
|
||||||
components::ErrorPage::build(&format!(
|
})
|
||||||
"category with id {} not found",
|
|
||||||
item.item.category_id
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// TODO biggest_category_weight?
|
// TODO biggest_category_weight?
|
||||||
@@ -738,8 +658,8 @@ async fn trip_item_set_pick(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<Redirect, (StatusCode, Markup)> {
|
) -> Result<Redirect, Error> {
|
||||||
Ok::<_, models::Error>(
|
Ok::<_, Error>(
|
||||||
trip_item_set_state(
|
trip_item_set_state(
|
||||||
&state,
|
&state,
|
||||||
trip_id,
|
trip_id,
|
||||||
@@ -749,32 +669,13 @@ async fn trip_item_set_pick(
|
|||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
)
|
)
|
||||||
.map(|_| -> Result<Redirect, (StatusCode, Markup)> {
|
.map(|_| -> Result<Redirect, Error> { Ok(Redirect::to(get_referer(&headers)?)) })?
|
||||||
Ok(Redirect::to(
|
|
||||||
headers
|
|
||||||
.get("referer")
|
|
||||||
.ok_or((
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
components::ErrorPage::build("no referer header found"),
|
|
||||||
))?
|
|
||||||
.to_str()
|
|
||||||
.map_err(|error| {
|
|
||||||
(
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
components::ErrorPage::build(&format!(
|
|
||||||
"referer could not be converted: {}",
|
|
||||||
error
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn trip_item_set_pick_htmx(
|
async fn trip_item_set_pick_htmx(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
||||||
) -> Result<(StatusCode, HeaderMap, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, HeaderMap, Markup), Error> {
|
||||||
trip_item_set_state(
|
trip_item_set_state(
|
||||||
&state,
|
&state,
|
||||||
trip_id,
|
trip_id,
|
||||||
@@ -799,8 +700,8 @@ async fn trip_item_set_unpick(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<Redirect, (StatusCode, Markup)> {
|
) -> Result<Redirect, Error> {
|
||||||
Ok::<_, models::Error>(
|
Ok::<_, Error>(
|
||||||
trip_item_set_state(
|
trip_item_set_state(
|
||||||
&state,
|
&state,
|
||||||
trip_id,
|
trip_id,
|
||||||
@@ -810,32 +711,13 @@ async fn trip_item_set_unpick(
|
|||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
)
|
)
|
||||||
.map(|_| -> Result<Redirect, (StatusCode, Markup)> {
|
.map(|_| -> Result<Redirect, Error> { Ok(Redirect::to(get_referer(&headers)?)) })?
|
||||||
Ok(Redirect::to(
|
|
||||||
headers
|
|
||||||
.get("referer")
|
|
||||||
.ok_or((
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
components::ErrorPage::build("no referer header found"),
|
|
||||||
))?
|
|
||||||
.to_str()
|
|
||||||
.map_err(|error| {
|
|
||||||
(
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
components::ErrorPage::build(&format!(
|
|
||||||
"referer could not be converted: {}",
|
|
||||||
error
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn trip_item_set_unpick_htmx(
|
async fn trip_item_set_unpick_htmx(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
||||||
) -> Result<(StatusCode, HeaderMap, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, HeaderMap, Markup), Error> {
|
||||||
trip_item_set_state(
|
trip_item_set_state(
|
||||||
&state,
|
&state,
|
||||||
trip_id,
|
trip_id,
|
||||||
@@ -860,8 +742,8 @@ async fn trip_item_set_pack(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<Redirect, (StatusCode, Markup)> {
|
) -> Result<Redirect, Error> {
|
||||||
Ok::<_, models::Error>(
|
Ok::<_, Error>(
|
||||||
trip_item_set_state(
|
trip_item_set_state(
|
||||||
&state,
|
&state,
|
||||||
trip_id,
|
trip_id,
|
||||||
@@ -871,32 +753,13 @@ async fn trip_item_set_pack(
|
|||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
)
|
)
|
||||||
.map(|_| -> Result<Redirect, (StatusCode, Markup)> {
|
.map(|_| -> Result<Redirect, Error> { Ok(Redirect::to(get_referer(&headers)?)) })?
|
||||||
Ok(Redirect::to(
|
|
||||||
headers
|
|
||||||
.get("referer")
|
|
||||||
.ok_or((
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
components::ErrorPage::build("no referer header found"),
|
|
||||||
))?
|
|
||||||
.to_str()
|
|
||||||
.map_err(|error| {
|
|
||||||
(
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
components::ErrorPage::build(&format!(
|
|
||||||
"referer could not be converted: {}",
|
|
||||||
error
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn trip_item_set_pack_htmx(
|
async fn trip_item_set_pack_htmx(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
||||||
) -> Result<(StatusCode, HeaderMap, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, HeaderMap, Markup), Error> {
|
||||||
trip_item_set_state(
|
trip_item_set_state(
|
||||||
&state,
|
&state,
|
||||||
trip_id,
|
trip_id,
|
||||||
@@ -921,8 +784,8 @@ async fn trip_item_set_unpack(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<Redirect, (StatusCode, Markup)> {
|
) -> Result<Redirect, Error> {
|
||||||
Ok::<_, models::Error>(
|
Ok::<_, Error>(
|
||||||
trip_item_set_state(
|
trip_item_set_state(
|
||||||
&state,
|
&state,
|
||||||
trip_id,
|
trip_id,
|
||||||
@@ -932,32 +795,13 @@ async fn trip_item_set_unpack(
|
|||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
)
|
)
|
||||||
.map(|_| -> Result<Redirect, (StatusCode, Markup)> {
|
.map(|_| -> Result<Redirect, Error> { Ok(Redirect::to(get_referer(&headers)?)) })?
|
||||||
Ok(Redirect::to(
|
|
||||||
headers
|
|
||||||
.get("referer")
|
|
||||||
.ok_or((
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
components::ErrorPage::build("no referer header found"),
|
|
||||||
))?
|
|
||||||
.to_str()
|
|
||||||
.map_err(|error| {
|
|
||||||
(
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
components::ErrorPage::build(&format!(
|
|
||||||
"referer could not be converted: {}",
|
|
||||||
error
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn trip_item_set_unpack_htmx(
|
async fn trip_item_set_unpack_htmx(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
||||||
) -> Result<(StatusCode, HeaderMap, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, HeaderMap, Markup), Error> {
|
||||||
trip_item_set_state(
|
trip_item_set_state(
|
||||||
&state,
|
&state,
|
||||||
trip_id,
|
trip_id,
|
||||||
@@ -981,13 +825,9 @@ async fn trip_item_set_unpack_htmx(
|
|||||||
async fn trip_total_weight_htmx(
|
async fn trip_total_weight_htmx(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(trip_id): Path<Uuid>,
|
Path(trip_id): Path<Uuid>,
|
||||||
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, Markup), Error> {
|
||||||
let total_weight = models::Trip::find_total_picked_weight(&state.database_pool, trip_id)
|
let total_weight =
|
||||||
.await?
|
models::Trip::find_total_picked_weight(&state.database_pool, trip_id).await?;
|
||||||
.ok_or((
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
components::ErrorPage::build(&format!("trip with id {trip_id} not found")),
|
|
||||||
))?;
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
components::trip::TripInfoTotalWeightRow::build(trip_id, total_weight),
|
components::trip::TripInfoTotalWeightRow::build(trip_id, total_weight),
|
||||||
@@ -1003,12 +843,11 @@ struct NewCategory {
|
|||||||
async fn inventory_category_create(
|
async fn inventory_category_create(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Form(new_category): Form<NewCategory>,
|
Form(new_category): Form<NewCategory>,
|
||||||
) -> Result<Redirect, (StatusCode, Markup)> {
|
) -> Result<Redirect, Error> {
|
||||||
if new_category.name.is_empty() {
|
if new_category.name.is_empty() {
|
||||||
return Err((
|
return Err(Error::Request(RequestError::EmptyFormElement {
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
name: "name".to_string(),
|
||||||
components::ErrorPage::build("name cannot be empty"),
|
}));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let _new_id = models::Category::save(&state.database_pool, &new_category.name).await?;
|
let _new_id = models::Category::save(&state.database_pool, &new_category.name).await?;
|
||||||
@@ -1020,14 +859,13 @@ async fn trip_state_set(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Path((trip_id, new_state)): Path<(Uuid, models::TripState)>,
|
Path((trip_id, new_state)): Path<(Uuid, models::TripState)>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Markup)> {
|
) -> Result<impl IntoResponse, Error> {
|
||||||
let exists = models::Trip::set_state(&state.database_pool, trip_id, &new_state).await?;
|
let exists = models::Trip::set_state(&state.database_pool, trip_id, &new_state).await?;
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return Err((
|
return Err(Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("trip with id {trip_id} not found"),
|
||||||
components::ErrorPage::build(&format!("trip with id {id} not found", id = trip_id)),
|
}));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_htmx(&headers) {
|
if is_htmx(&headers) {
|
||||||
@@ -1056,7 +894,7 @@ struct TripTypeQuery {
|
|||||||
async fn trips_types(
|
async fn trips_types(
|
||||||
State(mut state): State<AppState>,
|
State(mut state): State<AppState>,
|
||||||
Query(trip_type_query): Query<TripTypeQuery>,
|
Query(trip_type_query): Query<TripTypeQuery>,
|
||||||
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, Markup), Error> {
|
||||||
state.client_state.trip_type_edit = trip_type_query.edit;
|
state.client_state.trip_type_edit = trip_type_query.edit;
|
||||||
|
|
||||||
let trip_types: Vec<models::TripsType> = models::TripsType::all(&state.database_pool).await?;
|
let trip_types: Vec<models::TripsType> = models::TripsType::all(&state.database_pool).await?;
|
||||||
@@ -1079,12 +917,11 @@ struct NewTripType {
|
|||||||
async fn trip_type_create(
|
async fn trip_type_create(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Form(new_trip_type): Form<NewTripType>,
|
Form(new_trip_type): Form<NewTripType>,
|
||||||
) -> Result<Redirect, (StatusCode, Markup)> {
|
) -> Result<Redirect, Error> {
|
||||||
if new_trip_type.name.is_empty() {
|
if new_trip_type.name.is_empty() {
|
||||||
return Err((
|
return Err(Error::Request(RequestError::EmptyFormElement {
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
name: "name".to_string(),
|
||||||
components::ErrorPage::build("name cannot be empty"),
|
}));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let _new_id = models::TripsType::save(&state.database_pool, &new_trip_type.name).await?;
|
let _new_id = models::TripsType::save(&state.database_pool, &new_trip_type.name).await?;
|
||||||
@@ -1102,12 +939,11 @@ async fn trips_types_edit_name(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(trip_type_id): Path<Uuid>,
|
Path(trip_type_id): Path<Uuid>,
|
||||||
Form(trip_update): Form<TripTypeUpdate>,
|
Form(trip_update): Form<TripTypeUpdate>,
|
||||||
) -> Result<Redirect, (StatusCode, Markup)> {
|
) -> Result<Redirect, Error> {
|
||||||
if trip_update.new_value.is_empty() {
|
if trip_update.new_value.is_empty() {
|
||||||
return Err((
|
return Err(Error::Request(RequestError::EmptyFormElement {
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
name: "name".to_string(),
|
||||||
components::ErrorPage::build("name cannot be empty"),
|
}));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let exists =
|
let exists =
|
||||||
@@ -1115,13 +951,9 @@ async fn trips_types_edit_name(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
Err((
|
return Err(Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("trip type with id {trip_type_id} not found"),
|
||||||
components::ErrorPage::build(&format!(
|
}));
|
||||||
"tript type with id {id} not found",
|
|
||||||
id = trip_type_id
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(Redirect::to("/trips/types/"))
|
Ok(Redirect::to("/trips/types/"))
|
||||||
}
|
}
|
||||||
@@ -1130,13 +962,12 @@ async fn trips_types_edit_name(
|
|||||||
async fn inventory_item(
|
async fn inventory_item(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, Markup), Error> {
|
||||||
let item = models::InventoryItem::find(&state.database_pool, id)
|
let item = models::InventoryItem::find(&state.database_pool, id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or((
|
.ok_or(Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("inventory item with id {id} not found"),
|
||||||
components::ErrorPage::build(&format!("inventory item with id {id} not found")),
|
}))?;
|
||||||
))?;
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
@@ -1150,13 +981,12 @@ async fn inventory_item(
|
|||||||
async fn trip_category_select(
|
async fn trip_category_select(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path((trip_id, category_id)): Path<(Uuid, Uuid)>,
|
Path((trip_id, category_id)): Path<(Uuid, Uuid)>,
|
||||||
) -> Result<(StatusCode, HeaderMap, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, HeaderMap, Markup), Error> {
|
||||||
let mut trip = models::Trip::find(&state.database_pool, trip_id)
|
let mut trip = models::Trip::find(&state.database_pool, trip_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or((
|
.ok_or(Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("trip with id {trip_id} not found"),
|
||||||
components::ErrorPage::build(&format!("trip with id {trip_id} not found")),
|
}))?;
|
||||||
))?;
|
|
||||||
|
|
||||||
trip.load_categories(&state.database_pool).await?;
|
trip.load_categories(&state.database_pool).await?;
|
||||||
|
|
||||||
@@ -1164,10 +994,9 @@ async fn trip_category_select(
|
|||||||
.categories()
|
.categories()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|c| c.category.id == category_id)
|
.find(|c| c.category.id == category_id)
|
||||||
.ok_or((
|
.ok_or(Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("category with id {category_id} not found"),
|
||||||
components::ErrorPage::build(&format!("category with id {category_id} not found")),
|
}))?;
|
||||||
))?;
|
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert::<HeaderName>(
|
headers.insert::<HeaderName>(
|
||||||
@@ -1185,7 +1014,7 @@ async fn trip_category_select(
|
|||||||
async fn inventory_category_select(
|
async fn inventory_category_select(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(category_id): Path<Uuid>,
|
Path(category_id): Path<Uuid>,
|
||||||
) -> Result<(StatusCode, HeaderMap, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, HeaderMap, Markup), Error> {
|
||||||
let inventory = models::Inventory::load(&state.database_pool).await?;
|
let inventory = models::Inventory::load(&state.database_pool).await?;
|
||||||
|
|
||||||
let active_category: Option<&models::Category> = Some(
|
let active_category: Option<&models::Category> = Some(
|
||||||
@@ -1193,12 +1022,9 @@ async fn inventory_category_select(
|
|||||||
.categories
|
.categories
|
||||||
.iter()
|
.iter()
|
||||||
.find(|category| category.id == category_id)
|
.find(|category| category.id == category_id)
|
||||||
.ok_or((
|
.ok_or(Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("a category with id {category_id} not found"),
|
||||||
components::ErrorPage::build(&format!(
|
}))?,
|
||||||
"a category with id {category_id} not found"
|
|
||||||
)),
|
|
||||||
))?,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
@@ -1223,13 +1049,12 @@ async fn inventory_category_select(
|
|||||||
async fn trip_packagelist(
|
async fn trip_packagelist(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(trip_id): Path<Uuid>,
|
Path(trip_id): Path<Uuid>,
|
||||||
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, Markup), Error> {
|
||||||
let mut trip = models::Trip::find(&state.database_pool, trip_id)
|
let mut trip = models::Trip::find(&state.database_pool, trip_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or((
|
.ok_or(Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("trip with id {trip_id} not found"),
|
||||||
components::ErrorPage::build(&format!("trip with id {trip_id} not found")),
|
}))?;
|
||||||
))?;
|
|
||||||
|
|
||||||
trip.load_categories(&state.database_pool).await?;
|
trip.load_categories(&state.database_pool).await?;
|
||||||
|
|
||||||
@@ -1245,7 +1070,7 @@ async fn trip_packagelist(
|
|||||||
async fn trip_item_packagelist_set_pack_htmx(
|
async fn trip_item_packagelist_set_pack_htmx(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
||||||
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, Markup), Error> {
|
||||||
trip_item_set_state(
|
trip_item_set_state(
|
||||||
&state,
|
&state,
|
||||||
trip_id,
|
trip_id,
|
||||||
@@ -1257,10 +1082,9 @@ async fn trip_item_packagelist_set_pack_htmx(
|
|||||||
|
|
||||||
let item = models::TripItem::find(&state.database_pool, trip_id, item_id)
|
let item = models::TripItem::find(&state.database_pool, trip_id, item_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or((
|
.ok_or(Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("an item with id {item_id} does not exist"),
|
||||||
components::ErrorPage::build(&format!("an item with id {item_id} does not exist")),
|
}))?;
|
||||||
))?;
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
@@ -1271,7 +1095,7 @@ async fn trip_item_packagelist_set_pack_htmx(
|
|||||||
async fn trip_item_packagelist_set_unpack_htmx(
|
async fn trip_item_packagelist_set_unpack_htmx(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
||||||
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, Markup), Error> {
|
||||||
trip_item_set_state(
|
trip_item_set_state(
|
||||||
&state,
|
&state,
|
||||||
trip_id,
|
trip_id,
|
||||||
@@ -1285,10 +1109,9 @@ async fn trip_item_packagelist_set_unpack_htmx(
|
|||||||
// return 404. but error handling cannot hurt ;)
|
// return 404. but error handling cannot hurt ;)
|
||||||
let item = models::TripItem::find(&state.database_pool, trip_id, item_id)
|
let item = models::TripItem::find(&state.database_pool, trip_id, item_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or((
|
.ok_or(Error::Request(RequestError::NotFound {
|
||||||
StatusCode::NOT_FOUND,
|
message: format!("an item with id {item_id} does not exist"),
|
||||||
components::ErrorPage::build(&format!("an item with id {item_id} does not exist")),
|
}))?;
|
||||||
))?;
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_variant::to_variant_name;
|
use serde_variant::to_variant_name;
|
||||||
use sqlx::{
|
|
||||||
database::Database,
|
|
||||||
database::HasValueRef,
|
|
||||||
sqlite::{Sqlite, SqliteRow},
|
|
||||||
Decode, Row,
|
|
||||||
};
|
|
||||||
use std::convert;
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::num::TryFromIntError;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use futures::TryFutureExt;
|
use futures::TryFutureExt;
|
||||||
@@ -288,7 +279,7 @@ impl TripItem {
|
|||||||
) -> Result<Option<Self>, Error> {
|
) -> Result<Option<Self>, Error> {
|
||||||
let item_id_param = item_id.to_string();
|
let item_id_param = item_id.to_string();
|
||||||
let trip_id_param = trip_id.to_string();
|
let trip_id_param = trip_id.to_string();
|
||||||
let item: Result<Result<TripItem, Error>, Error> = sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
DbTripsItemsRow,
|
DbTripsItemsRow,
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -309,18 +300,10 @@ impl TripItem {
|
|||||||
item_id_param,
|
item_id_param,
|
||||||
trip_id_param,
|
trip_id_param,
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_optional(pool)
|
||||||
.map_ok(|row| row.try_into())
|
.await?
|
||||||
.await
|
.map(|row| row.try_into())
|
||||||
.map_err(|error| error.into());
|
.transpose()
|
||||||
|
|
||||||
match item {
|
|
||||||
Err(error) => match error {
|
|
||||||
Error::Query(QueryError::NotFound { description: _ }) => Ok(None),
|
|
||||||
_ => Err(error),
|
|
||||||
},
|
|
||||||
Ok(v) => Ok(Some(v?)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_state(
|
pub async fn set_state(
|
||||||
@@ -446,7 +429,7 @@ impl Trip {
|
|||||||
trip_id: Uuid,
|
trip_id: Uuid,
|
||||||
) -> Result<Option<Self>, Error> {
|
) -> Result<Option<Self>, Error> {
|
||||||
let trip_id_param = trip_id.to_string();
|
let trip_id_param = trip_id.to_string();
|
||||||
let trip = sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
DbTripRow,
|
DbTripRow,
|
||||||
"SELECT
|
"SELECT
|
||||||
id,
|
id,
|
||||||
@@ -462,18 +445,10 @@ impl Trip {
|
|||||||
WHERE id = ?",
|
WHERE id = ?",
|
||||||
trip_id_param
|
trip_id_param
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_optional(pool)
|
||||||
.map_ok(|row| row.try_into())
|
.await?
|
||||||
.map_err(|error| error.into())
|
.map(|row| row.try_into())
|
||||||
.await;
|
.transpose()
|
||||||
|
|
||||||
match trip {
|
|
||||||
Err(error) => match error {
|
|
||||||
Error::Query(QueryError::NotFound { description: _ }) => Ok(None),
|
|
||||||
_ => Err(error),
|
|
||||||
},
|
|
||||||
Ok(v) => Ok(Some(v?)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn trip_type_remove(
|
pub async fn trip_type_remove(
|
||||||
@@ -615,7 +590,7 @@ impl Trip {
|
|||||||
pub async fn find_total_picked_weight(
|
pub async fn find_total_picked_weight(
|
||||||
pool: &sqlx::Pool<sqlx::Sqlite>,
|
pool: &sqlx::Pool<sqlx::Sqlite>,
|
||||||
trip_id: Uuid,
|
trip_id: Uuid,
|
||||||
) -> Result<Option<i64>, Error> {
|
) -> Result<i64, Error> {
|
||||||
let trip_id_param = trip_id.to_string();
|
let trip_id_param = trip_id.to_string();
|
||||||
let weight = sqlx::query_as!(
|
let weight = sqlx::query_as!(
|
||||||
DbTripWeightRow,
|
DbTripWeightRow,
|
||||||
@@ -634,17 +609,10 @@ impl Trip {
|
|||||||
trip_id_param
|
trip_id_param
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.map_ok(|row| row.total_weight.map(|weight| weight as i64))
|
.map_ok(|row| row.total_weight.unwrap() as i64)
|
||||||
.map_err(|error| error.into())
|
.await?;
|
||||||
.await;
|
|
||||||
|
|
||||||
match weight {
|
Ok(weight)
|
||||||
Err(error) => match error {
|
|
||||||
Error::Query(QueryError::NotFound { description: _ }) => Ok(None),
|
|
||||||
_ => Err(error.into()),
|
|
||||||
},
|
|
||||||
Ok(v) => Ok(v),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn types(&self) -> &Vec<TripType> {
|
pub fn types(&self) -> &Vec<TripType> {
|
||||||
@@ -997,7 +965,7 @@ impl Category {
|
|||||||
id: Uuid,
|
id: Uuid,
|
||||||
) -> Result<Option<Category>, Error> {
|
) -> Result<Option<Category>, Error> {
|
||||||
let id_param = id.to_string();
|
let id_param = id.to_string();
|
||||||
let item = sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
DbCategoryRow,
|
DbCategoryRow,
|
||||||
"SELECT
|
"SELECT
|
||||||
id,
|
id,
|
||||||
@@ -1007,18 +975,10 @@ impl Category {
|
|||||||
WHERE category.id = ?",
|
WHERE category.id = ?",
|
||||||
id_param,
|
id_param,
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_optional(pool)
|
||||||
.map_ok(|row| row.try_into())
|
.await?
|
||||||
.map_err(|error| error.into())
|
.map(|row| row.try_into())
|
||||||
.await;
|
.transpose()
|
||||||
|
|
||||||
match item {
|
|
||||||
Err(error) => match error {
|
|
||||||
Error::Query(QueryError::NotFound { description: _ }) => Ok(None),
|
|
||||||
_ => Err(error),
|
|
||||||
},
|
|
||||||
Ok(v) => Ok(Some(v?)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save(pool: &sqlx::Pool<sqlx::Sqlite>, name: &str) -> Result<Uuid, Error> {
|
pub async fn save(pool: &sqlx::Pool<sqlx::Sqlite>, name: &str) -> Result<Uuid, Error> {
|
||||||
@@ -1100,7 +1060,7 @@ impl TryFrom<DbInventoryItemsRow> 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 id_param = id.to_string();
|
let id_param = id.to_string();
|
||||||
let item = sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
DbInventoryItemsRow,
|
DbInventoryItemsRow,
|
||||||
"SELECT
|
"SELECT
|
||||||
id,
|
id,
|
||||||
@@ -1112,28 +1072,22 @@ impl Item {
|
|||||||
WHERE item.id = ?",
|
WHERE item.id = ?",
|
||||||
id_param,
|
id_param,
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_optional(pool)
|
||||||
.map_err(|error| error.into())
|
.await?
|
||||||
.map_ok(|row| row.try_into())
|
.map(|row| row.try_into())
|
||||||
.await;
|
.transpose()
|
||||||
|
|
||||||
match item {
|
|
||||||
Err(error) => match error {
|
|
||||||
Error::Query(QueryError::NotFound { description: _ }) => Ok(None),
|
|
||||||
_ => Err(error),
|
|
||||||
},
|
|
||||||
Ok(v) => Ok(Some(v?)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update(
|
pub async fn update(
|
||||||
pool: &sqlx::Pool<sqlx::Sqlite>,
|
pool: &sqlx::Pool<sqlx::Sqlite>,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
name: &str,
|
name: &str,
|
||||||
weight: i64,
|
weight: u32,
|
||||||
) -> Result<Option<Uuid>, Error> {
|
) -> Result<Uuid, Error> {
|
||||||
|
let weight = i64::try_from(weight).unwrap();
|
||||||
|
|
||||||
let id_param = id.to_string();
|
let id_param = id.to_string();
|
||||||
let id = sqlx::query!(
|
Ok(sqlx::query!(
|
||||||
"UPDATE inventory_items AS item
|
"UPDATE inventory_items AS item
|
||||||
SET
|
SET
|
||||||
name = ?,
|
name = ?,
|
||||||
@@ -1146,22 +1100,8 @@ impl Item {
|
|||||||
id_param,
|
id_param,
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.map_ok(|row| {
|
.map_ok(|row| Uuid::try_parse(&row.id.unwrap()))
|
||||||
let id: &str = &row.id.unwrap(); // TODO
|
.await??)
|
||||||
let uuid: Result<Uuid, uuid::Error> = Uuid::try_parse(id);
|
|
||||||
let uuid: Result<Uuid, Error> = uuid.map_err(|error| error.into());
|
|
||||||
uuid
|
|
||||||
})
|
|
||||||
.map_err(|error| error.into())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match id {
|
|
||||||
Err(error) => match error {
|
|
||||||
Error::Query(QueryError::NotFound { description: _ }) => Ok(None),
|
|
||||||
_ => Err(error.into()),
|
|
||||||
},
|
|
||||||
Ok(v) => Ok(Some(v?)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_category_max_weight(
|
pub async fn get_category_max_weight(
|
||||||
@@ -1197,7 +1137,7 @@ impl Item {
|
|||||||
category_id: Uuid,
|
category_id: Uuid,
|
||||||
) -> Result<i64, Error> {
|
) -> Result<i64, Error> {
|
||||||
let category_id_param = category_id.to_string();
|
let category_id_param = category_id.to_string();
|
||||||
let weight: Result<i64, Error> = sqlx::query!(
|
Ok(sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT COALESCE(SUM(i_item.weight), 0) as weight
|
SELECT COALESCE(SUM(i_item.weight), 0) as weight
|
||||||
FROM inventory_items_categories as category
|
FROM inventory_items_categories as category
|
||||||
@@ -1218,10 +1158,7 @@ impl Item {
|
|||||||
// We can be certain that the row exists, as we COALESCE it
|
// We can be certain that the row exists, as we COALESCE it
|
||||||
row.weight.unwrap() as i64
|
row.weight.unwrap() as i64
|
||||||
})
|
})
|
||||||
.map_err(|error| error.into())
|
.await?)
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(weight?)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1316,7 +1253,7 @@ impl InventoryItem {
|
|||||||
pub async fn find(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<Option<Self>, Error> {
|
pub async fn find(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<Option<Self>, Error> {
|
||||||
let id_param = id.to_string();
|
let id_param = id.to_string();
|
||||||
|
|
||||||
let item = sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
DbInventoryItemRow,
|
DbInventoryItemRow,
|
||||||
"SELECT
|
"SELECT
|
||||||
item.id AS id,
|
item.id AS id,
|
||||||
@@ -1338,39 +1275,23 @@ impl InventoryItem {
|
|||||||
WHERE item.id = ?",
|
WHERE item.id = ?",
|
||||||
id_param,
|
id_param,
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_optional(pool)
|
||||||
.map_ok(|row| row.try_into())
|
.await?
|
||||||
.map_err(|error| error.into())
|
.map(|row| row.try_into())
|
||||||
.await;
|
.transpose()
|
||||||
|
|
||||||
match item {
|
|
||||||
Err(error) => match error {
|
|
||||||
Error::Query(QueryError::NotFound { description: _ }) => Ok(None),
|
|
||||||
_ => Err(error.into()),
|
|
||||||
},
|
|
||||||
Ok(v) => Ok(Some(v?)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn name_exists(pool: &sqlx::Pool<sqlx::Sqlite>, name: &str) -> Result<bool, Error> {
|
pub async fn name_exists(pool: &sqlx::Pool<sqlx::Sqlite>, name: &str) -> Result<bool, Error> {
|
||||||
let item = sqlx::query!(
|
Ok(sqlx::query!(
|
||||||
"SELECT id
|
"SELECT id
|
||||||
FROM inventory_items
|
FROM inventory_items
|
||||||
WHERE name = ?",
|
WHERE name = ?",
|
||||||
name,
|
name,
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_optional(pool)
|
||||||
.map_ok(|_row| ())
|
.await?
|
||||||
.map_err(|error| error.into())
|
.map(|_row| ())
|
||||||
.await;
|
.is_some())
|
||||||
|
|
||||||
match item {
|
|
||||||
Err(error) => match error {
|
|
||||||
Error::Query(QueryError::NotFound { description: _ }) => Ok(false),
|
|
||||||
_ => Err(error.into()),
|
|
||||||
},
|
|
||||||
Ok(_) => Ok(true),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<bool, Error> {
|
pub async fn delete(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<bool, Error> {
|
||||||
@@ -1427,23 +1348,9 @@ impl Inventory {
|
|||||||
.fetch(pool)
|
.fetch(pool)
|
||||||
.map_ok(|row: DbCategoryRow| row.try_into())
|
.map_ok(|row: DbCategoryRow| row.try_into())
|
||||||
.try_collect::<Vec<Result<Category, Error>>>()
|
.try_collect::<Vec<Result<Category, Error>>>()
|
||||||
.await
|
.await?
|
||||||
// we have two error handling lines here. these are distinct errors
|
|
||||||
// this one is the SQL error that may arise during the query
|
|
||||||
.map_err(|error| {
|
|
||||||
Error::Database(DatabaseError::Sql {
|
|
||||||
description: error.to_string(),
|
|
||||||
})
|
|
||||||
})?
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Result<Vec<Category>, Error>>()
|
.collect::<Result<Vec<Category>, Error>>()?;
|
||||||
// and this one is the model mapping error that may arise e.g. during
|
|
||||||
// reading of the rows
|
|
||||||
.map_err(|error| {
|
|
||||||
Error::Database(DatabaseError::Sql {
|
|
||||||
description: error.to_string(),
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
for category in &mut categories {
|
for category in &mut categories {
|
||||||
category.populate_items(pool).await?;
|
category.populate_items(pool).await?;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use std::convert;
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use sqlx::error::DatabaseError as _;
|
use sqlx::error::DatabaseError as _;
|
||||||
@@ -43,9 +42,6 @@ impl fmt::Display for DatabaseError {
|
|||||||
pub enum QueryError {
|
pub enum QueryError {
|
||||||
/// Errors that are caused by wrong input data, e.g. ids that cannot be found, or
|
/// Errors that are caused by wrong input data, e.g. ids that cannot be found, or
|
||||||
/// inserts that violate unique constraints
|
/// inserts that violate unique constraints
|
||||||
Constraint {
|
|
||||||
description: String,
|
|
||||||
},
|
|
||||||
Duplicate {
|
Duplicate {
|
||||||
description: String,
|
description: String,
|
||||||
},
|
},
|
||||||
@@ -60,9 +56,6 @@ pub enum QueryError {
|
|||||||
impl fmt::Display for QueryError {
|
impl fmt::Display for QueryError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Constraint { description } => {
|
|
||||||
write!(f, "SQL constraint error: {description}")
|
|
||||||
}
|
|
||||||
Self::Duplicate { description } => {
|
Self::Duplicate { description } => {
|
||||||
write!(f, "Duplicate data entry: {description}")
|
write!(f, "Duplicate data entry: {description}")
|
||||||
}
|
}
|
||||||
@@ -97,7 +90,7 @@ impl fmt::Debug for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl convert::From<uuid::Error> for Error {
|
impl From<uuid::Error> for Error {
|
||||||
fn from(value: uuid::Error) -> Self {
|
fn from(value: uuid::Error) -> Self {
|
||||||
Error::Database(DatabaseError::Uuid {
|
Error::Database(DatabaseError::Uuid {
|
||||||
description: value.to_string(),
|
description: value.to_string(),
|
||||||
@@ -105,7 +98,7 @@ impl convert::From<uuid::Error> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl convert::From<time::error::Format> for Error {
|
impl From<time::error::Format> for Error {
|
||||||
fn from(value: time::error::Format) -> Self {
|
fn from(value: time::error::Format) -> Self {
|
||||||
Error::Database(DatabaseError::TimeParse {
|
Error::Database(DatabaseError::TimeParse {
|
||||||
description: value.to_string(),
|
description: value.to_string(),
|
||||||
@@ -113,7 +106,7 @@ impl convert::From<time::error::Format> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl convert::From<sqlx::Error> for Error {
|
impl From<sqlx::Error> for Error {
|
||||||
fn from(value: sqlx::Error) -> Self {
|
fn from(value: sqlx::Error) -> Self {
|
||||||
match value {
|
match value {
|
||||||
sqlx::Error::RowNotFound => Error::Query(QueryError::NotFound {
|
sqlx::Error::RowNotFound => Error::Query(QueryError::NotFound {
|
||||||
@@ -124,11 +117,11 @@ impl convert::From<sqlx::Error> for Error {
|
|||||||
if let Some(code) = sqlite_error.code() {
|
if let Some(code) = sqlite_error.code() {
|
||||||
match &*code {
|
match &*code {
|
||||||
// SQLITE_CONSTRAINT_FOREIGNKEY
|
// SQLITE_CONSTRAINT_FOREIGNKEY
|
||||||
"787" => Error::Query(QueryError::Constraint {
|
"787" => Error::Query(QueryError::ReferenceNotFound {
|
||||||
description: format!("foreign key reference not found"),
|
description: format!("foreign key reference not found"),
|
||||||
}),
|
}),
|
||||||
// SQLITE_CONSTRAINT_UNIQUE
|
// SQLITE_CONSTRAINT_UNIQUE
|
||||||
"2067" => Error::Query(QueryError::Constraint {
|
"2067" => Error::Query(QueryError::Duplicate {
|
||||||
description: format!("item with unique constraint already exists",),
|
description: format!("item with unique constraint already exists",),
|
||||||
}),
|
}),
|
||||||
_ => Error::Database(DatabaseError::Sql {
|
_ => Error::Database(DatabaseError::Sql {
|
||||||
@@ -154,7 +147,7 @@ impl convert::From<sqlx::Error> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl convert::From<time::error::Parse> for Error {
|
impl From<time::error::Parse> for Error {
|
||||||
fn from(value: time::error::Parse) -> Self {
|
fn from(value: time::error::Parse) -> Self {
|
||||||
Error::Database(DatabaseError::TimeParse {
|
Error::Database(DatabaseError::TimeParse {
|
||||||
description: value.to_string(),
|
description: value.to_string(),
|
||||||
|
|||||||
18
rust/src/sqlite.rs
Normal file
18
rust/src/sqlite.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
||||||
|
pub use sqlx::{Pool, Sqlite};
|
||||||
|
|
||||||
|
use std::str::FromStr as _;
|
||||||
|
|
||||||
|
use crate::StartError;
|
||||||
|
|
||||||
|
pub async fn init_database_pool(url: &str) -> Result<Pool<Sqlite>, StartError> {
|
||||||
|
Ok(SqlitePoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect_with(SqliteConnectOptions::from_str(url)?.pragma("foreign_keys", "1"))
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn migrate(pool: &Pool<Sqlite>) -> Result<(), StartError> {
|
||||||
|
sqlx::migrate!().run(pool).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user