import json from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from ..auth import verify_token from ..database import get_db from ..models import GraphLayout from ..schemas import ( GraphLayoutCreate, GraphLayoutDetailResponse, GraphLayoutResponse, GraphLayoutUpdate, ) router = APIRouter(prefix="/api/layouts", tags=["layouts"], dependencies=[Depends(verify_token)]) def _to_response(layout: GraphLayout) -> GraphLayoutResponse: return GraphLayoutResponse.model_validate(layout) def _to_detail_response(layout: GraphLayout) -> GraphLayoutDetailResponse: base = GraphLayoutResponse.model_validate(layout) return GraphLayoutDetailResponse( **base.model_dump(), node_positions=json.loads(layout.node_positions), viewport=json.loads(layout.viewport), ) AUTOSAVE_NAME = "__autosave__" @router.get("", response_model=list[GraphLayoutResponse]) def list_layouts(project_id: int | None = None, db: Session = Depends(get_db)): query = db.query(GraphLayout).filter(GraphLayout.name != AUTOSAVE_NAME) if project_id is not None: query = query.filter(GraphLayout.project_id == project_id) return [_to_response(l) for l in query.order_by(GraphLayout.updated_at.desc()).all()] @router.get("/autosave", response_model=GraphLayoutDetailResponse) def get_autosave(db: Session = Depends(get_db)): layout = db.query(GraphLayout).filter(GraphLayout.name == AUTOSAVE_NAME).first() if not layout: raise HTTPException(404, "No autosave layout found") return _to_detail_response(layout) @router.put("/autosave", response_model=GraphLayoutDetailResponse) def upsert_autosave(data: GraphLayoutUpdate, db: Session = Depends(get_db)): layout = db.query(GraphLayout).filter(GraphLayout.name == AUTOSAVE_NAME).first() update_data = data.model_dump(exclude_unset=True) if "node_positions" in update_data: update_data["node_positions"] = json.dumps(update_data["node_positions"]) if "viewport" in update_data: update_data["viewport"] = json.dumps(update_data["viewport"]) if layout: for key, value in update_data.items(): setattr(layout, key, value) else: layout = GraphLayout( name=AUTOSAVE_NAME, description="Auto-saved working state", node_positions=update_data.get("node_positions", "{}"), viewport=update_data.get("viewport", "{}"), is_default=False, ) db.add(layout) db.commit() db.refresh(layout) return _to_detail_response(layout) @router.get("/{layout_id}", response_model=GraphLayoutDetailResponse) def get_layout(layout_id: int, db: Session = Depends(get_db)): layout = db.get(GraphLayout, layout_id) if not layout: raise HTTPException(404, "Layout not found") return _to_detail_response(layout) @router.post("", response_model=GraphLayoutDetailResponse, status_code=201) def create_layout(data: GraphLayoutCreate, db: Session = Depends(get_db)): if data.is_default: db.query(GraphLayout).filter( GraphLayout.project_id == data.project_id ).update({"is_default": False}) layout = GraphLayout( name=data.name, description=data.description, project_id=data.project_id, node_positions=json.dumps(data.node_positions), viewport=json.dumps(data.viewport), is_default=data.is_default, ) db.add(layout) db.commit() db.refresh(layout) return _to_detail_response(layout) @router.put("/{layout_id}", response_model=GraphLayoutDetailResponse) def update_layout(layout_id: int, data: GraphLayoutUpdate, db: Session = Depends(get_db)): layout = db.get(GraphLayout, layout_id) if not layout: raise HTTPException(404, "Layout not found") update_data = data.model_dump(exclude_unset=True) if update_data.get("is_default"): db.query(GraphLayout).filter( GraphLayout.project_id == layout.project_id, GraphLayout.id != layout_id, ).update({"is_default": False}) if "node_positions" in update_data: update_data["node_positions"] = json.dumps(update_data["node_positions"]) if "viewport" in update_data: update_data["viewport"] = json.dumps(update_data["viewport"]) for key, value in update_data.items(): setattr(layout, key, value) db.commit() db.refresh(layout) return _to_detail_response(layout) @router.delete("/{layout_id}", status_code=204) def delete_layout(layout_id: int, db: Session = Depends(get_db)): layout = db.get(GraphLayout, layout_id) if not layout: raise HTTPException(404, "Layout not found") db.delete(layout) db.commit()