fix(api): remove duplicate center node + format

This commit is contained in:
dextmorgn
2025-11-10 15:47:20 +01:00
parent 9a37954804
commit a784002f22

View File

@@ -1,5 +1,14 @@
from app.security.permissions import check_investigation_permission
from fastapi import APIRouter, HTTPException, Depends, status, UploadFile, File, Form, BackgroundTasks
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
@@ -230,7 +239,7 @@ def add_node(
node: NodeInput,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
current_user: Profile = Depends(get_current_user)
current_user: Profile = Depends(get_current_user),
):
node_data = node.data.model_dump()
@@ -430,102 +439,133 @@ def merge_nodes(
db: Session = Depends(get_db),
current_user: Profile = Depends(get_current_user),
):
# 1. Vérifier le sketch
# 1. Verify the sketch exists
sketch = db.query(Sketch).filter(Sketch.id == sketch_id).first()
if not sketch:
raise HTTPException(status_code=404, detail="Sketch not found")
oldNodeIds = [id for id in oldNodes]
if not oldNodes or len(oldNodes) == 0:
raise HTTPException(status_code=400, detail="oldNodes cannot be empty")
# 2. Préparer le node unique (utiliser nodeId)
node_id = getattr(newNode, "id", None)
if not node_id:
raise HTTPException(status_code=400, detail="newNode.id is required")
oldNodeIds = oldNodes
properties = {}
if newNode.data:
flattened_data = flatten(newNode.data.dict())
properties.update(flattened_data)
# 2. Prepare the merged node data
node_data = newNode.data.model_dump() if newNode.data else {}
node_type = node_data.get("type", "Node")
cypher_props = dict_to_cypher_props(properties)
node_type = getattr(newNode, "type", "Node")
# Build properties for the new merged node
properties = {
"type": node_type.lower(),
"sketch_id": sketch_id,
"label": node_data.get("label", "Merged Node"),
"caption": node_data.get("label", "Merged Node"),
}
# Add all other data from the node
flattened_data = flatten(node_data)
properties.update(flattened_data)
# 3. Check if the newNode.id is one of the old nodes (reusing existing node)
# or if we need to create a brand new node
is_reusing_node = newNode.id in oldNodeIds
if is_reusing_node:
# Update the existing node that we're keeping
set_clause = ", ".join(f"n.{key} = ${key}" for key in properties.keys())
create_query = f"""
MATCH (n)
WHERE elementId(n) = $nodeId AND n.sketch_id = $sketch_id
SET {set_clause}
RETURN elementId(n) as newElementId
"""
params = {"nodeId": newNode.id, "sketch_id": sketch_id, **properties}
else:
# Create a completely new node
create_query = f"""
CREATE (n:`{node_type}`)
SET n = $properties
RETURN elementId(n) as newElementId
"""
params = {"properties": properties}
# 3. Créer ou merger le nouveau node
create_query = f"""
MERGE (new:`{node_type}` {{nodeId: $nodeId}})
SET new += $nodeData
RETURN elementId(new) as newElementId
"""
try:
result = neo4j_connection.query(
create_query, {"nodeId": node_id, "nodeData": cypher_props}
)
result = neo4j_connection.query(create_query, params)
if not result:
raise HTTPException(
status_code=500, detail="Failed to create/update merged node"
)
new_node_element_id = result[0]["newElementId"]
except Exception as e:
print(f"Error creating/merging new node: {e}")
raise HTTPException(status_code=500, detail="Failed to create new node")
# 4. Récupérer tous les types de relations des oldNodes
rel_types_query = """
MATCH (old)
WHERE elementId(old) IN $oldNodeIds AND old.sketch_id = $sketch_id
MATCH (old)-[r]-()
RETURN DISTINCT type(r) AS relType
"""
try:
rel_types_result = neo4j_connection.query(
rel_types_query, {"oldNodeIds": oldNodeIds, "sketch_id": sketch_id}
print(f"Error creating/updating merged node: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to create merged node: {str(e)}"
)
rel_types = [row["relType"] for row in rel_types_result] or []
except Exception as e:
print(f"Error fetching relation types: {e}")
raise HTTPException(status_code=500, detail="Failed to fetch relation types")
# 5. Construire la query pour copier les relations
blocks = []
for rel_type in rel_types:
block = f"""
// Relations entrantes
MATCH (new) WHERE elementId(new) = $newElementId
MATCH (old) WHERE elementId(old) IN $oldNodeIds
OPTIONAL MATCH (src)-[r:`{rel_type}`]->(old)
WITH src, new, r WHERE src IS NOT NULL
MERGE (src)-[newRel:`{rel_type}`]->(new)
ON CREATE SET newRel = r
ON MATCH SET newRel += r
WITH DISTINCT new
# 4. Copy all relationships from old nodes to the new node
# This handles both incoming and outgoing relationships while preserving types and properties
copy_relationships_query = """
MATCH (new) WHERE elementId(new) = $newElementId
// Relations sortantes
MATCH (new) WHERE elementId(new) = $newElementId
MATCH (old) WHERE elementId(old) IN $oldNodeIds
OPTIONAL MATCH (old)-[r:`{rel_type}`]->(dst)
WITH dst, new, r WHERE dst IS NOT NULL
MERGE (new)-[newRel2:`{rel_type}`]->(dst)
ON CREATE SET newRel2 = r
ON MATCH SET newRel2 += r
WITH DISTINCT new
"""
blocks.append(block)
UNWIND $oldNodeIds AS oldNodeId
MATCH (old) WHERE elementId(old) = oldNodeId AND old.sketch_id = $sketch_id
# 6. Supprimer les anciens nodes
delete_query = """
MATCH (old)
WHERE elementId(old) IN $oldNodeIds
DETACH DELETE old
// Copy incoming relationships - get all unique combinations
WITH new, collect(old) as oldNodes
UNWIND oldNodes as old
MATCH (src)-[r]->(old)
WHERE elementId(src) NOT IN $oldNodeIds AND elementId(src) <> $newElementId
WITH new, src, type(r) as relType, properties(r) as relProps, r
MERGE (src)-[newRel:RELATED_TO {sketch_id: $sketch_id}]->(new)
SET newRel = relProps
WITH new, $oldNodeIds as oldNodeIds
UNWIND oldNodeIds AS oldNodeId
MATCH (old) WHERE elementId(old) = oldNodeId AND old.sketch_id = $sketch_id
// Copy outgoing relationships
MATCH (old)-[r]->(dst)
WHERE elementId(dst) NOT IN oldNodeIds AND elementId(dst) <> $newElementId
WITH new, dst, type(r) as relType, properties(r) as relProps
MERGE (new)-[newRel:RELATED_TO {sketch_id: $sketch_id}]->(dst)
SET newRel = relProps
"""
full_query = "\n".join(blocks) + delete_query
# 7. Exécuter la query
try:
neo4j_connection.query(
full_query, {"newElementId": new_node_element_id, "oldNodeIds": oldNodeIds}
copy_relationships_query,
{
"newElementId": new_node_element_id,
"oldNodeIds": oldNodeIds,
"sketch_id": sketch_id,
},
)
except Exception as e:
print(f"Node merging error: {e}")
raise HTTPException(status_code=500, detail="Failed to merge node relations")
print(f"Error copying relationships: {e}")
# Don't fail if relationship copying has issues, continue to deletion
return {"status": "nodes merged", "count": len(oldNodeIds)}
# 5. Delete the old nodes (except if we're reusing one)
nodes_to_delete = [nid for nid in oldNodeIds if nid != new_node_element_id]
if nodes_to_delete:
delete_query = """
UNWIND $nodeIds AS nodeId
MATCH (old)
WHERE elementId(old) = nodeId AND old.sketch_id = $sketch_id
DETACH DELETE old
"""
try:
neo4j_connection.query(
delete_query, {"nodeIds": nodes_to_delete, "sketch_id": sketch_id}
)
except Exception as e:
print(f"Error deleting old nodes: {e}")
raise HTTPException(status_code=500, detail="Failed to delete old nodes")
return {
"status": "nodes merged",
"count": len(oldNodeIds),
"new_node_id": new_node_element_id,
}
@router.get("/{sketch_id}/nodes/{node_id}")
@@ -647,7 +687,11 @@ def get_related_nodes(
seen_relationships.add(record["rel_id"])
# Add related node if not seen
if record["other_node_id"] and record["other_node_id"] not in seen_nodes:
if (
record["other_node_id"]
and record["other_node_id"] not in seen_nodes
and record["other_node_id"] != center_node["id"]
):
related_nodes.append(
{
"id": record["other_node_id"],
@@ -709,9 +753,7 @@ async def analyze_import_file(
try:
content = await file.read()
except Exception as e:
raise HTTPException(
status_code=400, detail=f"Failed to read file: {str(e)}"
)
raise HTTPException(status_code=400, detail=f"Failed to read file: {str(e)}")
# Parse and analyze the file
try:
@@ -723,9 +765,7 @@ async def analyze_import_file(
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Failed to parse file: {str(e)}"
)
raise HTTPException(status_code=500, detail=f"Failed to parse file: {str(e)}")
# Convert entities to response models (no slicing)
entity_previews = [
@@ -734,7 +774,7 @@ async def analyze_import_file(
data=e.data,
detected_type=e.detected_type,
primary_value=e.primary_value,
confidence=e.confidence
confidence=e.confidence,
)
for e in result.entities
]
@@ -743,7 +783,7 @@ async def analyze_import_file(
entities=entity_previews,
total_entities=result.total_entities,
type_distribution=result.type_distribution,
columns=result.columns
columns=result.columns,
)
@@ -813,14 +853,10 @@ async def execute_import(
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Failed to parse file: {str(e)}"
)
raise HTTPException(status_code=500, detail=f"Failed to parse file: {str(e)}")
# Create mapping lookup by row index
mappings_by_row = {
m.row_index: m for m in entity_mappings if m.include
}
mappings_by_row = {m.row_index: m for m in entity_mappings if m.include}
# Import entities
nodes_created = 0
@@ -861,7 +897,7 @@ async def execute_import(
try:
neo4j_connection.query(
create_query,
{"sketch_id": sketch_id, "label": label, "props": flattened_props}
{"sketch_id": sketch_id, "label": label, "props": flattened_props},
)
nodes_created += 1
except Exception as e: