diff --git a/flowsint-api/app/api/routes/sketches.py b/flowsint-api/app/api/routes/sketches.py index 0dc25c3..b074464 100644 --- a/flowsint-api/app/api/routes/sketches.py +++ b/flowsint-api/app/api/routes/sketches.py @@ -1,5 +1,5 @@ from app.security.permissions import check_investigation_permission -from fastapi import APIRouter, HTTPException, Depends, status, UploadFile, File, Form +from fastapi import APIRouter, HTTPException, Depends, status, UploadFile, File, Form, BackgroundTasks from pydantic import BaseModel, Field from typing import Literal, List, Optional, Dict, Any from flowsint_core.utils import flatten @@ -11,6 +11,7 @@ from flowsint_core.core.graph_db import neo4j_connection from flowsint_core.core.postgre_db import get_db from app.api.deps import get_current_user from flowsint_core.imports import parse_file +from app.api.sketch_utils import update_sketch_timestamp router = APIRouter() @@ -223,8 +224,13 @@ async def get_sketch_nodes( @router.post("/{sketch_id}/nodes/add") +@update_sketch_timestamp def add_node( - sketch_id: str, node: NodeInput, current_user: Profile = Depends(get_current_user) + sketch_id: str, + node: NodeInput, + background_tasks: BackgroundTasks, + db: Session = Depends(get_db), + current_user: Profile = Depends(get_current_user) ): node_data = node.data.model_dump() @@ -285,9 +291,12 @@ class RelationInput(BaseModel): @router.post("/{sketch_id}/relations/add") +@update_sketch_timestamp def add_edge( sketch_id: str, relation: RelationInput, + background_tasks: BackgroundTasks, + db: Session = Depends(get_db), current_user: Profile = Depends(get_current_user), ): @@ -320,9 +329,11 @@ def add_edge( @router.put("/{sketch_id}/nodes/edit") +@update_sketch_timestamp def edit_node( sketch_id: str, node_edit: NodeEditInput, + background_tasks: BackgroundTasks, db: Session = Depends(get_db), current_user: Profile = Depends(get_current_user), ): @@ -377,9 +388,11 @@ def edit_node( @router.delete("/{sketch_id}/nodes") +@update_sketch_timestamp def delete_nodes( sketch_id: str, nodes: NodeDeleteInput, + background_tasks: BackgroundTasks, db: Session = Depends(get_db), current_user: Profile = Depends(get_current_user), ): @@ -408,10 +421,12 @@ def delete_nodes( @router.post("/{sketch_id}/nodes/merge") +@update_sketch_timestamp def merge_nodes( sketch_id: str, oldNodes: List[str], newNode: NodeMergeInput, + background_tasks: BackgroundTasks, db: Session = Depends(get_db), current_user: Profile = Depends(get_current_user), ): @@ -752,10 +767,12 @@ class ImportExecuteResponse(BaseModel): @router.post("/{sketch_id}/import/execute", response_model=ImportExecuteResponse) +@update_sketch_timestamp async def execute_import( sketch_id: str, file: UploadFile = File(...), entity_mappings_json: str = Form(...), + background_tasks: BackgroundTasks = BackgroundTasks(), db: Session = Depends(get_db), current_user: Profile = Depends(get_current_user), ): diff --git a/flowsint-api/app/api/sketch_utils.py b/flowsint-api/app/api/sketch_utils.py new file mode 100644 index 0000000..180b9e8 --- /dev/null +++ b/flowsint-api/app/api/sketch_utils.py @@ -0,0 +1,119 @@ +"""Utilities for sketch operations, including automatic timestamp updates.""" + +from functools import wraps +from datetime import datetime +from typing import Callable +from uuid import UUID +from fastapi import BackgroundTasks +from sqlalchemy.orm import Session +from flowsint_core.core.models import Sketch, Investigation + + +def update_sketch_last_modified(db: Session, sketch_id: str | UUID) -> None: + """ + Update the last_updated_at timestamp for a sketch and its parent investigation. + + This function is designed to be run as a background task to avoid + blocking the response. It updates both the sketch's and its parent + investigation's last_updated_at fields to the current time. + + Args: + db: SQLAlchemy database session + sketch_id: The ID of the sketch to update + """ + try: + sketch = db.query(Sketch).filter(Sketch.id == sketch_id).first() + if sketch: + current_time = datetime.now() + + # Update sketch timestamp + sketch.last_updated_at = current_time + + # Update parent investigation timestamp if it exists + if sketch.investigation_id: + investigation = db.query(Investigation).filter( + Investigation.id == sketch.investigation_id + ).first() + if investigation: + investigation.last_updated_at = current_time + + db.commit() + except Exception as e: + # Log error but don't raise to avoid disrupting background task + print(f"Error updating sketch/investigation timestamp for {sketch_id}: {e}") + db.rollback() + + +def update_sketch_timestamp(func: Callable) -> Callable: + """ + Decorator to automatically update sketch's last_updated_at timestamp. + + This decorator: + 1. Extracts the sketch_id from route parameters + 2. Schedules a background task to update last_updated_at + 3. Returns the response immediately (non-blocking) + + Usage: + @router.post("/{sketch_id}/nodes/add") + @update_sketch_timestamp + def add_node( + sketch_id: str, + node: NodeInput, + background_tasks: BackgroundTasks, + db: Session = Depends(get_db), + ... + ): + # Your route logic here + pass + + Requirements: + - Route must have a 'sketch_id' parameter (path or query) + - Route must have 'background_tasks: BackgroundTasks' parameter + - Route must have 'db: Session' parameter + """ + @wraps(func) + async def async_wrapper(*args, **kwargs): + # Extract required dependencies from kwargs + sketch_id = kwargs.get("sketch_id") + background_tasks: BackgroundTasks = kwargs.get("background_tasks") + db: Session = kwargs.get("db") + + if not sketch_id: + raise ValueError("sketch_id parameter is required for @update_sketch_timestamp") + if not background_tasks: + raise ValueError("background_tasks parameter is required for @update_sketch_timestamp") + if not db: + raise ValueError("db parameter is required for @update_sketch_timestamp") + + # Schedule the timestamp update as a background task + background_tasks.add_task(update_sketch_last_modified, db, sketch_id) + + # Execute the original route function + return await func(*args, **kwargs) + + @wraps(func) + def sync_wrapper(*args, **kwargs): + # Extract required dependencies from kwargs + sketch_id = kwargs.get("sketch_id") + background_tasks: BackgroundTasks = kwargs.get("background_tasks") + db: Session = kwargs.get("db") + + if not sketch_id: + raise ValueError("sketch_id parameter is required for @update_sketch_timestamp") + if not background_tasks: + raise ValueError("background_tasks parameter is required for @update_sketch_timestamp") + if not db: + raise ValueError("db parameter is required for @update_sketch_timestamp") + + # Schedule the timestamp update as a background task + background_tasks.add_task(update_sketch_last_modified, db, sketch_id) + + # Execute the original route function + return func(*args, **kwargs) + + # Return the appropriate wrapper based on whether the function is async + import inspect + if inspect.iscoroutinefunction(func): + return async_wrapper + else: + return sync_wrapper