Add support for meals (#1)

Reviewed-on: #1
Co-authored-by: Simon Bernier St-Pierre <git.sbstp.ca@gmail.com>
This commit was merged in pull request #1.
This commit is contained in:
2026-08-01 20:27:38 -04:00
committed by sbstp
parent 6a2cef2003
commit 9b32fd23a7
11 changed files with 1858 additions and 135 deletions
+173 -7
View File
@@ -1,9 +1,12 @@
use std::sync::Arc;
use crate::domain::{DomainError, DomainResult, GroceryList, Item, SessionUser, User};
use crate::domain::{
DomainError, DomainResult, GroceryList, Item, Meal, SessionUser, User,
};
use crate::ports::{
CategoryRepository, InvitationRepository, ItemRepository, ListRepository, PasswordHasher,
RealtimeNotifier, SessionRepository, TokenGenerator, UserRepository,
CategoryRepository, InvitationRepository, ItemRepository, ListRepository, MealIngredientRepository,
MealRepository, NewItem, PasswordHasher, RealtimeNotifier, SessionRepository, TokenGenerator,
UserRepository,
};
use crate::sqlite::SqliteDatabase;
@@ -187,10 +190,10 @@ impl ListService {
.await
}
pub async fn categories(&self, list_id: i64) -> DomainResult<Vec<crate::domain::Category>> {
pub async fn categories(&self) -> DomainResult<Vec<crate::domain::Category>> {
let categories = Arc::clone(&self.categories);
self.db
.run(move |txn| Box::pin(async move { categories.categories(txn, list_id).await }))
.run(move |txn| Box::pin(async move { categories.categories(txn).await }))
.await
}
@@ -270,12 +273,175 @@ impl ListService {
Ok(revision)
}
pub async fn create_category(&self, list_id: i64, name: String) -> DomainResult<i64> {
pub async fn create_category(&self, name: String) -> DomainResult<i64> {
let categories = Arc::clone(&self.categories);
self.db
.run(move |txn| Box::pin(async move { categories.create_category(txn, name).await }))
.await
}
}
pub struct MealService {
db: SqliteDatabase,
meals: Arc<dyn MealRepository>,
ingredients: Arc<dyn MealIngredientRepository>,
lists: Arc<dyn ListRepository>,
items: Arc<dyn ItemRepository>,
realtime: Arc<dyn RealtimeNotifier>,
}
impl MealService {
pub fn new(
db: SqliteDatabase,
meals: Arc<dyn MealRepository>,
ingredients: Arc<dyn MealIngredientRepository>,
lists: Arc<dyn ListRepository>,
items: Arc<dyn ItemRepository>,
realtime: Arc<dyn RealtimeNotifier>,
) -> Self {
Self {
db,
meals,
ingredients,
lists,
items,
realtime,
}
}
pub async fn create_meal(&self, name: String, description: String) -> DomainResult<Meal> {
let meals = Arc::clone(&self.meals);
self.db
.run(move |txn| Box::pin(async move { meals.create_meal(txn, name, description).await }))
.await
}
pub async fn get_meal(&self, meal_id: i64) -> DomainResult<Option<Meal>> {
let meals = Arc::clone(&self.meals);
self.db
.run(move |txn| Box::pin(async move { meals.get_meal(txn, meal_id).await }))
.await
}
pub async fn list_meals(&self) -> DomainResult<Vec<Meal>> {
let meals = Arc::clone(&self.meals);
self.db
.run(move |txn| Box::pin(async move { meals.list_meals(txn).await }))
.await
}
pub async fn update_meal(
&self,
meal_id: i64,
name: String,
description: String,
) -> DomainResult<()> {
let meals = Arc::clone(&self.meals);
self.db
.run(move |txn| {
Box::pin(async move { meals.update_meal(txn, meal_id, name, description).await })
})
.await
}
pub async fn delete_meal(&self, meal_id: i64) -> DomainResult<()> {
let meals = Arc::clone(&self.meals);
self.db
.run(move |txn| Box::pin(async move { meals.delete_meal(txn, meal_id).await }))
.await
}
pub async fn add_ingredient(
&self,
meal_id: i64,
name: String,
quantity: String,
note: String,
category_id: Option<i64>,
) -> DomainResult<i64> {
let ingredients = Arc::clone(&self.ingredients);
self.db
.run(move |txn| {
Box::pin(async move {
ingredients
.add_ingredient(txn, meal_id, name, quantity, note, category_id)
.await
})
})
.await
}
pub async fn update_ingredient(
&self,
meal_id: i64,
ingredient_id: i64,
name: String,
quantity: String,
note: String,
category_id: Option<i64>,
) -> DomainResult<()> {
let ingredients = Arc::clone(&self.ingredients);
self.db
.run(move |txn| {
Box::pin(async move {
ingredients
.update_ingredient(
txn,
meal_id,
ingredient_id,
name,
quantity,
note,
category_id,
)
.await
})
})
.await
}
pub async fn delete_ingredient(&self, meal_id: i64, ingredient_id: i64) -> DomainResult<()> {
let ingredients = Arc::clone(&self.ingredients);
self.db
.run(move |txn| {
Box::pin(async move {
ingredients
.delete_ingredient(txn, meal_id, ingredient_id)
.await
})
})
.await
}
/// Expands a meal's ingredients into items on a list in one unit of work,
/// bumping the list revision exactly once.
pub async fn add_meal_to_list(&self, meal_id: i64, list_id: i64) -> DomainResult<i64> {
let meals = Arc::clone(&self.meals);
let lists = Arc::clone(&self.lists);
let items = Arc::clone(&self.items);
let revision = self
.db
.run(move |txn| {
Box::pin(async move { categories.create_category(txn, list_id, name).await })
Box::pin(async move {
let meal = meals
.get_meal(txn, meal_id)
.await?
.ok_or(DomainError::NotFound)?;
if lists.get_list(txn, list_id).await?.is_none() {
return Err(DomainError::NotFound);
}
let new_items = meal
.ingredients
.into_iter()
.map(|ingredient| NewItem {
name: ingredient.name,
quantity: ingredient.quantity,
note: ingredient.note,
category_id: ingredient.category_id,
})
.collect();
items.add_items_bulk(txn, list_id, new_items).await
})
})
.await?;
self.realtime.publish_list_changed(list_id, revision).await;