diff --git a/apps/web/src/app/app.tsx b/apps/web/src/app/app.tsx index 0ecebbec8facf92f7c03a8f4a5116fccbef50c03..c616b6452aca9754acfd38ccee16dcc4f3e84364 100644 --- a/apps/web/src/app/app.tsx +++ b/apps/web/src/app/app.tsx @@ -19,7 +19,7 @@ import { } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; import { Query2BackendQuery, QueryBuilder } from '@graphpolaris/shared/lib/querybuilder'; import { Schema } from '@graphpolaris/shared/lib/schema/panel'; -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { Navbar } from '../components/navbar/navbar'; import Panel from '../components/panels/panel'; import { VisualizationPanel } from './panels/Visualization'; @@ -70,9 +70,9 @@ export function App(props: App) { useEffect(() => { // Newly (un)authorized - console.log('Auth changed', auth.authorized, isLogin); + // console.log('Auth changed', auth.authorized, isLogin); if (auth.authorized) { - console.info('App is authorized; Getting Databases', isLogin); + console.debug('App is authorized; Getting Databases', isLogin); api.GetAllDatabases({ updateSessionCache: true }); setAuthCheck(true); } else { @@ -80,17 +80,18 @@ export function App(props: App) { } }, [auth]); - useEffect(() => { - // New query + const runQuery = () => { if (session?.currentDatabase && query) { - console.log('New query', query); - if (query.edges.length === 0) { dispatch(resetGraphQueryResults()); } else { api_query.execute(Query2BackendQuery(session.currentDatabase, query)); } } + }; + + useEffect(() => { + runQuery(); }, [queryHash]); return ( @@ -114,7 +115,13 @@ export function App(props: App) { </div> <div className={styles.queryBuilder}> <Panel content="Query Panel"> - <QueryBuilder /> + <QueryBuilder + onRunQuery={() => { + console.log('Run Query'); + + runQuery(); + }} + /> </Panel> </div> </div> diff --git a/apps/web/src/components/navbar/navbar.tsx b/apps/web/src/components/navbar/navbar.tsx index c5918591ad5b43cba063ebbfbcfb0195d4ac9ab1..9e1fe77168fcb8d28e5cbf22a98d7a16da8281ef 100644 --- a/apps/web/src/components/navbar/navbar.tsx +++ b/apps/web/src/components/navbar/navbar.tsx @@ -58,7 +58,7 @@ export const Navbar = (props: NavbarComponentProps) => { const dispatch = useAppDispatch(); useEffect(() => { - console.log(auth); + // console.log(auth); }, [auth.accessToken]); // const { navbarViewModel, currentColours } = props; diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 483b0d21b5718072f82dc621247151aa686e74e6..b6d8c226a8380854efa7ccfd37ce8c77c0868828 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -16,8 +16,6 @@ const domNode = document.getElementById('root'); if (domNode) { const root = createRoot(domNode); - - console.log(store); root.render( <Provider store={store}> <GraphPolarisThemeProvider> @@ -29,6 +27,6 @@ if (domNode) { </Routes> </Router> </GraphPolarisThemeProvider> - </Provider> + </Provider>, ); } diff --git a/libs/shared/lib/data-access/api/database.ts b/libs/shared/lib/data-access/api/database.ts index 9c3abbe8501d76abe6ce7cb32e99ac690405bd4c..11caee2a6784d525f600f8ccedc90034e649b5fc 100644 --- a/libs/shared/lib/data-access/api/database.ts +++ b/libs/shared/lib/data-access/api/database.ts @@ -53,7 +53,7 @@ export const useDatabaseAPI = () => { }), body: JSON.stringify(request), }).then((response: Response) => { - console.log(response); + console.info('Added Database', response); if (!response.ok) { reject(response.statusText); @@ -76,7 +76,7 @@ export const useDatabaseAPI = () => { Authorization: 'Bearer ' + accessToken, }), }); - console.log(response); + // console.log(response); if (!response.ok) { const text = await response.text(); diff --git a/libs/shared/lib/data-access/api/query.ts b/libs/shared/lib/data-access/api/query.ts index 5fd651facc8018b7e4ec8329aa0213680ec0a699..1aad449bfa8a250fca8e0a20ed5671d9acc3084d 100644 --- a/libs/shared/lib/data-access/api/query.ts +++ b/libs/shared/lib/data-access/api/query.ts @@ -25,7 +25,7 @@ export const useQueryAPI = () => { return; } const ret = await response.json(); - console.log('Sent Query EXECUTION', ret); + console.debug('Sent Query EXECUTION', ret); } async function retrieveCachedQuery(queryID: string) { diff --git a/libs/shared/lib/data-access/api/schema.ts b/libs/shared/lib/data-access/api/schema.ts index 59a7b2ecf317e442d6a7397fdcb6bfdb09a7ceee..417b607835f553030bb195ecc5459621cb27fc9d 100644 --- a/libs/shared/lib/data-access/api/schema.ts +++ b/libs/shared/lib/data-access/api/schema.ts @@ -33,7 +33,7 @@ export const useSchemaAPI = () => { console.error(response, ret); } // const ret = await response.json(); - console.log(response); + // console.debug('Schema Requested', response); } return { RequestSchema }; diff --git a/libs/shared/lib/data-access/authorization/useAuth.jsx b/libs/shared/lib/data-access/authorization/useAuth.jsx index ab8bb0a74df81b45eb4ea42cb1ad43065af06dc9..25e0643c631801ea207bcffb540b91b816dfc59a 100644 --- a/libs/shared/lib/data-access/authorization/useAuth.jsx +++ b/libs/shared/lib/data-access/authorization/useAuth.jsx @@ -17,7 +17,7 @@ export const useAuth = () => { useEffect(() => { if (import.meta.env.VITE_SKIP_LOGIN) { - console.log('skipping login'); + console.warn('skipping login'); setLogin(true); dispatch( authorized({ diff --git a/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.tsx b/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.tsx index bca2d0453b50e7092062f48eda0ef0da4af039a6..64b41b9dffb20eb5dfa7afd0baee6ec8f721fcee 100644 --- a/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.tsx +++ b/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.tsx @@ -67,7 +67,7 @@ export class WebSocketHandler implements BackendMessageReceiver { */ public onWebSocketMessage = (event: MessageEvent<any>) => { let data = JSON.parse(event.data); - console.log('WS message: ', data); + console.debug('WS message: ', data); Broker.instance().publish(data.value, data.type); }; @@ -77,6 +77,6 @@ export class WebSocketHandler implements BackendMessageReceiver { * @param {any} event contains the event data. */ private onError(event: any): void { - console.log(event); + console.error(event); } } diff --git a/libs/shared/lib/data-access/socket/broker/index.tsx b/libs/shared/lib/data-access/socket/broker/index.tsx index b78b7acc9d08ddcc40ce9d1c605cbe71d6c77eb8..098413fd9772a19905aee82bdbf9021aa57100e6 100644 --- a/libs/shared/lib/data-access/socket/broker/index.tsx +++ b/libs/shared/lib/data-access/socket/broker/index.tsx @@ -42,7 +42,7 @@ export default class Broker { Object.values(this.listeners[routingKey]).forEach((listener) => listener(jsonObject, routingKey)); // If there are no listeners, log the message else - console.log( + console.debug( `no listeners for message with routing key %c${routingKey}`, 'font-weight:bold; color: blue; background-color: white;', jsonObject diff --git a/libs/shared/lib/data-access/store/graphQueryResultSlice.ts b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts index 95c98320965e379babcb4a1e92eb44bc4a1e04d8..a776ada94396ee7a1f3659386bfae9894272e7c1 100644 --- a/libs/shared/lib/data-access/store/graphQueryResultSlice.ts +++ b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts @@ -65,7 +65,7 @@ export const graphQueryResultSlice = createSlice({ reducers: { assignNewGraphQueryResult: (state, action: PayloadAction<GraphQueryResultFromBackendPayload>) => { const payload = action.payload.payload; - console.log('!!!assignNewGraphQueryResult', action.payload.payload); + // console.log('!!!assignNewGraphQueryResult', action.payload.payload); // Maybe do some data quality checking and parsing // ... @@ -73,15 +73,15 @@ export const graphQueryResultSlice = createSlice({ // Collect all the different nodetypes in the result const nodeTypes: string[] = []; payload.nodes = payload.nodes.map((node) => { - // TODO FIXME!! Note: works only for arangodb + let _node = { ...node }; let nodeType = node.id.split('/')[0]; let innerLabels = node?.attributes?.labels as string[]; if (innerLabels.length > 0) { - nodeType = innerLabels[0] as string; // TODO: Not sure it works yet + nodeType = innerLabels[0] as string; } if (!nodeTypes.includes(nodeType)) nodeTypes.push(nodeType); - node.label = nodeType; - return node; + _node.label = nodeType; + return _node; }); payload.edges = payload.edges.map((edge) => { diff --git a/libs/shared/lib/data-access/store/querybuilderSlice.ts b/libs/shared/lib/data-access/store/querybuilderSlice.ts index 09903d7870398b4b40d53e9837321ebe180c5f55..26993303d8c1ec3b3501d2b97690dfbfa58473ff 100644 --- a/libs/shared/lib/data-access/store/querybuilderSlice.ts +++ b/libs/shared/lib/data-access/store/querybuilderSlice.ts @@ -1,12 +1,14 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from './store'; -import { MultiGraph } from 'graphology'; +import Graph, { MultiGraph } from 'graphology'; import { Attributes, SerializedGraph } from 'graphology-types'; -import { QueryMultiGraph, QueryMultiGraphology } from '../../querybuilder'; +import { QueryMultiGraph, QueryMultiGraphology as QueryGraphology } from '../../querybuilder/model/graphology/utils'; // Define the initial state using that type -export const initialState = { - graphologySerialized: new QueryMultiGraphology().export(), +export const initialState: { + graphologySerialized: QueryMultiGraph; +} = { + graphologySerialized: new QueryGraphology().export(), // schemaLayout: 'Graphology_noverlap', }; @@ -15,9 +17,11 @@ export const querybuilderSlice = createSlice({ // `createSlice` will infer the state type from the `initialState` argument initialState, reducers: { - setQuerybuilderNodes: (state, action: PayloadAction<SerializedGraph<Attributes, Attributes, Attributes>>) => { + setQuerybuilderNodes: (state, action: PayloadAction<QueryMultiGraph>) => { // console.log('setQuerybuilderNodes', action.payload); - state.graphologySerialized = QueryMultiGraphology.from(action.payload).export() as QueryMultiGraph; + // @ts-ignore + state.graphologySerialized = QueryGraphology.from(action.payload).export(); + // state.graphologySerialized = action.payload; }, // updateQBAttributeOperator: (state, action: PayloadAction<{ id: string; operator: string }>) => { // const graph = QueryMultiGraphology.from(state.graphologySerialized); @@ -30,7 +34,7 @@ export const querybuilderSlice = createSlice({ // state.graphologySerialized = graph.export(); // }, clearQB: (state) => { - state.graphologySerialized = new QueryMultiGraphology().export(); + state.graphologySerialized = new QueryGraphology().export(); }, // addQuerybuilderNode: ( @@ -50,11 +54,11 @@ export const querybuilderSlice = createSlice({ export const { setQuerybuilderNodes, clearQB } = querybuilderSlice.actions; /** Select the querybuilder nodes in serialized fromat */ -export const selectQuerybuilderGraphology = (state: RootState): QueryMultiGraphology => { +export const selectQuerybuilderGraphology = (state: RootState): QueryGraphology => { // This is really weird but for some reason all the attributes appeared as read-only otherwise - let ret = new QueryMultiGraphology(); - ret.import(MultiGraph.from(state.querybuilder.graphologySerialized).export()); + let ret = new QueryGraphology(); + ret.import(Graph.from(state.querybuilder.graphologySerialized).export()); return ret; }; @@ -66,10 +70,21 @@ export const selectQuerybuilderGraph = (state: RootState): QueryMultiGraph => { /** Select the querybuilder nodes and convert it to a graphology object */ export const selectQuerybuilderHash = (state: RootState): any => { + const hashedNodes = state.querybuilder.graphologySerialized.nodes.map((n) => { + let node = { ...n }; + if (n?.attributes) { + let newAttributes = { ...n?.attributes }; + newAttributes.x = 0; + newAttributes.y = 0; + newAttributes.height = 0; + newAttributes.width = 0; + node.attributes = newAttributes; + } + return node; + }); + return JSON.stringify({ - nodes: state.querybuilder.graphologySerialized.nodes.map((n) => ({ - key: n.key, - })), + nodes: hashedNodes, edges: state.querybuilder.graphologySerialized.edges.map((n) => ({ key: n.key, source: n.source, diff --git a/libs/shared/lib/data-access/store/schemaSlice.ts b/libs/shared/lib/data-access/store/schemaSlice.ts index 3b5c2890548f433f7c86ba2214495df9bf1f86b6..80f9573051bf67a33f5763e50a61468103a10cfc 100644 --- a/libs/shared/lib/data-access/store/schemaSlice.ts +++ b/libs/shared/lib/data-access/store/schemaSlice.ts @@ -33,7 +33,7 @@ export const schemaSlice = createSlice({ readInSchemaFromBackend: (state, action: PayloadAction<SchemaFromBackend>) => { state.graphologySerialized = SchemaUtils.schemaBackend2Graphology(action.payload).export(); - console.log('Updated schema from backend'); + // console.log('Updated schema from backend'); // The graph schema needs a node for each node AND edge. These need then be connected // nodes.forEach((node) => { diff --git a/libs/shared/lib/data-access/store/sessionSlice.ts b/libs/shared/lib/data-access/store/sessionSlice.ts index 4de3a9179be4bd2764c121695c278d2ae3672798..bc9e98a8b5aa61faadfcbd92565b8f2552ce36ff 100644 --- a/libs/shared/lib/data-access/store/sessionSlice.ts +++ b/libs/shared/lib/data-access/store/sessionSlice.ts @@ -51,7 +51,7 @@ export const sessionSlice = createSlice({ state.currentDatabase = action.payload; }, updateDatabaseList(state, action: PayloadAction<string[]>) { - console.log('Updating database list', action); + console.debug('Updating database list', action); state.databases = action.payload; if (state.databases.length > 0) { if (!state.currentDatabase || !state.databases.includes(state.currentDatabase)) state.currentDatabase = state.databases[0]; diff --git a/libs/shared/lib/graph-layout/cytoscape-layouts.ts b/libs/shared/lib/graph-layout/cytoscape-layouts.ts index 7970ad5aa5336f614b03d7510a1d0d8ded363eab..7b0fe6945d9501d8ff42a2d8bd24ab32826eaa2a 100644 --- a/libs/shared/lib/graph-layout/cytoscape-layouts.ts +++ b/libs/shared/lib/graph-layout/cytoscape-layouts.ts @@ -241,10 +241,10 @@ class CytoscapeKlay extends Cytoscape { // boundingBox: undefined, ready: function () { - console.info('Layout.ready'); + // console.info('Layout.ready'); }, // on layoutready stop: function () { - console.debug('Layout.stop'); + // console.debug('Layout.stop'); }, // on layoutstop } as any); layout.run(); diff --git a/libs/shared/lib/graph-layout/layout.ts b/libs/shared/lib/graph-layout/layout.ts index 8066aed2d486114e0f1a2bfb9a0cf0953a7bb748..e99ef69bbaf54a4b8cdff419e9332b24bee5a62b 100644 --- a/libs/shared/lib/graph-layout/layout.ts +++ b/libs/shared/lib/graph-layout/layout.ts @@ -8,11 +8,11 @@ import { Providers, LayoutAlgorithm } from './layout-creator-usecase'; export abstract class Layout<provider extends Providers> { constructor(public provider: provider, public algorithm: LayoutAlgorithm<provider>) { - console.info(`Created the following Layout: ${provider} - ${this.algorithm}`); + // console.info(`Created the following Layout: ${provider} - ${this.algorithm}`); } public layout(graph: Graph, verbose?: boolean) { - console.log(`${this.provider} [${this.algorithm}] layouting now`); + // console.log(`${this.provider} [${this.algorithm}] layouting now`); graph.forEachNode((node) => { const attr = graph.getNodeAttributes(node); diff --git a/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx b/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx index 98608922bd2b021b06a4fb8a76817ca0bb901b08..06a26612530935cf68837302d4c8064e4ed0319e 100644 --- a/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx +++ b/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx @@ -4,7 +4,8 @@ * © Copyright Utrecht University (Department of Information and Computing Sciences) */ -import { InputNodeType } from './logic/general'; +import { type } from 'os'; +import { AllLogicStatement, AnyStatement, InputNodeType } from './logic/general'; /** JSON query format used to send a query to the backend. */ export interface BackendQueryResultFormat { @@ -29,6 +30,7 @@ export interface BackendQueryFormat { limit: number; return: string[]; query: QueryStruct[]; + logic?: AnyStatement; // entities: Entity[]; // relations: Relation[]; // groupBys: GroupBy[]; @@ -46,27 +48,28 @@ export interface QueryStruct { export interface NodeStruct { label?: string; ID?: string; - logic?: LogicStruct[]; + logic?: AllLogicStatement; + filter?: FilterStruct[]; relation?: RelationStruct; subQuery?: QueryStruct; export?: ExportNodeStruct[]; } export interface ExportNodeStruct { - ID: number; + ID: string; attribute: string; } export interface LogicStruct { - ID: number; + ID: string; attribute: string; operation: LogicOperationType; } export interface FilterStruct { - ID: number; attribute: string; operation: FilterOperationType; + value: string; } export interface RelationStruct { @@ -82,7 +85,7 @@ export interface QuerySearchDepthStruct { max: number; } -export type FilterOperationType = 'AND' | 'OR'; +export type FilterOperationType = 'EQ' | 'NEQ' | 'GT' | 'LT' | 'GTE' | 'LTE' | 'LIKE' | 'NOT LIKE' | 'IN' | 'NOT IN'; export type LogicOperationType = 'AND' | 'OR'; /** Interface for an entity in the JSON for the query. */ diff --git a/libs/shared/lib/querybuilder/model/graphology/model.ts b/libs/shared/lib/querybuilder/model/graphology/model.ts index 650f0e48901f7b18847091279af808f215597e43..09832ac558c7ad3e344ab7b43fae76eecac7dc8d 100644 --- a/libs/shared/lib/querybuilder/model/graphology/model.ts +++ b/libs/shared/lib/querybuilder/model/graphology/model.ts @@ -2,15 +2,15 @@ import { Attributes as GAttributes } from 'graphology-types'; import { XYPosition } from 'reactflow'; import { MultiGraph } from 'graphology'; import './utils'; -import { SchemaAttribute } from '@graphpolaris/shared/lib/schema'; -import { GeneralDescription, InputNodeType } from '../logic/general'; +import { GeneralDescription, InputNodeType, InputNodeTypeTypes } from '../logic/general'; import { AllLogicTypes } from '../logic'; +import { QueryElementTypes } from '../reactflow'; // export interface Attributes extends EntityNode | RelationNode | AttributeNode | FunctionNode | ModifierNode { // } -export interface NodeAttribute extends SchemaAttribute { - handleId: string; +export interface NodeAttribute { + handleData: QueryGraphEdgeHandle; // nodeCount: number; // summedNullAmount: number; // connectedRatio: number; @@ -19,7 +19,7 @@ export interface NodeAttribute extends SchemaAttribute { export type NodeDefaults = { id?: string; - type: string; + type: QueryElementTypes; width?: number; height?: number; attributes?: NodeAttribute[]; @@ -27,18 +27,18 @@ export type NodeDefaults = { /** Interface for the data in an entity node. */ export interface EntityData { - name: string; - leftRelationHandleId?: string; - rightRelationHandleId?: string; + name?: string; + leftRelationHandleId?: QueryGraphEdgeHandle; + rightRelationHandleId?: QueryGraphEdgeHandle; } /** Interface for the data in an relation node. */ export interface RelationData { - name: string; + name?: string; collection: string; depth: { min: number; max: number }; - leftEntityHandleId?: string; - rightEntityHandleId?: string; + leftEntityHandleId?: QueryGraphEdgeHandle; + rightEntityHandleId?: QueryGraphEdgeHandle; } export interface LogicData { @@ -47,6 +47,7 @@ export interface LogicData { // logicType: AllLogicTypes; // key: string; logic: GeneralDescription<AllLogicTypes>; + inputs: Record<string, InputNodeTypeTypes>; // name from InputNode -> InputNodeTypeTypes } export type EntityNodeAttributes = XYPosition & EntityData & NodeDefaults; @@ -55,4 +56,27 @@ export type LogicNodeAttributes = XYPosition & LogicData & NodeDefaults; export type QueryGraphNodes = EntityNodeAttributes | RelationNodeAttributes | LogicNodeAttributes; -// export class QueryGraph extends MultiGraph<QueryGraphNodes, GAttributes, GAttributes>; // is in utils.ts +export type QueryGraphEdgeAttribute = { + attributeName?: string; + attributeType?: InputNodeType; +}; + +export type QueryGraphEdgeHandle = { + nodeId: string; + nodeName: string; + nodeType: QueryElementTypes; +} & QueryGraphEdgeAttribute; + +export type QueryGraphEdges = { + type: string; + sourceHandleData: QueryGraphEdgeHandle; + targetHandleData: QueryGraphEdgeHandle; +}; + +export type QueryGraphEdgesOpt = { + type?: string; + sourceHandleData?: QueryGraphEdgeHandle; + targetHandleData?: QueryGraphEdgeHandle; +}; + +// export class QueryGraph extends Graph<QueryGraphNodes, GAttributes, GAttributes>; // is in utils.ts diff --git a/libs/shared/lib/querybuilder/model/graphology/utils.ts b/libs/shared/lib/querybuilder/model/graphology/utils.ts index 027d1661b71cf339ec3482289dd759d7cf2e432b..5da3f1cd3861eb71913ab41f24e5c48eeeaca88c 100644 --- a/libs/shared/lib/querybuilder/model/graphology/utils.ts +++ b/libs/shared/lib/querybuilder/model/graphology/utils.ts @@ -1,10 +1,22 @@ -import { setQuerybuilderNodes, store } from '@graphpolaris/shared/lib/data-access/store'; +// 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 { EntityNodeAttributes, LogicNodeAttributes, QueryGraphNodes, RelationNodeAttributes } from './model'; +import { + EntityNodeAttributes, + LogicNodeAttributes, + QueryGraphEdgeAttribute, + QueryGraphEdgeHandle, + QueryGraphEdges, + QueryGraphEdgesOpt, + QueryGraphNodes, + RelationNodeAttributes, +} from './model'; import { XYPosition } from 'reactflow'; import { Handles, QueryElementTypes } from '../reactflow'; -import { getHandleId } from '..'; +import { toHandleId } from '..'; +import { s } from 'vitest/dist/env-afee91f0'; +import { SchemaAttribute, SchemaAttributeTypes } from '@graphpolaris/shared/lib/schema'; +import { InputNodeType, InputNodeTypeTypes } from '../logic/general'; /** monospace fontsize table */ const widthPerFontsize = { @@ -13,13 +25,20 @@ const widthPerFontsize = { 10: 6.0167, }; -export type QueryMultiGraph = SerializedGraph<QueryGraphNodes, GAttributes, GAttributes>; +export type AddEdge2GraphologyOptions = { + // attributeSourceHandle?: string; + sourceHandleName?: string; + targetHandleName?: string; + attributeTargetHandleName?: string; + attributeTargetHandleType?: string; +}; + +export type QueryMultiGraph = SerializedGraph<QueryGraphNodes, QueryGraphEdges, GAttributes>; -export class QueryMultiGraphology extends MultiGraph<QueryGraphNodes, GAttributes, GAttributes> { - public addPill2Graphology(attributes: QueryGraphNodes): QueryGraphNodes { +export class QueryMultiGraphology extends Graph<QueryGraphNodes, QueryGraphEdges, GAttributes> { + public configureDefaults(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 @@ -29,47 +48,161 @@ export class QueryMultiGraphology extends MultiGraph<QueryGraphNodes, GAttribute // Get the width and height of a node const { width, height } = calcWidthHeightOfPill(attributes); + attributes.x = x; + attributes.y = y; + attributes.width = width; + attributes.height = height; + + if (!attributes.id) attributes.id = 'id_' + (Date.now() + Math.floor(Math.random() * 1000)).toString(); + + return attributes; + } + + public addPill2Graphology(attributes: QueryGraphNodes, optAttributes: SchemaAttribute[] | undefined = undefined): QueryGraphNodes { + attributes = this.configureDefaults(attributes); + if (!attributes.type || !attributes.name || !attributes.id) throw Error('type or name is not defined'); + // 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) { + const defaultHandleData: QueryGraphEdgeHandle = { + nodeId: attributes.id, + nodeName: attributes.name, + nodeType: attributes.type, + }; + if (attributes.type === QueryElementTypes.Entity) { + var entityAttributes = attributes as EntityNodeAttributes; + entityAttributes.leftRelationHandleId = { ...defaultHandleData, attributeName: Handles.EntityLeft }; + entityAttributes.rightRelationHandleId = { ...defaultHandleData, attributeName: Handles.EntityRight }; + optAttributes?.forEach((optAttribute) => { + if (!entityAttributes.attributes) entityAttributes.attributes = []; + entityAttributes.attributes.push({ + handleData: { ...defaultHandleData, attributeName: optAttribute.name, attributeType: optAttribute.type }, + }); + }); + } else if (attributes.type === QueryElementTypes.Relation) { + var relationAttributes = attributes as RelationNodeAttributes; + relationAttributes.leftEntityHandleId = { ...defaultHandleData, attributeName: Handles.RelationLeft }; + relationAttributes.rightEntityHandleId = { ...defaultHandleData, attributeName: Handles.RelationRight }; + optAttributes?.forEach((optAttribute) => { + if (!relationAttributes.attributes) relationAttributes.attributes = []; + relationAttributes.attributes.push({ + handleData: { ...defaultHandleData, attributeName: optAttribute.name, attributeType: optAttribute.type }, + }); + }); + } else if (attributes.type === QueryElementTypes.Logic) { + throw Error('using wrong function! use addLogicPill2Graphology instead'); } // Add a node to the graphology object - const nodeId = this.addNode(attributes.id, { ...attributes, x, y, width, height }); + const nodeId = this.addNode(attributes.id, { ...attributes }); // Set the new nodes in the query builder slice TODO: maybe remove for efficiency - store.dispatch(setQuerybuilderNodes(this.export())); + // dispatch(setQuerybuilderNodes(this.export())); // Can't do this due to loop import return attributes; } - public addEdge2Graphology(source: QueryGraphNodes, target: QueryGraphNodes, attributes: GAttributes): string | null { + public addLogicPill2Graphology(attributes: QueryGraphNodes, inputValues: Record<string, InputNodeTypeTypes> = {}): QueryGraphNodes { + attributes = this.configureDefaults(attributes); + if (!attributes.type) attributes.type = QueryElementTypes.Logic; + if (!attributes.name || !attributes.id) throw Error('type or name is not defined'); + + if (attributes.type === QueryElementTypes.Logic) { + attributes = attributes as LogicNodeAttributes; + (attributes as LogicNodeAttributes).logic.inputs.forEach((input, i) => { + // Setup default non-linked inputs as regular values matching the input expected type + if (!(attributes as LogicNodeAttributes).inputs) (attributes as LogicNodeAttributes).inputs = {}; + (attributes as LogicNodeAttributes).inputs[input.name] = inputValues?.[input.name] || input.default; + }); + // (attributes as LogicNodeAttributes).leftEntityHandleId = getHandleId(attributes.id, name, type, Handles.RelationLeft, ''); + // (attributes as LogicNodeAttributes).rightEntityHandleId = getHandleId(attributes.id, name, type, Handles.RelationRight, ''); + } else throw Error('using wrong function! use addPill2Graphology instead'); + + // Add a node to the graphology object + const nodeId = this.addNode(attributes.id, { ...attributes }); + + // Set the new nodes in the query builder slice TODO: maybe remove for efficiency + // dispatch(setQuerybuilderNodes(this.export())); // Can't do this due to loop import + + return attributes; + } + + public addEdge2Graphology( + source: QueryGraphNodes, + target: QueryGraphNodes, + attributes: QueryGraphEdgesOpt = {}, + options: AddEdge2GraphologyOptions = {} + ): string | null { + let sourceAttributeName = ''; + let sourceAttributeType: InputNodeType | undefined; + let targetAttributeName = ''; + let targetAttributeType: InputNodeType | undefined; + + if (source.type === QueryElementTypes.Entity) { + source = source as EntityNodeAttributes; + if (!!options?.sourceHandleName) { + sourceAttributeName = options?.sourceHandleName; + sourceAttributeType = source?.attributes?.find((a) => a.handleData.attributeName === sourceAttributeName)?.handleData.attributeType; + } else { + sourceAttributeName = Handles.EntityRight; + } + } else if (source.type === QueryElementTypes.Relation) { + sourceAttributeName = Handles.RelationRight; + // } else if (source.type === QueryElementTypes.Logic && !!options?.attributeSourceHandle) { + // sourceAttributeName = Handles.LogicRight; + // sourceAttributeType = options.attributeSourceHandle; + } else if (source.type === QueryElementTypes.Logic) { + if (!options.sourceHandleName) throw Error('sourceHandleName is not defined'); + sourceAttributeName = options.sourceHandleName; + sourceAttributeType = (source as LogicNodeAttributes).logic.output.type; + if (!sourceAttributeType) throw Error(`sourceHandleName ${sourceAttributeName} does not exist!`); + } else { + throw Error('source.type is not correctly defined'); + } + + if (target.type === QueryElementTypes.Entity) { + // if (!!options?.attributeTargetHandle) { + // targetAttributeName = Handles.EntityAttribute; + // targetAttributeType = options.attributeTargetHandle; + // } else { + targetAttributeName = Handles.EntityRight; + // } + } else if (target.type === QueryElementTypes.Relation) { + targetAttributeName = Handles.RelationRight; + } else if (target.type === QueryElementTypes.Logic) { + if (!options.targetHandleName) throw Error('targetHandleName is not defined'); + targetAttributeName = options.targetHandleName; + targetAttributeType = (target as LogicNodeAttributes).logic.inputs.find((i) => i.name === targetAttributeName)?.type; + if (!targetAttributeType) throw Error(`targetHandleName ${targetAttributeName} does not exist!`); + } else { + throw Error('target.type is not correctly defined'); + } + + if (!source.id) throw Error('source.id is not defined'); + if (!target.id) throw Error('target.id is not defined'); + if (!attributes.type) attributes.type = 'connection'; // 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, - '' - ); + attributes.sourceHandleData = { + nodeId: source.id, + nodeName: source.name || '', + nodeType: source.type, + attributeName: sourceAttributeName, + attributeType: sourceAttributeType, + }; + attributes.targetHandleData = { + nodeId: target.id, + nodeName: target.name || '', + nodeType: target.type, + attributeName: targetAttributeName, + attributeType: targetAttributeType, + }; + + // console.log('newEdge', attributes, source, target); // Add an edge to the graphology object - const edgeId = this.addEdge(source.id, target.id, attributes); + const edgeId = this.addEdge(source.id, target.id, attributes as QueryGraphEdges); // Set the new nodes in the query builder slice TODO: maybe remove for efficiency - store.dispatch(setQuerybuilderNodes(this.export())); + // store.dispatch(setQuerybuilderNodes(this.export())); return edgeId; } diff --git a/libs/shared/lib/querybuilder/model/index.ts b/libs/shared/lib/querybuilder/model/index.ts index d9af8a031ab3bfe3f014b74dd7ec6d042a8efbdb..b1b8ea45ac6d0f8c90cd4e7f8ef9bdc0f288a1e3 100644 --- a/libs/shared/lib/querybuilder/model/index.ts +++ b/libs/shared/lib/querybuilder/model/index.ts @@ -1,5 +1,6 @@ -import { NodeAttribute, QueryGraphNodes } from './graphology'; -import { SchemaReactflowNode } from './reactflow'; +import { NodeAttribute, QueryGraphEdgeHandle, QueryGraphNodes } from './graphology'; +import { InputNodeType } from './logic/general'; +import { QueryElementTypes, SchemaReactflowNode } from './reactflow'; export * from './BackendQueryFormat'; export * from './graphology'; @@ -7,28 +8,27 @@ export * from './logic'; export * from './reactflow'; type ExtraProps = { extra?: string; separator?: string }; -export function getHandleId( - nodeId: string, - nodeName: string, - nodeType: string, - attributeName: string, - attributeType: string, - { extra, separator }: ExtraProps = {} -): string { - if (!extra) extra = ''; +export function toHandleId(handleData: QueryGraphEdgeHandle, separator: string = '__'): string { + // if (!extra) extra = ''; if (!separator) separator = '__'; - return [nodeId, nodeType, nodeName, attributeName, attributeType, extra].join(separator); + return [handleData.nodeId, handleData.nodeType, handleData.nodeName, handleData.attributeName, handleData.attributeType].join(separator); } -export function getHandleIdFromGraphology(node: QueryGraphNodes, attribute: NodeAttribute, options: ExtraProps = {}): string { - return getHandleId(node.id || '', node.name, node.type, attribute.name, attribute.type, options); +// export function getHandleIdFromGraphology(node: QueryGraphNodes, attribute: NodeAttribute, options: ExtraProps = {}): string { +// return toHandleId(node.id || '', node.name, node.type, attribute.name, attribute.type, options); +// } +export function handleDataFromReactflowToId(node: SchemaReactflowNode, attribute: NodeAttribute, options: ExtraProps = {}): string { + if (!node.data.name) throw Error('node.data is not defined'); + return toHandleId({ + nodeId: node.id, + nodeName: node.data.name, + nodeType: node.type as QueryElementTypes, + attributeName: attribute.handleData.attributeName, + attributeType: attribute.handleData.attributeType, + }); } -export function getHandleIdFromReactflow(node: SchemaReactflowNode, attribute: NodeAttribute, options: ExtraProps = {}): string { - return getHandleId(node.id, node.data.name, node.type, attribute.name, attribute.type, options); -} -export function fromHandleId( - handleId: string, - separator: string = '__' -): { 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 }; +export function toHandleData(handleId: string, separator: string = '__'): QueryGraphEdgeHandle { + let [nodeId, nodeType, nodeName, attributeName, attributeType] = handleId.split(separator); + const _nodeType = nodeType as QueryElementTypes; + const _attributeType = attributeType as InputNodeType; + return { nodeId, nodeType: _nodeType, nodeName, attributeName, attributeType: _attributeType }; } diff --git a/libs/shared/lib/querybuilder/model/logic/general.ts b/libs/shared/lib/querybuilder/model/logic/general.ts index c706c77c97b3d44f679edb4bab8581b266c21f0f..7963d68accbba848401df7ecbb36169e9d4835a5 100644 --- a/libs/shared/lib/querybuilder/model/logic/general.ts +++ b/libs/shared/lib/querybuilder/model/logic/general.ts @@ -1,11 +1,196 @@ -import { Position } from 'reactflow'; - +export type InputNodeTypeTypes = string | number | boolean; export type InputNodeType = 'string' | 'float' | 'int' | 'bool' | 'date' | 'time' | 'datetime' | 'duration'; +export enum MathFilterTypes { + EQUAL = '==', + NOT_EQUAL = '!=', + GREATER_THAN = '>', + LESS_THAN = '<', + GREATER_THAN_EQUAL = '>=', + LESS_THAN_EQUAL = '<=', +} + +export enum MathAggregationTypes { + AVG = 'Avg', + COUNT = 'Count', + MAX = 'Max', + MIN = 'Min', + SUM = 'Sum', +} + +export enum MathFunctionTypes { + ADD = '+', + SUBTRACT = '-', + MULTIPLY = '*', + DIVIDE = '/', + // MODULO = '%', + // POWER = '^', + // SQRT = 'Sqrt', + // ABS = 'Abs', + // LOG = 'Log', + // EXP = 'Exp', + // ROUND = 'Round', + // CEIL = 'Ceil', + // FLOOR = 'Floor', +} + +export enum StringFunctionTypes { + LOWER = 'Lower', + UPPER = 'Upper', + // CONCAT = 'Concat', + // SUBSTRING = 'Substring', + // TRIM = 'Trim', +} + +export enum StringFilterTypes { + EQUAL = '==', + NOT_EQUAL = '!=', + LIKE = 'Like', + // NOT_LIKE = 'Not Like', + // IN = 'In', + // NOT_IN = 'Not In', +} + +export enum LogicFunctionTypes { + AND = 'And', + OR = 'Or', + NOT = 'Not', +} + +// Logic +export type AndLogicStatement = ['And', AnyStatement, AnyStatement]; +export type OrLogicStatement = ['Or', AnyStatement, AnyStatement]; +export type NotLogicStatement = ['Not', AnyStatement]; +export type LogicStatement = AndLogicStatement | OrLogicStatement | NotLogicStatement; + +// Numbers +export type PlusStatement = ['+', AnyStatement, AnyStatement]; +export type MinusStatement = ['-', AnyStatement, AnyStatement]; +export type MultiplyStatement = ['*', AnyStatement, AnyStatement]; +export type DivideStatement = ['/', AnyStatement, AnyStatement]; +export type ModuloStatement = ['%', AnyStatement, AnyStatement]; +export type PowerStatement = ['^', AnyStatement, AnyStatement]; +export type SqrtStatement = ['Sqrt', AnyStatement]; +export type AbsStatement = ['Abs', AnyStatement]; +export type LogStatement = ['Log', AnyStatement]; +export type ExpStatement = ['Exp', AnyStatement]; +export type AllMathStatement = + | PlusStatement + | MinusStatement + | MultiplyStatement + | DivideStatement + | ModuloStatement + | PowerStatement + | SqrtStatement + | AbsStatement + | LogStatement + | ExpStatement; + +// Comparisons +export type EqualStatement = ['==', AnyStatement, AnyStatement]; +export type NotEqualStatement = ['!=', AnyStatement, AnyStatement]; +export type GreaterThanStatement = ['>', AnyStatement, AnyStatement]; +export type LessThanStatement = ['<', AnyStatement, AnyStatement]; +export type GreaterThanEqualStatement = ['>=', AnyStatement, AnyStatement]; +export type LessThanEqualStatement = ['<=', AnyStatement, AnyStatement]; +export type AllComparisonStatement = + | EqualStatement + | NotEqualStatement + | GreaterThanStatement + | LessThanStatement + | GreaterThanEqualStatement + | LessThanEqualStatement; + +// Strings +export type ConcatStatement = ['Concat', AnyStatement, AnyStatement]; +export type LowerStatement = ['Lower', AnyStatement]; +export type UpperStatement = ['Upper', AnyStatement]; +export type SubstringStatement = ['Substring', AnyStatement, AnyStatement, AnyStatement]; +export type TrimStatement = ['Trim', AnyStatement]; +export type AllStringStatement = ConcatStatement | LowerStatement | UpperStatement | SubstringStatement | TrimStatement; + +// Dates +export type DateStatement = ['Date', AnyStatement]; +export type YearStatement = ['Year', AnyStatement]; +export type MonthStatement = ['Month', AnyStatement]; +export type DayStatement = ['Day', AnyStatement]; +export type HourStatement = ['Hour', AnyStatement]; +export type MinuteStatement = ['Minute', AnyStatement]; +export type SecondStatement = ['Second', AnyStatement]; +export type AllDateStatement = + | DateStatement + | YearStatement + | MonthStatement + | DayStatement + | HourStatement + | MinuteStatement + | SecondStatement; + +// Aggregations +export type AvgStatement = ['Avg', AnyStatement]; +export type CountStatement = ['Count', AnyStatement]; +export type MaxStatement = ['Max', AnyStatement]; +export type MinStatement = ['Min', AnyStatement]; +export type SumStatement = ['Sum', AnyStatement]; +export type RoundStatement = ['Round', AnyStatement]; +export type CeilStatement = ['Ceil', AnyStatement]; +export type FloorStatement = ['Floor', AnyStatement]; +export type AllAggregationStatement = + | AvgStatement + | CountStatement + | MaxStatement + | MinStatement + | SumStatement + | RoundStatement + | CeilStatement + | FloorStatement; + +// Filters +export type EqualFilterStatement = ['==', AnyStatement, AnyStatement]; +export type NotEqualFilterStatement = ['!=', AnyStatement, AnyStatement]; +export type LikeFilterStatement = ['Like', AnyStatement, AnyStatement]; +export type NotLikeFilterStatement = ['Not Like', AnyStatement, AnyStatement]; +export type InFilterStatement = ['In', AnyStatement, AnyStatement]; +export type NotInFilterStatement = ['Not In', AnyStatement, AnyStatement]; +export type FilterStatement = + | EqualFilterStatement + | NotEqualFilterStatement + | LikeFilterStatement + | NotLikeFilterStatement + | InFilterStatement + | NotInFilterStatement; + +// Regular +type WithPrefix<T extends string> = `${T}${string}`; + +export type NumberStatement = number; +export type StringStatement = string; +export type BooleanStatement = boolean; +export type DateStringStatement = string; +export type ReferenceStatement = WithPrefix<'@'>; +export type RegularStatement = NumberStatement | StringStatement | BooleanStatement | DateStringStatement; +export type AllRegularStatement = RegularStatement | ReferenceStatement; + +export type AllLogicStatement = + | LogicStatement + | AllMathStatement + | AllComparisonStatement + | AllStringStatement + | AllDateStatement + | AllAggregationStatement + | FilterStatement; + +export type AnyStatement = AllLogicStatement | AllRegularStatement; + +export interface OutputNode { + name: string; + type: InputNodeType; +} + export interface InputNode { name: string; - type: InputNodeType[]; - position: Position; + type: InputNodeType; + default: number | string | boolean; } export interface GeneralDescription<T> { @@ -14,5 +199,7 @@ export interface GeneralDescription<T> { description: string; numInputs?: number; inputs: InputNode[]; + output: OutputNode; key: string; + logic: AllLogicStatement; } diff --git a/libs/shared/lib/querybuilder/model/logic/index.ts b/libs/shared/lib/querybuilder/model/logic/index.ts index 9b2dd330196c59dd261d22584884c1f92025fc5c..00f4dfeff9c0e16898eec9ed21875671f9cac1b7 100644 --- a/libs/shared/lib/querybuilder/model/logic/index.ts +++ b/libs/shared/lib/querybuilder/model/logic/index.ts @@ -1,17 +1,27 @@ -import { GeneralDescription, InputNodeType } from './general'; -import { MathFilterTypes, MathFilters } from './mathFilters'; -import { MathFunctionTypes, MathFunctions } from './mathFunctions'; -import { StringFilterTypes, StringFilters } from './stringFilters'; -import { StringFunctionTypes, StringFunctions } from './stringFunctions'; +import { + GeneralDescription, + InputNodeType, + MathFunctionTypes, + MathFilterTypes, + StringFilterTypes, + StringFunctionTypes, + MathAggregationTypes, +} from './general'; +import { MathAggregations } from './mathAggregations'; +import { MathFilters } from './mathFilters'; +import { MathFunctions } from './mathFunctions'; +import { StringFilters } from './stringFilters'; +import { StringFunctions } from './stringFunctions'; -export type AllLogicTypes = MathFilterTypes | MathFunctionTypes | StringFilterTypes | StringFunctionTypes; +export type AllLogicTypes = MathFilterTypes | MathFunctionTypes | MathAggregationTypes | StringFilterTypes | StringFunctionTypes; export type AllLogicDescriptions = GeneralDescription<AllLogicTypes>; export const AllLogicMap: Record<string, AllLogicDescriptions> = { - ...Object.assign({}, ...MathFilters.map((d) => ({ [d.key]: d }))), - ...Object.assign({}, ...MathFunctions.map((d) => ({ [d.key]: d }))), - ...Object.assign({}, ...StringFilters.map((d) => ({ [d.key]: d }))), - ...Object.assign({}, ...StringFunctions.map((d) => ({ [d.key]: d }))), + ...Object.fromEntries(Object.values(MathFilters).map((x) => [x.key, x])), + ...Object.fromEntries(Object.values(MathFunctions).map((x) => [x.key, x])), + ...Object.fromEntries(Object.values(MathAggregations).map((x) => [x.key, x])), + ...Object.fromEntries(Object.values(StringFilters).map((x) => [x.key, x])), + ...Object.fromEntries(Object.values(StringFunctions).map((x) => [x.key, x])), }; export * from './graphFunctions'; diff --git a/libs/shared/lib/querybuilder/model/logic/mathAggregations.tsx b/libs/shared/lib/querybuilder/model/logic/mathAggregations.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5baebb3b616deeca5186e454d524bc2f07fa6e3e --- /dev/null +++ b/libs/shared/lib/querybuilder/model/logic/mathAggregations.tsx @@ -0,0 +1,745 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ + +import { Position } from 'reactflow'; +import { GeneralDescription, MathAggregationTypes } from './general'; + +export const MathAggregations: Record<MathAggregationTypes, GeneralDescription<MathAggregationTypes>> = { + [MathAggregationTypes.AVG]: { + key: 'mathFunctionAvg', + name: 'Average', + type: MathAggregationTypes.AVG, + description: 'Average of all values', + numInputs: 1, + inputs: [{ name: '1', type: 'float', default: 0 }], + output: { name: 'avg', type: 'float' }, + logic: ['Avg', '@1'], + }, + [MathAggregationTypes.COUNT]: { + key: 'mathFunctionCount', + name: 'Count', + type: MathAggregationTypes.COUNT, + description: 'Count the number of values', + numInputs: 1, + inputs: [{ name: '1', type: 'float', default: 0 }], + + output: { name: 'count', type: 'float' }, + logic: ['Count', '@1'], + }, + [MathAggregationTypes.MAX]: { + key: 'mathFunctionMax', + name: 'Maximum', + type: MathAggregationTypes.MAX, + description: 'Maximum of all values', + numInputs: 1, + inputs: [{ name: '1', type: 'float', default: 0 }], + output: { name: 'max', type: 'float' }, + logic: ['Max', '@1'], + }, + [MathAggregationTypes.MIN]: { + key: 'mathFunctionMin', + name: 'Minimum', + type: MathAggregationTypes.MIN, + description: 'Minimum of all values', + numInputs: 1, + inputs: [{ name: '1', type: 'float', default: 0 }], + output: { name: 'min', type: 'float' }, + logic: ['Min', '@1'], + }, + [MathAggregationTypes.SUM]: { + key: 'mathFunctionSum', + name: 'Sum', + type: MathAggregationTypes.SUM, + description: 'Sum of all values', + numInputs: 1, + inputs: [{ name: '1', type: 'float', default: 0 }], + output: { name: 'sum', type: 'float' }, + logic: ['Sum', '@1'], + }, + // [MathAggregationTypes.STD]: { + // key: 'mathFunctionStd', + // name: 'Standard Deviation', + // type: MathAggregationTypes.STD, + // description: 'Standard deviation of all values', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'std', type: 'float' }, + // logic: ['Std', '@1'], + // }, + // [MathAggregationTypes.ADD]: { + // key: 'mathFunctionAdd', + // name: 'Add', + // type: MathAggregationTypes.ADD, + // description: 'Add two values', + // numInputs: 2, + // inputs: [ + // { name: '1', type: 'float', default: 0 }, + // { name: '2', type: 'float', default: 0 }, + // ], + // output: { name: '+', type: 'float' }, + // logic: ['+', '@1', '@2'], + // }, + // [MathAggregationTypes.SUBTRACT]: { + // key: 'mathFunctionSubtract', + // name: 'Subtract', + // type: MathAggregationTypes.SUBTRACT, + // description: 'Subtract two values', + // numInputs: 2, + // inputs: [ + // { name: '1', type: 'float' }, + // { name: '2', type: 'float' }, + // ], + // output: { name: '-', type: 'float' }, + // logic: ['-', '@1', '@2'], + // }, + // [MathAggregationTypes.MULTIPLY]: { + // key: 'mathFunctionMultiply', + // name: 'Multiply', + // type: MathAggregationTypes.MULTIPLY, + // description: 'Multiply two values', + // numInputs: 2, + // inputs: [ + // { name: '1', type: 'float' }, + // { name: '2', type: 'float' }, + // ], + // output: { name: '*', type: 'float' }, + // logic: ['*', '@1', '@2'], + // }, + // [MathAggregationTypes.DIVIDE]: { + // key: 'mathFunctionDivide', + // name: 'Divide', + // type: MathAggregationTypes.DIVIDE, + // description: 'Divide two values', + + // numInputs: 2, + // inputs: [ + // { name: '1', type: 'float' }, + // { name: '2', type: 'float' }, + // ], + // output: { name: '/', type: 'float' }, + // logic: ['/', '@1', '@2'], + // }, + // [MathAggregationTypes.POWER]: { + // key: 'mathFunctionPower', + // name: 'Power', + // type: MathAggregationTypes.POWER, + // description: 'Raise a value to the power of another value', + // numInputs: 2, + // inputs: [ + // { name: '1', type: 'float' }, + // { name: '2', type: 'float' }, + // ], + // output: { name: '^', type: 'float' }, + // logic: ['^', '@1', '@2'], + // }, + // [MathAggregationTypes.SQRT]: { + // key: 'mathFunctionSqrt', + // name: 'Square Root', + // type: MathAggregationTypes.SQRT, + // description: 'Square root of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'sqrt', type: 'float' }, + // logic: ['Sqrt', '@1'], + // }, + // [MathAggregationTypes.LOG]: { + // key: 'mathFunctionLog', + // name: 'Logarithm', + // type: MathAggregationTypes.LOG, + // description: 'Logarithm of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'log', type: 'float' }, + // logic: ['Log', '@1'], + // }, + // [MathAggregationTypes.EXP]: { + // key: 'mathFunctionExp', + // name: 'Exponential', + // type: MathAggregationTypes.EXP, + // description: 'Exponential of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'exp', type: 'float' }, + // logic: ['Exp', '@1'], + // }, + // [MathAggregationTypes.ABS]: { + // key: 'mathFunctionAbs', + // name: 'Absolute Value', + // type: MathAggregationTypes.ABS, + // description: 'Absolute value of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'abs', type: 'float' }, + // logic: ['Abs', '@1'], + // }, + // [MathAggregationTypes.CEIL]: { + // key: 'mathFunctionCeil', + // name: 'Ceiling', + // type: MathAggregationTypes.CEIL, + // description: 'Ceiling of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'ceil', type: 'float' }, + // logic: ['Ceil', '@1'], + // }, + // [MathAggregationTypes.FLOOR]: { + // key: 'mathFunctionFloor', + // name: 'Floor', + // type: MathAggregationTypes.FLOOR, + // description: 'Floor of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'floor', type: 'float' }, + // logic: ['Floor', '@1'], + // }, + // [MathAggregationTypes.ROUND]: { + // key: 'mathFunctionRound', + // name: 'Round', + // type: MathAggregationTypes.ROUND, + // description: 'Round a value to the nearest integer', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'round', type: 'float' }, + // logic: ['Round', '@1'], + // }, + // [MathAggregationTypes.TRUNC]: { + // key: 'mathFunctionTrunc', + // name: 'Truncate', + // type: MathAggregationTypes.TRUNC, + // description: 'Truncate a value to the nearest integer towards zero', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + + // output: { name: 'trunc', type: 'float' }, + // logic: ['Trunc', '@1'], + // }, + // [MathAggregationTypes.RANDOM]: { + // key: 'mathFunctionRandom', + // name: 'Random', + // type: MathAggregationTypes.RANDOM, + // description: 'Random value between 0 and 1', + // numInputs: 0, + // inputs: [], + // output: { name: 'random', type: 'float' }, + // logic: ['Random'], + // }, + // [MathAggregationTypes.RANDOMINT]: { + // key: 'mathFunctionRandomInt', + // name: 'Random Integer', + // type: MathAggregationTypes.RANDOMINT, + // description: 'Random integer between two values', + // numInputs: 2, + // inputs: [ + // { name: 'min', type: 'float' }, + // { name: 'max', type: 'float' }, + // ], + // output: { name: 'randomInt', type: 'float' }, + // logic: ['RandomInt', '@1', '@2'], + // }, + // [MathAggregationTypes.SIN]: { + // key: 'mathFunctionSin', + // name: 'Sine', + // type: MathAggregationTypes.SIN, + // description: 'Sine of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'sin', type: 'float' }, + // logic: ['Sin', '@1'], + // }, + // [MathAggregationTypes.COS]: { + // key: 'mathFunctionCos', + // name: 'Cosine', + // type: MathAggregationTypes.COS, + // description: 'Cosine of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'cos', type: 'float' }, + // logic: ['Cos', '@1'], + // }, + // [MathAggregationTypes.TAN]: { + // key: 'mathFunctionTan', + // name: 'Tangent', + // type: MathAggregationTypes.TAN, + // description: 'Tangent of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'tan', type: 'float' }, + // logic: ['Tan', '@1'], + // }, + // [MathAggregationTypes.ASIN]: { + // key: 'mathFunctionAsin', + // name: 'Arcsine', + // type: MathAggregationTypes.ASIN, + // description: 'Arcsine of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'asin', type: 'float' }, + // logic: ['Asin', '@1'], + // }, + // [MathAggregationTypes.ACOS]: { + // key: 'mathFunctionAcos', + // name: 'Arccosine', + // type: MathAggregationTypes.ACOS, + // description: 'Arccosine of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'acos', type: 'float' }, + // logic: ['Acos', '@1'], + // }, + // [MathAggregationTypes.ATAN]: { + // key: 'mathFunctionAtan', + // name: 'Arctangent', + // type: MathAggregationTypes.ATAN, + // description: 'Arctangent of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'atan', type: 'float' }, + // logic: ['Atan', '@1'], + // }, + // [MathAggregationTypes.SINH]: { + // key: 'mathFunctionSinh', + // name: 'Hyperbolic Sine', + // type: MathAggregationTypes.SINH, + // description: 'Hyperbolic sine of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'sinh', type: 'float' }, + // logic: ['Sinh', '@1'], + // }, + // [MathAggregationTypes.COSH]: { + // key: 'mathFunctionCosh', + // name: 'Hyperbolic Cosine', + // type: MathAggregationTypes.COSH, + // description: 'Hyperbolic cosine of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'cosh', type: 'float' }, + // logic: ['Cosh', '@1'], + // }, + // [MathAggregationTypes.TANH]: { + // key: 'mathFunctionTanh', + // name: 'Hyperbolic Tangent', + // type: MathAggregationTypes.TANH, + // description: 'Hyperbolic tangent of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'tanh', type: 'float' }, + // logic: ['Tanh', '@1'], + // }, + // [MathAggregationTypes.ASINH]: { + // key: 'mathFunctionAsinh', + // name: 'Hyperbolic Arcsine', + // type: MathAggregationTypes.ASINH, + // description: 'Hyperbolic arcsine of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'asinh', type: 'float' }, + // logic: ['Asinh', '@1'], + // }, + // [MathAggregationTypes.ACOSH]: { + // key: 'mathFunctionAcosh', + // name: 'Hyperbolic Arccosine', + // type: MathAggregationTypes.ACOSH, + // description: 'Hyperbolic arccosine of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'acosh', type: 'float' }, + // logic: ['Acosh', '@1'], + // }, + // [MathAggregationTypes.ATANH]: { + // key: 'mathFunctionAtanh', + // name: 'Hyperbolic Arctangent', + // type: MathAggregationTypes.ATANH, + // description: 'Hyperbolic arctangent of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'atanh', type: 'float' }, + // logic: ['Atanh', '@1'], + // }, + // [MathAggregationTypes.DEGREES]: { + // key: 'mathFunctionDegrees', + // name: 'Degrees', + // type: MathAggregationTypes.DEGREES, + // description: 'Convert a value from radians to degrees', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'degrees', type: 'float' }, + // logic: ['Degrees', '@1'], + // }, + // [MathAggregationTypes.RADIANS]: { + // key: 'mathFunctionRadians', + // name: 'Radians', + // type: MathAggregationTypes.RADIANS, + // description: 'Convert a value from degrees to radians', + // numInputs: 1, + + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'radians', type: 'float' }, + // logic: ['Radians', '@1'], + // }, + // [MathAggregationTypes.SIGN]: { + // key: 'mathFunctionSign', + // name: 'Sign', + // type: MathAggregationTypes.SIGN, + // description: 'Sign of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'sign', type: 'float' }, + // logic: ['Sign', '@1'], + // }, + // [MathAggregationTypes.RANDOMNORMAL]: { + // key: 'mathFunctionRandomNormal', + // name: 'Random Normal', + // type: MathAggregationTypes.RANDOMNORMAL, + // description: 'Random value from a normal distribution', + // numInputs: 2, + // inputs: [ + // { name: 'mean', type: 'float' }, + // { name: 'std', type: 'float' }, + // ], + // output: { name: 'randomNormal', type: 'float' }, + // logic: ['RandomNormal', '@1', '@2'], + // }, + // [MathAggregationTypes.RANDOMLOGNORMAL]: { + // key: 'mathFunctionRandomLogNormal', + // name: 'Random Log Normal', + // type: MathAggregationTypes.RANDOMLOGNORMAL, + // description: 'Random value from a log normal distribution', + // numInputs: 2, + // inputs: [ + // { name: 'mean', type: 'float' }, + // { name: 'std', type: 'float' }, + // ], + // output: { name: 'randomLogNormal', type: 'float' }, + // logic: ['RandomLogNormal', '@1', '@2'], + // }, + // [MathAggregationTypes.RANDOMEXPONENTIAL]: { + // key: 'mathFunctionRandomExponential', + // name: 'Random Exponential', + // type: MathAggregationTypes.RANDOMEXPONENTIAL, + // description: 'Random value from an exponential distribution', + // numInputs: 1, + // inputs: [{ name: 'lambda', type: 'float' }], + // output: { name: 'randomExponential', type: 'float' }, + // logic: ['RandomExponential', '@1'], + // }, + // [MathAggregationTypes.RANDOMGAMMA]: { + // key: 'mathFunctionRandomGamma', + // name: 'Random Gamma', + // type: MathAggregationTypes.RANDOMGAMMA, + // description: 'Random value from a gamma distribution', + // numInputs: 2, + // inputs: [ + // { name: 'alpha', type: 'float' }, + // { name: 'beta', type: 'float' }, + // ], + // output: { name: 'randomGamma', type: 'float' }, + // logic: ['RandomGamma', '@1', '@2'], + // }, + // [MathAggregationTypes.RANDOMBETA]: { + // key: 'mathFunctionRandomBeta', + // name: 'Random Beta', + // type: MathAggregationTypes.RANDOMBETA, + // description: 'Random value from a beta distribution', + // numInputs: 2, + // inputs: [ + // { name: 'alpha', type: 'float' }, + // { name: 'beta', type: 'float' }, + // ], + // output: { name: 'randomBeta', type: 'float' }, + // logic: ['RandomBeta', '@1', '@2'], + // }, + // [MathAggregationTypes.RANDOMCHISQUARE]: { + // key: 'mathFunctionRandomChiSquare', + // name: 'Random Chi Square', + // type: MathAggregationTypes.RANDOMCHISQUARE, + // description: 'Random value from a chi square distribution', + // numInputs: 1, + // inputs: [{ name: 'k', type: 'float' }], + // output: { name: 'randomChiSquare', type: 'float' }, + // logic: ['RandomChiSquare', '@1'], + // }, + // [MathAggregationTypes.RANDOMWEIBULL]: { + // key: 'mathFunctionRandomWeibull', + // name: 'Random Weibull', + // type: MathAggregationTypes.RANDOMWEIBULL, + // description: 'Random value from a Weibull distribution', + // numInputs: 2, + // inputs: [ + // { name: 'k', type: 'float' }, + // { name: 'lambda', type: 'float' }, + // ], + // output: { name: 'randomWeibull', type: 'float' }, + // logic: ['RandomWeibull', '@1', '@2'], + // }, + // [MathAggregationTypes.RANDOMCAUCHY]: { + // key: 'mathFunctionRandomCauchy', + // name: 'Random Cauchy', + // type: MathAggregationTypes.RANDOMCAUCHY, + // description: 'Random value from a Cauchy distribution', + // numInputs: 2, + // inputs: [ + // { name: 'x0', type: 'float' }, + // { name: 'gamma', type: 'float' }, + // ], + // output: { name: 'randomCauchy', type: 'float' }, + // logic: ['RandomCauchy', '@1', '@2'], + // }, + // [MathAggregationTypes.RANDOMPOISSON]: { + // key: 'mathFunctionRandomPoisson', + // name: 'Random Poisson', + // type: MathAggregationTypes.RANDOMPOISSON, + // description: 'Random value from a Poisson distribution', + // numInputs: 1, + // inputs: [{ name: 'lambda', type: 'float' }], + // output: { name: 'randomPoisson', type: 'float' }, + // logic: ['RandomPoisson', '@1'], + // }, + // [MathAggregationTypes.RANDOMIRWINHALL]: { + // key: 'mathFunctionRandomIrwinHall', + // name: 'Random Irwin Hall', + // type: MathAggregationTypes.RANDOMIRWINHALL, + // description: 'Random value from an Irwin Hall distribution', + // numInputs: 2, + // inputs: [ + // { name: 'n', type: 'float' }, + // { name: 'scale', type: 'float' }, + // ], + // output: { name: 'randomIrwinHall', type: 'float' }, + // logic: ['RandomIrwinHall', '@1', '@2'], + // }, + // [MathAggregationTypes.CHIQUARETEST]: { + // key: 'mathFunctionChiSquareTest', + // name: 'Chi Square Test', + // type: MathAggregationTypes.CHIQUARETEST, + // description: 'Chi square test', + // numInputs: 2, + // inputs: [ + // { name: 'observed', type: 'float' }, + // { name: 'expected', type: 'float' }, + // ], + // output: { name: 'chiSquareTest', type: 'float' }, + // logic: ['ChiSquareTest', '@1', '@2'], + // }, + // [MathAggregationTypes.CORRELATION]: { + // key: 'mathFunctionCorrelation', + // name: 'Correlation', + // type: MathAggregationTypes.CORRELATION, + // description: 'Correlation between two values', + // numInputs: 2, + // inputs: [ + // { name: '1', type: 'float' }, + // { name: '2', type: 'float' }, + // ], + // output: { name: 'correlation', type: 'float' }, + // logic: ['Correlation', '@1', '@2'], + // }, + // [MathAggregationTypes.COVARIANCE]: { + // key: 'mathFunctionCovariance', + // name: 'Covariance', + // type: MathAggregationTypes.COVARIANCE, + // description: 'Covariance between two values', + // numInputs: 2, + // inputs: [ + // { name: '1', type: 'float' }, + // { name: '2', type: 'float' }, + // ], + // output: { name: 'covariance', type: 'float' }, + // logic: ['Covariance', '@1', '@2'], + // }, + // [MathAggregationTypes.FREQUENCY]: { + // key: 'mathFunctionFrequency', + // name: 'Frequency', + // type: MathAggregationTypes.FREQUENCY, + // description: 'Frequency of a value in a dataset', + // numInputs: 2, + // inputs: [ + // { name: 'value', type: 'float' }, + // { name: 'dataset', type: 'float' }, + // ], + // output: { name: 'frequency', type: 'float' }, + // logic: ['Frequency', '@1', '@2'], + // }, + // [MathAggregationTypes.MEAN]: { + // key: 'mathFunctionMean', + // name: 'Mean', + // type: MathAggregationTypes.MEAN, + // description: 'Mean of a dataset', + // numInputs: 1, + // inputs: [{ name: 'dataset', type: 'float' }], + // output: { name: 'mean', type: 'float' }, + // logic: ['Mean', '@1'], + // }, + // [MathAggregationTypes.MEDIAN]: { + // key: 'mathFunctionMedian', + // name: 'Median', + // type: MathAggregationTypes.MEDIAN, + // description: 'Median of a dataset', + // numInputs: 1, + // inputs: [{ name: 'dataset', type: 'float' }], + // output: { name: 'median', type: 'float' }, + // logic: ['Median', '@1'], + // }, + // [MathAggregationTypes.MODE]: { + // key: 'mathFunctionMode', + // name: 'Mode', + // type: MathAggregationTypes.MODE, + // description: 'Mode of a dataset', + // numInputs: 1, + // inputs: [{ name: 'dataset', type: 'float' }], + // output: { name: 'mode', type: 'float' }, + // logic: ['Mode', '@1'], + // }, + // [MathAggregationTypes.RANK]: { + // key: 'mathFunctionRank', + // name: 'Rank', + // type: MathAggregationTypes.RANK, + // description: 'Rank of a value in a dataset', + // numInputs: 2, + // inputs: [ + // { name: 'value', type: 'float' }, + // { name: 'dataset', type: 'float' }, + // ], + // output: { name: 'rank', type: 'float' }, + // logic: ['Rank', '@1', '@2'], + // }, + // [MathAggregationTypes.STDEV]: { + // key: 'mathFunctionStdev', + // name: 'Standard Deviation', + // type: MathAggregationTypes.STDEV, + // description: 'Standard deviation of a dataset', + // numInputs: 1, + // inputs: [{ name: 'dataset', type: 'float' }], + // output: { name: 'stdev', type: 'float' }, + // logic: ['Stdev', '@1'], + // }, + // [MathAggregationTypes.VARIANCE]: { + // key: 'mathFunctionVariance', + // name: 'Variance', + // type: MathAggregationTypes.VARIANCE, + // description: 'Variance of a dataset', + // numInputs: 1, + // inputs: [{ name: 'dataset', type: 'float' }], + // output: { name: 'variance', type: 'float' }, + // logic: ['Variance', '@1'], + // }, + // [MathAggregationTypes.ZSCORE]: { + // key: 'mathFunctionZscore', + // name: 'Z-Score', + // type: MathAggregationTypes.ZSCORE, + // description: 'Z-score of a value in a dataset', + // numInputs: 2, + // inputs: [ + // { name: 'value', type: 'float' }, + // { name: 'dataset', type: 'float' }, + // ], + // output: { name: 'zscore', type: 'float' }, + // logic: ['Zscore', '@1', '@2'], + // }, + // [MathAggregationTypes.AND]: { + // key: 'mathFunctionAnd', + // name: 'And', + // type: MathAggregationTypes.AND, + // description: 'Logical AND of two values', + // numInputs: 2, + // inputs: [ + // { name: '1', type: 'boolean' }, + // { name: '2', type: 'boolean' }, + // ], + // output: { name: 'and', type: 'boolean' }, + // logic: ['And', '@1', '@2'], + // }, + // [MathAggregationTypes.OR]: { + // key: 'mathFunctionOr', + // name: 'Or', + // type: MathAggregationTypes.OR, + // description: 'Logical OR of two values', + // numInputs: 2, + // inputs: [ + // { name: '1', type: 'boolean' }, + // { name: '2', type: 'boolean' }, + // ], + // output: { name: 'or', type: 'boolean' }, + // logic: ['Or', '@1', '@2'], + // }, + // [MathAggregationTypes.NOT]: { + // key: 'mathFunctionNot', + // name: 'Not', + // type: MathAggregationTypes.NOT, + // description: 'Logical NOT of a value', + // numInputs: 1, + // inputs: [{ name: '1', type: 'boolean' }], + // output: { name: 'not', type: 'boolean' }, + // logic: ['Not', '@1'], + // }, + + // { + // key: 'mathFunctionCount', + // name: 'Count', + // type: MathAggregationTypes.COUNT, + // description: 'Count the number of values', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'count', type: 'float' }, + // logic: ['Count', '@1'], + // }, + // { + // key: 'mathFunctionMax', + // name: 'Maximum', + // type: MathAggregationTypes.MAX, + // description: 'Maximum of all values', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'max', type: 'float' }, + // logic: ['Max', '@1'], + // }, + // { + // key: 'mathFunctionMin', + // name: 'Minimum', + // type: MathAggregationTypes.MIN, + // description: 'Minimum of all values', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'min', type: 'float' }, + // logic: ['Min', '@1'], + // }, + // { + // key: 'mathFunctionSum', + // name: 'Sum', + // type: MathAggregationTypes.SUM, + // description: 'Sum of all values', + // numInputs: 1, + // inputs: [{ name: '1', type: 'float' }], + // output: { name: 'sum', type: 'float' }, + // logic: ['Sum', '@1'], + // }, + // // { + // // key: 'mathFunctionStd', + // // name: 'Standard Deviation', + // // type: MathAggregationTypes.STD, + // // description: 'Standard deviation of all values', + // // numInputs: 1, + // // inputs: [{ name: '1', type: 'float' }], + // // output: { name: 'std', type: 'float' }, + // // logic: ['Std', '@1'], + // // }, + // { + // key: 'mathFunctionAdd', + // name: 'Add', + // type: MathAggregationTypes.ADD, + // description: 'Add two values', + // numInputs: 2, + // inputs: [ + // { name: '1', type: 'float' }, + // { name: '2', type: 'float' }, + // ], + // output: { name: '+', type: 'float' }, + // logic: ['+', '@1', '@2'], + // }, +}; + +/** All available functions in the function bar. */ +export const MathAggregationArray: Array<GeneralDescription<MathAggregationTypes>> = Object.values(MathAggregations); diff --git a/libs/shared/lib/querybuilder/model/logic/mathFilters.tsx b/libs/shared/lib/querybuilder/model/logic/mathFilters.tsx index 7c86cdb195a974cb945019d1612cf908ee274176..0c235dd780eddf0da0e5f0f2f62d3ad494cd9396 100644 --- a/libs/shared/lib/querybuilder/model/logic/mathFilters.tsx +++ b/libs/shared/lib/querybuilder/model/logic/mathFilters.tsx @@ -5,39 +5,89 @@ */ import { Position } from 'reactflow'; -import { GeneralDescription, InputNode } from './general'; +import { GeneralDescription, InputNode, MathFilterTypes } from './general'; -export enum MathFilterTypes { - EQUAL = '==', - NOT_EQUAL = '!=', - GREATER_THAN = '>', - LESS_THAN = '<', - GREATER_THAN_EQUAL = '>=', - LESS_THAN_EQUAL = '<=', -} - -/** All available functions in the function bar. */ -export const MathFilters: Array<GeneralDescription<MathFilterTypes>> = [ - { +export const MathFilters: Record<MathFilterTypes, GeneralDescription<MathFilterTypes>> = { + [MathFilterTypes.EQUAL]: { key: 'mathFilterEqual', name: 'Equal', type: MathFilterTypes.EQUAL, description: 'Equal to another value', numInputs: 2, inputs: [ - { name: '1', type: ['float'], position: Position.Left }, - { name: '2', type: ['float'], position: Position.Left }, + { name: '1', type: 'float', default: 0 }, + { name: '2', type: 'float', default: 0 }, ], + output: { name: '==', type: 'float' }, + logic: ['==', '@1', '@2'], }, - { + [MathFilterTypes.NOT_EQUAL]: { key: 'mathFilterNotEqual', name: 'Not Equal', type: MathFilterTypes.NOT_EQUAL, description: 'Not equal to another value', numInputs: 2, inputs: [ - { name: '1', type: ['float'], position: Position.Left }, - { name: '2', type: ['float'], position: Position.Left }, + { name: '1', type: 'float', default: 0 }, + { name: '2', type: 'float', default: 0 }, + ], + output: { name: '!=', type: 'float' }, + logic: ['!=', '@1', '@2'], + }, + [MathFilterTypes.LESS_THAN]: { + key: 'mathFilterLessThan', + name: 'Less Than', + type: MathFilterTypes.LESS_THAN, + description: 'Less than another value', + numInputs: 2, + inputs: [ + { name: '1', type: 'float', default: 0 }, + { name: '2', type: 'float', default: 0 }, ], + output: { name: '<', type: 'float' }, + logic: ['<', '@1', '@2'], }, -]; + [MathFilterTypes.LESS_THAN_EQUAL]: { + key: 'mathFilterLessThanOrEqual', + name: 'Less Than or Equal', + type: MathFilterTypes.LESS_THAN_EQUAL, + description: 'Less than or equal to another value', + numInputs: 2, + inputs: [ + { name: '1', type: 'float', default: 0 }, + { name: '2', type: 'float', default: 0 }, + ], + output: { name: '<=', type: 'float' }, + logic: ['<=', '@1', '@2'], + }, + [MathFilterTypes.GREATER_THAN]: { + key: 'mathFilterGreaterThan', + name: 'Greater Than', + type: MathFilterTypes.GREATER_THAN, + description: 'Greater than another value', + + numInputs: 2, + inputs: [ + { name: '1', type: 'float', default: 0 }, + { name: '2', type: 'float', default: 0 }, + ], + output: { name: '>', type: 'float' }, + logic: ['>', '@1', '@2'], + }, + [MathFilterTypes.GREATER_THAN_EQUAL]: { + key: 'mathFilterGreaterThanOrEqual', + name: 'Greater Than or Equal', + type: MathFilterTypes.GREATER_THAN_EQUAL, + description: 'Greater than or equal to another value', + numInputs: 2, + inputs: [ + { name: '1', type: 'float', default: 0 }, + { name: '2', type: 'float', default: 0 }, + ], + output: { name: '>=', type: 'float' }, + logic: ['>=', '@1', '@2'], + }, +}; + +/** All available functions in the function bar. */ +export const MathFilterArray: Array<GeneralDescription<MathFilterTypes>> = Object.values(MathFilters); diff --git a/libs/shared/lib/querybuilder/model/logic/mathFunctions.tsx b/libs/shared/lib/querybuilder/model/logic/mathFunctions.tsx index 1e0fce8d747d147bae39da6472ce4253ef299552..5375664aa5ef832c5ca60840f7e5c829d526c67f 100644 --- a/libs/shared/lib/querybuilder/model/logic/mathFunctions.tsx +++ b/libs/shared/lib/querybuilder/model/logic/mathFunctions.tsx @@ -5,47 +5,63 @@ */ import { Position } from 'reactflow'; -import { GeneralDescription } from './general'; +import { GeneralDescription, MathFunctionTypes } from './general'; -export enum MathFunctionTypes { - AVG = 'AVG', - COUNT = 'COUNT', - MAX = 'MAX', - MIN = 'MIN', - SUM = 'SUM', - ROUND = 'ROUND', - CEIL = 'CEIL', - FLOOR = 'FLOOR', - ADD = '+', - SUBTRACT = '-', - MULTIPLY = '*', - DIVIDE = '/', - MODULO = '%', - CUSTOM = 'CUSTOM', -} - -/** All available functions in the function bar. */ -export const MathFunctions: Array<GeneralDescription<MathFunctionTypes>> = [ - { - key: 'mathFunctionAvg', - name: 'Average', - type: MathFunctionTypes.AVG, - description: 'Average of all values', - numInputs: 1, +export const MathFunctions: Record<MathFunctionTypes, GeneralDescription<MathFunctionTypes>> = { + [MathFunctionTypes.ADD]: { + key: 'mathFunctionAdd', + name: 'Add', + type: MathFunctionTypes.ADD, + description: 'Add two values', + numInputs: 2, + inputs: [ + { name: '1', type: 'float', default: 0 }, + { name: '2', type: 'float', default: 0 }, + ], + output: { name: '+', type: 'float' }, + logic: ['+', '@1', '@2'], + }, + [MathFunctionTypes.SUBTRACT]: { + key: 'mathFunctionSubtract', + name: 'Subtract', + type: MathFunctionTypes.SUBTRACT, + description: 'Subtract two values', + numInputs: 2, inputs: [ - { name: '1', type: ['float'], position: Position.Left }, - { name: 'out', type: ['float'], position: Position.Right }, + { name: '1', type: 'float', default: 0 }, + { name: '2', type: 'float', default: 0 }, ], + output: { name: '-', type: 'float' }, + logic: ['-', '@1', '@2'], }, - { - key: 'mathFunctionCount', - name: 'Count', - type: MathFunctionTypes.COUNT, - description: 'Count the number of values', - numInputs: 1, + [MathFunctionTypes.MULTIPLY]: { + key: 'mathFunctionMultiply', + name: 'Multiply', + type: MathFunctionTypes.MULTIPLY, + description: 'Multiply two values', + numInputs: 2, inputs: [ - { name: '1', type: ['float'], position: Position.Left }, - { name: 'out', type: ['float'], position: Position.Right }, + { name: '1', type: 'float', default: 0 }, + { name: '2', type: 'float', default: 0 }, ], + output: { name: '*', type: 'float' }, + logic: ['*', '@1', '@2'], }, -]; + [MathFunctionTypes.DIVIDE]: { + key: 'mathFunctionDivide', + name: 'Divide', + type: MathFunctionTypes.DIVIDE, + description: 'Divide two values', + + numInputs: 2, + inputs: [ + { name: '1', type: 'float', default: 0 }, + { name: '2', type: 'float', default: 0 }, + ], + output: { name: '/', type: 'float' }, + logic: ['/', '@1', '@2'], + }, +}; + +/** All available functions in the function bar. */ +export const MathFunctionArray: Array<GeneralDescription<MathFunctionTypes>> = Object.values(MathFunctions); diff --git a/libs/shared/lib/querybuilder/model/logic/stringFilters.tsx b/libs/shared/lib/querybuilder/model/logic/stringFilters.tsx index db8b52b3cf21dd985407f246aaec07e7d284d2bb..5799d1804e9511231b0d6fcd68f233cb6a135c9e 100644 --- a/libs/shared/lib/querybuilder/model/logic/stringFilters.tsx +++ b/libs/shared/lib/querybuilder/model/logic/stringFilters.tsx @@ -5,28 +5,62 @@ */ import { Position } from 'reactflow'; -import { GeneralDescription } from './general'; +import { GeneralDescription, StringFilterTypes } from './general'; -export enum StringFilterTypes { - EQUAL = '==', - NOT_EQUAL = '!=', - LIKE = 'LIKE', - NOT_LIKE = 'NOT LIKE', - IN = 'IN', - NOT_IN = 'NOT IN', -} - -/** All available functions in the function bar. */ -export const StringFilters: Array<GeneralDescription<StringFilterTypes>> = [ - { +export const StringFilters: Record<StringFilterTypes, GeneralDescription<StringFilterTypes>> = { + [StringFilterTypes.EQUAL]: { key: 'stringFilterEqual', name: 'Equal', type: StringFilterTypes.EQUAL, description: 'Equal to another value', numInputs: 1, inputs: [ - { name: '1', type: ['string'], position: Position.Left }, - { name: '2', type: ['string'], position: Position.Left }, + { name: '1', type: 'string', default: '' }, + { name: '2', type: 'string', default: '' }, + ], + output: { name: StringFilterTypes.EQUAL, type: 'bool' }, + logic: [StringFilterTypes.EQUAL, '@1', '@2'], + }, + [StringFilterTypes.NOT_EQUAL]: { + key: 'stringFilterNotEqual', + name: 'Not Equal', + type: StringFilterTypes.NOT_EQUAL, + description: 'Not equal to another value', + numInputs: 1, + inputs: [ + { name: '1', type: 'string', default: '' }, + { name: '2', type: 'string', default: '' }, ], + output: { name: StringFilterTypes.NOT_EQUAL, type: 'bool' }, + logic: [StringFilterTypes.NOT_EQUAL, '@1', '@2'], }, -]; + // [StringFilterTypes.IN]: { + // key: 'stringFilterContains', + // name: 'Contains', + // type: StringFilterTypes.IN, + // description: 'Contains another value', + // numInputs: 1, + // inputs: [ + // { name: '1', type: 'string', default: '' }, + // { name: '2', type: 'string', default: '' }, + // ], + // output: { name: StringFilterTypes.IN, type: 'bool' }, + // logic: [StringFilterTypes.IN, '@1', '@2'], + // }, + [StringFilterTypes.LIKE]: { + key: 'stringFilterLike', + name: 'Like', + type: StringFilterTypes.LIKE, + description: 'Like another value', + numInputs: 1, + inputs: [ + { name: '1', type: 'string', default: '' }, + { name: '2', type: 'string', default: '' }, + ], + output: { name: StringFilterTypes.LIKE, type: 'bool' }, + logic: [StringFilterTypes.LIKE, '@1', '@2'], + }, +}; + +/** All available functions in the function bar. */ +export const StringFilterArray: Array<GeneralDescription<StringFilterTypes>> = Object.values(StringFilters); diff --git a/libs/shared/lib/querybuilder/model/logic/stringFunctions.tsx b/libs/shared/lib/querybuilder/model/logic/stringFunctions.tsx index df031c477f7c6bfe633c3ce43b77f9ac9ee96b0d..3ba9a3d1a334a6e67e03a9d787b636fe0af86f62 100644 --- a/libs/shared/lib/querybuilder/model/logic/stringFunctions.tsx +++ b/libs/shared/lib/querybuilder/model/logic/stringFunctions.tsx @@ -5,27 +5,40 @@ */ import { Position } from 'reactflow'; -import { GeneralDescription } from './general'; +import { GeneralDescription, StringFunctionTypes } from './general'; -export enum StringFunctionTypes { - CONCAT = 'CONCAT', - LOWER = 'LOWER', - UPPER = 'UPPER', - SUBSTRING = 'SUBSTRING', - TRIM = 'TRIM', -} - -/** All available functions in the function bar. */ -export const StringFunctions: Array<GeneralDescription<StringFunctionTypes>> = [ - { - key: 'stringFunctionConcat', - name: 'Lower', +export const StringFunctions: Record<StringFunctionTypes, GeneralDescription<StringFunctionTypes>> = { + // [StringFunctionTypes.CONCAT]: { + // key: 'stringFunctionConcat', + // name: 'Concat', + // type: StringFunctionTypes.CONCAT, + // description: 'Lowercase all characters', + // numInputs: 1, + // inputs: [{ name: '1', type: 'string', default: '' }], + // output: { name: 'lower_case', type: 'string' }, + // logic: ['Lower', '@1'], + // }, + [StringFunctionTypes.LOWER]: { + key: 'stringFunctionLowerCase', + name: 'Lower Case', type: StringFunctionTypes.LOWER, description: 'Lowercase all characters', numInputs: 1, - inputs: [ - { name: '1', type: ['string'], position: Position.Left }, - { name: 'lower_case', type: ['string'], position: Position.Right }, - ], + inputs: [{ name: '1', type: 'string', default: '' }], + output: { name: 'lower_case', type: 'string' }, + logic: ['Lower', '@1'], + }, + [StringFunctionTypes.UPPER]: { + key: 'stringFunctionUpperCase', + name: 'Upper Case', + type: StringFunctionTypes.UPPER, + description: 'Uppercase all characters', + numInputs: 1, + inputs: [{ name: '1', type: 'string', default: '' }], + output: { name: 'upper_case', type: 'string' }, + logic: ['Upper', '@1'], }, -]; +}; + +/** All available functions in the function bar. */ +export const StringFunctionArray: Array<GeneralDescription<StringFunctionTypes>> = Object.values(StringFunctions); diff --git a/libs/shared/lib/querybuilder/model/reactflow/handles.tsx b/libs/shared/lib/querybuilder/model/reactflow/handles.tsx index c0b7fdca504a8302e946def1ae9f3625dbfcfe09..8c913b2c36fcfdc448dc381a423759cf18d1d145 100644 --- a/libs/shared/lib/querybuilder/model/reactflow/handles.tsx +++ b/libs/shared/lib/querybuilder/model/reactflow/handles.tsx @@ -18,6 +18,7 @@ export enum Handles { ToAttribute = 'attributesHandle', //target EntityLeft = 'entityLeftHandle', //source EntityRight = 'entityRightHandle', //target + EntityAttribute = 'entityAttributeHandle', //source OnAttribute = 'onAttributeHandle', //source ReceiveFunction = 'receiveFunctionHandle', //target FunctionBase = 'functionHandle_', // + name from FunctionTypes args //source diff --git a/libs/shared/lib/querybuilder/model/reactflow/utils.ts b/libs/shared/lib/querybuilder/model/reactflow/utils.ts index 984eb3ea1bbcce035d21b73679415c4d4ac0214b..b8a0dd7913527737b527ede89bd5d96d80c884cd 100644 --- a/libs/shared/lib/querybuilder/model/reactflow/utils.ts +++ b/libs/shared/lib/querybuilder/model/reactflow/utils.ts @@ -1,5 +1,6 @@ import Graph from 'graphology'; import { Node, Edge } from 'reactflow'; +import { toHandleId } from '..'; // Takes the querybuilder graph as an input and creates react flow elements for them. export function createReactFlowElements<T extends Graph>( @@ -12,6 +13,8 @@ export function createReactFlowElements<T extends Graph>( const edges: Array<Edge> = []; graph.forEachNode((node, attributes): void => { + // console.log('attributes', attributes); + let position = { x: attributes?.x || 0, y: attributes?.y || 0 }; const RFNode: Node<typeof attributes> = { id: node, @@ -32,13 +35,15 @@ export function createReactFlowElements<T extends Graph>( source: source, target: target, type: 'connection', - sourceHandle: attributes.sourceHandle, - targetHandle: attributes.targetHandle, + sourceHandle: toHandleId(attributes.sourceHandleData), + targetHandle: toHandleId(attributes.targetHandleData), data: attributes, zIndex: 1, }; edges.push(RFEdge); }); + // console.log('nodes', nodes, 'edges', edges); + return { nodes, edges }; } diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.tsx index 93f4bb8dea83994eb9c825b58cd2347348477a53..73bc730e7caf1961e35f4613792cec44f278f851 100644 --- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/querybuilder.tsx @@ -29,6 +29,7 @@ import ReactFlow, { NodePositionChange, } from 'reactflow'; import styles from './querybuilder.module.scss'; +import CachedIcon from '@mui/icons-material/Cached'; import React, { ReactComponentElement, useMemo, useRef, useEffect, useCallback, useState, DragEventHandler } from 'react'; import { AttributePill, ConnectionDragLine, ConnectionLine, EntityFlowElement, RelationPill } from '../pills'; @@ -38,6 +39,7 @@ import { clearQB } from '@graphpolaris/shared/lib/data-access/store/querybuilder import { RelationPosToFromEntityPos, RelationPosToToEntityPos } from '@graphpolaris/shared/lib/querybuilder/model/graphology/utils'; import { useDispatch } from 'react-redux'; import { + Box, Card, CardContent, Dialog, @@ -49,26 +51,31 @@ import { ListItemText, Paper, PaperProps, + Tab, + Tabs, Typography, } from '@mui/material'; -import { Handles, NodeAttribute, QueryElementTypes, QueryGraphNodes, createReactFlowElements, fromHandleId, getHandleId } from '../model'; -import Draggable from 'react-draggable'; -import { GeneralDescription, InputNodeType } from '../model/logic/general'; import { - MathFilterTypes, + AllLogicDescriptions, + AllLogicMap, + Handles, MathFilters, - MathFunctionTypes, - StringFilterTypes, + StringFilters, MathFunctions, + NodeAttribute, + QueryElementTypes, + QueryGraphNodes, StringFunctions, - StringFilters, - StringFunctionTypes, - AllLogicDescriptions, - AllLogicMap, -} from '../model/logic'; + createReactFlowElements, + toHandleData, + QueryGraphEdgeHandle, +} from '../model'; +import Draggable from 'react-draggable'; +import { GeneralDescription, InputNodeType } from '../model/logic/general'; import LogicPill from '../pills/customFlowPills/logicpill/logicpill'; import { current } from '@reduxjs/toolkit'; import { SchemaAttributeTypes } from '../../schema'; +import { MathAggregations } from '../model/logic/mathAggregations'; const nodeTypes = { entity: EntityFlowElement, @@ -84,7 +91,7 @@ const edgeTypes = { /** * This is the main querybuilder component. It is responsible for holding all pills and fire off the visual part of the querybuilder panel logic */ -export const QueryBuilderInner: React.FC = () => { +export const QueryBuilderInner = (props: QueryBuilderProps) => { const [openPopup, setOpenPopup] = useState(false); const reactFlowWrapper = useRef<HTMLDivElement>(null); @@ -229,42 +236,44 @@ export const QueryBuilderInner: React.FC = () => { switch (dragData.type) { case QueryElementTypes.Entity: - graphologyGraph.addPill2Graphology({ - type: QueryElementTypes.Entity, - x: position.x, - y: position.y, - name: dragData.name, - }); + // console.log('entity drop', dragData, schema.getNodeAttribute(dragData.name, 'attributes')); + graphologyGraph.addPill2Graphology( + { + type: QueryElementTypes.Entity, + x: position.x, + y: position.y, + name: dragData.name, + }, + schema.getNodeAttribute(dragData.name, 'attributes') + ); 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 relation = graphologyGraph.addPill2Graphology({ - type: QueryElementTypes.Relation, - x: position.x, - y: position.y, - depth: { min: 0, max: 1 }, - // name: dragData.name, - name: dragData.collection, - collection: dragData.collection, - }); - const leftEntity = graphologyGraph.addPill2Graphology({ - type: QueryElementTypes.Entity, - ...RelationPosToFromEntityPos(position), - name: dragData.from, - }); - const rightEntity = graphologyGraph.addPill2Graphology({ - type: QueryElementTypes.Entity, - ...RelationPosToToEntityPos(position), - name: dragData.to, - }); - - graphologyGraph.addEdge2Graphology(leftEntity, relation, { - type: 'connection', - }); - graphologyGraph.addEdge2Graphology(relation, rightEntity, { - type: 'connection', - }); + console.log('relation drop', dragData, schema.export()); + + const relation = graphologyGraph.addPill2Graphology( + { + type: QueryElementTypes.Relation, + x: position.x, + y: position.y, + depth: { min: 0, max: 1 }, + name: dragData.collection, + collection: dragData.collection, + }, + schema.getEdgeAttribute(dragData.label, 'attributes') + ); + // const leftEntity = graphologyGraph.addPill2Graphology( + // { type: QueryElementTypes.Entity, ...RelationPosToFromEntityPos(position), name: dragData.from }, + // schema.getNodeAttribute(dragData.from, 'attributes') + // ); + // const rightEntity = graphologyGraph.addPill2Graphology( + // { type: QueryElementTypes.Entity, ...RelationPosToToEntityPos(position), name: dragData.to }, + // schema.getNodeAttribute(dragData.to, 'attributes') + // ); + + // graphologyGraph.addEdge2Graphology(leftEntity, relation); + // graphologyGraph.addEdge2Graphology(relation, rightEntity); if (config.autoSendQueries) { // sendQuery(); @@ -281,6 +290,21 @@ export const QueryBuilderInner: React.FC = () => { // dragData.datatype // ); // break; + default: + const logic = AllLogicMap[dragData.value.key]; + const firstLeftLogicInput = logic.inputs?.[0]; + if (!firstLeftLogicInput) return; + + // logicAttributes[0].handles = [connectingNodeId.current.handleId]; + const logicNode = graphologyGraph.addLogicPill2Graphology({ + name: dragData.value.name, + type: QueryElementTypes.Logic, + x: position.x, + y: position.y, + logic: logic, + }); + + dispatch(setQuerybuilderNodes(graphologyGraph.export())); } }; @@ -297,12 +321,17 @@ export const QueryBuilderInner: React.FC = () => { (connection: Connection) => { if (!isEdgeUpdating.current) { isOnConnect.current = true; - graphologyGraph.addEdge(connection.source, connection.target, { - type: 'connection', - sourceHandle: connection.sourceHandle, - targetHandle: connection.targetHandle, - }); - dispatch(setQuerybuilderNodes(graphologyGraph.export())); + if (!connection.sourceHandle || !connection.targetHandle) throw new Error('Connection has no source or target'); + console.log('onConnect', connection); + + if (!graphologyGraph.hasEdge(connection.source, connection.target)) { + graphologyGraph.addEdge(connection.source, connection.target, { + type: 'connection', + sourceHandleData: toHandleData(connection.sourceHandle), + targetHandleData: toHandleData(connection.targetHandle), + }); + dispatch(setQuerybuilderNodes(graphologyGraph.export())); + } } }, [graph] @@ -310,17 +339,18 @@ export const QueryBuilderInner: React.FC = () => { const onConnectStart = useCallback( (event: React.MouseEvent | React.TouchEvent, params: OnConnectStartParams) => { + // console.log('onConnectStart', params); if (!params?.handleId) return; let node = graphologyGraph.getNodeAttributes(params.nodeId); - const { attributeName, attributeType } = fromHandleId(params.handleId); + const handleData = toHandleData(params.handleId); // console.log(attributeName, attributeType, node.attributes?.filter((a) => a.name === attributeName)?.[0]); connectingNodeId.current = { params, node, position: { x: 0, y: 0 }, - attribute: { name: attributeName, type: attributeType as SchemaAttributeTypes, handleId: params.handleId }, + attribute: { handleData: handleData }, }; }, [graph] @@ -362,27 +392,32 @@ export const QueryBuilderInner: React.FC = () => { const params = connectingNodeId.current.params; const position = connectingNodeId.current.position; + // console.log('onNewNodeFromPopup', value, type, params, position); + const logic = AllLogicMap[value.key]; - const firstLeftLogicInput = logic.inputs.filter((input) => input.position === Position.Left)?.[0]; + const firstLeftLogicInput = logic.inputs?.[0]; if (!firstLeftLogicInput) return; // logicAttributes[0].handles = [connectingNodeId.current.handleId]; - const logicNode = graphologyGraph.addPill2Graphology({ + const logicNode = graphologyGraph.addLogicPill2Graphology({ name: value.name, type: QueryElementTypes.Logic, x: position.x, y: position.y, logic: logic, }); + if (!logicNode?.id) throw new Error('Logic node has no id'); + if (!logicNode?.name) throw new Error('Logic node has no name'); + if (!params.handleId) throw new Error('Connection has no source or target'); - graphologyGraph.addEdge(params.nodeId, logicNode.id, { - type: 'connection', - sourceHandle: params.handleId, // newAttribute data? - targetHandle: getHandleId(logicNode.id, logicNode.name, logicNode.type, firstLeftLogicInput.name, firstLeftLogicInput.type.join(''), { - extra: 'left', - }), - }); + const sourceHandleData = toHandleData(params.handleId); + graphologyGraph.addEdge2Graphology( + graphologyGraph.getNodeAttributes(params.nodeId), + graphologyGraph.getNodeAttributes(logicNode.id), + { type: 'connection' }, + { sourceHandleName: sourceHandleData.attributeName, targetHandleName: firstLeftLogicInput.name } + ); dispatch(setQuerybuilderNodes(graphologyGraph.export())); setOpenPopup(false); @@ -401,10 +436,11 @@ export const QueryBuilderInner: React.FC = () => { graphologyGraph.dropEdge(oldEdge.id); } + if (!newConnection.sourceHandle || !newConnection.targetHandle) throw new Error('Connection has no source or target'); graphologyGraph.addEdge(newConnection.source, newConnection.target, { type: 'connection', - sourceHandle: newConnection.sourceHandle, - targetHandle: newConnection.targetHandle, + sourceHandleData: toHandleData(newConnection.sourceHandle), + targetHandleData: toHandleData(newConnection.targetHandle), }); dispatch(setQuerybuilderNodes(graphologyGraph.export())); } @@ -430,11 +466,18 @@ export const QueryBuilderInner: React.FC = () => { [graph] ); + const onNodeContextMenu = (event: React.MouseEvent, node: Node) => { + event.preventDefault(); + // console.log('context menu', node); + graphologyGraph.dropNode(node.id); + dispatch(setQuerybuilderNodes(graphologyGraph.export())); + }; + return ( <div ref={reactFlowWrapper} className={styles.full}> <PopupMenu open={openPopup} - type={connectingNodeId.current?.attribute?.type || 'int'} + type={connectingNodeId.current?.attribute.handleData.attributeType || 'int'} onClose={() => { setOpenPopup(false); }} @@ -463,6 +506,8 @@ export const QueryBuilderInner: React.FC = () => { onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateEnd={onEdgeUpdateEnd} onDrop={onDrop} + // onContextMenu={onContextMenu} + onNodeContextMenu={onNodeContextMenu} // onNodesDelete={onNodesDelete} // onNodesChange={onNodesChange} deleteKeyCode="Backspace" @@ -510,6 +555,16 @@ export const QueryBuilderInner: React.FC = () => { > <SettingsIcon /> </ControlButton> + <ControlButton + className={styles.buttons} + title={'Run Query'} + onClick={(event) => { + event.stopPropagation(); + if (props.onRunQuery) props.onRunQuery(); + }} + > + <CachedIcon /> + </ControlButton> </Controls> </ReactFlow> </div> @@ -529,12 +584,45 @@ function PaperComponent(props: PaperProps) { ); } +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +function CustomTabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + <div + role="tabpanel" + hidden={value !== index} + id={`simple-tabpanel-${index}`} + aria-labelledby={`simple-tab-${index}`} + {...other} + style={{ maxHeight: '85%', overflow: 'auto' }} + > + {value === index && ( + <Box> + <Typography>{children}</Typography> + </Box> + )} + </div> + ); +} + function PopupMenu(props: { open: boolean; type: InputNodeType; onClose: () => void; onClick: (value: AllLogicDescriptions, type: InputNodeType) => void; }) { + const [value, setValue] = React.useState(props.type === 'string' ? 1 : 0); + + const handleChange = (event: React.SyntheticEvent, newValue: number) => { + setValue(newValue); + }; + const handleClose = () => { props.onClose(); }; @@ -543,60 +631,100 @@ function PopupMenu(props: { props.onClick(value, type); }; - const generateList = (list: AllLogicDescriptions[], type: InputNodeType) => ( - <Grid container sx={{ pt: 0 }}> - {list.map((f, i) => ( - <Grid item key={JSON.stringify(f) + type + i}> - <ListItemButton onClick={() => handleListItemClick(f, type)} key={f + type}> - {/* <ListItemAvatar> + const generateList = (list: Record<string, AllLogicDescriptions>, type: InputNodeType) => ( + <List sx={{ pt: 0 }}> + {Object.keys(list).map((f, i) => ( + <ListItemButton onClick={() => handleListItemClick(list[f], type)} key={f + type}> + {/* <ListItemAvatar> <Avatar sx={{ bgcolor: blue[100], color: blue[600] }}> <PersonIcon /> </Avatar> </ListItemAvatar> */} - <ListItemText primary={f.name} secondary={f.description} /> - </ListItemButton> - </Grid> + <ListItemText primary={list[f].name} secondary={list[f].description} /> + </ListItemButton> ))} - </Grid> + </List> ); + + function a11yProps(index: number) { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, + }; + } + return ( <Dialog onClose={handleClose} open={props.open} PaperComponent={PaperComponent}> <DialogTitle>Add New Node</DialogTitle> - <DialogTitle>Collection Nodes</DialogTitle> - {props.type === 'float' && generateList(MathFunctions, props.type)} - {props.type === 'int' && generateList(MathFunctions, props.type)} - {props.type === 'string' && generateList(StringFunctions, props.type)} - <DialogTitle>Filter Nodes</DialogTitle> - {props.type === 'float' && generateList(MathFilters, props.type)} - {props.type === 'int' && generateList(MathFilters, props.type)} - {props.type === 'string' && generateList(StringFilters, props.type)} + <Box> + <Box sx={{ borderBottom: 1, borderColor: 'divider' }}> + <Tabs value={value} onChange={handleChange} aria-label="basic tabs example"> + <Tab label="Aggregations" disabled={props.type === 'string'} {...a11yProps(0)} /> + <Tab label="Operations" {...a11yProps(1)} /> + <Tab label="Filters" {...a11yProps(2)} /> + </Tabs> + </Box> + <CustomTabPanel value={value} index={0}> + {props.type === 'float' && generateList(MathAggregations, props.type)} + {props.type === 'int' && generateList(MathAggregations, props.type)} + {/* {props.type === 'string' && generateList(MathAggregations, props.type)} */} + </CustomTabPanel> + <CustomTabPanel value={value} index={1}> + {props.type === 'float' && generateList(MathFunctions, props.type)} + {props.type === 'int' && generateList(MathFunctions, props.type)} + {props.type === 'string' && generateList(StringFunctions, props.type)} + </CustomTabPanel> + <CustomTabPanel value={value} index={2}> + {props.type === 'float' && generateList(MathFilters, props.type)} + {props.type === 'int' && generateList(MathFilters, props.type)} + {props.type === 'string' && generateList(StringFilters, props.type)} + </CustomTabPanel> + </Box> </Dialog> ); } -export const QueryBuilderPills = (props: { onClose: () => void; onClick: (value: AllLogicDescriptions, type: InputNodeType) => void }) => { - const onDragStart = (event: React.DragEvent, nodeType: InputNodeType) => { +export const QueryBuilderPills = () => { + const onDragStart = (event: React.DragEvent, value: AllLogicDescriptions, nodeType: InputNodeType) => { console.log('drag start', nodeType); - event.dataTransfer.setData('application/reactflow', nodeType); + event.dataTransfer.setData('application/reactflow', JSON.stringify({ value, nodeType })); event.dataTransfer.effectAllowed = 'move'; }; - const generateList = (list: AllLogicDescriptions[], type: InputNodeType) => ( - <List> - {list.map((f, i) => ( - <ListItem key={JSON.stringify(f) + type + i}> - {/* <ListItemAvatar> - <Avatar sx={{ bgcolor: blue[100], color: blue[600] }}> - <PersonIcon /> - </Avatar> - </ListItemAvatar> */} + const [value, setValue] = React.useState(0); + const handleChange = (event: React.SyntheticEvent, newValue: number) => { + setValue(newValue); + }; + + function a11yProps(index: number) { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, + }; + } + + const handleListItemClick = (value: AllLogicDescriptions, type: InputNodeType) => {}; + + // const generateList = (list: Record<string, AllLogicDescriptions>, type: InputNodeType) => ( + // <List sx={{ pt: 0 }}> + // {Object.keys(list).map((f, i) => ( + // <ListItemButton onClick={() => handleListItemClick(list[f], type)} key={f + type}> + // <ListItemText primary={list[f].name} secondary={list[f].description} /> + // </ListItemButton> + // ))} + // </List> + // ); + const generateList = (list: Record<string, AllLogicDescriptions>, type: InputNodeType) => ( + <List> + {Object.keys(list).map((f, i) => ( + <ListItem key={JSON.stringify(list[f]) + type + i}> <ListItemText draggable - primary={f.name} - secondary={f.description} - onDragStart={(event) => onDragStart(event, type)} + primary={list[f].name} + secondary={list[f].description} + onDragStart={(event) => onDragStart(event, list[f], type)} key={f + type} /> </ListItem> @@ -605,25 +733,46 @@ export const QueryBuilderPills = (props: { onClose: () => void; onClick: (value: ); return ( - <aside> - <div>{generateList(MathFunctions, 'string')}</div> + <aside className=""> + <Box> + <Tabs value={value} onChange={handleChange} aria-label="basic tabs example"> + <Tab label="Aggregations" {...a11yProps(0)} /> + <Tab label="Operations" {...a11yProps(1)} /> + <Tab label="Filters" {...a11yProps(2)} /> + </Tabs> + </Box> + <CustomTabPanel value={value} index={0}> + {generateList(MathAggregations, 'float')} + </CustomTabPanel> + <CustomTabPanel value={value} index={1}> + {generateList(MathFunctions, 'float')} + {generateList(StringFunctions, 'string')} + </CustomTabPanel> + <CustomTabPanel value={value} index={2}> + {generateList(MathFilters, 'float')} + {generateList(StringFilters, 'string')} + </CustomTabPanel> </aside> ); }; -export const QueryBuilder = () => { +export type QueryBuilderProps = { + onRunQuery?: () => void; +}; + +export const QueryBuilder = (props: QueryBuilderProps) => { return ( <div style={{ width: '100%', - height: '100%', + height: '22rem', display: 'flex', gap: '1rem', }} > - {/* <QueryBuilderPills /> */} + <QueryBuilderPills /> <ReactFlowProvider> - <QueryBuilderInner /> + <QueryBuilderInner {...props} /> </ReactFlowProvider> </div> ); 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 a71f52347fb878fd59b815c88c6ecf5793d548ab..6d8c661f340482b9ea1a03da3902bce9462e58db 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, QueryElementTypes, QueryMultiGraphology, getHandleId } from '../../model'; +import { Handles, NodeAttribute, QueryElementTypes, QueryMultiGraphology, toHandleId } from '../../model'; import { SchemaUtils } from '../../../schema/schema-utils'; import { ReactFlowProvider } from 'reactflow'; @@ -53,27 +53,31 @@ export const SimpleDisconnected = { store.dispatch(setSchema(schema.export())); - 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[], - }); + const entity1 = graph.addPill2Graphology( + { + id: '0', + type: QueryElementTypes.Entity, + x: 100, + y: 100, + name: 'Airport 1', + }, + schema.getNodeAttribute('entity', 'attributes') + ); + const entity2 = graph.addPill2Graphology( + { + id: '10', + type: QueryElementTypes.Entity, + x: 200, + y: 200, + name: 'Airport 2', + }, + schema.getNodeAttribute('entity', 'attributes') + ); - // graph.addNode('0', { type: 'entity', x: 100, y: 100, name: 'Entity Pill' }); + // graph.addNode('0', { type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Entity Pill' }); const relation1 = graph.addPill2Graphology({ id: '1', - type: 'relation', + type: QueryElementTypes.Relation, x: 140, y: 140, name: 'Flight between airports', 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 955a41623f2b8d717c22344e1fad98b622043b22..bd57e342d07dc0c4b8541317fec3fc306c6615da 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, QueryElementTypes, QueryMultiGraphology, getHandleId } from '../../model'; +import { Handles, NodeAttribute, QueryElementTypes, QueryMultiGraphology, toHandleId } from '../../model'; import { SchemaUtils } from '../../../schema/schema-utils'; const Component: Meta<typeof QueryBuilder> = { @@ -67,27 +67,31 @@ export const Simple = { store.dispatch(setSchema(schema.export())); - 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[], - }); + const entity1 = graph.addPill2Graphology( + { + id: '0', + type: QueryElementTypes.Entity, + x: 100, + y: 100, + name: 'Airport 1', + }, + schema.getNodeAttribute('entity', 'attributes') + ); + const entity2 = graph.addPill2Graphology( + { + id: '10', + type: QueryElementTypes.Entity, + x: 200, + y: 200, + name: 'Airport 2', + }, + schema.getNodeAttribute('entity', 'attributes') + ); - // graph.addNode('0', { type: 'entity', x: 100, y: 100, name: 'Entity Pill' }); + // graph.addNode('0', { type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Entity Pill' }); const relation1 = graph.addPill2Graphology({ id: '1', - type: 'relation', + type: QueryElementTypes.Relation, x: 140, y: 140, name: 'Flight between airports', @@ -136,12 +140,8 @@ export const Simple = { // }, // graph // ); - graph.addEdge2Graphology(entity1, relation1, { - type: 'connection', - }); - graph.addEdge2Graphology(relation1, entity2, { - type: 'connection', - }); + graph.addEdge2Graphology(entity1, relation1); + graph.addEdge2Graphology(relation1, entity2); // console.log(graph.getNodeAttributes('2')); // graph.addEdge('2', '1', { type: 'attribute_connection' }); // graph.addEdge('3', '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 a5ca1fc7f4e966f0823c5e87fbbac95ff9d72a60..c147c023d91d4dc49e80b3b4a6a6c392c3d5a910 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 { QueryElementTypes, QueryMultiGraphology, getHandleId } from '../../model'; +import { QueryElementTypes, QueryMultiGraphology, toHandleId } from '../../model'; const Component: Meta<typeof QueryBuilder> = { component: QueryBuilder, @@ -39,7 +39,7 @@ export const SingleEntity = { play: async () => { const graph = new QueryMultiGraphology(); graph.addPill2Graphology({ - type: 'entity', + type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Entity Pill', 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 017a42f10f5f7242c44f493953182fda7314e4c8..db2d6634ff70de4183e8bf89a578cc71fc822533 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 { QueryElementTypes, QueryMultiGraphology, getHandleId } from '../../model'; +import { QueryElementTypes, QueryMultiGraphology, toHandleId } from '../../model'; const Component: Meta<typeof QueryBuilder> = { component: QueryBuilder, @@ -39,7 +39,7 @@ export const SingleRelationship = { play: async () => { const graph = new QueryMultiGraphology(); graph.addPill2Graphology({ - type: 'relation', + type: QueryElementTypes.Relation, x: 140, y: 140, name: 'Relation Pill', 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 ac79584b6fb78aec820b5319d8e2d2e585f83cca..fd47ee277672faa3352036060444216684637321 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 @@ -6,6 +6,7 @@ import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; import { QueryBuilder } from '../../../panel'; import { QueryMultiGraphology } from '@graphpolaris/shared/lib/querybuilder/model/graphology/utils'; +import { QueryElementTypes } from '../../../model'; const Component: Meta<typeof QueryBuilder> = { component: QueryBuilder, @@ -27,7 +28,7 @@ const mockStore = configureStore({ }, }); const graph = new QueryMultiGraphology(); -graph.addPill2Graphology({ id: '2', type: 'entity', x: 100, y: 100, name: 'Entity Pill' }); +graph.addPill2Graphology({ id: '2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Entity Pill' }); console.log(graph.export()); mockStore.dispatch(setQuerybuilderNodes(graph.export())); diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.module.scss b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.module.scss index 5dc94dd972be4c41e35b1e1569966b05f1cd1fa2..2590cb9d86996c39f88e06a402d3b4088df79327 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.module.scss +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.module.scss @@ -36,6 +36,7 @@ font-family: monospace; font-weight: bold; color: black; + min-width: 8rem; font-size: 10px; border-radius: 2px; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx index c2b191fda7947648eb6a7803b9f4badd83cd1cde..2d0cd37a3ff2e83066dae23b967e489ad8da1de5 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx @@ -1,9 +1,16 @@ // import { handles } from '@graphpolaris/shared/lib/querybuilder/usecases'; import { useTheme } from '@mui/material'; -import React, { MouseEventHandler, useEffect } from 'react'; +import React, { MouseEventHandler, useEffect, useMemo } from 'react'; import { ReactFlow, Handle, Position, getConnectedEdges } from 'reactflow'; import styles from './entitypill.module.scss'; -import { SchemaReactflowEntityNode, Handles, getHandleIdFromReactflow } from '../../../model'; +import { + SchemaReactflowEntityNode, + Handles, + toHandleId, + handleDataFromReactflowToId, + QueryElementTypes, + NodeAttribute, +} from '../../../model'; import { SchemaAttribute } from '@graphpolaris/shared/lib/schema'; import { styleHandleMap } from '../../utils'; import { useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access'; @@ -14,12 +21,17 @@ import { useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access'; */ export const EntityFlowElement = React.memo((node: SchemaReactflowEntityNode) => { const theme = useTheme(); + // console.log('EntityFlowElement', node); + const data = node.data; - const forceOpen: boolean = true; + const forceOpen: boolean = false; + if (!data.leftRelationHandleId) throw new Error('EntityFlowElement: data.leftRelationHandleId is undefined'); + if (!data.rightRelationHandleId) throw new Error('EntityFlowElement: data.rightRelationHandleId is undefined'); const graph = useQuerybuilderGraph(); - const myEdges = graph.edges.filter( - (edge) => (edge.source === node.id || edge.target === node.id) && !edge?.attributes?.sourceHandle.includes(Handles.EntityRight) // no need to show if only the relation handle is connected + const attributeEdges = useMemo( + () => graph.edges.filter((edge) => edge.source === node.id && !!edge?.attributes?.sourceHandleData.attributeType), + [graph] ); const [hovered, setHovered] = React.useState(false); @@ -36,7 +48,7 @@ export const EntityFlowElement = React.memo((node: SchemaReactflowEntityNode) => setHovered(false); }; - const onHandleMouseDown = (attribute: SchemaAttribute, i: number, event: React.MouseEvent) => { + const onHandleMouseDown = (attribute: NodeAttribute, i: number, event: React.MouseEvent) => { setHandleBeingDragged(i); window.addEventListener('mouseup', onHandleMouseUp, true); }; @@ -50,7 +62,7 @@ export const EntityFlowElement = React.memo((node: SchemaReactflowEntityNode) => console.log('EntityPill onConnect', params); }; - const showingDropdown = hovered || handleBeingDragged !== -1 || myEdges.length > 0; + const showingDropdown = hovered || handleBeingDragged !== -1 || attributeEdges.length > 0; return ( <div @@ -65,14 +77,14 @@ export const EntityFlowElement = React.memo((node: SchemaReactflowEntityNode) => > <Handle // id={getHandleId(data.name, data.type, Handles.ToRelation, '')} - id={data.leftRelationHandleId} + id={toHandleId(data.leftRelationHandleId)} type="target" position={Position.Left} className={styles.handle_to_relation} /> <Handle // id={getHandleId(data.name, data.type, Handles.ToRelation, '')} - id={data.rightRelationHandleId} + id={toHandleId(data.rightRelationHandleId)} type="source" position={Position.Right} className={styles.handle_to_relation} @@ -110,28 +122,39 @@ export const EntityFlowElement = React.memo((node: SchemaReactflowEntityNode) => <span className={styles.entitySpan}>{data.name}</span> </div> */} {data?.attributes && ( - <div className={styles.content + ' ' + (showingDropdown || forceOpen ? styles.content_display : '')}> + <div className={styles.content + ' ' + (showingDropdown || forceOpen || hovered ? styles.content_display : '')}> {data.attributes .filter( (attribute, i) => forceOpen || hovered || handleBeingDragged === i || - myEdges.some((edge) => edge?.attributes?.sourceHandle === getHandleIdFromReactflow(node, attribute)) + (attributeEdges.some( + (edge) => + edge?.attributes?.sourceHandleData && + toHandleId(edge?.attributes?.sourceHandleData) === handleDataFromReactflowToId(node, attribute) + ) && + !!attribute.handleData.attributeName) ) .map((attribute, i) => ( <div - key={attribute.name + i} + key={(attribute.handleData.attributeName || '') + i} onMouseDown={(event: React.MouseEvent) => { onHandleMouseDown(attribute, i, event); }} > - {attribute.name} + {attribute.handleData.attributeName} <Handle - id={getHandleIdFromReactflow(node, attribute)} + id={handleDataFromReactflowToId(node, attribute)} type="source" position={Position.Right} - className={(styleHandleMap?.[attribute.type] || '') + ' ' + styles.io + ' ' + styles.io_right} + className={ + (attribute.handleData.attributeType ? styleHandleMap[attribute.handleData.attributeType] : '') + + ' ' + + styles.io + + ' ' + + styles.io_right + } ></Handle> </div> ))} diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.module.scss b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.module.scss index 97dd71472be75eddbdb848e47ab918379375d9c2..81e3710c83b8462aeabfba66f132a52991ea5f0f 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.module.scss +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.module.scss @@ -5,13 +5,13 @@ // background-color: #e68067; background-color: #fdfdfd; border: #e68067 solid 1px; - border-radius: 5px; + border-radius: 2px; font-family: monospace; - font-weight: bolder; + font-weight: bold; font-size: 10; display: flex; - border-top-right-radius: 5px; - border-bottom-right-radius: 5px; + // border-top-right-radius: 5px; + // border-bottom-right-radius: 5px; // height: 3rem; .logicInput { // float: right; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx index 37294f09e566d212cf7ccd20dbbcddddb61a8ddd..538e6391a46ad144c4fbabb4be100f7c078aed5c 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx @@ -8,80 +8,129 @@ /* The comment above was added so the code coverage wouldn't count this file towards code coverage. * We do not test components/renderfunctions/styling files. * See testing plan for more details.*/ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { Handle, HandleType, NodeProps, Position } from 'reactflow'; import styles from './logicpill.module.scss'; -import { AllLogicMap, Handles, SchemaReactflowLogicNode, fromHandleId, getHandleId, getHandleIdFromReactflow } from '../../../model'; +import { + AllLogicMap, + EntityNodeAttributes, + Handles, + LogicData, + LogicNodeAttributes, + QueryGraphEdges, + QueryGraphNodes, + SchemaReactflowLogicNode, + toHandleData, + toHandleId, +} from '../../../model'; import { Input } from '@mui/material'; import { styleHandleMap } from '../../utils'; -import { useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access'; -import { InputNode } from '../../../model/logic/general'; +import { + setQuerybuilderNodes, + useAppDispatch, + useQuerybuilderGraph, + useQuerybuilderGraphology, +} from '@graphpolaris/shared/lib/data-access'; +import { InputNode, InputNodeTypeTypes } from '../../../model/logic/general'; +import { SerializedEdge, SerializedNode } from 'graphology-types'; /** * Component to render an entity flow element * @param param0 Data of the flow element. */ export default function LogicPill(node: SchemaReactflowLogicNode) { + const dispatch = useAppDispatch(); const data = node.data; const logic = data.logic; - const left = logic.inputs.filter((input) => input.position === Position.Left); - const right = logic.inputs.filter((input) => input.position === Position.Right); + const output = data.logic.output; const graph = useQuerybuilderGraph(); - const leftEdges = graph.edges.filter((edge) => edge.target === node.id); - const rightEdges = graph.edges.filter((edge) => edge.source === node.id); + const graphology = useQuerybuilderGraphology(); + const connectionsToLeft = useMemo(() => graph.edges.filter((edge) => edge.target === node.id), [graph]); + const connectionsToRight = useMemo(() => graph.edges.filter((edge) => edge.source === node.id), [graph]); - function createHandles(side: InputNode[], positionSide: Position, handleType: HandleType) { - let numOfInputs = 0; - let ret = side.map((input, i) => { - let inputTextBox = null; - if ( - positionSide === Position.Left && - !leftEdges.some( - (edge) => - edge?.attributes?.targetHandle === - getHandleId(data?.id || 'awkudgi', data.name, node.type, input.name, input.type.join(''), { extra: positionSide }) // TODO - ) - ) { - inputTextBox = ( - <Input - className={styles.logicInput} - // value={0} - style={{ top: -5, transform: `translateY(-${i * 20}%)` }} - onChange={(e) => {}} - /> + if (!data.id) throw new Error('LogicPill: data.id is undefined'); + const defaultHandleData = { + nodeId: data.id, + nodeName: data.name, + nodeType: data.type, + }; + + const onInputUpdated = (value: string, input: InputNode, idx: number) => { + let logicNode = { ...(graphology.getNodeAttributes(node.id) as LogicNodeAttributes) }; + if (!logicNode) throw new Error('LogicPill: logicNode is undefined'); + let logicNodeInputs = { ...logicNode.inputs }; + if (logicNodeInputs[input.name] != value) { + logicNodeInputs[input.name] = value; + logicNode.inputs = logicNodeInputs; + graphology.setNodeAttribute<any>(node.id, 'inputs', logicNodeInputs); // FIXME: I'm not sure why TS requires <any> to work here + dispatch(setQuerybuilderNodes(graphology.export())); + } + }; + + const createLeftHandles = useCallback( + (sideInputs: InputNode[], positionSide: Position, handleType: HandleType) => { + let numOfInputs = 0; + let ret = sideInputs.map((input, i) => { + let inputTextBox = null; + if ( + !connectionsToLeft.some( + (edge) => + edge?.attributes?.targetHandleData.nodeId === data.id && edge?.attributes?.targetHandleData.attributeName === input.name + ) + ) { + inputTextBox = ( + <Input + className={styles.logicInput} + // value={0} + style={{ top: -5, transform: `translateY(-${i * 20}%)` }} + // onChange={(e) => onInputUpdated(e.target.value, input)} + onKeyDown={(e) => { + if (e.key === 'Enter') onInputUpdated((e.target as HTMLInputElement).value, input, i); + }} + onBlur={(e) => onInputUpdated(e.target.value, input, i)} + /> + ); + numOfInputs++; + } + return ( + <Handle + type={handleType} + position={positionSide} + id={toHandleId({ ...defaultHandleData, attributeName: input.name, attributeType: input.type })} // TODO + key={input.name + input.type} + // style={{ top: `${((i + 0.8) / (side.length + 0.6)) * 120}%` }} + style={{ top: `${((i + 0.8) / (sideInputs.length + 0.6)) * 100}%` }} + className={styleHandleMap[input.type]} + > + {inputTextBox} + </Handle> ); - numOfInputs++; - } - return ( - <Handle - type={handleType} - position={positionSide} - id={getHandleId(data?.id || 'awkudgi', data.name, node.type, input.name, input.type.join(''), { extra: positionSide })} // TODO - key={input.name + input.type} - // style={{ top: `${((i + 0.8) / (side.length + 0.6)) * 120}%` }} - style={{ top: `${((i + 0.8) / (side.length + 0.6)) * 100}%` }} - className={input.type.map((type) => styleHandleMap?.[type] || '').join(' ')} - > - {inputTextBox} - </Handle> - ); - }); - return { handles: ret, number: numOfInputs }; - } - const { handles: leftHandles, number: leftInputsNumber } = createHandles(left, Position.Left, 'target'); - const { handles: rightHandles, number: rightInputsNumber } = createHandles(right, Position.Right, 'source'); + }); + return { handles: ret, number: numOfInputs }; + }, + [node] + ); + const { handles: leftHandles, number: leftInputsNumber } = createLeftHandles(node.data.logic.inputs, Position.Left, 'target'); return ( <div className={styles.logic}> - <span className={styles.logicSpan} style={{ height: `${Math.max(leftInputsNumber * 2.6, rightInputsNumber * 2, 1)}rem` }}> + <span className={styles.logicSpan} style={{ height: `${Math.max(leftInputsNumber * 2.6, 1.5)}rem` }}> { <span> - {leftEdges.map((e) => fromHandleId(e?.attributes?.sourceHandle)?.attributeName).join(', ')}.{right?.[0].name} + {connectionsToLeft.map((e) => e?.attributes?.sourceHandleData.attributeName)}.{output.name} </span> } {leftHandles} - {rightHandles} + {!!node.data.logic.output && ( + <Handle + type={'source'} + position={Position.Right} + id={toHandleId({ ...defaultHandleData, attributeName: output.name, attributeType: output.type })} // TODO + // style={{ top: `${((i + 0.8) / (side.length + 0.6)) * 100}%` }} + className={styleHandleMap?.[output.type]} + ></Handle> + )} </span> </div> ); 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 ad157810b71186da356bba689eee980639a33a1b..5b6d0c2313a98ec3c9cd1e54b1d44ec3c121bc08 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,7 +6,7 @@ import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; import { MultiGraph } from 'graphology'; import { QueryBuilder } from '../../../panel'; -import { QueryMultiGraphology } from '../../../model'; +import { QueryElementTypes, QueryMultiGraphology } from '../../../model'; const Component: Meta<typeof QueryBuilder> = { component: QueryBuilder, @@ -30,7 +30,7 @@ const mockStore = configureStore({ const graph = new QueryMultiGraphology(); graph.addPill2Graphology({ id: '2', - type: 'relation', + type: QueryElementTypes.Relation, x: 140, y: 140, name: 'Relation Pill', diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx index a714cd120dd53cd86f1820698e90da9eea0218d0..323310f8a6cdd6d1fd5900594a2b5aef5c20895d 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx @@ -4,7 +4,7 @@ import { useTheme } from '@mui/material'; import { Handle, Position } from 'reactflow'; import styles from './relationpill.module.scss'; -import { SchemaReactflowRelationNode, Handles, getHandleId } from '../../../model'; +import { SchemaReactflowRelationNode, Handles, toHandleId } from '../../../model'; // export type RelationRFPillProps = { // data: { @@ -81,12 +81,14 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => { className={styles.relationHandleFiller} // style={{ transform: 'translate(-100px,0)' }} > - <Handle - id={data.leftEntityHandleId} - type="target" - position={Position.Left} - className={styles.relationHandleLeft + ' ' + (false ? styles.handleConnectedBorderLeft : '')} - /> + {data.leftEntityHandleId && ( + <Handle + id={toHandleId(data.leftEntityHandleId)} + type="target" + position={Position.Left} + className={styles.relationHandleLeft + ' ' + (false ? styles.handleConnectedBorderLeft : '')} + /> + )} </span> {/* <span className={styles.relationHandleFiller}> <Handle @@ -165,12 +167,14 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => { </span> </div> <span className={styles.relationHandleFiller}> - <Handle - id={data.rightEntityHandleId} - type="source" - position={Position.Right} - className={styles.relationHandleRight + ' ' + (false ? styles.handleConnectedBorderRight : '')} - /> + {data.rightEntityHandleId && ( + <Handle + id={toHandleId(data.rightEntityHandleId)} + type="source" + position={Position.Right} + className={styles.relationHandleRight + ' ' + (false ? styles.handleConnectedBorderRight : '')} + /> + )} </span> </div> </div> diff --git a/libs/shared/lib/querybuilder/pills/handles.module.scss b/libs/shared/lib/querybuilder/pills/handles.module.scss index 7a138b0ed5b6155b6abeed953be123d4953be7b2..a39b5f8ea31b24060a1cf612dc6ad6ccadc0e86f 100644 --- a/libs/shared/lib/querybuilder/pills/handles.module.scss +++ b/libs/shared/lib/querybuilder/pills/handles.module.scss @@ -1,11 +1,13 @@ .handle { z-index: 1; + height: 8px !important; + width: 8px !important; } .handle_to_relation { @extend .handle; border-radius: 1px !important; - top: 0.55rem !important; + top: 0.6rem !important; background: rgb(39, 131, 145) !important; } diff --git a/libs/shared/lib/querybuilder/pills/utils.ts b/libs/shared/lib/querybuilder/pills/utils.ts index 6570ee17cd01f76b6186ce72b2dc27fe06c0370f..02098af6680785ff8e65cdbdf91fc5ae4d1ebd30 100644 --- a/libs/shared/lib/querybuilder/pills/utils.ts +++ b/libs/shared/lib/querybuilder/pills/utils.ts @@ -1,6 +1,7 @@ +import { InputNodeType } from '../model/logic/general'; import styles from './querypills.module.scss'; -export const styleHandleMap = { +export const styleHandleMap: Record<InputNodeType, string> = { string: styles.handle_logic_string, int: styles.handle_logic_int, float: styles.handle_logic_float, diff --git a/libs/shared/lib/querybuilder/query-utils/index.ts b/libs/shared/lib/querybuilder/query-utils/index.ts index b63764185e8e7fa3525f34a150dc11205acbd8f5..0477263bfc5189dadef3cd6655463ac3bb942dbc 100644 --- a/libs/shared/lib/querybuilder/query-utils/index.ts +++ b/libs/shared/lib/querybuilder/query-utils/index.ts @@ -1,2 +1,2 @@ -export * from './query-utils'; +export * from './query2backend'; export * from '../model/BackendQueryFormat'; diff --git a/libs/shared/lib/querybuilder/query-utils/query-utils.spec.ts b/libs/shared/lib/querybuilder/query-utils/query-utils.spec.ts deleted file mode 100644 index 8e9b2b1f2c1a3e27bf42b3ac1dea32e5ec91f109..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/query-utils/query-utils.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 71a935e628d1b74eeb185ee67a927119de587240..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/query-utils/query-utils.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { EntityNodeAttributes, QueryGraphNodes, RelationNodeAttributes } from '../model/graphology/model'; -import { QueryMultiGraph } from '../model/graphology/utils'; -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 { - let query: BackendQueryFormat = { - databaseName: databaseName, - query: [], - limit: 5000, // TODO - return: ['*'], // TODO - }; - 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/querybuilder/query-utils/query2backend.spec.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e99347c6f47572236228dfb87d9744cf726b3dbb --- /dev/null +++ b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts @@ -0,0 +1,960 @@ +import { describe, expect, it } from 'vitest'; +// import { Query2BackendQuery } from './query-utils'; +import { BackendQueryFormat, LogicNodeAttributes, MathFilters, MathFunctions, QueryElementTypes } from '../model'; +import { QueryMultiGraphology } from '../model/graphology/utils'; +import { MathAggregationTypes, MathFilterTypes, MathFunctionTypes } from '../model/logic/general'; +import { Query2BackendQuery, calculateQueryLogic } from './query2backend'; +import { SerializedNode } from 'graphology-types'; +import { MathAggregations } from '../model/logic/mathAggregations'; + +const defaultQuery = { + databaseName: 'database', + query: [], + limit: 500, + return: ['*'], +}; + +describe('QueryUtils Entity and Relations', () => { + it('should create an instance', () => { + const graph = new QueryMultiGraphology(); + expect(true).toBeTruthy(); + }); + + it('should run simple query translation', () => { + const graph = new QueryMultiGraphology(); + + const entity1 = graph.addPill2Graphology({ + id: '0', + type: QueryElementTypes.Entity, + x: 100, + y: 100, + name: 'Airport 1', + }); + const entity2 = graph.addPill2Graphology({ + id: '10', + type: QueryElementTypes.Entity, + x: 200, + y: 200, + name: 'Airport 2', + }); + + const relation1 = graph.addPill2Graphology({ + id: '1', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + + graph.addEdge2Graphology(entity1, relation1); + graph.addEdge2Graphology(relation1, entity2); + + const expected = { + ...defaultQuery, + query: [ + { + ID: 'path_0', + node: { + ID: '0', + label: 'Airport 1', + relation: { + ID: '1', + label: 'Flight between airports', + direction: 'TO', + node: { + ID: '10', + label: 'Airport 2', + relation: undefined, + }, + }, + }, + }, + ], + }; + + let ret = Query2BackendQuery('database', graph.export()); + expect(ret).toMatchObject(expected); + }); + + it('should run multiple path query translation', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology({ id: 'e0', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e2 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 2' }); + + const r1 = graph.addPill2Graphology({ + id: 'r1', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + + graph.addEdge2Graphology(e1, r1, { type: 'connection' }); + graph.addEdge2Graphology(r1, e2, { type: 'connection' }); + + const e12 = graph.addPill2Graphology({ id: 'e12', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 12' }); + const e22 = graph.addPill2Graphology({ id: 'e22', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 22' }); + + const r12 = graph.addPill2Graphology({ + id: 'r12', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports 2', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + + graph.addEdge2Graphology(e12, r12); + graph.addEdge2Graphology(r12, e22); + + const expected = { + ...defaultQuery, + query: [ + { + ID: 'path_0', + node: { + ID: 'e0', + label: 'Airport 1', + relation: { + ID: 'r1', + label: 'Flight between airports', + direction: 'TO', + node: { + ID: 'e1', + label: 'Airport 2', + relation: undefined, + }, + }, + }, + }, + { + ID: 'path_1', + node: { + ID: 'e12', + label: 'Airport 12', + relation: { + ID: 'r12', + label: 'Flight between airports 2', + direction: 'TO', + node: { + ID: 'e22', + label: 'Airport 22', + relation: undefined, + }, + }, + }, + }, + ], + }; + + let ret = Query2BackendQuery('database', graph.export()); + expect(ret).toMatchObject(expected); + }); + + it('should run one relation multiple entity query translation', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 2' }); + const e12 = graph.addPill2Graphology({ id: 'e12', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 12' }); + const e22 = graph.addPill2Graphology({ id: 'e22', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 22' }); + + const r1 = graph.addPill2Graphology({ + id: 'r1', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + + graph.addEdge2Graphology(e1, r1); + graph.addEdge2Graphology(r1, e2); + + graph.addEdge2Graphology(e12, r1); + graph.addEdge2Graphology(r1, e22); + + const expected = { + ...defaultQuery, + query: [ + { + ID: 'path_0', + node: { + ID: 'e1', + label: 'Airport 1', + relation: { ID: 'r1', label: 'Flight between airports', direction: 'TO', node: { ID: 'e2', label: 'Airport 2' } }, + }, + }, + { + ID: 'path_1', + node: { + ID: 'e1', + label: 'Airport 1', + relation: { ID: 'r1', label: 'Flight between airports', direction: 'TO', node: { ID: 'e22', label: 'Airport 22' } }, + }, + }, + { + ID: 'path_2', + node: { + ID: 'e12', + label: 'Airport 12', + relation: { ID: 'r1', label: 'Flight between airports', direction: 'TO', node: { ID: 'e2', label: 'Airport 2' } }, + }, + }, + { + ID: 'path_3', + node: { + ID: 'e12', + label: 'Airport 12', + relation: { ID: 'r1', label: 'Flight between airports', direction: 'TO', node: { ID: 'e22', label: 'Airport 22' } }, + }, + }, + ], + }; + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); + + it('should run lone entities query translation', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology({ id: 'e0', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e2 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 2' }); + const e12 = graph.addPill2Graphology({ id: 'e12', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 12' }); + const e22 = graph.addPill2Graphology({ id: 'e22', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 22' }); + + const r1 = graph.addPill2Graphology({ + id: 'r1', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + + graph.addEdge2Graphology(e1, r1); + graph.addEdge2Graphology(r1, e2); + + const expected = { + ...defaultQuery, + query: [ + { + ID: 'path_0', + node: { + ID: 'e0', + label: 'Airport 1', + relation: { ID: 'r1', label: 'Flight between airports', direction: 'TO', node: { ID: 'e1', label: 'Airport 2' } }, + }, + }, + { ID: 'path_1', node: { ID: 'e12', label: 'Airport 12' } }, + { ID: 'path_2', node: { ID: 'e22', label: 'Airport 22' } }, + ], + }; + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); + + it('should run lone relations query translation', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology({ id: 'e0', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e2 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 2' }); + + const r1 = graph.addPill2Graphology({ + id: 'r1', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + const r2 = graph.addPill2Graphology({ + id: 'r2', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports 2', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + + graph.addEdge2Graphology(e1, r1); + graph.addEdge2Graphology(r1, e2); + + const expected = { + ...defaultQuery, + query: [ + { + ID: 'path_0', + node: { + ID: 'e0', + label: 'Airport 1', + relation: { ID: 'r1', label: 'Flight between airports', direction: 'TO', node: { ID: 'e1', label: 'Airport 2' } }, + }, + }, + { + ID: 'path_1', + node: { relation: { ID: 'r2', label: 'Flight between airports 2', direction: 'TO', node: {} } }, + }, + ], + }; + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); + + it('should run relation only left side connected query translation', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology({ id: 'e0', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + + const r1 = graph.addPill2Graphology({ + id: 'r1', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + + graph.addEdge2Graphology(e1, r1); + + const expected = { + ...defaultQuery, + query: [ + { + ID: 'path_0', + node: { + ID: 'e0', + label: 'Airport 1', + relation: { ID: 'r1', label: 'Flight between airports', direction: 'TO', node: {} }, + }, + }, + ], + }; + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); + + it('should run relation only right side connected query translation', () => { + const graph = new QueryMultiGraphology(); + + const e2 = graph.addPill2Graphology({ id: 'e0', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 2' }); + + const r1 = graph.addPill2Graphology({ + id: 'r1', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + + graph.addEdge2Graphology(r1, e2); + + const expected = { + ...defaultQuery, + query: [ + { + ID: 'path_0', + node: { + relation: { ID: 'r1', label: 'Flight between airports', direction: 'TO', node: { ID: 'e0', label: 'Airport 2' } }, + }, + }, + ], + }; + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); + + it('should run entity and relations multi connection', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 2' }); + + const r1 = graph.addPill2Graphology({ + id: 'r1', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + + const r2 = graph.addPill2Graphology({ + id: 'r2', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports 2', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + + graph.addEdge2Graphology(e1, r1); + graph.addEdge2Graphology(e1, r2); + graph.addEdge2Graphology(r1, e2); + graph.addEdge2Graphology(r2, e2); + + const expected = { + ...defaultQuery, + query: [ + { + ID: 'path_0', + node: { + ID: 'e1', + label: 'Airport 1', + relation: { ID: 'r1', label: 'Flight between airports', direction: 'TO', node: { ID: 'e2', label: 'Airport 2' } }, + }, + }, + { + ID: 'path_1', + node: { + ID: 'e1', + label: 'Airport 1', + relation: { ID: 'r2', label: 'Flight between airports 2', direction: 'TO', node: { ID: 'e2', label: 'Airport 2' } }, + }, + }, + ], + }; + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); + + it('should run in case of loops', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + + const r1 = graph.addPill2Graphology({ + id: 'r1', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + + graph.addEdge2Graphology(e1, r1, { type: 'connection' }); + graph.addEdge2Graphology(r1, e1, { type: 'connection' }); + + const expected = { + ...defaultQuery, + query: [ + { + ID: 'path_0', + node: { + ID: 'e1', + label: 'Airport 1', + relation: { ID: 'r1', label: 'Flight between airports', direction: 'TO', node: { ID: 'e1', label: 'Airport 1' } }, + }, + }, + ], + }; + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); + + it('should run in case of loops and regular', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + + const r1 = graph.addPill2Graphology({ + id: 'r1', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + + graph.addEdge2Graphology(e1, r1, { type: 'connection' }); + graph.addEdge2Graphology(r1, e1, { type: 'connection' }); + graph.addEdge2Graphology(r1, e2, { type: 'connection' }); + + const expected = { + ...defaultQuery, + query: [ + { + ID: 'path_0', + node: { + ID: 'e1', + label: 'Airport 1', + relation: { + ID: 'r1', + label: 'Flight between airports', + direction: 'TO', + node: { ID: 'e2', label: 'Airport 1', relation: undefined }, + }, + }, + }, + { + ID: 'path_1', + node: { + ID: 'e1', + label: 'Airport 1', + relation: { ID: 'r1', label: 'Flight between airports', direction: 'TO', node: { ID: 'e1', label: 'Airport 1' } }, + }, + }, + ], + }; + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); + + it('should run in case of loops and regular left', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + + const r1 = graph.addPill2Graphology({ + id: 'r1', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + + graph.addEdge2Graphology(e1, r1, { type: 'connection' }); + graph.addEdge2Graphology(e2, r1, { type: 'connection' }); + graph.addEdge2Graphology(r1, e1, { type: 'connection' }); + + const expected = { + ...defaultQuery, + query: [ + { + ID: 'path_0', + node: { + ID: 'e1', + label: 'Airport 1', + relation: { ID: 'r1', label: 'Flight between airports', direction: 'TO', node: { ID: 'e1', label: 'Airport 1' } }, + }, + }, + { + ID: 'path_1', + node: { + ID: 'e2', + label: 'Airport 1', + relation: { ID: 'r1', label: 'Flight between airports', direction: 'TO', node: { ID: 'e1', label: 'Airport 1' } }, + }, + }, + ], + }; + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); + + it('should run in case of direct entity connection', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + + graph.addEdge2Graphology(e1, e2, { type: 'connection' }); + + const expected = { + ...defaultQuery, + query: [ + { + ID: 'path_0', + node: { + ID: 'e1', + label: 'Airport 1', + relation: { direction: 'TO', node: { ID: 'e2', label: 'Airport 1' } }, + }, + }, + ], + }; + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); + + it('should run in case of direct relation connection', () => { + const graph = new QueryMultiGraphology(); + + const r1 = graph.addPill2Graphology({ + id: 'r1', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + + const r2 = graph.addPill2Graphology({ + id: 'r2', + type: QueryElementTypes.Relation, + x: 140, + y: 140, + name: 'Flight between airports 2', + collection: 'Relation Pill', + depth: { min: 0, max: 1 }, + }); + + graph.addEdge2Graphology(r1, r2, { type: 'connection' }); + + const expected = { + ...defaultQuery, + query: [ + { + ID: 'path_0', + node: { + relation: { + ID: 'r1', + label: 'Flight between airports', + direction: 'TO', + node: { relation: { ID: 'r2', label: 'Flight between airports 2', direction: 'TO', node: {} } }, + }, + }, + }, + ], + }; + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); + + it('should run in case of entity only loop', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + + graph.addEdge2Graphology(e1, e2, { type: 'connection' }); + graph.addEdge2Graphology(e2, e1, { type: 'connection' }); + + const expected = { + ...defaultQuery, + query: [ + { + ID: 'path_0', + node: { + ID: 'e1', + label: 'Airport 1', + relation: { direction: 'TO', node: { ID: 'e2', label: 'Airport 1' } }, + }, + }, + { + ID: 'path_1', + node: { + ID: 'e2', + label: 'Airport 1', + relation: { direction: 'TO', node: { ID: 'e1', label: 'Airport 1' } }, + }, + }, + ], + }; + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); + + it('should run in case of entity only self loop', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + + graph.addEdge2Graphology(e1, e1, { type: 'connection' }); + + const expected = { + ...defaultQuery, + query: [ + { + ID: 'path_0', + node: { ID: 'e1', label: 'Airport 1', relation: { direction: 'TO', node: { ID: 'e1', label: 'Airport 1' } } }, + }, + ], + }; + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); +}); + +describe('QueryUtils calculateQueryLogic', () => { + it('should run simple logic', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology( + { + id: 'e1', + type: QueryElementTypes.Entity, + x: 100, + y: 100, + name: 'Airport 1', + }, + [{ name: 'age', type: 'string' }] + ); + + const l1 = graph.addLogicPill2Graphology({ + id: 'l1', + type: QueryElementTypes.Logic, + x: 100, + y: 100, + name: 'Logic 1', + logic: MathFilters[MathFilterTypes.EQUAL], + }); + + graph.addEdge2Graphology(e1, l1, { type: 'connection' }, { sourceHandleName: 'age', targetHandleName: '1' }); + const graphExport = graph.export(); + let logics = graphExport.nodes.filter((n) => n?.attributes?.type === QueryElementTypes.Logic) as SerializedNode<LogicNodeAttributes>[]; + + const ret = calculateQueryLogic(logics[0], graphExport, logics); + console.log(ret); + }); +}); + +describe('QueryUtils with Logic', () => { + it('should run simple query with logic', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology( + { + id: 'e1', + type: QueryElementTypes.Entity, + x: 100, + y: 100, + name: 'Airport 1', + }, + [{ name: 'age', type: 'string' }] + ); + + const l1 = graph.addLogicPill2Graphology({ + id: 'l1', + type: QueryElementTypes.Logic, + x: 100, + y: 100, + name: 'Logic 1', + logic: MathFilters[MathFilterTypes.EQUAL], + }); + + graph.addEdge2Graphology(e1, l1, { type: 'connection' }, { sourceHandleName: 'age', targetHandleName: '1' }); + + const expected: BackendQueryFormat = { + ...defaultQuery, + logic: ['==', '@e1.age', 0], + query: [ + { + ID: 'path_0', + node: { + ID: 'e1', + label: 'Airport 1', + relation: undefined, + }, + }, + ], + }; + + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); + + it('should run add age and filter', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology( + { + id: 'e1', + type: QueryElementTypes.Entity, + x: 100, + y: 100, + name: 'Airport 1', + }, + [{ name: 'age', type: 'string' }] + ); + + const e2 = graph.addPill2Graphology( + { + id: 'e2', + type: QueryElementTypes.Entity, + x: 100, + y: 100, + name: 'Airport 2', + }, + [{ name: 'age', type: 'string' }] + ); + + const l1 = graph.addLogicPill2Graphology({ + id: 'l1', + type: QueryElementTypes.Logic, + x: 100, + y: 100, + name: 'Filter EQ', + logic: MathFilters[MathFilterTypes.EQUAL], + }); + + const l2 = graph.addLogicPill2Graphology({ + id: 'l2', + type: QueryElementTypes.Logic, + x: 100, + y: 100, + name: 'Logic ADD', + logic: MathFunctions[MathFunctionTypes.ADD], + }); + + graph.addEdge2Graphology(e1, l2, { type: 'connection' }, { sourceHandleName: 'age', targetHandleName: '1' }); + graph.addEdge2Graphology(e2, l2, { type: 'connection' }, { sourceHandleName: 'age', targetHandleName: '2' }); + graph.addEdge2Graphology(l2, l1, { type: 'connection' }, { sourceHandleName: MathFilterTypes.EQUAL, targetHandleName: '1' }); + + const expected: BackendQueryFormat = { + ...defaultQuery, + logic: ['==', ['+', '@e1.age', '@e2.age'], 0], + query: [ + { + ID: 'path_0', + node: { + ID: 'e1', + label: 'Airport 1', + relation: undefined, + }, + }, + { + ID: 'path_1', + node: { + ID: 'e2', + label: 'Airport 2', + relation: undefined, + }, + }, + ], + }; + + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); + + it('should handle average logic', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology( + { + id: 'e1', + type: QueryElementTypes.Entity, + x: 100, + y: 100, + name: 'Airport 1', + }, + [{ name: 'age', type: 'string' }] + ); + + const l1 = graph.addLogicPill2Graphology({ + id: 'l1', + type: QueryElementTypes.Logic, + x: 100, + y: 100, + name: 'Logic LT', + logic: MathFilters[MathFilterTypes.LESS_THAN], + }); + + const l2 = graph.addLogicPill2Graphology({ + id: 'l2', + type: QueryElementTypes.Logic, + x: 100, + y: 100, + name: 'Logic average', + logic: MathAggregations[MathAggregationTypes.AVG], + }); + + graph.addEdge2Graphology(e1, l2, { type: 'connection' }, { sourceHandleName: 'age', targetHandleName: '1' }); + graph.addEdge2Graphology(l2, l1, { type: 'connection' }, { sourceHandleName: MathAggregationTypes.AVG, targetHandleName: '1' }); + + const expected: BackendQueryFormat = { + ...defaultQuery, + logic: ['<', ['Avg', '@e1.age'], 0], + query: [ + { + ID: 'path_0', + node: { + ID: 'e1', + label: 'Airport 1', + relation: undefined, + }, + }, + ], + }; + + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); + + it('should allow custom values in logic pills', () => { + const graph = new QueryMultiGraphology(); + + const e1 = graph.addPill2Graphology( + { + id: 'e1', + type: QueryElementTypes.Entity, + x: 100, + y: 100, + name: 'Airport 1', + }, + [{ name: 'age', type: 'string' }] + ); + + const l1 = graph.addLogicPill2Graphology( + { + id: 'l1', + type: QueryElementTypes.Logic, + x: 100, + y: 100, + name: 'Logic LT', + logic: MathFilters[MathFilterTypes.LESS_THAN], + }, + { '2': 5 } + ); + + graph.addEdge2Graphology(e1, l1, { type: 'connection' }, { sourceHandleName: 'age', targetHandleName: '1' }); + + const expected: BackendQueryFormat = { + ...defaultQuery, + logic: ['<', '@e1.age', 5], + query: [ + { + ID: 'path_0', + node: { + ID: 'e1', + label: 'Airport 1', + relation: undefined, + }, + }, + ], + }; + + let ret = Query2BackendQuery('database', graph.export()); + // console.log(JSON.stringify(ret, null, 2)); + expect(ret).toMatchObject(expected); + }); +}); diff --git a/libs/shared/lib/querybuilder/query-utils/query2backend.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.ts new file mode 100644 index 0000000000000000000000000000000000000000..f4f74943dbd01ac7efa25e1fcc0c39273129efe2 --- /dev/null +++ b/libs/shared/lib/querybuilder/query-utils/query2backend.ts @@ -0,0 +1,311 @@ +import { EntityNodeAttributes, LogicNodeAttributes, QueryGraphNodes, RelationNodeAttributes } from '../model/graphology/model'; +import { QueryMultiGraph } from '../model/graphology/utils'; +import { BackendQueryFormat, NodeStruct, QueryStruct, RelationStruct } from '../model/BackendQueryFormat'; +import { Handles, QueryElementTypes, toHandleData } from '../model'; +import { get } from 'http'; +import { SerializedEdge, SerializedNode } from 'graphology-types'; +import { G } from 'vitest/dist/types-fafda418'; +import { hasCycle } from 'graphology-dag'; +import Graph, { MultiGraph } from 'graphology'; +import { allSimplePaths } from 'graphology-simple-path'; +import { AllLogicStatement, ReferenceStatement } from '../model/logic/general'; + +// export type QueryI { + +// } + +export type Query2BackendQueryOptions = { + limit?: number; +}; + +// Chunk extraction: traverse graph to find all paths of logic between relations and entities +const traverseEntityRelationPaths = ( + node: SerializedNode<QueryGraphNodes>, + paths: QueryGraphNodes[][], + currentIdx: number, + graph: QueryMultiGraph, + entities: SerializedNode<EntityNodeAttributes>[], + relations: SerializedNode<RelationNodeAttributes>[] +): number => { + if (!node?.attributes) throw Error('Malformed Graph! Node has no attributes'); + // console.log(paths); + + if (!paths?.[currentIdx]) { + // console.log('new path'); + paths.push([]); + if (node.attributes.type === QueryElementTypes.Relation) { + paths[currentIdx].push({ type: QueryElementTypes.Entity, x: node.attributes.x, y: node.attributes.y }); + } + } else if (paths[currentIdx].length > 0) { + const lastNode = paths[currentIdx][paths[currentIdx].length - 1]; + if (lastNode.type === node.attributes.type) { + if (lastNode.type === QueryElementTypes.Entity) { + paths[currentIdx].push({ type: QueryElementTypes.Relation, x: node.attributes.x, y: node.attributes.x }); + } else { + paths[currentIdx].push({ type: QueryElementTypes.Entity, x: node.attributes.x, y: node.attributes.x }); + } + } + } + paths[currentIdx].push(node.attributes); + // if ( + // (node.attributes.type === QueryElementTypes.Entity || node.attributes.type === QueryElementTypes.Relation) && + // paths[currentIdx].some((n) => n.type === QueryElementTypes.Logic) + // ) { + // return 0; + // } + + // const rightHandle = + // node.type === QueryElementTypes.Entity + // ? (node as EntityNodeAttributes)?.rightRelationHandleId + // : (node as RelationNodeAttributes)?.rightEntityHandleId; + + const edges = graph.edges.filter( + (n) => + n?.attributes?.sourceHandleData.nodeType !== QueryElementTypes.Logic && + n?.attributes?.targetHandleData.nodeType !== QueryElementTypes.Logic + ); + let connections = edges.filter((e) => e.source === node.key); + if (connections.length === 0) { + if (node.attributes.type === QueryElementTypes.Relation) { + paths[currentIdx].push({ type: QueryElementTypes.Entity, x: node.attributes.x, y: node.attributes.x }); + } + return 0; + } + // console.log('connections', connections); + + const nodesToRight = connections + .map((c, i) => { + const rightNodeHandleData = c.attributes?.targetHandleData; + if (!rightNodeHandleData) throw Error('Malformed Graph! One or more edges of a relation node do not exist'); + + // console.log('nodesToRight', c); + // console.log('entities', entities); + if (paths[currentIdx].length > 10 || currentIdx > 10) + throw Error('Malformed Graph! One or more edges of a relation node do not exist'); + const rightNode = + rightNodeHandleData.nodeType === QueryElementTypes.Entity + ? entities.find((r) => r.key === c.target) + : relations.find((r) => r.key === c.target); + return rightNode; + }) + .filter((n) => n !== undefined) as SerializedNode<QueryGraphNodes>[]; + + let chunkOffset = 0; + let pathBeforeTraversing = [...paths[currentIdx]]; + nodesToRight.forEach((rightNode, i) => { + if (i > 0) { + paths.push([...pathBeforeTraversing]); // clone previous path in case of branching + } + chunkOffset += traverseEntityRelationPaths(rightNode, paths, currentIdx + i + chunkOffset, graph, entities, relations); + }); + + return chunkOffset + nodesToRight.length - 1; // offset +}; + +export function calculateQueryLogic( + node: SerializedNode<LogicNodeAttributes>, + graph: QueryMultiGraph, + logics: SerializedNode<LogicNodeAttributes>[] +): AllLogicStatement { + if (!node?.attributes) throw Error('Malformed Graph! Node has no attributes'); + let connectionsToLeft = graph.edges.filter((e) => e.target === node.key); + + let ret = [...node.attributes.logic.logic].map((l) => { + if (typeof l !== 'string') throw Error('Malformed Graph! Logic node has no logic'); + if (!node.attributes) throw Error('Malformed Graph! Logic node has no attributes'); + + if (l.includes('@')) { + // @ means it needs to fetch data from connection + const inputRefIdx = node.attributes.logic.inputs.findIndex((input, i) => input.name === l.replace('@', '')); // fetches the corresponding element from input definition + const inputRef = node.attributes.logic.inputs[inputRefIdx]; + if (!inputRef) throw Error('Malformed Graph! Logic node has incorrect input reference'); + + const connectionToInputRef = connectionsToLeft.find((c) => c?.attributes?.targetHandleData.attributeName === inputRef.name); + if (!connectionToInputRef) { + // Not connected, search for set or default value + let val = node.attributes.inputs?.[inputRef.name] || inputRef.default; + if (val && inputRef.type === 'string') val = `\"${val}\"`; + console.log('val', val, inputRef, node); + return val; + } else if (connectionToInputRef.attributes?.sourceHandleData.nodeType === QueryElementTypes.Logic) { + // Is connected to another logic node + const leftLogic = logics.find((r) => r.key === connectionToInputRef.attributes?.sourceHandleData.nodeId); + if (!leftLogic) throw Error('Malformed Graph! Logic node is connected but has no logic data'); + return calculateQueryLogic(leftLogic, graph, logics); + } else { + if (!connectionToInputRef.attributes?.sourceHandleData) + throw Error('Malformed Graph! Logic node is connected but has no sourceHandleData'); + // Is connected to entity or relation node + return `@${connectionToInputRef.attributes.sourceHandleData.nodeId}.${connectionToInputRef.attributes.sourceHandleData.attributeName}`; + } + } else { + return l; + } + }); + return ret as AllLogicStatement; +} + +function queryLogicUnion(graphLogicChunks: AllLogicStatement[]): AllLogicStatement | undefined { + if (graphLogicChunks.length === 1) return graphLogicChunks[0]; + else if (graphLogicChunks.length > 1) { + return ['And', graphLogicChunks[0], queryLogicUnion(graphLogicChunks.slice(1)) || '0']; + } + return undefined; +} + +/** + * 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, + options: Query2BackendQueryOptions = {} +): BackendQueryFormat { + let query: BackendQueryFormat = { + databaseName: databaseName, + query: [], + limit: options.limit || 500, + return: ['*'], // TODO + }; + + let entities = graph.nodes.filter((n) => n?.attributes?.type === QueryElementTypes.Entity) as SerializedNode<EntityNodeAttributes>[]; + let relations = graph.nodes.filter((n) => n?.attributes?.type === QueryElementTypes.Relation) as SerializedNode<RelationNodeAttributes>[]; + + const graphologyQuery = Graph.from(graph); + graphologyQuery + .filterNodes((n, att) => att?.type == QueryElementTypes.Logic) + .forEach((n) => { + graphologyQuery.dropNode(n); + }); // Remove all logic nodes from the graph for cycle test + if (hasCycle(graphologyQuery)) { + const cycles = entities.map((entity, i) => { + return allSimplePaths(graphologyQuery, entity.key, entity.key); + }); + cycles.forEach((cycles_inner, i) => { + cycles_inner.forEach((cycle, 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); + // edgeAttributes.target = newOrigin; + graphologyQuery.addEdge(target, newOrigin, edgeAttributes); + }); + }); + + console.log('graph', graph); + return Query2BackendQuery(databaseName, graphologyQuery.export()); + + // if ( + // relations.some((entity, i) => { + // return allSimplePaths(graphologyQuery, entity.id, entity.id); + // }) + // ) + // throw Error('Cycles in query are not supported yet'); + // console.log('cycles', cycles); + // return null; + // } + // return null; + } + // Chunk extraction: traverse graph to find all paths of logic between relations and entities + let graphSequenceChunks: QueryGraphNodes[][] = []; + let graphSequenceLogicChunks: QueryGraphNodes[][] = []; + let graphSequenceChunksIdMap: Record<string, [number, number]> = {}; + let chunkOffset = 0; + + let entitiesEmptyLeftHandle = entities.filter((n) => !graph.edges.some((e) => e.target === n.key)); + let relationsEmptyLeftHandle = relations.filter((n) => !graph.edges.some((e) => e.target === n.key)); + // let entitiesEmptyRightHandle = entities.filter((n) => !n?.attributes?.rightRelationHandleId); + entitiesEmptyLeftHandle.map((entity, i) => { + // start with all entities that have no left handle, which means it "starts" a logic + chunkOffset += traverseEntityRelationPaths(entity, graphSequenceChunks, i + chunkOffset, graph, entities, relations); + }); + if (entitiesEmptyLeftHandle.length > 0) chunkOffset++; + relationsEmptyLeftHandle.map((entity, i) => { + // then, for all relations that have no left handle, since they weren't accounted by the loop above + chunkOffset += traverseEntityRelationPaths(entity, graphSequenceChunks, i + chunkOffset, graph, entities, relations); + }); + graphSequenceChunks.forEach((chunkSequence, i) => { + chunkSequence.forEach((chunk, j) => { + graphSequenceChunksIdMap[chunk.id || chunk.name || ''] = [i, j]; + }); + }); + + // Logic pathways extraction: now traverse the graph again though the logic components to construct the logic chunks + // The traversal is done in reverse order, starting from the logic pill's right handle connected to an entity or relation, and going backwards + let logics = graph.nodes.filter((n) => n?.attributes?.type === QueryElementTypes.Logic) as SerializedNode<LogicNodeAttributes>[]; + let logicsRightHandleConnectedOutside = logics.filter((n) => { + return graph.edges.some((e) => e.source === n.key && e.attributes?.targetHandleData.nodeType === QueryElementTypes.Entity); + }); + let logicsRightHandleFinal = logics.filter((n) => { + return !graph.edges.some((e) => e.source === n.key); + }); + let graphLogicChunks = logicsRightHandleFinal.map((node) => calculateQueryLogic(node, graph, logics)); + query.logic = queryLogicUnion(graphLogicChunks); + + // Logic pathways extraction: now traverse the graph again though the logic components to construct the logic chunks + + // console.log('logics', logics); + // console.log('graphSequenceChunks', graphSequenceChunks); + // console.log('graphLogicChunks', graphLogicChunks); + // console.log('logicsRightHandleConnectedOutside', logicsRightHandleConnectedOutside); + // console.log('logicsRightHandleFinal', logicsRightHandleFinal); + // console.log('graphSequenceChunksIdMap', graphSequenceChunksIdMap); + if (!graphSequenceChunks || graphSequenceChunks.length === 0 || graphSequenceChunks?.[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 || undefined, + // 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 = graphSequenceChunks.map((chunk, i) => { + const ret: QueryStruct = { + ID: 'path_' + i, //TODO: chunk[0].id || + node: processConnection(chunk, 0), + }; + return ret; + }); + + console.debug('New query', graph, 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/model/reactflow.tsx b/libs/shared/lib/schema/model/reactflow.tsx index da2e44015744bc2bfac43adfdeffbc04f48bb73d..88dc8dc5512e9753b27e36e604de38c177fd2c5e 100644 --- a/libs/shared/lib/schema/model/reactflow.tsx +++ b/libs/shared/lib/schema/model/reactflow.tsx @@ -34,6 +34,7 @@ export interface SchemaReactflowData { attributes: SchemaGraphologyNode[]; nodeCount: number; summedNullAmount: number; + label: string; } export interface SchemaReactflowNode extends SchemaReactflowData { diff --git a/libs/shared/lib/schema/panel/schema.tsx b/libs/shared/lib/schema/panel/schema.tsx index 2c5ee5dee4e726a4ba4485c7c44728e8d7263505..846faaeeab9806381efa6a450c8d7f8d5ad0092e 100644 --- a/libs/shared/lib/schema/panel/schema.tsx +++ b/libs/shared/lib/schema/panel/schema.tsx @@ -127,8 +127,7 @@ export const Schema = (props: Props) => { return; } // console.log(schemaGraphology.export()); - - console.log(schemaLayout); + // console.log(schemaLayout); updateLayout(); const expandedSchema = schemaExpandRelation(schemaGraphology); diff --git a/libs/shared/lib/schema/panel/view-model/schemaViewModel.test.t b/libs/shared/lib/schema/panel/view-model/schemaViewModel.test.t index 2460545f69f3985250eea8a587403452e7172973..1eaaf31a0e5c3078ae3cad44b2d3b08a1a1a8672 100644 --- a/libs/shared/lib/schema/panel/view-model/schemaViewModel.test.t +++ b/libs/shared/lib/schema/panel/view-model/schemaViewModel.test.t @@ -1,1320 +1,1320 @@ -/** - * This program has been developed by students from the bachelor Computer Science at - * Utrecht University within the Software Project course. - * © Copyright Utrecht University (Department of Information and Computing Sciences) - */ -import { - schema, - schema2, -} from '../../../data/mock-data/graph-schema/MockGraph'; -import { - mockAttributeDataNLEdge2, - mockAttributeDataNLEdge2IncorrectId, - mockAttributeDataNLNode1, - mockAttributeDataNLNode2, - mockAttributeDataNLNode2IncorrectId, -} from '../../../data/mock-data/graph-schema/MockAttributeDataBatchedNL'; -import SecondChamberSchemaMock from '../../../data/mock-data/schema-result/2ndChamberSchemaMock'; -import GraphUseCase from '../../../domain/usecases/graph-schema/GraphUseCase'; -import SchemaViewModelImpl from './SchemaViewModel'; -import { Node, Edge, ArrowHeadType } from 'react-flow-renderer'; -import DrawOrderUseCase from '../../../domain/usecases/graph-schema/DrawOrderUseCase'; -import EdgeUseCase from '../../../domain/usecases/graph-schema/EdgeUseCase'; -import NodeUseCase from '../../../domain/usecases/graph-schema/NodeUseCase'; -import { - AttributeCategory, - BoundingBox, - NodeQualityDataForEntities, - NodeQualityDataForRelations, - NodeType, -} from '../../../domain/entity/graph-schema/structures/Types'; -import { - Attribute, - AttributeData, -} from '../../../domain/entity/graph-schema/structures/InputDataTypes'; -import mockQueryResult from '../../../data/mock-data/query-result/big2ndChamberQueryResult'; -import mockSchemaResult from '../../../data/mock-data/schema-result/2ndChamberSchemaMock'; -import Broker from '../../../domain/entity/broker/broker'; - -jest.mock('../../view/graph-schema/SchemaStyleSheet'); -jest.mock('../../util/graph-schema/utils.tsx', () => { - return { - //TODO Is this already updated? - getWidthOfText: () => { - return 10; - }, - calcWidthRelationNodeBox: () => { - return 75; - }, - calcWidthEntityNodeBox: () => { - return 75; - }, - makeBoundingBox: (x: number, y: number, width: number, height: number) => { - let boundingBox: BoundingBox; - boundingBox = { - topLeft: { x: x, y: y }, - bottomRight: { x: x + width, y: y + height }, - }; - return boundingBox; - }, - doBoxesOverlap: (firstBB: BoundingBox, secondBB: BoundingBox) => { - if ( - firstBB.topLeft.x >= secondBB.bottomRight.x || - secondBB.topLeft.x >= firstBB.bottomRight.x - ) - return false; - - if ( - firstBB.topLeft.y >= secondBB.bottomRight.y || - secondBB.topLeft.y >= firstBB.bottomRight.y - ) - return false; - - return true; - }, - }; -}); - -describe('schemaViewModelImpl', () => { - beforeEach(() => jest.resetModules()); - const graphUseCase = new GraphUseCase(); - const drawOrderUseCase = new DrawOrderUseCase(); - const nodeUseCase = new NodeUseCase(); - const edgeUseCase = new EdgeUseCase(); - const attributesInQueryBuilder: any = []; - const addAttribute = (name: string, type: string) => { - attributesInQueryBuilder.push({ name: name, type: type }); - return; - }; - - function anonymous( - attributes: Attribute[], - id: string, - hiddenAttributes: boolean - ): void { - //Empty methode. - } - - it('should create a relation', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - - const expectedrelationode = { - type: 'relation', - id: '5', - position: { x: -72.5, y: 20 }, - data: { - width: 220, - height: 20, - collection: 'none', - attributes: [], - from: 'from:here', - to: 'to:here', - nodeCount: 0, - summedNullAmount: 0, - fromRatio: 0, - toRatio: 0, - }, - }; - - schemaViewModel.createRelationNode( - '5', - [], - 'none', - schemaViewModel.elements - ); - schemaViewModel.setRelationNodePosition( - 0, - 0, - '5', - 'from:here', - 'to:here', - [] - ); - expect(JSON.stringify(schemaViewModel.elements.nodes[0])).toEqual( - JSON.stringify(expectedrelationode) - ); - - const expectedrelationode2 = { - type: 'relation', - id: '6', - position: { x: -72.5, y: 40 }, - data: { - width: 220, - height: 20, - collection: 'none', - attributes: [], - from: 'from:hereagain', - to: 'to:hereagain', - nodeCount: 0, - summedNullAmount: 0, - fromRatio: 0, - toRatio: 0, - }, - }; - schemaViewModel.createRelationNode( - '6', - [], - 'none', - schemaViewModel.elements - ); - schemaViewModel.setRelationNodePosition( - 0, - 0, - '6', - 'from:hereagain', - 'to:hereagain', - [] - ); - expect(JSON.stringify(schemaViewModel.elements.nodes[1])).toEqual( - JSON.stringify(expectedrelationode2) - ); - }); - - it('should console log that method is not implemented', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - const consoleSpy = jest.spyOn(console, 'log'); - schemaViewModel.exportToPDF(); - expect(consoleSpy).toHaveBeenCalledWith('Method not implemented.'); - }); - - it('fitToView', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - const getWidthHeight = { width: 300, height: 700 }; - const spy = jest.spyOn(schemaViewModel, 'getWidthHeight'); - spy.mockReturnValue(getWidthHeight); - - schemaViewModel.consumeMessageFromBackend(mockSchemaResult); - schemaViewModel.fitToView(); - - const consoleSpy = jest.spyOn(console, 'log'); - expect(consoleSpy).toHaveBeenCalledWith( - 'this.reactFlowInstance is undefined!' - ); - }); - - /** - * These are the testcases for consuming messages from the backend - */ - describe('consumeMessageFromBackend', () => { - it('should consume schema result only when subscribed to broker for schema_result', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - const mockConsumeMessages = jest.fn(); - const message = 'test schema result'; - - schemaViewModel.consumeMessageFromBackend = mockConsumeMessages; - - // should consume message for schema result when subscribed - schemaViewModel.subscribeToSchemaResult(); - Broker.instance().publish(message, 'schema_result'); - expect(mockConsumeMessages.mock.calls[0][0]).toEqual(message); - expect(mockConsumeMessages).toBeCalledTimes(1); - - // should not consume message for query_result - Broker.instance().publish(message, 'query_result'); - expect(mockConsumeMessages).toBeCalledTimes(1); - - // should not consume message for schema result when unsubscribed - schemaViewModel.unSubscribeFromSchemaResult(); - Broker.instance().publish(message, 'schema_result'); - expect(mockConsumeMessages).toBeCalledTimes(1); - }); - - it('should consume attribute-data only when subscribed to broker for gsa_node_result & gsa_edge_result', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - const mockConsumeMessages = jest.fn(); - const message = 'test analytics data'; - - schemaViewModel.consumeMessageFromBackend = mockConsumeMessages; - - // should consume message for schema result when subscribed - schemaViewModel.subscribeToAnalyticsData(); - Broker.instance().publish(message, 'gsa_node_result'); - Broker.instance().publish(message, 'gsa_edge_result'); - expect(mockConsumeMessages.mock.calls[0][0]).toEqual(message); - expect(mockConsumeMessages).toBeCalledTimes(2); - - // should not consume message for schema result when unsubscribed - schemaViewModel.unSubscribeFromAnalyticsData(); - Broker.instance().publish(message, 'schema_result'); - Broker.instance().publish(message, 'gsa_node_result'); - Broker.instance().publish(message, 'gsa_edge_result'); - expect(mockConsumeMessages).toBeCalledTimes(2); - }); - - //TODO: also test the message for the analytics - it('should console log and should not change any elements when receiving unrelated messages', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - const consoleSpy = jest.spyOn(console, 'log'); - - expect(schemaViewModel.visible).toEqual(true); - schemaViewModel.consumeMessageFromBackend(mockQueryResult); - expect(consoleSpy).toHaveBeenCalledWith('This is no valid input!'); - - expect(schemaViewModel.visible).toEqual(true); - expect(schemaViewModel.elements.nodes).toEqual([]); - expect(schemaViewModel.elements.edges).toEqual([]); - }); - }); - - /** - * These are the testcases for the attribute-analytics popup menu - */ - describe('AttributeAnalyticsPopupMenu', () => { - it('should throw error that given id does not exist', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.consumeMessageFromBackend(schema); - // Nodes that are not in elements cannot have popups - const name = 'Edje'; - expect( - schemaViewModel.elements.nodes.find((node) => node.id == name) - ).toEqual(undefined); - expect(() => - schemaViewModel.toggleAttributeAnalyticsPopupMenu(name) - ).toThrowError( - 'Node ' + name + ' does not exist therefore no popup menu can be shown.' - ); - }); - - it('should show the attribute-analytics popup menu', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - const popup = schemaViewModel.attributeAnalyticsPopupMenu; - schemaViewModel.consumeMessageFromBackend(schema); - - // test for entity node - const node = schemaViewModel.elements.nodes[0]; - schemaViewModel.toggleAttributeAnalyticsPopupMenu(node.id); - expect(popup.isHidden).toEqual(false); - expect(popup.nodeID).toEqual(node.id); - - // test for relation node - const edge = schemaViewModel.elements.edges[0]; - schemaViewModel.setRelationNodePosition( - 0, - 0, - edge.id, - edge.source, - edge.target, - edge.data.attributes - ); - schemaViewModel.toggleAttributeAnalyticsPopupMenu(edge.id); - expect(popup.isHidden).toEqual(false); - expect(popup.nodeID).toEqual(edge.id); - }); - - it('should hide the attribute-analytics popup menu as it was already open', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.consumeMessageFromBackend(schema); - schemaViewModel.attributeAnalyticsPopupMenu.nodeID = 'Thijs'; - schemaViewModel.attributeAnalyticsPopupMenu.isHidden = false; - schemaViewModel.toggleAttributeAnalyticsPopupMenu('Thijs'); - expect(schemaViewModel.attributeAnalyticsPopupMenu.isHidden).toEqual( - true - ); - }); - - it('should close the attribute-analytics menu', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.consumeMessageFromBackend(schema); - schemaViewModel.attributeAnalyticsData['Thijs'].onClickCloseButton(); - expect(schemaViewModel.attributeAnalyticsPopupMenu.isHidden).toEqual( - true - ); - }); - - it('should make an empty attribute-analytics popupmenu', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.attributeAnalyticsPopupMenu = - schemaViewModel.emptyAttributeAnalyticsPopupMenuNode(); - expect(schemaViewModel.attributeAnalyticsPopupMenu.id).toEqual( - 'attributeAnalyticsPopupMenu' - ); - expect(schemaViewModel.attributeAnalyticsPopupMenu.nodeID).toEqual(''); - expect(schemaViewModel.attributeAnalyticsPopupMenu.data.nodeType).toEqual( - NodeType.relation - ); - expect( - schemaViewModel.attributeAnalyticsPopupMenu.data.attributes - ).toEqual([]); - expect( - schemaViewModel.attributeAnalyticsPopupMenu.data.isAttributeDataIn - ).toEqual(false); - }); - - it('should place an attribute node in the querybuilder', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.consumeMessageFromBackend(schema); - const node = schemaViewModel.elements.nodes[0]; - const attribute = node.data.attributes; - schemaViewModel.attributeAnalyticsData[ - node.id - ].onClickPlaceInQueryBuilderButton(attribute.name, attribute.type); - expect(attributesInQueryBuilder[0]).toEqual({ - name: attribute.name, - type: attribute.type, - }); - }); - - it('should have a working searchbar', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.consumeMessageFromBackend(mockSchemaResult); - schemaViewModel.searchForAttributes('kamerleden', 'aa'); - - let attributes = - schemaViewModel.attributeAnalyticsPopupMenu.data.attributes; - expect(attributes.length).toEqual(2); - - schemaViewModel.searchForAttributes('kamerleden', ''); - attributes = schemaViewModel.attributeAnalyticsPopupMenu.data.attributes; - expect(attributes.length).toEqual(6); - }); - - it('should have working filters', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.consumeMessageFromBackend(mockSchemaResult); - schemaViewModel.consumeMessageFromBackend(mockAttributeDataNLNode1); - - schemaViewModel.applyAttributeFilters( - 'kamerleden', - AttributeCategory.categorical, - 'Bigger', - -1 - ); - let attributes = - schemaViewModel.attributeAnalyticsPopupMenu.data.attributes; - expect(attributes.length).toEqual(1); - - schemaViewModel.resetAttributeFilters('kamerleden'); - attributes = schemaViewModel.attributeAnalyticsPopupMenu.data.attributes; - expect(attributes.length).toEqual(6); - }); - }); - - /** - * These are the testcases for the node-quality popup menu - */ - describe('nodeQualityPopup', () => { - it('should throw error that given id does not exist', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.consumeMessageFromBackend(schema); - expect(() => schemaViewModel.toggleNodeQualityPopup('Edje')).toThrowError( - 'Node does not exist therefore no popup can be shown.' - ); - }); - - it('should show the node-quality popup menu for an entity node', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.consumeMessageFromBackend(SecondChamberSchemaMock); - schemaViewModel.toggleNodeQualityPopup('commissies'); - expect(schemaViewModel.nodeQualityPopup.isHidden).toEqual(false); - expect(schemaViewModel.nodeQualityPopup.type).toEqual( - 'nodeQualityEntityPopup' - ); - expect(schemaViewModel.nodeQualityPopup.nodeID).toEqual('commissies'); - }); - - it('should show the node-quality popup menu for a relation node', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.createRelationNode( - '5', - [], - '5', - schemaViewModel.elements - ); - schemaViewModel.toggleNodeQualityPopup('5'); - expect(schemaViewModel.nodeQualityPopup.isHidden).toEqual(false); - expect(schemaViewModel.nodeQualityPopup.type).toEqual( - 'nodeQualityRelationPopup' - ); - expect(schemaViewModel.nodeQualityPopup.nodeID).toEqual('5'); - }); - - it('should hide the node-quality popup menu as it was already open', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.consumeMessageFromBackend(schema); - schemaViewModel.nodeQualityPopup.nodeID = 'Thijs'; - schemaViewModel.nodeQualityPopup.isHidden = false; - schemaViewModel.toggleNodeQualityPopup('Thijs'); - expect(schemaViewModel.nodeQualityPopup.isHidden).toEqual(true); - }); - - it('should close the node-quality menu', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.consumeMessageFromBackend(schema); - schemaViewModel.nodeQualityData['Thijs'].onClickCloseButton(); - expect(schemaViewModel.nodeQualityPopup.isHidden).toEqual(true); - }); - - it('should make an empty node-quality popupmenu', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.nodeQualityPopup = - schemaViewModel.emptyNodeQualityPopupNode(); - expect(schemaViewModel.nodeQualityPopup.id).toEqual('nodeQualityPopup'); - expect(schemaViewModel.nodeQualityPopup.nodeID).toEqual(''); - expect(schemaViewModel.nodeQualityPopup.data.nodeCount).toEqual(0); - expect(schemaViewModel.nodeQualityPopup.data.attributeNullCount).toEqual( - 0 - ); - expect(schemaViewModel.nodeQualityPopup.data.isAttributeDataIn).toEqual( - false - ); - }); - }); - - describe('AttributeData', () => { - it('should process the incoming data correctly for node-data', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.consumeMessageFromBackend(mockSchemaResult); - schemaViewModel.consumeMessageFromBackend(mockAttributeDataNLNode2); - - const attributeAnalyticsData = - schemaViewModel.attributeAnalyticsData['commissies']; - const nodeQualityData = schemaViewModel.nodeQualityData[ - 'commissies' - ] as NodeQualityDataForEntities; - expect(attributeAnalyticsData.isAttributeDataIn).toEqual(true); - expect(attributeAnalyticsData.attributes[0].category).toEqual( - AttributeCategory.other - ); - expect(attributeAnalyticsData.attributes[0].nullAmount).toEqual(1); - expect(nodeQualityData.nodeCount).toEqual(38); - expect(nodeQualityData.attributeNullCount).toEqual(1); - expect(nodeQualityData.notConnectedNodeCount).toEqual(0.03); - }); - - it('should process the incoming data correctly for edge-data', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.consumeMessageFromBackend(mockSchemaResult); - schemaViewModel.consumeMessageFromBackend(mockAttributeDataNLEdge2); - - const attributeAnalyticsData = - schemaViewModel.attributeAnalyticsData['lid_van']; - const nodeQualityData = schemaViewModel.nodeQualityData[ - 'lid_van' - ] as NodeQualityDataForRelations; - expect(attributeAnalyticsData.isAttributeDataIn).toEqual(true); - expect(attributeAnalyticsData.attributes[0].category).toEqual( - AttributeCategory.categorical - ); - expect(attributeAnalyticsData.attributes[0].nullAmount).toEqual(0); - expect(nodeQualityData.nodeCount).toEqual(149); - expect(nodeQualityData.attributeNullCount).toEqual(0); - expect(nodeQualityData.fromRatio).toEqual(1); - expect(nodeQualityData.toRatio).toEqual(1); - }); - - it('should console log when the given data has no corresponding id for entities', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.consumeMessageFromBackend(mockSchemaResult); - - const consoleSpy = jest.spyOn(console, 'log'); - schemaViewModel.consumeMessageFromBackend( - mockAttributeDataNLNode2IncorrectId - ); - - expect(consoleSpy).toBeCalledTimes(2); - }); - - it('should console log when the given data has no corresponding id for relations', () => { - const schemaViewModel = new SchemaViewModelImpl( - drawOrderUseCase, - nodeUseCase, - edgeUseCase, - graphUseCase, - addAttribute - ); - schemaViewModel.consumeMessageFromBackend(mockSchemaResult); - - const consoleSpy = jest.spyOn(console, 'log'); - schemaViewModel.consumeMessageFromBackend( - mockAttributeDataNLEdge2IncorrectId - ); - - expect(consoleSpy).toBeCalledTimes(2); - }); - }); - - /** expected results */ - - const expectedNodesInElements: Node[] = [ - { - type: 'entity', - id: 'Airport', - position: { x: 0, y: 0 }, - data: { - attributes: [ - { name: 'city', type: 'string' }, - { name: 'vip', type: 'bool' }, - { name: 'state', type: 'string' }, - ], - handles: [ - 'entityTargetBottom', - 'entityTargetRight', - 'entityTargetRight', - 'entitySourceLeft', - 'entitySourceLeft', - 'entitySourceLeft', - 'entityTargetRight', - ], - width: 165, - height: 20, - nodeCount: 0, - summedNullAmount: 0, - connectedRatio: 0, - }, - }, - { - type: 'entity', - id: 'Plane', - position: { x: 0, y: 150 }, - data: { - attributes: [ - { name: 'type', type: 'string' }, - { name: 'maxFuelCapacity', type: 'int' }, - ], - handles: ['entitySourceTop', 'entityTargetBottom', 'entityTargetRight'], - width: 165, - height: 20, - nodeCount: 0, - summedNullAmount: 0, - connectedRatio: 0, - }, - }, - { - type: 'entity', - id: 'Staff', - position: { x: 0, y: 300 }, - data: { - attributes: [], - handles: ['entityTargetLeft', 'entitySourceTop', 'entitySourceBottom'], - width: 165, - height: 20, - nodeCount: 0, - summedNullAmount: 0, - connectedRatio: 0, - }, - }, - { - type: 'entity', - id: 'Airport2', - position: { x: 0, y: 450 }, - data: { - attributes: [ - { name: 'city', type: 'string' }, - { name: 'vip', type: 'bool' }, - { name: 'state', type: 'string' }, - ], - handles: ['entitySourceRight', 'entitySourceRight', 'entityTargetTop'], - width: 165, - height: 20, - nodeCount: 0, - summedNullAmount: 0, - connectedRatio: 0, - }, - }, - - { - type: 'entity', - id: 'Thijs', - position: { x: 0, y: 600 }, - data: { - attributes: [], - handles: ['entitySourceRight', 'entityTargetLeft'], - width: 165, - height: 20, - nodeCount: 0, - summedNullAmount: 0, - connectedRatio: 0, - }, - }, - - { - type: 'entity', - id: 'Unconnected', - position: { x: 0, y: 750 }, - data: { - attributes: [], - handles: [], - width: 165, - height: 20, - nodeCount: 0, - summedNullAmount: 0, - connectedRatio: 0, - }, - }, - { - type: 'relation', - id: 'flights', - position: { x: 0, y: 0 }, - data: { - width: 220, - height: 40, - collection: 'flights', - attributes: [ - { - name: 'arrivalTime', - type: 'int', - }, - { - name: 'departureTime', - type: 'int', - }, - ], - from: '', - to: '', - nodeCount: 0, - summedNullAmount: 0, - fromRatio: 0, - toRatio: 0, - }, - }, - { - type: 'relation', - id: 'flights', - position: { x: 0, y: 0 }, - data: { - width: 220, - height: 40, - collection: 'flights', - attributes: [ - { - name: 'salary', - type: 'int', - }, - ], - from: '', - to: '', - nodeCount: 0, - summedNullAmount: 0, - fromRatio: 0, - toRatio: 0, - }, - }, - { - type: 'relation', - id: 'flights', - position: { x: 0, y: 0 }, - data: { - width: 220, - height: 40, - collection: 'flights', - attributes: [], - from: '', - to: '', - nodeCount: 0, - summedNullAmount: 0, - fromRatio: 0, - toRatio: 0, - }, - }, - { - type: 'relation', - id: 'flights', - position: { x: 0, y: 0 }, - data: { - width: 220, - height: 40, - collection: 'flights', - attributes: [ - { - name: 'hallo', - type: 'string', - }, - ], - from: '', - to: '', - nodeCount: 0, - summedNullAmount: 0, - fromRatio: 0, - toRatio: 0, - }, - }, - { - type: 'relation', - id: 'flights', - position: { x: 0, y: 0 }, - data: { - width: 220, - height: 40, - collection: 'flights', - attributes: [ - { - name: 'hallo', - type: 'string', - }, - ], - from: '', - to: '', - nodeCount: 0, - summedNullAmount: 0, - fromRatio: 0, - toRatio: 0, - }, - }, - { - type: 'relation', - id: 'flights', - position: { x: 0, y: 0 }, - data: { - width: 220, - height: 40, - collection: 'flights', - attributes: [ - { - name: 'hallo', - type: 'string', - }, - ], - from: '', - to: '', - nodeCount: 0, - summedNullAmount: 0, - fromRatio: 0, - toRatio: 0, - }, - }, - { - type: 'relation', - id: 'flights', - position: { x: 0, y: 0 }, - data: { - width: 220, - height: 40, - collection: 'flights', - attributes: [ - { - name: 'hallo', - type: 'string', - }, - ], - from: '', - to: '', - nodeCount: 0, - summedNullAmount: 0, - fromRatio: 0, - toRatio: 0, - }, - }, - { - type: 'relation', - id: 'flights', - position: { x: 0, y: 0 }, - data: { - width: 220, - height: 40, - collection: 'flights', - attributes: [ - { - name: 'hallo', - type: 'string', - }, - ], - from: '', - to: '', - nodeCount: 0, - summedNullAmount: 0, - fromRatio: 0, - toRatio: 0, - }, - }, - { - type: 'relation', - id: 'flights', - position: { x: 0, y: 0 }, - data: { - width: 220, - height: 40, - collection: 'flights', - attributes: [ - { - name: 'test', - type: 'string', - }, - ], - from: '', - to: '', - nodeCount: 0, - summedNullAmount: 0, - fromRatio: 0, - toRatio: 0, - }, - }, - ]; - - const expectedEdgesInElements: Edge[] = [ - { - id: 'flights', - source: 'Plane', - target: 'Airport', - type: 'nodeEdge', - label: 'Plane:Airport', - data: { - attributes: [], - d: 0, - created: false, - collection: 'flights', - edgeCount: 0, - view: anonymous, - }, - arrowHeadType: ArrowHeadType.Arrow, - sourceHandle: 'entitySourceTop', - targetHandle: 'entityTargetBottom', - }, - { - id: 'flights', - source: 'Airport2', - target: 'Airport', - type: 'nodeEdge', - label: 'Airport2:Airport', - data: { - attributes: [ - { name: 'arrivalTime', type: 'int' }, - { name: 'departureTime', type: 'int' }, - ], - d: 40, - created: false, - collection: 'flights', - edgeCount: 0, - view: anonymous, - }, - arrowHeadType: ArrowHeadType.Arrow, - sourceHandle: 'entitySourceRight', - targetHandle: 'entityTargetRight', - }, - { - id: 'flights', - source: 'Thijs', - target: 'Airport', - type: 'nodeEdge', - label: 'Thijs:Airport', - data: { - attributes: [{ name: 'hallo', type: 'string' }], - d: 80, - created: false, - collection: 'flights', - edgeCount: 0, - view: anonymous, - }, - arrowHeadType: ArrowHeadType.Arrow, - sourceHandle: 'entitySourceRight', - targetHandle: 'entityTargetRight', - }, - { - id: 'flights', - source: 'Airport', - target: 'Staff', - type: 'nodeEdge', - label: 'Airport:Staff', - data: { - attributes: [{ name: 'salary', type: 'int' }], - d: -40, - created: false, - collection: 'flights', - edgeCount: 0, - view: anonymous, - }, - arrowHeadType: ArrowHeadType.Arrow, - sourceHandle: 'entitySourceLeft', - targetHandle: 'entityTargetLeft', - }, - { - id: 'flights', - source: 'Airport', - target: 'Thijs', - type: 'nodeEdge', - label: 'Airport:Thijs', - data: { - attributes: [{ name: 'hallo', type: 'string' }], - d: -80, - created: false, - collection: 'flights', - edgeCount: 0, - view: anonymous, - }, - arrowHeadType: ArrowHeadType.Arrow, - sourceHandle: 'entitySourceLeft', - targetHandle: 'entityTargetLeft', - }, - { - id: 'flights', - source: 'Staff', - target: 'Plane', - type: 'nodeEdge', - label: 'Staff:Plane', - data: { - attributes: [{ name: 'hallo', type: 'string' }], - d: 0, - created: false, - collection: 'flights', - edgeCount: 0, - view: anonymous, - }, - arrowHeadType: ArrowHeadType.Arrow, - sourceHandle: 'entitySourceTop', - targetHandle: 'entityTargetBottom', - }, - { - id: 'flights', - source: 'Airport2', - target: 'Plane', - type: 'nodeEdge', - label: 'Airport2:Plane', - data: { - attributes: [{ name: 'hallo', type: 'string' }], - d: 120, - created: false, - collection: 'flights', - edgeCount: 0, - view: anonymous, - }, - arrowHeadType: ArrowHeadType.Arrow, - sourceHandle: 'entitySourceRight', - targetHandle: 'entityTargetRight', - }, - { - id: 'flights', - source: 'Staff', - target: 'Airport2', - type: 'nodeEdge', - label: 'Staff:Airport2', - data: { - attributes: [{ name: 'hallo', type: 'string' }], - d: 0, - created: false, - collection: 'flights', - edgeCount: 0, - view: anonymous, - }, - arrowHeadType: ArrowHeadType.Arrow, - sourceHandle: 'entitySourceBottom', - targetHandle: 'entityTargetTop', - }, - { - id: 'flights', - source: 'Airport', - target: 'Airport', - type: 'selfEdge', - label: 'Airport:Airport', - data: { - attributes: [{ name: 'test', type: 'string' }], - d: 58, - created: false, - collection: 'flights', - edgeCount: 0, - view: anonymous, - }, - arrowHeadType: ArrowHeadType.Arrow, - sourceHandle: 'entitySourceLeft', - targetHandle: 'entityTargetRight', - }, - ]; - - const expectedAttributes: Node[] = [ - { - type: 'attribute', - id: 'Airport:city', - position: { x: 0, y: 21 }, - data: { name: 'city', datatype: 'string' }, - isHidden: true, - }, - { - type: 'attribute', - id: 'Airport:vip', - position: { x: 0, y: 41 }, - data: { name: 'vip', datatype: 'bool' }, - isHidden: true, - }, - { - type: 'attribute', - id: 'Airport:state', - position: { x: 0, y: 61 }, - data: { name: 'state', datatype: 'string' }, - isHidden: true, - }, - { - type: 'attribute', - id: 'Plane:type', - position: { x: 0, y: 171 }, - data: { name: 'type', datatype: 'string' }, - isHidden: true, - }, - { - type: 'attribute', - id: 'Plane:maxFuelCapacity', - position: { x: 0, y: 191 }, - data: { name: 'maxFuelCapacity', datatype: 'int' }, - isHidden: true, - }, - { - type: 'attribute', - id: 'Airport2:city', - position: { x: 0, y: 471 }, - data: { name: 'city', datatype: 'string' }, - isHidden: true, - }, - { - type: 'attribute', - id: 'Airport2:vip', - position: { x: 0, y: 491 }, - data: { name: 'vip', datatype: 'bool' }, - isHidden: true, - }, - { - type: 'attribute', - id: 'Airport2:state', - position: { x: 0, y: 511 }, - data: { name: 'state', datatype: 'string' }, - isHidden: true, - }, - ]; -}); - -/** Result nodes. */ -const nodes: Node[] = [ - { - type: 'entity', - id: 'Thijs', - position: { x: 0, y: 0 }, - data: { attributes: [] }, - }, - { - type: 'entity', - id: 'Airport', - position: { x: 0, y: 0 }, - data: { attributes: [] }, - }, - { - type: 'entity', - id: 'Airport2', - position: { x: 0, y: 0 }, - data: { attributes: [] }, - }, - { - type: 'entity', - id: 'Plane', - position: { x: 0, y: 0 }, - data: { attributes: [] }, - }, - { - type: 'entity', - id: 'Staff', - position: { x: 0, y: 0 }, - data: { attributes: [] }, - }, -]; - -/** Result links. */ -const edges: Edge[] = [ - { - id: 'Airport2:Airport', - label: 'Airport2:Airport', - type: 'nodeEdge', - source: 'Airport2', - target: 'Airport', - arrowHeadType: ArrowHeadType.Arrow, - data: { - d: '', - attributes: [], - }, - }, - { - id: 'Airport:Staff', - label: 'Airport:Staff', - type: 'nodeEdge', - source: 'Airport', - target: 'Staff', - arrowHeadType: ArrowHeadType.Arrow, - data: { d: '', attributes: [] }, - }, - { - id: 'Plane:Airport', - label: 'Plane:Airport', - type: 'nodeEdge', - source: 'Plane', - target: 'Airport', - arrowHeadType: ArrowHeadType.Arrow, - data: { d: '', attributes: [] }, - }, - { - id: 'Airport:Thijs', - label: 'Airport:Thijs', - type: 'nodeEdge', - source: 'Airport', - target: 'Thijs', - arrowHeadType: ArrowHeadType.Arrow, - data: { d: '', attributes: [] }, - }, - { - id: 'Thijs:Airport', - label: 'Thijs:Airport', - type: 'nodeEdge', - source: 'Thijs', - target: 'Airport', - arrowHeadType: ArrowHeadType.Arrow, - data: { d: '', attributes: [] }, - }, - { - id: 'Staff:Plane', - label: 'Staff:Plane', - type: 'nodeEdge', - source: 'Staff', - target: 'Plane', - arrowHeadType: ArrowHeadType.Arrow, - data: { d: '', attributes: [] }, - }, - { - id: 'Staff:Airport2', - label: 'Staff:Airport2', - type: 'nodeEdge', - source: 'Staff', - target: 'Airport2', - arrowHeadType: ArrowHeadType.Arrow, - data: { d: '', attributes: [] }, - }, - { - id: 'Airport2:Plane', - label: 'Airport2:Plane', - type: 'nodeEdge', - source: 'Airport2', - target: 'Plane', - arrowHeadType: ArrowHeadType.Arrow, - data: { d: '', attributes: [] }, - }, -]; +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ +import { + schema, + schema2, +} from '../../../data/mock-data/graph-schema/MockGraph'; +import { + mockAttributeDataNLEdge2, + mockAttributeDataNLEdge2IncorrectId, + mockAttributeDataNLNode1, + mockAttributeDataNLNode2, + mockAttributeDataNLNode2IncorrectId, +} from '../../../data/mock-data/graph-schema/MockAttributeDataBatchedNL'; +import SecondChamberSchemaMock from '../../../data/mock-data/schema-result/2ndChamberSchemaMock'; +import GraphUseCase from '../../../domain/usecases/graph-schema/GraphUseCase'; +import SchemaViewModelImpl from './SchemaViewModel'; +import { Node, Edge, ArrowHeadType } from 'react-flow-renderer'; +import DrawOrderUseCase from '../../../domain/usecases/graph-schema/DrawOrderUseCase'; +import EdgeUseCase from '../../../domain/usecases/graph-schema/EdgeUseCase'; +import NodeUseCase from '../../../domain/usecases/graph-schema/NodeUseCase'; +import { + AttributeCategory, + BoundingBox, + NodeQualityDataForEntities, + NodeQualityDataForRelations, + NodeType, +} from '../../../domain/entity/graph-schema/structures/Types'; +import { + Attribute, + AttributeData, +} from '../../../domain/entity/graph-schema/structures/InputDataTypes'; +import mockQueryResult from '../../../data/mock-data/query-result/big2ndChamberQueryResult'; +import mockSchemaResult from '../../../data/mock-data/schema-result/2ndChamberSchemaMock'; +import Broker from '../../../domain/entity/broker/broker'; + +jest.mock('../../view/graph-schema/SchemaStyleSheet'); +jest.mock('../../util/graph-schema/utils.tsx', () => { + return { + //TODO Is this already updated? + getWidthOfText: () => { + return 10; + }, + calcWidthRelationNodeBox: () => { + return 75; + }, + calcWidthEntityNodeBox: () => { + return 75; + }, + makeBoundingBox: (x: number, y: number, width: number, height: number) => { + let boundingBox: BoundingBox; + boundingBox = { + topLeft: { x: x, y: y }, + bottomRight: { x: x + width, y: y + height }, + }; + return boundingBox; + }, + doBoxesOverlap: (firstBB: BoundingBox, secondBB: BoundingBox) => { + if ( + firstBB.topLeft.x >= secondBB.bottomRight.x || + secondBB.topLeft.x >= firstBB.bottomRight.x + ) + return false; + + if ( + firstBB.topLeft.y >= secondBB.bottomRight.y || + secondBB.topLeft.y >= firstBB.bottomRight.y + ) + return false; + + return true; + }, + }; +}); + +describe('schemaViewModelImpl', () => { + beforeEach(() => jest.resetModules()); + const graphUseCase = new GraphUseCase(); + const drawOrderUseCase = new DrawOrderUseCase(); + const nodeUseCase = new NodeUseCase(); + const edgeUseCase = new EdgeUseCase(); + const attributesInQueryBuilder: any = []; + const addAttribute = (name: string, type: string) => { + attributesInQueryBuilder.push({ name: name, type: type }); + return; + }; + + function anonymous( + attributes: Attribute[], + id: string, + hiddenAttributes: boolean + ): void { + //Empty methode. + } + + it('should create a relation', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + + const expectedrelationode = { + type: 'relation', + id: '5', + position: { x: -72.5, y: 20 }, + data: { + width: 220, + height: 20, + collection: 'none', + attributes: [], + from: 'from:here', + to: 'to:here', + nodeCount: 0, + summedNullAmount: 0, + fromRatio: 0, + toRatio: 0, + }, + }; + + schemaViewModel.createRelationNode( + '5', + [], + 'none', + schemaViewModel.elements + ); + schemaViewModel.setRelationNodePosition( + 0, + 0, + '5', + 'from:here', + 'to:here', + [] + ); + expect(JSON.stringify(schemaViewModel.elements.nodes[0])).toEqual( + JSON.stringify(expectedrelationode) + ); + + const expectedrelationode2 = { + type: 'relation', + id: '6', + position: { x: -72.5, y: 40 }, + data: { + width: 220, + height: 20, + collection: 'none', + attributes: [], + from: 'from:hereagain', + to: 'to:hereagain', + nodeCount: 0, + summedNullAmount: 0, + fromRatio: 0, + toRatio: 0, + }, + }; + schemaViewModel.createRelationNode( + '6', + [], + 'none', + schemaViewModel.elements + ); + schemaViewModel.setRelationNodePosition( + 0, + 0, + '6', + 'from:hereagain', + 'to:hereagain', + [] + ); + expect(JSON.stringify(schemaViewModel.elements.nodes[1])).toEqual( + JSON.stringify(expectedrelationode2) + ); + }); + + it('should console log that method is not implemented', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + const consoleSpy = jest.spyOn(console, 'log'); + schemaViewModel.exportToPDF(); + expect(consoleSpy).toHaveBeenCalledWith('Method not implemented.'); + }); + + it('fitToView', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + const getWidthHeight = { width: 300, height: 700 }; + const spy = jest.spyOn(schemaViewModel, 'getWidthHeight'); + spy.mockReturnValue(getWidthHeight); + + schemaViewModel.consumeMessageFromBackend(mockSchemaResult); + schemaViewModel.fitToView(); + + const consoleSpy = jest.spyOn(console, 'log'); + expect(consoleSpy).toHaveBeenCalledWith( + 'this.reactFlowInstance is undefined!' + ); + }); + + /** + * These are the testcases for consuming messages from the backend + */ + describe('consumeMessageFromBackend', () => { + it('should consume schema result only when subscribed to broker for schema_result', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + const mockConsumeMessages = jest.fn(); + const message = 'test schema result'; + + schemaViewModel.consumeMessageFromBackend = mockConsumeMessages; + + // should consume message for schema result when subscribed + schemaViewModel.subscribeToSchemaResult(); + Broker.instance().publish(message, 'schema_result'); + expect(mockConsumeMessages.mock.calls[0][0]).toEqual(message); + expect(mockConsumeMessages).toBeCalledTimes(1); + + // should not consume message for query_result + Broker.instance().publish(message, 'query_result'); + expect(mockConsumeMessages).toBeCalledTimes(1); + + // should not consume message for schema result when unsubscribed + schemaViewModel.unSubscribeFromSchemaResult(); + Broker.instance().publish(message, 'schema_result'); + expect(mockConsumeMessages).toBeCalledTimes(1); + }); + + it('should consume attribute-data only when subscribed to broker for gsa_node_result & gsa_edge_result', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + const mockConsumeMessages = jest.fn(); + const message = 'test analytics data'; + + schemaViewModel.consumeMessageFromBackend = mockConsumeMessages; + + // should consume message for schema result when subscribed + schemaViewModel.subscribeToAnalyticsData(); + Broker.instance().publish(message, 'gsa_node_result'); + Broker.instance().publish(message, 'gsa_edge_result'); + expect(mockConsumeMessages.mock.calls[0][0]).toEqual(message); + expect(mockConsumeMessages).toBeCalledTimes(2); + + // should not consume message for schema result when unsubscribed + schemaViewModel.unSubscribeFromAnalyticsData(); + Broker.instance().publish(message, 'schema_result'); + Broker.instance().publish(message, 'gsa_node_result'); + Broker.instance().publish(message, 'gsa_edge_result'); + expect(mockConsumeMessages).toBeCalledTimes(2); + }); + + //TODO: also test the message for the analytics + it('should console log and should not change any elements when receiving unrelated messages', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + const consoleSpy = jest.spyOn(console, 'log'); + + expect(schemaViewModel.visible).toEqual(true); + schemaViewModel.consumeMessageFromBackend(mockQueryResult); + expect(consoleSpy).toHaveBeenCalledWith('This is no valid input!'); + + expect(schemaViewModel.visible).toEqual(true); + expect(schemaViewModel.elements.nodes).toEqual([]); + expect(schemaViewModel.elements.edges).toEqual([]); + }); + }); + + /** + * These are the testcases for the attribute-analytics popup menu + */ + describe('AttributeAnalyticsPopupMenu', () => { + it('should throw error that given id does not exist', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.consumeMessageFromBackend(schema); + // Nodes that are not in elements cannot have popups + const name = 'Edje'; + expect( + schemaViewModel.elements.nodes.find((node) => node.id == name) + ).toEqual(undefined); + expect(() => + schemaViewModel.toggleAttributeAnalyticsPopupMenu(name) + ).toThrowError( + 'Node ' + name + ' does not exist therefore no popup menu can be shown.' + ); + }); + + it('should show the attribute-analytics popup menu', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + const popup = schemaViewModel.attributeAnalyticsPopupMenu; + schemaViewModel.consumeMessageFromBackend(schema); + + // test for entity node + const node = schemaViewModel.elements.nodes[0]; + schemaViewModel.toggleAttributeAnalyticsPopupMenu(node.id); + expect(popup.isHidden).toEqual(false); + expect(popup.nodeID).toEqual(node.id); + + // test for relation node + const edge = schemaViewModel.elements.edges[0]; + schemaViewModel.setRelationNodePosition( + 0, + 0, + edge.id, + edge.source, + edge.target, + edge.data.attributes + ); + schemaViewModel.toggleAttributeAnalyticsPopupMenu(edge.id); + expect(popup.isHidden).toEqual(false); + expect(popup.nodeID).toEqual(edge.id); + }); + + it('should hide the attribute-analytics popup menu as it was already open', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.consumeMessageFromBackend(schema); + schemaViewModel.attributeAnalyticsPopupMenu.nodeID = 'Thijs'; + schemaViewModel.attributeAnalyticsPopupMenu.isHidden = false; + schemaViewModel.toggleAttributeAnalyticsPopupMenu('Thijs'); + expect(schemaViewModel.attributeAnalyticsPopupMenu.isHidden).toEqual( + true + ); + }); + + it('should close the attribute-analytics menu', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.consumeMessageFromBackend(schema); + schemaViewModel.attributeAnalyticsData['Thijs'].onClickCloseButton(); + expect(schemaViewModel.attributeAnalyticsPopupMenu.isHidden).toEqual( + true + ); + }); + + it('should make an empty attribute-analytics popupmenu', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.attributeAnalyticsPopupMenu = + schemaViewModel.emptyAttributeAnalyticsPopupMenuNode(); + expect(schemaViewModel.attributeAnalyticsPopupMenu.id).toEqual( + 'attributeAnalyticsPopupMenu' + ); + expect(schemaViewModel.attributeAnalyticsPopupMenu.nodeID).toEqual(''); + expect(schemaViewModel.attributeAnalyticsPopupMenu.data.nodeType).toEqual( + NodeType.relation + ); + expect( + schemaViewModel.attributeAnalyticsPopupMenu.data.attributes + ).toEqual([]); + expect( + schemaViewModel.attributeAnalyticsPopupMenu.data.isAttributeDataIn + ).toEqual(false); + }); + + it('should place an attribute node in the querybuilder', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.consumeMessageFromBackend(schema); + const node = schemaViewModel.elements.nodes[0]; + const attribute = node.data.attributes; + schemaViewModel.attributeAnalyticsData[ + node.id + ].onClickPlaceInQueryBuilderButton(attribute.name, attribute.type); + expect(attributesInQueryBuilder[0]).toEqual({ + name: attribute.name, + type: attribute.type, + }); + }); + + it('should have a working searchbar', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.consumeMessageFromBackend(mockSchemaResult); + schemaViewModel.searchForAttributes('kamerleden', 'aa'); + + let attributes = + schemaViewModel.attributeAnalyticsPopupMenu.data.attributes; + expect(attributes.length).toEqual(2); + + schemaViewModel.searchForAttributes('kamerleden', ''); + attributes = schemaViewModel.attributeAnalyticsPopupMenu.data.attributes; + expect(attributes.length).toEqual(6); + }); + + it('should have working filters', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.consumeMessageFromBackend(mockSchemaResult); + schemaViewModel.consumeMessageFromBackend(mockAttributeDataNLNode1); + + schemaViewModel.applyAttributeFilters( + 'kamerleden', + AttributeCategory.categorical, + 'Bigger', + -1 + ); + let attributes = + schemaViewModel.attributeAnalyticsPopupMenu.data.attributes; + expect(attributes.length).toEqual(1); + + schemaViewModel.resetAttributeFilters('kamerleden'); + attributes = schemaViewModel.attributeAnalyticsPopupMenu.data.attributes; + expect(attributes.length).toEqual(6); + }); + }); + + /** + * These are the testcases for the node-quality popup menu + */ + describe('nodeQualityPopup', () => { + it('should throw error that given id does not exist', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.consumeMessageFromBackend(schema); + expect(() => schemaViewModel.toggleNodeQualityPopup('Edje')).toThrowError( + 'Node does not exist therefore no popup can be shown.' + ); + }); + + it('should show the node-quality popup menu for an entity node', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.consumeMessageFromBackend(SecondChamberSchemaMock); + schemaViewModel.toggleNodeQualityPopup('commissies'); + expect(schemaViewModel.nodeQualityPopup.isHidden).toEqual(false); + expect(schemaViewModel.nodeQualityPopup.type).toEqual( + 'nodeQualityEntityPopup' + ); + expect(schemaViewModel.nodeQualityPopup.nodeID).toEqual('commissies'); + }); + + it('should show the node-quality popup menu for a relation node', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.createRelationNode( + '5', + [], + '5', + schemaViewModel.elements + ); + schemaViewModel.toggleNodeQualityPopup('5'); + expect(schemaViewModel.nodeQualityPopup.isHidden).toEqual(false); + expect(schemaViewModel.nodeQualityPopup.type).toEqual( + 'nodeQualityRelationPopup' + ); + expect(schemaViewModel.nodeQualityPopup.nodeID).toEqual('5'); + }); + + it('should hide the node-quality popup menu as it was already open', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.consumeMessageFromBackend(schema); + schemaViewModel.nodeQualityPopup.nodeID = 'Thijs'; + schemaViewModel.nodeQualityPopup.isHidden = false; + schemaViewModel.toggleNodeQualityPopup('Thijs'); + expect(schemaViewModel.nodeQualityPopup.isHidden).toEqual(true); + }); + + it('should close the node-quality menu', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.consumeMessageFromBackend(schema); + schemaViewModel.nodeQualityData['Thijs'].onClickCloseButton(); + expect(schemaViewModel.nodeQualityPopup.isHidden).toEqual(true); + }); + + it('should make an empty node-quality popupmenu', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.nodeQualityPopup = + schemaViewModel.emptyNodeQualityPopupNode(); + expect(schemaViewModel.nodeQualityPopup.id).toEqual('nodeQualityPopup'); + expect(schemaViewModel.nodeQualityPopup.nodeID).toEqual(''); + expect(schemaViewModel.nodeQualityPopup.data.nodeCount).toEqual(0); + expect(schemaViewModel.nodeQualityPopup.data.attributeNullCount).toEqual( + 0 + ); + expect(schemaViewModel.nodeQualityPopup.data.isAttributeDataIn).toEqual( + false + ); + }); + }); + + describe('AttributeData', () => { + it('should process the incoming data correctly for node-data', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.consumeMessageFromBackend(mockSchemaResult); + schemaViewModel.consumeMessageFromBackend(mockAttributeDataNLNode2); + + const attributeAnalyticsData = + schemaViewModel.attributeAnalyticsData['commissies']; + const nodeQualityData = schemaViewModel.nodeQualityData[ + 'commissies' + ] as NodeQualityDataForEntities; + expect(attributeAnalyticsData.isAttributeDataIn).toEqual(true); + expect(attributeAnalyticsData.attributes[0].category).toEqual( + AttributeCategory.other + ); + expect(attributeAnalyticsData.attributes[0].nullAmount).toEqual(1); + expect(nodeQualityData.nodeCount).toEqual(38); + expect(nodeQualityData.attributeNullCount).toEqual(1); + expect(nodeQualityData.notConnectedNodeCount).toEqual(0.03); + }); + + it('should process the incoming data correctly for edge-data', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.consumeMessageFromBackend(mockSchemaResult); + schemaViewModel.consumeMessageFromBackend(mockAttributeDataNLEdge2); + + const attributeAnalyticsData = + schemaViewModel.attributeAnalyticsData['lid_van']; + const nodeQualityData = schemaViewModel.nodeQualityData[ + 'lid_van' + ] as NodeQualityDataForRelations; + expect(attributeAnalyticsData.isAttributeDataIn).toEqual(true); + expect(attributeAnalyticsData.attributes[0].category).toEqual( + AttributeCategory.categorical + ); + expect(attributeAnalyticsData.attributes[0].nullAmount).toEqual(0); + expect(nodeQualityData.nodeCount).toEqual(149); + expect(nodeQualityData.attributeNullCount).toEqual(0); + expect(nodeQualityData.fromRatio).toEqual(1); + expect(nodeQualityData.toRatio).toEqual(1); + }); + + it('should console log when the given data has no corresponding id for entities', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.consumeMessageFromBackend(mockSchemaResult); + + const consoleSpy = jest.spyOn(console, 'log'); + schemaViewModel.consumeMessageFromBackend( + mockAttributeDataNLNode2IncorrectId + ); + + expect(consoleSpy).toBeCalledTimes(2); + }); + + it('should console log when the given data has no corresponding id for relations', () => { + const schemaViewModel = new SchemaViewModelImpl( + drawOrderUseCase, + nodeUseCase, + edgeUseCase, + graphUseCase, + addAttribute + ); + schemaViewModel.consumeMessageFromBackend(mockSchemaResult); + + const consoleSpy = jest.spyOn(console, 'log'); + schemaViewModel.consumeMessageFromBackend( + mockAttributeDataNLEdge2IncorrectId + ); + + expect(consoleSpy).toBeCalledTimes(2); + }); + }); + + /** expected results */ + + const expectedNodesInElements: Node[] = [ + { + type: QueryElementTypes.Entity, + id: 'Airport', + position: { x: 0, y: 0 }, + data: { + attributes: [ + { name: 'city', type: 'string' }, + { name: 'vip', type: 'bool' }, + { name: 'state', type: 'string' }, + ], + handles: [ + 'entityTargetBottom', + 'entityTargetRight', + 'entityTargetRight', + 'entitySourceLeft', + 'entitySourceLeft', + 'entitySourceLeft', + 'entityTargetRight', + ], + width: 165, + height: 20, + nodeCount: 0, + summedNullAmount: 0, + connectedRatio: 0, + }, + }, + { + type: QueryElementTypes.Entity, + id: 'Plane', + position: { x: 0, y: 150 }, + data: { + attributes: [ + { name: 'type', type: 'string' }, + { name: 'maxFuelCapacity', type: 'int' }, + ], + handles: ['entitySourceTop', 'entityTargetBottom', 'entityTargetRight'], + width: 165, + height: 20, + nodeCount: 0, + summedNullAmount: 0, + connectedRatio: 0, + }, + }, + { + type: QueryElementTypes.Entity, + id: 'Staff', + position: { x: 0, y: 300 }, + data: { + attributes: [], + handles: ['entityTargetLeft', 'entitySourceTop', 'entitySourceBottom'], + width: 165, + height: 20, + nodeCount: 0, + summedNullAmount: 0, + connectedRatio: 0, + }, + }, + { + type: QueryElementTypes.Entity, + id: 'Airport2', + position: { x: 0, y: 450 }, + data: { + attributes: [ + { name: 'city', type: 'string' }, + { name: 'vip', type: 'bool' }, + { name: 'state', type: 'string' }, + ], + handles: ['entitySourceRight', 'entitySourceRight', 'entityTargetTop'], + width: 165, + height: 20, + nodeCount: 0, + summedNullAmount: 0, + connectedRatio: 0, + }, + }, + + { + type: QueryElementTypes.Entity, + id: 'Thijs', + position: { x: 0, y: 600 }, + data: { + attributes: [], + handles: ['entitySourceRight', 'entityTargetLeft'], + width: 165, + height: 20, + nodeCount: 0, + summedNullAmount: 0, + connectedRatio: 0, + }, + }, + + { + type: QueryElementTypes.Entity, + id: 'Unconnected', + position: { x: 0, y: 750 }, + data: { + attributes: [], + handles: [], + width: 165, + height: 20, + nodeCount: 0, + summedNullAmount: 0, + connectedRatio: 0, + }, + }, + { + type: 'relation', + id: 'flights', + position: { x: 0, y: 0 }, + data: { + width: 220, + height: 40, + collection: 'flights', + attributes: [ + { + name: 'arrivalTime', + type: 'int', + }, + { + name: 'departureTime', + type: 'int', + }, + ], + from: '', + to: '', + nodeCount: 0, + summedNullAmount: 0, + fromRatio: 0, + toRatio: 0, + }, + }, + { + type: 'relation', + id: 'flights', + position: { x: 0, y: 0 }, + data: { + width: 220, + height: 40, + collection: 'flights', + attributes: [ + { + name: 'salary', + type: 'int', + }, + ], + from: '', + to: '', + nodeCount: 0, + summedNullAmount: 0, + fromRatio: 0, + toRatio: 0, + }, + }, + { + type: 'relation', + id: 'flights', + position: { x: 0, y: 0 }, + data: { + width: 220, + height: 40, + collection: 'flights', + attributes: [], + from: '', + to: '', + nodeCount: 0, + summedNullAmount: 0, + fromRatio: 0, + toRatio: 0, + }, + }, + { + type: 'relation', + id: 'flights', + position: { x: 0, y: 0 }, + data: { + width: 220, + height: 40, + collection: 'flights', + attributes: [ + { + name: 'hallo', + type: 'string', + }, + ], + from: '', + to: '', + nodeCount: 0, + summedNullAmount: 0, + fromRatio: 0, + toRatio: 0, + }, + }, + { + type: 'relation', + id: 'flights', + position: { x: 0, y: 0 }, + data: { + width: 220, + height: 40, + collection: 'flights', + attributes: [ + { + name: 'hallo', + type: 'string', + }, + ], + from: '', + to: '', + nodeCount: 0, + summedNullAmount: 0, + fromRatio: 0, + toRatio: 0, + }, + }, + { + type: 'relation', + id: 'flights', + position: { x: 0, y: 0 }, + data: { + width: 220, + height: 40, + collection: 'flights', + attributes: [ + { + name: 'hallo', + type: 'string', + }, + ], + from: '', + to: '', + nodeCount: 0, + summedNullAmount: 0, + fromRatio: 0, + toRatio: 0, + }, + }, + { + type: 'relation', + id: 'flights', + position: { x: 0, y: 0 }, + data: { + width: 220, + height: 40, + collection: 'flights', + attributes: [ + { + name: 'hallo', + type: 'string', + }, + ], + from: '', + to: '', + nodeCount: 0, + summedNullAmount: 0, + fromRatio: 0, + toRatio: 0, + }, + }, + { + type: 'relation', + id: 'flights', + position: { x: 0, y: 0 }, + data: { + width: 220, + height: 40, + collection: 'flights', + attributes: [ + { + name: 'hallo', + type: 'string', + }, + ], + from: '', + to: '', + nodeCount: 0, + summedNullAmount: 0, + fromRatio: 0, + toRatio: 0, + }, + }, + { + type: 'relation', + id: 'flights', + position: { x: 0, y: 0 }, + data: { + width: 220, + height: 40, + collection: 'flights', + attributes: [ + { + name: 'test', + type: 'string', + }, + ], + from: '', + to: '', + nodeCount: 0, + summedNullAmount: 0, + fromRatio: 0, + toRatio: 0, + }, + }, + ]; + + const expectedEdgesInElements: Edge[] = [ + { + id: 'flights', + source: 'Plane', + target: 'Airport', + type: 'nodeEdge', + label: 'Plane:Airport', + data: { + attributes: [], + d: 0, + created: false, + collection: 'flights', + edgeCount: 0, + view: anonymous, + }, + arrowHeadType: ArrowHeadType.Arrow, + sourceHandle: 'entitySourceTop', + targetHandle: 'entityTargetBottom', + }, + { + id: 'flights', + source: 'Airport2', + target: 'Airport', + type: 'nodeEdge', + label: 'Airport2:Airport', + data: { + attributes: [ + { name: 'arrivalTime', type: 'int' }, + { name: 'departureTime', type: 'int' }, + ], + d: 40, + created: false, + collection: 'flights', + edgeCount: 0, + view: anonymous, + }, + arrowHeadType: ArrowHeadType.Arrow, + sourceHandle: 'entitySourceRight', + targetHandle: 'entityTargetRight', + }, + { + id: 'flights', + source: 'Thijs', + target: 'Airport', + type: 'nodeEdge', + label: 'Thijs:Airport', + data: { + attributes: [{ name: 'hallo', type: 'string' }], + d: 80, + created: false, + collection: 'flights', + edgeCount: 0, + view: anonymous, + }, + arrowHeadType: ArrowHeadType.Arrow, + sourceHandle: 'entitySourceRight', + targetHandle: 'entityTargetRight', + }, + { + id: 'flights', + source: 'Airport', + target: 'Staff', + type: 'nodeEdge', + label: 'Airport:Staff', + data: { + attributes: [{ name: 'salary', type: 'int' }], + d: -40, + created: false, + collection: 'flights', + edgeCount: 0, + view: anonymous, + }, + arrowHeadType: ArrowHeadType.Arrow, + sourceHandle: 'entitySourceLeft', + targetHandle: 'entityTargetLeft', + }, + { + id: 'flights', + source: 'Airport', + target: 'Thijs', + type: 'nodeEdge', + label: 'Airport:Thijs', + data: { + attributes: [{ name: 'hallo', type: 'string' }], + d: -80, + created: false, + collection: 'flights', + edgeCount: 0, + view: anonymous, + }, + arrowHeadType: ArrowHeadType.Arrow, + sourceHandle: 'entitySourceLeft', + targetHandle: 'entityTargetLeft', + }, + { + id: 'flights', + source: 'Staff', + target: 'Plane', + type: 'nodeEdge', + label: 'Staff:Plane', + data: { + attributes: [{ name: 'hallo', type: 'string' }], + d: 0, + created: false, + collection: 'flights', + edgeCount: 0, + view: anonymous, + }, + arrowHeadType: ArrowHeadType.Arrow, + sourceHandle: 'entitySourceTop', + targetHandle: 'entityTargetBottom', + }, + { + id: 'flights', + source: 'Airport2', + target: 'Plane', + type: 'nodeEdge', + label: 'Airport2:Plane', + data: { + attributes: [{ name: 'hallo', type: 'string' }], + d: 120, + created: false, + collection: 'flights', + edgeCount: 0, + view: anonymous, + }, + arrowHeadType: ArrowHeadType.Arrow, + sourceHandle: 'entitySourceRight', + targetHandle: 'entityTargetRight', + }, + { + id: 'flights', + source: 'Staff', + target: 'Airport2', + type: 'nodeEdge', + label: 'Staff:Airport2', + data: { + attributes: [{ name: 'hallo', type: 'string' }], + d: 0, + created: false, + collection: 'flights', + edgeCount: 0, + view: anonymous, + }, + arrowHeadType: ArrowHeadType.Arrow, + sourceHandle: 'entitySourceBottom', + targetHandle: 'entityTargetTop', + }, + { + id: 'flights', + source: 'Airport', + target: 'Airport', + type: 'selfEdge', + label: 'Airport:Airport', + data: { + attributes: [{ name: 'test', type: 'string' }], + d: 58, + created: false, + collection: 'flights', + edgeCount: 0, + view: anonymous, + }, + arrowHeadType: ArrowHeadType.Arrow, + sourceHandle: 'entitySourceLeft', + targetHandle: 'entityTargetRight', + }, + ]; + + const expectedAttributes: Node[] = [ + { + type: 'attribute', + id: 'Airport:city', + position: { x: 0, y: 21 }, + data: { name: 'city', datatype: 'string' }, + isHidden: true, + }, + { + type: 'attribute', + id: 'Airport:vip', + position: { x: 0, y: 41 }, + data: { name: 'vip', datatype: 'bool' }, + isHidden: true, + }, + { + type: 'attribute', + id: 'Airport:state', + position: { x: 0, y: 61 }, + data: { name: 'state', datatype: 'string' }, + isHidden: true, + }, + { + type: 'attribute', + id: 'Plane:type', + position: { x: 0, y: 171 }, + data: { name: 'type', datatype: 'string' }, + isHidden: true, + }, + { + type: 'attribute', + id: 'Plane:maxFuelCapacity', + position: { x: 0, y: 191 }, + data: { name: 'maxFuelCapacity', datatype: 'int' }, + isHidden: true, + }, + { + type: 'attribute', + id: 'Airport2:city', + position: { x: 0, y: 471 }, + data: { name: 'city', datatype: 'string' }, + isHidden: true, + }, + { + type: 'attribute', + id: 'Airport2:vip', + position: { x: 0, y: 491 }, + data: { name: 'vip', datatype: 'bool' }, + isHidden: true, + }, + { + type: 'attribute', + id: 'Airport2:state', + position: { x: 0, y: 511 }, + data: { name: 'state', datatype: 'string' }, + isHidden: true, + }, + ]; +}); + +/** Result nodes. */ +const nodes: Node[] = [ + { + type: QueryElementTypes.Entity, + id: 'Thijs', + position: { x: 0, y: 0 }, + data: { attributes: [] }, + }, + { + type: QueryElementTypes.Entity, + id: 'Airport', + position: { x: 0, y: 0 }, + data: { attributes: [] }, + }, + { + type: QueryElementTypes.Entity, + id: 'Airport2', + position: { x: 0, y: 0 }, + data: { attributes: [] }, + }, + { + type: QueryElementTypes.Entity, + id: 'Plane', + position: { x: 0, y: 0 }, + data: { attributes: [] }, + }, + { + type: QueryElementTypes.Entity, + id: 'Staff', + position: { x: 0, y: 0 }, + data: { attributes: [] }, + }, +]; + +/** Result links. */ +const edges: Edge[] = [ + { + id: 'Airport2:Airport', + label: 'Airport2:Airport', + type: 'nodeEdge', + source: 'Airport2', + target: 'Airport', + arrowHeadType: ArrowHeadType.Arrow, + data: { + d: '', + attributes: [], + }, + }, + { + id: 'Airport:Staff', + label: 'Airport:Staff', + type: 'nodeEdge', + source: 'Airport', + target: 'Staff', + arrowHeadType: ArrowHeadType.Arrow, + data: { d: '', attributes: [] }, + }, + { + id: 'Plane:Airport', + label: 'Plane:Airport', + type: 'nodeEdge', + source: 'Plane', + target: 'Airport', + arrowHeadType: ArrowHeadType.Arrow, + data: { d: '', attributes: [] }, + }, + { + id: 'Airport:Thijs', + label: 'Airport:Thijs', + type: 'nodeEdge', + source: 'Airport', + target: 'Thijs', + arrowHeadType: ArrowHeadType.Arrow, + data: { d: '', attributes: [] }, + }, + { + id: 'Thijs:Airport', + label: 'Thijs:Airport', + type: 'nodeEdge', + source: 'Thijs', + target: 'Airport', + arrowHeadType: ArrowHeadType.Arrow, + data: { d: '', attributes: [] }, + }, + { + id: 'Staff:Plane', + label: 'Staff:Plane', + type: 'nodeEdge', + source: 'Staff', + target: 'Plane', + arrowHeadType: ArrowHeadType.Arrow, + data: { d: '', attributes: [] }, + }, + { + id: 'Staff:Airport2', + label: 'Staff:Airport2', + type: 'nodeEdge', + source: 'Staff', + target: 'Airport2', + arrowHeadType: ArrowHeadType.Arrow, + data: { d: '', attributes: [] }, + }, + { + id: 'Airport2:Plane', + label: 'Airport2:Plane', + type: 'nodeEdge', + source: 'Airport2', + target: 'Plane', + arrowHeadType: ArrowHeadType.Arrow, + data: { d: '', attributes: [] }, + }, +]; diff --git a/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx b/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx index 1bdbb7dba3782207eecbe970eb2846353e602309..49d6a6500fbd5dadd32d2d5b660eee712ef8e82a 100644 --- a/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx +++ b/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx @@ -14,6 +14,7 @@ import styles from './entity.module.scss'; import { calcWidthEntityNodeBox, calculateAttributeQuality, calculateEntityQuality } from '@graphpolaris/shared/lib/schema/schema-utils'; import { useTheme } from '@mui/material'; import { SchemaReactflowNodeWithFunctions } from '../../../model/reactflow'; +import { QueryElementTypes } from '@graphpolaris/shared/lib/querybuilder'; /** * EntityNode is the node that represents the database entities. @@ -32,8 +33,9 @@ export const EntityNode = React.memo(({ id, data }: NodeProps<SchemaReactflowNod * @param event React Mouse drag event */ const onDragStart = (event: React.DragEvent<HTMLDivElement>) => { + // console.log('dragging entiry', id, data); // console.log(id, data); - event.dataTransfer.setData('application/reactflow', JSON.stringify({ type: 'entity', name: id })); + event.dataTransfer.setData('application/reactflow', JSON.stringify({ type: QueryElementTypes.Entity, name: id })); event.dataTransfer.effectAllowed = 'move'; }; diff --git a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx b/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx index 2aafdd01651c4675a03d102a8f5ab314f4852ea1..6eab676f8161c4d7aff9542d67bf7f24f3f9e129 100644 --- a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx +++ b/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx @@ -20,6 +20,7 @@ import { } from '@graphpolaris/shared/lib/schema/schema-utils'; import { useTheme } from '@mui/material'; import { SchemaReactflowRelation, SchemaReactflowRelationWithFunctions } from '../../../model/reactflow'; +import { QueryElementTypes } from '@graphpolaris/shared/lib/querybuilder'; /** * Relation node component that renders a relation node for the schema. @@ -36,15 +37,16 @@ export const RelationNode = React.memo(({ id, data }: NodeProps<SchemaReactflowR * @param event React Mouse drag event. */ const onDragStart = (event: React.DragEvent<HTMLDivElement>) => { - // console.log(id, data); + console.log('dragging relation', id, data); event.dataTransfer.setData( 'application/reactflow', JSON.stringify({ - type: 'relation', + type: QueryElementTypes.Relation, name: id, //TODO id? from: data.from, to: data.to, collection: data.collection, + label: data.label, }) ); event.dataTransfer.effectAllowed = 'move'; diff --git a/libs/shared/lib/schema/schema-utils/schema-usecases.spec.ts b/libs/shared/lib/schema/schema-utils/schema-usecases.spec.ts index 42c767efd7a1ec5fd68dac611b10fc4c2d912950..15a3cbb729da171e37a2603daa0d99256ead6dc6 100644 --- a/libs/shared/lib/schema/schema-utils/schema-usecases.spec.ts +++ b/libs/shared/lib/schema/schema-utils/schema-usecases.spec.ts @@ -1,7 +1,5 @@ -import { assert, describe, expect, it, test } from 'vitest'; +import { describe, expect, it, test } from 'vitest'; import { SchemaUtils } from '@graphpolaris/shared/lib/schema/schema-utils'; -import { MultiGraph } from 'graphology'; -import { Attributes } from 'graphology-types'; import { movieSchemaRaw, northwindSchemaRaw, simpleSchemaRaw, twitterSchemaRaw } from '@graphpolaris/shared/lib/mock-data'; import { SchemaFromBackend, SchemaGraphology } from '../model'; diff --git a/libs/shared/lib/schema/schema-utils/schema-usecases.ts b/libs/shared/lib/schema/schema-utils/schema-usecases.ts index 3bea392e220692a16d866829660984abf6495c60..780870b8b6818873261f9a8fc5ac5fbe8df46cc8 100644 --- a/libs/shared/lib/schema/schema-utils/schema-usecases.ts +++ b/libs/shared/lib/schema/schema-utils/schema-usecases.ts @@ -3,6 +3,7 @@ import Graph from 'graphology'; import { Attributes } from 'graphology-types'; import { MarkerType, Edge, Node } from 'reactflow'; import { SchemaReactflowNode } from '../model/reactflow'; +import { QueryElementTypes } from '../../querybuilder'; //TODO does not belong here; maybe should go into the GraphPolarisThemeProvider const ANIMATEDEDGES = false; @@ -12,7 +13,7 @@ export function schemaExpandRelation(graph: Graph): Graph { newGraph.forEachNode((node, attributes) => { // console.log(node, attributes); - newGraph.mergeNodeAttributes(node, { type: 'entity' }); + newGraph.mergeNodeAttributes(node, { type: QueryElementTypes.Entity }); }); //makeNewRelationNodes @@ -30,7 +31,7 @@ export function schemaExpandRelation(graph: Graph): Graph { ...attributes, x: 0, y: 0, - type: 'relation', + type: QueryElementTypes.Relation, }); const id = 'RelationEdge' + source + '->' + newID; @@ -117,7 +118,7 @@ export function createReactFlowEdges(graph: Graph): Array<Edge> { // name: edge, // }, // position: { x: attributes.x, y: attributes.y }, -// type: 'relation', +// type: QueryElementTypes.Relation, // }; // nodeElements.push(newRelationNode); // }); diff --git a/libs/shared/lib/vis/nodelink/NodeLinkViewModel.tsx b/libs/shared/lib/vis/nodelink/NodeLinkViewModel.tsx index c1548f3c0560591c10f2f33a04d188ee01a0c9eb..fa2a2ffc8efbfb959291faa3f41aa5878d9c7f50 100644 --- a/libs/shared/lib/vis/nodelink/NodeLinkViewModel.tsx +++ b/libs/shared/lib/vis/nodelink/NodeLinkViewModel.tsx @@ -130,7 +130,7 @@ export default class NodeLinkViewModel { this.graph = this.resultNodeLinkParserUseCase.parseQueryResult(jsonObject); this.SetNodeGraphics(this.graph, this.radius); this.simulation.restart(); - console.log('simulation restarted', jsonObject, this.graph); + console.debug('simulation restarted', jsonObject, this.graph); // this.notifyViewAboutChanges(); // if (isNodeLinkResult(jsonObject)) { @@ -509,14 +509,6 @@ export default class NodeLinkViewModel { event.subject.fy = event.subject.y; this.dragOffset.x = event.subject.x; this.dragOffset.y = event.subject.y; - - // Toggle display of the attributes of the node - const node = this.simulation.find(event.x, event.y, 15); - - // Null check - if (node) { - this.ToggleInformationOnNode(node); - } }; /** @@ -527,9 +519,9 @@ export default class NodeLinkViewModel { public ToggleInformationOnNode(node: NodeType) { this.simulation.alphaTarget(0).restart(); // renderer will not always update without this line this.showAttributes(node); - this.highlightNode(node); - this.highlightLinks(node); - this.showShortestPath(); + // this.highlightNode(node); + // this.highlightLinks(node); + // this.showShortestPath(); } /** @@ -638,6 +630,13 @@ export default class NodeLinkViewModel { } event.subject.fx = null; event.subject.fy = null; + + // Toggle display of the attributes of the node + const node = this.simulation.find(event.x, event.y, 15); + // Null check + if (!!node) { + this.ToggleInformationOnNode(node); + } }; /** @@ -724,8 +723,8 @@ export default class NodeLinkViewModel { /** Ticked is the updater function of the simulation. Every 'tick' all the new positions of the nodes (and thus the edges) are recalculated and updated. */ public ticked = () => { this.graph.nodes.forEach((node: any) => { - if (node === undefined) return; - if (node?.gfx === undefined) node.gfx = {}; + if (!node || !node.x || !node.y) return; + // if (node?.gfx === undefined) node.gfx = {}; node.gfx.position = new PIXI.Point(node.x, node.y); // Update attributes position if they exist if (node.gfxAttributes) { diff --git a/libs/shared/lib/vis/nodelink/nodelinkvis.tsx b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx index 1c6d4b2c60e7609c7b7b80280e61fa5f79092e3c..8a4b6aa11d106bf04d9fc1b66a62d883ace58dbd 100644 --- a/libs/shared/lib/vis/nodelink/nodelinkvis.tsx +++ b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx @@ -64,14 +64,14 @@ export const NodeLinkVis = React.memo((props: Props) => { const theme = useTheme(); useEffect(() => { - console.log('update nodelink useEffect', graphQueryResult); + console.debug('update nodelink useEffect', graphQueryResult); nodeLinkViewModelRef?.current?.consumeMessageFromBackend(graphQueryResult); nodeLinkViewModelRef?.current?.startSimulation(); nodeLinkViewModelRef?.current?.selectD3Elements(); }, [graphQueryResult]); useEffect(() => { - console.log('loaded NodeLinkVis'); + console.debug('loaded NodeLinkVis'); const resultNodeLinkParserUseCase = new ResultNodeLinkParserUseCase(); const nodeLinkViewModel = new NodeLinkViewModel( resultNodeLinkParserUseCase, diff --git a/libs/shared/package.json b/libs/shared/package.json index 2fd83cbbb5b3e8cc30e82ff772213eac1b15a635..221adab0f26c255ac9acce3f4b3ecc0e87ef98e8 100644 --- a/libs/shared/package.json +++ b/libs/shared/package.json @@ -38,9 +38,11 @@ "d3": "^6.6", "deck.gl": "^8.9.19", "graphology": "^0.25.1", + "graphology-dag": "^0.3.0", "graphology-layout": "^0.6.1", "graphology-layout-forceatlas2": "^0.10.1", "graphology-layout-noverlap": "^0.4.2", + "graphology-simple-path": "^0.2.0", "graphology-types": "^0.24.7", "immer": "^10.0.2", "jspdf": "^2.5.1", diff --git a/libs/shared/vite.config.ts b/libs/shared/vite.config.ts index 6cd9dbf471c5be669162a001f05e615be1e12ecb..2f263c107da35d9ffdc9bad6eee216978771431c 100644 --- a/libs/shared/vite.config.ts +++ b/libs/shared/vite.config.ts @@ -5,6 +5,7 @@ import { resolve } from 'path'; import { defineConfig } from 'vite'; import dts from 'vite-plugin-dts'; import path from 'path'; +import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ plugins: [ @@ -12,7 +13,9 @@ export default defineConfig({ dts({ insertTypesEntry: true, }), + tsconfigPaths(), ], + optimizeDeps: {}, build: { lib: { entry: resolve(__dirname, './index.ts'), @@ -20,6 +23,10 @@ export default defineConfig({ formats: ['es', 'umd'], fileName: (format) => `@graphpolaris-shared.${format}.js`, }, + commonjsOptions: { + include: [], + transformMixedEsModules: true, + }, rollupOptions: { external: ['react', 'react-dom'], output: { @@ -36,12 +43,9 @@ export default defineConfig({ }, }, test: { - setupFiles: ['./vitest.setup.ts'], environment: 'happy-dom', - deps: { - // inline: ['vitest-canvas-mock'], - }, - threads: false, + deps: {}, + threads: true, environmentOptions: { jsdom: { resources: 'usable', diff --git a/libs/shared/vitest.setup.ts b/libs/shared/vitest.setup.ts deleted file mode 100644 index 721b8f9bbb6642b4a1e2cae3b780f0d10f19a48f..0000000000000000000000000000000000000000 --- a/libs/shared/vitest.setup.ts +++ /dev/null @@ -1 +0,0 @@ -// import 'vitest-canvas-mock' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abdb21d3a6fe4c393cca5b9cfb8aa1c625ccbd08..73e36f4b94ecb26a651ddce45fb071acf7c5eb96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -232,6 +232,9 @@ importers: graphology: specifier: ^0.25.1 version: 0.25.1(graphology-types@0.24.7) + graphology-dag: + specifier: ^0.3.0 + version: 0.3.0(graphology-types@0.24.7) graphology-layout: specifier: ^0.6.1 version: 0.6.1(graphology-types@0.24.7) @@ -241,6 +244,9 @@ importers: graphology-layout-noverlap: specifier: ^0.4.2 version: 0.4.2(graphology-types@0.24.7) + graphology-simple-path: + specifier: ^0.2.0 + version: 0.2.0(graphology-types@0.24.7) graphology-types: specifier: ^0.24.7 version: 0.24.7 @@ -563,7 +569,7 @@ importers: version: 8.7.0(eslint@7.32.0) eslint-config-turbo: specifier: latest - version: 1.10.6(eslint@7.32.0) + version: 1.10.9(eslint@7.32.0) eslint-plugin-react: specifier: 7.31.8 version: 7.31.8(eslint@7.32.0) @@ -9235,15 +9241,6 @@ packages: dependencies: eslint: 7.32.0 - /eslint-config-turbo@1.10.6(eslint@7.32.0): - resolution: {integrity: sha512-iZ63etePRUdEIDY5MxdUhU2ekV9TDbVdHg0BK00QqVFgQTXUYuJ7rsQj/wUKTsw3jwhbLfaY6H5sknAgYyWZ2g==} - peerDependencies: - eslint: '>6.6.0' - dependencies: - eslint: 7.32.0 - eslint-plugin-turbo: 1.10.6(eslint@7.32.0) - dev: false - /eslint-config-turbo@1.10.9(eslint@7.32.0): resolution: {integrity: sha512-YA5QWxWte/NiRJL0/Cv7aATfIvS5sUAuyD6ZuyTZEzwyU7E6FUXGo44amjf9INkyj96HrJ2nYWoFkCRx3vs6Ag==} peerDependencies: @@ -9251,7 +9248,6 @@ packages: dependencies: eslint: 7.32.0 eslint-plugin-turbo: 1.10.9(eslint@7.32.0) - dev: true /eslint-import-resolver-node@0.3.7: resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} @@ -9394,15 +9390,6 @@ packages: semver: 6.3.0 string.prototype.matchall: 4.0.8 - /eslint-plugin-turbo@1.10.6(eslint@7.32.0): - resolution: {integrity: sha512-jlzfxYaK8hcz1DTk8Glxxi1r0kgdy85191a4CbFOTiiBulmKHMLJgzhsyE9Ong796MA62n91KFpc20BiKjlHwg==} - peerDependencies: - eslint: '>6.6.0' - dependencies: - dotenv: 16.0.3 - eslint: 7.32.0 - dev: false - /eslint-plugin-turbo@1.10.9(eslint@7.32.0): resolution: {integrity: sha512-o8Nga4WFMvzF0lo3d3UyjGli2JOUn/4SRtRdvcf4EA9/TPotU/NUHqO16Cp0SHZJG/tGYIy5LY1O/EO7Mxbd1A==} peerDependencies: @@ -9410,7 +9397,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==} @@ -10177,6 +10163,16 @@ packages: lodash: 4.17.21 dev: true + /graphology-dag@0.3.0(graphology-types@0.24.7): + resolution: {integrity: sha512-dg4JPb+/LDEbDinZIj7ezWzlEXDRokshdpTL8oAuftE9Uy0uTKGOKSmYULY8p3j/vw0HB31Wog9T/kpqprUQpg==} + peerDependencies: + graphology-types: '>=0.19.0' + dependencies: + graphology-types: 0.24.7 + graphology-utils: 2.5.2(graphology-types@0.24.7) + mnemonist: 0.39.5 + dev: false + /graphology-generators@0.11.2(graphology-types@0.24.7): resolution: {integrity: sha512-hx+F0OZRkVdoQ0B1tWrpxoakmHZNex0c6RAoR0PrqJ+6fz/gz6CQ88Qlw78C6yD9nlZVRgepIoDYhRTFV+bEHg==} peerDependencies: @@ -10248,9 +10244,27 @@ packages: mnemonist: 0.39.5 dev: true + /graphology-simple-path@0.2.0(graphology-types@0.24.7): + resolution: {integrity: sha512-4cGMWbVuJM0zlKDUx6dS6JGGLddizDPe8PsTokXVz2eTeHYg07qa5TgwIco15ta2RMM05+xy8N1mFnpS85y0kw==} + peerDependencies: + graphology-types: '>=0.20.0' + dependencies: + graphology-types: 0.24.7 + graphology-utils: 1.8.0(graphology-types@0.24.7) + mnemonist: 0.39.5 + dev: false + /graphology-types@0.24.7: resolution: {integrity: sha512-tdcqOOpwArNjEr0gNQKCXwaNCWnQJrog14nJNQPeemcLnXQUUGrsCWpWkVKt46zLjcS6/KGoayeJfHHyPDlvwA==} + /graphology-utils@1.8.0(graphology-types@0.24.7): + resolution: {integrity: sha512-Pa7SW30OMm8fVtyH49b3GJ/uxlMHGfXly50wIhlcc7ZoX9ahZa7sPBz+obo4WZClrRV6wh3tIu0GJoI42eao1A==} + peerDependencies: + graphology-types: '>=0.19.0' + dependencies: + graphology-types: 0.24.7 + dev: false + /graphology-utils@2.5.2(graphology-types@0.24.7): resolution: {integrity: sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==} peerDependencies: