feat(api): update sketch and investigation last_updated_at on action

This commit is contained in:
dextmorgn
2025-11-09 23:48:09 +01:00
parent 53e3cfe685
commit f131167c95
2 changed files with 138 additions and 2 deletions

View File

@@ -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),
):

View File

@@ -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