feat(api): nodes positions

This commit is contained in:
dextmorgn
2025-11-13 23:12:22 +01:00
parent c21db239ac
commit 0427c7919c
2 changed files with 91 additions and 0 deletions

View File

@@ -203,6 +203,9 @@ async def get_sketch_nodes(
"data": record["data"],
"label": record["data"].get("label", "Node"),
"idx": idx,
# Extract x and y positions if they exist
**({"x": record["data"]["x"]} if "x" in record["data"] else {}),
**({"y": record["data"]["y"]} if "y" in record["data"] else {}),
}
for idx, record in enumerate(nodes_result)
]
@@ -400,6 +403,60 @@ def edit_node(
}
class NodePosition(BaseModel):
nodeId: str
x: float
y: float
class UpdatePositionsInput(BaseModel):
positions: List[NodePosition]
@router.put("/{sketch_id}/nodes/positions")
@update_sketch_timestamp
def update_node_positions(
sketch_id: str,
data: UpdatePositionsInput,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
current_user: Profile = Depends(get_current_user),
):
"""
Update positions (x, y) for multiple nodes in batch.
This is used to persist node positions after drag operations in the graph viewer.
"""
# Verify the sketch exists and user has access
sketch = db.query(Sketch).filter(Sketch.id == sketch_id).first()
if not sketch:
raise HTTPException(status_code=404, detail="Sketch not found")
check_investigation_permission(
current_user.id, sketch.investigation_id, actions=["update"], db=db
)
if not data.positions:
return {"status": "no positions to update", "count": 0}
# Convert Pydantic models to dicts for GraphRepository
positions = [pos.model_dump() for pos in data.positions]
# Update positions using GraphRepository
try:
graph_repo = GraphRepository(neo4j_connection)
updated_count = graph_repo.update_nodes_positions(
positions=positions,
sketch_id=sketch_id
)
except Exception as e:
print(f"Position update error: {e}")
raise HTTPException(status_code=500, detail="Failed to update node positions")
return {
"status": "positions updated",
"count": updated_count,
}
@router.delete("/{sketch_id}/nodes")
@update_sketch_timestamp
def delete_nodes(

View File

@@ -547,6 +547,40 @@ class GraphRepository:
return self._connection.query(cypher, parameters)
def update_nodes_positions(
self,
positions: List[Dict[str, Any]],
sketch_id: str
) -> int:
"""
Update positions (x, y) for multiple nodes in batch.
Args:
positions: List of dicts with keys 'nodeId', 'x', 'y'
sketch_id: Investigation sketch ID (for safety)
Returns:
Number of nodes updated
"""
if not self._connection or not positions:
return 0
query = """
UNWIND $positions AS pos
MATCH (n)
WHERE elementId(n) = pos.nodeId AND n.sketch_id = $sketch_id
SET n.x = pos.x, n.y = pos.y
RETURN count(n) as updated_count
"""
params = {
"positions": positions,
"sketch_id": sketch_id
}
result = self._connection.query(query, params)
return result[0]["updated_count"] if result else 0
def __enter__(self):
"""Context manager entry."""
return self