Skip to content

Commit f7d8b96

Browse files
committed
chore: wip.
1 parent 5e5492e commit f7d8b96

22 files changed

Lines changed: 3642 additions & 298 deletions

backend/app/database.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,14 +275,36 @@ async def rebuild_all_search_indexes(db):
275275
await db.commit()
276276

277277

278+
async def rebuild_all_backlinks(db):
279+
"""Rebuild backlinks for all existing pages."""
280+
from app.services.wikilink import parse_and_update_backlinks
281+
282+
rows = await db.execute_fetchall("SELECT COUNT(*) as cnt FROM backlinks")
283+
if rows[0]["cnt"] > 0:
284+
return # Already populated
285+
286+
pages = await db.execute_fetchall("SELECT id, content_md FROM pages")
287+
for p in pages:
288+
await parse_and_update_backlinks(db, p["id"], p["content_md"])
289+
if pages:
290+
await db.commit()
291+
292+
278293
async def init_db():
279294
db = await get_db()
280-
await db.executescript(SCHEMA_SQL)
295+
# Execute each statement individually to avoid blocking the event loop
296+
for statement in SCHEMA_SQL.split(';'):
297+
statement = statement.strip()
298+
if statement:
299+
await db.execute(statement)
281300
await db.commit()
282301

283302
# Rebuild search index for existing pages
284303
await rebuild_all_search_indexes(db)
285304

305+
# Rebuild backlinks for existing pages
306+
await rebuild_all_backlinks(db)
307+
286308
# Seed default templates
287309
for t in DEFAULT_TEMPLATES:
288310
existing = await db.execute_fetchall(

backend/app/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from app.database import init_db, close_db
66
from app.auth import ensure_admin_exists
7-
from app.routers import auth_router, pages, media, templates, search, tags, activity, bookmarks
7+
from app.routers import auth_router, pages, media, templates, search, tags, activity, bookmarks, versions, diagrams
88

99

1010
@asynccontextmanager
@@ -33,6 +33,8 @@ async def lifespan(app: FastAPI):
3333
app.include_router(tags.router)
3434
app.include_router(activity.router)
3535
app.include_router(bookmarks.router)
36+
app.include_router(versions.router)
37+
app.include_router(diagrams.router)
3638

3739

3840
@app.get("/api/health")

backend/app/routers/diagrams.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from fastapi import APIRouter, Depends, HTTPException
2+
from app.schemas import DiagramCreate, DiagramUpdate, DiagramResponse
3+
from app.auth import get_current_user
4+
from app.database import get_db
5+
6+
router = APIRouter(prefix="/api/diagrams", tags=["diagrams"])
7+
8+
9+
@router.post("", response_model=DiagramResponse, status_code=201)
10+
async def create_diagram(
11+
body: DiagramCreate,
12+
user=Depends(get_current_user),
13+
):
14+
db = await get_db()
15+
cursor = await db.execute(
16+
"""INSERT INTO diagrams (name, xml_data, page_id, created_by, updated_at)
17+
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)""",
18+
(body.name, body.xml_data, body.page_id, user["id"]),
19+
)
20+
await db.commit()
21+
row = await db.execute_fetchall(
22+
"SELECT * FROM diagrams WHERE id = ?", (cursor.lastrowid,)
23+
)
24+
d = row[0]
25+
return dict(d)
26+
27+
28+
@router.get("/{diagram_id}", response_model=DiagramResponse)
29+
async def get_diagram(diagram_id: int, user=Depends(get_current_user)):
30+
db = await get_db()
31+
rows = await db.execute_fetchall(
32+
"SELECT * FROM diagrams WHERE id = ?", (diagram_id,)
33+
)
34+
if not rows:
35+
raise HTTPException(status_code=404, detail="Diagram not found")
36+
return dict(rows[0])
37+
38+
39+
@router.put("/{diagram_id}", response_model=DiagramResponse)
40+
async def update_diagram(
41+
diagram_id: int,
42+
body: DiagramUpdate,
43+
user=Depends(get_current_user),
44+
):
45+
db = await get_db()
46+
rows = await db.execute_fetchall(
47+
"SELECT * FROM diagrams WHERE id = ?", (diagram_id,)
48+
)
49+
if not rows:
50+
raise HTTPException(status_code=404, detail="Diagram not found")
51+
52+
updates = []
53+
values = []
54+
for field in ("name", "xml_data", "svg_cache", "page_id"):
55+
val = getattr(body, field, None)
56+
if val is not None:
57+
updates.append(f"{field} = ?")
58+
values.append(val)
59+
60+
if updates:
61+
updates.append("updated_at = CURRENT_TIMESTAMP")
62+
values.append(diagram_id)
63+
await db.execute(
64+
f"UPDATE diagrams SET {', '.join(updates)} WHERE id = ?", values
65+
)
66+
await db.commit()
67+
68+
rows = await db.execute_fetchall(
69+
"SELECT * FROM diagrams WHERE id = ?", (diagram_id,)
70+
)
71+
return dict(rows[0])
72+
73+
74+
@router.delete("/{diagram_id}", status_code=204)
75+
async def delete_diagram(diagram_id: int, user=Depends(get_current_user)):
76+
db = await get_db()
77+
rows = await db.execute_fetchall(
78+
"SELECT * FROM diagrams WHERE id = ?", (diagram_id,)
79+
)
80+
if not rows:
81+
raise HTTPException(status_code=404, detail="Diagram not found")
82+
await db.execute("DELETE FROM diagrams WHERE id = ?", (diagram_id,))
83+
await db.commit()
84+
85+
86+
@router.get("/{diagram_id}/svg")
87+
async def get_diagram_svg(diagram_id: int, user=Depends(get_current_user)):
88+
db = await get_db()
89+
rows = await db.execute_fetchall(
90+
"SELECT svg_cache FROM diagrams WHERE id = ?", (diagram_id,)
91+
)
92+
if not rows:
93+
raise HTTPException(status_code=404, detail="Diagram not found")
94+
svg = rows[0]["svg_cache"]
95+
if not svg:
96+
raise HTTPException(status_code=404, detail="No SVG cache available")
97+
from fastapi.responses import Response
98+
return Response(content=svg, media_type="image/svg+xml")

backend/app/routers/pages.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import re
22
import unicodedata
33
from fastapi import APIRouter, HTTPException, Depends, Query
4-
from app.schemas import PageCreate, PageUpdate, PageResponse, PageListResponse
4+
from app.schemas import PageCreate, PageUpdate, PageResponse, PageListResponse, PageMoveRequest
55
from app.auth import get_current_user
66
from app.database import get_db
77
from app.services.search import rebuild_search_index, remove_from_search_index
8+
from app.services.wikilink import parse_and_update_backlinks
89
from app.routers.activity import log_activity
10+
from app.routers.versions import save_version
911

1012
router = APIRouter(prefix="/api/pages", tags=["pages"])
1113

@@ -90,6 +92,18 @@ def build_tree(parent_id):
9092
return build_tree(None)
9193

9294

95+
@router.get("/graph")
96+
async def page_graph(user=Depends(get_current_user)):
97+
db = await get_db()
98+
pages = await db.execute_fetchall("SELECT id, slug, title FROM pages")
99+
nodes = [{"id": p["id"], "slug": p["slug"], "title": p["title"]} for p in pages]
100+
101+
backlinks = await db.execute_fetchall("SELECT source_page_id, target_page_id FROM backlinks")
102+
links = [{"source": b["source_page_id"], "target": b["target_page_id"]} for b in backlinks]
103+
104+
return {"nodes": nodes, "links": links}
105+
106+
93107
@router.post("", response_model=PageResponse, status_code=201)
94108
async def create_page(body: PageCreate, user=Depends(get_current_user)):
95109
db = await get_db()
@@ -113,6 +127,8 @@ async def create_page(body: PageCreate, user=Depends(get_current_user)):
113127

114128
# Update search index
115129
await rebuild_search_index(db, page_id, body.title, content)
130+
# Parse wikilinks → update backlinks
131+
await parse_and_update_backlinks(db, page_id, content)
116132
# Log activity
117133
await log_activity(db, user["id"], "created", "page", page_id, {"title": body.title, "slug": slug})
118134
await db.commit()
@@ -149,9 +165,12 @@ async def update_page(slug: str, body: PageUpdate, user=Depends(get_current_user
149165
current = dict(rows[0])
150166
title = body.title if body.title is not None else current["title"]
151167
content = body.content_md if body.content_md is not None else current["content_md"]
152-
parent_id = body.parent_id if body.parent_id is not None else current["parent_id"]
168+
parent_id = body.parent_id if "parent_id" in body.model_fields_set else current["parent_id"]
153169
sort_order = body.sort_order if body.sort_order is not None else current["sort_order"]
154170

171+
# Save current state as a version before updating
172+
await save_version(db, current["id"], current["title"], current["content_md"], user["id"])
173+
155174
await db.execute(
156175
"""UPDATE pages SET title = ?, content_md = ?, parent_id = ?, sort_order = ?,
157176
updated_at = CURRENT_TIMESTAMP WHERE slug = ?""",
@@ -160,6 +179,8 @@ async def update_page(slug: str, body: PageUpdate, user=Depends(get_current_user
160179

161180
# Update search index
162181
await rebuild_search_index(db, current["id"], title, content)
182+
# Parse wikilinks → update backlinks
183+
await parse_and_update_backlinks(db, current["id"], content)
163184
# Log activity
164185
await log_activity(db, user["id"], "updated", "page", current["id"], {"title": title, "slug": slug})
165186
await db.commit()
@@ -168,6 +189,63 @@ async def update_page(slug: str, body: PageUpdate, user=Depends(get_current_user
168189
return dict(rows[0])
169190

170191

192+
@router.get("/{slug}/children")
193+
async def get_children(slug: str, user=Depends(get_current_user)):
194+
db = await get_db()
195+
rows = await db.execute_fetchall("SELECT id FROM pages WHERE slug = ?", (slug,))
196+
if not rows:
197+
raise HTTPException(status_code=404, detail="Page not found")
198+
page_id = rows[0]["id"]
199+
children = await db.execute_fetchall(
200+
"SELECT * FROM pages WHERE parent_id = ? ORDER BY sort_order, title",
201+
(page_id,),
202+
)
203+
return [dict(c) for c in children]
204+
205+
206+
@router.get("/{slug}/backlinks")
207+
async def get_backlinks(slug: str, user=Depends(get_current_user)):
208+
db = await get_db()
209+
rows = await db.execute_fetchall("SELECT id FROM pages WHERE slug = ?", (slug,))
210+
if not rows:
211+
raise HTTPException(status_code=404, detail="Page not found")
212+
page_id = rows[0]["id"]
213+
backlinks = await db.execute_fetchall(
214+
"""SELECT p.id, p.slug, p.title
215+
FROM backlinks b
216+
JOIN pages p ON p.id = b.source_page_id
217+
WHERE b.target_page_id = ?
218+
ORDER BY p.title""",
219+
(page_id,),
220+
)
221+
return [dict(b) for b in backlinks]
222+
223+
224+
@router.patch("/{slug}/move")
225+
async def move_page(slug: str, body: PageMoveRequest, user=Depends(get_current_user)):
226+
db = await get_db()
227+
rows = await db.execute_fetchall("SELECT id FROM pages WHERE slug = ?", (slug,))
228+
if not rows:
229+
raise HTTPException(status_code=404, detail="Page not found")
230+
231+
updates = []
232+
params = []
233+
if "parent_id" in body.model_fields_set:
234+
updates.append("parent_id = ?")
235+
params.append(body.parent_id)
236+
if "sort_order" in body.model_fields_set:
237+
updates.append("sort_order = ?")
238+
params.append(body.sort_order)
239+
240+
if not updates:
241+
raise HTTPException(status_code=400, detail="No fields to update")
242+
243+
params.append(slug)
244+
await db.execute(f"UPDATE pages SET {', '.join(updates)} WHERE slug = ?", params)
245+
await db.commit()
246+
return {"ok": True}
247+
248+
171249
@router.delete("/{slug}")
172250
async def delete_page(slug: str, user=Depends(get_current_user)):
173251
db = await get_db()

0 commit comments

Comments
 (0)