Implement categories

This commit is contained in:
Adrian Woźniak 2022-11-29 11:11:04 +01:00
parent 864f9842f0
commit df1594e3f4
10 changed files with 581 additions and 138 deletions

View File

@ -24,4 +24,4 @@ thiserror = { version = "1.0.37" }
tokio = { version = "1.21.2", features = ['full'] }
tracing = { version = "0.1.37" }
whatlang = { version = "0.16.2" }
strum = { version = "0.24.1", features = ['strum_macros', 'default'] }
strum = { version = "0.24.1", features = ['strum_macros', 'default', 'derive'] }

View File

@ -1,4 +1,58 @@
use strum::IntoStaticStr;
use rumqttc::QoS;
use crate::AsyncClient;
impl AsyncClient {
pub async fn emit_category_created(&self, category: &model::v2::Category) {
self.publish_or_log(Topic::CategoryCreated, QoS::AtLeastOnce, true, category)
.await
}
pub async fn emit_category_updated(&self, category: &model::v2::Category) {
self.publish_or_log(Topic::CategoryUpdated, QoS::AtLeastOnce, true, category)
.await
}
pub async fn emit_category_deleted(&self, category_id: &model::v2::CategoryId) {
self.publish_or_log(Topic::CategoryDeleted, QoS::AtLeastOnce, true, category_id)
.await
}
}
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum Topic {
CategoryCreated,
CategoryUpdated,
CategoryDeleted,
}
impl Topic {
pub fn to_str(self) -> &'static str {
match self {
Topic::CategoryCreated => "category/created",
Topic::CategoryUpdated => "category/updated",
Topic::CategoryDeleted => "category/deleted",
}
}
}
impl Into<String> for Topic {
fn into(self) -> String {
self.to_str().into()
}
}
impl<'s> PartialEq<&'s str> for Topic {
fn eq(&self, other: &&'s str) -> bool {
self.to_str() == *other
}
}
impl PartialEq<String> for Topic {
fn eq(&self, other: &String) -> bool {
self.to_str() == other.as_str()
}
}
pub mod create_category {
use model::v2::*;
@ -32,7 +86,9 @@ pub mod delete_category {
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Details {}
pub struct Details {
pub category: Option<Category>,
}
pub type Output = Result<Details, Error>;
}
@ -53,8 +109,7 @@ pub mod update_category {
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Details {
pub limit: Limit,
pub offset: Offset,
pub category: Category,
}
pub type Output = Result<Details, Error>;
@ -66,7 +121,10 @@ pub mod all_categories {
use crate::stocks::Error;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Input {}
pub struct Input {
pub limit: Limit,
pub offset: Offset,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Details {
@ -75,12 +133,3 @@ pub mod all_categories {
pub type Output = Result<Details, Error>;
}
#[derive(
Copy, Clone, Debug, PartialOrd, PartialEq, serde::Serialize, serde::Deserialize, IntoStaticStr,
)]
pub enum Topic {
CategoryCreated,
CategoryUpdated,
CategoryDeleted,
}

View File

@ -7,7 +7,7 @@ pub mod product_variant;
pub use categories::*;
pub use load::*;
use model::v2::{ProductId, ProductVariantId};
use model::v2::{CategoryId, ProductId, ProductVariantId};
pub use product::*;
pub use product_photo::*;
pub use product_stock::*;
@ -56,7 +56,13 @@ pub enum Error {
#[error("Failed to load product variants {0:?}")]
FindProductVariants(Vec<ProductVariantId>),
#[error("Failed to load all categories")]
Categories,
AllCategories,
#[error("Failed to crate products category")]
CreateCategory,
#[error("Failed to delete products category {0:?}")]
DeleteCategory(CategoryId),
#[error("Failed to update products category {0:?}")]
UpdateCategory(CategoryId),
}
pub mod rpc {
@ -67,10 +73,13 @@ pub mod rpc {
use crate::stocks::product_photo::*;
use crate::stocks::product_stock::*;
use crate::stocks::product_variant::*;
use crate::stocks::{all_categories, create_category, delete_category, update_category};
#[tarpc::service]
pub trait Stocks {
// Product
// ####################
// PRODUCT
// ####################
/// Create new product.
async fn create_product(input: create_product::Input) -> create_product::Output;
/// Update product information.
@ -78,7 +87,21 @@ pub mod rpc {
/// Delete product.
async fn delete_product(input: delete_product::Input) -> delete_product::Output;
// Product variant
// ####################
// DETAILED PRODUCT
// ####################
/// Single product with stock size and photos
async fn detailed_product(input: detailed_product::Input) -> detailed_product::Output;
/// List of products with stock size and photos
async fn detailed_products(input: detailed_products::Input) -> detailed_products::Output;
/// List of products for shopping cart
async fn shopping_cart_products(input: find_products::Input) -> find_products::Output;
// ####################
// PRODUCT VARIANTS
// ####################
/// Create new variant of the product.
async fn create_product_variant(
input: create_product_variant::Input,
@ -92,7 +115,14 @@ pub mod rpc {
input: delete_product_variant::Input,
) -> delete_product_variant::Output;
// Product photo
/// List of products variants for shopping cart
async fn shopping_cart_product_variants(
input: find_product_variants::Input,
) -> find_product_variants::Output;
// ####################
// PRODUCT PHOTO
// ####################
/// Load all photos
async fn all_product_photo(input: all_product_photo::Input) -> all_product_photo::Output;
@ -104,7 +134,9 @@ pub mod rpc {
input: delete_product_photo::Input,
) -> delete_product_photo::Output;
// Product stock
// ####################
// PRODUCT STOCK
// ####################
/// Create product stock.
async fn create_product_stock(
input: create_product_stock::Input,
@ -114,22 +146,20 @@ pub mod rpc {
input: update_product_stock::Input,
) -> update_product_stock::Output;
// Load
/// Single product with stock size and photos
async fn detailed_product(input: detailed_product::Input) -> detailed_product::Output;
// ####################
// CATEGORIES
// ####################
/// Create new products category
async fn create_category(input: create_category::Input) -> create_category::Output;
/// List of products with stock size and photos
async fn detailed_products(input: detailed_products::Input) -> detailed_products::Output;
/// Delete existing products category
async fn delete_category(input: delete_category::Input) -> delete_category::Output;
/// List of products for shopping cart
async fn shopping_cart_products(input: find_products::Input) -> find_products::Output;
///Change existing products category
async fn update_category(input: update_category::Input) -> update_category::Output;
/// List of products variants for shopping cart
async fn shopping_cart_product_variants(
input: find_product_variants::Input,
) -> find_product_variants::Output;
// async fn
/// List of call products categories
async fn all_categories(input: all_categories::Input) -> all_categories::Output;
}
pub async fn create_client(config: SharedAppConfig) -> StocksClient {

View File

@ -1626,6 +1626,7 @@ pub mod v2 {
#[derive(
Debug,
Clone,
Copy,
Default,
PartialOrd,
PartialEq,

View File

@ -0,0 +1,176 @@
use channels::stocks::{all_categories, create_category, delete_category, update_category, Error};
use channels::AsyncClient;
use config::SharedAppConfig;
use db_utils::PgT;
use crate::db::{AllCategories, CreateCategory, Database, DeleteCategory, UpdateCategory};
use crate::{begin_t, dbm_run};
pub async fn create_category(
input: create_category::Input,
db: Database,
_mqtt: AsyncClient,
_config: SharedAppConfig,
) -> create_category::Output {
let mut t = begin_t!(db, Error::InternalServerError);
let res = inner_create_category(input, &mut t).await;
match res {
Ok(v) => {
if let Err(e) = t.commit().await {
tracing::error!("{}", e);
return Err(Error::InternalServerError);
} else {
Ok(v)
}
}
Err(e) => {
if let Err(e) = t.rollback().await {
tracing::error!("{}", e);
}
Err(e)
}
}
}
async fn inner_create_category(
input: create_category::Input,
t: &mut PgT<'_>,
) -> create_category::Output {
let dbm = CreateCategory {
parent_id: input.parent_id,
name: input.name,
key: input.key,
svg: input.svg,
};
Ok(create_category::Details {
product: dbm_run!(dbm, t, Error::CreateCategory),
})
}
pub async fn delete_category(
input: delete_category::Input,
db: Database,
_mqtt: AsyncClient,
_config: SharedAppConfig,
) -> delete_category::Output {
let mut t = begin_t!(db, Error::InternalServerError);
let res = inner_delete_category(input, &mut t).await;
match res {
Ok(v) => {
if let Err(e) = t.commit().await {
tracing::error!("{}", e);
return Err(Error::InternalServerError);
} else {
Ok(v)
}
}
Err(e) => {
if let Err(e) = t.rollback().await {
tracing::error!("{}", e);
}
Err(e)
}
}
}
async fn inner_delete_category(
input: delete_category::Input,
t: &mut PgT<'_>,
) -> delete_category::Output {
let dbm = DeleteCategory {
category_id: input.category_id,
};
Ok(delete_category::Details {
category: dbm_run!(dbm, t, Error::DeleteCategory(input.category_id)),
})
}
pub async fn update_category(
input: update_category::Input,
db: Database,
_mqtt: AsyncClient,
_config: SharedAppConfig,
) -> update_category::Output {
let mut t = begin_t!(db, Error::InternalServerError);
let res = inner_update_category(input, &mut t).await;
match res {
Ok(v) => {
if let Err(e) = t.commit().await {
tracing::error!("{}", e);
return Err(Error::InternalServerError);
} else {
Ok(v)
}
}
Err(e) => {
if let Err(e) = t.rollback().await {
tracing::error!("{}", e);
}
Err(e)
}
}
}
async fn inner_update_category(
input: update_category::Input,
t: &mut PgT<'_>,
) -> update_category::Output {
let dbm = UpdateCategory {
id: input.id,
parent_id: input.parent_id,
name: input.name,
key: input.key,
svg: input.svg,
};
Ok(update_category::Details {
category: dbm_run!(dbm, t, Error::UpdateCategory(input.id)),
})
}
pub async fn all_categories(
input: all_categories::Input,
db: Database,
_mqtt: AsyncClient,
_config: SharedAppConfig,
) -> all_categories::Output {
let mut t = begin_t!(db, Error::InternalServerError);
let res = inner_all_categories(input, &mut t).await;
match res {
Ok(v) => {
if let Err(e) = t.commit().await {
tracing::error!("{}", e);
return Err(Error::InternalServerError);
} else {
Ok(v)
}
}
Err(e) => {
if let Err(e) = t.rollback().await {
tracing::error!("{}", e);
}
Err(e)
}
}
}
async fn inner_all_categories(
input: all_categories::Input,
t: &mut PgT<'_>,
) -> all_categories::Output {
let dbm = AllCategories {
limit: input.limit,
offset: input.offset,
};
Ok(all_categories::Details {
categories: dbm_run!(dbm, t, Error::AllCategories),
})
}

View File

@ -61,7 +61,7 @@ async fn inner_detailed_product(
limit: Limit::from_u32(2000),
offset: Offset::from_u32(0),
};
let categories = dbm_run!(dbm, t, Error::Categories);
let categories = dbm_run!(dbm, t, Error::AllCategories);
let mut variants = utils::vec_to_hash_vec(variants, 10, |p| p.product_id);
let mut stocks = utils::vec_to_hash_vec(stocks, 10, |s| s.product_variant_id);
@ -146,7 +146,7 @@ async fn inner_detailed_products(
limit: Limit::from_u32(2000),
offset: Offset::from_u32(0),
};
let categories = dbm_run!(dbm, t, Error::Categories);
let categories = dbm_run!(dbm, t, Error::AllCategories);
let mut variants = utils::vec_to_hash_vec(variants, 10, |p| p.product_id);
let mut stocks = utils::vec_to_hash_vec(stocks, 10, |s| s.product_variant_id);

View File

@ -1,9 +1,11 @@
pub mod categories;
pub mod load;
pub mod product;
pub mod product_photo;
pub mod product_stock;
pub mod product_variant;
pub use categories::*;
pub use load::*;
pub use product::*;
pub use product_photo::*;

View File

@ -8,6 +8,10 @@ pub enum Error {
All,
#[error("Failed to create category")]
Create,
#[error("Failed to delete category {0:?}")]
Delete(CategoryId),
#[error("Failed to update category {0:?}")]
Update(CategoryId),
}
pub type Result<T> = std::result::Result<T, Error>;
@ -46,6 +50,74 @@ RETURNING id,
}
}
pub struct UpdateCategory {
pub id: CategoryId,
pub parent_id: Option<CategoryId>,
pub name: CategoryName,
pub key: CategoryKey,
pub svg: CategorySvg,
}
impl UpdateCategory {
/// Update category
///
/// ## Example
///
/// ```rust
/// async fn change(t: &mut PgT<'_>) {
/// let dbm = UpdateCategory {};
/// dbm.run(t).await;
/// }
/// ```
pub async fn run(self, t: &mut PgT<'_>) -> Result<Category> {
sqlx::query_as(
r#"
UPDATE categories
SET parent_id = $2,
name = $3,
key = $4,
svg = $5
WHERE id = $1
RETURNING id,
parent_id,
name,
key,
svg
"#,
)
.bind(self.id)
.bind(self.parent_id)
.bind(self.name)
.bind(self.key)
.bind(self.svg)
.fetch_one(t)
.await
.map_err(|e| {
tracing::warn!("{e:?}");
dbg!(e);
Error::Update(self.id)
})
}
}
pub struct DeleteCategory {
pub category_id: CategoryId,
}
impl DeleteCategory {
pub async fn run(self, t: &mut PgT<'_>) -> Result<Option<Category>> {
sqlx::query_as("DELETE FROM categories WHERE id = $1")
.bind(self.category_id)
.fetch_optional(t)
.await
.map_err(|e| {
tracing::error!("{}", e);
dbg!(e);
Error::Delete(self.category_id)
})
}
}
pub struct AllCategories {
pub limit: Limit,
pub offset: Offset,
@ -84,7 +156,7 @@ mod tests {
use model::v2::{Category, CategoryName};
use model::{Limit, Offset};
use crate::db::{AllCategories, CreateCategory, Database};
use crate::db::{AllCategories, CreateCategory, Database, DeleteCategory, UpdateCategory};
struct NoOpts;
@ -121,4 +193,88 @@ mod tests {
assert_eq!(res.unwrap().len(), 3);
}
#[tokio::test]
async fn delete_one() {
testx::db_t_ref!(t);
test_category("Electronics".into(), &mut t).await;
let second = test_category("Shoes".into(), &mut t).await;
test_category("Pants".into(), &mut t).await;
let len1 = AllCategories {
limit: Limit::from_u32(2000),
offset: Offset::from_u32(0),
}
.run(&mut t)
.await;
let deleted1 = DeleteCategory {
category_id: second.id,
}
.run(&mut t)
.await;
let deleted2 = DeleteCategory {
category_id: second.id,
}
.run(&mut t)
.await;
let len2 = AllCategories {
limit: Limit::from_u32(2000),
offset: Offset::from_u32(0),
}
.run(&mut t)
.await;
testx::db_rollback!(t);
let len1 = len1.unwrap();
let deleted1 = deleted1.unwrap();
let deleted2 = deleted2.unwrap();
let len2 = len2.unwrap();
assert_eq!(len1.len(), 3);
assert_eq!(len2.len(), 2);
assert!(deleted1.is_some());
assert!(deleted2.is_none());
}
#[tokio::test]
async fn update_one() {
testx::db_t_ref!(t);
let first = test_category("Electronics".into(), &mut t).await;
let second = test_category("Shoes".into(), &mut t).await;
let third = test_category("Pants".into(), &mut t).await;
let res = UpdateCategory {
id: second.id,
parent_id: None,
name: "Wearables".into(),
key: second.key.clone(),
svg: second.svg.clone(),
}
.run(&mut t)
.await;
let all = AllCategories {
limit: Limit::from_u32(2000),
offset: Offset::from_u32(0),
}
.run(&mut t)
.await;
testx::db_rollback!(t);
let res = res.unwrap();
let all = all.unwrap();
assert_eq!(res.name, "Wearables".into());
assert_eq!(res.id, second.id);
assert_eq!(all[0].name, first.name);
assert_eq!(all[1].name, "Wearables".into());
assert_eq!(all[2].name, third.name);
}
}

View File

@ -2,75 +2,75 @@ use model::v2::*;
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)]
pub enum Error {
#[error("Unable to load all stocks")]
All,
// #[error("Unable to load all stocks")]
// All,
#[error("Unable to create stock")]
Create,
#[error("Unable to update stock {0:?}")]
Update(StockId),
#[error("Unable to delete stock {0:?}")]
Delete(StockId),
// #[error("Unable to delete stock {0:?}")]
// Delete(StockId),
// #[error("Unable to delete all stock for variant {0:?}")]
// DeleteAllProductStocks(ProductId),
#[error("Unable find stock for product")]
ProductVariantStock,
#[error("Stock {0:?} does not exists")]
NotFound(StockId),
// #[error("Stock {0:?} does not exists")]
// NotFound(StockId),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub struct AllStocks {
pub limit: i32,
pub offset: i32,
}
impl AllStocks {
pub async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Vec<Stock>> {
sqlx::query_as(
r#"
SELECT id, product_variant_id, quantity, quantity_unit
FROM stocks
ORDER BY id ASC
LIMIT $1 OFFSET $2
"#,
)
.bind(self.limit)
.bind(self.offset)
.fetch_all(pool)
.await
.map_err(|e| {
tracing::warn!("{e:?}");
Error::All
})
}
}
#[derive(Debug)]
pub struct FindStock {
pub id: StockId,
}
impl FindStock {
pub async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Stock> {
sqlx::query_as(
r#"
SELECT id, product_variant_id, quantity, quantity_unit
FROM stocks
WHERE id = $1
"#,
)
.bind(self.id)
.fetch_one(pool)
.await
.map_err(|e| {
tracing::warn!("{e:?}");
dbg!(e);
Error::NotFound(self.id)
})
}
}
// #[derive(Debug)]
// pub struct AllStocks {
// pub limit: i32,
// pub offset: i32,
// }
//
// impl AllStocks {
// pub async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>)
// -> Result<Vec<Stock>> { sqlx::query_as(
// r#"
// SELECT id, product_variant_id, quantity, quantity_unit
// FROM stocks
// ORDER BY id ASC
// LIMIT $1 OFFSET $2
// "#,
// )
// .bind(self.limit)
// .bind(self.offset)
// .fetch_all(pool)
// .await
// .map_err(|e| {
// tracing::warn!("{e:?}");
// Error::All
// })
// }
// }
//
// #[derive(Debug)]
// pub struct FindStock {
// pub id: StockId,
// }
//
// impl FindStock {
// pub async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>)
// -> Result<Stock> { sqlx::query_as(
// r#"
// SELECT id, product_variant_id, quantity, quantity_unit
// FROM stocks
// WHERE id = $1
// "#,
// )
// .bind(self.id)
// .fetch_one(pool)
// .await
// .map_err(|e| {
// tracing::warn!("{e:?}");
// dbg!(e);
// Error::NotFound(self.id)
// })
// }
// }
#[derive(Debug)]
pub struct CreateStock {
@ -134,32 +134,32 @@ RETURNING id, product_variant_id, quantity, quantity_unit
}
}
#[derive(Debug)]
pub struct DeleteStock {
pub stock_id: StockId,
}
impl DeleteStock {
pub async fn run(
self,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<Stock>> {
sqlx::query_as(
r#"
DELETE FROM stocks
WHERE id = $1
RETURNING id, product_variant_id, quantity, quantity_unit
"#,
)
.bind(self.stock_id)
.fetch_optional(pool)
.await
.map_err(|e| {
tracing::warn!("{e:?}");
Error::Delete(self.stock_id)
})
}
}
// #[derive(Debug)]
// pub struct DeleteStock {
// pub stock_id: StockId,
// }
//
// impl DeleteStock {
// pub async fn run(
// self,
// pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
// ) -> Result<Option<Stock>> {
// sqlx::query_as(
// r#"
// DELETE FROM stocks
// WHERE id = $1
// RETURNING id, product_variant_id, quantity, quantity_unit
// "#,
// )
// .bind(self.stock_id)
// .fetch_optional(pool)
// .await
// .map_err(|e| {
// tracing::warn!("{e:?}");
// Error::Delete(self.stock_id)
// })
// }
// }
#[derive(Debug)]
pub struct ProductVariantsStock {

View File

@ -6,6 +6,7 @@ use crate::db::Database;
use crate::rpc::rpc::StocksServer;
pub mod rpc {
use channels::stocks::create_category::{Input, Output};
use channels::stocks::rpc::Stocks;
use channels::stocks::*;
use config::SharedAppConfig;
@ -46,6 +47,30 @@ pub mod rpc {
actions::delete_product(input, self.db, self.mqtt_client, self.config).await
}
async fn detailed_product(
self,
_: context::Context,
input: detailed_product::Input,
) -> detailed_product::Output {
actions::detailed_product(input, self.db, self.mqtt_client, self.config).await
}
async fn detailed_products(
self,
_: context::Context,
input: detailed_products::Input,
) -> detailed_products::Output {
actions::detailed_products(input, self.db, self.mqtt_client, self.config).await
}
async fn shopping_cart_products(
self,
_: context::Context,
input: find_products::Input,
) -> find_products::Output {
actions::find_products(input, self.db, self.mqtt_client, self.config).await
}
async fn create_product_variant(
self,
_: context::Context,
@ -70,6 +95,14 @@ pub mod rpc {
actions::delete_product_variant(input, self.db, self.mqtt_client, self.config).await
}
async fn shopping_cart_product_variants(
self,
_: context::Context,
input: find_product_variants::Input,
) -> find_product_variants::Output {
actions::find_product_variants(input, self.db, self.mqtt_client, self.config).await
}
async fn all_product_photo(
self,
_: context::Context,
@ -110,36 +143,32 @@ pub mod rpc {
actions::update_product_stock(input, self.db, self.mqtt_client, self.config).await
}
async fn detailed_product(
self,
_: context::Context,
input: detailed_product::Input,
) -> detailed_product::Output {
actions::detailed_product(input, self.db, self.mqtt_client, self.config).await
async fn create_category(self, _: context::Context, input: Input) -> Output {
actions::create_category(input, self.db, self.mqtt_client, self.config).await
}
async fn detailed_products(
async fn delete_category(
self,
_: context::Context,
input: detailed_products::Input,
) -> detailed_products::Output {
actions::detailed_products(input, self.db, self.mqtt_client, self.config).await
input: delete_category::Input,
) -> delete_category::Output {
actions::delete_category(input, self.db, self.mqtt_client, self.config).await
}
async fn shopping_cart_products(
async fn update_category(
self,
_: context::Context,
input: find_products::Input,
) -> find_products::Output {
actions::find_products(input, self.db, self.mqtt_client, self.config).await
input: update_category::Input,
) -> update_category::Output {
actions::update_category(input, self.db, self.mqtt_client, self.config).await
}
async fn shopping_cart_product_variants(
async fn all_categories(
self,
_: context::Context,
input: find_product_variants::Input,
) -> find_product_variants::Output {
actions::find_product_variants(input, self.db, self.mqtt_client, self.config).await
input: all_categories::Input,
) -> all_categories::Output {
actions::all_categories(input, self.db, self.mqtt_client, self.config).await
}
}
}