diff --git a/actors/database_manager/src/lib.rs b/actors/database_manager/src/lib.rs index f8a4997..666d1dd 100644 --- a/actors/database_manager/src/lib.rs +++ b/actors/database_manager/src/lib.rs @@ -1,6 +1,7 @@ use actix::{Actor, Context}; use config::SharedAppConfig; use sqlx::PgPool; +use sqlx_core::arguments::Arguments; pub use crate::account_orders::*; pub use crate::accounts::*; @@ -156,3 +157,97 @@ impl Database { impl Actor for Database { type Context = Context; } + +/// Multi-query load for large amount of records to read +/// +/// Examples +/// +/// ``` +/// # use database_manager::photos::Error; +/// async fn load() { +/// # let pool: sqlx::PgPool::connect("").await.unwrap(); +/// use database_manager::MultiLoad; +/// let t = pool.begin().await.unwrap(); +/// let mut multi = MultiLoad::new( +/// &mut t, +/// "SELECT id, name FROM products WHERE ", +/// " id = " +/// ); +/// multi.load(4, vec![1,2,3,4], |_| Error::All.into()); +/// t.commit().await.unwrap(); +/// } +/// ``` +pub struct MultiLoad<'transaction, 'transaction2, 'header, 'condition, T> { + pool: &'transaction mut sqlx::Transaction<'transaction2, sqlx::Postgres>, + header: &'header str, + condition: &'condition str, + __phantom: std::marker::PhantomData, +} + +impl<'transaction, 'transaction2, 'header, 'condition, T> + MultiLoad<'transaction, 'transaction2, 'header, 'condition, T> +where + T: for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow> + Send + Unpin, +{ + pub fn new( + pool: &'transaction mut sqlx::Transaction<'transaction2, sqlx::Postgres>, + header: &'header str, + condition: &'condition str, + ) -> Self { + Self { + pool, + header, + condition, + __phantom: Default::default(), + } + } + + pub async fn load<'query, Error, Ids>( + &mut self, + len: usize, + items: Ids, + on_error: Error, + ) -> std::result::Result, crate::Error> + where + Ids: Iterator, + Error: Fn(sqlx::Error) -> crate::Error, + { + let mut res = Vec::new(); + + for ids in items.fold( + Vec::>::with_capacity(len), + |mut v, id| { + if matches!(v.last().map(|v| v.len()), Some(20) | None) { + v.push(Vec::with_capacity(20)); + } + v.last_mut().unwrap().push(id); + v + }, + ) { + let query: String = self.header.into(); + let query = ids.iter().enumerate().fold(query, |mut q, (idx, _id)| { + if idx != 0 { + q.push_str(" OR"); + } + q.push_str(&format!(" {} ${}", self.condition, idx + 1)); + q + }); + let q = sqlx::query_as_with( + query.as_str(), + ids.into_iter() + .fold(sqlx::postgres::PgArguments::default(), |mut args, id| { + args.add(id); + args + }), + ); + + let records: Vec = match q.fetch_all(&mut *self.pool).await { + Ok(rec) => rec, + Err(e) => return Err(on_error(e)), + }; + res.extend(records); + } + + Ok(res) + } +} diff --git a/actors/database_manager/src/photos.rs b/actors/database_manager/src/photos.rs index 741fe7b..95103ed 100644 --- a/actors/database_manager/src/photos.rs +++ b/actors/database_manager/src/photos.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::{MultiLoad, Result}; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -88,44 +88,23 @@ pub(crate) async fn photos_for_products( pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result> { log::debug!("all product ids {:?}", msg.product_ids); - let mut res: Vec = Vec::with_capacity(100); - - let len = msg.product_ids.len() / 20; - for ids in msg.product_ids.into_iter().fold( - Vec::>::with_capacity(len), - |mut v, id| { - if matches!(v.last().map(|v| v.len()), Some(20) | None) { - v.push(Vec::with_capacity(20)); - } - v.last_mut().unwrap().push(id); - v - }, - ) { - log::debug!("scoped product ids {:?}", ids); - let query: String = r#" -SELECT photos.id, photos.local_path, photos.file_name, product_photos.product_id, photos.unique_name -FROM photos -INNER JOIN product_photos - ON photos.id = product_photos.photo_id -WHERE - "# - .into(); - let query = ids.iter().enumerate().fold(query, |mut q, (idx, _id)| { - if idx != 0 { - q.push_str(" OR"); - } - q.push_str(&format!(" product_photos.product_id = ${}", idx + 1)); - q - }); - let q = sqlx::query_as::<_, model::ProductLinkedPhoto>(query.as_str()); - let q = ids.into_iter().map(|id| *id).fold(q, |q, id| q.bind(id)); - - let records = q.fetch_all(&mut *pool).await.map_err(|e| { - log::error!("{e:?}"); - crate::Error::Photo(Error::All) - })?; - res.extend(records); - } + let res: Vec = MultiLoad::new( + pool, + r#" + SELECT photos.id, photos.local_path, photos.file_name, + product_photos.product_id, photos.unique_name FROM photos + INNER JOIN product_photos + ON photos.id = product_photos.photo_id + WHERE + "#, + " product_photos.product_id =", + ) + .load( + msg.product_ids.len(), + msg.product_ids.into_iter().map(|id| *id), + |_e| crate::Error::Photo(Error::All), + ) + .await?; log::debug!("product linked photos {:?}", res); Ok(res) } diff --git a/actors/database_manager/src/stocks.rs b/actors/database_manager/src/stocks.rs index 9318f2f..7445727 100644 --- a/actors/database_manager/src/stocks.rs +++ b/actors/database_manager/src/stocks.rs @@ -2,7 +2,7 @@ use actix::Message; use model::{ProductId, Quantity, QuantityUnit, Stock, StockId}; use sqlx::PgPool; -use crate::Result; +use crate::{MultiLoad, Result}; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -14,6 +14,8 @@ pub enum Error { Update, #[error("Unable to delete stock")] Delete, + #[error("Unable find stock for product")] + ProductStock, } #[derive(Message)] @@ -124,3 +126,37 @@ RETURNING id, product_id, quantity, quantity_unit crate::Error::Stock(Error::Delete) }) } + +#[derive(Message)] +#[rtype(result = "Result>")] +pub struct ProductsStock { + pub product_ids: Vec, +} + +crate::db_async_handler!( + ProductsStock, + product_stock, + Vec, + inner_product_stock +); + +async fn product_stock( + msg: ProductsStock, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result> { + Ok(MultiLoad::new( + pool, + r#" + SELECT id, product_id, quantity, quantity_unit + FROM stocks + WHERE + "#, + " product_id =", + ) + .load( + msg.product_ids.len(), + msg.product_ids.into_iter().map(|id| *id), + |_e| crate::Error::Stock(Error::ProductStock), + ) + .await?) +} diff --git a/api/src/routes/admin/api_v1/products.rs b/api/src/routes/admin/api_v1/products.rs index 1d57e23..10b62a4 100644 --- a/api/src/routes/admin/api_v1/products.rs +++ b/api/src/routes/admin/api_v1/products.rs @@ -30,14 +30,16 @@ async fn products( let db = db.into_inner(); - let products = admin_send_db!(db, database_manager::AllProducts); + let products: Vec = admin_send_db!(db, database_manager::AllProducts); + let product_ids: Vec = products.iter().map(|p| p.id).collect(); let photos = admin_send_db!( db, database_manager::PhotosForProducts { - product_ids: products.iter().map(|p| p.id).collect() + product_ids: product_ids.clone() } ); - Ok(Json((products, photos, public_path).into())) + let products_stock = admin_send_db!(db, database_manager::ProductsStock { product_ids }); + Ok(Json((products, photos, products_stock, public_path).into())) } #[derive(Deserialize)] diff --git a/api/src/routes/public/api_v1/unrestricted.rs b/api/src/routes/public/api_v1/unrestricted.rs index b4329d5..946c53a 100644 --- a/api/src/routes/public/api_v1/unrestricted.rs +++ b/api/src/routes/public/api_v1/unrestricted.rs @@ -23,14 +23,20 @@ async fn products( }; let products: Vec = public_send_db!(owned, db, database_manager::AllProducts); + let product_ids: Vec = products.iter().map(|p| p.id).collect(); + let products_stock: Vec = public_send_db!( + owned, + db, + database_manager::ProductsStock { + product_ids: product_ids.clone() + } + ); let photos: Vec = public_send_db!( owned, db, - database_manager::PhotosForProducts { - product_ids: products.iter().map(|p| p.id).collect() - } + database_manager::PhotosForProducts { product_ids } ); - Ok(Json((products, photos, public_path).into())) + Ok(Json((products, photos, products_stock, public_path).into())) } #[get("/product/{id}")] @@ -48,6 +54,13 @@ async fn product( let product: model::Product = public_send_db!(owned, db, database_manager::FindProduct { product_id }); + let mut products_stock = public_send_db!( + owned, + db, + database_manager::ProductsStock { + product_ids: vec![product.id] + } + ); let mut photos: Vec = public_send_db!( owned, db, @@ -55,7 +68,15 @@ async fn product( product_ids: vec![product.id] } ); - Ok(Json((product, &mut photos, public_path.as_str()).into())) + Ok(Json( + ( + product, + &mut photos, + &mut products_stock, + public_path.as_str(), + ) + .into(), + )) } #[get("/stocks")] diff --git a/shared/model/src/api.rs b/shared/model/src/api.rs index b92a87f..f59be1c 100644 --- a/shared/model/src/api.rs +++ b/shared/model/src/api.rs @@ -189,11 +189,20 @@ pub struct Product { pub long_description: crate::ProductLongDesc, pub category: Option, pub price: crate::Price, + pub available: bool, + pub quantity_unit: crate::QuantityUnit, pub deliver_days_flag: crate::Days, pub photos: Vec, } -impl<'path> From<(crate::Product, &mut Vec, &'path str)> for Product { +impl<'path> + From<( + crate::Product, + &mut Vec, + &mut Vec, + &'path str, + )> for Product +{ fn from( ( crate::Product { @@ -206,9 +215,22 @@ impl<'path> From<(crate::Product, &mut Vec, &'path str)> for deliver_days_flag, }, photos, + product_stocks, public_path, - ): (crate::Product, &mut Vec, &'path str), + ): ( + crate::Product, + &mut Vec, + &mut Vec, + &'path str, + ), ) -> Self { + let pos = product_stocks + .iter() + .position(|stock| stock.product_id == id); + let (available, quantity_unit) = pos + .map(|idx| product_stocks.remove(idx)) + .map(|stock| (**stock.quantity > 0, stock.quantity_unit)) + .unwrap_or_else(|| (false, crate::QuantityUnit::Piece)); Self { id, name, @@ -224,6 +246,8 @@ impl<'path> From<(crate::Product, &mut Vec, &'path str)> for }) }), price, + available, + quantity_unit, deliver_days_flag, photos: photos .drain_filter(|photo| photo.product_id == id) @@ -251,14 +275,26 @@ impl<'path> From<(crate::Product, &mut Vec, &'path str)> for #[serde(transparent)] pub struct Products(pub Vec); -impl From<(Vec, Vec, String)> for Products { +impl + From<( + Vec, + Vec, + Vec, + String, + )> for Products +{ fn from( - (products, mut photos, public_path): (Vec, Vec, String), + (products, mut photos, mut products_stock, public_path): ( + Vec, + Vec, + Vec, + String, + ), ) -> Self { Self( products .into_iter() - .map(|p| (p, &mut photos, public_path.as_str()).into()) + .map(|p| (p, &mut photos, &mut products_stock, public_path.as_str()).into()) .collect(), ) } diff --git a/shared/model/src/lib.rs b/shared/model/src/lib.rs index a6a8485..b7383d2 100644 --- a/shared/model/src/lib.rs +++ b/shared/model/src/lib.rs @@ -184,6 +184,26 @@ pub enum QuantityUnit { Piece, } +impl QuantityUnit { + pub fn name(self) -> &'static str { + match self { + Self::Gram => "Gram", + Self::Decagram => "Decagram", + Self::Kilogram => "Kilogram", + Self::Piece => "Piece", + } + } + + pub fn short_name(&self) -> &'static str { + match self { + Self::Gram => "g", + Self::Decagram => "dkg", + Self::Kilogram => "kg", + Self::Piece => "piece", + } + } +} + #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] diff --git a/web/src/pages/public/product.rs b/web/src/pages/public/product.rs index 4863df8..6d1d0b2 100644 --- a/web/src/pages/public/product.rs +++ b/web/src/pages/public/product.rs @@ -95,6 +95,7 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node { C!["mb-2 leading-tight tracking-tight font-bold text-gray-800 text-2xl md:text-3xl"], product.name.as_str() ], + div![model.i18n.t("Price per"), ": ", model.i18n.t(product.quantity_unit.short_name())], div![ delivery ], @@ -135,6 +136,11 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node { } fn delivery_available(product: &model::api::Product, model: &crate::Model) -> Node { + match product.deliver_days_flag.len() { + 0 => return empty![], + 7 => return div![model.i18n.t("Delivery all week")], + _ => {} + }; let days = product .deliver_days_flag .iter() @@ -143,7 +149,7 @@ fn delivery_available(product: &model::api::Product, model: &crate::Model) -> No model.i18n.t(day.short_name()) ]); div![ - div![C![""], model.i18n.t("Delivery every")], + div![model.i18n.t("Delivery every")], div![C!["flex py-4 space-x-4"], days] ] }