"""FlowDeck — v6.5.0 database row content pages (synced blocks production). A database row (``collection_pages``) has no block content of its own: its Notion-style content lives in a shadow ``pages`` row linked via ``pages.collection_row_id``. The full page editor (synced blocks, versions, realtime, permissions) is reused unchanged on that page. ``ensure_row_page()`` lazily creates (or heals) the shadow page and is the single entry point used by the row-open endpoint. """ from __future__ import annotations import logging from app.db import get_conn logger = logging.getLogger(__name__) def _host_workspace(conn, coll: dict) -> tuple[str, int | None]: """Workspace (key, id) the shadow page should live in. Resolution order: full-page database host page → inline parent page → the collection's own ``workspace_id`` (key left empty). """ host = conn.execute( "SELECT workspace, workspace_id FROM pages " "WHERE collection_id=? AND deleted_at IS NULL " "AND collection_row_id IS NULL LIMIT 1", (coll.get("id"),), ).fetchone() if not host and coll.get("parent_page_id"): host = conn.execute( "SELECT workspace, workspace_id FROM pages WHERE id=?", (coll["parent_page_id"],), ).fetchone() if host: return host["workspace"] or "", host["workspace_id"] if coll.get("gitea_owner") and coll.get("gitea_repo"): return f"{coll['gitea_owner']}/{coll['gitea_repo']}", coll.get("workspace_id") return "", coll.get("workspace_id") def ensure_row_page(row_id: int) -> int: """Return the content page id for a database row, creating it if needed. - existing live page → returned as-is; - soft-deleted page → restored (title resynced); - missing → created with the row's title and the collection workspace. Raises ``KeyError`` when the row does not exist. """ with get_conn() as conn: row = conn.execute( "SELECT id, collection_id, title FROM collection_pages WHERE id=?", (row_id,), ).fetchone() if not row: raise KeyError(f"row {row_id} not found") coll_row = conn.execute( "SELECT id, workspace_id, parent_page_id, gitea_owner, gitea_repo " "FROM collections WHERE id=?", (row["collection_id"],), ).fetchone() coll = dict(coll_row) if coll_row else {"id": row["collection_id"]} existing = conn.execute( "SELECT id, deleted_at, title FROM pages WHERE collection_row_id=?", (row_id,), ).fetchone() if existing: if existing["deleted_at"]: conn.execute( "UPDATE pages SET deleted_at=NULL, parent_section='DbRow', " "title=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", (row["title"] or existing["title"] or "Untitled", existing["id"]), ) conn.commit() return existing["id"] ws_key, ws_id = _host_workspace(conn, coll) cur = conn.execute( """INSERT INTO pages (workspace, workspace_id, title, content, content_format, parent_section, collection_id, collection_row_id) VALUES (?, ?, ?, '', 'blocks', 'DbRow', ?, ?)""", (ws_key, ws_id, row["title"] or "Untitled", row["collection_id"], row_id), ) page_id = cur.lastrowid conn.commit() logger.debug("created content page %s for row %s", page_id, row_id) return page_id def sync_row_title_to_page(conn, row_id: int) -> None: """Row → page: copy the row's title onto its content page (if any).""" conn.execute( """UPDATE pages SET title=( SELECT title FROM collection_pages WHERE id=pages.collection_row_id ), updated_at=CURRENT_TIMESTAMP WHERE collection_row_id=? AND EXISTS ( SELECT 1 FROM collection_pages cp WHERE cp.id=pages.collection_row_id AND cp.title<>pages.title )""", (row_id,), ) def sync_page_title_to_row(conn, page_id: int) -> None: """Page → row: a rename inside the editor propagates to the row.""" conn.execute( """UPDATE collection_pages SET title=( SELECT title FROM pages WHERE id=? ), updated_at=CURRENT_TIMESTAMP WHERE id=(SELECT collection_row_id FROM pages WHERE id=?) AND EXISTS ( SELECT 1 FROM pages p WHERE p.id=? AND p.collection_row_id IS NOT NULL AND p.title<>collection_pages.title )""", (page_id, page_id, page_id), )