From a12f813cdee12ebe26085e1e395312029f199ce5 Mon Sep 17 00:00:00 2001
From: Leonardo <leomilho@gmail.com>
Date: Sat, 26 Oct 2024 20:11:52 +0200
Subject: [PATCH] feat(qb): allow multigraph for same entity logic

---
 .../data-access/store/querybuilderSlice.ts    |  4 +--
 .../querybuilder/model/graphology/utils.ts    |  6 ++--
 .../lib/querybuilder/panel/QueryBuilder.tsx   | 14 ++++-----
 .../query-utils/query2backend.spec.ts         | 31 ++++++++++++-------
 .../querybuilder/query-utils/query2backend.ts | 25 +++++++++------
 5 files changed, 46 insertions(+), 34 deletions(-)

diff --git a/libs/shared/lib/data-access/store/querybuilderSlice.ts b/libs/shared/lib/data-access/store/querybuilderSlice.ts
index ce77ca763..bfa417fda 100644
--- a/libs/shared/lib/data-access/store/querybuilderSlice.ts
+++ b/libs/shared/lib/data-access/store/querybuilderSlice.ts
@@ -1,6 +1,6 @@
 import { createSlice, PayloadAction } from '@reduxjs/toolkit';
 import type { RootState } from './store';
-import Graph from 'graphology';
+import Graph, { MultiGraph } from 'graphology';
 import { QueryMultiGraph, QueryMultiGraphology as QueryGraphology } from '../../querybuilder/model/graphology/utils';
 import { AllLayoutAlgorithms } from '../../graph-layout';
 import { QueryGraphEdgeHandle } from '../../querybuilder';
@@ -106,7 +106,7 @@ export const setQuerybuilderGraphology = (payload: QueryGraphology) => {
 /** Select the querybuilder nodes in serialized fromat */
 export const toQuerybuilderGraphology = (graph: QueryMultiGraph): QueryGraphology => {
   let ret = new QueryGraphology();
-  ret.import(Graph.from(graph).export());
+  ret.import(MultiGraph.from(graph).export());
   return ret;
 };
 
diff --git a/libs/shared/lib/querybuilder/model/graphology/utils.ts b/libs/shared/lib/querybuilder/model/graphology/utils.ts
index d05f1c764..5f6610db5 100644
--- a/libs/shared/lib/querybuilder/model/graphology/utils.ts
+++ b/libs/shared/lib/querybuilder/model/graphology/utils.ts
@@ -1,4 +1,4 @@
-import Graph from 'graphology';
+import Graph, { MultiGraph } from 'graphology';
 import { Attributes as GAttributes, Attributes, SerializedGraph } from 'graphology-types';
 import {
   EntityNodeAttributes,
@@ -32,7 +32,7 @@ export type AddEdge2GraphologyOptions = {
 
 export type QueryMultiGraph = SerializedGraph<QueryGraphNodes, QueryGraphEdges, GAttributes>;
 
-export class QueryMultiGraphology extends Graph<QueryGraphNodes, QueryGraphEdges, GAttributes> {
+export class QueryMultiGraphology extends MultiGraph<QueryGraphNodes, QueryGraphEdges, GAttributes> {
   public configureDefaults(attributes: QueryGraphNodes): QueryGraphNodes {
     const { type, name } = attributes;
     if (!type || !name) throw Error('type or name is not defined');
@@ -295,7 +295,7 @@ export function calcWidthHeightOfPill(attributes: Attributes): {
 }
 
 /** Interface for x and y position of node */
-export interface NodePosition extends XYPosition {}
+export interface NodePosition extends XYPosition { }
 
 /** Returns from-position of relation node */
 export function RelationPosToFromEntityPos(position: XYPosition): NodePosition {
diff --git a/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx
index ccfe0197a..10579c534 100644
--- a/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx
+++ b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx
@@ -357,14 +357,12 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
         isOnConnect.current = true;
         if (!connection.sourceHandle || !connection.targetHandle) throw new Error('Connection has no source or target');
 
-        if (!graphologyGraph.hasEdge(connection.source, connection.target)) {
-          graphologyGraph.addEdge(connection.source, connection.target, {
-            type: 'connection',
-            sourceHandleData: toHandleData(connection.sourceHandle),
-            targetHandleData: toHandleData(connection.targetHandle),
-          });
-          dispatch(setQuerybuilderGraphology(graphologyGraph));
-        }
+        graphologyGraph.addEdge(connection.source, connection.target, {
+          type: 'connection',
+          sourceHandleData: toHandleData(connection.sourceHandle),
+          targetHandleData: toHandleData(connection.targetHandle),
+        });
+        dispatch(setQuerybuilderGraphology(graphologyGraph));
       }
     },
     [graph],
diff --git a/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts
index da531d725..18fe1b7c7 100644
--- a/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts
+++ b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts
@@ -664,7 +664,7 @@ describe('QueryUtils Entity and Relations', () => {
     const graph = new QueryMultiGraphology();
 
     const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] });
-    const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] });
+    const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 2', attributes: [] });
 
     graph.addEdge2Graphology(e1, e2, { type: 'connection' });
     graph.addEdge2Graphology(e2, e1, { type: 'connection' });
@@ -677,17 +677,18 @@ describe('QueryUtils Entity and Relations', () => {
           node: {
             ID: 'e1',
             label: 'Airport 1',
-            relation: { direction: 'BOTH', node: { ID: 'e2', label: 'Airport 1' } },
-          },
-        },
-        {
-          ID: 'path_1',
-          node: {
-            ID: 'e2',
-            label: 'Airport 1',
-            relation: { direction: 'BOTH', node: { ID: 'e1', label: 'Airport 1' } },
+            relation: {
+              direction: 'BOTH',
+              node: {
+                ID: 'e2', label: 'Airport 2',
+                relation: {
+                  direction: 'BOTH',
+                  node: { ID: 'e1', label: 'Airport 1' }
+                },
+              }
+            },
           },
-        },
+        }
       ],
     };
     let ret = Query2BackendQuery('database', graph.export(), defaultSettings);
@@ -706,7 +707,13 @@ describe('QueryUtils Entity and Relations', () => {
       query: [
         {
           ID: 'path_0',
-          node: { ID: 'e1', label: 'Airport 1', relation: { direction: 'BOTH', node: { ID: 'e1', label: 'Airport 1' } } },
+          node: {
+            ID: 'e1', label: 'Airport 1',
+            relation: {
+              direction: 'BOTH',
+              node: { ID: 'e1', label: 'Airport 1' }
+            }
+          },
         },
       ],
     };
diff --git a/libs/shared/lib/querybuilder/query-utils/query2backend.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.ts
index 875783278..670d1c1f8 100644
--- a/libs/shared/lib/querybuilder/query-utils/query2backend.ts
+++ b/libs/shared/lib/querybuilder/query-utils/query2backend.ts
@@ -175,7 +175,7 @@ export function Query2BackendQuery(
   graph: QueryMultiGraph,
   settings: QueryBuilderSettings,
   ml: ML = mlDefaultState,
-  unionTypes: { [node_id: string]: QueryUnionType },
+  unionTypes: { [node_id: string]: QueryUnionType } = {},
 ): BackendQueryFormat {
   let query: BackendQueryFormat = {
     saveStateID: saveStateID,
@@ -205,16 +205,23 @@ export function Query2BackendQuery(
     const cycles = entities.map((entity, i) => {
       return allSimplePaths(graphologyQuery, entity.key, entity.key);
     });
-    cycles.forEach((cycles_inner, i) => {
-      cycles_inner.forEach((cycle, j) => {
+    for (let i = 0; i < cycles.length; i++) {
+      for (let j = 0; j < cycles[i].length; j++) {
+        const cycle = cycles[i][j];
         const origin = cycle[0];
         const target = cycle[cycle.length - 2];
-        const newOrigin = graphologyQuery.addNode(origin + 'cycle', graphologyQuery.getNodeAttributes(origin));
-        const edgeAttributes = graphologyQuery.getEdgeAttributes(target, origin);
-        graphologyQuery.dropEdge(target, origin);
-        graphologyQuery.addEdge(target, newOrigin, edgeAttributes);
-      });
-    });
+        const edges = graphologyQuery.edges(target, origin);
+        if (edges.length > 0) {
+          const edge = edges[edges.length - 1];
+          const newOrigin = graphologyQuery.addNode(origin + 'cycle' + edge, graphologyQuery.getNodeAttributes(origin));
+          const edgeAttributes = graphologyQuery.getEdgeAttributes(edge);
+          graphologyQuery.dropEdge(edge);
+          graphologyQuery.addEdge(target, newOrigin, edgeAttributes);
+        }
+        break; // only do one cycle
+      }
+      break; // only do one cycle
+    }
 
     return Query2BackendQuery(saveStateID, graphologyQuery.export(), settings, ml, unionTypes);
   }
-- 
GitLab