list archive
ci/woodpecker/push/e2e Pipeline was successful
ci/woodpecker/push/fmt Pipeline failed
ci/woodpecker/push/test Pipeline was successful

This commit is contained in:
2026-08-07 22:16:29 -04:00
parent 240d993d57
commit 689bd95ff0
10 changed files with 536 additions and 89 deletions
+139 -2
View File
@@ -442,8 +442,9 @@ pub struct SqliteListRepository;
impl ListRepository for SqliteListRepository {
async fn list_summaries(&self, txn: &mut SqliteConnection) -> DomainResult<Vec<GroceryList>> {
let rows = sqlx::query(
"SELECT l.id, l.name, l.revision
"SELECT l.id, l.name, l.revision, l.created_at, l.archived_at
FROM lists l
WHERE l.archived_at IS NULL
ORDER BY l.created_at DESC",
)
.fetch_all(&mut *txn)
@@ -455,6 +456,33 @@ impl ListRepository for SqliteListRepository {
id: row.get(0),
name: row.get(1),
revision: row.get(2),
created_at: row.get(3),
archived_at: row.get(4),
})
.collect())
}
async fn list_archived_summaries(
&self,
txn: &mut SqliteConnection,
) -> DomainResult<Vec<GroceryList>> {
let rows = sqlx::query(
"SELECT l.id, l.name, l.revision, l.created_at, l.archived_at
FROM lists l
WHERE l.archived_at IS NOT NULL
ORDER BY l.archived_at DESC, l.created_at DESC",
)
.fetch_all(&mut *txn)
.await
.map_err(db_error)?;
Ok(rows
.into_iter()
.map(|row| GroceryList {
id: row.get(0),
name: row.get(1),
revision: row.get(2),
created_at: row.get(3),
archived_at: row.get(4),
})
.collect())
}
@@ -479,6 +507,8 @@ impl ListRepository for SqliteListRepository {
id: list_id,
name,
revision: 0,
created_at: now(),
archived_at: None,
})
}
@@ -488,7 +518,7 @@ impl ListRepository for SqliteListRepository {
list_id: i64,
) -> DomainResult<Option<GroceryList>> {
let row = sqlx::query(
"SELECT l.id, l.name, l.revision
"SELECT l.id, l.name, l.revision, l.created_at, l.archived_at
FROM lists l
WHERE l.id = ?1",
)
@@ -500,8 +530,36 @@ impl ListRepository for SqliteListRepository {
id: row.get(0),
name: row.get(1),
revision: row.get(2),
created_at: row.get(3),
archived_at: row.get(4),
}))
}
async fn set_archived(
&self,
txn: &mut SqliteConnection,
list_id: i64,
archived: bool,
) -> DomainResult<()> {
let result = if archived {
sqlx::query("UPDATE lists SET archived_at = ?1 WHERE id = ?2")
.bind(now())
.bind(list_id)
.execute(&mut *txn)
.await
.map_err(db_error)?
} else {
sqlx::query("UPDATE lists SET archived_at = NULL WHERE id = ?1")
.bind(list_id)
.execute(&mut *txn)
.await
.map_err(db_error)?
};
if result.rows_affected() == 0 {
return Err(DomainError::NotFound);
}
Ok(())
}
}
#[derive(Clone, Copy)]
@@ -1608,6 +1666,85 @@ mod tests {
assert_eq!(ids, vec![first.id, second.id]);
}
#[tokio::test]
async fn archiving_a_list_hides_it_from_summaries() {
let db = setup().await;
let list = create_list(&db, "Weekly shop").await;
let lists = SqliteListRepository;
let list_id = list.id;
db.run(move |txn| {
let lists = lists.clone();
Box::pin(async move { lists.set_archived(txn, list_id, true).await })
})
.await
.unwrap();
let summaries = db
.run(move |txn| {
let lists = lists.clone();
Box::pin(async move { lists.list_summaries(txn).await })
})
.await
.unwrap();
assert!(summaries.is_empty());
let archived = db
.run(move |txn| {
let lists = lists.clone();
Box::pin(async move { lists.list_archived_summaries(txn).await })
})
.await
.unwrap();
assert_eq!(archived.len(), 1);
assert_eq!(archived[0].id, list.id);
assert!(archived[0].archived_at.is_some());
}
#[tokio::test]
async fn unarchiving_a_list_restores_it_to_summaries() {
let db = setup().await;
let list = create_list(&db, "Weekly shop").await;
let lists = SqliteListRepository;
let list_id = list.id;
db.run(move |txn| {
let lists = lists.clone();
Box::pin(async move { lists.set_archived(txn, list_id, true).await })
})
.await
.unwrap();
db.run(move |txn| {
let lists = lists.clone();
Box::pin(async move { lists.set_archived(txn, list_id, false).await })
})
.await
.unwrap();
let summaries = db
.run(move |txn| {
let lists = lists.clone();
Box::pin(async move { lists.list_summaries(txn).await })
})
.await
.unwrap();
assert_eq!(summaries.len(), 1);
assert!(summaries[0].archived_at.is_none());
}
#[tokio::test]
async fn archiving_a_missing_list_fails() {
let db = setup().await;
let lists = SqliteListRepository;
let result = db
.run(move |txn| {
let lists = lists.clone();
Box::pin(async move { lists.set_archived(txn, 9999, true).await })
})
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn get_list_returns_list_or_none() {
let db = setup().await;