diff --git a/src/utils/cypher/converter/model.ts b/src/utils/cypher/converter/model.ts index e4460ce12fa060f4c24043e3146332ce3a649b02..e552f45fe0673e4ddc85503a469c324de399d4e1 100644 --- a/src/utils/cypher/converter/model.ts +++ b/src/utils/cypher/converter/model.ts @@ -11,4 +11,5 @@ export interface EntityCacheData { export interface QueryCacheData { entities: EntityCacheData[]; relations: RelationCacheData[]; + unwinds: string[]; } diff --git a/src/utils/cypher/converter/node.ts b/src/utils/cypher/converter/node.ts index 08376339b16f4e48efbf0c57b4d5c8c7d8342477..091bb84cfb75f7dbf9952b6df2b87b8a662b9be7 100644 --- a/src/utils/cypher/converter/node.ts +++ b/src/utils/cypher/converter/node.ts @@ -2,7 +2,7 @@ import type { NodeStruct } from 'ts-common'; import type { QueryCacheData } from './model'; import { getRelationCypher } from './relation'; -export function getNodeCypher(JSONQuery: NodeStruct): [string, QueryCacheData] { +export function getNodeCypher(JSONQuery: NodeStruct, cacheData: QueryCacheData): [string, QueryCacheData] { let label = ''; if (JSONQuery.label) { label = `:${JSONQuery.label}`; @@ -12,8 +12,6 @@ export function getNodeCypher(JSONQuery: NodeStruct): [string, QueryCacheData] { id = JSONQuery.id; } - const cacheData: QueryCacheData = { entities: [], relations: [] }; - if (id) { cacheData.entities.push({ id, queryId: '' }); } @@ -21,11 +19,9 @@ export function getNodeCypher(JSONQuery: NodeStruct): [string, QueryCacheData] { let cypher = `(${id}${label})`; if (JSONQuery.relation) { - const [relationCypher, subCache] = getRelationCypher(JSONQuery.relation); + const [relationCypher, _cacheData] = getRelationCypher(JSONQuery.relation, cacheData); 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') { @@ -34,6 +30,7 @@ export function getNodeCypher(JSONQuery: NodeStruct): [string, QueryCacheData] { cypher += `-${relationCypher}`; } } + return [cypher, _cacheData]; } return [cypher, cacheData]; } diff --git a/src/utils/cypher/converter/queryConverter.test.ts b/src/utils/cypher/converter/queryConverter.test.ts index bb3cd7ef60c79628855a2dcd7be25f6119840d17..90b0a508a5e1992aaf1d5b6886ec77affa131fcc 100644 --- a/src/utils/cypher/converter/queryConverter.test.ts +++ b/src/utils/cypher/converter/queryConverter.test.ts @@ -532,4 +532,99 @@ describe('query2Cypher', () => { expect(fixCypherSpaces(cypher)).toBe(fixCypherSpaces(expectedCypher)); }); + + it('should return correctly on a query with boolean logic in relation', () => { + const query: BackendQueryFormat = { + saveStateID: '31966faf-0741-4a70-b92b-62c4b5e25d91', + query: [ + { + id: 'path_0', + node: { + id: 'id_1737115397734', + label: 'Product', + relation: { + id: 'id_1737115397848', + label: 'PRODUCT_MATERIAL', + depth: { + max: 1, + min: 1, + }, + direction: 'TO', + node: { + id: 'id_1737115397423', + label: 'Material', + }, + }, + }, + }, + { + id: 'path_1', + node: { + id: 'id_1737115397734', + label: 'Product', + relation: { + id: 'id_1737115611667', + label: 'DEPARTS_FROM', + depth: { + max: 1, + min: 1, + }, + direction: 'TO', + node: { + id: 'id_1737115612952', + label: 'Country', + relation: { + id: 'id_1737115886717', + label: 'CountryFlow', + depth: { + max: 1, + min: 1, + }, + direction: 'TO', + node: { + id: 'id_1737115684010', + label: 'Country', + }, + }, + }, + }, + }, + }, + { + id: 'path_2', + node: { + id: 'id_1737115397734', + label: 'Product', + relation: { + id: 'id_1737115682374', + label: 'ARRIVES_AT', + depth: { + max: 1, + min: 1, + }, + direction: 'TO', + node: { + id: 'id_1737115684010', + label: 'Country', + }, + }, + }, + }, + ], + machineLearning: [], + limit: 100, + return: ['*'], + cached: false, + logic: ['And', ['==', '@id_1737115397423.GO', '"852349"'], ['==', '@id_1737115886717.goodFlow', 'false']], + }; + + const cypher = query2Cypher(query); + const expectedCypher = `MATCH path_0 = ((id_1737115397734:Product)-[id_1737115397848:PRODUCT_MATERIAL*1..1]->(id_1737115397423:Material)) + MATCH path_1 = ((id_1737115397734:Product)-[id_1737115611667:DEPARTS_FROM*1..1]->(id_1737115612952:Country)-[id_1737115886717:CountryFlow*1..1]->(id_1737115684010:Country)) + MATCH path_2 = ((id_1737115397734:Product)-[id_1737115682374:ARRIVES_AT*1..1]->(id_1737115684010:Country)) + WHERE ALL(path_2_rel_id_1737115886717 in id_1737115886717 WHERE ((id_1737115397423.GO = "852349") and + (path_2_rel_id_1737115886717.goodFlow = false))) RETURN * LIMIT 100`; + + expect(fixCypherSpaces(cypher)).toEqual(fixCypherSpaces(expectedCypher)); + }); }); diff --git a/src/utils/cypher/converter/queryConverter.ts b/src/utils/cypher/converter/queryConverter.ts index 240da9b17ee73dc5e76e6638424856a9318ee0e8..8c593a8efbd4c8319a544729229430a372f73b72 100644 --- a/src/utils/cypher/converter/queryConverter.ts +++ b/src/utils/cypher/converter/queryConverter.ts @@ -13,12 +13,12 @@ import { getNodeCypher } from './node'; export function query2Cypher(JSONQuery: BackendQueryFormat): string | null { let totalQuery = ''; let matchQuery = ''; - let cacheData: QueryCacheData = { entities: [], relations: [] }; + let cacheData: QueryCacheData = { entities: [], relations: [], unwinds: [] }; // MATCH block for (const query of JSONQuery.query) { let match = 'MATCH '; - const [nodeCypher, _cacheData] = getNodeCypher(query.node); + const [nodeCypher, _cacheData] = getNodeCypher(query.node, cacheData); cacheData = _cacheData; for (let i = 0; i < cacheData.entities.length; i++) { diff --git a/src/utils/cypher/converter/relation.ts b/src/utils/cypher/converter/relation.ts index 9689a605441495ee99af2a584368cbf548abd045..0cc8da4fd5433e935d51eaeeefd27632290a2725 100644 --- a/src/utils/cypher/converter/relation.ts +++ b/src/utils/cypher/converter/relation.ts @@ -4,8 +4,9 @@ import type { QueryCacheData } from './model'; export const NoNodeError = new Error('relation must have a node'); -export function getRelationCypher(JSONQuery: RelationStruct): [string | undefined, QueryCacheData] { +export function getRelationCypher(JSONQuery: RelationStruct, cacheData: QueryCacheData): [string | undefined, QueryCacheData] { let label = ''; + if (JSONQuery.label) { label = `:${JSONQuery.label}`; } @@ -14,12 +15,13 @@ export function getRelationCypher(JSONQuery: RelationStruct): [string | undefine id = JSONQuery.id; } let depth = ''; + if (JSONQuery.depth) { + // && JSONQuery.depth.min !== 1 && JSONQuery.depth.max !== 1 depth = `*${JSONQuery.depth.min}..${JSONQuery.depth.max}`; + cacheData.unwinds.push(id + '_unwind'); } - const cacheData: QueryCacheData = { entities: [], relations: [] }; - if (id) { cacheData.relations.push({ id, queryId: '' }); } @@ -29,11 +31,8 @@ export function getRelationCypher(JSONQuery: RelationStruct): [string | undefine if (!JSONQuery.node) { return [undefined, cacheData]; } + const [nodeCypher, _cacheData] = getNodeCypher(JSONQuery.node, 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') { @@ -41,5 +40,5 @@ export function getRelationCypher(JSONQuery: RelationStruct): [string | undefine } else { cypher += `-${nodeCypher}`; } - return [cypher, cacheData]; + return [cypher, _cacheData]; }