From e5586c972344a1d10fb84b9014bbeb2715d49925 Mon Sep 17 00:00:00 2001 From: Leonardo <leomilho@gmail.com> Date: Fri, 10 Jan 2025 16:43:03 +0100 Subject: [PATCH] chore: test, lint and prettier executed --- src/frontend/statistics/graphStatistics.ts | 9 +- src/frontend/statistics/index.ts | 34 +- ...teStats.spec.ts => attributeStats.test.ts} | 4 +- ...eType.spec.ts => getAttributeType.test.ts} | 2 +- ...istics.spec.ts => graphStatistics.test.ts} | 14 +- .../statistics/utils/attributeStats/array.ts | 1 - .../utils/attributeStats/boolean.ts | 1 - .../utils/attributeStats/categorical.ts | 3 +- .../utils/attributeStats/numerical.ts | 1 - .../statistics/utils/attributeStats/object.ts | 1 - .../utils/attributeStats/temporal.ts | 1 - .../statistics/utils/updateStatistics.ts | 1 - src/index.ts | 34 +- src/logger.ts | 7 +- src/readers/diffCheck.ts | 49 +- src/readers/insightProcessor.ts | 64 +- src/readers/queryService.ts | 249 ++--- src/readers/statCheck.ts | 110 +- src/utils/cypher/converter/export.ts | 31 +- src/utils/cypher/converter/filter.ts | 94 +- src/utils/cypher/converter/index.ts | 2 +- src/utils/cypher/converter/logic.ts | 142 +-- src/utils/cypher/converter/model.ts | 13 +- src/utils/cypher/converter/node.ts | 62 +- .../cypher/converter/queryConverter.test.ts | 954 +++++++++--------- src/utils/cypher/converter/queryConverter.ts | 120 +-- src/utils/cypher/converter/relation.ts | 84 +- src/utils/cypher/queryParser.ts | 265 ++--- src/utils/queryPublisher.ts | 2 +- src/utils/reactflow/query2backend.ts | 66 +- src/variables.ts | 79 +- 31 files changed, 1267 insertions(+), 1232 deletions(-) rename src/frontend/statistics/tests/{attributeStats.spec.ts => attributeStats.test.ts} (96%) rename src/frontend/statistics/tests/{getAttributeType.spec.ts => getAttributeType.test.ts} (97%) rename src/frontend/statistics/tests/{graphStatistics.spec.ts => graphStatistics.test.ts} (87%) diff --git a/src/frontend/statistics/graphStatistics.ts b/src/frontend/statistics/graphStatistics.ts index 93ed780..904223d 100644 --- a/src/frontend/statistics/graphStatistics.ts +++ b/src/frontend/statistics/graphStatistics.ts @@ -45,12 +45,12 @@ export const getGraphStatistics = (graph: GraphQueryResultFromBackend): GraphSta const attributeType = getAttributeType(attributeValue); nodeTypeAttributes[attributeId] = { attributeType, - statistics: initializeStatistics(attributeType) + statistics: initializeStatistics(attributeType), }; } updateStatistics(nodeTypeAttributes[attributeId], attributeValue); } - }; + } // Process edges // Pre-initialize sets and maps for faster lookups @@ -81,13 +81,12 @@ export const getGraphStatistics = (graph: GraphQueryResultFromBackend): GraphSta const attributeType = getAttributeType(attributeValue); edgeTypeAttributes[attributeId] = { attributeType, - statistics: initializeStatistics(attributeType) + statistics: initializeStatistics(attributeType), }; } updateStatistics(edgeTypeAttributes[attributeId], attributeValue); } - }; + } return metaData; }; - diff --git a/src/frontend/statistics/index.ts b/src/frontend/statistics/index.ts index 7d22fed..2d2a10f 100644 --- a/src/frontend/statistics/index.ts +++ b/src/frontend/statistics/index.ts @@ -4,24 +4,24 @@ import { getGraphStatistics } from './graphStatistics'; export * from './graphStatistics'; export const graphQueryBackend2graphQuery = (payload: GraphQueryResultFromBackend): GraphQueryResultMetaFromBackend => { - const nodeIDs = new Set(); - const edgeIDs = new Set(); + const nodeIDs = new Set(); + const edgeIDs = new Set(); - // Assign ids to a hidden attribute _id to better identify them - for (let i = 0; i < payload.nodes.length; i++) { - payload.nodes[i]._id = payload.nodes[i]._id || (payload.nodes[i] as any).id; - nodeIDs.add(payload.nodes[i]._id); - } - for (let i = 0; i < payload.edges.length; i++) { - payload.edges[i]._id = payload.edges[i]._id || (payload.edges[i] as any).id; - edgeIDs.add(payload.edges[i]._id); - } + // Assign ids to a hidden attribute _id to better identify them + for (let i = 0; i < payload.nodes.length; i++) { + payload.nodes[i]._id = payload.nodes[i]._id || (payload.nodes[i] as any).id; + nodeIDs.add(payload.nodes[i]._id); + } + for (let i = 0; i < payload.edges.length; i++) { + payload.edges[i]._id = payload.edges[i]._id || (payload.edges[i] as any).id; + edgeIDs.add(payload.edges[i]._id); + } - // Only keep one node and one edge per id. This is also done in the backend, but we do it here as well to be sure. - let nodes = [...nodeIDs].map((nodeID) => payload.nodes.find((node) => node._id === nodeID) as unknown as NodeQueryResult); - let edges = [...edgeIDs].map((edgeID) => payload.edges.find((edge) => edge._id === edgeID) as unknown as EdgeQueryResult); + // Only keep one node and one edge per id. This is also done in the backend, but we do it here as well to be sure. + const nodes = [...nodeIDs].map(nodeID => payload.nodes.find(node => node._id === nodeID) as unknown as NodeQueryResult); + const edges = [...edgeIDs].map(edgeID => payload.edges.find(edge => edge._id === edgeID) as unknown as EdgeQueryResult); - const metaData = getGraphStatistics(payload); + const metaData = getGraphStatistics(payload); - return { metaData, nodes, edges }; -}; \ No newline at end of file + return { metaData, nodes, edges }; +}; diff --git a/src/frontend/statistics/tests/attributeStats.spec.ts b/src/frontend/statistics/tests/attributeStats.test.ts similarity index 96% rename from src/frontend/statistics/tests/attributeStats.spec.ts rename to src/frontend/statistics/tests/attributeStats.test.ts index 1c794fb..17f3093 100644 --- a/src/frontend/statistics/tests/attributeStats.spec.ts +++ b/src/frontend/statistics/tests/attributeStats.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from 'bun:test'; import { updateArrayStats, updateBooleanStats, @@ -8,7 +8,7 @@ import { updateObjectStats, initializeStatistics, } from '../utils/attributeStats'; -import type { ArrayStats, BooleanStats, CategoricalStats, NumericalStats, TemporalStats, ObjectStats } from 'ts-common/src/model/query/statistics'; +import type { ArrayStats, BooleanStats, CategoricalStats, NumericalStats, TemporalStats, ObjectStats } from 'ts-common'; describe('updateArrayStats', () => { it('should update the length of the array', () => { diff --git a/src/frontend/statistics/tests/getAttributeType.spec.ts b/src/frontend/statistics/tests/getAttributeType.test.ts similarity index 97% rename from src/frontend/statistics/tests/getAttributeType.spec.ts rename to src/frontend/statistics/tests/getAttributeType.test.ts index 5625845..6b8923c 100644 --- a/src/frontend/statistics/tests/getAttributeType.spec.ts +++ b/src/frontend/statistics/tests/getAttributeType.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from 'bun:test'; import { getAttributeType } from '../utils/getAttributeType'; // Sample values for testing diff --git a/src/frontend/statistics/tests/graphStatistics.spec.ts b/src/frontend/statistics/tests/graphStatistics.test.ts similarity index 87% rename from src/frontend/statistics/tests/graphStatistics.spec.ts rename to src/frontend/statistics/tests/graphStatistics.test.ts index 9d59f29..f2b912e 100644 --- a/src/frontend/statistics/tests/graphStatistics.spec.ts +++ b/src/frontend/statistics/tests/graphStatistics.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect } from 'vitest'; -import { GraphQueryResultFromBackend } from '../../data-access/store/graphQueryResultSlice'; +import { describe, it, expect } from 'bun:test'; import { getGraphStatistics } from '../graphStatistics'; +import type { GraphQueryResultFromBackend } from 'ts-common'; describe('getGraphStatistics', () => { it('should return correct statistics for a graph with no nodes and edges', () => { @@ -93,9 +93,9 @@ describe('getGraphStatistics', () => { it('should correctly count self-loops', () => { const graph: GraphQueryResultFromBackend = { - nodes: [{ _id: '1', attributes: {} }], + nodes: [{ _id: '1', attributes: {}, label: 'Person' }], edges: [ - { _id: 'e1', attributes: {}, from: '1', to: '1' }, // self-loop + { _id: 'e1', attributes: {}, from: '1', to: '1', label: 'TO_PERSON' }, // self-loop ], }; @@ -107,10 +107,10 @@ describe('getGraphStatistics', () => { it('should correctly compute density for a graph with nodes and edges', () => { const graph: GraphQueryResultFromBackend = { nodes: [ - { _id: '1', attributes: {} }, - { _id: '2', attributes: {} }, + { _id: '1', attributes: {}, label: 'Person' }, + { _id: '2', attributes: {}, label: 'Person' }, ], - edges: [{ _id: 'e1', attributes: {}, from: '1', to: '2' }], + edges: [{ _id: 'e1', attributes: {}, from: '1', to: '2', label: 'TO_PERSON' }], }; const stats = getGraphStatistics(graph); diff --git a/src/frontend/statistics/utils/attributeStats/array.ts b/src/frontend/statistics/utils/attributeStats/array.ts index 25e217c..1845238 100644 --- a/src/frontend/statistics/utils/attributeStats/array.ts +++ b/src/frontend/statistics/utils/attributeStats/array.ts @@ -4,4 +4,3 @@ export const updateArrayStats = (stats: ArrayStats, value: any[]) => { stats.length = value.length; stats.count++; }; - diff --git a/src/frontend/statistics/utils/attributeStats/boolean.ts b/src/frontend/statistics/utils/attributeStats/boolean.ts index 5439ad7..a737e97 100644 --- a/src/frontend/statistics/utils/attributeStats/boolean.ts +++ b/src/frontend/statistics/utils/attributeStats/boolean.ts @@ -7,4 +7,3 @@ export const updateBooleanStats = (stats: BooleanStats, value: boolean) => { stats.false += 1; } }; - diff --git a/src/frontend/statistics/utils/attributeStats/categorical.ts b/src/frontend/statistics/utils/attributeStats/categorical.ts index 69a7d4c..860cda7 100644 --- a/src/frontend/statistics/utils/attributeStats/categorical.ts +++ b/src/frontend/statistics/utils/attributeStats/categorical.ts @@ -7,10 +7,9 @@ export const updateCategoricalStats = (stats: CategoricalStats, value: string | stats.uniqueItems = new Set(stats.values).size; const frequencyMap: { [key: string]: number } = {}; - stats.values.forEach((val) => { + stats.values.forEach(val => { frequencyMap[val] = (frequencyMap[val] || 0) + 1; }); stats.mode = Object.keys(frequencyMap).reduce((a, b) => (frequencyMap[a] > frequencyMap[b] ? a : b)); stats.count++; }; - diff --git a/src/frontend/statistics/utils/attributeStats/numerical.ts b/src/frontend/statistics/utils/attributeStats/numerical.ts index e3a8ddb..d0a68b5 100644 --- a/src/frontend/statistics/utils/attributeStats/numerical.ts +++ b/src/frontend/statistics/utils/attributeStats/numerical.ts @@ -7,4 +7,3 @@ export const updateNumericalStats = (stats: NumericalStats, value: number) => { stats.count++; stats.average = (stats.average * (stats.count - 1) + value) / stats.count; }; - diff --git a/src/frontend/statistics/utils/attributeStats/object.ts b/src/frontend/statistics/utils/attributeStats/object.ts index 6f38156..46e60f2 100644 --- a/src/frontend/statistics/utils/attributeStats/object.ts +++ b/src/frontend/statistics/utils/attributeStats/object.ts @@ -3,4 +3,3 @@ import type { ObjectStats } from 'ts-common'; export const updateObjectStats = (stats: ObjectStats, value: object) => { stats.length = Object.keys(value).length; }; - diff --git a/src/frontend/statistics/utils/attributeStats/temporal.ts b/src/frontend/statistics/utils/attributeStats/temporal.ts index 70468b2..49e1b3e 100644 --- a/src/frontend/statistics/utils/attributeStats/temporal.ts +++ b/src/frontend/statistics/utils/attributeStats/temporal.ts @@ -8,4 +8,3 @@ export const updateTemporalStats = (stats: TemporalStats, value: string | Date) stats.range = stats.max - stats.min; }; - diff --git a/src/frontend/statistics/utils/updateStatistics.ts b/src/frontend/statistics/utils/updateStatistics.ts index 694d2aa..03a49de 100644 --- a/src/frontend/statistics/utils/updateStatistics.ts +++ b/src/frontend/statistics/utils/updateStatistics.ts @@ -43,4 +43,3 @@ export const updateStatistics = (attribute: AttributeStats<AttributeType>, value break; } }; - diff --git a/src/index.ts b/src/index.ts index 8dff35c..c94c87a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,25 +4,31 @@ import { log } from './logger'; import { queryServiceReader } from './readers/queryService'; import { insightProcessor } from './readers/insightProcessor'; - async function main() { - log.info('Starting query-service...'); + log.info('Starting query-service...'); - log.info('Connecting to RabbitMQ...'); - const frontendPublisher = await new RabbitMqBroker(rabbitMq, 'ui-direct-exchange').connect(); - const mlPublisher = await new RabbitMqBroker(rabbitMq, 'ml-direct-exchange', "query-service", undefined, { autoDelete: false }, { autoDelete: false }).connect(); - log.info('Connected to RabbitMQ!'); + log.info('Connecting to RabbitMQ...'); + const frontendPublisher = await new RabbitMqBroker(rabbitMq, 'ui-direct-exchange').connect(); + const mlPublisher = await new RabbitMqBroker( + rabbitMq, + 'ml-direct-exchange', + 'query-service', + undefined, + { autoDelete: false }, + { autoDelete: false }, + ).connect(); + log.info('Connected to RabbitMQ!'); - log.info('Connecting to Redis...'); - const redis = new RedisConnector(REDIS_PASSWORD, REDIS_HOST, REDIS_PORT); - await redis.connect(); - log.info('Connected to Redis!'); + log.info('Connecting to Redis...'); + const redis = new RedisConnector(REDIS_PASSWORD, REDIS_HOST, REDIS_PORT); + await redis.connect(); + log.info('Connected to Redis!'); - queryServiceReader(frontendPublisher, mlPublisher, redis, 'neo4j'); - insightProcessor(); - // TODO: other query services for other databases + queryServiceReader(frontendPublisher, mlPublisher, redis, 'neo4j'); + insightProcessor(); + // TODO: other query services for other databases - log.info('Connected to RabbitMQ!'); + log.info('Connected to RabbitMQ!'); } main(); diff --git a/src/logger.ts b/src/logger.ts index 1afbe97..7c49b4a 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,5 +1,4 @@ -import { Logger } from "ts-common"; -import { LOG_LEVEL } from "./variables"; +import { Logger } from 'ts-common'; +import { LOG_LEVEL } from './variables'; - -export const log = new Logger(LOG_LEVEL as any, "query-service"); \ No newline at end of file +export const log = new Logger(LOG_LEVEL as any, 'query-service'); diff --git a/src/readers/diffCheck.ts b/src/readers/diffCheck.ts index 4fb4e2f..ba3dd09 100644 --- a/src/readers/diffCheck.ts +++ b/src/readers/diffCheck.ts @@ -1,30 +1,33 @@ -import type { SaveState } from "ts-common"; -import { log } from "../logger"; -import { hashDictionary, hashIsEqual } from "../utils/hashing"; -import type { GraphQueryResultMetaFromBackend } from "ts-common/src/model/webSocket/graphResult"; -import { ums } from "../variables"; -import type { InsightModel } from "ts-common"; +import type { SaveState } from 'ts-common'; +import { log } from '../logger'; +import { hashDictionary, hashIsEqual } from '../utils/hashing'; +import type { GraphQueryResultMetaFromBackend } from 'ts-common/src/model/webSocket/graphResult'; +import { ums } from '../variables'; +import type { InsightModel } from 'ts-common'; +export const diffCheck = async ( + insight: InsightModel, + ss: SaveState, + graphResult: GraphQueryResultMetaFromBackend, +): Promise<InsightModel> => { + const previousQueryResult = await ums.getInsightHash(insight.id); -export const diffCheck = async (insight: InsightModel, ss: SaveState, graphResult: GraphQueryResultMetaFromBackend): Promise<InsightModel> => { - const previousQueryResult = await ums.getInsightHash(insight.id); + log.debug('Received query request:', ss); - log.debug("Received query request:", ss); + const queryResultHash = hashDictionary({ + nodes: graphResult.nodes.map(node => node._id), + edges: graphResult.edges.map(edge => edge._id), + }); - const queryResultHash = hashDictionary({ - nodes: graphResult.nodes.map((node) => node._id), - edges: graphResult.edges.map((edge) => edge._id), - }); + log.debug('Comparing hash values from current and previous query'); + const changed = !previousQueryResult || !hashIsEqual(queryResultHash, previousQueryResult); + insight.status ||= changed; + log.debug('Updated node and edge ids in SaveState'); - log.debug("Comparing hash values from current and previous query"); - const changed = !previousQueryResult || !hashIsEqual(queryResultHash, previousQueryResult); - insight.status ||= changed; - log.debug("Updated node and edge ids in SaveState"); + if (changed) { + await ums.setInsightHash(insight.id, queryResultHash); + log.debug('Saved new hash value to the database'); + } - if (changed) { - await ums.setInsightHash(insight.id, queryResultHash); - log.debug("Saved new hash value to the database"); - } - - return insight; + return insight; }; diff --git a/src/readers/insightProcessor.ts b/src/readers/insightProcessor.ts index 77b6fec..e0fef37 100644 --- a/src/readers/insightProcessor.ts +++ b/src/readers/insightProcessor.ts @@ -1,16 +1,16 @@ -import { rabbitMq, ums, mail, SMTP_USER, DEBUG_EMAIL } from "../variables"; -import { log } from "../logger"; -import { RabbitMqBroker, type InsightModel } from "ts-common"; -import { createHeadlessEditor } from "@lexical/headless"; -import { $generateHtmlFromNodes } from "@lexical/html"; -import { JSDOM } from "jsdom"; -import { Query2BackendQuery } from "../utils/reactflow/query2backend"; -import { query2Cypher } from "../utils/cypher/converter"; -import { queryService } from "./queryService"; -import { statCheck } from "./statCheck"; -import { diffCheck } from "./diffCheck"; -import { VariableNode } from "../utils/lexical"; -import { populateTemplate } from "../utils/insights"; +import { rabbitMq, ums, mail, SMTP_USER, DEBUG_EMAIL } from '../variables'; +import { log } from '../logger'; +import { RabbitMqBroker, type InsightModel } from 'ts-common'; +import { createHeadlessEditor } from '@lexical/headless'; +import { $generateHtmlFromNodes } from '@lexical/html'; +import { JSDOM } from 'jsdom'; +import { Query2BackendQuery } from '../utils/reactflow/query2backend'; +import { query2Cypher } from '../utils/cypher/converter'; +import { queryService } from './queryService'; +import { statCheck } from './statCheck'; +import { diffCheck } from './diffCheck'; +import { VariableNode } from '../utils/lexical'; +import { populateTemplate } from '../utils/insights'; const dom = new JSDOM(); function setUpDom() { @@ -19,14 +19,13 @@ function setUpDom() { const _documentFragment = global.DocumentFragment; const _navigator = global.navigator; - // @ts-ignore + // @ts-expect-error - global.window is readonly global.window = dom.window; global.document = dom.window.document; global.DocumentFragment = dom.window.DocumentFragment; global.navigator = dom.window.navigator; return () => { - // @ts-ignore global.window = _window; global.document = _document; global.DocumentFragment = _documentFragment; @@ -37,36 +36,41 @@ function setUpDom() { export const insightProcessor = async () => { if (mail == null) { log.warn('Mail is not configured. Insight processor will be disabled'); - return + return; } log.info('Starting insight processor'); - const insightProcessorConsumer = await new RabbitMqBroker(rabbitMq, 'insight-processor', `insight-processor`, `insight-processor`).connect(); + const insightProcessorConsumer = await new RabbitMqBroker( + rabbitMq, + 'insight-processor', + `insight-processor`, + `insight-processor`, + ).connect(); log.info('Connected to RabbitMQ ST!'); - await insightProcessorConsumer.startConsuming<{ insight: InsightModel, force: boolean }>("query-service", async (message, headers) => { + await insightProcessorConsumer.startConsuming<{ insight: InsightModel; force: boolean }>('query-service', async (message, headers) => { let insight = message.insight; if (insight == null || insight.template == null || insight.userId == null || insight.saveStateId == null) { - log.error("Invalid Insight received in insightProcessorConsumer:", insight); + log.error('Invalid Insight received in insightProcessorConsumer:', insight); return; } - if (insight.alarmMode === "disabled" && !message.force) { - log.debug("Alarm mode is disabled", insight.id); + if (insight.alarmMode === 'disabled' && !message.force) { + log.debug('Alarm mode is disabled', insight.id); return; } if (insight.recipients == null || insight.recipients.length === 0) { - log.debug("No recipients found in the insight, skipping"); + log.debug('No recipients found in the insight, skipping'); return; } - log.info("Received insight to be processed", insight); + log.info('Received insight to be processed', insight); const editor = createHeadlessEditor({ nodes: [VariableNode], - onError: (error) => { + onError: error => { log.error(error); }, }); @@ -89,11 +93,11 @@ export const insightProcessor = async () => { insight.status = false; - if (insight.alarmMode === "always") { + if (insight.alarmMode === 'always') { insight.status = true; - } else if (insight.alarmMode === "diff") { + } else if (insight.alarmMode === 'diff') { insight = await diffCheck(insight, ss, result); - } else if (insight.alarmMode === "conditional" && insight.conditionsCheck && insight.conditionsCheck.length > 0) { + } else if (insight.alarmMode === 'conditional' && insight.conditionsCheck && insight.conditionsCheck.length > 0) { insight = statCheck(insight, result); } @@ -114,7 +118,7 @@ export const insightProcessor = async () => { for (const recipient of insight.recipients) { if (mail == null) { log.warn('Mail is not configured. Insight processor will be disabled'); - return + return; } if (DEBUG_EMAIL) { @@ -127,8 +131,8 @@ export const insightProcessor = async () => { to: recipient, from: SMTP_USER, subject: `GraphPolaris report: ${insight.name}`, - html: html - }) + html: html, + }); log.info('Mail sent to ', recipient); } }); diff --git a/src/readers/queryService.ts b/src/readers/queryService.ts index ac90c9a..3263a09 100644 --- a/src/readers/queryService.ts +++ b/src/readers/queryService.ts @@ -1,131 +1,132 @@ -import { type QueryRequest } from "ts-common"; -import { - Neo4jConnection, - type DbConnection, - RabbitMqBroker, - RedisConnector -} from "ts-common"; -import { rabbitMq, ums, type QueryExecutionTypes } from "../variables"; -import { log } from "../logger"; -import { QueryPublisher } from "../utils/queryPublisher"; -import { query2Cypher } from "../utils/cypher/converter"; -import { parseCypherQuery } from "../utils/cypher/queryParser"; -import { formatTimeDifference } from "ts-common/src/logger/logger"; -import { graphQueryBackend2graphQuery } from "../frontend/statistics"; -import { Query2BackendQuery } from "../utils/reactflow/query2backend"; -import type { GraphQueryResultMetaFromBackend } from "ts-common/src/model/webSocket/graphResult"; - +import { type QueryRequest } from 'ts-common'; +import { Neo4jConnection, type DbConnection, RabbitMqBroker, RedisConnector } from 'ts-common'; +import { rabbitMq, ums, type QueryExecutionTypes } from '../variables'; +import { log } from '../logger'; +import { QueryPublisher } from '../utils/queryPublisher'; +import { query2Cypher } from '../utils/cypher/converter'; +import { parseCypherQuery } from '../utils/cypher/queryParser'; +import { formatTimeDifference } from 'ts-common/src/logger/logger'; +import { graphQueryBackend2graphQuery } from '../frontend/statistics'; +import { Query2BackendQuery } from '../utils/reactflow/query2backend'; +import type { GraphQueryResultMetaFromBackend } from 'ts-common/src/model/webSocket/graphResult'; export const queryService = async (db: DbConnection, query: string): Promise<GraphQueryResultMetaFromBackend> => { - // TODO: only neo4j is supported for now - const connection = new Neo4jConnection(db); - try { - const [neo4jResult] = await connection.run([query]); - const graph = parseCypherQuery(neo4jResult.records); - - // calculate metadata - const result = graphQueryBackend2graphQuery(graph); - - return result - } catch (error) { - log.error('Error parsing query result:', query, error); - throw new Error('Error parsing query result'); - } finally { - connection.close(); + // TODO: only neo4j is supported for now + const connection = new Neo4jConnection(db); + try { + const [neo4jResult] = await connection.run([query]); + const graph = parseCypherQuery(neo4jResult.records); + + // calculate metadata + const result = graphQueryBackend2graphQuery(graph); + + return result; + } catch (error) { + log.error('Error parsing query result:', query, error); + throw new Error('Error parsing query result'); + } finally { + connection.close(); + } +}; + +export const queryServiceReader = async ( + frontendPublisher: RabbitMqBroker, + mlPublisher: RabbitMqBroker, + redis: RedisConnector, + type: QueryExecutionTypes, +) => { + if (type == null) { + log.error('Unsupported query execution type:', type); + throw new Error('Unsupported query execution type'); + } + log.info('Starting query reader for', type); + + const publisher = new QueryPublisher(frontendPublisher, mlPublisher); + + const queryServiceConsumer = await new RabbitMqBroker( + rabbitMq, + 'requests-exchange', + `${type}-query-queue`, + `${type}-query-request`, + ).connect(); + + log.info('Connected to RabbitMQ ST!'); + + await queryServiceConsumer.startConsuming<QueryRequest>('query-service', async (message, headers) => { + const startTime = Date.now(); + const ss = await ums.getUserSaveState(headers.message.sessionData.userID, message.saveStateID); + + if (!ss) { + log.error('Invalid SaveState received in queryServiceConsumer:', ss); + publisher.publishErrorToFrontend('Invalid SaveState'); + return; + } + + log.debug('Received query request:', message, headers, ss); + log.debug('Received routing key:', headers.routingKey); + + if (!headers.callID) { + log.error('QueryID not set in headers:', headers); + return; } -} + publisher.withHeaders(headers).withRoutingKey(headers.routingKey).withQueryID(headers.callID); + publisher.publishStatusToFrontend('Received'); + + if (ss == null || ss.dbConnections == null || ss.dbConnections[0] == null || ss.dbConnections.length === 0) { + log.error('Invalid SaveState received in queryServiceConsumer:', ss); + publisher.publishErrorToFrontend('Invalid SaveState'); + return; + } -export const queryServiceReader = async (frontendPublisher: RabbitMqBroker, mlPublisher: RabbitMqBroker, redis: RedisConnector, type: QueryExecutionTypes) => { - if (type == null) { - log.error('Unsupported query execution type:', type); - throw new Error('Unsupported query execution type'); + const visualQuery = ss.queries[0].graph; + log.debug('Received query request:', message, headers, visualQuery); + if (visualQuery.nodes.length === 0) { + log.info('Empty query received'); + publisher.publishResultToFrontend({ nodes: [], edges: [] }); + return; } - log.info('Starting query reader for', type); - - const publisher = new QueryPublisher(frontendPublisher, mlPublisher); - - const queryServiceConsumer = await new RabbitMqBroker(rabbitMq, 'requests-exchange', `${type}-query-queue`, `${type}-query-request`).connect(); - - log.info('Connected to RabbitMQ ST!'); - - await queryServiceConsumer.startConsuming<QueryRequest>("query-service", async (message, headers) => { - const startTime = Date.now(); - const ss = await ums.getUserSaveState(headers.message.sessionData.userID, message.saveStateID); - - if (!ss) { - log.error('Invalid SaveState received in queryServiceConsumer:', ss); - publisher.publishErrorToFrontend('Invalid SaveState'); - return; - } - - log.debug('Received query request:', message, headers, ss); - log.debug('Received routing key:', headers.routingKey); - - if (!headers.callID) { - log.error('QueryID not set in headers:', headers); - return; - } - - - publisher.withHeaders(headers).withRoutingKey(headers.routingKey).withQueryID(headers.callID); - publisher.publishStatusToFrontend('Received'); - - if (ss == null || ss.dbConnections == null || ss.dbConnections[0] == null || ss.dbConnections.length === 0) { - log.error('Invalid SaveState received in queryServiceConsumer:', ss); - publisher.publishErrorToFrontend('Invalid SaveState'); - return; - } - - const visualQuery = ss.queries[0].graph; - log.debug('Received query request:', message, headers, visualQuery); - if (visualQuery.nodes.length === 0) { - log.info('Empty query received'); - publisher.publishResultToFrontend({ nodes: [], edges: [] }); - return; - } - - const queryBuilderSettings = ss.queries[0].settings; - const ml = message.ml; - const convertedQuery = Query2BackendQuery(ss.id, visualQuery, queryBuilderSettings, ml); - - log.debug('translating query:', convertedQuery); - publisher.publishStatusToFrontend('Translating'); - - const query = query2Cypher(convertedQuery); - if (query == null) { - log.error('Error translating query:', convertedQuery); - publisher.publishErrorToFrontend('Error translating query'); - return; - } - - log.info('Translated query:', query); - publisher.publishTranslationResultToFrontend(query); - - for (let i = 0; i < ss.dbConnections.length; i++) { - queryService(ss.dbConnections[i], query).then((result) => { - publisher.publishResultToFrontend(result); - log.debug('Query result!'); - log.info(`Query executed in ${formatTimeDifference(Date.now() - startTime)}`); - - if (convertedQuery.machineLearning && convertedQuery.machineLearning.length > 0) { - for (let i = 0; i < convertedQuery.machineLearning.length; i++) { - try { - publisher.publishMachineLearningRequest(result, convertedQuery.machineLearning[i], headers); - log.debug('Published machine learning request', convertedQuery.machineLearning[i]); - } catch (error) { - log.error('Error publishing machine learning request', error); - publisher.publishErrorToFrontend('Error publishing machine learning request'); - } - } - } - - }).catch((error) => { - log.error('Error querying database', error); - publisher.publishErrorToFrontend('Error querying database'); - }); - } - - }); -} + const queryBuilderSettings = ss.queries[0].settings; + const ml = message.ml; + const convertedQuery = Query2BackendQuery(ss.id, visualQuery, queryBuilderSettings, ml); + + log.debug('translating query:', convertedQuery); + publisher.publishStatusToFrontend('Translating'); + + const query = query2Cypher(convertedQuery); + if (query == null) { + log.error('Error translating query:', convertedQuery); + publisher.publishErrorToFrontend('Error translating query'); + return; + } + + log.info('Translated query:', query); + publisher.publishTranslationResultToFrontend(query); + + for (let i = 0; i < ss.dbConnections.length; i++) { + queryService(ss.dbConnections[i], query) + .then(result => { + publisher.publishResultToFrontend(result); + log.debug('Query result!'); + log.info(`Query executed in ${formatTimeDifference(Date.now() - startTime)}`); + + if (convertedQuery.machineLearning && convertedQuery.machineLearning.length > 0) { + for (let i = 0; i < convertedQuery.machineLearning.length; i++) { + try { + publisher.publishMachineLearningRequest(result, convertedQuery.machineLearning[i], headers); + log.debug('Published machine learning request', convertedQuery.machineLearning[i]); + } catch (error) { + log.error('Error publishing machine learning request', error); + publisher.publishErrorToFrontend('Error publishing machine learning request'); + } + } + } + }) + .catch(error => { + log.error('Error querying database', error); + publisher.publishErrorToFrontend('Error querying database'); + }); + } + }); +}; diff --git a/src/readers/statCheck.ts b/src/readers/statCheck.ts index c130de6..96ea655 100644 --- a/src/readers/statCheck.ts +++ b/src/readers/statCheck.ts @@ -1,69 +1,67 @@ -import type { GraphQueryResultMetaFromBackend, InsightModel } from "ts-common"; -import { log } from "../logger"; +import type { GraphQueryResultMetaFromBackend, InsightModel } from 'ts-common'; +import { log } from '../logger'; function processAlarmStats(alarmStat: InsightModel, resultQuery: GraphQueryResultMetaFromBackend): boolean { - for (const condition of alarmStat.conditionsCheck) { - const ssInsightNode = condition.nodeLabel; - const ssInsightStatistic = condition.statistic; - const ssInsightOperator = condition.operator; - const ssInsightValue = condition.value; - // TODO: eventually we need to support multiple conditions with and/or + for (const condition of alarmStat.conditionsCheck) { + const ssInsightNode = condition.nodeLabel; + const ssInsightStatistic = condition.statistic; + const ssInsightOperator = condition.operator; + const ssInsightValue = condition.value; + // TODO: eventually we need to support multiple conditions with and/or - log.debug(`Checking condition: ${ssInsightNode} ${ssInsightStatistic} ${ssInsightOperator} ${ssInsightValue}`); + log.debug(`Checking condition: ${ssInsightNode} ${ssInsightStatistic} ${ssInsightOperator} ${ssInsightValue}`); - if (resultQuery.metaData.nodes.labels.includes(ssInsightNode)) { - const nodeCount = resultQuery.metaData.nodes.count; - let conditionMet = false; - switch (ssInsightOperator) { - case ">": - conditionMet = nodeCount > ssInsightValue; - break; - case ">=": - conditionMet = nodeCount >= ssInsightValue; - break; - case "==": - conditionMet = nodeCount === ssInsightValue; - break; - case "!=": - conditionMet = nodeCount !== ssInsightValue; - break; - case "<": - conditionMet = nodeCount < ssInsightValue; - break; - case "<=": - conditionMet = nodeCount <= ssInsightValue; - break; - default: - log.error(`Unsupported operator: ${ssInsightOperator}`); - throw new Error(`Unsupported operator: ${ssInsightOperator}`); - } + if (resultQuery.metaData.nodes.labels.includes(ssInsightNode)) { + const nodeCount = resultQuery.metaData.nodes.count; + let conditionMet = false; + switch (ssInsightOperator) { + case '>': + conditionMet = nodeCount > ssInsightValue; + break; + case '>=': + conditionMet = nodeCount >= ssInsightValue; + break; + case '==': + conditionMet = nodeCount === ssInsightValue; + break; + case '!=': + conditionMet = nodeCount !== ssInsightValue; + break; + case '<': + conditionMet = nodeCount < ssInsightValue; + break; + case '<=': + conditionMet = nodeCount <= ssInsightValue; + break; + default: + log.error(`Unsupported operator: ${ssInsightOperator}`); + throw new Error(`Unsupported operator: ${ssInsightOperator}`); + } - if (conditionMet) { - log.info("Condition met "); - return true; - } - - else { - log.info("Condition not met"); - return false; - } - } - - break; // TODO: only one condition is supported for now + if (conditionMet) { + log.info('Condition met '); + return true; + } else { + log.info('Condition not met'); + return false; + } } - return false; + break; // TODO: only one condition is supported for now + } + + return false; } export const statCheck = (insight: InsightModel, graphResult: GraphQueryResultMetaFromBackend): InsightModel => { - if (insight.type === "report") { - // report is not an alarm, just log the stats - log.debug("report!", insight); - return insight; - } + if (insight.type === 'report') { + // report is not an alarm, just log the stats + log.debug('report!', insight); + return insight; + } - insight.status ||= processAlarmStats(insight, graphResult); //statusResult; - log.debug("Alarm status", insight.status); + insight.status ||= processAlarmStats(insight, graphResult); //statusResult; + log.debug('Alarm status', insight.status); - return insight; + return insight; }; diff --git a/src/utils/cypher/converter/export.ts b/src/utils/cypher/converter/export.ts index 6c37069..fa39213 100644 --- a/src/utils/cypher/converter/export.ts +++ b/src/utils/cypher/converter/export.ts @@ -1,24 +1,23 @@ -import type { ExportNodeStruct, NodeStruct } from "ts-common"; - +import type { ExportNodeStruct, NodeStruct } from 'ts-common'; export function createExportCypher(JSONQuery: ExportNodeStruct, id: string): string { - return `${id}.${JSONQuery.attribute} as ${JSONQuery.id}`; + return `${id}.${JSONQuery.attribute} as ${JSONQuery.id}`; } export function extractExportCypher(JSONQuery: NodeStruct): string[] { - const exports: string[] = []; - if (JSONQuery.export) { - for (const exportItem of JSONQuery.export) { - if (exportItem && JSONQuery.id) { - const exportCypher = createExportCypher(exportItem, JSONQuery.id); - exports.push(exportCypher); - } - } + const exports: string[] = []; + if (JSONQuery.export) { + for (const exportItem of JSONQuery.export) { + if (exportItem && JSONQuery.id) { + const exportCypher = createExportCypher(exportItem, JSONQuery.id); + exports.push(exportCypher); + } } + } - if (JSONQuery.relation && JSONQuery.relation.node) { - const relationCypher = extractExportCypher(JSONQuery.relation.node); - exports.push(...relationCypher); - } - return exports; + if (JSONQuery.relation && JSONQuery.relation.node) { + const relationCypher = extractExportCypher(JSONQuery.relation.node); + exports.push(...relationCypher); + } + return exports; } diff --git a/src/utils/cypher/converter/filter.ts b/src/utils/cypher/converter/filter.ts index dbf1225..a4bb370 100644 --- a/src/utils/cypher/converter/filter.ts +++ b/src/utils/cypher/converter/filter.ts @@ -1,49 +1,49 @@ -import type { FilterStruct } from "ts-common"; +import type { FilterStruct } from 'ts-common'; export function createFilterCypher(JSONQuery: FilterStruct, ID: string): string { - let cypher = `${ID}.${JSONQuery.attribute}`; - switch (JSONQuery.operation) { - case "EQ": - cypher += ` = ${JSONQuery.value}`; - break; - case "NEQ": - cypher += ` <> ${JSONQuery.value}`; - break; - case "GT": - cypher += ` > ${JSONQuery.value}`; - break; - case "GTE": - cypher += ` >= ${JSONQuery.value}`; - break; - case "LT": - cypher += ` < ${JSONQuery.value}`; - break; - case "LTE": - cypher += ` <= ${JSONQuery.value}`; - break; - case "CONTAINS": - cypher += ` CONTAINS ${JSONQuery.value}`; - break; - case "STARTS_WITH": - cypher += ` STARTS WITH ${JSONQuery.value}`; - break; - case "ENDS_WITH": - cypher += ` ENDS WITH ${JSONQuery.value}`; - break; - case "IN": - cypher += ` IN ${JSONQuery.value}`; - break; - case "NOT_IN": - cypher += ` NOT IN ${JSONQuery.value}`; - break; - case "IS_NULL": - cypher += ` IS NULL`; - break; - case "IS_NOT_NULL": - cypher += ` IS NOT NULL`; - break; - default: - throw new Error("operator not supported"); - } - return cypher; -} \ No newline at end of file + let cypher = `${ID}.${JSONQuery.attribute}`; + switch (JSONQuery.operation) { + case 'EQ': + cypher += ` = ${JSONQuery.value}`; + break; + case 'NEQ': + cypher += ` <> ${JSONQuery.value}`; + break; + case 'GT': + cypher += ` > ${JSONQuery.value}`; + break; + case 'GTE': + cypher += ` >= ${JSONQuery.value}`; + break; + case 'LT': + cypher += ` < ${JSONQuery.value}`; + break; + case 'LTE': + cypher += ` <= ${JSONQuery.value}`; + break; + case 'CONTAINS': + cypher += ` CONTAINS ${JSONQuery.value}`; + break; + case 'STARTS_WITH': + cypher += ` STARTS WITH ${JSONQuery.value}`; + break; + case 'ENDS_WITH': + cypher += ` ENDS WITH ${JSONQuery.value}`; + break; + case 'IN': + cypher += ` IN ${JSONQuery.value}`; + break; + case 'NOT_IN': + cypher += ` NOT IN ${JSONQuery.value}`; + break; + case 'IS_NULL': + cypher += ` IS NULL`; + break; + case 'IS_NOT_NULL': + cypher += ` IS NOT NULL`; + break; + default: + throw new Error('operator not supported'); + } + return cypher; +} diff --git a/src/utils/cypher/converter/index.ts b/src/utils/cypher/converter/index.ts index b2283fc..2411846 100644 --- a/src/utils/cypher/converter/index.ts +++ b/src/utils/cypher/converter/index.ts @@ -1 +1 @@ -export { query2Cypher } from './queryConverter'; \ No newline at end of file +export { query2Cypher } from './queryConverter'; diff --git a/src/utils/cypher/converter/logic.ts b/src/utils/cypher/converter/logic.ts index ac4ddce..0fec27a 100644 --- a/src/utils/cypher/converter/logic.ts +++ b/src/utils/cypher/converter/logic.ts @@ -1,84 +1,86 @@ -import type { AnyStatement } from "ts-common/src/model/query/logic/general"; -import type { QueryCacheData } from "./model"; +import type { AnyStatement } from 'ts-common/src/model/query/logic/general'; +import type { QueryCacheData } from './model'; export function createWhereLogic(op: string, left: string, whereLogic: string, cacheData: QueryCacheData): [string, string] { - const newWhereLogic = `${left.replace(".", "_")}_${op}`; - if (whereLogic) { - whereLogic += ", "; - } - const remainingNodes: string[] = []; + const newWhereLogic = `${left.replace('.', '_')}_${op}`; + if (whereLogic) { + whereLogic += ', '; + } + const remainingNodes: string[] = []; - for (const entity of cacheData.entities) { - if (entity.id !== left) { - remainingNodes.push(entity.id); - } + for (const entity of cacheData.entities) { + if (entity.id !== left) { + remainingNodes.push(entity.id); } + } - // TODO: Relation temporarily ignored due to unnecessary added complexity in the query - // for (const relation of cacheData.relations) { - // if (relation.id !== left) { - // remainingNodes.push(relation.id); - // } - // } + // TODO: Relation temporarily ignored due to unnecessary added complexity in the query + // for (const relation of cacheData.relations) { + // if (relation.id !== left) { + // remainingNodes.push(relation.id); + // } + // } - const remainingNodesStr = remainingNodes.length > 0 ? `, ${remainingNodes.join(", ")}` : ""; - const newWithLogic = `${whereLogic}${op}(${left}) AS ${newWhereLogic}${remainingNodesStr}`; + const remainingNodesStr = remainingNodes.length > 0 ? `, ${remainingNodes.join(', ')}` : ''; + const newWithLogic = `${whereLogic}${op}(${left}) AS ${newWhereLogic}${remainingNodesStr}`; - return [newWhereLogic, newWithLogic]; + return [newWhereLogic, newWithLogic]; } export function extractLogicCypher(logicQuery: AnyStatement, cacheData: QueryCacheData): [string, string] { - switch (typeof logicQuery) { - case "object": - if (Array.isArray(logicQuery)) { - let op = logicQuery[0].replace("_", "").toLowerCase(); - let [left, whereLogic] = extractLogicCypher(logicQuery[1], cacheData); + switch (typeof logicQuery) { + case 'object': + if (Array.isArray(logicQuery)) { + let op = logicQuery[0].replace('_', '').toLowerCase(); + const [left, whereLogic] = extractLogicCypher(logicQuery[1], cacheData); + let whereLogicMutable = whereLogic; - switch (op) { - case "!=": - op = "<>"; - break; - case "==": - op = "="; - break; - case "like": - op = "=~"; - break; - case "isempty": - return [`(${left} IS NULL OR ${left} = "")`, whereLogic]; - case "lower": - return [`toLower(${left})`, whereLogic]; - case "upper": - return [`toUpper(${left})`, whereLogic]; - case "avg": - case "count": - case "max": - case "min": - case "sum": - return createWhereLogic(op, left, whereLogic, cacheData); - } - if (logicQuery.length > 2 && logicQuery[2]) { - let [right, whereLogicRight] = extractLogicCypher(logicQuery[2], cacheData); + switch (op) { + case '!=': + op = '<>'; + break; + case '==': + op = '='; + break; + case 'like': + op = '=~'; + break; + case 'isempty': + return [`(${left} IS NULL OR ${left} = "")`, whereLogicMutable]; + case 'lower': + return [`toLower(${left})`, whereLogicMutable]; + case 'upper': + return [`toUpper(${left})`, whereLogicMutable]; + case 'avg': + case 'count': + case 'max': + case 'min': + case 'sum': + return createWhereLogic(op, left, whereLogicMutable, cacheData); + } + if (logicQuery.length > 2 && logicQuery[2]) { + const [right, whereLogicRight] = extractLogicCypher(logicQuery[2], cacheData); + let rightMutable = right; - if (whereLogicRight) { - if (whereLogic) { - whereLogic += ", "; - } - whereLogic += whereLogicRight; - } - if (op === "=~") { - right = `(".*" + ${right} + ".*")`; - } - return [`(${left} ${op} ${right})`, whereLogic]; - } - return [`(${op} ${left})`, whereLogic]; + if (whereLogicRight) { + if (whereLogicMutable) { + whereLogicMutable += ', '; } - return [logicQuery, ""]; - case "string": - return [logicQuery.replace("@", ""), ""]; - case "number": - return [logicQuery.toString(), ""]; - default: - return [logicQuery as any, ""]; - } + whereLogicMutable += whereLogicRight; + } + if (op === '=~') { + rightMutable = `(".*" + ${rightMutable} + ".*")`; + } + return [`(${left} ${op} ${rightMutable})`, whereLogicMutable]; + } + return [`(${op} ${left})`, whereLogicMutable]; + } + return [logicQuery, '']; + case 'string': + return [logicQuery.replace('@', ''), '']; + case 'number': + return [logicQuery.toString(), '']; + default: + return [logicQuery as any, '']; + } } diff --git a/src/utils/cypher/converter/model.ts b/src/utils/cypher/converter/model.ts index 492fde7..e4460ce 100644 --- a/src/utils/cypher/converter/model.ts +++ b/src/utils/cypher/converter/model.ts @@ -1,15 +1,14 @@ - export interface RelationCacheData { - id: string; - queryId: string; + id: string; + queryId: string; } export interface EntityCacheData { - id: string; - queryId: string; + id: string; + queryId: string; } export interface QueryCacheData { - entities: EntityCacheData[]; - relations: RelationCacheData[]; + entities: EntityCacheData[]; + relations: RelationCacheData[]; } diff --git a/src/utils/cypher/converter/node.ts b/src/utils/cypher/converter/node.ts index cb6b0bf..0837633 100644 --- a/src/utils/cypher/converter/node.ts +++ b/src/utils/cypher/converter/node.ts @@ -1,39 +1,39 @@ -import type { NodeStruct } from "ts-common"; -import type { QueryCacheData } from "./model"; -import { getRelationCypher } from "./relation"; +import type { NodeStruct } from 'ts-common'; +import type { QueryCacheData } from './model'; +import { getRelationCypher } from './relation'; export function getNodeCypher(JSONQuery: NodeStruct): [string, QueryCacheData] { - let label = ""; - if (JSONQuery.label) { - label = `:${JSONQuery.label}`; - } - let id = ""; - if (JSONQuery.id) { - id = JSONQuery.id; - } + let label = ''; + if (JSONQuery.label) { + label = `:${JSONQuery.label}`; + } + let id = ''; + if (JSONQuery.id) { + id = JSONQuery.id; + } - const cacheData: QueryCacheData = { entities: [], relations: [] }; + const cacheData: QueryCacheData = { entities: [], relations: [] }; - if (id) { - cacheData.entities.push({ id, queryId: "" }); - } + if (id) { + cacheData.entities.push({ id, queryId: '' }); + } - let cypher = `(${id}${label})`; + let cypher = `(${id}${label})`; - if (JSONQuery.relation) { - const [relationCypher, subCache] = getRelationCypher(JSONQuery.relation); + if (JSONQuery.relation) { + const [relationCypher, subCache] = getRelationCypher(JSONQuery.relation); - if (relationCypher) { - cacheData.entities.push(...subCache.entities); - cacheData.relations.push(...subCache.relations); - if (JSONQuery.relation.direction === "FROM") { - cypher += `<-${relationCypher}`; - } else if (JSONQuery.relation.direction === "TO") { - cypher += `-${relationCypher}`; - } else { - cypher += `-${relationCypher}`; - } - } + if (relationCypher) { + cacheData.entities.push(...subCache.entities); + cacheData.relations.push(...subCache.relations); + if (JSONQuery.relation.direction === 'FROM') { + cypher += `<-${relationCypher}`; + } else if (JSONQuery.relation.direction === 'TO') { + cypher += `-${relationCypher}`; + } else { + cypher += `-${relationCypher}`; + } } - return [cypher, cacheData]; -} \ No newline at end of file + } + return [cypher, cacheData]; +} diff --git a/src/utils/cypher/converter/queryConverter.test.ts b/src/utils/cypher/converter/queryConverter.test.ts index becdd4c..bb3cd7e 100644 --- a/src/utils/cypher/converter/queryConverter.test.ts +++ b/src/utils/cypher/converter/queryConverter.test.ts @@ -1,318 +1,318 @@ -import { query2Cypher } from "./queryConverter"; -import type { BackendQueryFormat } from "ts-common"; -import { expect, test, describe, it } from "bun:test"; +import { query2Cypher } from './queryConverter'; +import type { BackendQueryFormat } from 'ts-common'; +import { expect, test, describe, it } from 'bun:test'; function fixCypherSpaces(cypher?: string | null): string { - if (!cypher) { - return ""; - } - - let trimmedCypher = cypher.replace(/\n/g, " "); - trimmedCypher = trimmedCypher.replaceAll(/ {2,50}/g, " "); - trimmedCypher = trimmedCypher.replace(/\t+/g, ""); - return trimmedCypher.trim(); + if (!cypher) { + return ''; + } + + let trimmedCypher = cypher.replace(/\n/g, ' '); + trimmedCypher = trimmedCypher.replaceAll(/ {2,50}/g, ' '); + trimmedCypher = trimmedCypher.replace(/\t+/g, ''); + return trimmedCypher.trim(); } -describe("query2Cypher", () => { - it("should return correctly on a simple query with multiple paths", () => { - const query: BackendQueryFormat = { - saveStateID: "test", - return: ["*"], - query: [ - { - id: "path1", - node: { - label: "Person", - id: "p1", - relation: { - label: "DIRECTED", - direction: "TO", - depth: { min: 1, max: 1 }, - node: { - label: "Movie", - id: "m1" - } - } - } - }, - { - id: "path2", - node: { - label: "Person", - id: "p1", - relation: { - label: "IN_GENRE", - direction: "TO", - depth: { min: 1, max: 1 }, - node: { - label: "Genre", - id: "g1" - } - } - } - } - ], - limit: 5000 - }; - - const cypher = query2Cypher(query); - const expectedCypher = `MATCH path1 = ((p1:Person)-[:DIRECTED*1..1]->(m1:Movie)) +describe('query2Cypher', () => { + it('should return correctly on a simple query with multiple paths', () => { + const query: BackendQueryFormat = { + saveStateID: 'test', + return: ['*'], + query: [ + { + id: 'path1', + node: { + label: 'Person', + id: 'p1', + relation: { + label: 'DIRECTED', + direction: 'TO', + depth: { min: 1, max: 1 }, + node: { + label: 'Movie', + id: 'm1', + }, + }, + }, + }, + { + id: 'path2', + node: { + label: 'Person', + id: 'p1', + relation: { + label: 'IN_GENRE', + direction: 'TO', + depth: { min: 1, max: 1 }, + node: { + label: 'Genre', + id: 'g1', + }, + }, + }, + }, + ], + limit: 5000, + }; + + const cypher = query2Cypher(query); + const expectedCypher = `MATCH path1 = ((p1:Person)-[:DIRECTED*1..1]->(m1:Movie)) MATCH path2 = ((p1:Person)-[:IN_GENRE*1..1]->(g1:Genre)) RETURN * LIMIT 5000`; - expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); - }); - - it("should return correctly on a complex query with logic", () => { - const query: BackendQueryFormat = { - saveStateID: "test", - return: ["*"], - logic: ["!=", "@p1.name", "\"Raymond Campbell\""], - query: [ - { - id: "path1", - node: { - label: "Person", - id: "p1", - relation: { - label: "DIRECTED", - direction: "TO", - depth: { min: 1, max: 1 }, - node: { - label: "Movie", - id: "m1" - } - } - } - }, - { - id: "path2", - node: { - label: "Person", - id: "p1", - relation: { - label: "IN_GENRE", - direction: "TO", - depth: { min: 1, max: 1 }, - node: { - label: "Genre", - id: "g1" - } - } - } - } - ], - limit: 5000 - }; - - const cypher = query2Cypher(query); - const expectedCypher = `MATCH path1 = ((p1:Person)-[:DIRECTED*1..1]->(m1:Movie)) + expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); + }); + + it('should return correctly on a complex query with logic', () => { + const query: BackendQueryFormat = { + saveStateID: 'test', + return: ['*'], + logic: ['!=', '@p1.name', '"Raymond Campbell"'], + query: [ + { + id: 'path1', + node: { + label: 'Person', + id: 'p1', + relation: { + label: 'DIRECTED', + direction: 'TO', + depth: { min: 1, max: 1 }, + node: { + label: 'Movie', + id: 'm1', + }, + }, + }, + }, + { + id: 'path2', + node: { + label: 'Person', + id: 'p1', + relation: { + label: 'IN_GENRE', + direction: 'TO', + depth: { min: 1, max: 1 }, + node: { + label: 'Genre', + id: 'g1', + }, + }, + }, + }, + ], + limit: 5000, + }; + + const cypher = query2Cypher(query); + const expectedCypher = `MATCH path1 = ((p1:Person)-[:DIRECTED*1..1]->(m1:Movie)) MATCH path2 = ((p1:Person)-[:IN_GENRE*1..1]->(g1:Genre)) WHERE (p1.name <> "Raymond Campbell") RETURN * LIMIT 5000`; - expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); - }); - - it("should return correctly on a query with group by logic", () => { - const query: BackendQueryFormat = { - saveStateID: "test", - limit: 5000, - logic: ["And", ["<", "@movie.imdbRating", 7.5], ["==", "p2.age", "p1.age"]], - query: [ - { - id: "path1", - node: { - label: "Person", - id: "p1", - relation: { - id: "acted", - label: "ACTED_IN", - depth: { min: 1, max: 1 }, - direction: "TO", - node: { - label: "Movie", - id: "movie" - } - } - } - }, - { - id: "path2", - node: { - label: "Person", - id: "p2" - } - } - ], - return: ["@path2"] - }; - - const cypher = query2Cypher(query); - const expectedCypher = `MATCH path1 = ((p1:Person)-[acted:ACTED_IN*1..1]->(movie:Movie)) + expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); + }); + + it('should return correctly on a query with group by logic', () => { + const query: BackendQueryFormat = { + saveStateID: 'test', + limit: 5000, + logic: ['And', ['<', '@movie.imdbRating', 7.5], ['==', 'p2.age', 'p1.age']], + query: [ + { + id: 'path1', + node: { + label: 'Person', + id: 'p1', + relation: { + id: 'acted', + label: 'ACTED_IN', + depth: { min: 1, max: 1 }, + direction: 'TO', + node: { + label: 'Movie', + id: 'movie', + }, + }, + }, + }, + { + id: 'path2', + node: { + label: 'Person', + id: 'p2', + }, + }, + ], + return: ['@path2'], + }; + + const cypher = query2Cypher(query); + const expectedCypher = `MATCH path1 = ((p1:Person)-[acted:ACTED_IN*1..1]->(movie:Movie)) MATCH path2 = ((p2:Person)) WHERE ((movie.imdbRating < 7.5) and (p2.age = p1.age)) RETURN path2 LIMIT 5000`; - expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); - }); - - it("should return correctly on a query with no label", () => { - const query: BackendQueryFormat = { - saveStateID: "test", - limit: 5000, - logic: ["<", ["-", "@movie.year", "p1.year"], 10], - query: [ - { - id: "path1", - node: { - id: "p1", - filter: [], - relation: { - id: "acted", - depth: { min: 1, max: 1 }, - direction: "TO", - node: { - label: "Movie", - id: "movie" - } - } - } - } - ], - return: ["*"] - }; - - const cypher = query2Cypher(query); - const expectedCypher = `MATCH path1 = ((p1)-[acted*1..1]->(movie:Movie)) + expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); + }); + + it('should return correctly on a query with no label', () => { + const query: BackendQueryFormat = { + saveStateID: 'test', + limit: 5000, + logic: ['<', ['-', '@movie.year', 'p1.year'], 10], + query: [ + { + id: 'path1', + node: { + id: 'p1', + filter: [], + relation: { + id: 'acted', + depth: { min: 1, max: 1 }, + direction: 'TO', + node: { + label: 'Movie', + id: 'movie', + }, + }, + }, + }, + ], + return: ['*'], + }; + + const cypher = query2Cypher(query); + const expectedCypher = `MATCH path1 = ((p1)-[acted*1..1]->(movie:Movie)) WHERE ((movie.year - p1.year) < 10) RETURN * LIMIT 5000`; - expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); - }); - - it("should return correctly on a query with no depth", () => { - const query: BackendQueryFormat = { - saveStateID: "test", - limit: 5000, - logic: ["And", ["<", "@movie.imdbRating", 7.5], ["==", "p2.age", "p1.age"]], - query: [ - { - id: "path1", - node: { - id: "p1", - relation: { - id: "acted", - direction: "TO", - node: { - label: "Movie", - id: "movie" - } - } - } - }, - { - id: "path2", - node: { - id: "p2" - } - } - ], - return: ["*"] - }; - - const cypher = query2Cypher(query); - const expectedCypher = `MATCH path1 = ((p1)-[acted]->(movie:Movie)) + expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); + }); + + it('should return correctly on a query with no depth', () => { + const query: BackendQueryFormat = { + saveStateID: 'test', + limit: 5000, + logic: ['And', ['<', '@movie.imdbRating', 7.5], ['==', 'p2.age', 'p1.age']], + query: [ + { + id: 'path1', + node: { + id: 'p1', + relation: { + id: 'acted', + direction: 'TO', + node: { + label: 'Movie', + id: 'movie', + }, + }, + }, + }, + { + id: 'path2', + node: { + id: 'p2', + }, + }, + ], + return: ['*'], + }; + + const cypher = query2Cypher(query); + const expectedCypher = `MATCH path1 = ((p1)-[acted]->(movie:Movie)) MATCH path2 = ((p2)) WHERE ((movie.imdbRating < 7.5) and (p2.age = p1.age)) RETURN * LIMIT 5000`; - expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); - }); - - it("should return correctly on a query with average calculation", () => { - const query: BackendQueryFormat = { - saveStateID: "test", - limit: 5000, - logic: ["<", "@p1.age", ["Avg", "@p1.age"]], - query: [ - { - id: "path1", - node: { - label: "Person", - id: "p1", - relation: { - id: "acted", - label: "ACTED_IN", - depth: { min: 1, max: 1 }, - direction: "TO", - node: { - label: "Movie", - id: "movie" - } - } - } - } - ], - return: ["*"] - }; - - const cypher = query2Cypher(query); - const expectedCypher = `MATCH path1 = ((p1:Person)-[acted:ACTED_IN*1..1]->(movie:Movie)) + expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); + }); + + it('should return correctly on a query with average calculation', () => { + const query: BackendQueryFormat = { + saveStateID: 'test', + limit: 5000, + logic: ['<', '@p1.age', ['Avg', '@p1.age']], + query: [ + { + id: 'path1', + node: { + label: 'Person', + id: 'p1', + relation: { + id: 'acted', + label: 'ACTED_IN', + depth: { min: 1, max: 1 }, + direction: 'TO', + node: { + label: 'Movie', + id: 'movie', + }, + }, + }, + }, + ], + return: ['*'], + }; + + const cypher = query2Cypher(query); + const expectedCypher = `MATCH path1 = ((p1:Person)-[acted:ACTED_IN*1..1]->(movie:Movie)) WITH avg(p1.age) AS p1_age_avg, p1, movie MATCH path1 = ((p1:Person)-[acted:ACTED_IN*1..1]->(movie:Movie)) WHERE (p1.age < p1_age_avg) RETURN * LIMIT 5000`; - expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); - }); - - it("should return correctly on a query with average calculation and multiple paths", () => { - const query: BackendQueryFormat = { - saveStateID: "test", - limit: 5000, - logic: ["<", "@p1.age", ["Avg", "@p1.age"]], - query: [ - { - id: "path1", - node: { - label: "Person", - id: "p1", - relation: { - id: "acted", - label: "ACTED_IN", - depth: { min: 1, max: 1 }, - direction: "TO", - node: { - label: "Movie", - id: "movie" - } - } - } - }, - { - id: "path2", - node: { - label: "Person", - id: "p2", - relation: { - id: "acted", - label: "ACTED_IN", - depth: { min: 1, max: 1 }, - direction: "TO", - node: { - label: "Movie", - id: "movie" - } - } - } - } - ], - return: ["*"] - }; - - const cypher = query2Cypher(query); - const expectedCypher = `MATCH path1 = ((p1:Person)-[acted:ACTED_IN*1..1]->(movie:Movie)) + expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); + }); + + it('should return correctly on a query with average calculation and multiple paths', () => { + const query: BackendQueryFormat = { + saveStateID: 'test', + limit: 5000, + logic: ['<', '@p1.age', ['Avg', '@p1.age']], + query: [ + { + id: 'path1', + node: { + label: 'Person', + id: 'p1', + relation: { + id: 'acted', + label: 'ACTED_IN', + depth: { min: 1, max: 1 }, + direction: 'TO', + node: { + label: 'Movie', + id: 'movie', + }, + }, + }, + }, + { + id: 'path2', + node: { + label: 'Person', + id: 'p2', + relation: { + id: 'acted', + label: 'ACTED_IN', + depth: { min: 1, max: 1 }, + direction: 'TO', + node: { + label: 'Movie', + id: 'movie', + }, + }, + }, + }, + ], + return: ['*'], + }; + + const cypher = query2Cypher(query); + const expectedCypher = `MATCH path1 = ((p1:Person)-[acted:ACTED_IN*1..1]->(movie:Movie)) MATCH path2 = ((p2:Person)-[acted:ACTED_IN*1..1]->(movie:Movie)) WITH avg(p1.age) AS p1_age_avg, p2, movie MATCH path1 = ((p1:Person)-[acted:ACTED_IN*1..1]->(movie:Movie)) @@ -320,216 +320,216 @@ describe("query2Cypher", () => { WHERE (p1.age < p1_age_avg) RETURN * LIMIT 5000`; - expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); - }); - - it("should return correctly on a single entity query with lower like logic", () => { - const query: BackendQueryFormat = { - saveStateID: "test", - limit: 5000, - logic: ["Like", ["Lower", "@p1.name"], "\"john\""], - query: [ - { - id: "path1", - node: { - label: "Person", - id: "p1" - } - } - ], - return: ["*"] - }; - - const cypher = query2Cypher(query); - const expectedCypher = `MATCH path1 = ((p1:Person)) + expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); + }); + + it('should return correctly on a single entity query with lower like logic', () => { + const query: BackendQueryFormat = { + saveStateID: 'test', + limit: 5000, + logic: ['Like', ['Lower', '@p1.name'], '"john"'], + query: [ + { + id: 'path1', + node: { + label: 'Person', + id: 'p1', + }, + }, + ], + return: ['*'], + }; + + const cypher = query2Cypher(query); + const expectedCypher = `MATCH path1 = ((p1:Person)) WHERE (toLower(p1.name) =~ (".*" + "john" + ".*")) RETURN * LIMIT 5000`; - expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); - }); - - it("should return correctly on a query with like logic", () => { - const query: BackendQueryFormat = { - saveStateID: "test", - limit: 500, - logic: ["Like", "@id_1691576718400.title", "\"ale\""], - query: [ - { - id: "path_0", - node: { - id: "id_1691576718400", - label: "Employee", - relation: { - id: "id_1691576720177", - label: "REPORTS_TO", - direction: "TO", - node: {} - } - } - } - ], - return: ["*"] - }; - - const cypher = query2Cypher(query); - const expectedCypher = `MATCH path_0 = ((id_1691576718400:Employee)-[id_1691576720177:REPORTS_TO]->()) + expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); + }); + + it('should return correctly on a query with like logic', () => { + const query: BackendQueryFormat = { + saveStateID: 'test', + limit: 500, + logic: ['Like', '@id_1691576718400.title', '"ale"'], + query: [ + { + id: 'path_0', + node: { + id: 'id_1691576718400', + label: 'Employee', + relation: { + id: 'id_1691576720177', + label: 'REPORTS_TO', + direction: 'TO', + node: {}, + }, + }, + }, + ], + return: ['*'], + }; + + const cypher = query2Cypher(query); + const expectedCypher = `MATCH path_0 = ((id_1691576718400:Employee)-[id_1691576720177:REPORTS_TO]->()) WHERE (id_1691576718400.title =~ (".*" + "ale" + ".*")) RETURN * LIMIT 500`; - expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); - }); - - it("should return correctly on a query with both direction relation", () => { - const query: BackendQueryFormat = { - saveStateID: "test", - limit: 500, - logic: ["Like", "@id_1691576718400.title", "\"ale\""], - query: [ - { - id: "path_0", - node: { - id: "id_1691576718400", - label: "Employee", - relation: { - id: "id_1691576720177", - label: "REPORTS_TO", - direction: "BOTH", - node: {} - } - } - } - ], - return: ["*"] - }; - - const cypher = query2Cypher(query); - const expectedCypher = `MATCH path_0 = ((id_1691576718400:Employee)-[id_1691576720177:REPORTS_TO]-()) + expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); + }); + + it('should return correctly on a query with both direction relation', () => { + const query: BackendQueryFormat = { + saveStateID: 'test', + limit: 500, + logic: ['Like', '@id_1691576718400.title', '"ale"'], + query: [ + { + id: 'path_0', + node: { + id: 'id_1691576718400', + label: 'Employee', + relation: { + id: 'id_1691576720177', + label: 'REPORTS_TO', + direction: 'BOTH', + node: {}, + }, + }, + }, + ], + return: ['*'], + }; + + const cypher = query2Cypher(query); + const expectedCypher = `MATCH path_0 = ((id_1691576718400:Employee)-[id_1691576720177:REPORTS_TO]-()) WHERE (id_1691576718400.title =~ (".*" + "ale" + ".*")) RETURN * LIMIT 500`; - expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); - }); - - it("should return correctly on a query with relation logic", () => { - const query: BackendQueryFormat = { - saveStateID: "test", - limit: 500, - logic: ["<", "@id_1698231933579.unitPrice", "10"], - query: [ - { - id: "path_0", - node: { - relation: { - id: "id_1698231933579", - label: "CONTAINS", - depth: { min: 0, max: 1 }, - direction: "TO", - node: {} - } - } - } - ], - return: ["*"] - }; - - const cypher = query2Cypher(query); - const expectedCypher = `MATCH path_0 = (()-[id_1698231933579:CONTAINS*0..1]->()) + expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); + }); + + it('should return correctly on a query with relation logic', () => { + const query: BackendQueryFormat = { + saveStateID: 'test', + limit: 500, + logic: ['<', '@id_1698231933579.unitPrice', '10'], + query: [ + { + id: 'path_0', + node: { + relation: { + id: 'id_1698231933579', + label: 'CONTAINS', + depth: { min: 0, max: 1 }, + direction: 'TO', + node: {}, + }, + }, + }, + ], + return: ['*'], + }; + + const cypher = query2Cypher(query); + const expectedCypher = `MATCH path_0 = (()-[id_1698231933579:CONTAINS*0..1]->()) WHERE ALL(path_0_rel_id_1698231933579 in id_1698231933579 WHERE (path_0_rel_id_1698231933579.unitPrice < 10)) RETURN * LIMIT 500`; - expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); - }); - - it("should return correctly on a query with count logic", () => { - const query: BackendQueryFormat = { - saveStateID: "test", - return: ["*"], - logic: [">", ["Count", "@p1"], "1"], - query: [ - { - id: "path1", - node: { - label: "Person", - id: "p1", - relation: { - label: "DIRECTED", - direction: "TO", - depth: { min: 1, max: 1 }, - node: { - label: "Movie", - id: "m1" - } - } - } - } - ], - limit: 5000 - }; - - const cypher = query2Cypher(query); - const expectedCypher = `MATCH path1 = ((p1:Person)-[:DIRECTED*1..1]->(m1:Movie)) + expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); + }); + + it('should return correctly on a query with count logic', () => { + const query: BackendQueryFormat = { + saveStateID: 'test', + return: ['*'], + logic: ['>', ['Count', '@p1'], '1'], + query: [ + { + id: 'path1', + node: { + label: 'Person', + id: 'p1', + relation: { + label: 'DIRECTED', + direction: 'TO', + depth: { min: 1, max: 1 }, + node: { + label: 'Movie', + id: 'm1', + }, + }, + }, + }, + ], + limit: 5000, + }; + + const cypher = query2Cypher(query); + const expectedCypher = `MATCH path1 = ((p1:Person)-[:DIRECTED*1..1]->(m1:Movie)) WITH count(p1) AS p1_count, m1 MATCH path1 = ((p1:Person)-[:DIRECTED*1..1]->(m1:Movie)) WHERE (p1_count > 1) RETURN * LIMIT 5000`; - expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); - }); - - it("should return correctly on a query with empty relation", () => { - const query: BackendQueryFormat = { - saveStateID: "test", - limit: 500, - query: [ - { - "id": "path_0", - "node": { - "label": "Movie", - "id": "id_1730483610947", - "relation": { - "label": "", - "id": "", - "depth": { - "min": 0, - "max": 0 - }, - }, - } - } - ], - return: ["*"] - }; - - const cypher = query2Cypher(query); - const expectedCypher = `MATCH path_0 = ((id_1730483610947:Movie)) + expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); + }); + + it('should return correctly on a query with empty relation', () => { + const query: BackendQueryFormat = { + saveStateID: 'test', + limit: 500, + query: [ + { + id: 'path_0', + node: { + label: 'Movie', + id: 'id_1730483610947', + relation: { + label: '', + id: '', + depth: { + min: 0, + max: 0, + }, + }, + }, + }, + ], + return: ['*'], + }; + + const cypher = query2Cypher(query); + const expectedCypher = `MATCH path_0 = ((id_1730483610947:Movie)) RETURN * LIMIT 500`; - expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); - }); - - it("should return correctly on a query with upper case logic", () => { - const query: BackendQueryFormat = { - saveStateID: "test", - limit: 500, - query: [ - { - id: "path_0", - node: { - id: "id_1731428699410", - label: "Character" - } - } - ], - logic: ["Upper", "@id_1731428699410.name"], - return: ["*"] - }; - - const cypher = query2Cypher(query); - const expectedCypher = `MATCH path_0 = ((id_1731428699410:Character)) + expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); + }); + + it('should return correctly on a query with upper case logic', () => { + const query: BackendQueryFormat = { + saveStateID: 'test', + limit: 500, + query: [ + { + id: 'path_0', + node: { + id: 'id_1731428699410', + label: 'Character', + }, + }, + ], + logic: ['Upper', '@id_1731428699410.name'], + return: ['*'], + }; + + const cypher = query2Cypher(query); + const expectedCypher = `MATCH path_0 = ((id_1731428699410:Character)) WHERE toUpper(id_1731428699410.name) RETURN * LIMIT 500`; - expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); - }); + expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); + }); }); diff --git a/src/utils/cypher/converter/queryConverter.ts b/src/utils/cypher/converter/queryConverter.ts index 5eeacc7..240da9b 100644 --- a/src/utils/cypher/converter/queryConverter.ts +++ b/src/utils/cypher/converter/queryConverter.ts @@ -3,79 +3,81 @@ © Copyright Utrecht University(Department of Information and Computing Sciences) */ -import type { BackendQueryFormat } from "ts-common"; -import { extractLogicCypher } from "./logic"; -import { extractExportCypher } from "./export"; -import type { QueryCacheData } from "./model"; -import { getNodeCypher } from "./node"; - +import type { BackendQueryFormat } from 'ts-common'; +import { extractLogicCypher } from './logic'; +import { extractExportCypher } from './export'; +import type { QueryCacheData } from './model'; +import { getNodeCypher } from './node'; // formQuery uses the hierarchy to create cypher for each part of the query in the right order export function query2Cypher(JSONQuery: BackendQueryFormat): string | null { - let totalQuery = ""; - let matchQuery = ""; - let cacheData: QueryCacheData = { entities: [], relations: [] }; - - // MATCH block - for (const query of JSONQuery.query) { - let match = "MATCH "; - const [nodeCypher, _cacheData] = getNodeCypher(query.node); + let totalQuery = ''; + let matchQuery = ''; + let cacheData: QueryCacheData = { entities: [], relations: [] }; - cacheData = _cacheData; - for (let i = 0; i < cacheData.entities.length; i++) { - cacheData.entities[i].queryId = query.id; - } - for (let i = 0; i < cacheData.relations.length; i++) { - cacheData.relations[i].queryId = query.id; - } + // MATCH block + for (const query of JSONQuery.query) { + let match = 'MATCH '; + const [nodeCypher, _cacheData] = getNodeCypher(query.node); - // Generate cypher query for path - if (query.id) { - match += `${query.id} = (${nodeCypher})`; - } else { - match += nodeCypher; - } + cacheData = _cacheData; + for (let i = 0; i < cacheData.entities.length; i++) { + cacheData.entities[i].queryId = query.id; + } + for (let i = 0; i < cacheData.relations.length; i++) { + cacheData.relations[i].queryId = query.id; + } - // Exports (not used right now) - const exports = extractExportCypher(query.node); + // Generate cypher query for path + if (query.id) { + match += `${query.id} = (${nodeCypher})`; + } else { + match += nodeCypher; + } - if (exports.length > 0 && exports[0]) { - match += " WITH " + exports.join(", "); - } + // Exports (not used right now) + const exports = extractExportCypher(query.node); - matchQuery = match.replace(/@/g, "").replace(/\\"/g, "\"") + "\n"; - totalQuery += matchQuery; + if (exports.length > 0 && exports[0]) { + match += ' WITH ' + exports.join(', '); } - // logic calculation for WHERE block - if (JSONQuery.logic) { - const [_logic, whereLogic] = extractLogicCypher(JSONQuery.logic, cacheData); + matchQuery = match.replace(/@/g, '').replace(/\\"/g, '"') + '\n'; + totalQuery += matchQuery; + } - let logic = _logic as string; + // logic calculation for WHERE block + if (JSONQuery.logic) { + const [_logic, whereLogic] = extractLogicCypher(JSONQuery.logic, cacheData); - for (const relation of cacheData.relations) { - // if we need to do logic on relations, this with is required - if (relation.queryId && logic.includes(relation.id)) { - logic = `ALL(${relation.queryId}_rel_${relation.id} in ${relation.id} WHERE ${logic.replace(relation.id, `${relation.queryId}_rel_${relation.id}`)})`; - } - } - let totalQueryWithLogic = totalQuery; - if (whereLogic) { - totalQueryWithLogic += `WITH ${whereLogic}\n`; - totalQuery = totalQueryWithLogic + totalQuery; - } - totalQuery += `WHERE ${logic}\n`; - } + let logic = _logic as string; - // RETURN block - if (JSONQuery.return.length === 0 || JSONQuery.return[0] === "*") { - totalQuery += "RETURN *"; - } else { - totalQuery += "RETURN " + JSONQuery.return.join(", ").replace(/@/g, ""); + for (const relation of cacheData.relations) { + // if we need to do logic on relations, this with is required + if (relation.queryId && logic.includes(relation.id)) { + logic = `ALL(${relation.queryId}_rel_${relation.id} in ${relation.id} WHERE ${logic.replace( + relation.id, + `${relation.queryId}_rel_${relation.id}`, + )})`; + } } + let totalQueryWithLogic = totalQuery; + if (whereLogic) { + totalQueryWithLogic += `WITH ${whereLogic}\n`; + totalQuery = totalQueryWithLogic + totalQuery; + } + totalQuery += `WHERE ${logic}\n`; + } + + // RETURN block + if (JSONQuery.return.length === 0 || JSONQuery.return[0] === '*') { + totalQuery += 'RETURN *'; + } else { + totalQuery += 'RETURN ' + JSONQuery.return.join(', ').replace(/@/g, ''); + } - // LIMIT block - totalQuery += `\nLIMIT ${JSONQuery.limit}`; + // LIMIT block + totalQuery += `\nLIMIT ${JSONQuery.limit}`; - return totalQuery; + return totalQuery; } diff --git a/src/utils/cypher/converter/relation.ts b/src/utils/cypher/converter/relation.ts index 8a96316..9689a60 100644 --- a/src/utils/cypher/converter/relation.ts +++ b/src/utils/cypher/converter/relation.ts @@ -1,45 +1,45 @@ -import type { RelationStruct } from "ts-common"; -import { getNodeCypher } from "./node"; -import type { QueryCacheData } from "./model"; +import type { RelationStruct } from 'ts-common'; +import { getNodeCypher } from './node'; +import type { QueryCacheData } from './model'; -export const NoNodeError = new Error("relation must have a node"); +export const NoNodeError = new Error('relation must have a node'); export function getRelationCypher(JSONQuery: RelationStruct): [string | undefined, QueryCacheData] { - let label = ""; - if (JSONQuery.label) { - label = `:${JSONQuery.label}`; - } - let id = ""; - if (JSONQuery.id) { - id = JSONQuery.id; - } - let depth = ""; - if (JSONQuery.depth) { - depth = `*${JSONQuery.depth.min}..${JSONQuery.depth.max}`; - } - - const cacheData: QueryCacheData = { entities: [], relations: [] }; - - if (id) { - cacheData.relations.push({ id, queryId: "" }); - } - - let cypher = `[${id}${label}${depth}]`; - - if (!JSONQuery.node) { - return [undefined, cacheData]; - } - - const [nodeCypher, subCache] = getNodeCypher(JSONQuery.node); - - cacheData.entities.push(...subCache.entities); - cacheData.relations.push(...subCache.relations); - if (JSONQuery.direction === "TO") { - cypher += `->${nodeCypher}`; - } else if (JSONQuery.direction === "FROM") { - cypher += `-${nodeCypher}`; - } else { - cypher += `-${nodeCypher}`; - } - return [cypher, cacheData]; -} \ No newline at end of file + let label = ''; + if (JSONQuery.label) { + label = `:${JSONQuery.label}`; + } + let id = ''; + if (JSONQuery.id) { + id = JSONQuery.id; + } + let depth = ''; + if (JSONQuery.depth) { + depth = `*${JSONQuery.depth.min}..${JSONQuery.depth.max}`; + } + + const cacheData: QueryCacheData = { entities: [], relations: [] }; + + if (id) { + cacheData.relations.push({ id, queryId: '' }); + } + + let cypher = `[${id}${label}${depth}]`; + + if (!JSONQuery.node) { + return [undefined, cacheData]; + } + + const [nodeCypher, subCache] = getNodeCypher(JSONQuery.node); + + cacheData.entities.push(...subCache.entities); + cacheData.relations.push(...subCache.relations); + if (JSONQuery.direction === 'TO') { + cypher += `->${nodeCypher}`; + } else if (JSONQuery.direction === 'FROM') { + cypher += `-${nodeCypher}`; + } else { + cypher += `-${nodeCypher}`; + } + return [cypher, cacheData]; +} diff --git a/src/utils/cypher/queryParser.ts b/src/utils/cypher/queryParser.ts index d6c21ea..9b61108 100644 --- a/src/utils/cypher/queryParser.ts +++ b/src/utils/cypher/queryParser.ts @@ -1,150 +1,169 @@ -import { Record, Node, Relationship, Path, PathSegment, isRelationship, isNode, isPath, isPathSegment, Integer, isUnboundRelationship, type RecordShape } from 'neo4j-driver'; +import { + Record, + Node, + Relationship, + Path, + PathSegment, + isRelationship, + isNode, + isPath, + isPathSegment, + Integer, + isUnboundRelationship, + type RecordShape, +} from 'neo4j-driver'; import { log } from '../../logger'; import type { EdgeQueryResult, NodeQueryResult } from 'ts-common'; import type { GraphQueryResultFromBackend } from 'ts-common'; -export function parseCypherQuery(result: RecordShape[], returnType: "nodelink" | "table" = "nodelink"): GraphQueryResultFromBackend { +export function parseCypherQuery(result: RecordShape[], returnType: 'nodelink' | 'table' = 'nodelink'): GraphQueryResultFromBackend { + try { try { - try { - switch (returnType) { - case "nodelink": - return parseNodeLinkQuery(result); - case "table": - // TODO: add table support later - // return parseGroupByQuery(result.records as any); - log.error(`Table format not supported yet`); - throw new Error("Table format not supported yet"); - default: - log.error(`Error Unknown query Format`); - throw new Error("Unknown query Format"); - } - } catch (err) { - log.error(`Parsing failed ${err}`); - throw err; - } - + switch (returnType) { + case 'nodelink': + return parseNodeLinkQuery(result); + case 'table': + // TODO: add table support later + // return parseGroupByQuery(result.records as any); + log.error(`Table format not supported yet`); + throw new Error('Table format not supported yet'); + default: + log.error(`Error Unknown query Format`); + throw new Error('Unknown query Format'); + } } catch (err) { - log.error(`Error executing query`, err); - throw err; + log.error(`Parsing failed ${err}`); + throw err; } + } catch (err) { + log.error(`Error executing query`, err); + throw err; + } } function parseNodeLinkQuery(results: RecordShape[]): GraphQueryResultFromBackend { - let nodes: NodeQueryResult[] = []; - let edges: EdgeQueryResult[] = []; - let nodeIds: Set<string> = new Set(); - let edgeIds: Set<string> = new Set(); - const result: GraphQueryResultFromBackend = { nodes, edges }; - - for (let i = 0; i < results.length; i++) { - const resList = results[i]; - for (let j = 0; j < resList.length; j++) { - const res = resList.get(j); - parseNodeLinkEntity(res, nodes, edges, nodeIds, edgeIds); - } + const nodes: NodeQueryResult[] = []; + const edges: EdgeQueryResult[] = []; + const nodeIds: Set<string> = new Set(); + const edgeIds: Set<string> = new Set(); + const result: GraphQueryResultFromBackend = { nodes, edges }; + + for (let i = 0; i < results.length; i++) { + const resList = results[i]; + for (let j = 0; j < resList.length; j++) { + const res = resList.get(j); + parseNodeLinkEntity(res, nodes, edges, nodeIds, edgeIds); } + } - return result; + return result; } -function parseNodeLinkEntity(res: Node | Relationship | Path | PathSegment | any, nodes: NodeQueryResult[], edges: EdgeQueryResult[], nodeIds: Set<string>, edgeIds: Set<string>): void { - if (isRelationship(res)) { - const neoEdge = parseEdge(res); - if (!edgeIds.has(neoEdge._id)) { - edgeIds.add(neoEdge._id); - edges.push(neoEdge); - } - } else if (isNode(res)) { - const neoNode = parseNode(res); - if (!nodeIds.has(neoNode._id)) { - nodeIds.add(neoNode._id); - nodes.push(neoNode); - } - } else if (isPath(res)) { - parseNodeLinkEntity(res.start, nodes, edges, nodeIds, edgeIds); - for (const segment of res.segments) { - parseNodeLinkEntity(segment.relationship, nodes, edges, nodeIds, edgeIds); - parseNodeLinkEntity(segment.end, nodes, edges, nodeIds, edgeIds); - } - parseNodeLinkEntity(res.end, nodes, edges, nodeIds, edgeIds); - } else if (isPathSegment(res)) { - parseNodeLinkEntity(res.start, nodes, edges, nodeIds, edgeIds); - parseNodeLinkEntity(res.relationship, nodes, edges, nodeIds, edgeIds); - parseNodeLinkEntity(res.end, nodes, edges, nodeIds, edgeIds); - } else if (res?.constructor?.name === "Array") { - for (const r of res as any) { - parseNodeLinkEntity(r, nodes, edges, nodeIds, edgeIds); - } - } else { - log.warn(`Ignoring Unknown type ${res} ${res?.constructor?.name}`); +function parseNodeLinkEntity( + res: Node | Relationship | Path | PathSegment | any, + nodes: NodeQueryResult[], + edges: EdgeQueryResult[], + nodeIds: Set<string>, + edgeIds: Set<string>, +): void { + if (isRelationship(res)) { + const neoEdge = parseEdge(res); + if (!edgeIds.has(neoEdge._id)) { + edgeIds.add(neoEdge._id); + edges.push(neoEdge); + } + } else if (isNode(res)) { + const neoNode = parseNode(res); + if (!nodeIds.has(neoNode._id)) { + nodeIds.add(neoNode._id); + nodes.push(neoNode); + } + } else if (isPath(res)) { + parseNodeLinkEntity(res.start, nodes, edges, nodeIds, edgeIds); + for (const segment of res.segments) { + parseNodeLinkEntity(segment.relationship, nodes, edges, nodeIds, edgeIds); + parseNodeLinkEntity(segment.end, nodes, edges, nodeIds, edgeIds); + } + parseNodeLinkEntity(res.end, nodes, edges, nodeIds, edgeIds); + } else if (isPathSegment(res)) { + parseNodeLinkEntity(res.start, nodes, edges, nodeIds, edgeIds); + parseNodeLinkEntity(res.relationship, nodes, edges, nodeIds, edgeIds); + parseNodeLinkEntity(res.end, nodes, edges, nodeIds, edgeIds); + } else if (res?.constructor?.name === 'Array') { + for (const r of res as any) { + parseNodeLinkEntity(r, nodes, edges, nodeIds, edgeIds); } + } else { + log.warn(`Ignoring Unknown type ${res} ${res?.constructor?.name}`); + } } function parseNode(node: Node): NodeQueryResult { - return { - _id: node.identity.toString(), - label: node.labels[0], // TODO: only using first label - attributes: Object.fromEntries(Object.entries(node.properties)?.map(([k, v]) => { - if (Integer.isInteger(v)) { - return [k, v.toNumber()]; - } else if (typeof v === 'string') { - return [k, v]; - } else if (v instanceof Object) { - return [k, v.toString()]; - } + return { + _id: node.identity.toString(), + label: node.labels[0], // TODO: only using first label + attributes: Object.fromEntries( + Object.entries(node.properties)?.map(([k, v]) => { + if (Integer.isInteger(v)) { + return [k, v.toNumber()]; + } else if (typeof v === 'string') { + return [k, v]; + } else if (v instanceof Object) { + return [k, v.toString()]; + } - return [k, v]; - })), - }; + return [k, v]; + }), + ), + }; } function parseEdge(edge: Relationship): EdgeQueryResult { - return { - _id: edge.identity.toString(), - label: edge.type, - from: edge.start.toString(), - to: edge.end.toString(), - attributes: { ...edge.properties, type: edge.type }, - }; + return { + _id: edge.identity.toString(), + label: edge.type, + from: edge.start.toString(), + to: edge.end.toString(), + attributes: { ...edge.properties, type: edge.type }, + }; } - function parseGroupByQuery(results: Record<any, string>[]): any { - // TODO: not tested, since we don't use this yet - - let groupKey: string; - let byKey: string; - const group: string[] = []; - const by: string[] = []; - const result: { [key: string]: any } = {}; - - if (results[0].keys.length !== 2) { - throw new Error("invalid result format"); - } - - // Checks if the first letter of the first key is either an e or an r, since that would make it the By-key - // By-keys start with e0_attr or r0_attr and group keys with an aggregation funcion such as AVG_attr or MIN_attr - // This will work when an aggregation function starts with an E or R, since e != E - if (results[0].keys[0][0] === 'e' || results[0].keys[0][0] === 'r') { - byKey = results[0].keys[0]; - groupKey = results[0].keys[1]; - } else { - byKey = results[0].keys[1]; - groupKey = results[0].keys[0]; + // TODO: not tested, since we don't use this yet + + let groupKey: string; + let byKey: string; + const group: string[] = []; + const by: string[] = []; + const result: { [key: string]: any } = {}; + + if (results[0].keys.length !== 2) { + throw new Error('invalid result format'); + } + + // Checks if the first letter of the first key is either an e or an r, since that would make it the By-key + // By-keys start with e0_attr or r0_attr and group keys with an aggregation funcion such as AVG_attr or MIN_attr + // This will work when an aggregation function starts with an E or R, since e != E + if (results[0].keys[0][0] === 'e' || results[0].keys[0][0] === 'r') { + byKey = results[0].keys[0]; + groupKey = results[0].keys[1]; + } else { + byKey = results[0].keys[1]; + groupKey = results[0].keys[0]; + } + + // Form proper result structure + for (const res of results) { + const g = res.get(groupKey); + const b = res.get(byKey); + + if (g !== null && b !== null) { + group.push(g.toString()); + by.push(b.toString()); } + } - // Form proper result structure - for (const res of results) { - const g = res.get(groupKey); - const b = res.get(byKey); + result['group'] = { name: groupKey, values: group }; + result['by'] = { name: byKey, values: by }; - if (g !== null && b !== null) { - group.push(g.toString()); - by.push(b.toString()); - } - } - - result["group"] = { name: groupKey, values: group }; - result["by"] = { name: byKey, values: by }; - - return result; -} \ No newline at end of file + return result; +} diff --git a/src/utils/queryPublisher.ts b/src/utils/queryPublisher.ts index feb0751..8922fa0 100644 --- a/src/utils/queryPublisher.ts +++ b/src/utils/queryPublisher.ts @@ -111,7 +111,7 @@ export class QueryPublisher { mlAttributes: mlAttributes.parameters } - let queueName = mlType2Queue[mlAttributes.type]; + const queueName = mlType2Queue[mlAttributes.type]; if (!queueName) { log.error('Invalid ML type:', mlAttributes, mlAttributes.type); throw new Error('Invalid ML type'); diff --git a/src/utils/reactflow/query2backend.ts b/src/utils/reactflow/query2backend.ts index 3091656..6965baf 100644 --- a/src/utils/reactflow/query2backend.ts +++ b/src/utils/reactflow/query2backend.ts @@ -5,7 +5,13 @@ import { allSimplePaths } from 'graphology-simple-path'; import type { BackendQueryFormat, NodeStruct, QueryStruct, RelationStruct } from 'ts-common'; import type { MachineLearning } from 'ts-common/src/model/query/queryRequestModel'; -import type { QueryMultiGraph, EntityNodeAttributes, LogicNodeAttributes, QueryGraphNodes, RelationNodeAttributes } from 'ts-common/src/model/graphology'; +import type { + QueryMultiGraph, + EntityNodeAttributes, + LogicNodeAttributes, + QueryGraphNodes, + RelationNodeAttributes, +} from 'ts-common/src/model/graphology'; import type { AllLogicStatement } from 'ts-common/src/model/query/logic/general'; import { QueryElementTypes } from 'ts-common/src/model/reactflow'; import { type QueryBuilderSettings, QueryUnionType } from 'ts-common/src/model/query/queryBuilderModel'; @@ -49,11 +55,11 @@ const traverseEntityRelationPaths = ( paths[currentIdx].push(node.attributes); const edges = graph.edges.filter( - (n) => + n => n?.attributes?.sourceHandleData.nodeType !== QueryElementTypes.Logic && n?.attributes?.targetHandleData.nodeType !== QueryElementTypes.Logic, ); - let connections = edges.filter((e) => e.source === node.key); + const 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, attributes: [] }); @@ -70,14 +76,14 @@ const traverseEntityRelationPaths = ( 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); + ? entities.find(r => r.key === c.target) + : relations.find(r => r.key === c.target); return rightNode; }) - .filter((n) => n !== undefined) as SerializedNode<QueryGraphNodes>[]; + .filter(n => n !== undefined) as SerializedNode<QueryGraphNodes>[]; let chunkOffset = 0; - let pathBeforeTraversing = [...paths[currentIdx]]; + const pathBeforeTraversing = [...paths[currentIdx]]; nodesToRight.forEach((rightNode, i) => { if (i > 0) { paths.push([...pathBeforeTraversing]); // clone previous path in case of branching @@ -94,9 +100,9 @@ function calculateQueryLogic( 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); + const connectionsToLeft = graph.edges.filter(e => e.target === node.key); - let ret = [...node.attributes.logic.logic].map((l) => { + const 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'); @@ -111,21 +117,21 @@ function calculateQueryLogic( throw Error('Malformed Graph! Logic node has incorrect input reference'); } - const connectionToInputRef = connectionsToLeft.find((c) => c?.attributes?.targetHandleData.attributeName === inputRef.name); + 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 (inputRef.type === 'string') { if (val) { - val = `\"${val}\"`; + val = `"${val}"`; } else { - val = `\".*\"`; // Empty means allow anything + val = `".*"`; // Empty means allow anything } } 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); + 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 { @@ -151,13 +157,13 @@ function queryLogicUnion( logics: SerializedNode<LogicNodeAttributes>[], unionTypes: { [node_id: string]: QueryUnionType }, ): AllLogicStatement | undefined { - let graphLogicChunks = nodes.map((node) => calculateQueryLogic(node, graph, logics)); + const graphLogicChunks = nodes.map(node => calculateQueryLogic(node, graph, logics)); if (graphLogicChunks.length === 0) return undefined; if (graphLogicChunks.length === 1) return graphLogicChunks[0]; const constraintNodeId = nodes[0].key; - const entityNodeId = graph.edges.filter((x) => x.target == constraintNodeId)[0].source; + const entityNodeId = graph.edges.filter(x => x.target == constraintNodeId)[0].source; const unionType = unionTypes[entityNodeId] || QueryUnionType.AND; return [unionType, graphLogicChunks[0], queryLogicUnion(nodes.slice(1), graph, logics, unionTypes) || '0']; @@ -171,9 +177,9 @@ export function Query2BackendQuery( saveStateID: string, graph: QueryMultiGraph, settings: QueryBuilderSettings, - ml: MachineLearning[] + ml: MachineLearning[], ): BackendQueryFormat { - let query: BackendQueryFormat = { + const query: BackendQueryFormat = { saveStateID: saveStateID, query: [], machineLearning: ml, @@ -182,13 +188,13 @@ export function Query2BackendQuery( cached: false, }; - 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 entities = graph.nodes.filter(n => n?.attributes?.type === QueryElementTypes.Entity) as SerializedNode<EntityNodeAttributes>[]; + const 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) => { + .forEach(n => { graphologyQuery.dropNode(n); }); // Remove all logic nodes from the graph for cycle test if (hasCycle(graphologyQuery)) { @@ -216,13 +222,13 @@ export function Query2BackendQuery( return Query2BackendQuery(saveStateID, graphologyQuery.export(), settings, ml); } // 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]> = {}; + const graphSequenceChunks: QueryGraphNodes[][] = []; + const graphSequenceLogicChunks: QueryGraphNodes[][] = []; + const 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)); + const entitiesEmptyLeftHandle = entities.filter(n => !graph.edges.some(e => e.target === n.key)); + const 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 @@ -241,12 +247,12 @@ export function Query2BackendQuery( // 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); + const logics = graph.nodes.filter(n => n?.attributes?.type === QueryElementTypes.Logic) as SerializedNode<LogicNodeAttributes>[]; + const 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); + const logicsRightHandleFinal = logics.filter(n => { + return !graph.edges.some(e => e.source === n.key); }); query.logic = queryLogicUnion(logicsRightHandleFinal, graph, logics, settings.unionTypes); diff --git a/src/variables.ts b/src/variables.ts index 108de10..ad788c3 100644 --- a/src/variables.ts +++ b/src/variables.ts @@ -1,51 +1,56 @@ -import { RabbitMqConnection, UMSApi } from "ts-common"; -// @ts-ignore -import nodemailer from "nodemailer"; +import { RabbitMqConnection, UMSApi } from 'ts-common'; +import nodemailer from 'nodemailer'; export type QueryExecutionTypes = 'neo4j'; -export const CACHE_KEY_PREFIX = Bun.env.CACHE_KEY_PREFIX || "cached-schemas"; +export const CACHE_KEY_PREFIX = Bun.env.CACHE_KEY_PREFIX || 'cached-schemas'; export const CACHE_DURATION = Bun.env.CACHE_DURATION ? parseInt(Bun.env.CACHE_DURATION) : 5 * 60 * 1000; -export const USER_MANAGEMENT_SERVICE_API = Bun.env.USER_MANAGEMENT_SERVICE_API || "http://localhost:8000" +export const USER_MANAGEMENT_SERVICE_API = Bun.env.USER_MANAGEMENT_SERVICE_API || 'http://localhost:8000'; export const DEV = Bun.env.DEV || false; -export const ENV = Bun.env.ENV || "develop"; -export const RABBIT_USER = Bun.env.RABBIT_USER || "rabbitmq"; -export const RABBIT_PASSWORD = Bun.env.RABBIT_PASSWORD || "DevOnlyPass"; -export const RABBIT_HOST = Bun.env.RABBIT_HOST || "localhost"; -export const RABBIT_PORT = parseInt(Bun.env.RABBIT_PORT || "5672"); +export const ENV = Bun.env.ENV || 'develop'; +export const RABBIT_USER = Bun.env.RABBIT_USER || 'rabbitmq'; +export const RABBIT_PASSWORD = Bun.env.RABBIT_PASSWORD || 'DevOnlyPass'; +export const RABBIT_HOST = Bun.env.RABBIT_HOST || 'localhost'; +export const RABBIT_PORT = parseInt(Bun.env.RABBIT_PORT || '5672'); export const LOG_MESSAGES = Bun.env.LOG_MESSAGES || false; export const LOG_LEVEL = parseInt(Bun.env.LOG_LEVEL || '-1'); -export const REDIS_HOST = Bun.env.REDIS_HOST || "localhost"; -export const REDIS_PORT = parseInt(Bun.env.REDIS_PORT || "6379"); -export const REDIS_PASSWORD = Bun.env.REDIS_PASSWORD || "DevOnlyPass"; -export const REDIS_SCHEMA_CACHE_DURATION = Bun.env.REDIS_SCHEMA_CACHE_DURATION || "60m"; +export const REDIS_HOST = Bun.env.REDIS_HOST || 'localhost'; +export const REDIS_PORT = parseInt(Bun.env.REDIS_PORT || '6379'); +export const REDIS_PASSWORD = Bun.env.REDIS_PASSWORD || 'DevOnlyPass'; +export const REDIS_SCHEMA_CACHE_DURATION = Bun.env.REDIS_SCHEMA_CACHE_DURATION || '60m'; export const ums = new UMSApi(USER_MANAGEMENT_SERVICE_API); export const rabbitMq = new RabbitMqConnection({ - protocol: 'amqp', - hostname: RABBIT_HOST, - port: RABBIT_PORT, - username: RABBIT_USER, - password: RABBIT_PASSWORD, + protocol: 'amqp', + hostname: RABBIT_HOST, + port: RABBIT_PORT, + username: RABBIT_USER, + password: RABBIT_PASSWORD, }); -export const SMTP_HOST = Bun.env.SMTP_HOST || ""; +export const SMTP_HOST = Bun.env.SMTP_HOST || ''; export const SMTP_PORT = Bun.env.SMTP_PORT || 587; -export const SMTP_USER = Bun.env.SMTP_USER || ""; -export const SMTP_PASSWORD = Bun.env.SMTP_PASSWORD || ""; -export const DEBUG_EMAIL = (Bun.env.DEBUG_EMAIL !== "false") || false; +export const SMTP_USER = Bun.env.SMTP_USER || ''; +export const SMTP_PASSWORD = Bun.env.SMTP_PASSWORD || ''; +export const DEBUG_EMAIL = Bun.env.DEBUG_EMAIL !== 'false' || false; -export const mail = SMTP_HOST === '' || SMTP_USER === '' || SMTP_PASSWORD === '' ? undefined : nodemailer.createTransport({ - // @ts-ignore - host: SMTP_HOST, - port: SMTP_PORT, - secure: false, - tls: { - ciphers: "SSLv3", - rejectUnauthorized: false, - }, - auth: { - user: SMTP_USER, - pass: SMTP_PASSWORD, - } -}, { debug: true, logger: true }); \ No newline at end of file +export const mail = + SMTP_HOST === '' || SMTP_USER === '' || SMTP_PASSWORD === '' + ? undefined + : nodemailer.createTransport( + { + // @ts-expect-error - ciphers is not in the type + host: SMTP_HOST, + port: SMTP_PORT, + secure: false, + tls: { + ciphers: 'SSLv3', + rejectUnauthorized: false, + }, + auth: { + user: SMTP_USER, + pass: SMTP_PASSWORD, + }, + }, + { debug: true, logger: true }, + ); -- GitLab