using FlowAPI.Application.Interfaces; using FlowAPI.Domain.Entities; using FlowAPI.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace FlowAPI.Infrastructure.Repositories { public class GraphRepository : GenericRepository, IGraphRepository { public GraphRepository(AppDbContext context) : base(context) { } public async Task> GetAllByUserIdAsync(Guid userId) { return await _dbSet .Where(g => g.UserId == userId) .Include(g => g.Nodes) .Include(g => g.Edges) .AsNoTracking() .ToListAsync(); } public async Task GetByIdWithDetailsAsync(Guid id) { return await _dbSet .Include(g => g.Nodes) .Include(g => g.Edges) .FirstOrDefaultAsync(g => g.Id == id); } public async Task ClearDetailsAsync(Guid id) { var graph = await _dbSet .Include(g => g.Nodes) .Include(g => g.Edges) .FirstOrDefaultAsync(g => g.Id == id); if (graph != null) { if (graph.Edges.Any()) { _context.Edges.RemoveRange(graph.Edges); } if (graph.Nodes.Any()) { _context.TaskNodes.RemoveRange(graph.Nodes); } await _context.SaveChangesAsync(); } } public async Task UpsertFullGraphAsync(Guid graphId, IEnumerable nodes, IEnumerable edges) { var graph = await _dbSet .Include(g => g.Nodes) .Include(g => g.Edges) .FirstOrDefaultAsync(g => g.Id == graphId); if (graph == null) return false; var existingNodes = graph.Nodes.ToList(); var incomingNodes = nodes.ToList(); var existingEdges = graph.Edges.ToList(); var incomingEdges = edges.ToList(); // 1. Identify nodes and edges to delete var nodesToDelete = existingNodes.Where(e => !incomingNodes.Any(i => i.Id == e.Id)).ToList(); var deletedNodeIds = nodesToDelete.Select(n => n.Id).ToHashSet(); var edgesToDelete = existingEdges .Where(e => !incomingEdges.Any(i => i.FromNodeId == e.FromNodeId && i.ToNodeId == e.ToNodeId && i.Condition == e.Condition) || deletedNodeIds.Contains(e.FromNodeId) || deletedNodeIds.Contains(e.ToNodeId)) .ToList(); // 2. Queue deletes (but don't save yet) if (edgesToDelete.Any()) { _context.Edges.RemoveRange(edgesToDelete); } if (nodesToDelete.Any()) { _context.TaskNodes.RemoveRange(nodesToDelete); } // 3. Update or Create Nodes foreach (var node in incomingNodes) { var existingNode = existingNodes.FirstOrDefault(e => e.Id == node.Id); if (existingNode != null) { existingNode.Label = node.Label; existingNode.PosX = node.PosX; existingNode.PosY = node.PosY; existingNode.Type = node.Type; existingNode.Decision = node.Decision; existingNode.State = node.State; existingNode.Description = node.Description; existingNode.Color = node.Color; existingNode.IsPinned = node.IsPinned; existingNode.Width = node.Width; existingNode.Height = node.Height; if (node.Data != null) { existingNode.Data = node.Data; } } else { node.GraphId = graphId; _context.TaskNodes.Add(node); } } var incomingNodeIds = incomingNodes.Select(n => n.Id).ToHashSet(); // 4. Update or Create Edges foreach (var edge in incomingEdges) { // Only create/update edge if both FromNode and ToNode exist in the incoming nodes list if (!incomingNodeIds.Contains(edge.FromNodeId) || !incomingNodeIds.Contains(edge.ToNodeId)) { continue; } var existingEdge = existingEdges.FirstOrDefault(e => e.FromNodeId == edge.FromNodeId && e.ToNodeId == edge.ToNodeId && e.Condition == edge.Condition); if (existingEdge != null) { existingEdge.Condition = edge.Condition; } else { edge.Id = Guid.NewGuid(); edge.GraphId = graphId; _context.Edges.Add(edge); } } // 5. Single atomic save to minimize lock duration await _context.SaveChangesAsync(); return true; } } }