remove old stacks

This commit is contained in:
2023-09-11 20:12:49 +02:00
parent 205eae2264
commit 4c850f6c0b
174 changed files with 30 additions and 13842 deletions

152
src/models/error.rs Normal file
View File

@@ -0,0 +1,152 @@
use std::fmt;
use sqlx::error::DatabaseError as _;
pub enum DatabaseError {
/// Errors we can receive **from** the database that are caused by connection
/// problems or schema problems (e.g. we get a return value that does not fit our enum,
/// or a wrongly formatted date)
Sql {
description: String,
},
Uuid {
description: String,
},
Enum {
description: String,
},
TimeParse {
description: String,
},
}
impl fmt::Display for DatabaseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Sql { description } => {
write!(f, "SQL error: {description}")
}
Self::Uuid { description } => {
write!(f, "UUID error: {description}")
}
Self::Enum { description } => {
write!(f, "Enum error: {description}")
}
Self::TimeParse { description } => {
write!(f, "Date parse error: {description}")
}
}
}
}
pub enum QueryError {
/// Errors that are caused by wrong input data, e.g. ids that cannot be found, or
/// inserts that violate unique constraints
Duplicate {
description: String,
},
NotFound {
description: String,
},
ReferenceNotFound {
description: String,
},
}
impl fmt::Display for QueryError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Duplicate { description } => {
write!(f, "Duplicate data entry: {description}")
}
Self::NotFound { description } => {
write!(f, "not found: {description}")
}
Self::ReferenceNotFound { description } => {
write!(f, "SQL foreign key reference was not found: {description}")
}
}
}
}
pub enum Error {
Database(DatabaseError),
Query(QueryError),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Database(error) => write!(f, "{error}"),
Self::Query(error) => write!(f, "{error}"),
}
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// defer to Display
write!(f, "SQL error: {self}")
}
}
impl From<uuid::Error> for Error {
fn from(value: uuid::Error) -> Self {
Error::Database(DatabaseError::Uuid {
description: value.to_string(),
})
}
}
impl From<time::error::Format> for Error {
fn from(value: time::error::Format) -> Self {
Error::Database(DatabaseError::TimeParse {
description: value.to_string(),
})
}
}
impl From<sqlx::Error> for Error {
fn from(value: sqlx::Error) -> Self {
match value {
sqlx::Error::RowNotFound => Error::Query(QueryError::NotFound {
description: value.to_string(),
}),
sqlx::Error::Database(ref error) => {
let sqlite_error = error.downcast_ref::<sqlx::sqlite::SqliteError>();
if let Some(code) = sqlite_error.code() {
match &*code {
// SQLITE_CONSTRAINT_FOREIGNKEY
"787" => Error::Query(QueryError::ReferenceNotFound {
description: "foreign key reference not found".to_string(),
}),
// SQLITE_CONSTRAINT_UNIQUE
"2067" => Error::Query(QueryError::Duplicate {
description: "item with unique constraint already exists".to_string(),
}),
_ => Error::Database(DatabaseError::Sql {
description: format!("got error with unknown code: {sqlite_error}"),
}),
}
} else {
Error::Database(DatabaseError::Sql {
description: format!("got error without code: {sqlite_error}"),
})
}
}
_ => Error::Database(DatabaseError::Sql {
description: format!("got unknown error: {value}"),
}),
}
}
}
impl From<time::error::Parse> for Error {
fn from(value: time::error::Parse) -> Self {
Error::Database(DatabaseError::TimeParse {
description: value.to_string(),
})
}
}
impl std::error::Error for Error {}

489
src/models/inventory.rs Normal file
View File

@@ -0,0 +1,489 @@
use super::Error;
use crate::{sqlite, Context};
use futures::{TryFutureExt, TryStreamExt};
use uuid::Uuid;
pub struct Inventory {
pub categories: Vec<Category>,
}
impl Inventory {
#[tracing::instrument]
pub async fn load(ctx: &Context, pool: &sqlite::Pool) -> Result<Self, Error> {
let user_id = ctx.user.id.to_string();
let mut categories = crate::query_all!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Select,
component: sqlite::Component::Inventory,
},
pool,
DbCategoryRow,
Category,
"SELECT
id,
name,
description
FROM inventory_items_categories
WHERE user_id = ?",
user_id
)
.await?;
for category in &mut categories {
category.populate_items(ctx, pool).await?;
}
Ok(Self { categories })
}
}
#[derive(Debug)]
pub struct Category {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub items: Option<Vec<Item>>,
}
pub struct DbCategoryRow {
pub id: String,
pub name: String,
pub description: Option<String>,
}
impl TryFrom<DbCategoryRow> for Category {
type Error = Error;
fn try_from(row: DbCategoryRow) -> Result<Self, Self::Error> {
Ok(Category {
id: Uuid::try_parse(&row.id)?,
name: row.name,
description: row.description,
items: None,
})
}
}
impl Category {
#[tracing::instrument]
pub async fn _find(
ctx: &Context,
pool: &sqlite::Pool,
id: Uuid,
) -> Result<Option<Category>, Error> {
let id_param = id.to_string();
let user_id = ctx.user.id.to_string();
crate::query_one!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Select,
component: sqlite::Component::Inventory,
},
pool,
DbCategoryRow,
Category,
"SELECT
id,
name,
description
FROM inventory_items_categories AS category
WHERE
category.id = ?
AND category.user_id = ?",
id_param,
user_id,
)
.await
}
#[tracing::instrument]
pub async fn save(ctx: &Context, pool: &sqlite::Pool, name: &str) -> Result<Uuid, Error> {
let id = Uuid::new_v4();
let id_param = id.to_string();
let user_id = ctx.user.id.to_string();
crate::execute!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Insert,
component: sqlite::Component::Inventory,
},
pool,
"INSERT INTO inventory_items_categories
(id, name, user_id)
VALUES
(?, ?, ?)",
id_param,
name,
user_id,
)
.await?;
Ok(id)
}
#[tracing::instrument]
pub fn items(&self) -> &Vec<Item> {
self.items
.as_ref()
.expect("you need to call populate_items()")
}
#[tracing::instrument]
pub fn total_weight(&self) -> i64 {
self.items().iter().map(|item| item.weight).sum()
}
#[tracing::instrument]
pub async fn populate_items(
&mut self,
ctx: &Context,
pool: &sqlite::Pool,
) -> Result<(), Error> {
let id = self.id.to_string();
let user_id = ctx.user.id.to_string();
let items = crate::query_all!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Select,
component: sqlite::Component::Inventory,
},
pool,
DbInventoryItemsRow,
Item,
"SELECT
id,
name,
weight,
description,
category_id
FROM inventory_items
WHERE
category_id = ?
AND user_id = ?",
id,
user_id,
)
.await?;
self.items = Some(items);
Ok(())
}
}
#[derive(Debug)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub comment: Option<String>,
}
#[derive(Debug)]
pub struct InventoryItem {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub weight: i64,
pub category: Category,
pub product: Option<Product>,
}
struct DbInventoryItemRow {
pub id: String,
pub name: String,
pub description: Option<String>,
pub weight: i64,
pub category_id: String,
pub category_name: String,
pub category_description: Option<String>,
pub product_id: Option<String>,
pub product_name: Option<String>,
pub product_description: Option<String>,
pub product_comment: Option<String>,
}
impl TryFrom<DbInventoryItemRow> for InventoryItem {
type Error = Error;
fn try_from(row: DbInventoryItemRow) -> Result<Self, Self::Error> {
Ok(InventoryItem {
id: Uuid::try_parse(&row.id)?,
name: row.name,
description: row.description,
weight: row.weight,
category: Category {
id: Uuid::try_parse(&row.category_id)?,
name: row.category_name,
description: row.category_description,
items: None,
},
product: row
.product_id
.map(|id| -> Result<Product, Error> {
Ok(Product {
id: Uuid::try_parse(&id)?,
name: row.product_name.unwrap(),
description: row.product_description,
comment: row.product_comment,
})
})
.transpose()?,
})
}
}
impl InventoryItem {
#[tracing::instrument]
pub async fn find(ctx: &Context, pool: &sqlite::Pool, id: Uuid) -> Result<Option<Self>, Error> {
let id_param = id.to_string();
let user_id = ctx.user.id.to_string();
crate::query_one!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Select,
component: sqlite::Component::Inventory,
},
pool,
DbInventoryItemRow,
Self,
"SELECT
item.id AS id,
item.name AS name,
item.description AS description,
weight,
category.id AS category_id,
category.name AS category_name,
category.description AS category_description,
product.id AS product_id,
product.name AS product_name,
product.description AS product_description,
product.comment AS product_comment
FROM inventory_items AS item
INNER JOIN inventory_items_categories as category
ON item.category_id = category.id
LEFT JOIN inventory_products AS product
ON item.product_id = product.id
WHERE
item.id = ?
AND item.user_id = ?",
id_param,
user_id,
)
.await
}
#[tracing::instrument]
pub async fn name_exists(
ctx: &Context,
pool: &sqlite::Pool,
name: &str,
) -> Result<bool, Error> {
let user_id = ctx.user.id.to_string();
crate::query_exists!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Select,
component: sqlite::Component::Inventory,
},
pool,
"SELECT id
FROM inventory_items
WHERE
name = ?
AND user_id = ?",
name,
user_id
)
.await
}
#[tracing::instrument]
pub async fn delete(ctx: &Context, pool: &sqlite::Pool, id: Uuid) -> Result<bool, Error> {
let id_param = id.to_string();
let user_id = ctx.user.id.to_string();
let results = crate::execute!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Delete,
component: sqlite::Component::Inventory,
},
pool,
"DELETE FROM inventory_items
WHERE
id = ?
AND user_id = ?",
id_param,
user_id,
)
.await?;
Ok(results.rows_affected() != 0)
}
#[tracing::instrument]
pub async fn update(
ctx: &Context,
pool: &sqlite::Pool,
id: Uuid,
name: &str,
weight: u32,
) -> Result<Uuid, Error> {
let user_id = ctx.user.id.to_string();
let weight = i64::try_from(weight).unwrap();
let id_param = id.to_string();
crate::execute_returning_uuid!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Update,
component: sqlite::Component::Inventory,
},
pool,
"UPDATE inventory_items AS item
SET
name = ?,
weight = ?
WHERE
item.id = ?
AND item.user_id = ?
RETURNING inventory_items.category_id AS id
",
name,
weight,
id_param,
user_id,
)
.await
}
#[tracing::instrument]
pub async fn save(
ctx: &Context,
pool: &sqlite::Pool,
name: &str,
category_id: Uuid,
weight: u32,
) -> Result<Uuid, Error> {
let id = Uuid::new_v4();
let id_param = id.to_string();
let user_id = ctx.user.id.to_string();
let category_id_param = category_id.to_string();
crate::execute!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Insert,
component: sqlite::Component::Inventory,
},
pool,
"INSERT INTO inventory_items
(id, name, description, weight, category_id, user_id)
VALUES
(?, ?, ?, ?, ?, ?)",
id_param,
name,
"",
weight,
category_id_param,
user_id,
)
.await?;
Ok(id)
}
#[tracing::instrument]
pub async fn get_category_max_weight(
ctx: &Context,
pool: &sqlite::Pool,
category_id: Uuid,
) -> Result<i64, Error> {
let user_id = ctx.user.id.to_string();
let category_id_param = category_id.to_string();
let weight = crate::execute_returning!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Select,
component: sqlite::Component::Inventory,
},
pool,
"
SELECT COALESCE(MAX(i_item.weight), 0) as weight
FROM inventory_items_categories as category
INNER JOIN inventory_items as i_item
ON i_item.category_id = category.id
WHERE
category_id = ?
AND category.user_id = ?
",
i64,
|row| i64::from(row.weight),
category_id_param,
user_id,
)
.await?;
Ok(weight)
}
}
#[derive(Debug)]
pub struct Item {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub weight: i64,
pub category_id: Uuid,
}
pub struct DbInventoryItemsRow {
pub id: String,
pub name: String,
pub weight: i64,
pub description: Option<String>,
pub category_id: String,
}
impl TryFrom<DbInventoryItemsRow> for Item {
type Error = Error;
fn try_from(row: DbInventoryItemsRow) -> Result<Self, Self::Error> {
Ok(Item {
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 Item {
#[tracing::instrument]
pub async fn _get_category_total_picked_weight(
ctx: &Context,
pool: &sqlite::Pool,
category_id: Uuid,
) -> Result<i64, Error> {
let user_id = ctx.user.id.to_string();
let category_id_param = category_id.to_string();
crate::execute_returning!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Select,
component: sqlite::Component::Inventory,
},
pool,
"
SELECT COALESCE(SUM(i_item.weight), 0) as weight
FROM inventory_items_categories as category
INNER JOIN inventory_items as i_item
ON i_item.category_id = category.id
INNER JOIN trips_items as t_item
ON i_item.id = t_item.item_id
WHERE
category_id = ?
AND category.user_id = ?
AND t_item.pick = 1
",
i64,
|row| i64::from(row.weight),
category_id_param,
user_id,
)
.await
}
}

13
src/models/mod.rs Normal file
View File

@@ -0,0 +1,13 @@
pub mod inventory;
pub mod trips;
pub mod user;
mod error;
pub use error::{DatabaseError, Error, QueryError};
mod consts {
use time::{format_description::FormatItem, macros::format_description};
pub(super) const DATE_FORMAT: &[FormatItem<'static>] =
format_description!("[year]-[month]-[day]");
}

1489
src/models/trips.rs Normal file

File diff suppressed because it is too large Load Diff

81
src/models/user.rs Normal file
View File

@@ -0,0 +1,81 @@
use super::Error;
use uuid::Uuid;
use crate::sqlite;
#[derive(Debug, Clone)]
pub struct User {
pub id: Uuid,
pub username: String,
pub fullname: String,
}
#[derive(Debug)]
pub struct NewUser<'a> {
pub username: &'a str,
pub fullname: &'a str,
}
#[derive(Debug)]
pub struct DbUserRow {
id: String,
username: String,
fullname: String,
}
impl TryFrom<DbUserRow> for User {
type Error = Error;
fn try_from(row: DbUserRow) -> Result<Self, Self::Error> {
Ok(User {
id: Uuid::try_parse(&row.id)?,
username: row.username,
fullname: row.fullname,
})
}
}
impl User {
#[tracing::instrument]
pub async fn find_by_name(
pool: &sqlx::Pool<sqlx::Sqlite>,
name: &str,
) -> Result<Option<Self>, Error> {
crate::query_one!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Select,
component: sqlite::Component::User,
},
pool,
DbUserRow,
Self,
"SELECT id,username,fullname FROM users WHERE username = ?",
name
)
.await
}
}
#[tracing::instrument]
pub async fn create(pool: &sqlite::Pool, user: NewUser<'_>) -> Result<Uuid, Error> {
let id = Uuid::new_v4();
let id_param = id.to_string();
crate::execute!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Insert,
component: sqlite::Component::User,
},
pool,
"INSERT INTO users
(id, username, fullname)
VALUES
(?, ?, ?)",
id_param,
user.username,
user.fullname
)
.await?;
Ok(id)
}