From 044faf06ece02d30f963bb98f3e87cab6d36b57b Mon Sep 17 00:00:00 2001 From: MarcosPierasNL <pieras.marcos@gmail.com> Date: Fri, 7 Mar 2025 15:05:09 +0100 Subject: [PATCH 1/8] test: adds test with vite --- bun.lockb | Bin 150175 -> 182704 bytes package.json | 3 +- .../query}/queryConverter.test.ts | 4 +- .../queryTranslator/queryTranslator.test.ts | 92 +++ src/tests/query/queryTranslator/testData.ts | 541 ++++++++++++++++++ 5 files changed, 637 insertions(+), 3 deletions(-) rename src/{utils/cypher/converter => tests/query}/queryConverter.test.ts (99%) create mode 100644 src/tests/query/queryTranslator/queryTranslator.test.ts create mode 100644 src/tests/query/queryTranslator/testData.ts diff --git a/bun.lockb b/bun.lockb index 94886d7ec58aab76974728e32acbce8e5b53710b..2207e737365016760d1a78a9f35c8d1a457ac494 100755 GIT binary patch delta 35857 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 delta 15083 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 At^fc4 diff --git a/package.json b/package.json index 548cf5c..97913ac 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "@typescript-eslint/eslint-plugin": "^8.19.1", "@typescript-eslint/parser": "^8.19.1", "eslint": "^9.17.0", - "typescript-eslint": "^8.19.1" + "typescript-eslint": "^8.19.1", + "vitest": "^3.0.8" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/src/utils/cypher/converter/queryConverter.test.ts b/src/tests/query/queryConverter.test.ts similarity index 99% rename from src/utils/cypher/converter/queryConverter.test.ts rename to src/tests/query/queryConverter.test.ts index 5227c6c..e35f7b4 100644 --- a/src/utils/cypher/converter/queryConverter.test.ts +++ b/src/tests/query/queryConverter.test.ts @@ -1,6 +1,6 @@ -import { query2Cypher } from './queryConverter'; +import { query2Cypher } from '../../utils/cypher/converter/queryConverter'; import { StringFilterTypes, type BackendQueryFormat } from 'ts-common'; -import { expect, test, describe, it } from 'bun:test'; +import { expect, describe, it } from 'vitest'; function fixCypherSpaces(cypher?: string | null): string { if (!cypher) { diff --git a/src/tests/query/queryTranslator/queryTranslator.test.ts b/src/tests/query/queryTranslator/queryTranslator.test.ts new file mode 100644 index 0000000..c18a968 --- /dev/null +++ b/src/tests/query/queryTranslator/queryTranslator.test.ts @@ -0,0 +1,92 @@ +import { expect, describe, it } from 'vitest'; +import type { QueryMultiGraph } from 'ts-common/src/model/graphology'; +import { MLTypesEnum } from 'ts-common/src/model/query/machineLearningModel'; +import { type QueryBuilderSettings } from 'ts-common/src/model/query/queryBuilderModel'; +import { Query2BackendQuery } from './../../../utils/reactflow/query2backend'; +import type { MachineLearning } from 'ts-common/src/model/query/queryRequestModel'; +import { type BackendQueryFormat } from 'ts-common'; +import { createQueryMultiGraphFromData, settingsBase, ss_id } from './testData'; +import { visualQuery_1 } from './testData'; +import { expectedResult_1 } from './testData'; + +describe('query2backend', () => { + it('should return correctly a node - 0', () => { + const nodesData = [ + { + id: 'Movie', + schemaKey: 'Movie', + type: 'entity', + width: 100, + height: 100, + x: 50, + y: 50, + name: 'Movie', + attributes: [ + { name: '(# Connection)', type: 'float' }, + { name: 'tagline', type: 'string' }, + { name: 'votes', type: 'int' }, + { name: 'title', type: 'string' }, + { name: 'released', type: 'int' }, + ], + }, + ]; + + const visualQuery: QueryMultiGraph = createQueryMultiGraphFromData(nodesData, []); + + const ml: MachineLearning[] = [ + { type: MLTypesEnum.linkPrediction, parameters: [], id: 1 }, + { type: MLTypesEnum.centrality, parameters: [], id: 2 }, + { type: MLTypesEnum.communityDetection, parameters: [], id: 3 }, + { type: MLTypesEnum.shortestPath, parameters: [], id: 4 }, + ]; + + const result = Query2BackendQuery(ss_id, visualQuery, settingsBase, ml); + const expectedResult: BackendQueryFormat = { + saveStateID: 'test', + query: [ + { + id: 'path_0', + node: { + label: 'Movie', + id: 'Movie', + relation: undefined, + }, + }, + ], + machineLearning: [ + { type: MLTypesEnum.linkPrediction, parameters: [], id: 1 }, + { type: MLTypesEnum.centrality, parameters: [], id: 2 }, + { type: MLTypesEnum.communityDetection, parameters: [], id: 3 }, + { type: MLTypesEnum.shortestPath, parameters: [], id: 4 }, + ], + limit: 500, + return: ['*'], + cached: false, + logic: undefined, + }; + expect(result).toEqual(expectedResult); + }); + it('should return correctly on a simple query with multiple paths - 1', () => { + const ml: MachineLearning[] = [ + { type: MLTypesEnum.linkPrediction, parameters: [], id: 1 }, + { type: MLTypesEnum.centrality, parameters: [], id: 2 }, + { type: MLTypesEnum.communityDetection, parameters: [], id: 3 }, + { type: MLTypesEnum.shortestPath, parameters: [], id: 4 }, + ]; + + const result = Query2BackendQuery(ss_id, visualQuery_1, settingsBase, ml); + + expect(result).toEqual(expectedResult_1); + }); + /* + it('should return correctly on a complex query with logic', () => {}); + it('should return correctly on a query with group by logic', () => {}); + it('should return correctly on a query with no label', () => {}); + it('should return correctly on a query with no depth', () => {}); + it('should return correctly on a query with average calculation', () => {}); + it('should return correctly on a query with average calculation and multiple paths', () => {}); + it('should return correctly on a single entity query with lower like logic', () => {}); + it('should return correctly on a query with like logic', () => {}); + it('should return correctly on a query with both direction relation', () => {}); +*/ +}); diff --git a/src/tests/query/queryTranslator/testData.ts b/src/tests/query/queryTranslator/testData.ts new file mode 100644 index 0000000..6a3154e --- /dev/null +++ b/src/tests/query/queryTranslator/testData.ts @@ -0,0 +1,541 @@ +import type { QueryMultiGraph, QueryGraphNodes, QueryGraphEdges } from 'ts-common/src/model/graphology'; +import type { SerializedNode, SerializedEdge } from 'graphology-types'; +import { Handles, QueryElementTypes } from 'ts-common/src/model/reactflow'; +import { type BackendQueryFormat } from 'ts-common'; +import { MLTypesEnum } from 'ts-common/src/model/query/machineLearningModel'; +import { type QueryBuilderSettings } from 'ts-common/src/model/query/queryBuilderModel'; + +export function createQueryMultiGraphFromData(nodesData: any[], edgesData: any[]): QueryMultiGraph { + const nodes: SerializedNode<QueryGraphNodes>[] = nodesData.map(node => ({ + key: node.id, + attributes: { + id: node.id, + name: node.name, + schemaKey: node.schemaKey, + type: node.type, + width: node.width, + height: node.height, + x: node.x, + y: node.y, + attributes: node.attributes.map((attribute: any) => ({ + handleData: { + nodeId: node.id, + nodeName: node.name, + nodeType: node.type, + handleType: 'entityAttributeHandle' as Handles, // check if different reactflow Handles + attributeName: attribute.name, + attributeType: attribute.type, + }, + })), + leftRelationHandleId: { + nodeId: node.id, + nodeName: node.name, + nodeType: node.type, + handleType: 'entityLeftHandle' as Handles, + }, + rightRelationHandleId: { + nodeId: node.id, + nodeName: node.name, + nodeType: node.type, + handleType: 'entityRightHandle' as Handles, + }, + selected: node.selected || false, + }, + })); + + const edges: SerializedEdge<QueryGraphEdges>[] = edgesData.map(edge => ({ + source: edge.sourceNodeId, + target: edge.targetNodeId, + type: edge.type, + sourceHandleData: { + nodeId: edge.sourceNodeId, + nodeName: edge.sourceNodeName, + nodeType: edge.sourceNodeType, + handleType: edge.sourceHandleType, + attributeName: edge.sourceAttributeName, + attributeType: edge.sourceAttributeType, + }, + targetHandleData: { + nodeId: edge.targetNodeId, + nodeName: edge.targetNodeName, + nodeType: edge.targetNodeType, + handleType: edge.targetHandleType, + attributeName: edge.targetAttributeName, + attributeType: edge.targetAttributeType, + }, + })); + + return { + nodes: nodes, + edges: edges, + options: { + type: 'mixed', + multi: true, + allowSelfLoops: false, + }, + attributes: {}, + }; +} + +export const ss_id: string = 'test'; +export const settingsBase: QueryBuilderSettings = { + depth: { + max: 1, + min: 1, + }, + limit: 500, + layout: 'manual', + unionTypes: {}, + autocompleteRelation: true, +}; + +export const visualQuery_1: QueryMultiGraph = { + edges: [ + { + key: 'geid_183_0', + source: 'id_1741246511422', + target: 'id_1741246512287', + attributes: { + type: 'connection', + sourceHandleData: { + nodeId: 'id_1741246511422', + nodeName: 'Person', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityRight, + attributeName: '', + }, + targetHandleData: { + nodeId: 'id_1741246512287', + nodeName: 'WROTE', + nodeType: QueryElementTypes.Relation, + handleType: Handles.RelationLeft, + attributeName: '', + }, + }, + }, + { + key: 'geid_183_1', + source: 'id_1741246512287', + target: 'id_1741246511585', + attributes: { + type: 'connection', + sourceHandleData: { + nodeId: 'id_1741246512287', + nodeName: 'WROTE', + nodeType: QueryElementTypes.Relation, + handleType: Handles.RelationRight, + attributeName: '', + }, + targetHandleData: { + nodeId: 'id_1741246511585', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityLeft, + attributeName: '', + }, + }, + }, + { + key: 'geid_262_0', + source: 'id_1741246511422', + target: 'id_1741246625352', + attributes: { + type: 'connection', + sourceHandleData: { + nodeId: 'id_1741246511422', + nodeName: 'Person', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityRight, + attributeName: '', + }, + targetHandleData: { + nodeId: 'id_1741246625352', + nodeName: 'PRODUCED', + nodeType: QueryElementTypes.Relation, + handleType: Handles.RelationLeft, + attributeName: '', + }, + }, + }, + { + key: 'geid_319_0', + source: 'id_1741246625352', + target: 'id_1741246630119', + attributes: { + type: 'connection', + sourceHandleData: { + nodeId: 'id_1741246625352', + nodeName: 'PRODUCED', + nodeType: QueryElementTypes.Relation, + handleType: Handles.RelationRight, + attributeName: '', + }, + targetHandleData: { + nodeId: 'id_1741246630119', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityLeft, + attributeName: '', + }, + }, + }, + ], + nodes: [ + { + key: 'id_1741246512287', + attributes: { + x: 180, + y: 90, + id: 'id_1741246512287', + name: 'WROTE', + type: QueryElementTypes.Relation, + depth: { + max: 1, + min: 1, + }, + width: 86.15010000000001, + height: 20, + schemaKey: 'WROTE_PersonMovie', + attributes: [ + { + handleData: { + nodeId: 'id_1741246512287', + nodeName: 'WROTE', + nodeType: QueryElementTypes.Relation, + handleType: Handles.RelationAttribute, + attributeName: '(# Connection)', + attributeType: 'float', + }, + }, + ], + collection: 'WROTE', + leftEntityHandleId: { + nodeId: 'id_1741246512287', + nodeName: 'WROTE', + nodeType: QueryElementTypes.Relation, + handleType: Handles.RelationLeft, + }, + rightEntityHandleId: { + nodeId: 'id_1741246512287', + nodeName: 'WROTE', + nodeType: QueryElementTypes.Relation, + handleType: Handles.RelationRight, + }, + }, + }, + { + key: 'id_1741246511422', + attributes: { + x: 0, + y: 90, + id: 'id_1741246511422', + name: 'Person', + type: QueryElementTypes.Entity, + width: 78.2166, + height: 20, + schemaKey: 'Person', + attributes: [ + { + handleData: { + nodeId: 'id_1741246511422', + nodeName: 'Person', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityAttribute, + attributeName: '(# Connection)', + attributeType: 'float', + }, + }, + { + handleData: { + nodeId: 'id_1741246511422', + nodeName: 'Person', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityAttribute, + attributeName: 'name', + attributeType: 'string', + }, + }, + { + handleData: { + nodeId: 'id_1741246511422', + nodeName: 'Person', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityAttribute, + attributeName: 'born', + attributeType: 'int', + }, + }, + ], + leftRelationHandleId: { + nodeId: 'id_1741246511422', + nodeName: 'Person', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityLeft, + }, + rightRelationHandleId: { + nodeId: 'id_1741246511422', + nodeName: 'Person', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityRight, + }, + }, + }, + { + key: 'id_1741246511585', + attributes: { + x: 430, + y: 90, + id: 'id_1741246511585', + name: 'Movie', + type: QueryElementTypes.Entity, + width: 72.1999, + height: 20, + schemaKey: 'Movie', + attributes: [ + { + handleData: { + nodeId: 'id_1741246511585', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityAttribute, + attributeName: '(# Connection)', + attributeType: 'float', + }, + }, + { + handleData: { + nodeId: 'id_1741246511585', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.RelationAttribute, + attributeName: 'tagline', + attributeType: 'string', + }, + }, + { + handleData: { + nodeId: 'id_1741246511585', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityAttribute, + attributeName: 'votes', + attributeType: 'int', + }, + }, + { + handleData: { + nodeId: 'id_1741246511585', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityAttribute, + attributeName: 'title', + attributeType: 'string', + }, + }, + { + handleData: { + nodeId: 'id_1741246511585', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityAttribute, + attributeName: 'released', + attributeType: 'int', + }, + }, + ], + leftRelationHandleId: { + nodeId: 'id_1741246511585', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityLeft, + }, + rightRelationHandleId: { + nodeId: 'id_1741246511585', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityRight, + }, + }, + }, + { + key: 'id_1741246625352', + attributes: { + x: 180, + y: 170, + id: 'id_1741246625352', + name: 'PRODUCED', + type: QueryElementTypes.Relation, + depth: { + max: 1, + min: 1, + }, + width: 104.2002, + height: 20, + schemaKey: 'PRODUCED_PersonMovie', + attributes: [ + { + handleData: { + nodeId: 'id_1741246625352', + nodeName: 'PRODUCED', + nodeType: QueryElementTypes.Relation, + handleType: Handles.RelationAttribute, + attributeName: '(# Connection)', + attributeType: 'float', + }, + }, + ], + collection: 'PRODUCED', + leftEntityHandleId: { + nodeId: 'id_1741246625352', + nodeName: 'PRODUCED', + nodeType: QueryElementTypes.Relation, + handleType: Handles.RelationLeft, + }, + rightEntityHandleId: { + nodeId: 'id_1741246625352', + nodeName: 'PRODUCED', + nodeType: QueryElementTypes.Relation, + handleType: Handles.RelationRight, + }, + }, + }, + { + key: 'id_1741246630119', + attributes: { + x: 390, + y: 200, + id: 'id_1741246630119', + name: 'Movie', + type: QueryElementTypes.Entity, + width: 72.1999, + height: 20, + schemaKey: 'Movie', + attributes: [ + { + handleData: { + nodeId: 'id_1741246630119', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityAttribute, + attributeName: '(# Connection)', + attributeType: 'float', + }, + }, + { + handleData: { + nodeId: 'id_1741246630119', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.RelationAttribute, + attributeName: 'tagline', + attributeType: 'string', + }, + }, + { + handleData: { + nodeId: 'id_1741246630119', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.RelationAttribute, + attributeName: 'votes', + attributeType: 'int', + }, + }, + { + handleData: { + nodeId: 'id_1741246630119', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityAttribute, + attributeName: 'title', + attributeType: 'string', + }, + }, + { + handleData: { + nodeId: 'id_1741246630119', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.RelationAttribute, + attributeName: 'released', + attributeType: 'int', + }, + }, + ], + leftRelationHandleId: { + nodeId: 'id_1741246630119', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityLeft, + }, + rightRelationHandleId: { + nodeId: 'id_1741246630119', + nodeName: 'Movie', + nodeType: QueryElementTypes.Entity, + handleType: Handles.EntityRight, + }, + }, + }, + ], + options: { + type: 'mixed', + multi: true, + allowSelfLoops: true, + }, + attributes: {}, +}; +export const expectedResult_1: BackendQueryFormat = { + saveStateID: 'test', + return: ['*'], + query: [ + { + id: 'path_0', + node: { + id: 'id_1741246511422', + label: 'Person', + relation: { + id: 'id_1741246512287', + label: 'WROTE', + depth: { + max: 1, + min: 1, + }, + direction: 'BOTH', + node: { + id: 'id_1741246511585', + label: 'Movie', + }, + }, + }, + }, + { + id: 'path_1', + node: { + id: 'id_1741246511422', + label: 'Person', + relation: { + id: 'id_1741246625352', + label: 'PRODUCED', + depth: { + max: 1, + min: 1, + }, + direction: 'BOTH', + node: { + id: 'id_1741246630119', + label: 'Movie', + }, + }, + }, + }, + ], + machineLearning: [ + { type: MLTypesEnum.linkPrediction, parameters: [], id: 1 }, + { type: MLTypesEnum.centrality, parameters: [], id: 2 }, + { type: MLTypesEnum.communityDetection, parameters: [], id: 3 }, + { type: MLTypesEnum.shortestPath, parameters: [], id: 4 }, + ], + limit: 500, + cached: false, + logic: undefined, +}; -- GitLab From 8891e85df5c79aa4046512dbbc6d0bc619385bb9 Mon Sep 17 00:00:00 2001 From: MarcosPierasNL <pieras.marcos@gmail.com> Date: Tue, 11 Mar 2025 11:51:50 +0100 Subject: [PATCH 2/8] feat: adds updates --- bun.lockb | Bin 182704 -> 182704 bytes src/readers/queryService.ts | 2 ++ src/utils/queryPublisher.ts | 46 ++++++++++++++++++++++++++++++------ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/bun.lockb b/bun.lockb index 2207e737365016760d1a78a9f35c8d1a457ac494..60c32177a430fc1b9b24ea51dc61fd09c778caf1 100755 GIT binary patch delta 25 hcmdlmnR~-z?uIRl|6i~(#u@7w=vi!Md&#)!0RV@Y3P%6{ delta 25 ecmdlmnR~-z?uIRl|6i~(F@V8#wwH{n9smG(nF#X$ diff --git a/src/readers/queryService.ts b/src/readers/queryService.ts index 4609e93..765a995 100644 --- a/src/readers/queryService.ts +++ b/src/readers/queryService.ts @@ -87,6 +87,8 @@ export const queryServiceReader = async (frontendPublisher: RabbitMqBroker, mlPu } log.info('Starting query reader for', type); + const publisher = new QueryPublisher(frontendPublisher, mlPublisher); + const queryServiceConsumer = await new RabbitMqBroker( rabbitMq, 'requests-exchange', diff --git a/src/utils/queryPublisher.ts b/src/utils/queryPublisher.ts index d10bea7..ca17e2f 100644 --- a/src/utils/queryPublisher.ts +++ b/src/utils/queryPublisher.ts @@ -6,19 +6,35 @@ import type { RabbitMqBroker } from 'ts-common/rabbitMq'; export class QueryPublisher { private frontendPublisher: RabbitMqBroker; private mlPublisher: RabbitMqBroker; - private routingKey: string; - private headers: BackendMessageHeader; - private queryID: number; + private routingKey?: string; + private headers?: BackendMessageHeader; + private queryID?: string; - constructor(frontendPublisher: RabbitMqBroker, mlPublisher: RabbitMqBroker, headers: BackendMessageHeader, queryID: number) { + constructor(frontendPublisher: RabbitMqBroker, mlPublisher: RabbitMqBroker) { this.frontendPublisher = frontendPublisher; this.mlPublisher = mlPublisher; + } + + withHeaders(headers?: BackendMessageHeader) { this.headers = headers; - this.routingKey = headers.routingKey; + return this; + } + + withRoutingKey(routingKey?: string) { + this.routingKey = routingKey; + return this; + } + + withQueryID(queryID?: string) { this.queryID = queryID; + return this; } publishStatusToFrontend(status: string) { + if (!this.headers || !this.routingKey || !this.queryID) { + throw new Error('Headers or RoutingKey or queryID not set'); + } + this.frontendPublisher.publishMessageToFrontend( { type: wsReturnKey.queryStatusUpdate, @@ -32,6 +48,10 @@ export class QueryPublisher { } publishErrorToFrontend(reason: string) { + if (!this.headers || !this.routingKey || !this.queryID) { + throw new Error('Headers or RoutingKey or queryID not set'); + } + this.frontendPublisher.publishMessageToFrontend( { type: wsReturnKey.queryStatusError, @@ -45,13 +65,17 @@ export class QueryPublisher { } publishTranslationResultToFrontend(query: string) { + if (!this.headers || !this.routingKey || !this.queryID) { + throw new Error('Headers or RoutingKey or queryID not set'); + } + this.frontendPublisher.publishMessageToFrontend( { type: wsReturnKey.queryStatusTranslationResult, callID: this.headers.callID, value: { result: query, - queryID: this.queryID, + queryID: this.headers.callID, }, status: 'success', }, @@ -61,6 +85,10 @@ export class QueryPublisher { } publishResultToFrontend(result: GraphQueryResultMetaFromBackend) { + if (!this.headers || !this.routingKey || !this.queryID) { + throw new Error('Headers or RoutingKey or queryID not set'); + } + this.frontendPublisher.publishMessageToFrontend( { type: wsReturnKey.queryStatusResult, @@ -70,7 +98,7 @@ export class QueryPublisher { type: 'nodelink', payload: result, }, - queryID: this.queryID, + queryID: this.headers.callID, }, status: 'success', }, @@ -80,6 +108,10 @@ export class QueryPublisher { } publishMachineLearningRequest(result: GraphQueryResultFromBackend, mlAttributes: MachineLearning, headers: BackendMessageHeader) { + if (!this.headers || !this.routingKey) { + throw new Error('Headers or RoutingKey or queryID not set'); + } + // FIXME: Change ML to use the same message format that the frontend uses const toMlResult = { nodes: result.nodes.map(node => ({ ...node, id: node._id })), -- GitLab From 46bd50738a8af7315fb15e040b09eacb02c5c629 Mon Sep 17 00:00:00 2001 From: MarcosPierasNL <pieras.marcos@gmail.com> Date: Fri, 14 Mar 2025 11:40:13 +0100 Subject: [PATCH 3/8] test: adds ts logger --- package.json | 2 +- src/tests/query/queryConverter.test.ts | 3 +++ src/tests/query/queryTranslator/queryTranslator.test.ts | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 97913ac..618e5a6 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "version": "1.0.0", "scripts": { "build": "tsc", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "vitest", "dev": "bun run --watch --inspect=6498 src/index.ts", "start": "bun run --production src/index.ts", "lint": "eslint src/**/* --no-error-on-unmatched-pattern" diff --git a/src/tests/query/queryConverter.test.ts b/src/tests/query/queryConverter.test.ts index e35f7b4..81b5c0e 100644 --- a/src/tests/query/queryConverter.test.ts +++ b/src/tests/query/queryConverter.test.ts @@ -1,6 +1,9 @@ import { query2Cypher } from '../../utils/cypher/converter/queryConverter'; import { StringFilterTypes, type BackendQueryFormat } from 'ts-common'; import { expect, describe, it } from 'vitest'; +import { Logger } from 'ts-common'; + +Logger.excludedOwners.push('ts-common'); function fixCypherSpaces(cypher?: string | null): string { if (!cypher) { diff --git a/src/tests/query/queryTranslator/queryTranslator.test.ts b/src/tests/query/queryTranslator/queryTranslator.test.ts index c18a968..3a7ebdb 100644 --- a/src/tests/query/queryTranslator/queryTranslator.test.ts +++ b/src/tests/query/queryTranslator/queryTranslator.test.ts @@ -8,6 +8,9 @@ import { type BackendQueryFormat } from 'ts-common'; import { createQueryMultiGraphFromData, settingsBase, ss_id } from './testData'; import { visualQuery_1 } from './testData'; import { expectedResult_1 } from './testData'; +import { Logger } from 'ts-common'; + +Logger.excludedOwners.push('ts-common'); describe('query2backend', () => { it('should return correctly a node - 0', () => { -- GitLab From 8412bf264035ac45a688828592255d17d94dccb9 Mon Sep 17 00:00:00 2001 From: MarcosPierasNL <pieras.marcos@gmail.com> Date: Fri, 14 Mar 2025 11:52:59 +0100 Subject: [PATCH 4/8] test: query logger --- src/tests/query/queryConverter.test.ts | 1 + src/tests/query/queryTranslator/queryTranslator.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/tests/query/queryConverter.test.ts b/src/tests/query/queryConverter.test.ts index 81b5c0e..f5e819d 100644 --- a/src/tests/query/queryConverter.test.ts +++ b/src/tests/query/queryConverter.test.ts @@ -4,6 +4,7 @@ import { expect, describe, it } from 'vitest'; import { Logger } from 'ts-common'; Logger.excludedOwners.push('ts-common'); +Logger.excludedOwners.push('query-service'); function fixCypherSpaces(cypher?: string | null): string { if (!cypher) { diff --git a/src/tests/query/queryTranslator/queryTranslator.test.ts b/src/tests/query/queryTranslator/queryTranslator.test.ts index 3a7ebdb..85e7d47 100644 --- a/src/tests/query/queryTranslator/queryTranslator.test.ts +++ b/src/tests/query/queryTranslator/queryTranslator.test.ts @@ -11,6 +11,7 @@ import { expectedResult_1 } from './testData'; import { Logger } from 'ts-common'; Logger.excludedOwners.push('ts-common'); +Logger.excludedOwners.push('query-service'); describe('query2backend', () => { it('should return correctly a node - 0', () => { -- GitLab From 577604e8b7ca6292a6f2bdde0749741790722325 Mon Sep 17 00:00:00 2001 From: MarcosPierasNL <pieras.marcos@gmail.com> Date: Fri, 7 Mar 2025 15:53:37 +0100 Subject: [PATCH 5/8] test: adds tests for insights --- src/readers/diffCheck.ts | 7 +- src/readers/statCheck.ts | 2 +- src/tests/insights/diffCheck.test.ts | 64 +++++ src/tests/insights/statCheck.test.ts | 376 +++++++++++++++++++++++++++ 4 files changed, 447 insertions(+), 2 deletions(-) create mode 100644 src/tests/insights/diffCheck.test.ts create mode 100644 src/tests/insights/statCheck.test.ts diff --git a/src/readers/diffCheck.ts b/src/readers/diffCheck.ts index ba3dd09..eba2c6c 100644 --- a/src/readers/diffCheck.ts +++ b/src/readers/diffCheck.ts @@ -5,6 +5,10 @@ import type { GraphQueryResultMetaFromBackend } from 'ts-common/src/model/webSoc import { ums } from '../variables'; import type { InsightModel } from 'ts-common'; +export const compareHashedQueryResults = (previousHash: string | null, currentHash: string): boolean => { + return !previousHash || !hashIsEqual(currentHash, previousHash); +}; + export const diffCheck = async ( insight: InsightModel, ss: SaveState, @@ -20,7 +24,8 @@ export const diffCheck = async ( }); log.debug('Comparing hash values from current and previous query'); - const changed = !previousQueryResult || !hashIsEqual(queryResultHash, previousQueryResult); + const changed = compareHashedQueryResults(previousQueryResult, queryResultHash); + insight.status ||= changed; log.debug('Updated node and edge ids in SaveState'); diff --git a/src/readers/statCheck.ts b/src/readers/statCheck.ts index 96ea655..578f9cb 100644 --- a/src/readers/statCheck.ts +++ b/src/readers/statCheck.ts @@ -1,7 +1,7 @@ import type { GraphQueryResultMetaFromBackend, InsightModel } from 'ts-common'; import { log } from '../logger'; -function processAlarmStats(alarmStat: InsightModel, resultQuery: GraphQueryResultMetaFromBackend): boolean { +export function processAlarmStats(alarmStat: InsightModel, resultQuery: GraphQueryResultMetaFromBackend): boolean { for (const condition of alarmStat.conditionsCheck) { const ssInsightNode = condition.nodeLabel; const ssInsightStatistic = condition.statistic; diff --git a/src/tests/insights/diffCheck.test.ts b/src/tests/insights/diffCheck.test.ts new file mode 100644 index 0000000..5832586 --- /dev/null +++ b/src/tests/insights/diffCheck.test.ts @@ -0,0 +1,64 @@ +import { expect, test, describe, it } from 'bun:test'; +import type { GraphQueryResultMetaFromBackend } from 'ts-common'; +import { hashDictionary, hashIsEqual } from '../../utils/hashing'; +import { compareHashedQueryResults } from './../../readers/diffCheck'; + +describe('Hash Comparison Tests', () => { + it('should detect different hashes for different graph structures', () => { + // First query result + const query1: GraphQueryResultMetaFromBackend = { + nodes: [{ _id: 'node1' }, { _id: 'node2' }], + edges: [{ _id: 'edge1' }], + } as GraphQueryResultMetaFromBackend; + + // Second query result with different structure + const query2: GraphQueryResultMetaFromBackend = { + nodes: [{ _id: 'node1' }, { _id: 'node2' }, { _id: 'node3' }], + edges: [{ _id: 'edge1' }, { _id: 'edge2' }], + } as GraphQueryResultMetaFromBackend; + + const hash1 = hashDictionary({ + nodes: query1.nodes.map(node => node._id), + edges: query1.edges.map(edge => edge._id), + }); + + const hash2 = hashDictionary({ + nodes: query2.nodes.map(node => node._id), + edges: query2.edges.map(edge => edge._id), + }); + + // Test direct hash comparison + expect(hashIsEqual(hash1, hash2)).toBe(false); + + // Test using compareHashedQueryResults + expect(compareHashedQueryResults(hash1, hash2)).toBe(true); + }); + + it('should detect identical hashes for same graph structures', () => { + const query1 = { + nodes: [{ _id: 'node1' }, { _id: 'node2' }], + edges: [{ _id: 'edge1' }], + } as GraphQueryResultMetaFromBackend; + + const query2 = { + nodes: [{ _id: 'node1' }, { _id: 'node2' }], + edges: [{ _id: 'edge1' }], + } as GraphQueryResultMetaFromBackend; + + const hash1 = hashDictionary({ + nodes: query1.nodes.map(node => node._id), + edges: query1.edges.map(edge => edge._id), + }); + + const hash2 = hashDictionary({ + nodes: query2.nodes.map(node => node._id), + edges: query2.edges.map(edge => edge._id), + }); + + // Test direct hash comparison + expect(hashIsEqual(hash1, hash2)).toBe(true); + + // Test using compareHashedQueryResults + expect(compareHashedQueryResults(hash1, hash2)).toBe(false); + }); +}); diff --git a/src/tests/insights/statCheck.test.ts b/src/tests/insights/statCheck.test.ts new file mode 100644 index 0000000..b79e5ae --- /dev/null +++ b/src/tests/insights/statCheck.test.ts @@ -0,0 +1,376 @@ +import { expect, test, describe, it } from 'bun:test'; +import type { GraphQueryResultMetaFromBackend, InsightModel } from 'ts-common'; +import { processAlarmStats } from './../../readers/statCheck'; + +const baseInsight: Omit<InsightModel, 'conditionsCheck'> = { + id: 1, + name: 'Test Insight', + description: 'Base insight for testing', + recipients: ['test@example.com'], + frequency: 'daily', + template: 'default', + saveStateId: 'save-state-1', + type: 'alert', + alarmMode: 'conditional', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + previousResultHash: 'abc123', + lastProcessedAt: new Date().toISOString(), + status: true, +}; + +describe('QueryprocessAlarmStats', () => { + it('should return true when condition is met', () => { + const alarmStat: InsightModel = { + ...baseInsight, + conditionsCheck: [ + { + nodeLabel: 'TestNode', + statistic: 'count', + operator: '>', + value: 1, + }, + ], + }; + + const resultQuery: GraphQueryResultMetaFromBackend = { + nodes: [ + { + _id: 'node1', + label: 'TestNode', + attributes: { + name: 'Test Node 1', + value: 123, + }, + }, + { + _id: 'node2', + label: 'TestNode', + attributes: { + name: 'Test Node 2', + value: 456, + }, + }, + ], + edges: [ + { + _id: 'edge1', + label: 'CONNECTS', + from: 'node1', + to: 'node2', + attributes: { + weight: 1, + timestamp: '2025-03-06', + }, + }, + ], + metaData: { + topological: { + density: 0.5, + self_loops: 0, + }, + nodes: { + count: 2, + labels: ['TestNode'], + types: { + TestNode: { + count: 2, + avgDegreeIn: 0.5, + avgDegreeOut: 0.5, + attributes: { + name: { + attributeType: 'string', + statistics: { + uniqueItems: 2, + values: ['Test Node 1', 'Test Node 2'], + mode: 'Test Node 1', + count: 2, + }, + }, + value: { + attributeType: 'number', + statistics: { + min: 123, + max: 456, + average: 289.5, + count: 2, + }, + }, + }, + }, + }, + }, + edges: { + count: 1, + labels: ['CONNECTS'], + types: { + CONNECTS: { + count: 1, + attributes: { + weight: { + attributeType: 'number', + statistics: { + min: 1, + max: 1, + average: 1, + count: 1, + }, + }, + timestamp: { + attributeType: 'datetime', + statistics: { + min: 1743782400, + max: 1743782400, + range: 0, + }, + }, + }, + }, + }, + }, + }, + nodeCounts: { + TestNode: 2, + updatedAt: 122331, + }, + }; + + expect(processAlarmStats(alarmStat, resultQuery)).toBe(true); + }); + + it('should return false when condition is not met', () => { + const alarmStat: InsightModel = { + ...baseInsight, + conditionsCheck: [ + { + nodeLabel: 'TestNode', + statistic: 'count', + operator: '<', + value: 1, + }, + ], + }; + + const resultQuery: GraphQueryResultMetaFromBackend = { + nodes: [ + { + _id: 'node1', + label: 'TestNode', + attributes: { + name: 'Test Node 1', + value: 123, + }, + }, + { + _id: 'node2', + label: 'TestNode', + attributes: { + name: 'Test Node 2', + value: 456, + }, + }, + ], + edges: [ + { + _id: 'edge1', + label: 'CONNECTS', + from: 'node1', + to: 'node2', + attributes: { + weight: 1, + timestamp: '2025-03-06', + }, + }, + ], + metaData: { + topological: { + density: 0.5, + self_loops: 0, + }, + nodes: { + count: 2, + labels: ['TestNode'], + types: { + TestNode: { + count: 2, + avgDegreeIn: 0.5, + avgDegreeOut: 0.5, + attributes: { + name: { + attributeType: 'string', + statistics: { + uniqueItems: 2, + values: ['Test Node 1', 'Test Node 2'], + mode: 'Test Node 1', + count: 2, + }, + }, + value: { + attributeType: 'number', + statistics: { + min: 123, + max: 456, + average: 289.5, + count: 2, + }, + }, + }, + }, + }, + }, + edges: { + count: 1, + labels: ['CONNECTS'], + types: { + CONNECTS: { + count: 1, + attributes: { + weight: { + attributeType: 'number', + statistics: { + min: 1, + max: 1, + average: 1, + count: 1, + }, + }, + timestamp: { + attributeType: 'datetime', + statistics: { + min: 1743782400, + max: 1743782400, + range: 0, + }, + }, + }, + }, + }, + }, + }, + nodeCounts: { + TestNode: 2, + updatedAt: 122331, + }, + }; + + expect(processAlarmStats(alarmStat, resultQuery)).toBe(false); + }); + + it('should return false when nodeLabel is not found', () => { + const alarmStat: InsightModel = { + ...baseInsight, + conditionsCheck: [ + { + nodeLabel: 'NonExistentNode', + statistic: 'count', + operator: '>', + value: 5, + }, + ], + }; + + const resultQuery: GraphQueryResultMetaFromBackend = { + nodes: [ + { + _id: 'node1', + label: 'TestNode', + attributes: { + name: 'Test Node 1', + value: 123, + }, + }, + { + _id: 'node2', + label: 'TestNode', + attributes: { + name: 'Test Node 2', + value: 456, + }, + }, + ], + edges: [ + { + _id: 'edge1', + label: 'CONNECTS', + from: 'node1', + to: 'node2', + attributes: { + weight: 1, + timestamp: '2025-03-06', + }, + }, + ], + metaData: { + topological: { + density: 0.5, + self_loops: 0, + }, + nodes: { + count: 2, + labels: ['TestNode'], + types: { + TestNode: { + count: 2, + avgDegreeIn: 0.5, + avgDegreeOut: 0.5, + attributes: { + name: { + attributeType: 'string', + statistics: { + uniqueItems: 2, + values: ['Test Node 1', 'Test Node 2'], + mode: 'Test Node 1', + count: 2, + }, + }, + value: { + attributeType: 'number', + statistics: { + min: 123, + max: 456, + average: 289.5, + count: 2, + }, + }, + }, + }, + }, + }, + edges: { + count: 1, + labels: ['CONNECTS'], + types: { + CONNECTS: { + count: 1, + attributes: { + weight: { + attributeType: 'number', + statistics: { + min: 1, + max: 1, + average: 1, + count: 1, + }, + }, + timestamp: { + attributeType: 'datetime', + statistics: { + min: 1743782400, + max: 1743782400, + range: 0, + }, + }, + }, + }, + }, + }, + }, + nodeCounts: { + TestNode: 2, + updatedAt: 122331, + }, + }; + + expect(processAlarmStats(alarmStat, resultQuery)).toBe(false); + }); +}); -- GitLab From bb93fe1543650e535403acbc491d0828ad0ee81a Mon Sep 17 00:00:00 2001 From: MarcosPierasNL <pieras.marcos@gmail.com> Date: Fri, 14 Mar 2025 11:52:00 +0100 Subject: [PATCH 6/8] test: update to vitest --- src/tests/insights/diffCheck.test.ts | 6 +++++- src/tests/insights/statCheck.test.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/tests/insights/diffCheck.test.ts b/src/tests/insights/diffCheck.test.ts index 5832586..c0d0c64 100644 --- a/src/tests/insights/diffCheck.test.ts +++ b/src/tests/insights/diffCheck.test.ts @@ -1,7 +1,11 @@ -import { expect, test, describe, it } from 'bun:test'; +import { expect, test, describe, it } from 'vitest'; import type { GraphQueryResultMetaFromBackend } from 'ts-common'; import { hashDictionary, hashIsEqual } from '../../utils/hashing'; import { compareHashedQueryResults } from './../../readers/diffCheck'; +import { Logger } from 'ts-common'; + +Logger.excludedOwners.push('ts-common'); +Logger.excludedOwners.push('query-service'); describe('Hash Comparison Tests', () => { it('should detect different hashes for different graph structures', () => { diff --git a/src/tests/insights/statCheck.test.ts b/src/tests/insights/statCheck.test.ts index b79e5ae..a6b1fd3 100644 --- a/src/tests/insights/statCheck.test.ts +++ b/src/tests/insights/statCheck.test.ts @@ -1,6 +1,10 @@ -import { expect, test, describe, it } from 'bun:test'; +import { expect, test, describe, it } from 'vitest'; import type { GraphQueryResultMetaFromBackend, InsightModel } from 'ts-common'; import { processAlarmStats } from './../../readers/statCheck'; +import { Logger } from 'ts-common'; + +Logger.excludedOwners.push('ts-common'); +Logger.excludedOwners.push('query-service'); const baseInsight: Omit<InsightModel, 'conditionsCheck'> = { id: 1, -- GitLab From cf27478a37800d6b792de31fbd0096b3fdb169da Mon Sep 17 00:00:00 2001 From: MarcosPierasNL <pieras.marcos@gmail.com> Date: Fri, 7 Mar 2025 16:16:56 +0100 Subject: [PATCH 7/8] test: adds populate template test --- src/tests/insights/populateTemplate.test.ts | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/tests/insights/populateTemplate.test.ts diff --git a/src/tests/insights/populateTemplate.test.ts b/src/tests/insights/populateTemplate.test.ts new file mode 100644 index 0000000..fafa759 --- /dev/null +++ b/src/tests/insights/populateTemplate.test.ts @@ -0,0 +1,50 @@ +import { expect, test, describe, it } from 'bun:test'; +import { populateTemplate } from './../../utils/insights'; +import { type GraphQueryResultMetaFromBackend } from 'ts-common'; + +const mockResult: GraphQueryResultMetaFromBackend = { + metaData: { + topological: { + density: 0, + self_loops: 0, + }, + nodes: { + count: 1, + labels: ['NodeTypeA'], + types: { + NodeTypeA: { + count: 1, + attributes: { + age: { + attributeType: 'number', + statistics: { + min: 10, + max: 50, + average: 42, + count: 100, + }, + }, + }, + }, + }, + }, + edges: { + count: 0, + labels: [], + types: {}, + }, + }, + nodeCounts: { updatedAt: 2313 }, + nodes: [], + edges: [], +}; + +describe('populateTemplate', () => { + it('should replace statistic variables correctly', async () => { + const template = 'The mean value is {{ statistic:NodeTypeA • age • average }}.'; + const expectedOutput = 'The mean value is 42 .'; + const result = await populateTemplate(template, mockResult, []); + + expect(result).toBe(expectedOutput); + }); +}); -- GitLab From c669d9d086e0784d04a1bca9e7390872b574d713 Mon Sep 17 00:00:00 2001 From: MarcosPierasNL <pieras.marcos@gmail.com> Date: Fri, 14 Mar 2025 11:56:33 +0100 Subject: [PATCH 8/8] test: adds vitest and logger --- src/tests/insights/populateTemplate.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/tests/insights/populateTemplate.test.ts b/src/tests/insights/populateTemplate.test.ts index fafa759..ce8e70b 100644 --- a/src/tests/insights/populateTemplate.test.ts +++ b/src/tests/insights/populateTemplate.test.ts @@ -1,6 +1,10 @@ -import { expect, test, describe, it } from 'bun:test'; +import { expect, test, describe, it } from 'vitest'; import { populateTemplate } from './../../utils/insights'; import { type GraphQueryResultMetaFromBackend } from 'ts-common'; +import { Logger } from 'ts-common'; + +Logger.excludedOwners.push('ts-common'); +Logger.excludedOwners.push('query-service'); const mockResult: GraphQueryResultMetaFromBackend = { metaData: { -- GitLab