From 62b429b0cee46502b21b6fd6f0c8a539f340ab67 Mon Sep 17 00:00:00 2001 From: Leopoldadmin Date: Wed, 3 Jun 2026 23:52:10 +0000 Subject: [PATCH] Upload app/api/v1/tags.py --- app/api/v1/tags.py | 98 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 app/api/v1/tags.py diff --git a/app/api/v1/tags.py b/app/api/v1/tags.py new file mode 100644 index 0000000..0ef5753 --- /dev/null +++ b/app/api/v1/tags.py @@ -0,0 +1,98 @@ +"""Tag API endpoints (R-2: polymorphic parent validation).""" + +from __future__ import annotations + +from fastapi import APIRouter, Depends, HTTPException, Response, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.db import get_db +from app.core.deps import get_current_user +from app.models.user import User +from app.schemas.tag import TagCreate, TagLinkCreate, TagLinkOut, TagOut +from app.services.tag_service import ( + DuplicateTagLink, + InvalidParent, + TagNotFound, + create_tag as svc_create_tag, + link_tag as svc_link_tag, + list_tags as svc_list_tags, + unlink_tag as svc_unlink_tag, +) + +router = APIRouter(prefix="/tags", tags=["tags"]) + + +@router.post( + "/", + response_model=TagOut, + status_code=status.HTTP_201_CREATED, + summary="Create a new tag", +) +async def create_tag( + payload: TagCreate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> TagOut: + tag = await svc_create_tag(db, payload, org_id=current_user.org_id) + return TagOut.model_validate(tag) + + +@router.get( + "/", + response_model=list[TagOut], + summary="List all tags in the org", +) +async def list_tags( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> list[TagOut]: + tags = await svc_list_tags(db, org_id=current_user.org_id) + return [TagOut.model_validate(t) for t in tags] + + +@router.post( + "/link", + response_model=TagLinkOut, + status_code=status.HTTP_201_CREATED, + summary="Link a tag to an entity (R-2: validates parent exists)", +) +async def link_tag_endpoint( + payload: TagLinkCreate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> TagLinkOut: + try: + link = await svc_link_tag(db, payload, org_id=current_user.org_id) + except InvalidParent as e: + raise HTTPException(status_code=404, detail=str(e)) from e + except TagNotFound as e: + raise HTTPException(status_code=404, detail=str(e)) from e + except DuplicateTagLink as e: + raise HTTPException(status_code=409, detail=str(e)) from e + return TagLinkOut.model_validate(link) + + +@router.delete( + "/link", + status_code=status.HTTP_204_NO_CONTENT, + response_class=Response, + summary="Unlink a tag from an entity", +) +async def unlink_tag_endpoint( + payload: TagLinkCreate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> Response: + deleted = await svc_unlink_tag( + db, + tag_id=payload.tag_id, + parent_type=payload.parent_type, + parent_id=payload.parent_id, + org_id=current_user.org_id, + ) + if not deleted: + raise HTTPException(status_code=404, detail="Tag link not found") + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +__all__ = ["router"]