mirror of
https://github.com/reconurge/flowsint.git
synced 2026-03-09 07:17:07 -05:00
Merge pull request #125 from gustavorps/feat/soft-delete
feat(core): Soft delete
This commit is contained in:
@@ -120,6 +120,7 @@ class Neo4jGraphRepository:
|
|||||||
MERGE (n:{node_type} {{ nodeLabel: $node_label, sketch_id: $sketch_id }})
|
MERGE (n:{node_type} {{ nodeLabel: $node_label, sketch_id: $sketch_id }})
|
||||||
ON CREATE SET n.created_at = $created_at
|
ON CREATE SET n.created_at = $created_at
|
||||||
SET n += $props
|
SET n += $props
|
||||||
|
SET n.deleted_at = null
|
||||||
RETURN elementId(n) AS id
|
RETURN elementId(n) AS id
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -145,9 +146,12 @@ class Neo4jGraphRepository:
|
|||||||
|
|
||||||
query = f"""
|
query = f"""
|
||||||
MATCH (from:{from_type} {{nodeLabel: $from_label, sketch_id: $sketch_id}})
|
MATCH (from:{from_type} {{nodeLabel: $from_label, sketch_id: $sketch_id}})
|
||||||
|
WHERE from.deleted_at IS NULL
|
||||||
MATCH (to:{to_type} {{nodeLabel: $to_label, sketch_id: $sketch_id}})
|
MATCH (to:{to_type} {{nodeLabel: $to_label, sketch_id: $sketch_id}})
|
||||||
|
WHERE to.deleted_at IS NULL
|
||||||
MERGE (from)-[r:{rel_label} {{sketch_id: $sketch_id}}]->(to)
|
MERGE (from)-[r:{rel_label} {{sketch_id: $sketch_id}}]->(to)
|
||||||
SET r += $props
|
SET r += $props
|
||||||
|
SET r.deleted_at = null
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return query, params
|
return query, params
|
||||||
@@ -395,7 +399,7 @@ class Neo4jGraphRepository:
|
|||||||
|
|
||||||
query = """
|
query = """
|
||||||
MATCH (n)
|
MATCH (n)
|
||||||
WHERE elementId(n) = $element_id AND n.sketch_id = $sketch_id
|
WHERE elementId(n) = $element_id AND n.sketch_id = $sketch_id AND n.deleted_at IS NULL
|
||||||
SET n += $props
|
SET n += $props
|
||||||
RETURN elementId(n) AS id
|
RETURN elementId(n) AS id
|
||||||
"""
|
"""
|
||||||
@@ -411,14 +415,14 @@ class Neo4jGraphRepository:
|
|||||||
|
|
||||||
def delete_nodes(self, node_ids: List[str], sketch_id: str) -> int:
|
def delete_nodes(self, node_ids: List[str], sketch_id: str) -> int:
|
||||||
"""
|
"""
|
||||||
Delete nodes by their element IDs.
|
Soft delete nodes by their element IDs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
node_ids: List of Neo4j element IDs
|
node_ids: List of Neo4j element IDs
|
||||||
sketch_id: Investigation sketch ID (for safety)
|
sketch_id: Investigation sketch ID (for safety)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Number of nodes deleted
|
Number of nodes soft-deleted
|
||||||
"""
|
"""
|
||||||
if not self._connection or not node_ids:
|
if not self._connection or not node_ids:
|
||||||
return 0
|
return 0
|
||||||
@@ -426,26 +430,34 @@ class Neo4jGraphRepository:
|
|||||||
query = """
|
query = """
|
||||||
UNWIND $node_ids AS node_id
|
UNWIND $node_ids AS node_id
|
||||||
MATCH (n)
|
MATCH (n)
|
||||||
WHERE elementId(n) = node_id AND n.sketch_id = $sketch_id
|
WHERE elementId(n) = node_id AND n.sketch_id = $sketch_id AND n.deleted_at IS NULL
|
||||||
DETACH DELETE n
|
OPTIONAL MATCH (n)-[r]-()
|
||||||
RETURN count(n) as deleted_count
|
WHERE r.sketch_id = $sketch_id AND r.deleted_at IS NULL
|
||||||
|
SET n.deleted_at = $deleted_at
|
||||||
|
SET r.deleted_at = $deleted_at
|
||||||
|
RETURN count(DISTINCT n) as deleted_count
|
||||||
"""
|
"""
|
||||||
|
|
||||||
result = self._connection.query(
|
result = self._connection.query(
|
||||||
query, {"node_ids": node_ids, "sketch_id": sketch_id}
|
query,
|
||||||
|
{
|
||||||
|
"node_ids": node_ids,
|
||||||
|
"sketch_id": sketch_id,
|
||||||
|
"deleted_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return result[0]["deleted_count"] if result else 0
|
return result[0]["deleted_count"] if result else 0
|
||||||
|
|
||||||
def delete_relationships(self, relationship_ids: List[str], sketch_id: str) -> int:
|
def delete_relationships(self, relationship_ids: List[str], sketch_id: str) -> int:
|
||||||
"""
|
"""
|
||||||
Delete relationships by their element IDs.
|
Soft delete relationships by their element IDs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
relationship_ids: List of Neo4j element IDs
|
relationship_ids: List of Neo4j element IDs
|
||||||
sketch_id: Investigation sketch ID (for safety)
|
sketch_id: Investigation sketch ID (for safety)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Number of relationships deleted
|
Number of relationships soft-deleted
|
||||||
"""
|
"""
|
||||||
if not self._connection or not relationship_ids:
|
if not self._connection or not relationship_ids:
|
||||||
return 0
|
return 0
|
||||||
@@ -453,37 +465,51 @@ class Neo4jGraphRepository:
|
|||||||
query = """
|
query = """
|
||||||
UNWIND $relationship_ids AS rel_id
|
UNWIND $relationship_ids AS rel_id
|
||||||
MATCH ()-[r]->()
|
MATCH ()-[r]->()
|
||||||
WHERE elementId(r) = rel_id AND r.sketch_id = $sketch_id
|
WHERE elementId(r) = rel_id AND r.sketch_id = $sketch_id AND r.deleted_at IS NULL
|
||||||
DELETE r
|
SET r.deleted_at = $deleted_at
|
||||||
RETURN count(r) as deleted_count
|
RETURN count(r) as deleted_count
|
||||||
"""
|
"""
|
||||||
|
|
||||||
result = self._connection.query(
|
result = self._connection.query(
|
||||||
query, {"relationship_ids": relationship_ids, "sketch_id": sketch_id}
|
query,
|
||||||
|
{
|
||||||
|
"relationship_ids": relationship_ids,
|
||||||
|
"sketch_id": sketch_id,
|
||||||
|
"deleted_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return result[0]["deleted_count"] if result else 0
|
return result[0]["deleted_count"] if result else 0
|
||||||
|
|
||||||
def delete_all_sketch_nodes(self, sketch_id: str) -> int:
|
def delete_all_sketch_nodes(self, sketch_id: str) -> int:
|
||||||
"""
|
"""
|
||||||
Delete all nodes and relationships for a sketch.
|
Soft delete all nodes and relationships for a sketch.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
sketch_id: Investigation sketch ID
|
sketch_id: Investigation sketch ID
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Number of nodes deleted
|
Number of nodes soft-deleted
|
||||||
"""
|
"""
|
||||||
if not self._connection:
|
if not self._connection:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
OPTIONAL MATCH (n {sketch_id: $sketch_id})
|
OPTIONAL MATCH (n {sketch_id: $sketch_id})
|
||||||
WHERE n IS NOT NULL
|
WHERE n IS NOT NULL AND n.deleted_at IS NULL
|
||||||
DETACH DELETE n
|
OPTIONAL MATCH (n)-[r]-()
|
||||||
RETURN count(n) as deleted_count
|
WHERE r.sketch_id = $sketch_id AND r.deleted_at IS NULL
|
||||||
|
SET n.deleted_at = $deleted_at
|
||||||
|
SET r.deleted_at = $deleted_at
|
||||||
|
RETURN count(DISTINCT n) as deleted_count
|
||||||
"""
|
"""
|
||||||
|
|
||||||
result = self._connection.query(query, {"sketch_id": sketch_id})
|
result = self._connection.query(
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
"sketch_id": sketch_id,
|
||||||
|
"deleted_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
},
|
||||||
|
)
|
||||||
return result[0]["deleted_count"] if result else 0
|
return result[0]["deleted_count"] if result else 0
|
||||||
|
|
||||||
def get_sketch_graph(
|
def get_sketch_graph(
|
||||||
@@ -506,7 +532,7 @@ class Neo4jGraphRepository:
|
|||||||
# Use OPTIONAL MATCH to avoid Neo4j warning when sketch_id property doesn't exist yet
|
# Use OPTIONAL MATCH to avoid Neo4j warning when sketch_id property doesn't exist yet
|
||||||
nodes_query = """
|
nodes_query = """
|
||||||
OPTIONAL MATCH (n)
|
OPTIONAL MATCH (n)
|
||||||
WHERE n.sketch_id = $sketch_id
|
WHERE n.sketch_id = $sketch_id AND n.deleted_at IS NULL
|
||||||
WITH n
|
WITH n
|
||||||
WHERE n IS NOT NULL
|
WHERE n IS NOT NULL
|
||||||
RETURN elementId(n) as id, labels(n) as labels, properties(n) as data
|
RETURN elementId(n) as id, labels(n) as labels, properties(n) as data
|
||||||
@@ -524,7 +550,9 @@ class Neo4jGraphRepository:
|
|||||||
rels_query = """
|
rels_query = """
|
||||||
UNWIND $node_ids AS nid
|
UNWIND $node_ids AS nid
|
||||||
MATCH (a)-[r]->(b)
|
MATCH (a)-[r]->(b)
|
||||||
WHERE elementId(a) = nid AND elementId(b) IN $node_ids
|
WHERE elementId(a) = nid
|
||||||
|
AND elementId(b) IN $node_ids
|
||||||
|
AND r.deleted_at IS NULL
|
||||||
RETURN elementId(r) as id, type(r) as type, elementId(a) as source,
|
RETURN elementId(r) as id, type(r) as type, elementId(a) as source,
|
||||||
elementId(b) as target, properties(r) as data
|
elementId(b) as target, properties(r) as data
|
||||||
"""
|
"""
|
||||||
@@ -544,12 +572,13 @@ class Neo4jGraphRepository:
|
|||||||
# delete the old relationship and create a new one with the new type.
|
# delete the old relationship and create a new one with the new type.
|
||||||
query = f"""
|
query = f"""
|
||||||
MATCH (a)-[r]->(b)
|
MATCH (a)-[r]->(b)
|
||||||
WHERE elementId(r) = $element_id AND r.sketch_id = $sketch_id
|
WHERE elementId(r) = $element_id AND r.sketch_id = $sketch_id AND r.deleted_at IS NULL
|
||||||
WITH a, b, r, properties(r) AS old_props
|
WITH a, b, r, properties(r) AS old_props
|
||||||
DELETE r
|
DELETE r
|
||||||
CREATE (a)-[r2:`{new_label}`]->(b)
|
CREATE (a)-[r2:`{new_label}`]->(b)
|
||||||
SET r2 = old_props
|
SET r2 = old_props
|
||||||
SET r2 += $props
|
SET r2 += $props
|
||||||
|
SET r2.deleted_at = null
|
||||||
RETURN
|
RETURN
|
||||||
elementId(r2) AS id,
|
elementId(r2) AS id,
|
||||||
type(r2) AS type,
|
type(r2) AS type,
|
||||||
@@ -558,7 +587,7 @@ class Neo4jGraphRepository:
|
|||||||
else:
|
else:
|
||||||
query = """
|
query = """
|
||||||
MATCH ()-[r]->()
|
MATCH ()-[r]->()
|
||||||
WHERE elementId(r) = $element_id AND r.sketch_id = $sketch_id
|
WHERE elementId(r) = $element_id AND r.sketch_id = $sketch_id AND r.deleted_at IS NULL
|
||||||
SET r += $props
|
SET r += $props
|
||||||
RETURN
|
RETURN
|
||||||
elementId(r) AS id,
|
elementId(r) AS id,
|
||||||
@@ -603,9 +632,10 @@ class Neo4jGraphRepository:
|
|||||||
rel_props = f"{{{props_str}}}"
|
rel_props = f"{{{props_str}}}"
|
||||||
|
|
||||||
query = f"""
|
query = f"""
|
||||||
MATCH (a) WHERE elementId(a) = $from_id
|
MATCH (a) WHERE elementId(a) = $from_id AND a.deleted_at IS NULL
|
||||||
MATCH (b) WHERE elementId(b) = $to_id
|
MATCH (b) WHERE elementId(b) = $to_id AND b.deleted_at IS NULL
|
||||||
MERGE (a)-[r:`{rel_label}` {rel_props}]->(b)
|
MERGE (a)-[r:`{rel_label}` {rel_props}]->(b)
|
||||||
|
SET r.deleted_at = null
|
||||||
RETURN properties(r) as rel
|
RETURN properties(r) as rel
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -655,7 +685,7 @@ class Neo4jGraphRepository:
|
|||||||
query = """
|
query = """
|
||||||
UNWIND $positions AS pos
|
UNWIND $positions AS pos
|
||||||
MATCH (n)
|
MATCH (n)
|
||||||
WHERE elementId(n) = pos.nodeId AND n.sketch_id = $sketch_id
|
WHERE elementId(n) = pos.nodeId AND n.sketch_id = $sketch_id AND n.deleted_at IS NULL
|
||||||
SET n.x = pos.x, n.y = pos.y
|
SET n.x = pos.x, n.y = pos.y
|
||||||
RETURN count(n) as updated_count
|
RETURN count(n) as updated_count
|
||||||
"""
|
"""
|
||||||
@@ -684,7 +714,7 @@ class Neo4jGraphRepository:
|
|||||||
query = """
|
query = """
|
||||||
UNWIND $node_ids AS node_id
|
UNWIND $node_ids AS node_id
|
||||||
MATCH (n)
|
MATCH (n)
|
||||||
WHERE elementId(n) = node_id AND n.sketch_id = $sketch_id
|
WHERE elementId(n) = node_id AND n.sketch_id = $sketch_id AND n.deleted_at IS NULL
|
||||||
RETURN properties(n) as data
|
RETURN properties(n) as data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -726,7 +756,7 @@ class Neo4jGraphRepository:
|
|||||||
set_clause = ", ".join(f"n.{key} = ${key}" for key in properties.keys())
|
set_clause = ", ".join(f"n.{key} = ${key}" for key in properties.keys())
|
||||||
create_query = f"""
|
create_query = f"""
|
||||||
MATCH (n)
|
MATCH (n)
|
||||||
WHERE elementId(n) = $nodeId AND n.sketch_id = $sketch_id
|
WHERE elementId(n) = $nodeId AND n.sketch_id = $sketch_id AND n.deleted_at IS NULL
|
||||||
SET {set_clause}
|
SET {set_clause}
|
||||||
RETURN elementId(n) as newElementId
|
RETURN elementId(n) as newElementId
|
||||||
"""
|
"""
|
||||||
@@ -750,25 +780,33 @@ class Neo4jGraphRepository:
|
|||||||
MATCH (new) WHERE elementId(new) = $newElementId
|
MATCH (new) WHERE elementId(new) = $newElementId
|
||||||
|
|
||||||
UNWIND $oldNodeIds AS oldNodeId
|
UNWIND $oldNodeIds AS oldNodeId
|
||||||
MATCH (old) WHERE elementId(old) = oldNodeId AND old.sketch_id = $sketch_id
|
MATCH (old) WHERE elementId(old) = oldNodeId AND old.sketch_id = $sketch_id AND old.deleted_at IS NULL
|
||||||
|
|
||||||
WITH new, collect(old) as oldNodes
|
WITH new, collect(old) as oldNodes
|
||||||
UNWIND oldNodes as old
|
UNWIND oldNodes as old
|
||||||
MATCH (src)-[r]->(old)
|
MATCH (src)-[r]->(old)
|
||||||
WHERE elementId(src) NOT IN $oldNodeIds AND elementId(src) <> $newElementId
|
WHERE elementId(src) NOT IN $oldNodeIds
|
||||||
|
AND elementId(src) <> $newElementId
|
||||||
|
AND src.deleted_at IS NULL
|
||||||
|
AND r.deleted_at IS NULL
|
||||||
WITH new, src, type(r) as relType, properties(r) as relProps, r
|
WITH new, src, type(r) as relType, properties(r) as relProps, r
|
||||||
MERGE (src)-[newRel:RELATED_TO {sketch_id: $sketch_id}]->(new)
|
MERGE (src)-[newRel:RELATED_TO {sketch_id: $sketch_id}]->(new)
|
||||||
SET newRel = relProps
|
SET newRel = relProps
|
||||||
|
SET newRel.deleted_at = null
|
||||||
|
|
||||||
WITH new, $oldNodeIds as oldNodeIds
|
WITH new, $oldNodeIds as oldNodeIds
|
||||||
UNWIND oldNodeIds AS oldNodeId
|
UNWIND oldNodeIds AS oldNodeId
|
||||||
MATCH (old) WHERE elementId(old) = oldNodeId AND old.sketch_id = $sketch_id
|
MATCH (old) WHERE elementId(old) = oldNodeId AND old.sketch_id = $sketch_id AND old.deleted_at IS NULL
|
||||||
|
|
||||||
MATCH (old)-[r]->(dst)
|
MATCH (old)-[r]->(dst)
|
||||||
WHERE elementId(dst) NOT IN oldNodeIds AND elementId(dst) <> $newElementId
|
WHERE elementId(dst) NOT IN oldNodeIds
|
||||||
|
AND elementId(dst) <> $newElementId
|
||||||
|
AND dst.deleted_at IS NULL
|
||||||
|
AND r.deleted_at IS NULL
|
||||||
WITH new, dst, type(r) as relType, properties(r) as relProps
|
WITH new, dst, type(r) as relType, properties(r) as relProps
|
||||||
MERGE (new)-[newRel:RELATED_TO {sketch_id: $sketch_id}]->(dst)
|
MERGE (new)-[newRel:RELATED_TO {sketch_id: $sketch_id}]->(dst)
|
||||||
SET newRel = relProps
|
SET newRel = relProps
|
||||||
|
SET newRel.deleted_at = null
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self._connection.query(
|
self._connection.query(
|
||||||
@@ -785,11 +823,19 @@ class Neo4jGraphRepository:
|
|||||||
delete_query = """
|
delete_query = """
|
||||||
UNWIND $nodeIds AS nodeId
|
UNWIND $nodeIds AS nodeId
|
||||||
MATCH (old)
|
MATCH (old)
|
||||||
WHERE elementId(old) = nodeId AND old.sketch_id = $sketch_id
|
WHERE elementId(old) = nodeId AND old.sketch_id = $sketch_id AND old.deleted_at IS NULL
|
||||||
DETACH DELETE old
|
OPTIONAL MATCH (old)-[r]-()
|
||||||
|
WHERE r.sketch_id = $sketch_id AND r.deleted_at IS NULL
|
||||||
|
SET old.deleted_at = $deleted_at
|
||||||
|
SET r.deleted_at = $deleted_at
|
||||||
"""
|
"""
|
||||||
self._connection.query(
|
self._connection.query(
|
||||||
delete_query, {"nodeIds": nodes_to_delete, "sketch_id": sketch_id}
|
delete_query,
|
||||||
|
{
|
||||||
|
"nodeIds": nodes_to_delete,
|
||||||
|
"sketch_id": sketch_id,
|
||||||
|
"deleted_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return new_node_element_id
|
return new_node_element_id
|
||||||
@@ -811,10 +857,13 @@ class Neo4jGraphRepository:
|
|||||||
|
|
||||||
query = """
|
query = """
|
||||||
MATCH (n)
|
MATCH (n)
|
||||||
WHERE elementId(n) = $node_id AND n.sketch_id = $sketch_id
|
WHERE elementId(n) = $node_id AND n.sketch_id = $sketch_id AND n.deleted_at IS NULL
|
||||||
|
|
||||||
OPTIONAL MATCH (n)-[r]-(other)
|
OPTIONAL MATCH (n)-[r]-(other)
|
||||||
WHERE other.sketch_id = $sketch_id AND other <> n
|
WHERE other.sketch_id = $sketch_id
|
||||||
|
AND other <> n
|
||||||
|
AND other.deleted_at IS NULL
|
||||||
|
AND r.deleted_at IS NULL
|
||||||
|
|
||||||
RETURN
|
RETURN
|
||||||
elementId(n) AS center_id,
|
elementId(n) AS center_id,
|
||||||
@@ -894,7 +943,7 @@ class Neo4jGraphRepository:
|
|||||||
"""
|
"""
|
||||||
query = """
|
query = """
|
||||||
OPTIONAL MATCH (n)
|
OPTIONAL MATCH (n)
|
||||||
WHERE n.sketch_id = $sketch_id AND n IS NOT NULL
|
WHERE n.sketch_id = $sketch_id AND n.deleted_at IS NULL AND n IS NOT NULL
|
||||||
RETURN count(n) as total
|
RETURN count(n) as total
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -915,7 +964,11 @@ class Neo4jGraphRepository:
|
|||||||
"""
|
"""
|
||||||
query = """
|
query = """
|
||||||
OPTIONAL MATCH (n)-[r]->(m)
|
OPTIONAL MATCH (n)-[r]->(m)
|
||||||
WHERE n.sketch_id = $sketch_id AND m.sketch_id = $sketch_id
|
WHERE n.sketch_id = $sketch_id
|
||||||
|
AND m.sketch_id = $sketch_id
|
||||||
|
AND n.deleted_at IS NULL
|
||||||
|
AND m.deleted_at IS NULL
|
||||||
|
AND r.deleted_at IS NULL
|
||||||
RETURN count(r) as total
|
RETURN count(r) as total
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class InMemoryGraphRepository:
|
|||||||
):
|
):
|
||||||
# Update existing node
|
# Update existing node
|
||||||
self._nodes[element_id].update(node_obj)
|
self._nodes[element_id].update(node_obj)
|
||||||
|
self._nodes[element_id]["deleted_at"] = None
|
||||||
return element_id
|
return element_id
|
||||||
|
|
||||||
# Create new node
|
# Create new node
|
||||||
@@ -53,6 +54,7 @@ class InMemoryGraphRepository:
|
|||||||
**node_obj,
|
**node_obj,
|
||||||
"sketch_id": sketch_id,
|
"sketch_id": sketch_id,
|
||||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"deleted_at": None,
|
||||||
"_labels": [node_type] if node_type else ["Node"],
|
"_labels": [node_type] if node_type else ["Node"],
|
||||||
}
|
}
|
||||||
return element_id
|
return element_id
|
||||||
@@ -65,48 +67,46 @@ class InMemoryGraphRepository:
|
|||||||
return None
|
return None
|
||||||
if self._nodes[element_id].get("sketch_id") != sketch_id:
|
if self._nodes[element_id].get("sketch_id") != sketch_id:
|
||||||
return None
|
return None
|
||||||
|
if self._nodes[element_id].get("deleted_at") is not None:
|
||||||
|
return None
|
||||||
self._nodes[element_id].update(updates)
|
self._nodes[element_id].update(updates)
|
||||||
return element_id
|
return element_id
|
||||||
|
|
||||||
def delete_nodes(self, node_ids: List[str], sketch_id: str) -> int:
|
def delete_nodes(self, node_ids: List[str], sketch_id: str) -> int:
|
||||||
"""Delete nodes by their element IDs. Returns count deleted."""
|
"""Soft delete nodes by their element IDs. Returns count soft-deleted."""
|
||||||
deleted = 0
|
deleted = 0
|
||||||
|
deleted_at = datetime.now(timezone.utc).isoformat()
|
||||||
for node_id in node_ids:
|
for node_id in node_ids:
|
||||||
if node_id in self._nodes:
|
if node_id in self._nodes:
|
||||||
if self._nodes[node_id].get("sketch_id") == sketch_id:
|
if self._nodes[node_id].get("sketch_id") == sketch_id:
|
||||||
# Also delete related edges
|
if self._nodes[node_id].get("deleted_at") is not None:
|
||||||
edges_to_delete = [
|
continue
|
||||||
eid
|
|
||||||
for eid, edge in self._edges.items()
|
# Also soft delete related edges
|
||||||
if edge.get("source") == node_id
|
for edge in self._edges.values():
|
||||||
or edge.get("target") == node_id
|
if edge.get("source") == node_id or edge.get("target") == node_id:
|
||||||
]
|
if edge.get("sketch_id") == sketch_id and edge.get("deleted_at") is None:
|
||||||
for eid in edges_to_delete:
|
edge["deleted_at"] = deleted_at
|
||||||
del self._edges[eid]
|
|
||||||
del self._nodes[node_id]
|
self._nodes[node_id]["deleted_at"] = deleted_at
|
||||||
deleted += 1
|
deleted += 1
|
||||||
return deleted
|
return deleted
|
||||||
|
|
||||||
def delete_all_sketch_nodes(self, sketch_id: str) -> int:
|
def delete_all_sketch_nodes(self, sketch_id: str) -> int:
|
||||||
"""Delete all nodes for a sketch. Returns count deleted."""
|
"""Soft delete all nodes for a sketch. Returns count soft-deleted."""
|
||||||
to_delete = [
|
deleted_at = datetime.now(timezone.utc).isoformat()
|
||||||
eid
|
deleted_count = 0
|
||||||
for eid, data in self._nodes.items()
|
|
||||||
if data.get("sketch_id") == sketch_id
|
|
||||||
]
|
|
||||||
for eid in to_delete:
|
|
||||||
del self._nodes[eid]
|
|
||||||
|
|
||||||
# Also delete related edges
|
for data in self._nodes.values():
|
||||||
edges_to_delete = [
|
if data.get("sketch_id") == sketch_id and data.get("deleted_at") is None:
|
||||||
eid
|
data["deleted_at"] = deleted_at
|
||||||
for eid, data in self._edges.items()
|
deleted_count += 1
|
||||||
if data.get("sketch_id") == sketch_id
|
|
||||||
]
|
|
||||||
for eid in edges_to_delete:
|
|
||||||
del self._edges[eid]
|
|
||||||
|
|
||||||
return len(to_delete)
|
for data in self._edges.values():
|
||||||
|
if data.get("sketch_id") == sketch_id and data.get("deleted_at") is None:
|
||||||
|
data["deleted_at"] = deleted_at
|
||||||
|
|
||||||
|
return deleted_count
|
||||||
|
|
||||||
def get_nodes_by_ids(
|
def get_nodes_by_ids(
|
||||||
self, node_ids: List[str], sketch_id: str
|
self, node_ids: List[str], sketch_id: str
|
||||||
@@ -116,7 +116,7 @@ class InMemoryGraphRepository:
|
|||||||
for node_id in node_ids:
|
for node_id in node_ids:
|
||||||
if node_id in self._nodes:
|
if node_id in self._nodes:
|
||||||
node = self._nodes[node_id]
|
node = self._nodes[node_id]
|
||||||
if node.get("sketch_id") == sketch_id:
|
if node.get("sketch_id") == sketch_id and node.get("deleted_at") is None:
|
||||||
result.append({"data": node})
|
result.append({"data": node})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -129,6 +129,8 @@ class InMemoryGraphRepository:
|
|||||||
node_id = pos.get("nodeId")
|
node_id = pos.get("nodeId")
|
||||||
if node_id in self._nodes:
|
if node_id in self._nodes:
|
||||||
if self._nodes[node_id].get("sketch_id") == sketch_id:
|
if self._nodes[node_id].get("sketch_id") == sketch_id:
|
||||||
|
if self._nodes[node_id].get("deleted_at") is not None:
|
||||||
|
continue
|
||||||
self._nodes[node_id]["x"] = pos.get("x")
|
self._nodes[node_id]["x"] = pos.get("x")
|
||||||
self._nodes[node_id]["y"] = pos.get("y")
|
self._nodes[node_id]["y"] = pos.get("y")
|
||||||
updated += 1
|
updated += 1
|
||||||
@@ -153,6 +155,8 @@ class InMemoryGraphRepository:
|
|||||||
for eid, node in self._nodes.items():
|
for eid, node in self._nodes.items():
|
||||||
if node.get("sketch_id") != sketch_id:
|
if node.get("sketch_id") != sketch_id:
|
||||||
continue
|
continue
|
||||||
|
if node.get("deleted_at") is not None:
|
||||||
|
continue
|
||||||
if node.get("nodeLabel") == from_label:
|
if node.get("nodeLabel") == from_label:
|
||||||
source_id = eid
|
source_id = eid
|
||||||
if node.get("nodeLabel") == to_label:
|
if node.get("nodeLabel") == to_label:
|
||||||
@@ -166,6 +170,7 @@ class InMemoryGraphRepository:
|
|||||||
"target": target_id,
|
"target": target_id,
|
||||||
"type": rel_label,
|
"type": rel_label,
|
||||||
"sketch_id": sketch_id,
|
"sketch_id": sketch_id,
|
||||||
|
"deleted_at": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def create_relationship_by_element_id(
|
def create_relationship_by_element_id(
|
||||||
@@ -178,6 +183,10 @@ class InMemoryGraphRepository:
|
|||||||
"""Create a relationship using element IDs."""
|
"""Create a relationship using element IDs."""
|
||||||
if from_element_id not in self._nodes or to_element_id not in self._nodes:
|
if from_element_id not in self._nodes or to_element_id not in self._nodes:
|
||||||
return None
|
return None
|
||||||
|
if self._nodes[from_element_id].get("deleted_at") is not None:
|
||||||
|
return None
|
||||||
|
if self._nodes[to_element_id].get("deleted_at") is not None:
|
||||||
|
return None
|
||||||
|
|
||||||
element_id = self._generate_element_id("rel")
|
element_id = self._generate_element_id("rel")
|
||||||
edge_data = {
|
edge_data = {
|
||||||
@@ -185,6 +194,7 @@ class InMemoryGraphRepository:
|
|||||||
"target": to_element_id,
|
"target": to_element_id,
|
||||||
"type": rel_label,
|
"type": rel_label,
|
||||||
"sketch_id": sketch_id,
|
"sketch_id": sketch_id,
|
||||||
|
"deleted_at": None,
|
||||||
}
|
}
|
||||||
self._edges[element_id] = edge_data
|
self._edges[element_id] = edge_data
|
||||||
return {"sketch_id": sketch_id}
|
return {"sketch_id": sketch_id}
|
||||||
@@ -197,6 +207,8 @@ class InMemoryGraphRepository:
|
|||||||
return None
|
return None
|
||||||
if self._edges[element_id].get("sketch_id") != sketch_id:
|
if self._edges[element_id].get("sketch_id") != sketch_id:
|
||||||
return None
|
return None
|
||||||
|
if self._edges[element_id].get("deleted_at") is not None:
|
||||||
|
return None
|
||||||
self._edges[element_id].update(rel_obj)
|
self._edges[element_id].update(rel_obj)
|
||||||
return {
|
return {
|
||||||
"id": element_id,
|
"id": element_id,
|
||||||
@@ -205,12 +217,15 @@ class InMemoryGraphRepository:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def delete_relationships(self, relationship_ids: List[str], sketch_id: str) -> int:
|
def delete_relationships(self, relationship_ids: List[str], sketch_id: str) -> int:
|
||||||
"""Delete relationships by their element IDs. Returns count deleted."""
|
"""Soft delete relationships by their element IDs. Returns count soft-deleted."""
|
||||||
deleted = 0
|
deleted = 0
|
||||||
|
deleted_at = datetime.now(timezone.utc).isoformat()
|
||||||
for rel_id in relationship_ids:
|
for rel_id in relationship_ids:
|
||||||
if rel_id in self._edges:
|
if rel_id in self._edges:
|
||||||
if self._edges[rel_id].get("sketch_id") == sketch_id:
|
if self._edges[rel_id].get("sketch_id") == sketch_id:
|
||||||
del self._edges[rel_id]
|
if self._edges[rel_id].get("deleted_at") is not None:
|
||||||
|
continue
|
||||||
|
self._edges[rel_id]["deleted_at"] = deleted_at
|
||||||
deleted += 1
|
deleted += 1
|
||||||
return deleted
|
return deleted
|
||||||
|
|
||||||
@@ -226,7 +241,7 @@ class InMemoryGraphRepository:
|
|||||||
node_ids = set()
|
node_ids = set()
|
||||||
|
|
||||||
for eid, data in self._nodes.items():
|
for eid, data in self._nodes.items():
|
||||||
if data.get("sketch_id") == sketch_id:
|
if data.get("sketch_id") == sketch_id and data.get("deleted_at") is None:
|
||||||
nodes.append(
|
nodes.append(
|
||||||
{
|
{
|
||||||
"id": eid,
|
"id": eid,
|
||||||
@@ -240,7 +255,7 @@ class InMemoryGraphRepository:
|
|||||||
|
|
||||||
edges = []
|
edges = []
|
||||||
for eid, data in self._edges.items():
|
for eid, data in self._edges.items():
|
||||||
if data.get("sketch_id") == sketch_id:
|
if data.get("sketch_id") == sketch_id and data.get("deleted_at") is None:
|
||||||
if data.get("source") in node_ids and data.get("target") in node_ids:
|
if data.get("source") in node_ids and data.get("target") in node_ids:
|
||||||
edges.append(
|
edges.append(
|
||||||
{
|
{
|
||||||
@@ -262,6 +277,8 @@ class InMemoryGraphRepository:
|
|||||||
center = self._nodes[node_id]
|
center = self._nodes[node_id]
|
||||||
if center.get("sketch_id") != sketch_id:
|
if center.get("sketch_id") != sketch_id:
|
||||||
return {"nodes": [], "edges": []}
|
return {"nodes": [], "edges": []}
|
||||||
|
if center.get("deleted_at") is not None:
|
||||||
|
return {"nodes": [], "edges": []}
|
||||||
|
|
||||||
nodes = {node_id: {"id": node_id, "data": center}}
|
nodes = {node_id: {"id": node_id, "data": center}}
|
||||||
edges = {}
|
edges = {}
|
||||||
@@ -269,6 +286,8 @@ class InMemoryGraphRepository:
|
|||||||
for eid, edge in self._edges.items():
|
for eid, edge in self._edges.items():
|
||||||
if edge.get("sketch_id") != sketch_id:
|
if edge.get("sketch_id") != sketch_id:
|
||||||
continue
|
continue
|
||||||
|
if edge.get("deleted_at") is not None:
|
||||||
|
continue
|
||||||
|
|
||||||
source = edge.get("source")
|
source = edge.get("source")
|
||||||
target = edge.get("target")
|
target = edge.get("target")
|
||||||
@@ -276,6 +295,8 @@ class InMemoryGraphRepository:
|
|||||||
if source == node_id:
|
if source == node_id:
|
||||||
# Outgoing edge
|
# Outgoing edge
|
||||||
if target in self._nodes:
|
if target in self._nodes:
|
||||||
|
if self._nodes[target].get("deleted_at") is not None:
|
||||||
|
continue
|
||||||
nodes[target] = {"id": target, "data": self._nodes[target]}
|
nodes[target] = {"id": target, "data": self._nodes[target]}
|
||||||
edges[eid] = {
|
edges[eid] = {
|
||||||
"id": eid,
|
"id": eid,
|
||||||
@@ -286,6 +307,8 @@ class InMemoryGraphRepository:
|
|||||||
elif target == node_id:
|
elif target == node_id:
|
||||||
# Incoming edge
|
# Incoming edge
|
||||||
if source in self._nodes:
|
if source in self._nodes:
|
||||||
|
if self._nodes[source].get("deleted_at") is not None:
|
||||||
|
continue
|
||||||
nodes[source] = {"id": source, "data": self._nodes[source]}
|
nodes[source] = {"id": source, "data": self._nodes[source]}
|
||||||
edges[eid] = {
|
edges[eid] = {
|
||||||
"id": eid,
|
"id": eid,
|
||||||
@@ -315,25 +338,30 @@ class InMemoryGraphRepository:
|
|||||||
if new_node_id and new_node_id in old_node_ids:
|
if new_node_id and new_node_id in old_node_ids:
|
||||||
target_id = new_node_id
|
target_id = new_node_id
|
||||||
self._nodes[target_id].update(new_node_data)
|
self._nodes[target_id].update(new_node_data)
|
||||||
|
self._nodes[target_id]["deleted_at"] = None
|
||||||
else:
|
else:
|
||||||
target_id = self._generate_element_id("node")
|
target_id = self._generate_element_id("node")
|
||||||
self._nodes[target_id] = {
|
self._nodes[target_id] = {
|
||||||
**new_node_data,
|
**new_node_data,
|
||||||
"sketch_id": sketch_id,
|
"sketch_id": sketch_id,
|
||||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"deleted_at": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Transfer relationships
|
# Transfer relationships
|
||||||
for eid, edge in list(self._edges.items()):
|
for eid, edge in list(self._edges.items()):
|
||||||
|
if edge.get("deleted_at") is not None:
|
||||||
|
continue
|
||||||
if edge.get("source") in old_node_ids and edge.get("source") != target_id:
|
if edge.get("source") in old_node_ids and edge.get("source") != target_id:
|
||||||
edge["source"] = target_id
|
edge["source"] = target_id
|
||||||
if edge.get("target") in old_node_ids and edge.get("target") != target_id:
|
if edge.get("target") in old_node_ids and edge.get("target") != target_id:
|
||||||
edge["target"] = target_id
|
edge["target"] = target_id
|
||||||
|
|
||||||
# Delete old nodes (except target)
|
# Soft delete old nodes (except target)
|
||||||
|
deleted_at = datetime.now(timezone.utc).isoformat()
|
||||||
for node_id in old_node_ids:
|
for node_id in old_node_ids:
|
||||||
if node_id != target_id and node_id in self._nodes:
|
if node_id != target_id and node_id in self._nodes:
|
||||||
del self._nodes[node_id]
|
self._nodes[node_id]["deleted_at"] = deleted_at
|
||||||
|
|
||||||
return target_id
|
return target_id
|
||||||
|
|
||||||
@@ -458,17 +486,21 @@ class InMemoryGraphRepository:
|
|||||||
"""Get total node count, optionally filtered by sketch_id."""
|
"""Get total node count, optionally filtered by sketch_id."""
|
||||||
if sketch_id:
|
if sketch_id:
|
||||||
return sum(
|
return sum(
|
||||||
1 for n in self._nodes.values() if n.get("sketch_id") == sketch_id
|
1
|
||||||
|
for n in self._nodes.values()
|
||||||
|
if n.get("sketch_id") == sketch_id and n.get("deleted_at") is None
|
||||||
)
|
)
|
||||||
return len(self._nodes)
|
return sum(1 for n in self._nodes.values() if n.get("deleted_at") is None)
|
||||||
|
|
||||||
def get_edge_count(self, sketch_id: Optional[str] = None) -> int:
|
def get_edge_count(self, sketch_id: Optional[str] = None) -> int:
|
||||||
"""Get total edge count, optionally filtered by sketch_id."""
|
"""Get total edge count, optionally filtered by sketch_id."""
|
||||||
if sketch_id:
|
if sketch_id:
|
||||||
return sum(
|
return sum(
|
||||||
1 for e in self._edges.values() if e.get("sketch_id") == sketch_id
|
1
|
||||||
|
for e in self._edges.values()
|
||||||
|
if e.get("sketch_id") == sketch_id and e.get("deleted_at") is None
|
||||||
)
|
)
|
||||||
return len(self._edges)
|
return sum(1 for e in self._edges.values() if e.get("deleted_at") is None)
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
"""Clear all data (useful between tests)."""
|
"""Clear all data (useful between tests)."""
|
||||||
|
|||||||
Reference in New Issue
Block a user