mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 16:56:22 +02:00
235 lines
6.9 KiB
Python
235 lines
6.9 KiB
Python
"""
|
|
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}
|