Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/api/app/api/v1/routes/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ async def list_documents(
return response


@router.get("/namespaces")
async def list_namespaces(
current_user: CurrentUser = Depends(with_current_user),
db: AsyncSession = Depends(get_db),
):
return await _document_service.list_namespaces(
db,
user_id=current_user.user_id,
)


@router.get("/{document_id}")
async def get_document(
document_id: str,
Expand Down
15 changes: 15 additions & 0 deletions apps/api/app/repositories/document_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ async def count_by_user_namespace(
)
return int(result.scalar_one())

async def list_namespace_counts_for_user(
self,
db: AsyncSession,
*,
user_id: str,
) -> Sequence[tuple[str, int]]:
result = await db.execute(
select(Document.namespace, func.count(Document.document_id))
.where(Document.user_id == user_id)
.where(Document.status != "archived")
.group_by(Document.namespace)
.order_by(Document.namespace.asc())
)
return [(row[0], int(row[1])) for row in result.all()]

async def get_document(
self,
db: AsyncSession,
Expand Down
16 changes: 16 additions & 0 deletions apps/api/app/services/documents/lifecycle_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,22 @@ async def list_documents(
},
}

async def list_namespaces(
self,
db: AsyncSession,
*,
user_id: str,
) -> dict[str, Any]:
rows = await self._repository.list_namespace_counts_for_user(
db,
user_id=user_id,
)
namespaces = [
{"namespace": namespace, "document_count": count}
for namespace, count in rows
]
return {"namespaces": namespaces}

async def list_document_chunks(
self,
db: AsyncSession,
Expand Down
63 changes: 63 additions & 0 deletions apps/api/tests/contract/test_documents_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -1334,3 +1334,66 @@ async def test_should_archive_a_document_via_the_legacy_archive_route(
assert response_json["archived_at"]
assert persisted_document["status"] == "archived"
assert persisted_document["archived_at"] is not None


@pytest.mark.asyncio
async def test_should_list_namespaces_with_document_counts_for_the_authenticated_user(
developer_api_client_factory: Callable[
[], AbstractAsyncContextManager[AsyncClient]
],
) -> None:
other_user_id = f"contract-user-{uuid4().hex[:12]}"

async with developer_api_client_factory() as api_client:
await ContractDatabase.insert_user(user_id=other_user_id)

await _insert_document(document_id=f"doc_{uuid4().hex[:12]}", namespace="alpha")
await _insert_document(document_id=f"doc_{uuid4().hex[:12]}", namespace="alpha")
await _insert_document(document_id=f"doc_{uuid4().hex[:12]}", namespace="beta")
await _insert_document(
document_id=f"doc_{uuid4().hex[:12]}",
namespace="other-user-ns",
user_id=other_user_id,
)
await _insert_document(
document_id=f"doc_{uuid4().hex[:12]}",
namespace="archived-ns",
status="archived",
)

response = await api_client.get("/api/v1/documents/namespaces")

assert response.status_code == 200

response_json = cast(dict[str, object], response.json())
namespaces = cast(list[dict[str, object]], response_json["namespaces"])

assert namespaces == [
{"namespace": "alpha", "document_count": 2},
{"namespace": "beta", "document_count": 1},
]


@pytest.mark.asyncio
async def test_should_return_empty_namespace_list_when_user_has_no_documents(
developer_api_client_factory: Callable[
[], AbstractAsyncContextManager[AsyncClient]
],
) -> None:
async with developer_api_client_factory() as api_client:
other_user_id = f"contract-user-{uuid4().hex[:12]}"
await ContractDatabase.insert_user(user_id=other_user_id)
await _insert_document(
document_id=f"doc_{uuid4().hex[:12]}",
namespace="not-mine",
user_id=other_user_id,
)

response = await api_client.get("/api/v1/documents/namespaces")

assert response.status_code == 200

response_json = cast(dict[str, object], response.json())
namespaces = cast(list[dict[str, object]], response_json["namespaces"])

assert namespaces == []