Upload app/api/v1/tags.py
This commit is contained in:
@@ -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"]
|
||||||
Reference in New Issue
Block a user