mirror of
https://github.com/reconurge/flowsint.git
synced 2026-03-11 17:34:31 -05:00
feat(api): update sketch and investigation last_updated_at on action
This commit is contained in:
@@ -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),
|
||||
):
|
||||
|
||||
119
flowsint-api/app/api/sketch_utils.py
Normal file
119
flowsint-api/app/api/sketch_utils.py
Normal 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
|
||||
Reference in New Issue
Block a user