feat(db): category repository with crud
This commit is contained in:
@@ -186,3 +186,98 @@ class ModuleRepository:
|
||||
params.append(offset)
|
||||
rows = self._conn.execute(sql, params).fetchall()
|
||||
return [_row_to_module(r) for r in rows]
|
||||
|
||||
|
||||
class CategoryNotFound(LookupError):
|
||||
"""Raised when a requested category id does not exist."""
|
||||
|
||||
|
||||
def _row_to_category(row: sqlite3.Row) -> dict[str, Any]:
|
||||
return {
|
||||
"id": row["id"],
|
||||
"name": row["name"],
|
||||
"color": row["color"],
|
||||
"icon": row["icon"],
|
||||
"sort_order": row["sort_order"],
|
||||
}
|
||||
|
||||
|
||||
class CategoryRepository:
|
||||
def __init__(self, conn: sqlite3.Connection) -> None:
|
||||
self._conn = conn
|
||||
|
||||
def create(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
color: str | None,
|
||||
icon: str | None,
|
||||
sort_order: int = 0,
|
||||
) -> dict[str, Any]:
|
||||
cid = _slug(name)
|
||||
try:
|
||||
with self._conn:
|
||||
self._conn.execute(
|
||||
"INSERT INTO module_categories("
|
||||
"id, name, color, icon, sort_order"
|
||||
") VALUES(?, ?, ?, ?, ?);",
|
||||
(cid, name, color, icon, sort_order),
|
||||
)
|
||||
except sqlite3.IntegrityError as e:
|
||||
raise ValueError(f"category name not unique: {name}") from e
|
||||
return self.get(cid)
|
||||
|
||||
def get(self, category_id: str) -> dict[str, Any]:
|
||||
row = self._conn.execute(
|
||||
"SELECT * FROM module_categories WHERE id = ?;",
|
||||
(category_id,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise CategoryNotFound(category_id)
|
||||
return _row_to_category(row)
|
||||
|
||||
def update(
|
||||
self,
|
||||
category_id: str,
|
||||
*,
|
||||
name: str | None = None,
|
||||
color: str | None | _Unset = _UNSET,
|
||||
icon: str | None | _Unset = _UNSET,
|
||||
sort_order: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
existing = self.get(category_id)
|
||||
new = {
|
||||
"name": existing["name"] if name is None else name,
|
||||
"color": existing["color"] if isinstance(color, _Unset) else color,
|
||||
"icon": existing["icon"] if isinstance(icon, _Unset) else icon,
|
||||
"sort_order": (
|
||||
existing["sort_order"] if sort_order is None else sort_order
|
||||
),
|
||||
}
|
||||
with self._conn:
|
||||
self._conn.execute(
|
||||
"UPDATE module_categories SET "
|
||||
"name = ?, color = ?, icon = ?, sort_order = ? "
|
||||
"WHERE id = ?;",
|
||||
(
|
||||
new["name"], new["color"], new["icon"],
|
||||
new["sort_order"], category_id,
|
||||
),
|
||||
)
|
||||
return self.get(category_id)
|
||||
|
||||
def delete(self, category_id: str) -> None:
|
||||
with self._conn:
|
||||
cur = self._conn.execute(
|
||||
"DELETE FROM module_categories WHERE id = ?;",
|
||||
(category_id,),
|
||||
)
|
||||
if cur.rowcount == 0:
|
||||
raise CategoryNotFound(category_id)
|
||||
|
||||
def list(self) -> list[dict[str, Any]]:
|
||||
rows = self._conn.execute(
|
||||
"SELECT * FROM module_categories "
|
||||
"ORDER BY sort_order ASC, name COLLATE NOCASE ASC;"
|
||||
).fetchall()
|
||||
return [_row_to_category(r) for r in rows]
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Tests for CategoryRepository."""
|
||||
import pytest
|
||||
|
||||
from engine.db.repositories import CategoryNotFound, CategoryRepository
|
||||
|
||||
|
||||
def test_create_returns_row_with_id(wp_db):
|
||||
repo = CategoryRepository(wp_db)
|
||||
row = repo.create(name="Style", color="#a970ff", icon="pi pi-palette")
|
||||
assert row["id"] == "style"
|
||||
assert row["name"] == "Style"
|
||||
assert row["color"] == "#a970ff"
|
||||
assert row["icon"] == "pi pi-palette"
|
||||
assert row["sort_order"] == 0
|
||||
|
||||
|
||||
def test_create_lowercase_collision_raises_value_error(wp_db):
|
||||
repo = CategoryRepository(wp_db)
|
||||
repo.create(name="Style", color=None, icon=None)
|
||||
with pytest.raises(ValueError):
|
||||
repo.create(name="STYLE", color=None, icon=None)
|
||||
|
||||
|
||||
def test_get_returns_existing(wp_db):
|
||||
repo = CategoryRepository(wp_db)
|
||||
repo.create(name="Camera", color=None, icon=None)
|
||||
got = repo.get("camera")
|
||||
assert got["name"] == "Camera"
|
||||
|
||||
|
||||
def test_get_missing_raises(wp_db):
|
||||
repo = CategoryRepository(wp_db)
|
||||
with pytest.raises(CategoryNotFound):
|
||||
repo.get("ghost")
|
||||
|
||||
|
||||
def test_update_changes_fields(wp_db):
|
||||
repo = CategoryRepository(wp_db)
|
||||
repo.create(name="A", color=None, icon=None)
|
||||
updated = repo.update("a", color="#ff0000", icon="pi pi-tag", sort_order=10)
|
||||
assert updated["color"] == "#ff0000"
|
||||
assert updated["icon"] == "pi pi-tag"
|
||||
assert updated["sort_order"] == 10
|
||||
|
||||
|
||||
def test_update_clears_color_with_explicit_none(wp_db):
|
||||
repo = CategoryRepository(wp_db)
|
||||
repo.create(name="A", color="#ff0000", icon="pi pi-tag")
|
||||
updated = repo.update("a", color=None)
|
||||
assert updated["color"] is None
|
||||
assert updated["icon"] == "pi pi-tag" # untouched
|
||||
|
||||
|
||||
def test_update_keeps_unchanged_fields(wp_db):
|
||||
repo = CategoryRepository(wp_db)
|
||||
repo.create(name="A", color="#ff0000", icon="pi pi-tag", sort_order=5)
|
||||
updated = repo.update("a", name="B") # only name
|
||||
assert updated["name"] == "B"
|
||||
assert updated["color"] == "#ff0000"
|
||||
assert updated["icon"] == "pi pi-tag"
|
||||
assert updated["sort_order"] == 5
|
||||
|
||||
|
||||
def test_update_missing_raises(wp_db):
|
||||
repo = CategoryRepository(wp_db)
|
||||
with pytest.raises(CategoryNotFound):
|
||||
repo.update("ghost", name="X")
|
||||
|
||||
|
||||
def test_delete_removes(wp_db):
|
||||
repo = CategoryRepository(wp_db)
|
||||
repo.create(name="Tmp", color=None, icon=None)
|
||||
repo.delete("tmp")
|
||||
with pytest.raises(CategoryNotFound):
|
||||
repo.get("tmp")
|
||||
|
||||
|
||||
def test_delete_missing_raises(wp_db):
|
||||
repo = CategoryRepository(wp_db)
|
||||
with pytest.raises(CategoryNotFound):
|
||||
repo.delete("ghost")
|
||||
|
||||
|
||||
def test_list_orders_by_sort_order_then_name(wp_db):
|
||||
repo = CategoryRepository(wp_db)
|
||||
repo.create(name="Bravo", color=None, icon=None, sort_order=10)
|
||||
repo.create(name="Alpha", color=None, icon=None, sort_order=10)
|
||||
repo.create(name="Charlie", color=None, icon=None, sort_order=0)
|
||||
rows = repo.list()
|
||||
assert [r["name"] for r in rows] == ["Charlie", "Alpha", "Bravo"]
|
||||
|
||||
|
||||
def test_list_empty(wp_db):
|
||||
repo = CategoryRepository(wp_db)
|
||||
assert repo.list() == []
|
||||
Reference in New Issue
Block a user