Product with photos

This commit is contained in:
eraden 2022-05-08 14:59:59 +02:00
parent 01d5dde052
commit 15363fc19d
12 changed files with 196 additions and 13 deletions

1
Cargo.lock generated
View File

@ -1117,6 +1117,7 @@ dependencies = [
"chrono", "chrono",
"config", "config",
"fake", "fake",
"itertools",
"log", "log",
"model", "model",
"pretty_env_logger", "pretty_env_logger",

View File

@ -26,3 +26,5 @@ pretty_env_logger = { version = "0.4", features = [] }
fake = { version = "2.4.3", features = ["derive", "chrono", "http", "uuid"], optional = true } fake = { version = "2.4.3", features = ["derive", "chrono", "http", "uuid"], optional = true }
rand = { version = "0.8.5", optional = true } rand = { version = "0.8.5", optional = true }
itertools = { version = "0.10.3" }

View File

@ -63,3 +63,67 @@ RETURNING id, local_path, file_name
crate::Error::Photo(Error::Create) crate::Error::Photo(Error::Create)
}) })
} }
//######################################
// SPECIAL CASES
//######################################
#[derive(actix::Message)]
#[rtype(result = "Result<Vec<model::ProductLinkedPhoto>>")]
pub struct PhotosForProducts {
pub product_ids: Vec<model::ProductId>,
}
crate::db_async_handler!(
PhotosForProducts,
photos_for_products,
Vec<model::ProductLinkedPhoto>,
inner_photos_for_products
);
pub(crate) async fn photos_for_products(
msg: PhotosForProducts,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<model::ProductLinkedPhoto>> {
log::debug!("all product ids {:?}", msg.product_ids);
let mut res: Vec<model::ProductLinkedPhoto> = Vec::with_capacity(100);
let len = msg.product_ids.len() / 20;
for ids in msg.product_ids.into_iter().fold(
Vec::<Vec<model::ProductId>>::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)
}

View File

@ -1 +0,0 @@
pub mod order_state;

View File

@ -1 +0,0 @@

View File

@ -18,7 +18,6 @@ use opts::{
}; };
use validator::{validate_email, validate_length}; use validator::{validate_email, validate_length};
pub mod logic;
mod opts; mod opts;
pub mod routes; pub mod routes;

View File

@ -15,8 +15,7 @@ use crate::routes::{RequireLogin, Result};
#[macro_export] #[macro_export]
macro_rules! admin_send_db { macro_rules! admin_send_db {
($db: expr, $msg: expr) => {{ ($db: expr, $msg: expr) => {{
let db = $db; match $db.send($msg).await {
match db.send($msg).await {
Ok(Ok(res)) => res, Ok(Ok(res)) => res,
Ok(Err(e)) => { Ok(Err(e)) => {
log::error!("{}", e); log::error!("{}", e);

View File

@ -4,7 +4,7 @@ use actix_web::web::{Data, Json, ServiceConfig};
use actix_web::{delete, get, patch, post, HttpResponse}; use actix_web::{delete, get, patch, post, HttpResponse};
use database_manager::Database; use database_manager::Database;
use model::{ use model::{
Days, Price, ProductCategory, ProductId, ProductLongDesc, ProductName, ProductShortDesc, api, Days, Price, ProductCategory, ProductId, ProductLongDesc, ProductName, ProductShortDesc,
Quantity, QuantityUnit, Quantity, QuantityUnit,
}; };
use search_manager::SearchManager; use search_manager::SearchManager;
@ -15,11 +15,22 @@ use crate::routes::RequireLogin;
use crate::{admin_send_db, routes}; use crate::{admin_send_db, routes};
#[get("/products")] #[get("/products")]
async fn products(session: Session, db: Data<Addr<Database>>) -> routes::Result<HttpResponse> { async fn products(
session: Session,
db: Data<Addr<Database>>,
) -> routes::Result<Json<api::Products>> {
session.require_admin()?; session.require_admin()?;
let db = db.into_inner();
let products = admin_send_db!(db, database_manager::AllProducts); 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)] #[derive(Deserialize)]

View File

@ -9,8 +9,7 @@ macro_rules! public_send_db {
($db: expr, $msg: expr) => {{ ($db: expr, $msg: expr) => {{
use crate::routes::PublicError; 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(Ok(res)) => Ok(HttpResponse::Ok().json(res)),
Ok(Err(e)) => { Ok(Err(e)) => {
log::error!("{}", 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)] #[derive(Debug, thiserror::Error)]

View File

@ -3,7 +3,7 @@ use actix_web::web::{Data, Json, ServiceConfig};
use actix_web::{get, post, HttpResponse}; use actix_web::{get, post, HttpResponse};
use config::SharedAppConfig; use config::SharedAppConfig;
use database_manager::{query_db, Database}; 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 payment_manager::{PaymentManager, PaymentNotification};
use token_manager::TokenManager; use token_manager::TokenManager;
@ -12,8 +12,18 @@ use crate::routes::{self, Result};
use crate::{public_send_db, Login, Password}; use crate::{public_send_db, Login, Password};
#[get("/products")] #[get("/products")]
async fn products(db: Data<Addr<Database>>) -> Result<HttpResponse> { async fn products(db: Data<Addr<Database>>) -> Result<Json<api::Products>> {
public_send_db!(db.into_inner(), database_manager::AllProducts) let db = db.into_inner();
let products: Vec<model::Product> = public_send_db!(owned, db, database_manager::AllProducts);
let photos: Vec<model::ProductLinkedPhoto> = public_send_db!(
owned,
db,
database_manager::PhotosForProducts {
product_ids: products.iter().map(|p| p.id).collect()
}
);
Ok(Json((products, photos).into()))
} }
#[get("/stocks")] #[get("/stocks")]

View File

@ -1,5 +1,7 @@
use serde::Serialize; use serde::Serialize;
use crate::ProductLinkedPhoto;
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
#[serde(transparent)] #[serde(transparent)]
pub struct AccountOrders(pub Vec<AccountOrder>); pub struct AccountOrders(pub Vec<AccountOrder>);
@ -121,3 +123,74 @@ impl From<(crate::ShoppingCart, Vec<crate::ShoppingCartItem>)> 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<crate::ProductCategory>,
pub price: crate::Price,
pub deliver_days_flag: crate::Days,
pub photos: Vec<Photo>,
}
impl From<(crate::Product, &mut Vec<crate::ProductLinkedPhoto>)> for Product {
fn from(
(
crate::Product {
id,
name,
short_description,
long_description,
category,
price,
deliver_days_flag,
},
photos,
): (crate::Product, &mut Vec<ProductLinkedPhoto>),
) -> 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<Product>);
impl From<(Vec<crate::Product>, Vec<crate::ProductLinkedPhoto>)> for Products {
fn from((products, mut photos): (Vec<crate::Product>, Vec<crate::ProductLinkedPhoto>)) -> Self {
Self(
products
.into_iter()
.map(|p| (p, &mut photos).into())
.collect(),
)
}
}

View File

@ -767,6 +767,16 @@ pub struct Photo {
pub file_name: FileName, 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 = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]