"""FlowDeck — Collections router: Notion-style databases (v1.3.0).""" from __future__ import annotations import json import logging from fastapi import APIRouter, Request, HTTPException, Query from fastapi.responses import HTMLResponse, RedirectResponse from app.db import get_conn from app.auth.session import SessionManager logger = logging.getLogger(__name__) router = APIRouter(tags=["collections"], prefix="/db") def _get_workspace(request: Request) -> str: """Returns the current workspace name from session.""" user = SessionManager.decode_session( request.cookies.get("flowdeck_session", "") ) return user.get("login", "Default") if user else "Default" # ── Collection CRUD ── @router.get("", response_class=HTMLResponse) async def list_collections(request: Request): """Page listing all collections in the workspace.""" with get_conn() as conn: rows = conn.execute( "SELECT * FROM collections ORDER BY name" ).fetchall() collections = [dict(r) for r in rows] return HTMLResponse( f"
{json.dumps(collections, indent=2, default=str)}"
f"{collection_dict.get('description', '')}
View: {view_dict['name']} ({view_dict['view_type']})
{json.dumps(pages_list, indent=2, default=str)}
""")
@router.get("/{collection_id}/api")
async def get_collection_api(request: Request, collection_id: int):
"""API: get a single collection with its pages."""
with get_conn() as conn:
collection = conn.execute(
"SELECT * FROM collections WHERE id=?", (collection_id,)
).fetchone()
if not collection:
raise HTTPException(status_code=404, detail="Collection not found")
pages = conn.execute(
"SELECT * FROM collection_pages WHERE collection_id=? ORDER BY position",
(collection_id,),
).fetchall()
views = conn.execute(
"SELECT * FROM collection_views WHERE collection_id=? ORDER BY position",
(collection_id,),
).fetchall()
return {
"collection": dict(collection),
"pages": [dict(p) for p in pages],
"views": [dict(v) for v in views],
}
@router.put("/api/{collection_id}")
async def update_collection_api(request: Request, collection_id: int):
"""API: update a collection."""
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
existing = conn.execute(
"SELECT * FROM collections WHERE id=?", (collection_id,)
).fetchone()
if not existing:
raise HTTPException(status_code=404, detail="Collection not found")
name = body.get("name", existing["name"])
description = body.get("description", existing["description"])
icon = body.get("icon", existing["icon"])
is_locked = body.get("is_locked", existing["is_locked"])
schema_json = json.dumps(body.get("schema", json.loads(existing["schema_json"])))
gitea_owner = body.get("gitea_owner", existing["gitea_owner"])
gitea_repo = body.get("gitea_repo", existing["gitea_repo"])
conn.execute(
"""UPDATE collections SET name=?, description=?, icon=?, schema_json=?,
is_locked=?, gitea_owner=?, gitea_repo=?, updated_at=CURRENT_TIMESTAMP
WHERE id=?""",
(name, description, icon, schema_json, int(is_locked),
gitea_owner, gitea_repo, collection_id),
)
conn.commit()
return {"id": collection_id, "status": "updated"}
@router.delete("/api/{collection_id}")
async def delete_collection_api(request: Request, collection_id: int):
"""API: delete a collection and its pages (CASCADE)."""
with get_conn() as conn:
existing = conn.execute(
"SELECT * FROM collections WHERE id=?", (collection_id,)
).fetchone()
if not existing:
raise HTTPException(status_code=404, detail="Collection not found")
conn.execute("DELETE FROM collections WHERE id=?", (collection_id,))
conn.commit()
return {"id": collection_id, "status": "deleted"}
# ── Collection Pages CRUD ──
@router.post("/{collection_id}/pages/api")
async def create_page_api(request: Request, collection_id: int):
"""API: create a page in a collection."""
try:
body = await request.json()
except Exception:
body = {}
title = body.get("title", "").strip()
if not title:
raise HTTPException(status_code=400, detail="title is required")
icon = body.get("icon", "📄")
property_values = json.dumps(body.get("properties", {}))
gitea_issue_id = body.get("gitea_issue_id")
gitea_issue_number = body.get("gitea_issue_number")
with get_conn() as conn:
# Verify collection exists
coll = conn.execute("SELECT id FROM collections WHERE id=?", (collection_id,)).fetchone()
if not coll:
raise HTTPException(status_code=404, detail="Collection not found")
# Get next position
max_pos = conn.execute(
"SELECT COALESCE(MAX(position), -1) + 1 FROM collection_pages WHERE collection_id=?",
(collection_id,),
).fetchone()[0]
cur = conn.execute(
"""INSERT INTO collection_pages
(collection_id, title, icon, position, gitea_issue_id, gitea_issue_number, property_values_json)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(collection_id, title, icon, max_pos, gitea_issue_id, gitea_issue_number, property_values),
)
conn.commit()
page_id = cur.lastrowid
return {"id": page_id, "title": title, "status": "created"}
@router.get("/pages/{page_id}/api")
async def get_page_api(request: Request, page_id: int):
"""API: get a single page."""
with get_conn() as conn:
page = conn.execute(
"SELECT * FROM collection_pages WHERE id=?", (page_id,)
).fetchone()
if not page:
raise HTTPException(status_code=404, detail="Page not found")
return dict(page)
@router.put("/pages/{page_id}/api")
async def update_page_api(request: Request, page_id: int):
"""API: update a page's properties."""
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
existing = conn.execute(
"SELECT * FROM collection_pages WHERE id=?", (page_id,)
).fetchone()
if not existing:
raise HTTPException(status_code=404, detail="Page not found")
title = body.get("title", existing["title"])
icon = body.get("icon", existing["icon"])
position = body.get("position", existing["position"])
parent_id = body.get("parent_id", existing["parent_id"])
property_values = json.dumps(
body.get("properties", json.loads(existing["property_values_json"]))
)
conn.execute(
"""UPDATE collection_pages
SET title=?, icon=?, position=?, parent_id=?, property_values_json=?,
updated_at=CURRENT_TIMESTAMP
WHERE id=?""",
(title, icon, position, parent_id, property_values, page_id),
)
conn.commit()
return {"id": page_id, "status": "updated"}
@router.delete("/pages/{page_id}/api")
async def delete_page_api(request: Request, page_id: int):
"""API: delete a page from its collection."""
with get_conn() as conn:
existing = conn.execute(
"SELECT * FROM collection_pages WHERE id=?", (page_id,)
).fetchone()
if not existing:
raise HTTPException(status_code=404, detail="Page not found")
conn.execute("DELETE FROM collection_pages WHERE id=?", (page_id,))
conn.commit()
return {"id": page_id, "status": "deleted"}