From 15363fc19d2f86975b201d9580c926edc2d32bff Mon Sep 17 00:00:00 2001 From: eraden Date: Sun, 8 May 2022 14:59:59 +0200 Subject: [PATCH] Product with photos --- Cargo.lock | 1 + actors/database_manager/Cargo.toml | 2 + actors/database_manager/src/photos.rs | 64 +++++++++++++++++ api/src/logic/mod.rs | 1 - api/src/logic/order_state.rs | 1 - api/src/main.rs | 1 - api/src/routes/admin.rs | 3 +- api/src/routes/admin/api_v1/products.rs | 17 ++++- api/src/routes/public.rs | 20 +++++- api/src/routes/public/api_v1/unrestricted.rs | 16 ++++- shared/model/src/api.rs | 73 ++++++++++++++++++++ shared/model/src/lib.rs | 10 +++ 12 files changed, 196 insertions(+), 13 deletions(-) delete mode 100644 api/src/logic/mod.rs delete mode 100644 api/src/logic/order_state.rs diff --git a/Cargo.lock b/Cargo.lock index 98ef2c1..50ae28d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1117,6 +1117,7 @@ dependencies = [ "chrono", "config", "fake", + "itertools", "log", "model", "pretty_env_logger", diff --git a/actors/database_manager/Cargo.toml b/actors/database_manager/Cargo.toml index f7dfd50..2b86f6b 100644 --- a/actors/database_manager/Cargo.toml +++ b/actors/database_manager/Cargo.toml @@ -26,3 +26,5 @@ pretty_env_logger = { version = "0.4", features = [] } fake = { version = "2.4.3", features = ["derive", "chrono", "http", "uuid"], optional = true } rand = { version = "0.8.5", optional = true } + +itertools = { version = "0.10.3" } diff --git a/actors/database_manager/src/photos.rs b/actors/database_manager/src/photos.rs index ed21d82..1a4c080 100644 --- a/actors/database_manager/src/photos.rs +++ b/actors/database_manager/src/photos.rs @@ -63,3 +63,67 @@ RETURNING id, local_path, file_name crate::Error::Photo(Error::Create) }) } + +//###################################### +// SPECIAL CASES +//###################################### + +#[derive(actix::Message)] +#[rtype(result = "Result>")] +pub struct PhotosForProducts { + pub product_ids: Vec, +} + +crate::db_async_handler!( + PhotosForProducts, + photos_for_products, + Vec, + inner_photos_for_products +); + +pub(crate) async fn photos_for_products( + msg: PhotosForProducts, + 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 +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); + } + log::debug!("product linked photos {:?}", res); + Ok(res) +} diff --git a/api/src/logic/mod.rs b/api/src/logic/mod.rs deleted file mode 100644 index 7d93bff..0000000 --- a/api/src/logic/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod order_state; diff --git a/api/src/logic/order_state.rs b/api/src/logic/order_state.rs deleted file mode 100644 index 8b13789..0000000 --- a/api/src/logic/order_state.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/api/src/main.rs b/api/src/main.rs index b3ab0e3..b36f74e 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -18,7 +18,6 @@ use opts::{ }; use validator::{validate_email, validate_length}; -pub mod logic; mod opts; pub mod routes; diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index 4d7edc8..86c1933 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -15,8 +15,7 @@ use crate::routes::{RequireLogin, Result}; #[macro_export] macro_rules! admin_send_db { ($db: expr, $msg: expr) => {{ - let db = $db; - match db.send($msg).await { + match $db.send($msg).await { Ok(Ok(res)) => res, Ok(Err(e)) => { log::error!("{}", e); diff --git a/api/src/routes/admin/api_v1/products.rs b/api/src/routes/admin/api_v1/products.rs index 2654c72..98c85f9 100644 --- a/api/src/routes/admin/api_v1/products.rs +++ b/api/src/routes/admin/api_v1/products.rs @@ -4,7 +4,7 @@ use actix_web::web::{Data, Json, ServiceConfig}; use actix_web::{delete, get, patch, post, HttpResponse}; use database_manager::Database; use model::{ - Days, Price, ProductCategory, ProductId, ProductLongDesc, ProductName, ProductShortDesc, + api, Days, Price, ProductCategory, ProductId, ProductLongDesc, ProductName, ProductShortDesc, Quantity, QuantityUnit, }; use search_manager::SearchManager; @@ -15,11 +15,22 @@ use crate::routes::RequireLogin; use crate::{admin_send_db, routes}; #[get("/products")] -async fn products(session: Session, db: Data>) -> routes::Result { +async fn products( + session: Session, + db: Data>, +) -> routes::Result> { session.require_admin()?; + let db = db.into_inner(); + let products = admin_send_db!(db, database_manager::AllProducts); - Ok(HttpResponse::Ok().json(products)) + let photos = admin_send_db!( + db, + database_manager::PhotosForProducts { + product_ids: products.iter().map(|p| p.id).collect() + } + ); + Ok(Json((products, photos).into())) } #[derive(Deserialize)] diff --git a/api/src/routes/public.rs b/api/src/routes/public.rs index 4b2ce77..9b066cc 100644 --- a/api/src/routes/public.rs +++ b/api/src/routes/public.rs @@ -9,8 +9,7 @@ macro_rules! public_send_db { ($db: expr, $msg: expr) => {{ use crate::routes::PublicError; - let db = $db; - return match db.send($msg).await { + return match $db.send($msg).await { Ok(Ok(res)) => Ok(HttpResponse::Ok().json(res)), Ok(Err(e)) => { log::error!("{}", e); @@ -24,6 +23,23 @@ macro_rules! public_send_db { } }; }}; + (owned, $db: expr, $msg: expr) => {{ + use crate::routes::PublicError; + + match $db.send($msg).await { + Ok(Ok(res)) => res, + Ok(Err(e)) => { + log::error!("{}", e); + return Err(crate::routes::Error::Public(PublicError::Database(e))); + } + Err(e) => { + log::error!("{}", e); + return Err(crate::routes::Error::Public( + PublicError::DatabaseConnection, + )); + } + } + }}; } #[derive(Debug, thiserror::Error)] diff --git a/api/src/routes/public/api_v1/unrestricted.rs b/api/src/routes/public/api_v1/unrestricted.rs index 8416b78..55241d9 100644 --- a/api/src/routes/public/api_v1/unrestricted.rs +++ b/api/src/routes/public/api_v1/unrestricted.rs @@ -3,7 +3,7 @@ use actix_web::web::{Data, Json, ServiceConfig}; use actix_web::{get, post, HttpResponse}; use config::SharedAppConfig; use database_manager::{query_db, Database}; -use model::{Audience, Encrypt, FullAccount, Token, TokenString}; +use model::{api, Audience, Encrypt, FullAccount, Token, TokenString}; use payment_manager::{PaymentManager, PaymentNotification}; use token_manager::TokenManager; @@ -12,8 +12,18 @@ use crate::routes::{self, Result}; use crate::{public_send_db, Login, Password}; #[get("/products")] -async fn products(db: Data>) -> Result { - public_send_db!(db.into_inner(), database_manager::AllProducts) +async fn products(db: Data>) -> Result> { + let db = db.into_inner(); + + let products: Vec = public_send_db!(owned, db, database_manager::AllProducts); + let photos: Vec = public_send_db!( + owned, + db, + database_manager::PhotosForProducts { + product_ids: products.iter().map(|p| p.id).collect() + } + ); + Ok(Json((products, photos).into())) } #[get("/stocks")] diff --git a/shared/model/src/api.rs b/shared/model/src/api.rs index d13e330..d03ba92 100644 --- a/shared/model/src/api.rs +++ b/shared/model/src/api.rs @@ -1,5 +1,7 @@ use serde::Serialize; +use crate::ProductLinkedPhoto; + #[derive(Serialize, Debug)] #[serde(transparent)] pub struct AccountOrders(pub Vec); @@ -121,3 +123,74 @@ impl From<(crate::ShoppingCart, Vec)> for ShoppingCart } } } + +#[derive(Serialize, Debug)] +pub struct Photo { + pub id: crate::PhotoId, + pub file_name: crate::FileName, +} + +#[derive(Serialize, Debug)] +pub struct Product { + pub id: crate::ProductId, + pub name: crate::ProductName, + pub short_description: crate::ProductShortDesc, + pub long_description: crate::ProductLongDesc, + pub category: Option, + pub price: crate::Price, + pub deliver_days_flag: crate::Days, + pub photos: Vec, +} + +impl From<(crate::Product, &mut Vec)> for Product { + fn from( + ( + crate::Product { + id, + name, + short_description, + long_description, + category, + price, + deliver_days_flag, + }, + photos, + ): (crate::Product, &mut Vec), + ) -> Self { + Self { + id, + name, + short_description, + long_description, + category, + price, + deliver_days_flag, + photos: photos + .drain_filter(|photo| photo.product_id == id) + .map( + |ProductLinkedPhoto { + id, + local_path: _, + file_name, + product_id: _, + }| Photo { id, file_name }, + ) + .collect(), + } + } +} + +#[derive(Serialize, Debug)] +#[serde(transparent)] +pub struct Products(Vec); + +impl From<(Vec, Vec)> for Products { + fn from((products, mut photos): (Vec, Vec)) -> Self { + Self( + products + .into_iter() + .map(|p| (p, &mut photos).into()) + .collect(), + ) + } +} diff --git a/shared/model/src/lib.rs b/shared/model/src/lib.rs index e312a29..4f47d4f 100644 --- a/shared/model/src/lib.rs +++ b/shared/model/src/lib.rs @@ -767,6 +767,16 @@ pub struct Photo { pub file_name: FileName, } +#[cfg_attr(feature = "dummy", derive(fake::Dummy))] +#[cfg_attr(feature = "db", derive(sqlx::FromRow))] +#[derive(Serialize, Deserialize, Debug)] +pub struct ProductLinkedPhoto { + pub id: PhotoId, + pub local_path: LocalPath, + pub file_name: FileName, + pub product_id: ProductId, +} + #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[derive(Serialize, Deserialize, Debug)]