record meals that were added to list, allow delete with ingredients
This commit is contained in:
+197
-5
@@ -7,11 +7,11 @@ use sha2::{Digest, Sha256};
|
||||
use sqlx::{Connection, Row, SqliteConnection, SqlitePool, sqlite::SqliteConnectOptions};
|
||||
|
||||
use crate::domain::{
|
||||
Category, DomainError, DomainResult, GroceryList, Item, Meal, MealCategory, MealIngredient,
|
||||
Passkey, SessionUser, User,
|
||||
Category, DomainError, DomainResult, GroceryList, Item, ListMeal, Meal, MealCategory,
|
||||
MealIngredient, Passkey, SessionUser, User,
|
||||
};
|
||||
use crate::ports::{
|
||||
CategoryRepository, InvitationRepository, ItemRepository, ListRepository,
|
||||
CategoryRepository, InvitationRepository, ItemRepository, ListMealRepository, ListRepository,
|
||||
MealCategoryRepository, MealIngredientRepository, MealRepository, NewItem, PasskeyRepository,
|
||||
SessionRepository, UserRepository,
|
||||
};
|
||||
@@ -640,8 +640,8 @@ impl ItemRepository for SqliteItemRepository {
|
||||
ensure_category(txn, item.category_id).await?;
|
||||
sqlx::query(
|
||||
"INSERT INTO items
|
||||
(list_id, name, quantity, note, category_id, checked, version, position, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, 0, 1, ?6, ?7, ?7)",
|
||||
(list_id, name, quantity, note, category_id, checked, version, position, list_meal_id, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, 0, 1, ?6, ?7, ?8, ?8)",
|
||||
)
|
||||
.bind(list_id)
|
||||
.bind(&item.name)
|
||||
@@ -649,6 +649,7 @@ impl ItemRepository for SqliteItemRepository {
|
||||
.bind(&item.note)
|
||||
.bind(item.category_id)
|
||||
.bind(position)
|
||||
.bind(item.list_meal_id)
|
||||
.bind(now)
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
@@ -738,6 +739,83 @@ impl ItemRepository for SqliteItemRepository {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SqliteListMealRepository;
|
||||
|
||||
#[async_trait]
|
||||
impl ListMealRepository for SqliteListMealRepository {
|
||||
async fn list_meals(
|
||||
&self,
|
||||
txn: &mut SqliteConnection,
|
||||
list_id: i64,
|
||||
) -> DomainResult<Vec<ListMeal>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, meal_id, name, created_at
|
||||
FROM list_meals
|
||||
WHERE list_id = ?1
|
||||
ORDER BY created_at ASC, id ASC",
|
||||
)
|
||||
.bind(list_id)
|
||||
.fetch_all(&mut *txn)
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|row| ListMeal {
|
||||
id: row.get(0),
|
||||
meal_id: row.get(1),
|
||||
name: row.get(2),
|
||||
created_at: row.get(3),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn add_meal(
|
||||
&self,
|
||||
txn: &mut SqliteConnection,
|
||||
list_id: i64,
|
||||
meal_id: i64,
|
||||
name: String,
|
||||
) -> DomainResult<i64> {
|
||||
sqlx::query(
|
||||
"INSERT INTO list_meals (list_id, meal_id, name, created_at)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
)
|
||||
.bind(list_id)
|
||||
.bind(meal_id)
|
||||
.bind(&name)
|
||||
.bind(now())
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
let id = sqlx::query("SELECT last_insert_rowid()")
|
||||
.fetch_one(&mut *txn)
|
||||
.await
|
||||
.map_err(db_error)?
|
||||
.get::<i64, _>(0);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
async fn remove_meal(
|
||||
&self,
|
||||
txn: &mut SqliteConnection,
|
||||
list_id: i64,
|
||||
list_meal_id: i64,
|
||||
) -> DomainResult<i64> {
|
||||
let changed = sqlx::query("DELETE FROM list_meals WHERE id = ?1 AND list_id = ?2")
|
||||
.bind(list_meal_id)
|
||||
.bind(list_id)
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(db_error)?
|
||||
.rows_affected();
|
||||
if changed == 0 {
|
||||
return Err(DomainError::NotFound);
|
||||
}
|
||||
bump_revision(txn, list_id).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SqliteInvitationRepository;
|
||||
|
||||
@@ -1771,12 +1849,14 @@ mod tests {
|
||||
quantity: "500g".into(),
|
||||
note: String::new(),
|
||||
category_id: None,
|
||||
list_meal_id: None,
|
||||
},
|
||||
NewItem {
|
||||
name: "Tomato".into(),
|
||||
quantity: "2".into(),
|
||||
note: String::new(),
|
||||
category_id: None,
|
||||
list_meal_id: None,
|
||||
},
|
||||
];
|
||||
let revision = db
|
||||
@@ -2342,6 +2422,118 @@ mod tests {
|
||||
assert!(matches!(result, Err(DomainError::NotFound)));
|
||||
}
|
||||
|
||||
// ---- ListMealRepository ----
|
||||
|
||||
async fn add_meal_to_list(db: &SqliteDatabase, list_id: i64, meal: &Meal) -> ListMeal {
|
||||
let list_meals = SqliteListMealRepository;
|
||||
let name = meal.name.clone();
|
||||
let meal_id = meal.id;
|
||||
let id = db
|
||||
.run(move |txn| {
|
||||
let list_meals = list_meals.clone();
|
||||
Box::pin(async move { list_meals.add_meal(txn, list_id, meal_id, name).await })
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
ListMeal {
|
||||
id,
|
||||
meal_id: Some(meal.id),
|
||||
name: meal.name.clone(),
|
||||
created_at: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_meals_returns_meals_added_to_a_list() {
|
||||
let db = setup().await;
|
||||
let list = create_list(&db, "Weekly shop").await;
|
||||
let meal = create_meal(&db, "Pasta").await;
|
||||
add_meal_to_list(&db, list.id, &meal).await;
|
||||
|
||||
let list_meals = SqliteListMealRepository;
|
||||
let meals = db
|
||||
.run(move |txn| {
|
||||
let list_meals = list_meals.clone();
|
||||
Box::pin(async move { list_meals.list_meals(txn, list.id).await })
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(meals.len(), 1);
|
||||
assert_eq!(meals[0].name, "Pasta");
|
||||
assert_eq!(meals[0].meal_id, Some(meal.id));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn removing_a_meal_deletes_its_items_and_bumps_revision() {
|
||||
let db = setup().await;
|
||||
let list = create_list(&db, "Weekly shop").await;
|
||||
let meal = create_meal(&db, "Pasta").await;
|
||||
add_ingredient(&db, meal.id, "Penne", None).await;
|
||||
add_ingredient(&db, meal.id, "Tomato", None).await;
|
||||
let list_meal = add_meal_to_list(&db, list.id, &meal).await;
|
||||
|
||||
let items = SqliteItemRepository;
|
||||
let ingredients = SqliteMealIngredientRepository;
|
||||
let ingredient_rows = db
|
||||
.run(move |txn| {
|
||||
let ingredients = ingredients.clone();
|
||||
Box::pin(async move { ingredients.ingredients_for_meal(txn, meal.id).await })
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let new_items = ingredient_rows
|
||||
.into_iter()
|
||||
.map(|ingredient| NewItem {
|
||||
name: ingredient.name,
|
||||
quantity: ingredient.quantity,
|
||||
note: ingredient.note,
|
||||
category_id: ingredient.category_id,
|
||||
list_meal_id: Some(list_meal.id),
|
||||
})
|
||||
.collect();
|
||||
db.run(move |txn| {
|
||||
let items = items.clone();
|
||||
Box::pin(async move { items.add_items_bulk(txn, list.id, new_items).await })
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(get_items(&db, list.id).await.len(), 2);
|
||||
|
||||
let list_meals = SqliteListMealRepository;
|
||||
let revision = db
|
||||
.run(move |txn| {
|
||||
let list_meals = list_meals.clone();
|
||||
Box::pin(async move { list_meals.remove_meal(txn, list.id, list_meal.id).await })
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(revision, 2);
|
||||
|
||||
let meals = db
|
||||
.run(move |txn| {
|
||||
let list_meals = list_meals.clone();
|
||||
Box::pin(async move { list_meals.list_meals(txn, list.id).await })
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(meals.is_empty());
|
||||
assert!(get_items(&db, list.id).await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn removing_an_unknown_meal_from_a_list_fails() {
|
||||
let db = setup().await;
|
||||
let list = create_list(&db, "Weekly shop").await;
|
||||
let list_meals = SqliteListMealRepository;
|
||||
let result = db
|
||||
.run(move |txn| {
|
||||
let list_meals = list_meals.clone();
|
||||
Box::pin(async move { list_meals.remove_meal(txn, list.id, 9999).await })
|
||||
})
|
||||
.await;
|
||||
assert!(matches!(result, Err(DomainError::NotFound)));
|
||||
}
|
||||
|
||||
// ---- PasskeyRepository ----
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user