Fetch stocks

This commit is contained in:
eraden 2022-05-12 20:33:16 +02:00
parent ef903c6f31
commit 1e941a4122
8 changed files with 249 additions and 54 deletions

View File

@ -1,6 +1,7 @@
use actix::{Actor, Context}; use actix::{Actor, Context};
use config::SharedAppConfig; use config::SharedAppConfig;
use sqlx::PgPool; use sqlx::PgPool;
use sqlx_core::arguments::Arguments;
pub use crate::account_orders::*; pub use crate::account_orders::*;
pub use crate::accounts::*; pub use crate::accounts::*;
@ -156,3 +157,97 @@ impl Database {
impl Actor for Database { impl Actor for Database {
type Context = Context<Self>; type Context = Context<Self>;
} }
/// 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<T>,
}
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<Vec<T>, crate::Error>
where
Ids: Iterator<Item = model::RecordId>,
Error: Fn(sqlx::Error) -> crate::Error,
{
let mut res = Vec::new();
for ids in items.fold(
Vec::<Vec<model::RecordId>>::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<T> = match q.fetch_all(&mut *self.pool).await {
Ok(rec) => rec,
Err(e) => return Err(on_error(e)),
};
res.extend(records);
}
Ok(res)
}
}

View File

@ -1,4 +1,4 @@
use crate::Result; use crate::{MultiLoad, Result};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
@ -88,44 +88,23 @@ pub(crate) async fn photos_for_products(
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<model::ProductLinkedPhoto>> { ) -> Result<Vec<model::ProductLinkedPhoto>> {
log::debug!("all product ids {:?}", msg.product_ids); log::debug!("all product ids {:?}", msg.product_ids);
let mut res: Vec<model::ProductLinkedPhoto> = Vec::with_capacity(100); let res: Vec<model::ProductLinkedPhoto> = MultiLoad::new(
pool,
let len = msg.product_ids.len() / 20; r#"
for ids in msg.product_ids.into_iter().fold( SELECT photos.id, photos.local_path, photos.file_name,
Vec::<Vec<model::ProductId>>::with_capacity(len), product_photos.product_id, photos.unique_name FROM photos
|mut v, id| { INNER JOIN product_photos
if matches!(v.last().map(|v| v.len()), Some(20) | None) { ON photos.id = product_photos.photo_id
v.push(Vec::with_capacity(20)); WHERE
} "#,
v.last_mut().unwrap().push(id); " product_photos.product_id =",
v )
}, .load(
) { msg.product_ids.len(),
log::debug!("scoped product ids {:?}", ids); msg.product_ids.into_iter().map(|id| *id),
let query: String = r#" |_e| crate::Error::Photo(Error::All),
SELECT photos.id, photos.local_path, photos.file_name, product_photos.product_id, photos.unique_name )
FROM photos .await?;
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); log::debug!("product linked photos {:?}", res);
Ok(res) Ok(res)
} }

View File

@ -2,7 +2,7 @@ use actix::Message;
use model::{ProductId, Quantity, QuantityUnit, Stock, StockId}; use model::{ProductId, Quantity, QuantityUnit, Stock, StockId};
use sqlx::PgPool; use sqlx::PgPool;
use crate::Result; use crate::{MultiLoad, Result};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
@ -14,6 +14,8 @@ pub enum Error {
Update, Update,
#[error("Unable to delete stock")] #[error("Unable to delete stock")]
Delete, Delete,
#[error("Unable find stock for product")]
ProductStock,
} }
#[derive(Message)] #[derive(Message)]
@ -124,3 +126,37 @@ RETURNING id, product_id, quantity, quantity_unit
crate::Error::Stock(Error::Delete) crate::Error::Stock(Error::Delete)
}) })
} }
#[derive(Message)]
#[rtype(result = "Result<Vec<model::Stock>>")]
pub struct ProductsStock {
pub product_ids: Vec<ProductId>,
}
crate::db_async_handler!(
ProductsStock,
product_stock,
Vec<model::Stock>,
inner_product_stock
);
async fn product_stock(
msg: ProductsStock,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<model::Stock>> {
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?)
}

View File

@ -30,14 +30,16 @@ async fn products(
let db = db.into_inner(); let db = db.into_inner();
let products = admin_send_db!(db, database_manager::AllProducts); let products: Vec<model::Product> = admin_send_db!(db, database_manager::AllProducts);
let product_ids: Vec<model::ProductId> = products.iter().map(|p| p.id).collect();
let photos = admin_send_db!( let photos = admin_send_db!(
db, db,
database_manager::PhotosForProducts { 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)] #[derive(Deserialize)]

View File

@ -23,14 +23,20 @@ async fn products(
}; };
let products: Vec<model::Product> = public_send_db!(owned, db, database_manager::AllProducts); let products: Vec<model::Product> = public_send_db!(owned, db, database_manager::AllProducts);
let product_ids: Vec<model::ProductId> = products.iter().map(|p| p.id).collect();
let products_stock: Vec<model::Stock> = public_send_db!(
owned,
db,
database_manager::ProductsStock {
product_ids: product_ids.clone()
}
);
let photos: Vec<model::ProductLinkedPhoto> = public_send_db!( let photos: Vec<model::ProductLinkedPhoto> = public_send_db!(
owned, owned,
db, db,
database_manager::PhotosForProducts { database_manager::PhotosForProducts { product_ids }
product_ids: products.iter().map(|p| p.id).collect()
}
); );
Ok(Json((products, photos, public_path).into())) Ok(Json((products, photos, products_stock, public_path).into()))
} }
#[get("/product/{id}")] #[get("/product/{id}")]
@ -48,6 +54,13 @@ async fn product(
let product: model::Product = let product: model::Product =
public_send_db!(owned, db, database_manager::FindProduct { product_id }); 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<model::ProductLinkedPhoto> = public_send_db!( let mut photos: Vec<model::ProductLinkedPhoto> = public_send_db!(
owned, owned,
db, db,
@ -55,7 +68,15 @@ async fn product(
product_ids: vec![product.id] 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")] #[get("/stocks")]

View File

@ -189,11 +189,20 @@ pub struct Product {
pub long_description: crate::ProductLongDesc, pub long_description: crate::ProductLongDesc,
pub category: Option<Category>, pub category: Option<Category>,
pub price: crate::Price, pub price: crate::Price,
pub available: bool,
pub quantity_unit: crate::QuantityUnit,
pub deliver_days_flag: crate::Days, pub deliver_days_flag: crate::Days,
pub photos: Vec<Photo>, pub photos: Vec<Photo>,
} }
impl<'path> From<(crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str)> for Product { impl<'path>
From<(
crate::Product,
&mut Vec<ProductLinkedPhoto>,
&mut Vec<crate::Stock>,
&'path str,
)> for Product
{
fn from( fn from(
( (
crate::Product { crate::Product {
@ -206,9 +215,22 @@ impl<'path> From<(crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str)> for
deliver_days_flag, deliver_days_flag,
}, },
photos, photos,
product_stocks,
public_path, public_path,
): (crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str), ): (
crate::Product,
&mut Vec<ProductLinkedPhoto>,
&mut Vec<crate::Stock>,
&'path str,
),
) -> Self { ) -> 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 { Self {
id, id,
name, name,
@ -224,6 +246,8 @@ impl<'path> From<(crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str)> for
}) })
}), }),
price, price,
available,
quantity_unit,
deliver_days_flag, deliver_days_flag,
photos: photos photos: photos
.drain_filter(|photo| photo.product_id == id) .drain_filter(|photo| photo.product_id == id)
@ -251,14 +275,26 @@ impl<'path> From<(crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str)> for
#[serde(transparent)] #[serde(transparent)]
pub struct Products(pub Vec<Product>); pub struct Products(pub Vec<Product>);
impl From<(Vec<crate::Product>, Vec<ProductLinkedPhoto>, String)> for Products { impl
From<(
Vec<crate::Product>,
Vec<ProductLinkedPhoto>,
Vec<crate::Stock>,
String,
)> for Products
{
fn from( fn from(
(products, mut photos, public_path): (Vec<crate::Product>, Vec<ProductLinkedPhoto>, String), (products, mut photos, mut products_stock, public_path): (
Vec<crate::Product>,
Vec<ProductLinkedPhoto>,
Vec<crate::Stock>,
String,
),
) -> Self { ) -> Self {
Self( Self(
products products
.into_iter() .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(), .collect(),
) )
} }

View File

@ -184,6 +184,26 @@ pub enum QuantityUnit {
Piece, 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 = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] #[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))]

View File

@ -95,6 +95,7 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
C!["mb-2 leading-tight tracking-tight font-bold text-gray-800 text-2xl md:text-3xl"], C!["mb-2 leading-tight tracking-tight font-bold text-gray-800 text-2xl md:text-3xl"],
product.name.as_str() product.name.as_str()
], ],
div![model.i18n.t("Price per"), ": ", model.i18n.t(product.quantity_unit.short_name())],
div![ div![
delivery delivery
], ],
@ -135,6 +136,11 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
} }
fn delivery_available(product: &model::api::Product, model: &crate::Model) -> Node<Msg> { fn delivery_available(product: &model::api::Product, model: &crate::Model) -> Node<Msg> {
match product.deliver_days_flag.len() {
0 => return empty![],
7 => return div![model.i18n.t("Delivery all week")],
_ => {}
};
let days = product let days = product
.deliver_days_flag .deliver_days_flag
.iter() .iter()
@ -143,7 +149,7 @@ fn delivery_available(product: &model::api::Product, model: &crate::Model) -> No
model.i18n.t(day.short_name()) model.i18n.t(day.short_name())
]); ]);
div![ div![
div![C![""], model.i18n.t("Delivery every")], div![model.i18n.t("Delivery every")],
div![C!["flex py-4 space-x-4"], days] div![C!["flex py-4 space-x-4"], days]
] ]
} }