diff --git a/app/api/v1/notes.py b/app/api/v1/notes.py new file mode 100644 index 0000000..3a75040 --- /dev/null +++ b/app/api/v1/notes.py @@ -0,0 +1,123 @@ +"""Note API endpoints (R-2: polymorphic parent validation).""" + +from __future__ import annotations + +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Query, 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.note import NoteParentType +from app.models.user import User +from app.schemas.note import NoteCreate, NoteOut, NoteUpdate +from app.services.note_service import ( + InvalidParent, + create_note as svc_create_note, + get_note as svc_get_note, + list_notes as svc_list_notes, + soft_delete_note as svc_soft_delete_note, + update_note as svc_update_note, +) + +router = APIRouter(prefix="/notes", tags=["notes"]) + + +@router.post( + "/", + response_model=NoteOut, + status_code=status.HTTP_201_CREATED, + summary="Create a new note (R-2: validates parent exists)", +) +async def create_note( + payload: NoteCreate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> NoteOut: + try: + note = await svc_create_note( + db, payload, org_id=current_user.org_id, author_id=current_user.id + ) + except InvalidParent as e: + raise HTTPException(status_code=404, detail=str(e)) from e + return NoteOut.model_validate(note) + + +@router.get( + "/", + response_model=list[NoteOut], + summary="List notes (filterable by parent_type + parent_id)", +) +async def list_notes( + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + parent_type: Optional[NoteParentType] = None, + parent_id: Optional[int] = None, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> list[NoteOut]: + notes = await svc_list_notes( + db, + org_id=current_user.org_id, + skip=skip, + limit=limit, + parent_type=parent_type.value if parent_type else None, + parent_id=parent_id, + ) + return [NoteOut.model_validate(n) for n in notes] + + +@router.get( + "/{note_id}", + response_model=NoteOut, + summary="Get one note", +) +async def get_note( + note_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> NoteOut: + note = await svc_get_note(db, note_id, org_id=current_user.org_id) + if note is None: + raise HTTPException(status_code=404, detail="Note not found") + return NoteOut.model_validate(note) + + +@router.patch( + "/{note_id}", + response_model=NoteOut, + summary="Update a note's body", +) +async def update_note( + note_id: int, + payload: NoteUpdate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> NoteOut: + note = await svc_get_note(db, note_id, org_id=current_user.org_id) + if note is None: + raise HTTPException(status_code=404, detail="Note not found") + updated = await svc_update_note(db, note, payload) + return NoteOut.model_validate(updated) + + +@router.delete( + "/{note_id}", + status_code=status.HTTP_204_NO_CONTENT, + response_class=Response, + summary="Soft-delete a note", +) +async def delete_note( + note_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> Response: + note = await svc_get_note(db, note_id, org_id=current_user.org_id) + if note is None: + raise HTTPException(status_code=404, detail="Note not found") + await svc_soft_delete_note(db, note) + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +__all__ = ["router"]