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 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<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)]
pub enum Error {
@ -88,44 +88,23 @@ pub(crate) async fn photos_for_products(
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, 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<model::ProductLinkedPhoto> = 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)
}

View File

@ -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<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 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!(
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)]

View File

@ -23,14 +23,20 @@ async fn products(
};
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!(
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<model::ProductLinkedPhoto> = 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")]

View File

@ -189,11 +189,20 @@ pub struct Product {
pub long_description: crate::ProductLongDesc,
pub category: Option<Category>,
pub price: crate::Price,
pub available: bool,
pub quantity_unit: crate::QuantityUnit,
pub deliver_days_flag: crate::Days,
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(
(
crate::Product {
@ -206,9 +215,22 @@ impl<'path> From<(crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str)> for
deliver_days_flag,
},
photos,
product_stocks,
public_path,
): (crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str),
): (
crate::Product,
&mut Vec<ProductLinkedPhoto>,
&mut Vec<crate::Stock>,
&'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<ProductLinkedPhoto>, &'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<ProductLinkedPhoto>, &'path str)> for
#[serde(transparent)]
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(
(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(
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(),
)
}

View File

@ -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"))]

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"],
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<crate::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
.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]
]
}