""" Notes routes for creating and managing note documents. """ from datetime import UTC, datetime from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.db import Document, DocumentType, Permission, User, get_async_session from app.schemas import DocumentRead, PaginatedResponse from app.users import current_active_user from app.utils.rbac import check_permission router = APIRouter() class CreateNoteRequest(BaseModel): title: str source_markdown: str | None = None @router.post("/search-spaces/{search_space_id}/notes", response_model=DocumentRead) async def create_note( search_space_id: int, request: CreateNoteRequest, session: AsyncSession = Depends(get_async_session), user: User = Depends(current_active_user), ): """ Create a new note document. Requires DOCUMENTS_CREATE permission. """ # Check RBAC permission await check_permission( session, user, search_space_id, Permission.DOCUMENTS_CREATE.value, "You don't have permission to create notes in this search space", ) if not request.title or not request.title.strip(): raise HTTPException(status_code=400, detail="Title is required") # Default empty markdown if not provided source_markdown = request.source_markdown if request.source_markdown else "" # Generate content hash (use title for now, will be updated on save) import hashlib content_hash = hashlib.sha256(request.title.encode()).hexdigest() # Create document with NOTE type document = Document( search_space_id=search_space_id, title=request.title.strip(), document_type=DocumentType.NOTE, content="", # Empty initially, will be populated on first save/reindex content_hash=content_hash, source_markdown=source_markdown, content_needs_reindexing=False, # Will be set to True on first save document_metadata={"NOTE": True}, embedding=None, # Will be generated on first reindex updated_at=datetime.now(UTC), created_by_id=user.id, # Track who created this note ) session.add(document) await session.commit() await session.refresh(document) return DocumentRead( id=document.id, title=document.title, document_type=document.document_type, content=document.content, content_hash=document.content_hash, unique_identifier_hash=document.unique_identifier_hash, document_metadata=document.document_metadata, search_space_id=document.search_space_id, created_at=document.created_at, updated_at=document.updated_at, created_by_id=document.created_by_id, ) @router.get( "/search-spaces/{search_space_id}/notes", response_model=PaginatedResponse[DocumentRead], ) async def list_notes( search_space_id: int, skip: int | None = None, page: int | None = None, page_size: int = 50, session: AsyncSession = Depends(get_async_session), user: User = Depends(current_active_user), ): """ List all notes in a search space. Requires DOCUMENTS_READ permission. """ # Check RBAC permission await check_permission( session, user, search_space_id, Permission.DOCUMENTS_READ.value, "You don't have permission to read notes in this search space", ) from sqlalchemy import func # Build query query = select(Document).where( Document.search_space_id == search_space_id, Document.document_type == DocumentType.NOTE, ) # Get total count count_query = select(func.count()).select_from( select(Document) .where( Document.search_space_id == search_space_id, Document.document_type == DocumentType.NOTE, ) .subquery() ) total_result = await session.execute(count_query) total = total_result.scalar() or 0 # Apply pagination if skip is not None: query = query.offset(skip) elif page is not None: query = query.offset(page * page_size) else: query = query.offset(0) if page_size > 0: query = query.limit(page_size) # Order by updated_at descending (most recent first) query = query.order_by(Document.updated_at.desc()) # Execute query result = await session.execute(query) documents = result.scalars().all() # Convert to response models items = [ DocumentRead( id=doc.id, title=doc.title, document_type=doc.document_type, content=doc.content, content_hash=doc.content_hash, unique_identifier_hash=doc.unique_identifier_hash, document_metadata=doc.document_metadata, search_space_id=doc.search_space_id, created_at=doc.created_at, updated_at=doc.updated_at, ) for doc in documents ] # Calculate pagination info actual_skip = ( skip if skip is not None else (page * page_size if page is not None else 0) ) has_more = (actual_skip + len(items)) < total if page_size > 0 else False return PaginatedResponse( items=items, total=total, page=page if page is not None else (actual_skip // page_size if page_size > 0 else 0), page_size=page_size, has_more=has_more, ) @router.delete("/search-spaces/{search_space_id}/notes/{note_id}") async def delete_note( search_space_id: int, note_id: int, session: AsyncSession = Depends(get_async_session), user: User = Depends(current_active_user), ): """ Delete a note. Requires DOCUMENTS_DELETE permission. """ # Check RBAC permission await check_permission( session, user, search_space_id, Permission.DOCUMENTS_DELETE.value, "You don't have permission to delete notes in this search space", ) # Get document result = await session.execute( select(Document).where( Document.id == note_id, Document.search_space_id == search_space_id, Document.document_type == DocumentType.NOTE, ) ) document = result.scalars().first() if not document: raise HTTPException(status_code=404, detail="Note not found") # Check if note is pending or currently being processed doc_state = document.status.get("state") if document.status else None if doc_state in ("pending", "processing"): raise HTTPException( status_code=409, detail="Cannot delete note while it is pending or being processed. Please wait for processing to complete.", ) # Delete document (chunks will be cascade deleted) await session.delete(document) await session.commit() return {"message": "Note deleted successfully", "note_id": note_id}