diff --git a/src/basic_memory/api/routers/documents.py b/src/basic_memory/api/routers/documents.py index 9ec8501e..0f963f14 100644 --- a/src/basic_memory/api/routers/documents.py +++ b/src/basic_memory/api/routers/documents.py @@ -49,21 +49,21 @@ async def list_documents( return [DocumentCreateResponse.model_validate(doc.__dict__) for doc in documents] -@router.get("/{path:path}", response_model=DocumentResponse) +@router.get("/{id:int}", response_model=DocumentResponse) async def get_document( - path: str, + id: int, service: DocumentServiceDep, ) -> DocumentResponse: - """Get a document by path.""" + """Get a document by ID.""" try: - document, content = await service.read_document(path) + document, content = await service.read_document_by_id(id) doc_dict = document.__dict__ | {"content": content} response = DocumentResponse.model_validate(doc_dict) return response except DocumentNotFoundError: raise HTTPException( status_code=404, - detail=f"Document not found: {path}" + detail=f"Document not found: {id}" ) except DocumentWriteError as e: raise HTTPException(status_code=400, detail=str(e)) diff --git a/src/basic_memory/services/document_service.py b/src/basic_memory/services/document_service.py index f074322e..43e95813 100644 --- a/src/basic_memory/services/document_service.py +++ b/src/basic_memory/services/document_service.py @@ -3,7 +3,7 @@ import hashlib from datetime import datetime, UTC from pathlib import Path -from typing import Optional, Dict, Any, List +from typing import Optional, Dict, Any, List, Tuple import yaml from loguru import logger @@ -60,18 +60,16 @@ class DocumentService(BaseService[DocumentRepository]): except Exception as e: raise DocumentWriteError(f"Failed to create directory: {parent}: {e}") - async def add_frontmatter(self, content: str, doc_id: int, metadata: Optional[Dict[str, Any]] = None) -> str: + async def add_frontmatter( + self, content: str, doc_id: int, metadata: Optional[Dict[str, Any]] = None + ) -> str: """Add frontmatter to document content.""" # Generate frontmatter with timestamps now = datetime.now(UTC).isoformat() - frontmatter = { - "id": doc_id, - "created": now, - "modified": now - } + frontmatter = {"id": doc_id, "created": now, "modified": now} if metadata: frontmatter.update(metadata) - + yaml_fm = yaml.dump(frontmatter, sort_keys=False) return f"---\n{yaml_fm}---\n\n{content}" @@ -104,10 +102,7 @@ class DocumentService(BaseService[DocumentRepository]): try: # 1. Create initial DB record to get ID - doc = await self.repository.create({ - "path": str(path), - "doc_metadata": metadata - }) + doc = await self.repository.create({"path": str(path), "doc_metadata": metadata}) # 2. Add frontmatter with DB-generated ID content_with_frontmatter = await self.add_frontmatter(content, doc.id, metadata) @@ -122,11 +117,41 @@ class DocumentService(BaseService[DocumentRepository]): except Exception as e: # Clean up on any failure - if 'doc' in locals(): # DB record was created + if "doc" in locals(): # DB record was created await self.repository.delete(doc.id) file_path.unlink(missing_ok=True) raise DocumentWriteError(f"Failed to create document: {e}") + async def read_document_by_id(self, id: int) -> Tuple[Document, str]: + """ + Read a document and its content by ID. + + Args: + id: Document ID + + Returns: + Tuple of (document record, content) + + Raises: + DocumentNotFoundError: If document doesn't exist + DocumentError: If file read fails + """ + logger.debug(f"Reading document with ID {id}") + + # Get document record + query = select(Document).where(Document.id == id) + doc = await self.repository.find_one(query) + if not doc: + raise DocumentNotFoundError(f"Document not found: {id}") + + # Read content since file is source of truth + try: + file_path = Path(doc.path) + content = file_path.read_text() + return doc, content + except Exception as e: + raise DocumentError(f"Failed to read document {id}: {e}") + async def read_document(self, path: str) -> tuple[Document, str]: """ Read a document and its content. @@ -167,15 +192,15 @@ class DocumentService(BaseService[DocumentRepository]): ) -> Document: """ Update a document using its ID. - + Args: id: Document ID content: New content metadata: Optional new metadata - + Returns: Updated document record - + Raises: DocumentNotFoundError: If document doesn't exist DocumentWriteError: If update fails @@ -189,11 +214,7 @@ class DocumentService(BaseService[DocumentRepository]): raise DocumentNotFoundError(f"Document not found: {id}") # Use existing path to update file - return await self.update_document( - path=document.path, - content=content, - metadata=metadata - ) + return await self.update_document(path=document.path, content=content, metadata=metadata) async def update_document( self, path: str, content: str, metadata: Optional[Dict[str, Any]] = None @@ -239,7 +260,7 @@ class DocumentService(BaseService[DocumentRepository]): checksum = await self.compute_checksum(content) update_data = {"checksum": checksum} if metadata is not None: - update_data["doc_metadata"] = metadata + update_data["doc_metadata"] = metadata # pyright: ignore [reportArgumentType] updated_document = await self.repository.update(doc.id, update_data) assert updated_document is not None, f"Could not update document {doc.id}" @@ -272,4 +293,4 @@ class DocumentService(BaseService[DocumentRepository]): # Delete database record if it exists doc = await self.repository.find_by_path(str(path)) if doc: - await self.repository.delete(doc.id) \ No newline at end of file + await self.repository.delete(doc.id) diff --git a/tests/api/test_documents_router.py b/tests/api/test_documents_router.py index a0b23d4b..eec018e2 100644 --- a/tests/api/test_documents_router.py +++ b/tests/api/test_documents_router.py @@ -61,13 +61,14 @@ async def test_get_document(client: AsyncClient, tmp_path: Path): assert create_response.status_code == 201 created = create_response.json() - # Get document - response = await client.get(f"/documents/{test_doc['path']}") + # Get document by ID + response = await client.get(f"/documents/{created['id']}") assert response.status_code == 200 data = response.json() assert data["path"] == test_doc["path"] assert data["doc_metadata"] == test_doc["doc_metadata"] + assert data["id"] == created["id"] # Content checks - frontmatter followed by original content content = data["content"] @@ -78,9 +79,9 @@ async def test_get_document(client: AsyncClient, tmp_path: Path): @pytest.mark.asyncio -async def test_get_nonexistent_document(client: AsyncClient, tmp_path: Path): +async def test_get_nonexistent_document(client: AsyncClient): """Test getting a document that doesn't exist.""" - response = await client.get(f"/documents/{tmp_path}/nonexistent.md") + response = await client.get("/documents/99999") assert response.status_code == 404 assert "not found" in response.json()["detail"].lower() @@ -109,6 +110,7 @@ async def test_update_document(client: AsyncClient, tmp_path: Path): data = response.json() assert data["doc_metadata"] == update_doc["doc_metadata"] + assert data["id"] == created["id"] assert "# Updated" in data["content"] assert "Updated content" in data["content"] @@ -144,6 +146,7 @@ async def test_delete_document(client: AsyncClient, tmp_path: Path): # Create document create_response = await client.post("/documents/", json=test_doc) assert create_response.status_code == 201 + created = create_response.json() # Delete document response = await client.delete(f"/documents/{test_doc['path']}") @@ -154,7 +157,7 @@ async def test_delete_document(client: AsyncClient, tmp_path: Path): assert not doc_path.exists() # Verify 404 on subsequent get - get_response = await client.get(f"/documents/{test_doc['path']}") + get_response = await client.get(f"/documents/{created['id']}") assert get_response.status_code == 404