aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--crates/rocie-server/Cargo.toml2
-rw-r--r--crates/rocie-server/src/api/get/mod.rs13
-rw-r--r--crates/rocie-server/src/api/get/product.rs119
-rw-r--r--crates/rocie-server/src/api/get/product_parent.rs64
-rw-r--r--crates/rocie-server/src/api/get/recipe.rs66
-rw-r--r--crates/rocie-server/src/api/get/unit.rs46
-rw-r--r--crates/rocie-server/src/api/set/mod.rs4
-rw-r--r--crates/rocie-server/src/api/set/product.rs7
-rw-r--r--crates/rocie-server/src/api/set/product_parent.rs60
-rw-r--r--crates/rocie-server/src/api/set/recipe.rs54
-rw-r--r--crates/rocie-server/src/api/set/unit.rs2
-rw-r--r--crates/rocie-server/src/api/set/unit_property.rs4
-rw-r--r--crates/rocie-server/src/main.rs40
-rw-r--r--crates/rocie-server/src/storage/migrate/sql/0->1.sql10
-rw-r--r--crates/rocie-server/src/storage/sql/get/mod.rs2
-rw-r--r--crates/rocie-server/src/storage/sql/get/product/mod.rs83
-rw-r--r--crates/rocie-server/src/storage/sql/get/product_parent/mod.rs87
-rw-r--r--crates/rocie-server/src/storage/sql/get/recipe/mod.rs78
-rw-r--r--crates/rocie-server/src/storage/sql/insert/mod.rs2
-rw-r--r--crates/rocie-server/src/storage/sql/insert/product/mod.rs6
-rw-r--r--crates/rocie-server/src/storage/sql/insert/product_parent/mod.rs113
-rw-r--r--crates/rocie-server/src/storage/sql/insert/recipe/mod.rs95
-rw-r--r--crates/rocie-server/src/storage/sql/mod.rs35
-rw-r--r--crates/rocie-server/src/storage/sql/product.rs9
-rw-r--r--crates/rocie-server/src/storage/sql/product_parent.rs31
-rw-r--r--crates/rocie-server/src/storage/sql/recipe.rs18
-rw-r--r--crates/rocie-server/src/storage/sql/unit.rs1
-rw-r--r--crates/rocie-server/src/storage/sql/unit_property.rs1
-rw-r--r--crates/rocie-server/tests/_testenv/log.rs2
-rw-r--r--crates/rocie-server/tests/product_parents/mod.rs2
-rw-r--r--crates/rocie-server/tests/product_parents/query.rs144
-rw-r--r--crates/rocie-server/tests/product_parents/register.rs84
-rw-r--r--crates/rocie-server/tests/products/mod.rs4
-rw-r--r--crates/rocie-server/tests/products/register.rs11
-rw-r--r--crates/rocie-server/tests/recipies/mod.rs24
-rw-r--r--crates/rocie-server/tests/tests.rs2
-rw-r--r--crates/rocie-server/tests/units/fetch.rs88
-rw-r--r--crates/rocie-server/tests/units/mod.rs1
-rw-r--r--crates/rocie-server/tests/units/register.rs6
39 files changed, 1343 insertions, 77 deletions
diff --git a/crates/rocie-server/Cargo.toml b/crates/rocie-server/Cargo.toml
index 9f09b3c..51df09c 100644
--- a/crates/rocie-server/Cargo.toml
+++ b/crates/rocie-server/Cargo.toml
@@ -32,6 +32,8 @@ tokio.workspace = true
[dependencies]
actix-cors = "0.7.1"
+actix-identity = "0.9.0"
+actix-session = { version = "0.11.0", features = ["cookie-session"] }
actix-web = "4.11.0"
chrono = "0.4.41"
clap = { version = "4.5.45", features = ["derive", "env"] }
diff --git a/crates/rocie-server/src/api/get/mod.rs b/crates/rocie-server/src/api/get/mod.rs
index 1c32105..487b55c 100644
--- a/crates/rocie-server/src/api/get/mod.rs
+++ b/crates/rocie-server/src/api/get/mod.rs
@@ -2,6 +2,8 @@ use actix_web::web;
pub(crate) mod inventory;
pub(crate) mod product;
+pub(crate) mod product_parent;
+pub(crate) mod recipe;
pub(crate) mod unit;
pub(crate) mod unit_property;
@@ -9,8 +11,17 @@ pub(crate) fn register_paths(cfg: &mut web::ServiceConfig) {
cfg.service(product::product_by_id)
.service(product::product_by_name)
.service(product::product_suggestion_by_name)
- .service(product::products)
+ .service(product::products_registered)
+ .service(product::products_in_storage)
+ .service(product::products_by_product_parent_id_indirect)
+ .service(product::products_by_product_parent_id_direct)
+ .service(product_parent::product_parents)
+ .service(product_parent::product_parents_toplevel)
+ .service(product_parent::product_parents_under)
+ .service(recipe::recipe_by_id)
+ .service(recipe::recipes)
.service(unit::units)
+ .service(unit::units_by_property_id)
.service(unit::unit_by_id)
.service(unit_property::unit_properties)
.service(unit_property::unit_property_by_id)
diff --git a/crates/rocie-server/src/api/get/product.rs b/crates/rocie-server/src/api/get/product.rs
index 55e5d91..4216f9b 100644
--- a/crates/rocie-server/src/api/get/product.rs
+++ b/crates/rocie-server/src/api/get/product.rs
@@ -4,7 +4,11 @@ use percent_encoding::percent_decode_str;
use crate::{
app::App,
- storage::sql::product::{Product, ProductId, ProductIdStub},
+ storage::sql::{
+ product::{Product, ProductId, ProductIdStub},
+ product_amount::ProductAmount,
+ product_parent::{ProductParent, ProductParentId, ProductParentIdStub},
+ },
};
/// A String, that is not url-decoded on parse.
@@ -65,7 +69,7 @@ pub(crate) async fn product_by_name(
req: HttpRequest,
name: web::Path<String>,
) -> Result<impl Responder> {
- let _name = name;
+ drop(name);
let name = UrlEncodedString::from_str(
req.path()
@@ -96,7 +100,7 @@ pub(crate) async fn product_suggestion_by_name(
req: HttpRequest,
name: web::Path<String>,
) -> Result<impl Responder> {
- let _name = name;
+ drop(name);
let name = UrlEncodedString::from_str(
req.path()
@@ -118,13 +122,116 @@ pub(crate) async fn product_suggestion_by_name(
/// Return all registered products
#[utoipa::path(
responses(
- (status = OK, description = "All products founds", body = Vec<Product>),
+ (status = OK, description = "All products found", body = Vec<Product>),
(status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String)
),
)]
-#[get("/products/")]
-pub(crate) async fn products(app: web::Data<App>) -> Result<impl Responder> {
+#[get("/products_registered/")]
+pub(crate) async fn products_registered(app: web::Data<App>) -> Result<impl Responder> {
let all = Product::get_all(&app).await?;
Ok(HttpResponse::Ok().json(all))
}
+
+/// Return all products, which non-null amount in storage
+#[utoipa::path(
+ responses(
+ (status = OK, description = "All products found", body = Vec<Product>),
+ (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String)
+ ),
+)]
+#[get("/products_in_storage/")]
+pub(crate) async fn products_in_storage(app: web::Data<App>) -> Result<impl Responder> {
+ let all = Product::get_all(&app).await?;
+
+ let mut output_products = Vec::with_capacity(all.len());
+ for product in all {
+ let amount = ProductAmount::from_id(&app, product.id).await?;
+
+ if amount.is_some_and(|amount| amount.amount.value > 0) {
+ output_products.push(product);
+ }
+ }
+
+ Ok(HttpResponse::Ok().json(output_products))
+}
+
+/// Get Products by it's product parent id
+///
+/// This will also return all products below this product parent id
+#[utoipa::path(
+ responses(
+ (status = OK, description = "Products found from database", body = Vec<Product>),
+ (status = NOT_FOUND, description = "Product parent id not found in database"),
+ (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String)
+ ),
+ params(
+ ("id" = ProductParentId, description = "Product parent id" ),
+ )
+)]
+#[get("/product/by-product-parent-id-indirect/{id}")]
+pub(crate) async fn products_by_product_parent_id_indirect(
+ app: web::Data<App>,
+ id: web::Path<ProductParentIdStub>,
+) -> Result<impl Responder> {
+ let id = id.into_inner();
+
+ if let Some(parent) = ProductParent::from_id(&app, id.into()).await? {
+ async fn collect_products(app: &App, parent: ProductParent) -> Result<Vec<Product>> {
+ let mut all = Product::get_all(app)
+ .await?
+ .into_iter()
+ .filter(|prod| prod.parent.is_some_and(|val| val == parent.id))
+ .collect::<Vec<_>>();
+
+ if let Some(child) = ProductParent::get_all(app)
+ .await?
+ .into_iter()
+ .find(|pp| pp.parent.is_some_and(|id| id == parent.id))
+ {
+ all.extend(Box::pin(collect_products(app, child)).await?);
+ }
+
+ Ok(all)
+ }
+
+ let all = collect_products(&app, parent).await?;
+
+ Ok(HttpResponse::Ok().json(all))
+ } else {
+ Ok(HttpResponse::NotFound().finish())
+ }
+}
+
+/// Get Products by it's product parent id
+///
+/// This will only return products directly associated with this product parent id
+#[utoipa::path(
+ responses(
+ (status = OK, description = "Products found from database", body = Vec<Product>),
+ (status = NOT_FOUND, description = "Product parent id not found in database"),
+ (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String)
+ ),
+ params(
+ ("id" = ProductParentId, description = "Product parent id" ),
+ )
+)]
+#[get("/product/by-product-parent-id-direct/{id}")]
+pub(crate) async fn products_by_product_parent_id_direct(
+ app: web::Data<App>,
+ id: web::Path<ProductParentIdStub>,
+) -> Result<impl Responder> {
+ let id = id.into_inner();
+
+ if let Some(parent) = ProductParent::from_id(&app, id.into()).await? {
+ let all = Product::get_all(&app)
+ .await?
+ .into_iter()
+ .filter(|prod| prod.parent.is_some_and(|val| val == parent.id))
+ .collect::<Vec<_>>();
+
+ Ok(HttpResponse::Ok().json(all))
+ } else {
+ Ok(HttpResponse::NotFound().finish())
+ }
+}
diff --git a/crates/rocie-server/src/api/get/product_parent.rs b/crates/rocie-server/src/api/get/product_parent.rs
new file mode 100644
index 0000000..a62397c
--- /dev/null
+++ b/crates/rocie-server/src/api/get/product_parent.rs
@@ -0,0 +1,64 @@
+use actix_web::{HttpResponse, Responder, error::Result, get, web};
+
+use crate::{
+ app::App,
+ storage::sql::product_parent::{ProductParent, ProductParentId, ProductParentIdStub},
+};
+
+/// Return all registered product parents
+#[utoipa::path(
+ responses(
+ (status = OK, description = "All parents found", body = Vec<ProductParent>),
+ (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String)
+ ),
+)]
+#[get("/product_parents/")]
+pub(crate) async fn product_parents(app: web::Data<App>) -> Result<impl Responder> {
+ let all: Vec<ProductParent> = ProductParent::get_all(&app).await?;
+
+ Ok(HttpResponse::Ok().json(all))
+}
+
+/// Return all registered product parents, that have no parents themselves
+#[utoipa::path(
+ responses(
+ (status = OK, description = "All parents found", body = Vec<ProductParent>),
+ (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String)
+ ),
+)]
+#[get("/product_parents_toplevel/")]
+pub(crate) async fn product_parents_toplevel(app: web::Data<App>) -> Result<impl Responder> {
+ let all: Vec<ProductParent> = ProductParent::get_all(&app)
+ .await?
+ .into_iter()
+ .filter(|parent| parent.parent.is_none())
+ .collect();
+
+ Ok(HttpResponse::Ok().json(all))
+}
+
+/// Return all parents, that have this parent as parent
+#[utoipa::path(
+ responses(
+ (status = OK, description = "All parents found", body = Vec<ProductParent>),
+ (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String)
+ ),
+ params(
+ ("id" = ProductParentId, description = "Product parent id" ),
+ ),
+)]
+#[get("/product_parents_under/{id}")]
+pub(crate) async fn product_parents_under(
+ app: web::Data<App>,
+ id: web::Path<ProductParentIdStub>,
+) -> Result<impl Responder> {
+ let id = id.into_inner().into();
+
+ let all: Vec<_> = ProductParent::get_all(&app)
+ .await?
+ .into_iter()
+ .filter(|parent| parent.parent.is_some_and(|found| found == id))
+ .collect();
+
+ Ok(HttpResponse::Ok().json(all))
+}
diff --git a/crates/rocie-server/src/api/get/recipe.rs b/crates/rocie-server/src/api/get/recipe.rs
new file mode 100644
index 0000000..70bab39
--- /dev/null
+++ b/crates/rocie-server/src/api/get/recipe.rs
@@ -0,0 +1,66 @@
+use actix_web::{HttpResponse, Responder, error::Result, get, web};
+
+use crate::{
+ app::App,
+ storage::sql::recipe::{Recipe, RecipeId, RecipeIdStub},
+};
+
+/// Get an recipe by it's id.
+#[utoipa::path(
+ responses(
+ (
+ status = OK,
+ description = "Recipe found in database and fetched",
+ body = Recipe,
+ ),
+ (
+ status = NOT_FOUND,
+ description = "Recipe not found in database"
+ ),
+ (
+ status = INTERNAL_SERVER_ERROR,
+ description = "Server encountered error",
+ body = String
+ )
+ ),
+ params(
+ (
+ "id" = RecipeId,
+ description = "Recipe id"
+ ),
+ )
+)]
+#[get("/recipe/by-id/{id}")]
+pub(crate) async fn recipe_by_id(
+ app: web::Data<App>,
+ id: web::Path<RecipeIdStub>,
+) -> Result<impl Responder> {
+ let id = id.into_inner();
+
+ match Recipe::from_id(&app, id.into()).await? {
+ Some(recipe) => Ok(HttpResponse::Ok().json(recipe)),
+ None => Ok(HttpResponse::NotFound().finish()),
+ }
+}
+
+/// Get all added recipes
+#[utoipa::path(
+ responses(
+ (
+ status = OK,
+ description = "All recipes found in database and fetched",
+ body = Recipe,
+ ),
+ (
+ status = INTERNAL_SERVER_ERROR,
+ description = "Server encountered error",
+ body = String
+ )
+ ),
+)]
+#[get("/recipe/all")]
+pub(crate) async fn recipes(app: web::Data<App>) -> Result<impl Responder> {
+ let all = Recipe::get_all(&app).await?;
+
+ Ok(HttpResponse::Ok().json(all))
+}
diff --git a/crates/rocie-server/src/api/get/unit.rs b/crates/rocie-server/src/api/get/unit.rs
index 73aa626..caafaa3 100644
--- a/crates/rocie-server/src/api/get/unit.rs
+++ b/crates/rocie-server/src/api/get/unit.rs
@@ -1,6 +1,12 @@
-use actix_web::{get, web, HttpResponse, Responder, Result};
+use actix_web::{HttpResponse, Responder, Result, get, web};
-use crate::{app::App, storage::sql::unit::{Unit, UnitId, UnitIdStub}};
+use crate::{
+ app::App,
+ storage::sql::{
+ unit::{Unit, UnitId, UnitIdStub},
+ unit_property::{UnitPropertyId, UnitPropertyIdStub},
+ },
+};
/// Return all registered units
#[utoipa::path(
@@ -24,6 +30,42 @@ pub(crate) async fn units(app: web::Data<App>) -> Result<impl Responder> {
Ok(HttpResponse::Ok().json(all))
}
+/// Return all registered units for a specific unit property
+#[utoipa::path(
+ responses(
+ (
+ status = OK,
+ description = "All units founds",
+ body = Vec<Unit>
+ ),
+ (
+ status = INTERNAL_SERVER_ERROR,
+ description = "Server encountered error",
+ body = String
+ )
+ ),
+ params(
+ (
+ "id" = UnitPropertyId,
+ description = "Unit property id"
+ ),
+ )
+)]
+#[get("/units-by-property/{id}")]
+pub(crate) async fn units_by_property_id(
+ app: web::Data<App>,
+ id: web::Path<UnitPropertyIdStub>,
+) -> Result<impl Responder> {
+ let id = id.into_inner();
+ let all = Unit::get_all(&app)
+ .await?
+ .into_iter()
+ .filter(|unit| unit.unit_property == id.into())
+ .collect::<Vec<_>>();
+
+ Ok(HttpResponse::Ok().json(all))
+}
+
/// Get Unit by id
#[utoipa::path(
responses(
diff --git a/crates/rocie-server/src/api/set/mod.rs b/crates/rocie-server/src/api/set/mod.rs
index 5dd0219..a6037b9 100644
--- a/crates/rocie-server/src/api/set/mod.rs
+++ b/crates/rocie-server/src/api/set/mod.rs
@@ -2,12 +2,16 @@ use actix_web::web;
pub(crate) mod barcode;
pub(crate) mod product;
+pub(crate) mod product_parent;
+pub(crate) mod recipe;
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(product_parent::register_product_parent)
+ .service(recipe::add_recipe)
.service(unit::register_unit)
.service(unit_property::register_unit_property)
.service(barcode::consume_barcode)
diff --git a/crates/rocie-server/src/api/set/product.rs b/crates/rocie-server/src/api/set/product.rs
index 7372d23..74a92d2 100644
--- a/crates/rocie-server/src/api/set/product.rs
+++ b/crates/rocie-server/src/api/set/product.rs
@@ -8,6 +8,7 @@ use crate::{
barcode::Barcode,
insert::Operations,
product::{Product, ProductId, ProductIdStub},
+ product_parent::ProductParentId,
unit::Unit,
unit_property::UnitPropertyId,
},
@@ -22,10 +23,12 @@ struct ProductStub {
unit_property: UnitPropertyId,
/// A description.
+ #[schema(nullable = false)]
description: Option<String>,
/// A parent of this product, otherwise the parent will be the root of the parent tree.
- parent: Option<ProductId>,
+ #[schema(nullable = false)]
+ parent: Option<ProductParentId>,
}
/// Register a product
@@ -54,7 +57,7 @@ pub(crate) async fn register_product(
let product = Product::register(
product_stub.name.clone(),
product_stub.description.clone(),
- product_stub.parent,
+ product_stub.parent.into(),
product_stub.unit_property,
&mut ops,
);
diff --git a/crates/rocie-server/src/api/set/product_parent.rs b/crates/rocie-server/src/api/set/product_parent.rs
new file mode 100644
index 0000000..f917207
--- /dev/null
+++ b/crates/rocie-server/src/api/set/product_parent.rs
@@ -0,0 +1,60 @@
+use actix_web::{HttpResponse, Responder, Result, post, web};
+use serde::Deserialize;
+use utoipa::ToSchema;
+
+use crate::{
+ app::App,
+ storage::sql::{
+ insert::Operations,
+ product_parent::{ProductParent, ProductParentId},
+ },
+};
+
+#[derive(Deserialize, ToSchema)]
+struct ProductParentStub {
+ /// The name of the product parent
+ name: String,
+
+ /// A description.
+ #[schema(nullable = false)]
+ description: Option<String>,
+
+ /// A parent of this product parent, otherwise the parent will be the root of the parent tree.
+ #[schema(nullable = false)]
+ parent: Option<ProductParentId>,
+}
+
+/// Register a product parent
+#[utoipa::path(
+ responses(
+ (
+ status = 200,
+ description = "Product parent successfully registered in database",
+ body = ProductParentId,
+ ),
+ (
+ status = INTERNAL_SERVER_ERROR,
+ description = "Server encountered error",
+ body = String,
+ )
+ ),
+ request_body = ProductParentStub,
+)]
+#[post("/product_parent/new")]
+pub(crate) async fn register_product_parent(
+ app: web::Data<App>,
+ product_stub: web::Json<ProductParentStub>,
+) -> Result<impl Responder> {
+ let mut ops = Operations::new("register product parent");
+
+ let product = ProductParent::register(
+ product_stub.name.clone(),
+ product_stub.description.clone(),
+ product_stub.parent.into(),
+ &mut ops,
+ );
+
+ ops.apply(&app).await?;
+
+ Ok(HttpResponse::Ok().json(product.id))
+}
diff --git a/crates/rocie-server/src/api/set/recipe.rs b/crates/rocie-server/src/api/set/recipe.rs
new file mode 100644
index 0000000..bb5be37
--- /dev/null
+++ b/crates/rocie-server/src/api/set/recipe.rs
@@ -0,0 +1,54 @@
+use std::path::PathBuf;
+
+use actix_web::{HttpResponse, Responder, error::Result, post, web};
+use serde::Deserialize;
+use utoipa::ToSchema;
+
+use crate::{
+ app::App,
+ storage::sql::{
+ insert::Operations,
+ recipe::{Recipe, RecipeId},
+ },
+};
+
+#[derive(Deserialize, ToSchema)]
+struct RecipeStub {
+ /// The path the recipe should have
+ #[schema(value_type = String)]
+ path: PathBuf,
+
+ /// The content of this recipe, in cooklang format
+ content: String,
+}
+
+/// Register a product parent
+#[utoipa::path(
+ responses(
+ (
+ status = 200,
+ description = "Product parent successfully registered in database",
+ body = RecipeId,
+ ),
+ (
+ status = INTERNAL_SERVER_ERROR,
+ description = "Server encountered error",
+ body = String,
+ )
+ ),
+ request_body = RecipeStub,
+)]
+#[post("/recipe/new")]
+pub(crate) async fn add_recipe(
+ app: web::Data<App>,
+ stub: web::Json<RecipeStub>,
+) -> Result<impl Responder> {
+ let stub = stub.into_inner();
+ let mut ops = Operations::new("add recipe parent");
+
+ let recipe = Recipe::new(stub.path, stub.content, &mut ops);
+
+ ops.apply(&app).await?;
+
+ Ok(HttpResponse::Ok().json(recipe.id))
+}
diff --git a/crates/rocie-server/src/api/set/unit.rs b/crates/rocie-server/src/api/set/unit.rs
index 5f39c8f..1671918 100644
--- a/crates/rocie-server/src/api/set/unit.rs
+++ b/crates/rocie-server/src/api/set/unit.rs
@@ -17,6 +17,8 @@ struct UnitStub {
full_name_singular: String,
short_name: String,
unit_property: UnitPropertyId,
+
+ #[schema(nullable = false)]
description: Option<String>,
}
diff --git a/crates/rocie-server/src/api/set/unit_property.rs b/crates/rocie-server/src/api/set/unit_property.rs
index 9915e84..ca2960f 100644
--- a/crates/rocie-server/src/api/set/unit_property.rs
+++ b/crates/rocie-server/src/api/set/unit_property.rs
@@ -12,7 +12,11 @@ use crate::{
#[derive(Deserialize, ToSchema)]
struct UnitPropertyStub {
+ /// The name of the unit property.
name: String,
+
+ /// An optional description of the unit property.
+ #[schema(nullable = false)]
description: Option<String>,
}
diff --git a/crates/rocie-server/src/main.rs b/crates/rocie-server/src/main.rs
index 2329b0b..dc5be0b 100644
--- a/crates/rocie-server/src/main.rs
+++ b/crates/rocie-server/src/main.rs
@@ -1,5 +1,10 @@
use actix_cors::Cors;
-use actix_web::{App, HttpServer, middleware::Logger, web::Data};
+use actix_web::{
+ App, HttpServer,
+ cookie::{Key, SameSite},
+ middleware::Logger,
+ web::Data,
+};
use clap::Parser;
use utoipa::OpenApi;
@@ -10,6 +15,9 @@ mod app;
mod cli;
mod storage;
+use actix_identity::IdentityMiddleware;
+use actix_session::{SessionMiddleware, storage::CookieSessionStore};
+
#[actix_web::main]
#[expect(
clippy::needless_for_each,
@@ -22,14 +30,25 @@ async fn main() -> Result<(), std::io::Error> {
api::get::product::product_by_id,
api::get::product::product_by_name,
api::get::product::product_suggestion_by_name,
- api::get::product::products,
+ api::get::product::products_registered,
+ api::get::product::products_in_storage,
+ api::get::product::products_by_product_parent_id_indirect,
+ api::get::product::products_by_product_parent_id_direct,
+ api::get::product_parent::product_parents,
+ api::get::product_parent::product_parents_toplevel,
+ api::get::product_parent::product_parents_under,
+ api::get::recipe::recipe_by_id,
+ api::get::recipe::recipes,
api::get::unit::units,
+ api::get::unit::units_by_property_id,
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::product_parent::register_product_parent,
+ api::set::recipe::add_recipe,
api::set::unit::register_unit,
api::set::unit_property::register_unit_property,
api::set::barcode::buy_barcode,
@@ -45,6 +64,11 @@ async fn main() -> Result<(), std::io::Error> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
+ // When using `Key::generate()` it is important to initialize outside of the
+ // `HttpServer::new` closure. When deployed the secret key should be read from a
+ // configuration file or environment variables.
+ let secret_key = Key::generate();
+
let args = CliArgs::parse();
match args.command {
@@ -67,6 +91,18 @@ async fn main() -> Result<(), std::io::Error> {
.wrap(Logger::new(
r#"%a "%r" -> %s %b ("%{Referer}i" "%{User-Agent}i" %T s)"#,
))
+ // Install the identity framework before middle-ware (as actix is filo).
+ .wrap(IdentityMiddleware::default())
+ .wrap(
+ SessionMiddleware::builder(
+ CookieSessionStore::default(),
+ secret_key.clone(),
+ )
+ .cookie_secure(true)
+ .cookie_http_only(true)
+ .cookie_same_site(SameSite::Strict)
+ .build(),
+ )
.app_data(Data::clone(&data))
.configure(api::get::register_paths)
.configure(api::set::register_paths)
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 7f08738..664f40f 100644
--- a/crates/rocie-server/src/storage/migrate/sql/0->1.sql
+++ b/crates/rocie-server/src/storage/migrate/sql/0->1.sql
@@ -22,6 +22,8 @@ CREATE TABLE parents (
parent TEXT DEFAULT NULL CHECK (
id IS NOT parent
),
+ name TEXT UNIQUE NOT NULL,
+ description TEXT,
FOREIGN KEY(parent) REFERENCES parents(id)
) STRICT;
@@ -97,7 +99,7 @@ END;
CREATE TABLE units (
id TEXT UNIQUE NOT NULL PRIMARY KEY,
- unit_property TEXT UNIQUE NOT NULL,
+ unit_property TEXT NOT NULL,
full_name_singular TEXT UNIQUE NOT NULL,
full_name_plural TEXT UNIQUE NOT NULL,
short_name TEXT UNIQUE NOT NULL,
@@ -111,6 +113,12 @@ CREATE TABLE unit_properties (
description TEXT
) STRICT;
+CREATE TABLE recipies (
+ id TEXT UNIQUE NOT NULL PRIMARY KEY,
+ path TEXT UNIQUE NOT NULL,
+ content TEXT NOT NULL
+) STRICT;
+
-- Encodes unit conversions:
-- {factor} {from_unit} = 1 {to_unit}
-- E.g.: 1000 g = 1 kg
diff --git a/crates/rocie-server/src/storage/sql/get/mod.rs b/crates/rocie-server/src/storage/sql/get/mod.rs
index 62047b8..1fb54b0 100644
--- a/crates/rocie-server/src/storage/sql/get/mod.rs
+++ b/crates/rocie-server/src/storage/sql/get/mod.rs
@@ -1,5 +1,7 @@
pub(crate) mod product;
+pub(crate) mod product_parent;
pub(crate) mod product_amount;
pub(crate) mod unit;
pub(crate) mod unit_property;
pub(crate) mod barcode;
+pub(crate) mod recipe;
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 0df51d8..915da81 100644
--- a/crates/rocie-server/src/storage/sql/get/product/mod.rs
+++ b/crates/rocie-server/src/storage/sql/get/product/mod.rs
@@ -3,6 +3,7 @@ use crate::{
storage::sql::{
barcode::{Barcode, BarcodeId},
product::{Product, ProductId},
+ product_parent::ProductParentId,
unit::{UnitAmount, UnitId},
unit_property::UnitPropertyId,
},
@@ -10,11 +11,46 @@ use crate::{
use sqlx::query;
+macro_rules! product_from_record {
+ ($app:ident, $record:ident) => {{
+ let barcodes = query!(
+ "
+ SELECT id, amount, unit
+ FROM barcodes
+ WHERE product_id = ?
+ ",
+ $record.id,
+ )
+ .fetch_all(&$app.db)
+ .await?;
+
+ Self {
+ id: ProductId::from_db(&$record.id),
+ unit_property: UnitPropertyId::from_db(&$record.unit_property),
+ name: $record.name,
+ description: $record.description,
+ parent: $record
+ .parent
+ .map(|parent| ProductParentId::from_db(&parent)),
+ associated_bar_codes: barcodes
+ .into_iter()
+ .map(|record| Barcode {
+ id: BarcodeId::from_db(record.id),
+ amount: UnitAmount {
+ value: u32::try_from(record.amount).expect("Should be strictly positve"),
+ unit: UnitId::from_db(&record.unit),
+ },
+ })
+ .collect(),
+ }
+ }};
+}
+
impl Product {
pub(crate) async fn from_id(app: &App, id: ProductId) -> Result<Option<Self>, from_id::Error> {
let record = query!(
"
- SELECT name, unit_property, description, parent
+ SELECT name, id, unit_property, description, parent
FROM products
WHERE id = ?
",
@@ -24,13 +60,7 @@ impl Product {
.await?;
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
- }))
+ Ok(Some(product_from_record!(app, record)))
} else {
Ok(None)
}
@@ -49,13 +79,7 @@ impl Product {
.await?;
if let Some(record) = record {
- Ok(Some(Self {
- id: ProductId::from_db(&record.id),
- unit_property: UnitPropertyId::from_db(&record.unit_property),
- name,
- description: record.description,
- associated_bar_codes: vec![], // todo
- }))
+ Ok(Some(product_from_record!(app, record)))
} else {
Ok(None)
}
@@ -73,34 +97,9 @@ impl Product {
let mut all = Vec::with_capacity(records.len());
for record in records {
- let barcodes = query!(
- "
- SELECT id, amount, unit
- FROM barcodes
- WHERE product_id = ?
-",
- record.id,
- )
- .fetch_all(&app.db)
- .await?;
+ let product = product_from_record!(app, record);
- 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
- .into_iter()
- .map(|record| Barcode {
- id: BarcodeId::from_db(record.id),
- amount: UnitAmount {
- value: u32::try_from(record.amount)
- .expect("Should be strictly positve"),
- unit: UnitId::from_db(&record.unit),
- },
- })
- .collect(),
- });
+ all.push(product);
}
Ok(all)
diff --git a/crates/rocie-server/src/storage/sql/get/product_parent/mod.rs b/crates/rocie-server/src/storage/sql/get/product_parent/mod.rs
new file mode 100644
index 0000000..5b85b62
--- /dev/null
+++ b/crates/rocie-server/src/storage/sql/get/product_parent/mod.rs
@@ -0,0 +1,87 @@
+use crate::{
+ app::App,
+ storage::sql::product_parent::{ProductParent, ProductParentId},
+};
+
+use sqlx::query;
+
+impl ProductParent {
+ pub(crate) async fn get_all(app: &App) -> Result<Vec<Self>, get_all::Error> {
+ let records = query!(
+ "
+ SELECT id, parent, name, description
+ FROM parents
+"
+ )
+ .fetch_all(&app.db)
+ .await?;
+
+ let mut all = Vec::with_capacity(records.len());
+ for record in records {
+ let parent = ProductParent {
+ id: ProductParentId::from_db(&record.id),
+ parent: record
+ .parent
+ .map(|parent| ProductParentId::from_db(&parent)),
+ name: record.name,
+ description: record.description,
+ };
+
+ all.push(parent);
+ }
+
+ Ok(all)
+ }
+
+ pub(crate) async fn from_id(
+ app: &App,
+ id: ProductParentId,
+ ) -> Result<Option<Self>, from_id::Error> {
+ let record = query!(
+ "
+ SELECT parent, name, description
+ FROM parents
+ WHERE id = ?
+",
+ id
+ )
+ .fetch_optional(&app.db)
+ .await?;
+
+ match record {
+ Some(record) => Ok(Some(ProductParent {
+ id,
+ parent: record
+ .parent
+ .map(|parent| ProductParentId::from_db(&parent)),
+ name: record.name,
+ description: record.description,
+ })),
+ None => Ok(None),
+ }
+ }
+}
+
+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 {}
+}
+
+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 {}
+}
diff --git a/crates/rocie-server/src/storage/sql/get/recipe/mod.rs b/crates/rocie-server/src/storage/sql/get/recipe/mod.rs
new file mode 100644
index 0000000..9d6dc79
--- /dev/null
+++ b/crates/rocie-server/src/storage/sql/get/recipe/mod.rs
@@ -0,0 +1,78 @@
+use crate::{
+ app::App,
+ storage::sql::recipe::{Recipe, RecipeId},
+};
+
+use sqlx::query;
+
+impl Recipe {
+ pub(crate) async fn from_id(app: &App, id: RecipeId) -> Result<Option<Self>, from_id::Error> {
+ let record = query!(
+ "
+ SELECT content, path
+ FROM recipies
+ WHERE id = ?
+",
+ id
+ )
+ .fetch_optional(&app.db)
+ .await?;
+
+ if let Some(record) = record {
+ Ok(Some(Self {
+ id,
+ path: record
+ .path
+ .parse()
+ .expect("Was a path before, should still be one"),
+ content: record.content,
+ }))
+ } else {
+ Ok(None)
+ }
+ }
+
+ pub(crate) async fn get_all(app: &App) -> Result<Vec<Self>, get_all::Error> {
+ let records = query!(
+ "
+ SELECT id, content, path
+ FROM recipies
+",
+ )
+ .fetch_all(&app.db)
+ .await?;
+
+ Ok(records
+ .into_iter()
+ .map(|record| Self {
+ id: RecipeId::from_db(&record.id),
+ path: record.path.parse().expect("Is still valid"),
+ content: record.content,
+ })
+ .collect())
+ }
+}
+
+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 {}
+}
+
+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 {}
+}
diff --git a/crates/rocie-server/src/storage/sql/insert/mod.rs b/crates/rocie-server/src/storage/sql/insert/mod.rs
index 3b2d702..8a15385 100644
--- a/crates/rocie-server/src/storage/sql/insert/mod.rs
+++ b/crates/rocie-server/src/storage/sql/insert/mod.rs
@@ -9,8 +9,10 @@ use sqlx::{SqliteConnection, query};
pub(crate) mod barcode;
pub(crate) mod product;
+pub(crate) mod product_parent;
pub(crate) mod unit;
pub(crate) mod unit_property;
+pub(crate) mod recipe;
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 d762e9b..455eb4f 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},
+ product_parent::ProductParentId,
unit_property::UnitPropertyId,
};
@@ -15,7 +16,7 @@ pub(crate) enum Operation {
id: ProductId,
name: String,
description: Option<String>,
- parent: Option<ProductId>,
+ parent: Option<ProductParentId>,
unit_property: UnitPropertyId,
},
AssociateBarcode {
@@ -138,7 +139,7 @@ impl Product {
pub(crate) fn register(
name: String,
description: Option<String>,
- parent: Option<ProductId>,
+ parent: Option<ProductParentId>,
unit_property: UnitPropertyId,
ops: &mut Operations<Operation>,
) -> Self {
@@ -157,6 +158,7 @@ impl Product {
name,
description,
unit_property,
+ parent,
associated_bar_codes: vec![],
}
}
diff --git a/crates/rocie-server/src/storage/sql/insert/product_parent/mod.rs b/crates/rocie-server/src/storage/sql/insert/product_parent/mod.rs
new file mode 100644
index 0000000..644778f
--- /dev/null
+++ b/crates/rocie-server/src/storage/sql/insert/product_parent/mod.rs
@@ -0,0 +1,113 @@
+use serde::{Deserialize, Serialize};
+use sqlx::query;
+use uuid::Uuid;
+
+use crate::storage::sql::{
+ insert::{Operations, Transactionable},
+ product_parent::{ProductParent, ProductParentId},
+};
+
+#[derive(Debug, Deserialize, Serialize)]
+pub(crate) enum Operation {
+ RegisterProductParent {
+ id: ProductParentId,
+ name: String,
+ description: Option<String>,
+ parent: Option<ProductParentId>,
+ },
+}
+
+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::RegisterProductParent {
+ id,
+ name,
+ description,
+ parent,
+ } => {
+ query!(
+ "
+ INSERT INTO parents (id, name, description, parent)
+ VALUES (?,?,?,?)
+",
+ id,
+ name,
+ description,
+ parent
+ )
+ .execute(txn)
+ .await?;
+ }
+ }
+ Ok(())
+ }
+
+ async fn undo(self, txn: &mut sqlx::SqliteConnection) -> Result<(), undo::Error> {
+ match self {
+ Operation::RegisterProductParent {
+ id,
+ name,
+ description,
+ parent,
+ } => {
+ query!(
+ "
+ DELETE FROM products
+ WHERE id = ? AND name = ? AND description = ? AND parent = ?;
+",
+ id,
+ name,
+ description,
+ parent,
+ )
+ .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 ProductParent {
+ pub(crate) fn register(
+ name: String,
+ description: Option<String>,
+ parent: Option<ProductParentId>,
+ ops: &mut Operations<Operation>,
+ ) -> Self {
+ let id = ProductParentId::from(Uuid::new_v4());
+
+ ops.push(Operation::RegisterProductParent {
+ id,
+ name: name.clone(),
+ description: description.clone(),
+ parent,
+ });
+
+ Self {
+ id,
+ name,
+ description,
+ parent,
+ }
+ }
+}
diff --git a/crates/rocie-server/src/storage/sql/insert/recipe/mod.rs b/crates/rocie-server/src/storage/sql/insert/recipe/mod.rs
new file mode 100644
index 0000000..b223bfe
--- /dev/null
+++ b/crates/rocie-server/src/storage/sql/insert/recipe/mod.rs
@@ -0,0 +1,95 @@
+use std::path::PathBuf;
+
+use serde::{Deserialize, Serialize};
+use sqlx::query;
+use uuid::Uuid;
+
+use crate::storage::sql::{
+ insert::{Operations, Transactionable},
+ recipe::{Recipe, RecipeId},
+};
+
+#[derive(Debug, Deserialize, Serialize)]
+pub(crate) enum Operation {
+ New {
+ id: RecipeId,
+ path: PathBuf,
+ content: String,
+ },
+}
+
+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::New { id, path, content } => {
+ let path = path.display().to_string();
+
+ query!(
+ "
+ INSERT INTO recipies (id, path, content)
+ VALUES (?, ?, ?)
+",
+ id,
+ path,
+ content,
+ )
+ .execute(txn)
+ .await?;
+ }
+ }
+ Ok(())
+ }
+
+ async fn undo(self, txn: &mut sqlx::SqliteConnection) -> Result<(), undo::Error> {
+ match self {
+ Operation::New { id, path, content } => {
+ let path = path.display().to_string();
+
+ query!(
+ "
+ DELETE FROM recipies
+ WHERE id = ? AND path = ? AND content = ?
+",
+ id,
+ path,
+ content
+ )
+ .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 Recipe {
+ pub(crate) fn new(path: PathBuf, content: String, ops: &mut Operations<Operation>) -> Self {
+ let id = RecipeId::from(Uuid::new_v4());
+
+ ops.push(Operation::New {
+ id,
+ path: path.clone(),
+ content: content.clone(),
+ });
+
+ Self { id, path, content }
+ }
+}
diff --git a/crates/rocie-server/src/storage/sql/mod.rs b/crates/rocie-server/src/storage/sql/mod.rs
index a44fbad..dd46eab 100644
--- a/crates/rocie-server/src/storage/sql/mod.rs
+++ b/crates/rocie-server/src/storage/sql/mod.rs
@@ -5,14 +5,26 @@ pub(crate) mod insert;
pub(crate) mod barcode;
pub(crate) mod product;
pub(crate) mod product_amount;
+pub(crate) mod product_parent;
+pub(crate) mod recipe;
pub(crate) mod unit;
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");
+ mk_id!(
+ $name and $stub_name,
+ with uuid::Uuid, "uuid::Uuid",
+ to_string {|val: &uuid::Uuid| val.to_string()},
+ copy Copy
+ );
};
- ($name:ident and $stub_name:ident with $inner:path, $inner_string:literal $($args:meta)*) => {
+ (
+ $name:ident and $stub_name:ident,
+ with $inner:path $(=> $($args:meta)* )?, $inner_string:literal,
+ to_string $to_string:expr,
+ $(copy $copy:path)?
+ ) => {
#[derive(
serde::Deserialize,
serde::Serialize,
@@ -20,17 +32,28 @@ macro_rules! mk_id {
Default,
utoipa::ToSchema,
Clone,
- Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
+ $($copy,)?
)]
pub(crate) struct $name {
+ $(
+ $(
+ #[$args]
+ )*
+ )?
value: $inner,
}
- #[derive(Deserialize, Serialize, Debug, Clone, Copy)]
+ #[derive(
+ Deserialize,
+ Serialize,
+ Debug,
+ Clone,
+ $($copy,)?
+ )]
#[serde(from = $inner_string)]
pub(crate) struct $stub_name {
value: $inner,
@@ -55,7 +78,7 @@ macro_rules! mk_id {
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.value)
+ write!(f, "{}", $to_string(&self.value))
}
}
@@ -83,7 +106,7 @@ macro_rules! mk_id {
&self,
buf: &mut <DB as sqlx::Database>::ArgumentBuffer<'q>,
) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
- let inner = self.value.to_string();
+ let inner = $to_string(&self.value);
sqlx::Encode::<DB>::encode_by_ref(&inner, buf)
}
}
diff --git a/crates/rocie-server/src/storage/sql/product.rs b/crates/rocie-server/src/storage/sql/product.rs
index 1c5a7d8..8d2c951 100644
--- a/crates/rocie-server/src/storage/sql/product.rs
+++ b/crates/rocie-server/src/storage/sql/product.rs
@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
-use crate::storage::sql::{barcode::Barcode, mk_id, unit_property::UnitPropertyId};
+use crate::storage::sql::{barcode::Barcode, mk_id, product_parent::ProductParentId, unit_property::UnitPropertyId};
/// The base of rocie.
///
@@ -12,6 +12,12 @@ pub(crate) struct Product {
/// The id of the product.
pub(crate) id: ProductId,
+ /// The parent this product has.
+ ///
+ /// This is effectively it's anchor in the product DAG.
+ #[schema(nullable = false)]
+ pub(crate) parent: Option<ProductParentId>,
+
/// The property this product is measured in.
///
/// (This is probably always either Mass, Volume or Quantity).
@@ -23,6 +29,7 @@ pub(crate) struct Product {
pub(crate) name: String,
/// An optional description of this product.
+ #[schema(nullable = false)]
pub(super) description: Option<String>,
/// Which barcodes are associated with this product.
diff --git a/crates/rocie-server/src/storage/sql/product_parent.rs b/crates/rocie-server/src/storage/sql/product_parent.rs
new file mode 100644
index 0000000..f689024
--- /dev/null
+++ b/crates/rocie-server/src/storage/sql/product_parent.rs
@@ -0,0 +1,31 @@
+use serde::{Deserialize, Serialize};
+use utoipa::ToSchema;
+
+use crate::storage::sql::mk_id;
+
+/// The grouping system for products.
+///
+/// Every Product can have a related parent, and every parent can have a parent themselves.
+/// As such, the products list constructs a DAG.
+#[derive(Clone, ToSchema, Serialize, Deserialize)]
+pub(crate) struct ProductParent {
+ /// The id of the product parent.
+ pub(crate) id: ProductParentId,
+
+ /// The optional id of the parent of this product parent.
+ ///
+ /// This must not form a cycle.
+ #[schema(nullable = false)]
+ pub(crate) parent: Option<ProductParentId>,
+
+ /// The name of the product parent.
+ ///
+ /// This should be globally unique, to make searching easier for the user.
+ pub(crate) name: String,
+
+ /// An optional description of this product parent.
+ #[schema(nullable = false)]
+ pub(super) description: Option<String>,
+}
+
+mk_id!(ProductParentId and ProductParentIdStub);
diff --git a/crates/rocie-server/src/storage/sql/recipe.rs b/crates/rocie-server/src/storage/sql/recipe.rs
new file mode 100644
index 0000000..835d98b
--- /dev/null
+++ b/crates/rocie-server/src/storage/sql/recipe.rs
@@ -0,0 +1,18 @@
+use std::path::PathBuf;
+
+use serde::{Deserialize, Serialize};
+use utoipa::ToSchema;
+
+use crate::storage::sql::mk_id;
+
+#[derive(ToSchema, Debug, Clone, Serialize, Deserialize)]
+pub(crate) struct Recipe {
+ pub(crate) id: RecipeId,
+
+ #[schema(value_type = String)]
+ pub(crate) path: PathBuf,
+
+ pub(crate) content: String,
+}
+
+mk_id!(RecipeId and RecipeIdStub);
diff --git a/crates/rocie-server/src/storage/sql/unit.rs b/crates/rocie-server/src/storage/sql/unit.rs
index d16e783..8bbfe60 100644
--- a/crates/rocie-server/src/storage/sql/unit.rs
+++ b/crates/rocie-server/src/storage/sql/unit.rs
@@ -29,6 +29,7 @@ pub(crate) struct Unit {
pub(crate) short_name: String,
/// Description of this unit.
+ #[schema(nullable = false)]
pub(crate) description: Option<String>,
/// Which property is described by this unit.
diff --git a/crates/rocie-server/src/storage/sql/unit_property.rs b/crates/rocie-server/src/storage/sql/unit_property.rs
index 9da2d2e..adb4767 100644
--- a/crates/rocie-server/src/storage/sql/unit_property.rs
+++ b/crates/rocie-server/src/storage/sql/unit_property.rs
@@ -17,6 +17,7 @@ pub(crate) struct UnitProperty {
pub(crate) units: Vec<UnitId>,
/// An description of this property.
+ #[schema(nullable = false)]
pub(crate) description: Option<String>,
}
diff --git a/crates/rocie-server/tests/_testenv/log.rs b/crates/rocie-server/tests/_testenv/log.rs
index 9a07e78..837ccdd 100644
--- a/crates/rocie-server/tests/_testenv/log.rs
+++ b/crates/rocie-server/tests/_testenv/log.rs
@@ -48,8 +48,10 @@ macro_rules! request {
}};
(@format, $fn:ident, $($arg:expr),*) => {{
+ #[allow(unused_imports)]
use std::fmt::Write;
+ #[allow(unused_mut)]
let mut base = String::new();
$(
write!(base, "{:?}, ", $arg)
diff --git a/crates/rocie-server/tests/product_parents/mod.rs b/crates/rocie-server/tests/product_parents/mod.rs
new file mode 100644
index 0000000..46ec0ca
--- /dev/null
+++ b/crates/rocie-server/tests/product_parents/mod.rs
@@ -0,0 +1,2 @@
+mod register;
+mod query;
diff --git a/crates/rocie-server/tests/product_parents/query.rs b/crates/rocie-server/tests/product_parents/query.rs
new file mode 100644
index 0000000..6d16ca3
--- /dev/null
+++ b/crates/rocie-server/tests/product_parents/query.rs
@@ -0,0 +1,144 @@
+use rocie_client::{
+ apis::{
+ api_get_product_api::{products_by_product_parent_id_direct, products_by_product_parent_id_indirect},
+ api_get_product_parent_api::{product_parents_toplevel, product_parents_under},
+ api_set_product_api::register_product,
+ api_set_product_parent_api::register_product_parent,
+ api_set_unit_property_api::register_unit_property,
+ },
+ models::{ProductParentStub, ProductStub, UnitPropertyStub},
+};
+
+use crate::{
+ _testenv::init::function_name,
+ testenv::{TestEnv, log::request},
+};
+
+#[tokio::test]
+async fn test_product_parent_query() {
+ let env = TestEnv::new(function_name!());
+
+ let parent_dairy = request!(
+ env,
+ register_product_parent(ProductParentStub {
+ description: Some("Dairy replacment products".to_owned()),
+ name: "Dairy replacements".to_owned(),
+ parent: None,
+ })
+ );
+ request!(
+ env,
+ register_product_parent(ProductParentStub {
+ description: Some("Dairy replacment products".to_owned()),
+ name: "Wheat products".to_owned(),
+ parent: None,
+ })
+ );
+
+ request!(
+ env,
+ register_product_parent(ProductParentStub {
+ description: Some("Milk replacment products".to_owned()),
+ name: "Milk replacements".to_owned(),
+ parent: Some(parent_dairy),
+ })
+ );
+
+ assert_eq!(
+ request!(env, product_parents_toplevel())
+ .into_iter()
+ .map(|parent| parent.name)
+ .collect::<Vec<_>>(),
+ vec!["Dairy replacements".to_owned(), "Wheat products".to_owned(),]
+ );
+
+ assert_eq!(
+ request!(env, product_parents_under(parent_dairy))
+ .into_iter()
+ .map(|parent| parent.name)
+ .collect::<Vec<_>>(),
+ vec!["Milk replacements".to_owned()]
+ );
+}
+
+#[tokio::test]
+async fn test_product_parent_query_product() {
+ let env = TestEnv::new(function_name!());
+
+ let parent_dairy = request!(
+ env,
+ register_product_parent(ProductParentStub {
+ description: Some("Dairy replacment products".to_owned()),
+ name: "Dairy replacements".to_owned(),
+ parent: None,
+ })
+ );
+ let parent_dairy_milk = request!(
+ env,
+ register_product_parent(ProductParentStub {
+ description: Some("Dairy replacment products".to_owned()),
+ name: "milk products".to_owned(),
+ parent: Some(parent_dairy),
+ })
+ );
+
+ let unit_property = request!(
+ env,
+ register_unit_property(UnitPropertyStub {
+ description: None,
+ name: "Volume".to_owned()
+ })
+ );
+
+ request!(
+ env,
+ register_product(ProductStub {
+ description: None,
+ name: "Soy milk".to_owned(),
+ parent: Some(parent_dairy_milk),
+ unit_property,
+ })
+ );
+
+ request!(
+ env,
+ register_product(ProductStub {
+ description: None,
+ name: "Cheese".to_owned(),
+ parent: Some(parent_dairy),
+ unit_property,
+ })
+ );
+
+ assert_eq!(
+ request!(env, products_by_product_parent_id_indirect(parent_dairy_milk))
+ .into_iter()
+ .map(|product| product.name)
+ .collect::<Vec<_>>(),
+ vec!["Soy milk".to_owned()],
+ );
+
+ assert_eq!(
+ request!(env, products_by_product_parent_id_direct(parent_dairy_milk))
+ .into_iter()
+ .map(|product| product.name)
+ .collect::<Vec<_>>(),
+ vec!["Soy milk".to_owned()],
+ );
+
+ assert_eq!(
+ request!(env, products_by_product_parent_id_indirect(parent_dairy))
+ .into_iter()
+ .map(|product| product.name)
+ .collect::<Vec<_>>(),
+ vec!["Cheese".to_owned(), "Soy milk".to_owned(),],
+ );
+
+ assert_eq!(
+ request!(env, products_by_product_parent_id_direct(parent_dairy))
+ .into_iter()
+ .map(|product| product.name)
+ .collect::<Vec<_>>(),
+ vec!["Cheese".to_owned()],
+ );
+}
diff --git a/crates/rocie-server/tests/product_parents/register.rs b/crates/rocie-server/tests/product_parents/register.rs
new file mode 100644
index 0000000..c84ffea
--- /dev/null
+++ b/crates/rocie-server/tests/product_parents/register.rs
@@ -0,0 +1,84 @@
+use rocie_client::{
+ apis::{
+ api_get_product_api::product_by_id, api_set_product_api::register_product,
+ api_set_product_parent_api::register_product_parent,
+ api_set_unit_property_api::register_unit_property,
+ },
+ models::{ProductParentStub, ProductStub, UnitPropertyStub},
+};
+
+use crate::{
+ _testenv::init::function_name,
+ testenv::{TestEnv, log::request},
+};
+
+#[tokio::test]
+async fn test_product_parent_register_roundtrip() {
+ let env = TestEnv::new(function_name!());
+
+ let parent_dairy = request!(
+ env,
+ register_product_parent(ProductParentStub {
+ description: Some("Dairy replacment products".to_owned()),
+ name: "Dairy replacements".to_owned(),
+ parent: None,
+ })
+ );
+ let parent_dairy_milk = request!(
+ env,
+ register_product_parent(ProductParentStub {
+ description: Some("Milk replacment products".to_owned()),
+ name: "Milk replacements".to_owned(),
+ parent: Some(parent_dairy),
+ })
+ );
+
+ let unit_property = request!(
+ env,
+ register_unit_property(UnitPropertyStub {
+ description: Some("The total volume of a product".to_owned()),
+ name: "Volume".to_owned()
+ })
+ );
+
+ let product_soy_milk = request!(
+ env,
+ register_product(ProductStub {
+ description: Some("A soy based alternative to milk".to_owned()),
+ name: "Soy drink".to_owned(),
+ parent: Some(parent_dairy_milk),
+ unit_property,
+ })
+ );
+ let product_oat_milk = request!(
+ env,
+ register_product(ProductStub {
+ description: Some("A oat based alternative to milk".to_owned()),
+ name: "Oat drink".to_owned(),
+ parent: Some(parent_dairy_milk),
+ unit_property,
+ })
+ );
+
+ let product_vegan_cheese = request!(
+ env,
+ register_product(ProductStub {
+ description: None,
+ name: "Vegan cheese".to_owned(),
+ parent: Some(parent_dairy),
+ unit_property,
+ })
+ );
+
+ for product in [product_soy_milk, product_oat_milk] {
+ let product = request!(env, product_by_id(product));
+
+ assert_eq!(product.parent, Some(parent_dairy_milk));
+ }
+
+ {
+ let product = request!(env, product_by_id(product_vegan_cheese));
+
+ assert_eq!(product.parent, Some(parent_dairy));
+ }
+}
diff --git a/crates/rocie-server/tests/products/mod.rs b/crates/rocie-server/tests/products/mod.rs
index 886e5b7..ee139f0 100644
--- a/crates/rocie-server/tests/products/mod.rs
+++ b/crates/rocie-server/tests/products/mod.rs
@@ -20,7 +20,7 @@ async fn create_product(env: &TestEnv, unit_property: UnitPropertyId, name: &str
request!(
env,
register_product(ProductStub {
- description: Some(None),
+ description: None,
name: name.to_owned(),
parent: None,
unit_property
@@ -31,7 +31,7 @@ async fn create_unit(env: &TestEnv, name: &str, unit_property: UnitPropertyId) -
request!(
env,
register_unit(UnitStub {
- description: Some(None),
+ description: None,
full_name_plural: name.to_owned(),
full_name_singular: name.to_owned(),
short_name: name.to_owned(),
diff --git a/crates/rocie-server/tests/products/register.rs b/crates/rocie-server/tests/products/register.rs
index 4284bd1..bae7bc7 100644
--- a/crates/rocie-server/tests/products/register.rs
+++ b/crates/rocie-server/tests/products/register.rs
@@ -18,7 +18,7 @@ async fn test_product_register_roundtrip() {
let unit_property = request!(
env,
register_unit_property(UnitPropertyStub {
- description: Some(Some("The total mass of a product".to_owned())),
+ description: Some("The total mass of a product".to_owned()),
name: "Mass".to_owned()
})
);
@@ -26,7 +26,7 @@ async fn test_product_register_roundtrip() {
let id = request!(
env,
register_product(ProductStub {
- description: Some(Some("A soy based alternative to milk".to_owned())),
+ description: Some("A soy based alternative to milk".to_owned()),
name: "Soy drink".to_owned(),
parent: None,
unit_property,
@@ -38,11 +38,8 @@ async fn test_product_register_roundtrip() {
assert_eq!(&product.name, "Soy drink");
assert_eq!(
product.description,
- Some(Some("A soy based alternative to milk".to_owned()))
+ Some("A soy based alternative to milk".to_owned())
);
assert_eq!(product.id, id);
- // assert_eq!(
- // product.parent,
- // None,
- // );
+ assert_eq!(product.parent, None);
}
diff --git a/crates/rocie-server/tests/recipies/mod.rs b/crates/rocie-server/tests/recipies/mod.rs
new file mode 100644
index 0000000..e8aa3c2
--- /dev/null
+++ b/crates/rocie-server/tests/recipies/mod.rs
@@ -0,0 +1,24 @@
+use rocie_client::{
+ apis::{api_get_recipe_api::recipe_by_id, api_set_recipe_api::add_recipe},
+ models::RecipeStub,
+};
+
+use crate::testenv::{TestEnv, init::function_name, log::request};
+
+#[tokio::test]
+async fn test_recipe_roundtrip() {
+ let env = TestEnv::new(function_name!());
+
+ let recipe_id = request!(
+ env,
+ add_recipe(RecipeStub {
+ path: "/asia/curry".to_owned(),
+ content: "just make the curry".to_owned(),
+ })
+ );
+
+ let output = request!(env, recipe_by_id(recipe_id));
+
+ assert_eq!(output.path, "/asia/curry".to_owned());
+ assert_eq!(output.content, "just make the curry".to_owned());
+}
diff --git a/crates/rocie-server/tests/tests.rs b/crates/rocie-server/tests/tests.rs
index cf34156..3759042 100644
--- a/crates/rocie-server/tests/tests.rs
+++ b/crates/rocie-server/tests/tests.rs
@@ -4,4 +4,6 @@ mod _testenv;
pub(crate) use _testenv as testenv;
mod products;
+mod product_parents;
mod units;
+mod recipies;
diff --git a/crates/rocie-server/tests/units/fetch.rs b/crates/rocie-server/tests/units/fetch.rs
new file mode 100644
index 0000000..b0bfffb
--- /dev/null
+++ b/crates/rocie-server/tests/units/fetch.rs
@@ -0,0 +1,88 @@
+use rocie_client::{
+ apis::{
+ api_get_unit_api::units_by_property_id, api_set_unit_api::register_unit,
+ api_set_unit_property_api::register_unit_property,
+ },
+ models::{UnitPropertyStub, UnitStub},
+};
+
+use crate::testenv::{TestEnv, init::function_name, log::request};
+
+#[tokio::test]
+async fn test_units_fetch_by_property_id() {
+ let env = TestEnv::new(function_name!());
+
+ let unit_property = request!(
+ env,
+ register_unit_property(UnitPropertyStub {
+ description: Some("The total mass of a product".to_owned()),
+ name: "Mass".to_owned()
+ })
+ );
+ request!(
+ env,
+ register_unit(UnitStub {
+ description: Some("Fancy new unit".to_owned()),
+ full_name_plural: "Grams".to_owned(),
+ full_name_singular: "Gram".to_owned(),
+ short_name: "g".to_owned(),
+ unit_property,
+ })
+ );
+ request!(
+ env,
+ register_unit(UnitStub {
+ description: Some("Better new unit (we should make it SI)".to_owned()),
+ full_name_plural: "Kilograms".to_owned(),
+ full_name_singular: "Kilogram".to_owned(),
+ short_name: "kg".to_owned(),
+ unit_property,
+ })
+ );
+
+ let unit_property2 = request!(
+ env,
+ register_unit_property(UnitPropertyStub {
+ description: Some("The total volume of a product".to_owned()),
+ name: "Volume".to_owned()
+ })
+ );
+ request!(
+ env,
+ register_unit(UnitStub {
+ description: Some("Fancy new unit".to_owned()),
+ full_name_plural: "Liter".to_owned(),
+ full_name_singular: "Liter".to_owned(),
+ short_name: "l".to_owned(),
+ unit_property: unit_property2,
+ })
+ );
+ request!(
+ env,
+ register_unit(UnitStub {
+ description: Some("Better new unit (we should make it SI)".to_owned()),
+ full_name_plural: "Mililiters".to_owned(),
+ full_name_singular: "Mililiter".to_owned(),
+ short_name: "ml".to_owned(),
+ unit_property: unit_property2,
+ })
+ );
+
+ let units = request!(env, units_by_property_id(unit_property));
+ let other_units = request!(env, units_by_property_id(unit_property2));
+
+ assert_eq!(
+ units
+ .iter()
+ .map(|unit| unit.short_name.clone())
+ .collect::<Vec<_>>(),
+ vec!["g".to_owned(), "kg".to_owned(),]
+ );
+ assert_eq!(
+ other_units
+ .iter()
+ .map(|unit| unit.short_name.clone())
+ .collect::<Vec<_>>(),
+ vec!["l".to_owned(), "ml".to_owned(),]
+ );
+}
diff --git a/crates/rocie-server/tests/units/mod.rs b/crates/rocie-server/tests/units/mod.rs
index 5518167..0481d71 100644
--- a/crates/rocie-server/tests/units/mod.rs
+++ b/crates/rocie-server/tests/units/mod.rs
@@ -1 +1,2 @@
mod register;
+mod fetch;
diff --git a/crates/rocie-server/tests/units/register.rs b/crates/rocie-server/tests/units/register.rs
index 5367b55..8181c25 100644
--- a/crates/rocie-server/tests/units/register.rs
+++ b/crates/rocie-server/tests/units/register.rs
@@ -18,7 +18,7 @@ async fn test_unit_register_roundtrip() {
let unit_property = request!(
env,
register_unit_property(UnitPropertyStub {
- description: Some(Some("The total mass of a product".to_owned())),
+ description: Some("The total mass of a product".to_owned()),
name: "Mass".to_owned()
})
);
@@ -26,7 +26,7 @@ async fn test_unit_register_roundtrip() {
let id = request!(
env,
register_unit(UnitStub {
- description: Some(Some("Fancy new unit".to_owned())),
+ description: Some("Fancy new unit".to_owned()),
full_name_plural: "Grams".to_owned(),
full_name_singular: "Gram".to_owned(),
short_name: "g".to_owned(),
@@ -39,7 +39,7 @@ async fn test_unit_register_roundtrip() {
assert_eq!(&unit.short_name, "g");
assert_eq!(&unit.full_name_plural, "Grams");
assert_eq!(&unit.full_name_singular, "Gram");
- assert_eq!(unit.description, Some(Some("Fancy new unit".to_owned())));
+ assert_eq!(unit.description, Some("Fancy new unit".to_owned()));
assert_eq!(unit.id, id);
}