Phase 3: Graph view, backlinks, quick switcher, export
- Add outgoing_links (JSON) and backlinks_count to Document model
- POST /documents/{id}/detect-links — detect [[uuid]] patterns in content
- GET /documents/{id}/backlinks — documents referencing this doc
- GET /documents/{id}/outgoing-links — documents this doc references
- GET /documents/{id}/links — combined incoming + outgoing
- GET /projects/{id}/graph — full project relationship graph
- GET /search/quick — fuzzy search (Quick Switcher Cmd+K)
- GET /projects/{id}/documents/search — project-scoped search
- GET /documents/{id}/export — markdown|json export
- GET /projects/{id}/export — json|zip export
- 27 new tests
This commit is contained in:
@@ -145,3 +145,122 @@ class DocumentBriefResponse(BaseModel):
|
||||
|
||||
class DocumentListResponse(BaseModel):
|
||||
documents: list[DocumentBriefResponse]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Phase 3: Link Detection & Graph Schemas
|
||||
# =============================================================================
|
||||
|
||||
class DetectLinksRequest(BaseModel):
|
||||
content: str = Field(..., max_length=5_000_000) # ~5MB limit
|
||||
|
||||
|
||||
class BrokenLink(BaseModel):
|
||||
reference: str
|
||||
reason: str # "document_not_found" | "invalid_format"
|
||||
|
||||
|
||||
class DetectLinksResponse(BaseModel):
|
||||
document_id: str
|
||||
outgoing_links: list[str]
|
||||
links_detected: int
|
||||
links_broken: int
|
||||
broken_links: list[BrokenLink] = []
|
||||
|
||||
|
||||
class BacklinkItem(BaseModel):
|
||||
document_id: str
|
||||
title: str
|
||||
project_id: str
|
||||
project_name: str
|
||||
excerpt: str
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class BacklinksResponse(BaseModel):
|
||||
document_id: str
|
||||
backlinks_count: int
|
||||
backlinks: list[BacklinkItem]
|
||||
|
||||
|
||||
class OutgoingLinkItem(BaseModel):
|
||||
document_id: str
|
||||
title: str
|
||||
project_id: str
|
||||
project_name: str
|
||||
exists: bool
|
||||
updated_at: datetime | None
|
||||
|
||||
|
||||
class OutgoingLinksResponse(BaseModel):
|
||||
document_id: str
|
||||
outgoing_links_count: int
|
||||
outgoing_links: list[OutgoingLinkItem]
|
||||
|
||||
|
||||
class LinkItem(BaseModel):
|
||||
document_id: str
|
||||
title: str
|
||||
anchor_text: str | None = None
|
||||
|
||||
|
||||
class LinksResponse(BaseModel):
|
||||
document_id: str
|
||||
outgoing_links: list[LinkItem]
|
||||
backlinks: list[LinkItem]
|
||||
|
||||
|
||||
class GraphNode(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
type: str = "document"
|
||||
|
||||
|
||||
class GraphEdge(BaseModel):
|
||||
source: str
|
||||
target: str
|
||||
type: str = "reference"
|
||||
|
||||
|
||||
class GraphStats(BaseModel):
|
||||
total_documents: int
|
||||
total_references: int
|
||||
orphaned_documents: int
|
||||
|
||||
|
||||
class GraphResponse(BaseModel):
|
||||
project_id: str
|
||||
nodes: list[GraphNode]
|
||||
edges: list[GraphEdge]
|
||||
stats: GraphStats
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Phase 3: Export Schemas
|
||||
# =============================================================================
|
||||
|
||||
class DocumentExportResponse(BaseModel):
|
||||
"""Used for JSON export format."""
|
||||
id: str
|
||||
title: str
|
||||
content: str
|
||||
tiptap_content: dict[str, Any] | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ProjectExportDocument(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
content: str
|
||||
tiptap_content: dict[str, Any] | None = None
|
||||
outgoing_links: list[str] = []
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ProjectExportResponse(BaseModel):
|
||||
project: dict[str, Any]
|
||||
documents: list[ProjectExportDocument]
|
||||
exported_at: datetime
|
||||
format_version: str = "3.0"
|
||||
|
||||
@@ -16,3 +16,39 @@ class SearchResult(BaseModel):
|
||||
|
||||
class SearchResponse(BaseModel):
|
||||
results: list[SearchResult]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Phase 3: Quick Switcher Schemas (Fuzzy Search)
|
||||
# =============================================================================
|
||||
|
||||
class QuickSwitcherItem(BaseModel):
|
||||
id: str
|
||||
type: str # "document" | "project"
|
||||
title: str
|
||||
subtitle: str | None = None
|
||||
highlight: str | None = None # HTML with <mark> tags
|
||||
icon: str | None = None
|
||||
project_id: str | None = None
|
||||
|
||||
|
||||
class QuickSwitcherResponse(BaseModel):
|
||||
query: str
|
||||
results: list[QuickSwitcherItem]
|
||||
total: int
|
||||
search_type: str = "fuzzy"
|
||||
|
||||
|
||||
class ProjectDocumentSearchItem(BaseModel):
|
||||
document_id: str
|
||||
title: str
|
||||
excerpt: str
|
||||
updated_at: datetime
|
||||
score: float
|
||||
|
||||
|
||||
class ProjectDocumentSearchResponse(BaseModel):
|
||||
project_id: str
|
||||
query: str
|
||||
results: list[ProjectDocumentSearchItem]
|
||||
total: int
|
||||
|
||||
Reference in New Issue
Block a user