feat(backend): 无界列表端点加分页——projects/templates 支持 limit/offset

GET /projects 与 GET /templates 原一次返回全部 owner 行,行数随数据线性增长。
新增可复用分页依赖 ww_api.pagination.get_page(limit∈[1,200] 默认 50、offset≥0,
越界 → 422),两端点接入,Repository.list_for_owner 加 keyword-only limit/offset
把 LIMIT/OFFSET 下推 DB。向后兼容:不传参返回第一页。补 projects 分页 + 边界 422 测试。
This commit is contained in:
Yaojia Wang
2026-07-08 12:39:31 +02:00
parent 1392830e57
commit 0892ff6cda
9 changed files with 121 additions and 31 deletions

View File

@@ -63,8 +63,13 @@ class FakeProjectRepo:
self.rows[pid] = (owner_id, view)
return view
async def list_for_owner(self, owner_id: uuid.UUID) -> list[ProjectView]:
return [v for (o, v) in self.rows.values() if o == owner_id]
async def list_for_owner(
self, owner_id: uuid.UUID, *, limit: int | None = None, offset: int = 0
) -> list[ProjectView]:
views = [v for (o, v) in self.rows.values() if o == owner_id]
if limit is None:
return views
return views[offset : offset + limit]
async def get(self, owner_id: uuid.UUID, project_id: uuid.UUID) -> ProjectView | None:
entry = self.rows.get(project_id)

View File

@@ -222,6 +222,33 @@ async def test_list_projects() -> None:
assert all(p["pending_review_count"] == 0 for p in projects)
@pytest.mark.asyncio
async def test_list_projects_paginates_with_limit_and_offset() -> None:
client, _, _, _ = _make_client()
async with client:
for t in ("", "", ""):
await client.post("/projects", json={"title": t})
page1 = await client.get("/projects?limit=2")
page2 = await client.get("/projects?limit=2&offset=2")
assert page1.status_code == 200
assert [p["title"] for p in page1.json()["projects"]] == ["", ""]
assert page2.status_code == 200
assert [p["title"] for p in page2.json()["projects"]] == [""]
@pytest.mark.asyncio
async def test_list_projects_rejects_out_of_range_pagination() -> None:
client, _, _, _ = _make_client()
async with client:
too_small = await client.get("/projects?limit=0")
too_large = await client.get("/projects?limit=201")
neg_offset = await client.get("/projects?offset=-1")
# 边界越界 → FastAPI 422limit∈[1,200]、offset≥0
assert too_small.status_code == 422
assert too_large.status_code == 422
assert neg_offset.status_code == 422
@pytest.mark.asyncio
async def test_get_project_detail() -> None:
client, _, _, _ = _make_client()

View File

@@ -33,8 +33,13 @@ class _FakeTemplateRepo:
self.rows[view.id] = view
return view
async def list_for_owner(self, owner_id: uuid.UUID) -> list[TemplateView]:
return list(self.rows.values())
async def list_for_owner(
self, owner_id: uuid.UUID, *, limit: int | None = None, offset: int = 0
) -> list[TemplateView]:
views = list(self.rows.values())
if limit is None:
return views
return views[offset : offset + limit]
async def delete(self, owner_id: uuid.UUID, template_id: uuid.UUID) -> bool:
if template_id in self.rows: