From 906c5405f7c29d3ea0fef91dab68f7178ac19efa Mon Sep 17 00:00:00 2001 From: Leonardo Christino <leomilho@gmail.com> Date: Wed, 19 Jul 2023 23:00:13 +0200 Subject: [PATCH] feat(query): connect to new query format --- apps/web/.env.development | 2 +- apps/web/.env.production | 2 +- apps/web/.eslintignore | 7 +- apps/web/node.d.ts | 3 + apps/web/src/app/app.tsx | 4 +- .../navbar/AddDatabaseForm/index.tsx | 33 ++- libs/shared/.eslintignore | 7 +- libs/shared/lib/data-access/api/database.ts | 2 + libs/shared/lib/data-access/api/query.ts | 3 +- .../store/graphQueryResultSlice.ts | 18 +- .../data-access/store/querybuilderSlice.ts | 3 +- .../querybuilder/model/BackendQueryFormat.tsx | 187 ++++++++++------ .../querybuilder/model/graphology/model.ts | 6 + .../querybuilder/model/graphology/utils.ts | 56 ++++- libs/shared/lib/querybuilder/model/index.ts | 13 +- .../querybuilder/model/reactflow/handles.tsx | 7 +- .../lib/querybuilder/panel/querybuilder.tsx | 34 ++- .../panel/shemaquerybuilder.stories.tsx | 12 +- ...erybuilder-simple-disconnected.stories.tsx | 59 +++-- .../stories/querybuilder-simple.stories.tsx | 67 +++--- .../querybuilder-single-entity.stories.tsx | 9 +- ...erybuilder-single-relationship.stories.tsx | 20 +- .../pills/customFlowLines/connection.tsx | 16 +- .../pills/customFlowPills/edge-line.tsx | 2 +- .../entitypill/entitypill-full.stories.tsx | 3 - .../customFlowPills/entitypill/entitypill.tsx | 12 +- .../relation-full_reactflow.stories.tsx | 2 - .../relationpill/relationpill.tsx | 10 +- .../query-utils/query-utils.spec.ts | 16 ++ .../querybuilder/query-utils/query-utils.ts | 210 +++++++++++------- libs/shared/lib/schema/index.ts | 1 - libs/shared/lib/schema/panel/schema.tsx | 10 + libs/shared/lib/vis/nodelink/nodelinkvis.tsx | 1 - libs/shared/node.d.ts | 1 + libs/shared/tsconfig.json | 68 +++--- package.json | 3 +- pnpm-lock.yaml | 75 +++---- 37 files changed, 580 insertions(+), 404 deletions(-) create mode 100644 libs/shared/lib/querybuilder/query-utils/query-utils.spec.ts diff --git a/apps/web/.env.development b/apps/web/.env.development index f6b68d9b0..ddbadbf75 100644 --- a/apps/web/.env.development +++ b/apps/web/.env.development @@ -3,5 +3,5 @@ VITE_BACKEND_WSS_URL=ws://localhost:3001/ VITE_STAGING=dev VITE_SKIP_LOGIN=true VITE_BACKEND_USER=:3000 -VITE_BACKEND_QUERY=:8080 +VITE_BACKEND_QUERY=:3003 VITE_BACKEND_SCHEMA=:3002 \ No newline at end of file diff --git a/apps/web/.env.production b/apps/web/.env.production index cf5c3e8e8..4c548bc85 100644 --- a/apps/web/.env.production +++ b/apps/web/.env.production @@ -3,5 +3,5 @@ VITE_BACKEND_WSS_URL=ws://api.graphpolaris.com/socket/ VITE_STAGING=prod VITE_SKIP_LOGIN=false VITE_BACKEND_USER=/user -VITE_BACKEND_QUERY=:8080 +VITE_BACKEND_QUERY=/query VITE_BACKEND_SCHEMA=/schema \ No newline at end of file diff --git a/apps/web/.eslintignore b/apps/web/.eslintignore index 3421a7d28..ebd538ec9 100644 --- a/apps/web/.eslintignore +++ b/apps/web/.eslintignore @@ -1,3 +1,4 @@ -node_modules/* -node_modules/ -node_modules +node_modules/* +node_modules/ +node_modules +*.d.ts \ No newline at end of file diff --git a/apps/web/node.d.ts b/apps/web/node.d.ts index 34f3d984d..3d2ced5bc 100644 --- a/apps/web/node.d.ts +++ b/apps/web/node.d.ts @@ -2,6 +2,9 @@ interface ImportMeta { env: { VITE_BACKEND_URL: string; VITE_BACKEND_WSS_URL: string; + VITE_BACKEND_USER: string; + VITE_BACKEND_SCHEMA: string; + VITE_BACKEND_QUERY: string; VITE_STAGING: string; VITE_KEYCLOAK_URL: string; VITE_KEYCLOAK_REALM: string; diff --git a/apps/web/src/app/app.tsx b/apps/web/src/app/app.tsx index 97e18077a..9a83b2497 100644 --- a/apps/web/src/app/app.tsx +++ b/apps/web/src/app/app.tsx @@ -85,7 +85,9 @@ export function App(props: App) { if (query.edges.length === 0) { dispatch(resetGraphQueryResults()); - } else api_query.execute(Query2BackendQuery(session.currentDatabase, query)); + } else { + api_query.execute(Query2BackendQuery(session.currentDatabase, query)); + } } }, [queryHash]); diff --git a/apps/web/src/components/navbar/AddDatabaseForm/index.tsx b/apps/web/src/components/navbar/AddDatabaseForm/index.tsx index 15d8fe443..8df366cd4 100644 --- a/apps/web/src/components/navbar/AddDatabaseForm/index.tsx +++ b/apps/web/src/components/navbar/AddDatabaseForm/index.tsx @@ -11,7 +11,7 @@ import React, { useState, useEffect } from 'react'; import { TextField, Button, NativeSelect } from '@mui/material'; import styles from './add-database-form.module.scss'; -import { AddDatabaseRequest, DatabaseType, databaseNameMapping } from '@graphpolaris/shared/lib/data-access'; +import { AddDatabaseRequest, DatabaseType, databaseNameMapping, databaseProtocolMapping } from '@graphpolaris/shared/lib/data-access'; /** AddDatabaseFormProps is an interface containing the AuthViewModel. */ export interface AddDatabaseFormProps { @@ -44,11 +44,20 @@ export default function AddDatabaseForm(props: AddDatabaseFormProps) { // internal_database_name: 'TweedeKamer', // type: DatabaseType.ArangoDB, + // username: 'neo4j', + // password: 'oL3nNlebrx4le2A0zxaFVqAo3HAvodHxwEiI_7_2JxI', + // url: '635176c8.databases.neo4j.io', + // port: 7687, + // name: 'neo4j', + // internal_database_name: 'neo4j', + // type: DatabaseType.Neo4j, + username: 'neo4j', - password: 'oL3nNlebrx4le2A0zxaFVqAo3HAvodHxwEiI_7_2JxI', - url: '635176c8.databases.neo4j.io', + password: 'StrongPass2022', + url: 'localhost', port: 7687, name: 'neo4j', + protocol: 'neo4j://', internal_database_name: 'neo4j', type: DatabaseType.Neo4j, }); @@ -139,6 +148,24 @@ export default function AddDatabaseForm(props: AddDatabaseFormProps) { ))} </NativeSelect> </div> + <div className={styles.loginContainer}> + <NativeSelect + className={styles.passLabel} + value={state.protocol} + onChange={(event) => { + setState({ + ...state, + protocol: event.currentTarget.value, + }); + }} + > + {databaseProtocolMapping.map((protocol) => ( + <option value={protocol} key={protocol}> + {protocol} + </option> + ))} + </NativeSelect> + </div> <div className={styles.loginContainerRow}> <TextField className={styles.hostLabel} diff --git a/libs/shared/.eslintignore b/libs/shared/.eslintignore index 3421a7d28..86439d7da 100644 --- a/libs/shared/.eslintignore +++ b/libs/shared/.eslintignore @@ -1,3 +1,4 @@ -node_modules/* -node_modules/ -node_modules +node_modules/* +node_modules/ +node_modules +*.d.ts diff --git a/libs/shared/lib/data-access/api/database.ts b/libs/shared/lib/data-access/api/database.ts index bb7390c02..9c3abbe85 100644 --- a/libs/shared/lib/data-access/api/database.ts +++ b/libs/shared/lib/data-access/api/database.ts @@ -9,12 +9,14 @@ export enum DatabaseType { } export const databaseNameMapping: string[] = ['arangodb', 'neo4j']; +export const databaseProtocolMapping: string[] = ['neo4j://', 'neo4j+s://']; export type AddDatabaseRequest = { name: string; internal_database_name: string; url: string; port: number; + protocol: string; username: string; password: string; type: DatabaseType; // Database type. 0 = ArangoDB, 1 = Neo4j diff --git a/libs/shared/lib/data-access/api/query.ts b/libs/shared/lib/data-access/api/query.ts index 8f5c9da66..5fd651fac 100644 --- a/libs/shared/lib/data-access/api/query.ts +++ b/libs/shared/lib/data-access/api/query.ts @@ -7,9 +7,10 @@ export const useQueryAPI = () => { const cache = useSessionCache(); const { accessToken } = useAuthorizationCache(); const domain = import.meta.env.VITE_BACKEND_URL; + const query_url = import.meta.env.VITE_BACKEND_QUERY; async function execute(query: BackendQueryFormat) { - const response = await fetch(`${domain}/query/execute/`, { + const response = await fetch(`${domain}${query_url}/execute/`, { method: 'POST', credentials: 'same-origin', headers: new Headers({ diff --git a/libs/shared/lib/data-access/store/graphQueryResultSlice.ts b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts index e6480df6b..1f8e0562f 100644 --- a/libs/shared/lib/data-access/store/graphQueryResultSlice.ts +++ b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts @@ -1,6 +1,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from './store'; +export interface GraphQueryResultFromBackendPayload { + payload: GraphQueryResultFromBackend; + type: string; +} + export interface GraphQueryResultFromBackend { nodes: { id: string; @@ -58,13 +63,16 @@ export const graphQueryResultSlice = createSlice({ // `createSlice` will infer the state type from the `initialState` argument initialState, reducers: { - assignNewGraphQueryResult: (state, action: PayloadAction<GraphQueryResultFromBackend>) => { + assignNewGraphQueryResult: (state, action: PayloadAction<GraphQueryResultFromBackendPayload>) => { + const payload = action.payload.payload; + console.log('!!!assignNewGraphQueryResult', action.payload.payload); + // Maybe do some data quality checking and parsing // ... // Collect all the different nodetypes in the result const nodeTypes: string[] = []; - action.payload.nodes = action.payload.nodes.map((node) => { + payload.nodes = payload.nodes.map((node) => { // TODO FIXME!! Note: works only for arangodb let nodeType = node.id.split('/')[0]; if (node.attributes?.labels?.length > 0) { @@ -75,7 +83,7 @@ export const graphQueryResultSlice = createSlice({ return node; }); - action.payload.edges = action.payload.edges.map((edge) => { + payload.edges = payload.edges.map((edge) => { let edgeType = edge.id.split('/')[0]; if (!edge.id.includes('/')) { edgeType = edge.attributes.Type as string; @@ -85,8 +93,8 @@ export const graphQueryResultSlice = createSlice({ }); // Assign new state - state.nodes = action.payload.nodes as Node[]; - state.edges = action.payload.edges; + state.nodes = payload.nodes as Node[]; + state.edges = payload.edges; state.nodeTypes = nodeTypes; }, resetGraphQueryResults: (state) => { diff --git a/libs/shared/lib/data-access/store/querybuilderSlice.ts b/libs/shared/lib/data-access/store/querybuilderSlice.ts index ad4501d09..724dafd97 100644 --- a/libs/shared/lib/data-access/store/querybuilderSlice.ts +++ b/libs/shared/lib/data-access/store/querybuilderSlice.ts @@ -2,8 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from './store'; import { MultiGraph } from 'graphology'; import { Attributes, SerializedGraph } from 'graphology-types'; -import { QueryMultiGraphology, QueryMultiGraph } from '@graphpolaris/shared/lib/querybuilder/model/graphology/utils'; -import { json } from 'd3'; +import { QueryMultiGraphology } from '../../querybuilder'; // Define the initial state using that type export const initialState = { diff --git a/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx b/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx index a4c82df55..98608922b 100644 --- a/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx +++ b/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx @@ -16,8 +16,8 @@ export interface BackendQueryResultFormat { }; entities: Entity[]; relations: Relation[]; - groupBys: GroupBy[]; - machineLearning: MachineLearning[]; + // groupBys: GroupBy[]; + // machineLearning: MachineLearning[]; limit: number; // modifiers: ModifierStruct[]; // prefix: string; @@ -26,20 +26,65 @@ export interface BackendQueryResultFormat { /** JSON query format used to send a query to the backend. */ export interface BackendQueryFormat { databaseName: string; - return: { - entities: number[]; - relations: number[]; - groupBys: number[]; - }; - entities: Entity[]; - relations: Relation[]; - groupBys: GroupBy[]; - machineLearning: MachineLearning[]; limit: number; + return: string[]; + query: QueryStruct[]; + // entities: Entity[]; + // relations: Relation[]; + // groupBys: GroupBy[]; + // machineLearning: MachineLearning[]; // modifiers: ModifierStruct[]; // prefix: string; } +/** Interface for an entity in the JSON for the query. */ +export interface QueryStruct { + ID: string; + node: NodeStruct; +} + +export interface NodeStruct { + label?: string; + ID?: string; + logic?: LogicStruct[]; + relation?: RelationStruct; + subQuery?: QueryStruct; + export?: ExportNodeStruct[]; +} + +export interface ExportNodeStruct { + ID: number; + attribute: string; +} + +export interface LogicStruct { + ID: number; + attribute: string; + operation: LogicOperationType; +} + +export interface FilterStruct { + ID: number; + attribute: string; + operation: FilterOperationType; +} + +export interface RelationStruct { + ID?: string; + label?: string; + depth?: QuerySearchDepthStruct; + direction: 'TO' | 'FROM'; + node?: NodeStruct; +} + +export interface QuerySearchDepthStruct { + min: number; + max: number; +} + +export type FilterOperationType = 'AND' | 'OR'; +export type LogicOperationType = 'AND' | 'OR'; + /** Interface for an entity in the JSON for the query. */ export interface Entity { name: string; @@ -59,21 +104,33 @@ export interface Relation { constraints: Constraint[]; } -/** JSON query format used to send a query to the backend. */ -export interface TranslatedJSONQuery { - return: { - entities: number[]; - relations: number[]; - groupBys: number[]; - }; - entities: Entity[]; - relations: Relation[]; - groupBys: GroupBy[]; - machineLearning: MachineLearning[]; - limit: number; -} - -//////////////////// +// /** Interface for an relation in the JSON for the query. */ +// export interface Relation { +// name: string; +// ID: number; +// fromType: string; +// fromID: number; +// toType: string; +// toID: number; +// depth: { min: number; max: number }; +// constraints: Constraint[]; +// } + +// /** JSON query format used to send a query to the backend. */ +// export interface TranslatedJSONQuery { +// return: { +// entities: number[]; +// relations: number[]; +// groupBys: number[]; +// }; +// entities: Entity[]; +// relations: Relation[]; +// groupBys: GroupBy[]; +// machineLearning: MachineLearning[]; +// limit: number; +// } + +// //////////////////// /** * Constraint datatypes created from the attributes of a relation or entity. @@ -90,42 +147,42 @@ export interface Constraint { value: string; } -/** Interface for a function in the JSON for the query. */ -export interface GroupBy { - ID: number; - - groupType: string; - groupID: number[]; - groupAttribute: string; - - byType: string; - byID: number[]; - byAttribute: string; - - appliedModifier: string; - relationID: number; - constraints: Constraint[]; -} - -/** Interface for Machine Learning algorithm */ -export interface MachineLearning { - ID?: number; - queuename: string; - parameters: string[]; -} - -/** Interface for what the JSON needs for link predicition */ -export interface LinkPrediction { - queuename: string; - parameters: { - key: string; - value: string; - }[]; -} - -export interface ModifierStruct { - type: string; - selectedType: string; - selectedTypeID: number; - attributeIndex: number; -} +// /** Interface for a function in the JSON for the query. */ +// export interface GroupBy { +// ID: number; + +// groupType: string; +// groupID: number[]; +// groupAttribute: string; + +// byType: string; +// byID: number[]; +// byAttribute: string; + +// appliedModifier: string; +// relationID: number; +// constraints: Constraint[]; +// } + +// /** Interface for Machine Learning algorithm */ +// export interface MachineLearning { +// ID?: number; +// queuename: string; +// parameters: string[]; +// } + +// /** Interface for what the JSON needs for link predicition */ +// export interface LinkPrediction { +// queuename: string; +// parameters: { +// key: string; +// value: string; +// }[]; +// } + +// export interface ModifierStruct { +// type: string; +// selectedType: string; +// selectedTypeID: number; +// attributeIndex: number; +// } diff --git a/libs/shared/lib/querybuilder/model/graphology/model.ts b/libs/shared/lib/querybuilder/model/graphology/model.ts index 3d3060fa4..6fe37af6f 100644 --- a/libs/shared/lib/querybuilder/model/graphology/model.ts +++ b/libs/shared/lib/querybuilder/model/graphology/model.ts @@ -9,6 +9,7 @@ import { GeneralDescription, InputNodeType } from '../logic/general'; // } export interface NodeAttribute extends SchemaAttribute { + handleId: string; // nodeCount: number; // summedNullAmount: number; // connectedRatio: number; @@ -16,6 +17,7 @@ export interface NodeAttribute extends SchemaAttribute { } export type NodeDefaults = { + id?: string; type: string; width?: number; height?: number; @@ -25,6 +27,8 @@ export type NodeDefaults = { /** Interface for the data in an entity node. */ export interface EntityData { name: string; + leftRelationHandleId?: string; + rightRelationHandleId?: string; } /** Interface for the data in an relation node. */ @@ -32,6 +36,8 @@ export interface RelationData { name: string; collection: string; depth: { min: number; max: number }; + leftEntityHandleId?: string; + rightEntityHandleId?: string; } export interface LogicData { diff --git a/libs/shared/lib/querybuilder/model/graphology/utils.ts b/libs/shared/lib/querybuilder/model/graphology/utils.ts index a3d49d2b4..027d1661b 100644 --- a/libs/shared/lib/querybuilder/model/graphology/utils.ts +++ b/libs/shared/lib/querybuilder/model/graphology/utils.ts @@ -1,9 +1,10 @@ import { setQuerybuilderNodes, store } from '@graphpolaris/shared/lib/data-access/store'; import Graph, { MultiGraph } from 'graphology'; import { Attributes as GAttributes, Attributes, SerializedGraph } from 'graphology-types'; -import { QueryGraphNodes } from './model'; +import { EntityNodeAttributes, LogicNodeAttributes, QueryGraphNodes, RelationNodeAttributes } from './model'; import { XYPosition } from 'reactflow'; -import { NodeAttribute, SchemaGraphology } from '@graphpolaris/shared/lib/schema'; +import { Handles, QueryElementTypes } from '../reactflow'; +import { getHandleId } from '..'; /** monospace fontsize table */ const widthPerFontsize = { @@ -15,13 +16,10 @@ const widthPerFontsize = { export type QueryMultiGraph = SerializedGraph<QueryGraphNodes, GAttributes, GAttributes>; export class QueryMultiGraphology extends MultiGraph<QueryGraphNodes, GAttributes, GAttributes> { - public addPill2Graphology( - attributes: QueryGraphNodes, - id: string = (Date.now() + Math.floor(Math.random() * 1000)).toString() - ): string | null { - const { type, name, handles } = attributes; - if (!type || !name) return null; - if (!handles) attributes.handles = []; + public addPill2Graphology(attributes: QueryGraphNodes): QueryGraphNodes { + const { type, name } = attributes; + if (!type || !name) throw Error('type or name is not defined'); + if (!attributes.id) attributes.id = 'id_' + (Date.now() + Math.floor(Math.random() * 1000)).toString(); let { x, y } = attributes; // Check if x and y are present, otherwise set them to 0 @@ -31,13 +29,49 @@ export class QueryMultiGraphology extends MultiGraph<QueryGraphNodes, GAttribute // Get the width and height of a node const { width, height } = calcWidthHeightOfPill(attributes); + // fix handles + if (type === QueryElementTypes.Entity) { + (attributes as EntityNodeAttributes).leftRelationHandleId = getHandleId(attributes.id, name, type, Handles.EntityLeft, ''); + (attributes as EntityNodeAttributes).rightRelationHandleId = getHandleId(attributes.id, name, type, Handles.EntityRight, ''); + } else if (type === QueryElementTypes.Relation) { + (attributes as RelationNodeAttributes).leftEntityHandleId = getHandleId(attributes.id, name, type, Handles.RelationLeft, ''); + (attributes as RelationNodeAttributes).rightEntityHandleId = getHandleId(attributes.id, name, type, Handles.RelationRight, ''); + } else if (type === QueryElementTypes.Logic) { + } + // Add a node to the graphology object - const nodeId = this.addNode(id, { ...attributes, x, y, width, height }); + const nodeId = this.addNode(attributes.id, { ...attributes, x, y, width, height }); + + // Set the new nodes in the query builder slice TODO: maybe remove for efficiency + store.dispatch(setQuerybuilderNodes(this.export())); + + return attributes; + } + + public addEdge2Graphology(source: QueryGraphNodes, target: QueryGraphNodes, attributes: GAttributes): string | null { + // fix handles + attributes.sourceHandle = getHandleId( + source.id + '_TO_' + target.id, + source.name, + source.type, + source.type === QueryElementTypes.Entity ? Handles.EntityRight : Handles.RelationRight, + '' + ); + attributes.targetHandle = getHandleId( + source.id + '_TO_' + target.id, + target.name, + target.type, + target.type === QueryElementTypes.Entity ? Handles.EntityLeft : Handles.RelationLeft, + '' + ); + + // Add an edge to the graphology object + const edgeId = this.addEdge(source.id, target.id, attributes); // Set the new nodes in the query builder slice TODO: maybe remove for efficiency store.dispatch(setQuerybuilderNodes(this.export())); - return nodeId; + return edgeId; } } diff --git a/libs/shared/lib/querybuilder/model/index.ts b/libs/shared/lib/querybuilder/model/index.ts index 835ff79fd..d9af8a031 100644 --- a/libs/shared/lib/querybuilder/model/index.ts +++ b/libs/shared/lib/querybuilder/model/index.ts @@ -8,6 +8,7 @@ export * from './reactflow'; type ExtraProps = { extra?: string; separator?: string }; export function getHandleId( + nodeId: string, nodeName: string, nodeType: string, attributeName: string, @@ -16,18 +17,18 @@ export function getHandleId( ): string { if (!extra) extra = ''; if (!separator) separator = '__'; - return [nodeType, nodeName, attributeName, attributeType, extra].join(separator); + return [nodeId, nodeType, nodeName, attributeName, attributeType, extra].join(separator); } export function getHandleIdFromGraphology(node: QueryGraphNodes, attribute: NodeAttribute, options: ExtraProps = {}): string { - return getHandleId(node.name, node.type, attribute.name, attribute.type, options); + return getHandleId(node.id || '', node.name, node.type, attribute.name, attribute.type, options); } export function getHandleIdFromReactflow(node: SchemaReactflowNode, attribute: NodeAttribute, options: ExtraProps = {}): string { - return getHandleId(node.data.name, node.type, attribute.name, attribute.type, options); + return getHandleId(node.id, node.data.name, node.type, attribute.name, attribute.type, options); } export function fromHandleId( handleId: string, separator: string = '__' -): { nodeName: string; nodeType: string; attributeName: string; attributeType: string } { - const [nodeType, nodeName, attributeName, attributeType] = handleId.split(separator); - return { nodeType, nodeName, attributeName, attributeType }; +): { nodeId: string; nodeName: string; nodeType: string; attributeName: string; attributeType: string } { + const [nodeId, nodeType, nodeName, attributeName, attributeType] = handleId.split(separator); + return { nodeId, nodeType, nodeName, attributeName, attributeType }; } diff --git a/libs/shared/lib/querybuilder/model/reactflow/handles.tsx b/libs/shared/lib/querybuilder/model/reactflow/handles.tsx index d37ea317e..cb55e4d7d 100644 --- a/libs/shared/lib/querybuilder/model/reactflow/handles.tsx +++ b/libs/shared/lib/querybuilder/model/reactflow/handles.tsx @@ -13,10 +13,11 @@ import { SchemaReactflowNode, QueryElementTypes } from './model'; /** Links need handles to what they are connected to (and which side) */ export enum Handles { - RelationLeft = 'leftEntityHandle', //target - RelationRight = 'rightEntityHandle', //target + RelationLeft = 'relationLeftHandle', //target + RelationRight = 'relationRightHandle', //source ToAttribute = 'attributesHandle', //target - ToRelation = 'relationsHandle', //source + EntityLeft = 'entityLeftHandle', //source + EntityRight = 'entityRightHandle', //target OnAttribute = 'onAttributeHandle', //source ReceiveFunction = 'receiveFunctionHandle', //target FunctionBase = 'functionHandle_', // + name from FunctionTypes args //source diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.tsx index 98d6cf335..ececf9132 100644 --- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/querybuilder.tsx @@ -217,10 +217,13 @@ export const QueryBuilderInner: React.FC = () => { if (data.length == 0 || !reactFlow) return; const dragData = JSON.parse(data); + console.log(reactFlowWrapper); + + const bounds = reactFlowWrapper?.current?.getBoundingClientRect() || { x: 0, y: 0 }; const position = reactFlow.project({ //TODO: this position should be centre of entity, rather than topleft - x: event.clientX, - y: event.clientY, + x: event.clientX - bounds.x, + y: event.clientY - bounds.y, }); switch (dragData.type) { @@ -231,38 +234,35 @@ export const QueryBuilderInner: React.FC = () => { y: position.y, name: dragData.name, }); + dispatch(setQuerybuilderNodes(graphologyGraph.export())); break; // Creates a relation element and will also create the 2 related entities together with the connections case QueryElementTypes.Relation: - const relationId = graphologyGraph.addPill2Graphology({ + const relation = graphologyGraph.addPill2Graphology({ type: QueryElementTypes.Relation, x: position.x, y: position.y, depth: { min: 0, max: 1 }, // name: dragData.name, - name: dragData.collection, // TODO leave collection or use name? + name: dragData.collection, collection: dragData.collection, }); - const leftEntityId = graphologyGraph.addPill2Graphology({ + const leftEntity = graphologyGraph.addPill2Graphology({ type: QueryElementTypes.Entity, ...RelationPosToFromEntityPos(position), name: dragData.from, }); - const rightEntityId = graphologyGraph.addPill2Graphology({ + const rightEntity = graphologyGraph.addPill2Graphology({ type: QueryElementTypes.Entity, ...RelationPosToToEntityPos(position), name: dragData.to, }); - graphologyGraph.addEdge(leftEntityId, relationId, { + graphologyGraph.addEdge2Graphology(leftEntity, relation, { type: 'connection', - sourceHandle: Handles.ToRelation, - targetHandle: Handles.RelationLeft, }); - graphologyGraph.addEdge(rightEntityId, relationId, { + graphologyGraph.addEdge2Graphology(relation, rightEntity, { type: 'connection', - sourceHandle: Handles.ToRelation, - targetHandle: Handles.RelationRight, }); if (config.autoSendQueries) { @@ -361,17 +361,15 @@ export const QueryBuilderInner: React.FC = () => { if (!firstLeftLogicInput) return; // logicAttributes[0].handles = [connectingNodeId.current.handleId]; - const logicNode = { + const logicNode = graphologyGraph.addPill2Graphology({ name: value.name, type: QueryElementTypes.Logic, x: position.x, y: position.y, logic: logic, - }; - const logicId = graphologyGraph.addPill2Graphology(logicNode); - if (!logicId) return; + }); - graphologyGraph.addEdge(params.nodeId, logicId, { + graphologyGraph.addEdge(params.nodeId, logicNode.id, { type: 'connection', sourceHandle: params.handleId, // newAttribute data? targetHandle: getHandleId(logicNode.name, logicNode.type, firstLeftLogicInput.name, firstLeftLogicInput.type.join(''), { @@ -616,7 +614,7 @@ export const QueryBuilder = () => { gap: '1rem', }} > - <QueryBuilderPills /> + {/* <QueryBuilderPills /> */} <ReactFlowProvider> <QueryBuilderInner /> </ReactFlowProvider> diff --git a/libs/shared/lib/querybuilder/panel/shemaquerybuilder.stories.tsx b/libs/shared/lib/querybuilder/panel/shemaquerybuilder.stories.tsx index 6470560bd..f0ee5ed0d 100644 --- a/libs/shared/lib/querybuilder/panel/shemaquerybuilder.stories.tsx +++ b/libs/shared/lib/querybuilder/panel/shemaquerybuilder.stories.tsx @@ -1,22 +1,12 @@ import React from 'react'; import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; -import { - colorPaletteConfigSlice, - graphQueryResultSlice, - querybuilderSlice, - schemaSlice, - setQuerybuilderNodes, - setSchema, - store, -} from '@graphpolaris/shared/lib/data-access/store'; +import { setQuerybuilderNodes, setSchema, store } from '@graphpolaris/shared/lib/data-access/store'; import { GraphPolarisThemeProvider } from '@graphpolaris/shared/lib/data-access/theme'; import { SchemaUtils } from '@graphpolaris/shared/lib/schema/schema-utils'; import { Schema } from '@graphpolaris/shared/lib/schema/panel'; import { movieSchemaRaw } from '@graphpolaris/shared/lib/mock-data'; import { QueryBuilder } from '@graphpolaris/shared/lib/querybuilder'; -import { configureStore } from '@reduxjs/toolkit'; -import { configSlice } from '@graphpolaris/shared/lib/data-access/store/configSlice'; import { QueryMultiGraphology } from '@graphpolaris/shared/lib/querybuilder/model/graphology/utils'; const SchemaAndQueryBuilder = () => { diff --git a/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple-disconnected.stories.tsx b/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple-disconnected.stories.tsx index d69660dd4..c5498bae2 100644 --- a/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple-disconnected.stories.tsx +++ b/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple-disconnected.stories.tsx @@ -11,7 +11,7 @@ import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; import QueryBuilderInner from '../querybuilder'; -import { Handles, NodeAttribute, QueryMultiGraphology } from '../../model'; +import { Handles, NodeAttribute, QueryElementTypes, QueryMultiGraphology, getHandleId } from '../../model'; import { SchemaUtils } from '../../../schema/schema-utils'; import { ReactFlowProvider } from 'reactflow'; @@ -53,39 +53,34 @@ export const SimpleDisconnected = { store.dispatch(setSchema(schema.export())); - graph.addPill2Graphology( - { - type: 'entity', - x: 100, - y: 100, - name: 'Entity Pill', - attributes: schema.getNodeAttribute('entity', 'attributes') as NodeAttribute[], - }, - '0' - ); - graph.addPill2Graphology( - { - type: 'entity', - x: 200, - y: 200, - name: 'Entity Pill 2', - attributes: schema.getNodeAttribute('entity', 'attributes') as NodeAttribute[], - }, - '10' - ); + const entity1 = graph.addPill2Graphology({ + id: '0', + type: 'entity', + x: 100, + y: 100, + name: 'Airport 1', + attributes: schema.getNodeAttribute('entity', 'attributes') as NodeAttribute[], + }); + const entity2 = graph.addPill2Graphology({ + id: '10', + type: 'entity', + x: 200, + y: 200, + name: 'Airport 2', + attributes: schema.getNodeAttribute('entity', 'attributes') as NodeAttribute[], + }); // graph.addNode('0', { type: 'entity', x: 100, y: 100, name: 'Entity Pill' }); - graph.addPill2Graphology( - { - type: 'relation', - x: 140, - y: 140, - name: 'Relation Pill', - depth: { min: 0, max: 1 }, - attributes: schema.getEdgeAttribute('entity:entity_entityentity', 'attributes'), - }, - '1' - ); + const relation1 = graph.addPill2Graphology({ + id: '1', + type: 'relation', + x: 140, + y: 140, + name: 'Flight between airports', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + attributes: schema.getEdgeAttribute('entity:entity_entityentity', 'attributes'), + }); store.dispatch(setQuerybuilderNodes(graph.export())); return ( diff --git a/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple.stories.tsx b/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple.stories.tsx index 987c59408..955a41623 100644 --- a/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple.stories.tsx +++ b/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple.stories.tsx @@ -11,7 +11,7 @@ import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; import QueryBuilder from '../querybuilder'; -import { Handles, NodeAttribute, QueryMultiGraphology } from '../../model'; +import { Handles, NodeAttribute, QueryElementTypes, QueryMultiGraphology, getHandleId } from '../../model'; import { SchemaUtils } from '../../../schema/schema-utils'; const Component: Meta<typeof QueryBuilder> = { @@ -67,39 +67,34 @@ export const Simple = { store.dispatch(setSchema(schema.export())); - graph.addPill2Graphology( - { - type: 'entity', - x: 100, - y: 100, - name: 'Airport 1', - attributes: schema.getNodeAttribute('entity', 'attributes') as NodeAttribute[], - }, - '0' - ); - graph.addPill2Graphology( - { - type: 'entity', - x: 200, - y: 200, - name: 'Airport 2', - attributes: schema.getNodeAttribute('entity', 'attributes') as NodeAttribute[], - }, - '10' - ); + const entity1 = graph.addPill2Graphology({ + id: '0', + type: 'entity', + x: 100, + y: 100, + name: 'Airport 1', + attributes: schema.getNodeAttribute('entity', 'attributes') as NodeAttribute[], + }); + const entity2 = graph.addPill2Graphology({ + id: '10', + type: 'entity', + x: 200, + y: 200, + name: 'Airport 2', + attributes: schema.getNodeAttribute('entity', 'attributes') as NodeAttribute[], + }); // graph.addNode('0', { type: 'entity', x: 100, y: 100, name: 'Entity Pill' }); - graph.addPill2Graphology( - { - type: 'relation', - x: 140, - y: 140, - name: 'Flight between airports', - depth: { min: 0, max: 1 }, - attributes: schema.getEdgeAttribute('entity:entity_entityentity', 'attributes'), - }, - '1' - ); + const relation1 = graph.addPill2Graphology({ + id: '1', + type: 'relation', + x: 140, + y: 140, + name: 'Flight between airports', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + attributes: schema.getEdgeAttribute('entity:entity_entityentity', 'attributes'), + }); // addPill2Graphology( // '2', // { @@ -141,15 +136,11 @@ export const Simple = { // }, // graph // ); - graph.addEdge('0', '1', { + graph.addEdge2Graphology(entity1, relation1, { type: 'connection', - sourceHandle: Handles.ToRelation, - targetHandle: Handles.RelationLeft, }); - graph.addEdge('10', '1', { + graph.addEdge2Graphology(relation1, entity2, { type: 'connection', - sourceHandle: Handles.ToRelation, - targetHandle: Handles.RelationRight, }); // console.log(graph.getNodeAttributes('2')); // graph.addEdge('2', '1', { type: 'attribute_connection' }); diff --git a/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-entity.stories.tsx b/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-entity.stories.tsx index 09397f5c3..a5ca1fc7f 100644 --- a/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-entity.stories.tsx +++ b/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-entity.stories.tsx @@ -11,7 +11,7 @@ import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; import QueryBuilder from '../querybuilder'; -import { QueryMultiGraphology } from '../../model'; +import { QueryElementTypes, QueryMultiGraphology, getHandleId } from '../../model'; const Component: Meta<typeof QueryBuilder> = { component: QueryBuilder, @@ -38,7 +38,12 @@ export const SingleEntity = { args: {}, play: async () => { const graph = new QueryMultiGraphology(); - graph.addPill2Graphology({ type: 'entity', x: 100, y: 100, name: 'Entity Pill' }, '0'); + graph.addPill2Graphology({ + type: 'entity', + x: 100, + y: 100, + name: 'Entity Pill', + }); store.dispatch(setQuerybuilderNodes(graph.export())); }, }; diff --git a/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-relationship.stories.tsx b/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-relationship.stories.tsx index 26d064f9d..017a42f10 100644 --- a/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-relationship.stories.tsx +++ b/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-relationship.stories.tsx @@ -11,7 +11,7 @@ import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; import QueryBuilder from '../querybuilder'; -import { QueryMultiGraphology } from '../../model'; +import { QueryElementTypes, QueryMultiGraphology, getHandleId } from '../../model'; const Component: Meta<typeof QueryBuilder> = { component: QueryBuilder, @@ -38,16 +38,14 @@ export const SingleRelationship = { args: {}, play: async () => { const graph = new QueryMultiGraphology(); - graph.addPill2Graphology( - { - type: 'relation', - x: 140, - y: 140, - name: 'Relation Pill', - depth: { min: 0, max: 1 }, - }, - '0' - ); + graph.addPill2Graphology({ + type: 'relation', + x: 140, + y: 140, + name: 'Relation Pill', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); store.dispatch(setQuerybuilderNodes(graph.export())); }, }; diff --git a/libs/shared/lib/querybuilder/pills/customFlowLines/connection.tsx b/libs/shared/lib/querybuilder/pills/customFlowLines/connection.tsx index 50625840f..ef15a7b00 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowLines/connection.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowLines/connection.tsx @@ -1,26 +1,12 @@ import React from 'react'; import { EdgeProps, getSmoothStepPath, Position } from 'reactflow'; -import { handles } from '../../graph/reactflow/pillHandles'; /** * A custom query element edge line component. * @param {EdgeProps} param0 The coordinates for the start and end point, the id and the style. */ // export const EntityRFPill = React.memo(({ data }: { data: any }) => { -export function ConnectionLine({ - id, - sourceX, - sourceY, - targetX, - targetY, - style, - sourceHandleId, - targetHandleId, - source, - target, - sourcePosition, - targetPosition, -}: EdgeProps) { +export function ConnectionLine({ id, sourceX, sourceY, targetX, targetY, style, sourcePosition, targetPosition }: EdgeProps) { //Centering the line // sourceY -= 3; // targetY -= 3; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/edge-line.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/edge-line.tsx index 0b119180b..ffb927095 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/edge-line.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/edge-line.tsx @@ -10,7 +10,7 @@ * See testing plan for more details.*/ import React from 'react'; import { EdgeProps, getSmoothStepPath, Position } from 'reactflow'; -import { Handles } from '../../graph-reactflow/handles'; +import { Handles } from '../../model'; /** * A custom query element edge line component. diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx index 23990d1ff..2e408ee62 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx @@ -4,10 +4,7 @@ import { GraphPolarisThemeProvider } from '@graphpolaris/shared/lib/data-access/ import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; -import { MultiGraph } from 'graphology'; import { QueryBuilder } from '../../../panel'; -import { QueryGraph } from '../../../model/graphology/model'; -import { circular } from 'graphology-layout'; import { QueryMultiGraphology } from '@graphpolaris/shared/lib/querybuilder/model/graphology/utils'; const Component: Meta<typeof QueryBuilder> = { diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx index fbac410a0..960834e2d 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx @@ -3,7 +3,7 @@ import { useTheme } from '@mui/material'; import React, { MouseEventHandler, useEffect } from 'react'; import { ReactFlow, Handle, Position, getConnectedEdges } from 'reactflow'; import styles from './entitypill.module.scss'; -import { SchemaReactflowEntityNode, Handles, getHandleId, getHandleIdFromReactflow } from '../../../model'; +import { SchemaReactflowEntityNode, Handles, getHandleIdFromReactflow } from '../../../model'; import { SchemaAttribute } from '@graphpolaris/shared/lib/schema'; import { styleHandleMap } from '../../utils'; import { useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access'; @@ -64,7 +64,15 @@ export const EntityFlowElement = React.memo((node: SchemaReactflowEntityNode) => } > <Handle - id={getHandleId(data.name, data.type, Handles.ToRelation, '')} + // id={getHandleId(data.name, data.type, Handles.ToRelation, '')} + id={data.leftRelationHandleId} + type="target" + position={Position.Left} + className={styles.handle_to_relation} + /> + <Handle + // id={getHandleId(data.name, data.type, Handles.ToRelation, '')} + id={data.rightRelationHandleId} type="source" position={Position.Right} className={styles.handle_to_relation} diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx index ec6af4a20..d62df8343 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx @@ -6,8 +6,6 @@ import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; import { MultiGraph } from 'graphology'; import { QueryBuilder } from '../../../panel'; -import { QueryGraph } from '../../../model/graphology/model'; -import { circular } from 'graphology-layout'; import { QueryMultiGraphology } from '../../../model'; const Component: Meta<typeof QueryBuilder> = { diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx index d94494203..a714cd120 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx @@ -82,13 +82,13 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => { // style={{ transform: 'translate(-100px,0)' }} > <Handle - id={getHandleId(data.name, data.type, Handles.RelationLeft, '')} + id={data.leftEntityHandleId} type="target" position={Position.Left} className={styles.relationHandleLeft + ' ' + (false ? styles.handleConnectedBorderLeft : '')} /> </span> - <span className={styles.relationHandleFiller}> + {/* <span className={styles.relationHandleFiller}> <Handle id={getHandleId(data.name, data.type, Handles.ToAttribute, '')} type="target" @@ -103,7 +103,7 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => { position={Position.Left} className={styles.relationHandleFunction + ' ' + (false ? styles.handleConnectedFill : '')} /> - </span> + </span> */} <div className={styles.relationDataWrapper}> <span className={styles.relationSpan}>{data?.name}</span> <span className={styles.relationInputHolder}> @@ -166,8 +166,8 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => { </div> <span className={styles.relationHandleFiller}> <Handle - id={getHandleId(data.name, data.type, Handles.RelationRight, '')} - type="target" + id={data.rightEntityHandleId} + type="source" position={Position.Right} className={styles.relationHandleRight + ' ' + (false ? styles.handleConnectedBorderRight : '')} /> diff --git a/libs/shared/lib/querybuilder/query-utils/query-utils.spec.ts b/libs/shared/lib/querybuilder/query-utils/query-utils.spec.ts new file mode 100644 index 000000000..8e9b2b1f2 --- /dev/null +++ b/libs/shared/lib/querybuilder/query-utils/query-utils.spec.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from 'vitest'; +// import { Query2BackendQuery } from './query-utils'; +import { QueryMultiGraphology } from '../model/graphology/utils'; + +describe('QueryUtils', () => { + it('should create an instance', () => { + expect(true).toBeTruthy(); + }); + + // it('should run query translation', () => { + // const graph = new QueryMultiGraphology(); // FIXME: not working for some reason + + // let ret = Query2BackendQuery('database', graph.export()); + // console.log(ret); + // }); +}); diff --git a/libs/shared/lib/querybuilder/query-utils/query-utils.ts b/libs/shared/lib/querybuilder/query-utils/query-utils.ts index 7fc1cfe0c..71a935e62 100644 --- a/libs/shared/lib/querybuilder/query-utils/query-utils.ts +++ b/libs/shared/lib/querybuilder/query-utils/query-utils.ts @@ -1,90 +1,142 @@ -import { EntityNodeAttributes, RelationNodeAttributes } from '../model/graphology/model'; +import { EntityNodeAttributes, QueryGraphNodes, RelationNodeAttributes } from '../model/graphology/model'; import { QueryMultiGraph } from '../model/graphology/utils'; -import { BackendQueryFormat } from '../model/BackendQueryFormat'; -import { Handles, QueryElementTypes } from '../model'; +import { BackendQueryFormat, NodeStruct, QueryStruct, RelationStruct } from '../model/BackendQueryFormat'; +import { Handles, QueryElementTypes, fromHandleId } from '../model'; +import { get } from 'http'; +import { SerializedEdge } from 'graphology-types'; +import { G } from 'vitest/dist/types-fafda418'; + +// export type QueryI { + +// } /** * Converts the ReactFlow query to a json data structure to send the query to the backend. * @returns {BackendQueryFormat} A JSON object in the `JSONFormat`. */ export function Query2BackendQuery(databaseName: string, graph: QueryMultiGraph): BackendQueryFormat { - // dict of nodes per type - const entities = graph.nodes - .filter((n) => n.attributes?.type === QueryElementTypes.Entity) - .map((n) => ({ - name: n.attributes!.name, - ID: Number(n.key), - constraints: [], - })); - - const relations = graph.nodes - .filter((n) => n.attributes?.type === QueryElementTypes.Relation) - .map((n) => { - const attributes = n.attributes as RelationNodeAttributes; - const leftEdge = graph.edges.filter((e) => e.target === n.key && e.attributes!.targetHandle === Handles.RelationLeft)?.[0]; - const rightEdge = graph.edges.filter((e) => e.target === n.key && e.attributes!.targetHandle === Handles.RelationRight)?.[0]; - if (!leftEdge || !rightEdge) throw Error('Malformed Graph! One or more edges of a relation node do not exist'); - - const leftType = graph.nodes.filter((n) => n.key === leftEdge.source)?.[0]?.attributes?.type; - const rightType = graph.nodes.filter((n) => n.key === rightEdge.source)?.[0]?.attributes?.type; - if (!rightType || !leftType) throw Error('Malformed Graph! edges must have a type'); - - return { - name: attributes!.collection, - ID: Number(n.key), - depth: attributes!.depth, - fromType: leftType, - fromID: Number(leftEdge.source), // need to treat function==groupby - toType: rightType, - toID: Number(rightEdge.source), // need to treat function==groupby - constraints: [], - }; - }); - - // TODO: Groupby not implemented (constraints) - const result: BackendQueryFormat = { + let query: BackendQueryFormat = { databaseName: databaseName, - return: { - entities: entities.map((e) => e.ID), - relations: relations.map((r) => r.ID), - groupBys: [], - }, - entities: entities, - relations: relations, - groupBys: [], // TODO - machineLearning: [], // TODO + query: [], limit: 5000, // TODO + return: ['*'], // TODO }; - console.log(result, graph); - - //note that attribute nodes are not processed here, but are detected if they are connected to any entity/relation/function node - - //add nodes to JSON query - // entityNodes.forEach((node) => { - // this.AddEntityToJsonObject(result, node); - // }); - // relationNodes.forEach((node) => { - // this.AddRelationToJsonObject(result, node); - // }); - - // functionNodes.forEach((functionNode: FunctionNode) => { - // switch (functionNode.data?.functionType) { - // case FunctionTypes.GroupBy: - // this.AddGroupByToJsonObject(result, functionNode); - // break; - // case FunctionTypes.link: - // this.AddLinkPredictionToJsonObject(result, functionNode); - // break; - // case FunctionTypes.communityDetection: - // this.AddCommunityDetectionToJsonObject(result, functionNode); - // break; - // case FunctionTypes.centrality: - // this.AddCentralityToJsonObject(result, functionNode); - // break; - // case FunctionTypes.shortestPath: - // this.addShortestPathToJsonOBject(result, functionNode); - // break; - // } - // }); - return result; + let graphLogicChunks: QueryGraphNodes[][] = [[]]; + + const entities: EntityNodeAttributes[] = graph.nodes + .map((n) => n.attributes) + .filter((n) => n?.type === QueryElementTypes.Entity) as EntityNodeAttributes[]; + const entitiesEmptyLeftHandle = entities.filter((n) => !graph.edges.some((e) => e.target === n?.id)); + // const entitiesEmptyRightHandle = entities.filter((n) => !n?.rightRelationHandleId); + + const relations: RelationNodeAttributes[] = graph.nodes + .map((n) => n.attributes) + .filter((n) => n?.type === QueryElementTypes.Relation) as RelationNodeAttributes[]; + // const relationsEmptyLeftHandle = relations.filter((n) => !n?.leftEntityHandleId); + // const relationsEmptyRightHandle = relations.filter((n) => !n?.rightEntityHandleId); + + const traversePaths = (node: QueryGraphNodes, paths: QueryGraphNodes[][], currentIdx: number): number => { + if (!paths?.[currentIdx]) paths.push([]); + paths[currentIdx].push(node); + + // const rightHandle = + // node.type === QueryElementTypes.Entity + // ? (node as EntityNodeAttributes)?.rightRelationHandleId + // : (node as RelationNodeAttributes)?.rightEntityHandleId; + + let connections = graph.edges.filter((e) => e.source === node.id); + if (connections.length === 0) return 0; + + const nodesToRight = connections + .map((c, i) => { + const rightNodeHandleData = fromHandleId(c.attributes?.targetHandle); + const rightNode = + rightNodeHandleData.nodeType === QueryElementTypes.Entity + ? entities.find((r) => r.id === c.target) + : relations.find((r) => r.id === c.target); + return rightNode; + }) + .filter((n) => n !== undefined) as QueryGraphNodes[]; + + // if (!rightNode) return; + // if (currentIdx + i === paths.length) paths.push(paths[currentIdx - 1 + i]); + // inverseTraverse(rightNode, paths, currentIdx + i); + // paths[currentIdx + i].push(rightNode); + // nodesToRight.forEach((rightNode, i) => { + // if (currentIdx + i === paths.length) { + // paths.push(paths[currentIdx - 1 + i]); + // } + // }); + + let chunkOffset = 0; + nodesToRight.forEach((rightNode, i) => { + chunkOffset += traversePaths(rightNode, paths, currentIdx + i + chunkOffset); + }); + + return nodesToRight.length - 1; // offset + }; + + // find paths which ends at an entity with no right handle + let chunkOffset = 0; + entitiesEmptyLeftHandle.map((entity, i) => { + chunkOffset += traversePaths(entity, graphLogicChunks, i + chunkOffset); + }); + + console.log('graphLogicChunks', graphLogicChunks); + if (!graphLogicChunks || graphLogicChunks.length === 0 || graphLogicChunks?.[0].length === 0) return query; + + const processConnection = (chunk: QueryGraphNodes[], position: number): RelationStruct | NodeStruct => { + const currNode = chunk[position]; + + if (currNode.type === QueryElementTypes.Relation) { + const ret: RelationStruct = { + ID: currNode?.id, + label: currNode?.name, + // depth: QuerySearchDepthStruct; + direction: 'TO', + node: chunk.length === position + 1 ? undefined : (processConnection(chunk, position + 1) as NodeStruct), + }; + return ret; + } else if (currNode.type === QueryElementTypes.Entity) { + const ret: NodeStruct = { + ID: currNode?.id, + label: currNode?.name, + // logic: LogicStruct[]; + // subQuery?: QueryStruct; + // export: ExportNodeStruct[]; + relation: chunk.length === position + 1 ? undefined : (processConnection(chunk, position + 1) as RelationStruct), + }; + return ret; + } + + throw Error('Malformed Chunks! ' + chunk + position); + }; + + query.query = graphLogicChunks.map((chunk, i) => { + const ret: QueryStruct = { + ID: 'path_' + i, //TODO: chunk[0].id || + node: processConnection(chunk.reverse(), 0), + }; + return ret; + }); + + console.log(query); + + return query; } + +// function processConnectionFromRelation( +// c: SerializedEdge, +// entities: EntityNodeAttributes[], +// relations: RelationNodeAttributes[] +// ): NodeStruct { +// if (c.attributes?.targetHandle === null) throw Error('Malformed Graph! One or more edges of a relation node do not exist'); +// const targetHandleData = fromHandleId(c.attributes?.targetHandle); + +// if (targetHandleData.nodeType === QueryElementTypes.Entity) { +// const targetEntity = entities.find((r) => r.id === c.target); +// } else if (targetHandleData.nodeType === QueryElementTypes.Relation) { +// const targetRelation = relations.find((r) => r.id === c.target); +// } else if (targetHandleData.nodeType === QueryElementTypes.Logic) { +// } +// } diff --git a/libs/shared/lib/schema/index.ts b/libs/shared/lib/schema/index.ts index 3c5fd8443..9f8ccaddf 100644 --- a/libs/shared/lib/schema/index.ts +++ b/libs/shared/lib/schema/index.ts @@ -1,2 +1 @@ export * from './model'; -export * from '../querybuilder/model'; diff --git a/libs/shared/lib/schema/panel/schema.tsx b/libs/shared/lib/schema/panel/schema.tsx index 7245b9eab..2c5ee5dee 100644 --- a/libs/shared/lib/schema/panel/schema.tsx +++ b/libs/shared/lib/schema/panel/schema.tsx @@ -15,6 +15,7 @@ import ReactFlow, { ReactFlowInstance, Background, } from 'reactflow'; +import CachedIcon from '@mui/icons-material/Cached'; import 'reactflow/dist/style.css'; @@ -216,6 +217,15 @@ export const Schema = (props: Props) => { > <img src={exportIcon} width={21}></img> </ControlButton> */} + <ControlButton + className={styles.exportButton} + title={'Refresh graph schema'} + onClick={(event) => { + event.stopPropagation(); + }} + > + <CachedIcon /> + </ControlButton> </Controls> </ReactFlow> </ReactFlowProvider> diff --git a/libs/shared/lib/vis/nodelink/nodelinkvis.tsx b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx index 8771f7e94..3e28c34c9 100644 --- a/libs/shared/lib/vis/nodelink/nodelinkvis.tsx +++ b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx @@ -63,7 +63,6 @@ export const NodeLinkVis = React.memo((props: Props) => { const graphQueryResult = useGraphQueryResult(); const dispatch = useAppDispatch(); const theme = useTheme(); - console.log('update nodelink'); useEffect(() => { console.log('update nodelink useEffect', graphQueryResult); diff --git a/libs/shared/node.d.ts b/libs/shared/node.d.ts index 0cb37864e..3d2ced5bc 100644 --- a/libs/shared/node.d.ts +++ b/libs/shared/node.d.ts @@ -1,6 +1,7 @@ interface ImportMeta { env: { VITE_BACKEND_URL: string; + VITE_BACKEND_WSS_URL: string; VITE_BACKEND_USER: string; VITE_BACKEND_SCHEMA: string; VITE_BACKEND_QUERY: string; diff --git a/libs/shared/tsconfig.json b/libs/shared/tsconfig.json index 855b96076..b2617928c 100644 --- a/libs/shared/tsconfig.json +++ b/libs/shared/tsconfig.json @@ -1,34 +1,34 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "display": "React Library", - "target": "ESNext", - "compilerOptions": { - "composite": false, - "inlineSources": false, - "noUnusedLocals": false, - "noUnusedParameters": false, - "preserveWatchOutput": true, - "jsx": "react-jsx", - "target": "ESNext", - "useDefineForClassFields": true, - "lib": ["ES2017", "DOM", "DOM.Iterable", "ESNext"], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "node", - "resolveJsonModule": true, - "noEmit": true, - "baseUrl": ".", - "paths": { - "@graphpolaris/shared/lib/*": ["./lib/*"] - } - }, - "exclude": ["dist", "build", "node_modules"], - "include": ["src", "lib"], - "files": ["./node.d.ts"], - "references": [{ "path": "./tsconfig.node.json" }] -} +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "React Library", + "target": "ESNext", + "compilerOptions": { + "composite": false, + "inlineSources": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "preserveWatchOutput": true, + "jsx": "react-jsx", + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["ES2017", "DOM", "DOM.Iterable", "ESNext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "node", + "resolveJsonModule": true, + "noEmit": true, + "baseUrl": ".", + "paths": { + "@graphpolaris/shared/lib/*": ["./lib/*"] + } + }, + "exclude": ["dist", "build", "node_modules"], + "include": ["src", "lib"], + "files": ["./node.d.ts"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/package.json b/package.json index 38926ee17..c9e130644 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "eslint-config-custom": "workspace:*", "husky": "^8.0.0", "prettier": "latest", - "turbo": "latest" + "turbo": "latest", + "vitest": "^0.29.2" }, "engines": { "node": ">=14.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f6eec94c..64a3fc97a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,10 +25,13 @@ importers: version: 8.0.3 prettier: specifier: latest - version: 2.8.8 + version: 3.0.0 turbo: specifier: latest - version: 1.10.3 + version: 1.10.8 + vitest: + specifier: ^0.29.2 + version: 0.29.4(happy-dom@8.9.0)(jsdom@21.1.1)(sass@1.59.3) apps/docs: dependencies: @@ -557,7 +560,7 @@ importers: version: 8.7.0(eslint@7.32.0) eslint-config-turbo: specifier: latest - version: 1.10.3(eslint@7.32.0) + version: 1.10.6(eslint@7.32.0) eslint-plugin-react: specifier: 7.31.8 version: 7.31.8(eslint@7.32.0) @@ -8893,7 +8896,6 @@ packages: /dotenv@16.0.3: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} - dev: true /draco3d@1.5.5: resolution: {integrity: sha512-JVuNV0EJzD3LBYhGyIXJLeBID/EVtmFO1ZNhAYflTgiMiAJlbhXQmRRda/azjc8MRVMHh0gqGhiqHUo5dIXM8Q==} @@ -9224,15 +9226,6 @@ packages: dependencies: eslint: 7.32.0 - /eslint-config-turbo@1.10.3(eslint@7.32.0): - resolution: {integrity: sha512-ggzPfTJfMsMS383oZ4zfTP1zQvyMyiigOQJRUnLt1nqII6SKkTzdKZdwmXRDHU24KFwUfEFtT6c8vnm2VhL0uQ==} - peerDependencies: - eslint: '>6.6.0' - dependencies: - eslint: 7.32.0 - eslint-plugin-turbo: 1.10.3(eslint@7.32.0) - dev: false - /eslint-config-turbo@1.10.6(eslint@7.32.0): resolution: {integrity: sha512-iZ63etePRUdEIDY5MxdUhU2ekV9TDbVdHg0BK00QqVFgQTXUYuJ7rsQj/wUKTsw3jwhbLfaY6H5sknAgYyWZ2g==} peerDependencies: @@ -9240,7 +9233,6 @@ packages: dependencies: eslint: 7.32.0 eslint-plugin-turbo: 1.10.6(eslint@7.32.0) - dev: true /eslint-import-resolver-node@0.3.7: resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} @@ -9383,14 +9375,6 @@ packages: semver: 6.3.0 string.prototype.matchall: 4.0.8 - /eslint-plugin-turbo@1.10.3(eslint@7.32.0): - resolution: {integrity: sha512-g3Mnnk7el1FqxHfqbE/MayLvCsYjA/vKmAnUj66kV4AlM7p/EZqdt42NMcMSKtDVEm0w+utQkkzWG2Xsa0Pd/g==} - peerDependencies: - eslint: '>6.6.0' - dependencies: - eslint: 7.32.0 - dev: false - /eslint-plugin-turbo@1.10.6(eslint@7.32.0): resolution: {integrity: sha512-jlzfxYaK8hcz1DTk8Glxxi1r0kgdy85191a4CbFOTiiBulmKHMLJgzhsyE9Ong796MA62n91KFpc20BiKjlHwg==} peerDependencies: @@ -9398,7 +9382,6 @@ packages: dependencies: dotenv: 16.0.3 eslint: 7.32.0 - dev: true /eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} @@ -12501,6 +12484,12 @@ packages: hasBin: true dev: true + /prettier@3.0.0: + resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==} + engines: {node: '>=14'} + hasBin: true + dev: true + /pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -14327,65 +14316,65 @@ packages: tslib: 1.14.1 typescript: 4.9.5 - /turbo-darwin-64@1.10.3: - resolution: {integrity: sha512-IIB9IomJGyD3EdpSscm7Ip1xVWtYb7D0x7oH3vad3gjFcjHJzDz9xZ/iw/qItFEW+wGFcLSRPd+1BNnuLM8AsA==} + /turbo-darwin-64@1.10.8: + resolution: {integrity: sha512-FOK3qrLZE2Yq7/2DkAnAzghisGQroZJs85Rui3IXM/2e7rTtBADmU9w36d4k0Yw7RHEiOo8U4eAYUl52OWRwJQ==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.10.3: - resolution: {integrity: sha512-SBNmOZU9YEB0eyNIxeeQ+Wi0Ufd+nprEVp41rgUSRXEIpXjsDjyBnKnF+sQQj3+FLb4yyi/yZQckB+55qXWEsw==} + /turbo-darwin-arm64@1.10.8: + resolution: {integrity: sha512-8mbgH8oBycusa8RnbHlvrpHxfZsgNrk6CXMu/KJECpajYT3nSOMK2Rrs+422HqLDTVUw4GAqmTr26nUx8yJoyA==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.10.3: - resolution: {integrity: sha512-kvAisGKE7xHJdyMxZLvg53zvHxjqPK1UVj4757PQqtx9dnjYHSc8epmivE6niPgDHon5YqImzArCjVZJYpIGHQ==} + /turbo-linux-64@1.10.8: + resolution: {integrity: sha512-eJ1ND3LuILw28gd+9f3Ews7Eika9WOxp+/PxJI+EPHseTrbLMLYqSPAunmZdOx840Pq0Sk5j4Nik7NCzuCWXkg==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.10.3: - resolution: {integrity: sha512-Qgaqln0IYRgyL0SowJOi+PNxejv1I2xhzXOI+D+z4YHbgSx87ox1IsALYBlK8VRVYY8VCXl+PN12r1ioV09j7A==} + /turbo-linux-arm64@1.10.8: + resolution: {integrity: sha512-3+pVaOzGP/5GFvQakxuHDMsj43Y6bmaq5/84tvgGL0FgtKpsQvBfdaDs12HX5cb/zUnd2/jdQPNiGJwVeC/McA==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.10.3: - resolution: {integrity: sha512-rbH9wManURNN8mBnN/ZdkpUuTvyVVEMiUwFUX4GVE5qmV15iHtZfDLUSGGCP2UFBazHcpNHG1OJzgc55GFFrUw==} + /turbo-windows-64@1.10.8: + resolution: {integrity: sha512-LdryI+ZQsVrW4hWZw5G5vJz0syjWxyc0tnieZRefy+d9Ti1du/qCYLP0KQRgL9Yuh1klbH/tzmx70upGARgWKQ==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.10.3: - resolution: {integrity: sha512-ThlkqxhcGZX39CaTjsHqJnqVe+WImjX13pmjnpChz6q5HHbeRxaJSFzgrHIOt0sUUVx90W/WrNRyoIt/aafniw==} + /turbo-windows-arm64@1.10.8: + resolution: {integrity: sha512-whHnhM84KIa2Ly/fcw2Ujw2Rr/9wh8ynAdZ9bdvZoZKAbOr3tXKft0tmy50jQ6IsNr6Cj0XD4cuSTKhvqoGtYA==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.10.3: - resolution: {integrity: sha512-U4gKCWcKgLcCjQd4Pl8KJdfEKumpyWbzRu75A6FCj6Ctea1PIm58W6Ltw1QXKqHrl2pF9e1raAskf/h6dlrPCA==} + /turbo@1.10.8: + resolution: {integrity: sha512-lmPKkeRMC/3gjTVxICt93A8zAzjGjbZINdekjzivn4g/rOjpHVNuOuVANU5L4H4R1bzQr8FFvZNQeQaElOjz/Q==} hasBin: true requiresBuild: true optionalDependencies: - turbo-darwin-64: 1.10.3 - turbo-darwin-arm64: 1.10.3 - turbo-linux-64: 1.10.3 - turbo-linux-arm64: 1.10.3 - turbo-windows-64: 1.10.3 - turbo-windows-arm64: 1.10.3 + turbo-darwin-64: 1.10.8 + turbo-darwin-arm64: 1.10.8 + turbo-linux-64: 1.10.8 + turbo-linux-arm64: 1.10.8 + turbo-windows-64: 1.10.8 + turbo-windows-arm64: 1.10.8 dev: true /type-check@0.3.2: -- GitLab