From 08cf86a44a9a7c513cd12cbc4a0bac7c029b9ded Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Wed, 8 Oct 2025 11:54:04 +0200 Subject: feat(crates/rocie-server/unit-property): Init --- crates/rocie-server/src/api/get/mod.rs | 3 + crates/rocie-server/src/api/get/unit.rs | 36 +++++-- crates/rocie-server/src/api/get/unit_property.rs | 68 ++++++++++++ crates/rocie-server/src/api/set/barcode.rs | 7 +- crates/rocie-server/src/api/set/mod.rs | 2 + crates/rocie-server/src/api/set/product.rs | 15 ++- crates/rocie-server/src/api/set/unit.rs | 13 ++- crates/rocie-server/src/api/set/unit_property.rs | 47 +++++++++ crates/rocie-server/src/error.rs | 1 - crates/rocie-server/src/main.rs | 3 + .../rocie-server/src/storage/migrate/sql/0->1.sql | 56 +++++++++- crates/rocie-server/src/storage/sql/barcode.rs | 9 +- .../src/storage/sql/get/barcode/mod.rs | 4 +- crates/rocie-server/src/storage/sql/get/mod.rs | 1 + .../src/storage/sql/get/product/mod.rs | 11 +- .../src/storage/sql/get/product_amount/mod.rs | 5 +- .../rocie-server/src/storage/sql/get/unit/mod.rs | 11 +- .../src/storage/sql/get/unit_property/mod.rs | 117 +++++++++++++++++++++ .../src/storage/sql/insert/barcode/mod.rs | 9 +- crates/rocie-server/src/storage/sql/insert/mod.rs | 1 + .../src/storage/sql/insert/product/mod.rs | 17 ++- .../src/storage/sql/insert/unit/mod.rs | 22 ++-- .../src/storage/sql/insert/unit_property/mod.rs | 106 +++++++++++++++++++ crates/rocie-server/src/storage/sql/mod.rs | 95 ++++++++++++++++- crates/rocie-server/src/storage/sql/product.rs | 88 ++++------------ .../rocie-server/src/storage/sql/product_amount.rs | 2 +- crates/rocie-server/src/storage/sql/unit.rs | 100 ++++++------------ .../rocie-server/src/storage/sql/unit_property.rs | 23 ++++ 28 files changed, 685 insertions(+), 187 deletions(-) create mode 100644 crates/rocie-server/src/api/get/unit_property.rs create mode 100644 crates/rocie-server/src/api/set/unit_property.rs delete mode 100644 crates/rocie-server/src/error.rs create mode 100644 crates/rocie-server/src/storage/sql/get/unit_property/mod.rs create mode 100644 crates/rocie-server/src/storage/sql/insert/unit_property/mod.rs create mode 100644 crates/rocie-server/src/storage/sql/unit_property.rs (limited to 'crates') diff --git a/crates/rocie-server/src/api/get/mod.rs b/crates/rocie-server/src/api/get/mod.rs index 21684af..e21a120 100644 --- a/crates/rocie-server/src/api/get/mod.rs +++ b/crates/rocie-server/src/api/get/mod.rs @@ -3,11 +3,14 @@ use actix_web::web; pub(crate) mod inventory; pub(crate) mod product; pub(crate) mod unit; +pub(crate) mod unit_property; pub(crate) fn register_paths(cfg: &mut web::ServiceConfig) { cfg.service(product::product_by_id) .service(product::products) .service(unit::units) .service(unit::unit_by_id) + .service(unit_property::unit_properties) + .service(unit_property::unit_property_by_id) .service(inventory::amount_by_id); } diff --git a/crates/rocie-server/src/api/get/unit.rs b/crates/rocie-server/src/api/get/unit.rs index 4854ea3..73aa626 100644 --- a/crates/rocie-server/src/api/get/unit.rs +++ b/crates/rocie-server/src/api/get/unit.rs @@ -5,8 +5,16 @@ use crate::{app::App, storage::sql::unit::{Unit, UnitId, UnitIdStub}}; /// Return all registered units #[utoipa::path( responses( - (status = OK, description = "All units founds", body = Vec), - (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String) + ( + status = OK, + description = "All units founds", + body = Vec + ), + ( + status = INTERNAL_SERVER_ERROR, + description = "Server encountered error", + body = String + ) ), )] #[get("/units/")] @@ -19,12 +27,26 @@ pub(crate) async fn units(app: web::Data) -> Result { /// Get Unit by id #[utoipa::path( responses( - (status = OK, description = "Unit found from database", body = Unit), - (status = NOT_FOUND, description = "Unit not found in database"), - (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String) + ( + status = OK, + description = "Unit found from database", + body = Unit + ), + ( + status = NOT_FOUND, + description = "Unit not found in database" + ), + ( + status = INTERNAL_SERVER_ERROR, + description = "Server encountered error", + body = String + ) ), params( - ("id" = UnitId, description = "Unit id" ), + ( + "id" = UnitId, + description = "Unit id" + ), ) )] #[get("/unit/{id}")] @@ -35,7 +57,7 @@ pub(crate) async fn unit_by_id( let id = id.into_inner(); match Unit::from_id(&app, id.into()).await? { - Some(product) => Ok(HttpResponse::Ok().json(product)), + Some(unit) => Ok(HttpResponse::Ok().json(unit)), None => Ok(HttpResponse::NotFound().finish()), } } diff --git a/crates/rocie-server/src/api/get/unit_property.rs b/crates/rocie-server/src/api/get/unit_property.rs new file mode 100644 index 0000000..3160480 --- /dev/null +++ b/crates/rocie-server/src/api/get/unit_property.rs @@ -0,0 +1,68 @@ +use actix_web::{HttpResponse, Responder, Result, get, web}; + +use crate::{ + app::App, + storage::sql::{ + unit_property::{UnitProperty, UnitPropertyId, UnitPropertyIdStub}, + }, +}; + +/// Return all registered unit properties +#[utoipa::path( + responses( + ( + status = OK, + description = "All unit properties founds", + body = Vec + ), + ( + status = INTERNAL_SERVER_ERROR, + description = "Server encountered error", + body = String + ) + ), +)] +#[get("/unit-properties/")] +pub(crate) async fn unit_properties(app: web::Data) -> Result { + let all = UnitProperty::get_all(&app).await?; + + Ok(HttpResponse::Ok().json(all)) +} + +/// Get Unit property by id +#[utoipa::path( + responses( + ( + status = OK, + description = "Unit property found from database", + body = UnitProperty + ), + ( + status = NOT_FOUND, + description = "Unit Property not found in database" + ), + ( + status = INTERNAL_SERVER_ERROR, + description = "Server encountered error", + body = String + ) + ), + params( + ( + "id" = UnitPropertyId, + description = "Unit Property id" + ), + ) +)] +#[get("/unit-property/{id}")] +pub(crate) async fn unit_property_by_id( + app: web::Data, + id: web::Path, +) -> Result { + let id = id.into_inner(); + + match UnitProperty::from_id(&app, id.into()).await? { + Some(unit_property) => Ok(HttpResponse::Ok().json(unit_property)), + None => Ok(HttpResponse::NotFound().finish()), + } +} diff --git a/crates/rocie-server/src/api/set/barcode.rs b/crates/rocie-server/src/api/set/barcode.rs index 60b1650..29eac9e 100644 --- a/crates/rocie-server/src/api/set/barcode.rs +++ b/crates/rocie-server/src/api/set/barcode.rs @@ -4,8 +4,9 @@ use log::debug; use crate::{ app::App, storage::sql::{ - barcode::{Barcode, BarcodeId, BarcodeIdStub, UnitAmount}, + barcode::{Barcode, BarcodeId, BarcodeIdStub}, insert::Operations, + unit::UnitAmount, }, }; @@ -86,7 +87,9 @@ pub(crate) async fn consume_barcode( match barcode { Some(barcode) => { - barcode.consume(&app, unit_amount.into_inner(), &mut ops).await?; + barcode + .consume(&app, unit_amount.into_inner(), &mut ops) + .await?; ops.apply(&app).await?; diff --git a/crates/rocie-server/src/api/set/mod.rs b/crates/rocie-server/src/api/set/mod.rs index a7ddab5..5dd0219 100644 --- a/crates/rocie-server/src/api/set/mod.rs +++ b/crates/rocie-server/src/api/set/mod.rs @@ -3,11 +3,13 @@ use actix_web::web; pub(crate) mod barcode; pub(crate) mod product; pub(crate) mod unit; +pub(crate) mod unit_property; pub(crate) fn register_paths(cfg: &mut web::ServiceConfig) { cfg.service(product::register_product) .service(product::associate_barcode) .service(unit::register_unit) + .service(unit_property::register_unit_property) .service(barcode::consume_barcode) .service(barcode::buy_barcode); } diff --git a/crates/rocie-server/src/api/set/product.rs b/crates/rocie-server/src/api/set/product.rs index d347ee7..7372d23 100644 --- a/crates/rocie-server/src/api/set/product.rs +++ b/crates/rocie-server/src/api/set/product.rs @@ -9,13 +9,22 @@ use crate::{ insert::Operations, product::{Product, ProductId, ProductIdStub}, unit::Unit, + unit_property::UnitPropertyId, }, }; #[derive(Deserialize, ToSchema)] struct ProductStub { + /// The name of the product name: String, + + /// The Unit Property to use for this product. + unit_property: UnitPropertyId, + + /// A description. description: Option, + + /// A parent of this product, otherwise the parent will be the root of the parent tree. parent: Option, } @@ -46,6 +55,7 @@ pub(crate) async fn register_product( product_stub.name.clone(), product_stub.description.clone(), product_stub.parent, + product_stub.unit_property, &mut ops, ); @@ -77,7 +87,10 @@ pub(crate) async fn register_product( ) ), params ( - ("id" = ProductId, description = "The id of the product to associated the barcode with"), + ( + "id" = ProductId, + description = "The id of the product to associated the barcode with" + ), ), request_body = Barcode, )] diff --git a/crates/rocie-server/src/api/set/unit.rs b/crates/rocie-server/src/api/set/unit.rs index 4281e05..5f39c8f 100644 --- a/crates/rocie-server/src/api/set/unit.rs +++ b/crates/rocie-server/src/api/set/unit.rs @@ -2,13 +2,21 @@ use actix_web::{HttpResponse, Responder, Result, post, web}; use serde::Deserialize; use utoipa::ToSchema; -use crate::{app::App, storage::sql::{insert::Operations, unit::{Unit, UnitId}}}; +use crate::{ + app::App, + storage::sql::{ + insert::Operations, + unit::{Unit, UnitId}, + unit_property::UnitPropertyId, + }, +}; #[derive(Deserialize, ToSchema)] struct UnitStub { full_name_plural: String, full_name_singular: String, short_name: String, + unit_property: UnitPropertyId, description: Option, } @@ -17,7 +25,7 @@ struct UnitStub { responses( ( status = 200, - description = "Product successfully registered in database", + description = "Unit successfully registered in database", body = UnitId, ), ( @@ -40,6 +48,7 @@ pub(crate) async fn register_unit( unit.full_name_plural.clone(), unit.short_name.clone(), unit.description.clone(), + unit.unit_property, &mut ops, ); diff --git a/crates/rocie-server/src/api/set/unit_property.rs b/crates/rocie-server/src/api/set/unit_property.rs new file mode 100644 index 0000000..9915e84 --- /dev/null +++ b/crates/rocie-server/src/api/set/unit_property.rs @@ -0,0 +1,47 @@ +use actix_web::{HttpResponse, Responder, Result, post, web}; +use serde::Deserialize; +use utoipa::ToSchema; + +use crate::{ + app::App, + storage::sql::{ + insert::Operations, + unit_property::{UnitProperty, UnitPropertyId}, + }, +}; + +#[derive(Deserialize, ToSchema)] +struct UnitPropertyStub { + name: String, + description: Option, +} + +/// Register an Unit Property +#[utoipa::path( + responses( + ( + status = 200, + description = "Unit property successfully registered in database", + body = UnitPropertyId, + ), + ( + status = INTERNAL_SERVER_ERROR, + description = "Server encountered error", + body = String, + ) + ), + request_body = UnitPropertyStub, +)] +#[post("/unit-property/new")] +pub(crate) async fn register_unit_property( + app: web::Data, + unit: web::Json, +) -> Result { + let mut ops = Operations::new("register unit property"); + + let unit = UnitProperty::register(unit.name.clone(), unit.description.clone(), &mut ops); + + ops.apply(&app).await?; + + Ok(HttpResponse::Ok().json(unit.id)) +} diff --git a/crates/rocie-server/src/error.rs b/crates/rocie-server/src/error.rs deleted file mode 100644 index 8b13789..0000000 --- a/crates/rocie-server/src/error.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/rocie-server/src/main.rs b/crates/rocie-server/src/main.rs index 8e71034..af36c9e 100644 --- a/crates/rocie-server/src/main.rs +++ b/crates/rocie-server/src/main.rs @@ -22,10 +22,13 @@ async fn main() -> Result<(), std::io::Error> { api::get::product::products, api::get::unit::units, api::get::unit::unit_by_id, + api::get::unit_property::unit_property_by_id, + api::get::unit_property::unit_properties, api::get::inventory::amount_by_id, api::set::product::register_product, api::set::product::associate_barcode, api::set::unit::register_unit, + api::set::unit_property::register_unit_property, api::set::barcode::buy_barcode, api::set::barcode::consume_barcode, ), diff --git a/crates/rocie-server/src/storage/migrate/sql/0->1.sql b/crates/rocie-server/src/storage/migrate/sql/0->1.sql index fd48c68..7f08738 100644 --- a/crates/rocie-server/src/storage/migrate/sql/0->1.sql +++ b/crates/rocie-server/src/storage/migrate/sql/0->1.sql @@ -57,10 +57,12 @@ END; CREATE TABLE products ( id TEXT UNIQUE NOT NULL PRIMARY KEY, - name TEXT NOT NULL, + name TEXT UNIQUE NOT NULL, description TEXT, parent TEXT DEFAULT NULL, - FOREIGN KEY(parent) REFERENCES parents(id) + unit_property TEXT NOT NULL, + FOREIGN KEY(parent) REFERENCES parents(id), + FOREIGN KEY(unit_property) REFERENCES unit_properties(id) ) STRICT; CREATE TABLE barcodes ( @@ -71,12 +73,41 @@ CREATE TABLE barcodes ( FOREIGN KEY(product_id) REFERENCES products(id), FOREIGN KEY(unit) REFERENCES units(id) ) STRICT; +CREATE TRIGGER unit_has_property_of_product_update +BEFORE UPDATE OF product_id,unit ON barcodes +FOR EACH ROW +BEGIN + SELECT RAISE(FAIL, "Used different unit_property for product_id and unit") + FROM products, units + WHERE NEW.product_id = products.id + AND NEW.unit = units.id + AND units.unit_property != products.unit_property; +END; + +CREATE TRIGGER unit_has_property_of_product_insert +BEFORE INSERT ON barcodes +FOR EACH ROW +BEGIN + SELECT RAISE(FAIL, "Used different unit_property for product_id and unit") + FROM products, units + WHERE NEW.product_id = products.id + AND NEW.unit = units.id + AND units.unit_property != products.unit_property; +END; CREATE TABLE units ( id TEXT UNIQUE NOT NULL PRIMARY KEY, + unit_property TEXT UNIQUE NOT NULL, full_name_singular TEXT UNIQUE NOT NULL, full_name_plural TEXT UNIQUE NOT NULL, short_name TEXT UNIQUE NOT NULL, + description TEXT, + FOREIGN KEY(unit_property) REFERENCES unit_properties(id) +) STRICT; + +CREATE TABLE unit_properties ( + id TEXT UNIQUE NOT NULL PRIMARY KEY, + name TEXT UNIQUE NOT NULL, description TEXT ) STRICT; @@ -90,6 +121,27 @@ CREATE TABLE unit_conversions ( FOREIGN KEY(from_unit) REFERENCES units(id), FOREIGN KEY(to_unit) REFERENCES units(id) ) STRICT; +CREATE TRIGGER both_units_have_same_unit_property_update +BEFORE UPDATE OF from_unit,to_unit ON unit_conversions +FOR EACH ROW +BEGIN + SELECT RAISE(FAIL, "Used not the same unit_property") + FROM units, units AS units2 + WHERE NEW.from_unit = units.id + AND NEW.to_unit = units2.id + AND units.unit_property != units2.unit_property; +END; + +CREATE TRIGGER both_units_have_same_unit_property_insert +BEFORE INSERT ON unit_conversions +FOR EACH ROW +BEGIN + SELECT RAISE(FAIL, "Used not the same unit_property") + FROM units, units AS units2 + WHERE NEW.from_unit = units.id + AND NEW.to_unit = units2.id + AND units.unit_property != units2.unit_property; +END; -- Log of all the applied operations to this db. CREATE TABLE txn_log ( diff --git a/crates/rocie-server/src/storage/sql/barcode.rs b/crates/rocie-server/src/storage/sql/barcode.rs index 1c3c55a..f15d399 100644 --- a/crates/rocie-server/src/storage/sql/barcode.rs +++ b/crates/rocie-server/src/storage/sql/barcode.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::storage::sql::unit::UnitId; +use crate::storage::sql::unit::UnitAmount; #[derive(ToSchema, Debug, Clone, Serialize, Deserialize)] pub(crate) struct Barcode { @@ -41,10 +41,3 @@ impl From for BarcodeId { Self { value: value.value } } } - -#[derive(ToSchema, Debug, Clone, Copy, Serialize, Deserialize)] -pub(crate) struct UnitAmount { - #[schema(minimum = 0)] - pub(crate) value: u32, - pub(crate) unit: UnitId, -} diff --git a/crates/rocie-server/src/storage/sql/get/barcode/mod.rs b/crates/rocie-server/src/storage/sql/get/barcode/mod.rs index 7b656b1..4eba105 100644 --- a/crates/rocie-server/src/storage/sql/get/barcode/mod.rs +++ b/crates/rocie-server/src/storage/sql/get/barcode/mod.rs @@ -1,8 +1,8 @@ use crate::{ app::App, storage::sql::{ - barcode::{Barcode, BarcodeId, UnitAmount}, - unit::UnitId, + barcode::{Barcode, BarcodeId}, + unit::{UnitAmount, UnitId}, }, }; diff --git a/crates/rocie-server/src/storage/sql/get/mod.rs b/crates/rocie-server/src/storage/sql/get/mod.rs index 048cb3d..62047b8 100644 --- a/crates/rocie-server/src/storage/sql/get/mod.rs +++ b/crates/rocie-server/src/storage/sql/get/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod product; pub(crate) mod product_amount; pub(crate) mod unit; +pub(crate) mod unit_property; pub(crate) mod barcode; diff --git a/crates/rocie-server/src/storage/sql/get/product/mod.rs b/crates/rocie-server/src/storage/sql/get/product/mod.rs index 541f388..01047b2 100644 --- a/crates/rocie-server/src/storage/sql/get/product/mod.rs +++ b/crates/rocie-server/src/storage/sql/get/product/mod.rs @@ -1,9 +1,10 @@ use crate::{ app::App, storage::sql::{ - barcode::{Barcode, BarcodeId, UnitAmount}, + barcode::{Barcode, BarcodeId}, product::{Product, ProductId}, - unit::UnitId, + unit::{UnitAmount, UnitId}, + unit_property::UnitPropertyId, }, }; @@ -13,7 +14,7 @@ impl Product { pub(crate) async fn from_id(app: &App, id: ProductId) -> Result, from_id::Error> { let record = query!( " - SELECT name, description, parent + SELECT name, unit_property, description, parent FROM products WHERE id = ? ", @@ -25,6 +26,7 @@ impl Product { if let Some(record) = record { Ok(Some(Self { id, + unit_property: UnitPropertyId::from_db(&record.unit_property), name: record.name, description: record.description, associated_bar_codes: vec![], // todo @@ -37,7 +39,7 @@ impl Product { pub(crate) async fn get_all(app: &App) -> Result, get_all::Error> { let records = query!( " - SELECT id, name, description, parent + SELECT id, unit_property, name, description, parent FROM products " ) @@ -59,6 +61,7 @@ impl Product { all.push(Self { id: ProductId::from_db(&record.id), + unit_property: UnitPropertyId::from_db(&record.unit_property), name: record.name, description: record.description, associated_bar_codes: barcodes diff --git a/crates/rocie-server/src/storage/sql/get/product_amount/mod.rs b/crates/rocie-server/src/storage/sql/get/product_amount/mod.rs index 7700274..f82c2a0 100644 --- a/crates/rocie-server/src/storage/sql/get/product_amount/mod.rs +++ b/crates/rocie-server/src/storage/sql/get/product_amount/mod.rs @@ -1,8 +1,6 @@ use crate::{ app::App, - storage::sql::{ - barcode::UnitAmount, product::ProductId, product_amount::ProductAmount, unit::UnitId, - }, + storage::sql::{product::ProductId, product_amount::ProductAmount, unit::{UnitAmount, UnitId}}, }; use sqlx::query; @@ -20,6 +18,7 @@ SELECT FROM barcodes JOIN products ON products.id = ? JOIN buys ON buys.barcode_id = barcodes.id +WHERE barcodes.product_id = products.id GROUP BY barcodes.unit; "#, product_id diff --git a/crates/rocie-server/src/storage/sql/get/unit/mod.rs b/crates/rocie-server/src/storage/sql/get/unit/mod.rs index 6c2bbcc..6f5d297 100644 --- a/crates/rocie-server/src/storage/sql/get/unit/mod.rs +++ b/crates/rocie-server/src/storage/sql/get/unit/mod.rs @@ -1,6 +1,9 @@ use crate::{ app::App, - storage::sql::unit::{Unit, UnitId}, + storage::sql::{ + unit::{Unit, UnitId}, + unit_property::UnitPropertyId, + }, }; use sqlx::query; @@ -9,7 +12,7 @@ impl Unit { pub(crate) async fn get_all(app: &App) -> Result, get_all::Error> { let records = query!( " - SELECT id, full_name_singular, full_name_plural, short_name, description + SELECT id, unit_property, full_name_singular, full_name_plural, short_name, description FROM units " ) @@ -20,6 +23,7 @@ impl Unit { .into_iter() .map(|record| Self { id: UnitId::from_db(&record.id), + unit_property: UnitPropertyId::from_db(&record.unit_property), full_name_singular: record.full_name_singular, full_name_plural: record.full_name_plural, short_name: record.short_name, @@ -31,7 +35,7 @@ impl Unit { pub(crate) async fn from_id(app: &App, id: UnitId) -> Result, from_id::Error> { let record = query!( " - SELECT full_name_singular, full_name_plural, short_name, description + SELECT full_name_singular, unit_property, full_name_plural, short_name, description FROM units WHERE id = ? ", @@ -43,6 +47,7 @@ impl Unit { if let Some(record) = record { Ok(Some(Self { id, + unit_property: UnitPropertyId::from_db(&record.unit_property), full_name_singular: record.full_name_singular, full_name_plural: record.full_name_plural, short_name: record.short_name, diff --git a/crates/rocie-server/src/storage/sql/get/unit_property/mod.rs b/crates/rocie-server/src/storage/sql/get/unit_property/mod.rs new file mode 100644 index 0000000..be24181 --- /dev/null +++ b/crates/rocie-server/src/storage/sql/get/unit_property/mod.rs @@ -0,0 +1,117 @@ +use crate::{ + app::App, + storage::sql::{ + unit::UnitId, + unit_property::{UnitProperty, UnitPropertyId}, + }, +}; + +use sqlx::query; + +impl UnitProperty { + pub(crate) async fn get_all(app: &App) -> Result, get_all::Error> { + let records = query!( + " + SELECT id, name, description + FROM unit_properties +" + ) + .fetch_all(&app.db) + .await?; + + let mut output = Vec::with_capacity(records.len()); + for record in records { + let units = query!( + " + SELECT id + FROM units + WHERE units.unit_property = ? + ", + record.id + ) + .fetch_all(&app.db) + .await?; + + let units = units + .into_iter() + .map(|record| UnitId::from_db(&record.id)) + .collect(); + + output.push(Self { + id: UnitPropertyId::from_db(&record.id), + units, + name: record.name, + description: record.description, + }) + } + + Ok(output) + } + + pub(crate) async fn from_id( + app: &App, + id: UnitPropertyId, + ) -> Result, from_id::Error> { + let record = query!( + " + SELECT name, description + FROM unit_properties + WHERE id = ? +", + id + ) + .fetch_optional(&app.db) + .await?; + + if let Some(record) = record { + let units = query!( + " + SELECT id + FROM units + WHERE units.unit_property = ? + ", + id + ) + .fetch_all(&app.db) + .await?; + + let units = units + .into_iter() + .map(|record| UnitId::from_db(&record.id)) + .collect(); + + Ok(Some(Self { + id, + units, + name: record.name, + description: record.description, + })) + } else { + Ok(None) + } + } +} + +pub(crate) mod get_all { + use actix_web::ResponseError; + + #[derive(thiserror::Error, Debug)] + pub(crate) enum Error { + #[error("Failed to execute the sql query")] + SqlError(#[from] sqlx::Error), + } + + impl ResponseError for Error {} +} + +pub(crate) mod from_id { + use actix_web::ResponseError; + + #[derive(thiserror::Error, Debug)] + pub(crate) enum Error { + #[error("Failed to execute the sql query")] + SqlError(#[from] sqlx::Error), + } + + impl ResponseError for Error {} +} diff --git a/crates/rocie-server/src/storage/sql/insert/barcode/mod.rs b/crates/rocie-server/src/storage/sql/insert/barcode/mod.rs index 62a2e11..11707b9 100644 --- a/crates/rocie-server/src/storage/sql/insert/barcode/mod.rs +++ b/crates/rocie-server/src/storage/sql/insert/barcode/mod.rs @@ -9,9 +9,9 @@ use crate::{ storage::{ migrate::get_current_date, sql::{ - barcode::{Barcode, BarcodeId, UnitAmount}, + barcode::{Barcode, BarcodeId}, insert::{Operations, Transactionable}, - unit::Unit, + unit::{Unit, UnitAmount}, }, }, }; @@ -208,10 +208,7 @@ pub(crate) mod consume { use crate::storage::{ self, - sql::{ - barcode::UnitAmount, - unit::{Unit, UnitId}, - }, + sql::unit::{Unit, UnitAmount, UnitId}, }; #[derive(thiserror::Error, Debug)] diff --git a/crates/rocie-server/src/storage/sql/insert/mod.rs b/crates/rocie-server/src/storage/sql/insert/mod.rs index e6728d9..3b2d702 100644 --- a/crates/rocie-server/src/storage/sql/insert/mod.rs +++ b/crates/rocie-server/src/storage/sql/insert/mod.rs @@ -10,6 +10,7 @@ use sqlx::{SqliteConnection, query}; pub(crate) mod barcode; pub(crate) mod product; pub(crate) mod unit; +pub(crate) mod unit_property; pub(crate) trait Transactionable: Sized + std::fmt::Debug + Serialize + DeserializeOwned diff --git a/crates/rocie-server/src/storage/sql/insert/product/mod.rs b/crates/rocie-server/src/storage/sql/insert/product/mod.rs index e14d3f4..d762e9b 100644 --- a/crates/rocie-server/src/storage/sql/insert/product/mod.rs +++ b/crates/rocie-server/src/storage/sql/insert/product/mod.rs @@ -6,6 +6,7 @@ use crate::storage::sql::{ barcode::Barcode, insert::{Operations, Transactionable}, product::{Product, ProductId}, + unit_property::UnitPropertyId, }; #[derive(Debug, Deserialize, Serialize)] @@ -15,6 +16,7 @@ pub(crate) enum Operation { name: String, description: Option, parent: Option, + unit_property: UnitPropertyId, }, AssociateBarcode { id: ProductId, @@ -33,13 +35,15 @@ impl Transactionable for Operation { name, description, parent, + unit_property, } => { query!( " - INSERT INTO products (id, name, description, parent) - VALUES (?,?,?,?) + INSERT INTO products (id, unit_property, name, description, parent) + VALUES (?,?,?,?,?) ", id, + unit_property, name, description, parent @@ -75,17 +79,19 @@ impl Transactionable for Operation { id, name, description, + unit_property, parent, } => { query!( " DELETE FROM products - WHERE id = ? AND name = ? AND description = ? AND parent = ?; + WHERE id = ? AND name = ? AND description = ? AND parent = ? AND unit_property = ?; ", id, name, description, - parent + parent, + unit_property ) .execute(txn) .await?; @@ -133,6 +139,7 @@ impl Product { name: String, description: Option, parent: Option, + unit_property: UnitPropertyId, ops: &mut Operations, ) -> Self { let id = ProductId::from(Uuid::new_v4()); @@ -141,6 +148,7 @@ impl Product { id, name: name.clone(), description: description.clone(), + unit_property, parent, }); @@ -148,6 +156,7 @@ impl Product { id, name, description, + unit_property, associated_bar_codes: vec![], } } diff --git a/crates/rocie-server/src/storage/sql/insert/unit/mod.rs b/crates/rocie-server/src/storage/sql/insert/unit/mod.rs index ba08487..815cb1e 100644 --- a/crates/rocie-server/src/storage/sql/insert/unit/mod.rs +++ b/crates/rocie-server/src/storage/sql/insert/unit/mod.rs @@ -2,7 +2,11 @@ use serde::{Deserialize, Serialize}; use sqlx::query; use uuid::Uuid; -use crate::storage::sql::{insert::{Operations, Transactionable}, unit::{Unit, UnitId}}; +use crate::storage::sql::{ + insert::{Operations, Transactionable}, + unit::{Unit, UnitId}, + unit_property::UnitPropertyId, +}; #[derive(Debug, Deserialize, Serialize)] pub(crate) enum Operation { @@ -11,6 +15,7 @@ pub(crate) enum Operation { full_name_singular: String, full_name_plural: String, short_name: String, + unit_property: UnitPropertyId, description: Option, }, } @@ -27,13 +32,14 @@ impl Transactionable for Operation { full_name_plural, short_name, description, + unit_property, } => { query!( " - INSERT INTO units (id, full_name_singular, full_name_plural, short_name, description) - VALUES (?,?,?,?,?) + INSERT INTO units (id, unit_property, full_name_singular, full_name_plural, short_name, description) + VALUES (?,?,?,?,?,?) ", - id, full_name_singular, full_name_plural, short_name, description, + id, unit_property, full_name_singular, full_name_plural, short_name, description, ) .execute(txn) .await?; @@ -50,13 +56,14 @@ impl Transactionable for Operation { full_name_plural, short_name, description, + unit_property, } => { query!( " DELETE FROM units - WHERE id = ? AND full_name_singular = ? AND full_name_plural = ? AND short_name = ? AND description = ?; + WHERE id = ? AND full_name_singular = ? AND full_name_plural = ? AND short_name = ? AND description = ? AND unit_property = ?; ", - id, full_name_singular, full_name_plural, short_name, description, + id, full_name_singular, full_name_plural, short_name, description, unit_property ) .execute(txn) .await?; @@ -87,6 +94,7 @@ impl Unit { full_name_plural: String, short_name: String, description: Option, + unit_property: UnitPropertyId, ops: &mut Operations, ) -> Self { let id = UnitId::from(Uuid::new_v4()); @@ -97,6 +105,7 @@ impl Unit { full_name_plural: full_name_plural.clone(), short_name: short_name.clone(), description: description.clone(), + unit_property, }); Self { @@ -105,6 +114,7 @@ impl Unit { full_name_plural, short_name, description, + unit_property, } } } diff --git a/crates/rocie-server/src/storage/sql/insert/unit_property/mod.rs b/crates/rocie-server/src/storage/sql/insert/unit_property/mod.rs new file mode 100644 index 0000000..d340465 --- /dev/null +++ b/crates/rocie-server/src/storage/sql/insert/unit_property/mod.rs @@ -0,0 +1,106 @@ +use serde::{Deserialize, Serialize}; +use sqlx::query; +use uuid::Uuid; + +use crate::storage::sql::{ + insert::{Operations, Transactionable}, + unit_property::{UnitProperty, UnitPropertyId}, +}; + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) enum Operation { + RegisterUnitProperty { + id: UnitPropertyId, + name: String, + description: Option, + }, +} + +impl Transactionable for Operation { + type ApplyError = apply::Error; + type UndoError = undo::Error; + + async fn apply(self, txn: &mut sqlx::SqliteConnection) -> Result<(), apply::Error> { + match self { + Operation::RegisterUnitProperty { + id, + name, + description, + } => { + query!( + " + INSERT INTO unit_properties (id, name, description) + VALUES (?,?,?) +", + id, + name, + description, + ) + .execute(txn) + .await?; + } + } + Ok(()) + } + + async fn undo(self, txn: &mut sqlx::SqliteConnection) -> Result<(), undo::Error> { + match self { + Operation::RegisterUnitProperty { + id, + name, + description, + } => { + query!( + " + DELETE FROM unit_properties + WHERE id = ? AND name = ? AND description = ?; +", + id, + name, + description + ) + .execute(txn) + .await?; + } + } + Ok(()) + } +} + +pub(crate) mod undo { + #[derive(thiserror::Error, Debug)] + pub(crate) enum Error { + #[error("Failed to execute undo sql statments: {0}")] + SqlError(#[from] sqlx::Error), + } +} +pub(crate) mod apply { + #[derive(thiserror::Error, Debug)] + pub(crate) enum Error { + #[error("Failed to execute apply sql statments: {0}")] + SqlError(#[from] sqlx::Error), + } +} + +impl UnitProperty { + pub(crate) fn register( + name: String, + description: Option, + ops: &mut Operations, + ) -> Self { + let id = UnitPropertyId::from(Uuid::new_v4()); + + ops.push(Operation::RegisterUnitProperty { + id, + name: name.clone(), + description: description.clone(), + }); + + Self { + id, + units: vec![], + name, + description, + } + } +} diff --git a/crates/rocie-server/src/storage/sql/mod.rs b/crates/rocie-server/src/storage/sql/mod.rs index edce187..a44fbad 100644 --- a/crates/rocie-server/src/storage/sql/mod.rs +++ b/crates/rocie-server/src/storage/sql/mod.rs @@ -2,7 +2,100 @@ pub(crate) mod get; pub(crate) mod insert; // Types +pub(crate) mod barcode; pub(crate) mod product; pub(crate) mod product_amount; pub(crate) mod unit; -pub(crate) mod barcode; +pub(crate) mod unit_property; + +macro_rules! mk_id { + ($name:ident and $stub_name:ident) => { + mk_id!($name and $stub_name with uuid::Uuid, "uuid::Uuid"); + }; + ($name:ident and $stub_name:ident with $inner:path, $inner_string:literal $($args:meta)*) => { + #[derive( + serde::Deserialize, + serde::Serialize, + Debug, + Default, + utoipa::ToSchema, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + )] + pub(crate) struct $name { + value: $inner, + } + + #[derive(Deserialize, Serialize, Debug, Clone, Copy)] + #[serde(from = $inner_string)] + pub(crate) struct $stub_name { + value: $inner, + } + + impl $name { + pub(crate) fn from_db(id: &str) -> Self { + use std::str::FromStr; + + Self { + value: <$inner as FromStr>::from_str(id) + .expect( + concat!( + "We put an ", + $inner_string, + " into the db, it should also go out again" + ) + ), + } + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.value) + } + } + + impl From<$inner> for $name { + fn from(value: $inner) -> Self { + Self { value } + } + } + impl From<$inner> for $stub_name { + fn from(value: $inner) -> Self { + Self { value } + } + } + impl From<$stub_name> for $name { + fn from(value: $stub_name) -> Self { + Self { value: value.value } + } + } + + impl<'q, DB: sqlx::Database> sqlx::Encode<'q, DB> for $name + where + String: sqlx::Encode<'q, DB>, + { + fn encode_by_ref( + &self, + buf: &mut ::ArgumentBuffer<'q>, + ) -> Result { + let inner = self.value.to_string(); + sqlx::Encode::::encode_by_ref(&inner, buf) + } + } + impl sqlx::Type for $name + where + String: sqlx::Type, + { + fn type_info() -> DB::TypeInfo { + >::type_info() + } + } + }; +} + +pub(crate) use mk_id; diff --git a/crates/rocie-server/src/storage/sql/product.rs b/crates/rocie-server/src/storage/sql/product.rs index c94fcce..2575b59 100644 --- a/crates/rocie-server/src/storage/sql/product.rs +++ b/crates/rocie-server/src/storage/sql/product.rs @@ -1,82 +1,32 @@ -use std::{fmt::Display, str::FromStr}; - use serde::{Deserialize, Serialize}; -use sqlx::{Database, Encode, Type}; use utoipa::ToSchema; -use uuid::Uuid; -use crate::storage::sql::barcode::Barcode; +use crate::storage::sql::{barcode::Barcode, mk_id, unit_property::UnitPropertyId}; +/// The base of rocie. +/// +/// Products can be bought and consumed and represent, what you actually have in storage. +/// Not every product is bought, as some can also be obtained by cooking a recipe. #[derive(Clone, ToSchema, Serialize, Deserialize)] pub(crate) struct Product { + /// The id of the product. pub(crate) id: ProductId, - pub(super) name: String, - pub(super) description: Option, - pub(super) associated_bar_codes: Vec, -} - -#[derive( - Deserialize, Serialize, Debug, Default, ToSchema, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, -)] -pub(crate) struct ProductId { - value: Uuid, -} - -#[derive(Deserialize, Serialize, Debug, Clone, Copy)] -#[serde(from = "Uuid")] -pub(crate) struct ProductIdStub { - value: Uuid, -} -impl ProductId { - pub(crate) fn from_db(id: &str) -> ProductId { - Self { - value: Uuid::from_str(id) - .expect("We put an uuid into the db, it should also go out again"), - } - } -} + /// The property this product is measured in. + /// + /// (This is probably always either Mass, Volume or Quantity). + pub(crate) unit_property: UnitPropertyId, -impl Display for ProductId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.value) - } -} + /// The name of the product. + /// + /// This should be globally unique, to make searching easier for the user. + pub(super) name: String, -impl From for ProductId { - fn from(value: Uuid) -> Self { - Self { value } - } -} + /// An optional description of this product. + pub(super) description: Option, -impl From for ProductIdStub { - fn from(value: Uuid) -> Self { - Self { value } - } -} -impl From for ProductId { - fn from(value: ProductIdStub) -> Self { - Self { value: value.value } - } + /// Which barcodes are associated with this product. + pub(super) associated_bar_codes: Vec, } -impl<'q, DB: Database> Encode<'q, DB> for ProductId -where - String: Encode<'q, DB>, -{ - fn encode_by_ref( - &self, - buf: &mut ::ArgumentBuffer<'q>, - ) -> Result { - let inner = self.value.to_string(); - Encode::::encode_by_ref(&inner, buf) - } -} -impl Type for ProductId -where - String: Type, -{ - fn type_info() -> DB::TypeInfo { - >::type_info() - } -} +mk_id!(ProductId and ProductIdStub); diff --git a/crates/rocie-server/src/storage/sql/product_amount.rs b/crates/rocie-server/src/storage/sql/product_amount.rs index 232c5db..0f19afc 100644 --- a/crates/rocie-server/src/storage/sql/product_amount.rs +++ b/crates/rocie-server/src/storage/sql/product_amount.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::storage::sql::{barcode::UnitAmount, product::ProductId}; +use crate::storage::sql::{product::ProductId, unit::UnitAmount}; #[derive(Clone, ToSchema, Serialize, Deserialize)] pub(crate) struct ProductAmount { diff --git a/crates/rocie-server/src/storage/sql/unit.rs b/crates/rocie-server/src/storage/sql/unit.rs index 77e7a2e..d16e783 100644 --- a/crates/rocie-server/src/storage/sql/unit.rs +++ b/crates/rocie-server/src/storage/sql/unit.rs @@ -1,80 +1,50 @@ -use std::{fmt::Display, str::FromStr}; - use serde::{Deserialize, Serialize}; -use sqlx::{Database, Encode, Type}; use utoipa::ToSchema; -use uuid::Uuid; + +use crate::storage::sql::{mk_id, unit_property::UnitPropertyId}; #[derive(ToSchema, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)] pub(crate) struct Unit { + /// Unique id for this unit. pub(crate) id: UnitId, + + /// The singular version of this unit's name. + /// E.g.: + /// Kilogram + /// Gram pub(crate) full_name_singular: String, - pub(crate) full_name_plural: String, - pub(crate) short_name: String, - pub(crate) description: Option, -} -#[derive( - Deserialize, Serialize, Debug, Default, ToSchema, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, -)] -pub(crate) struct UnitId { - value: Uuid, -} + /// The plural version of this unit's name. Is used by a value of two and more. + /// (We do not support Slovenian dual numerus versions) + /// E.g.: + /// Kilogram -> Kilograms + /// gram -> meters + pub(crate) full_name_plural: String, -#[derive(Deserialize, Serialize, Debug, Clone, Copy)] -#[serde(from = "Uuid")] -pub(crate) struct UnitIdStub { - value: Uuid, -} + /// Short name or abbreviation of this unit. + /// E.g.: + /// kg for Kilogram + /// g for gram + /// m for meter + pub(crate) short_name: String, -impl UnitId { - pub(crate) fn from_db(id: &str) -> UnitId { - Self { - value: Uuid::from_str(id) - .expect("We put an uuid into the db, it should also go out again"), - } - } -} + /// Description of this unit. + pub(crate) description: Option, -impl Display for UnitId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.value) - } + /// Which property is described by this unit. + /// E.g.: + /// kg -> Mass + /// L -> Volume + /// m/s -> Speed + /// and so forth + pub(crate) unit_property: UnitPropertyId, } -impl From for UnitId { - fn from(value: Uuid) -> Self { - Self { value } - } -} -impl From for UnitIdStub { - fn from(value: Uuid) -> Self { - Self { value } - } -} -impl From for UnitId { - fn from(value: UnitIdStub) -> Self { - Self { value: value.value } - } +#[derive(ToSchema, Debug, Clone, Copy, Serialize, Deserialize)] +pub(crate) struct UnitAmount { + #[schema(minimum = 0)] + pub(crate) value: u32, + pub(crate) unit: UnitId, } -impl<'q, DB: Database> Encode<'q, DB> for UnitId -where - String: Encode<'q, DB>, -{ - fn encode_by_ref( - &self, - buf: &mut ::ArgumentBuffer<'q>, - ) -> Result { - let inner = self.value.to_string(); - Encode::::encode_by_ref(&inner, buf) - } -} -impl Type for UnitId -where - String: Type, -{ - fn type_info() -> DB::TypeInfo { - >::type_info() - } -} +mk_id!(UnitId and UnitIdStub); diff --git a/crates/rocie-server/src/storage/sql/unit_property.rs b/crates/rocie-server/src/storage/sql/unit_property.rs new file mode 100644 index 0000000..9da2d2e --- /dev/null +++ b/crates/rocie-server/src/storage/sql/unit_property.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::storage::sql::{mk_id, unit::UnitId}; + +/// An unit property describes a property that can be measured by units. +/// For example velocity, mass or volume. +#[derive(ToSchema, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)] +pub(crate) struct UnitProperty { + /// The unique ID for this unit property. + pub(crate) id: UnitPropertyId, + + /// The user-displayed name of this property. + pub(crate) name: String, + + /// The units with are measuring this property. + pub(crate) units: Vec, + + /// An description of this property. + pub(crate) description: Option, +} + +mk_id!(UnitPropertyId and UnitPropertyIdStub); -- cgit 1.4.1