feat: add visibility filter param and author_name to tree list endpoint

GET /trees now accepts ?visibility=private|team|link|public to scope results.
TreeListResponse includes author_name (full_name or email) and visibility.
Author names fetched in single query to avoid N+1.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-24 03:25:26 -05:00
parent 8c982a95ec
commit 29c3d5eed6
3 changed files with 77 additions and 2 deletions

View File

@@ -32,7 +32,7 @@ from app.core.tree_validation import can_publish_tree
router = APIRouter(prefix="/trees", tags=["trees"])
def build_tree_response(tree: Tree) -> TreeListResponse:
def build_tree_response(tree: Tree, author_map: dict | None = None) -> TreeListResponse:
"""Build TreeListResponse with category_info and tags."""
category_info = None
if tree.category_rel:
@@ -42,6 +42,8 @@ def build_tree_response(tree: Tree) -> TreeListResponse:
slug=tree.category_rel.slug
)
author_name = (author_map or {}).get(tree.author_id)
return TreeListResponse(
id=tree.id,
name=tree.name,
@@ -52,10 +54,12 @@ def build_tree_response(tree: Tree) -> TreeListResponse:
category_info=category_info,
tags=tree.tag_names,
author_id=tree.author_id,
author_name=author_name,
account_id=tree.account_id,
is_active=tree.is_active,
is_public=tree.is_public,
is_default=tree.is_default,
visibility=tree.visibility,
status=tree.status,
version=tree.version,
usage_count=tree.usage_count,
@@ -125,6 +129,7 @@ async def list_trees(
is_active: Optional[bool] = Query(None, description="Filter by active status"),
author_id: Optional[UUID] = Query(None, description="Filter by author ID"),
is_public: Optional[bool] = Query(None, description="Filter by public status"),
visibility: Optional[str] = Query(None, description="Filter by visibility: private, team, link, public"),
sort_by: Optional[str] = Query("usage_count", description="Sort order: usage_count, updated_at, created_at, name, name_desc, version"),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=100)
@@ -158,6 +163,8 @@ async def list_trees(
query = query.where(Tree.author_id == author_id)
if is_public is not None:
query = query.where(Tree.is_public == is_public)
if visibility:
query = query.where(Tree.visibility == visibility)
# Filter by tags (all specified tags must be present)
if tags:
@@ -206,7 +213,17 @@ async def list_trees(
result = await db.execute(query)
trees = result.scalars().unique().all()
return [build_tree_response(tree) for tree in trees]
# Fetch author names in one query (avoids N+1)
author_ids = {t.author_id for t in trees if t.author_id}
author_map: dict = {}
if author_ids:
authors_result = await db.execute(
select(User.id, User.name, User.email).where(User.id.in_(author_ids))
)
for row in authors_result:
author_map[row.id] = row.name or row.email
return [build_tree_response(tree, author_map) for tree in trees]
@router.get("/categories", response_model=list[str])