From fe012003306b8d773027caa16637f0f643a1613f Mon Sep 17 00:00:00 2001 From: Jason Athanasoglou Date: Sun, 3 Sep 2023 10:30:14 +0300 Subject: [PATCH] Add getVariableLengthRelationshipString to fix bug --- documentation/md/docs/QueryBuilder/Helpers.md | 55 +++++++++++++---- src/Queries/QueryBuilder/QueryBuilder.spec.ts | 13 +++- src/Queries/QueryBuilder/QueryBuilder.ts | 61 +++++++++++++++---- 3 files changed, 104 insertions(+), 25 deletions(-) diff --git a/documentation/md/docs/QueryBuilder/Helpers.md b/documentation/md/docs/QueryBuilder/Helpers.md index 0518b0c..3421e49 100644 --- a/documentation/md/docs/QueryBuilder/Helpers.md +++ b/documentation/md/docs/QueryBuilder/Helpers.md @@ -3,9 +3,9 @@ The `QueryBuilder` class also provides some helpers for generating strings which could be used in a statement. ## Getting normalized labels -`QueryRunner.getNormalizedLabels` returns a single string to be used in a query. +`QueryBuilder.getNormalizedLabels` returns a single string to be used in a query. ```js -const { getNormalizedLabels } = QueryRunner; +const { getNormalizedLabels } = QueryBuilder; console.log(getNormalizedLabels('Users')); // `Users` @@ -19,11 +19,11 @@ console.log(getNormalizedLabels(['Users', 'Active', 'Old'])); // "`Users:Active: ``` ## Getting a node statement -`QueryRunner.getNodeStatement` returns a string for a node's identifier, label, and inner info (like a where), to be used in a query. +`QueryBuilder.getNodeStatement` returns a string for a node's identifier, label, and inner info (like a where), to be used in a query. Every parameter is optional. ```js -const { getNodeStatement } = QueryRunner; +const { getNodeStatement } = QueryBuilder; console.log(getNodeStatement({ identifier: 'n', @@ -72,9 +72,9 @@ console.log(bindParam().get()); // { id: 1 } ``` ## Getting a relationship statement -`QueryRunner.getRelationshipStatement` returns a string for a relationship's direction, name, and inner info (like a where), to be used in a query. +`QueryBuilder.getRelationshipStatement` returns a string for a relationship's direction, name, and inner info (like a where), to be used in a query. ```js -const { getRelationshipStatement } = QueryRunner; +const { getRelationshipStatement } = QueryBuilder; console.log(getRelationshipStatement({ direction: 'out', @@ -135,9 +135,9 @@ console.log(bindParam.get()); // { id: 1 } ``` ## Getting an identifier with a label -`QueryRunner.getIdentifierWithLabel` returns a string to be used in a query, regardless if any of the identifier or label are null +`QueryBuilder.getIdentifierWithLabel` returns a string to be used in a query, regardless if any of the identifier or label are null ```js -const { getIdentifierWithLabel } = QueryRunner; +const { getIdentifierWithLabel } = QueryBuilder; console.log(getIdentifierWithLabel('MyIdentifier', 'MyLabel')); // "MyIdentifier:MyLabel" @@ -147,9 +147,9 @@ console.log(getIdentifierWithLabel('MyIdentifier', 'MyLabel')); // ":MyLabel" ``` ## Getting parts for a SET operation -`QueryRunner.getSetParts` returns the parts and the statement for a SET operation. +`QueryBuilder.getSetParts` returns the parts and the statement for a SET operation. ```js -const { getSetParts } = QueryRunner; +const { getSetParts } = QueryBuilder; const existingBindParam = new BindParam({}); const result = getSetParts({ @@ -184,14 +184,14 @@ console.log(bindParam.get()); // { x: 'irrelevant', x_aaaa: 5, y: 'foo' } ``` ## Getting properties with query param values -`QueryRunner` exposes a `getPropertiesWithParams` function which returns an object in a string format to be used in queries, while replacing its values with bind params. +`QueryBuilder` exposes a `getPropertiesWithParams` function which returns an object in a string format to be used in queries, while replacing its values with bind params. ```js /* --> an existing BindParam instance, could have existing values */ const bindParam = new BindParam({ x: 4, }); -const result = QueryRunner.getPropertiesWithParams( +const result = QueryBuilder.getPropertiesWithParams( /* --> the object to use */ { x: 5, @@ -205,3 +205,34 @@ const result = QueryRunner.getPropertiesWithParams( console.log(result); // "{ x: $x__aaaa, y: $y }" console.log(bindParam.get()); // { x: 4, x__aaaa: 5, y: 'foo' } ``` + +## Getting the inner string of a variable length relationship +```js +const onlyMinHops = QueryBuilder.getVariableLengthRelationshipString({ + minHops: 3, +}); +console.log(onlyMinHops); // "*3.." + +const onlyMaxHops = QueryBuilder.getVariableLengthRelationshipString({ + maxHops: 5, +}); +console.log(onlyMaxHops); // "*5.." + +const bothHops = QueryBuilder.getVariableLengthRelationshipString({ + minHops: 3, + maxHops: 5, +}); +console.log(bothHops); // "*3..5" + +const equalHops = QueryBuilder.getVariableLengthRelationshipString({ + minHops: 5, + maxHops: 5, +}); +console.log(equalHops); // "*5*" + +const ifiniteHops = QueryBuilder.getVariableLengthRelationshipString({ + maxHops: Infinity, +}); +console.log(ifiniteHops); // "*" +``` + diff --git a/src/Queries/QueryBuilder/QueryBuilder.spec.ts b/src/Queries/QueryBuilder/QueryBuilder.spec.ts index 9fe0706..7716d8c 100644 --- a/src/Queries/QueryBuilder/QueryBuilder.spec.ts +++ b/src/Queries/QueryBuilder/QueryBuilder.spec.ts @@ -541,6 +541,7 @@ describe.only('QueryBuilder', () => { model: ModelA, }, { + // only min hops direction: 'out', minHops: 2, }, @@ -548,6 +549,7 @@ describe.only('QueryBuilder', () => { model: ModelB, }, { + // infinity hops direction: 'in', maxHops: Infinity, }, @@ -555,6 +557,7 @@ describe.only('QueryBuilder', () => { model: ModelA, }, { + // both min and max hops direction: 'none', minHops: 2, maxHops: 5, @@ -562,12 +565,20 @@ describe.only('QueryBuilder', () => { { model: ModelB, }, + { + // only max hops + direction: 'none', + maxHops: 5, + }, + { + model: ModelA, + }, ], }); expectStatementEquals( queryBuilder, - 'MATCH (a1)<-[]-(:MyLabelA1)-[:RelationshipName1]->(a2:MyLabelA2)-[r1]-(:`ModelA`)<-[{ relProp1: $relProp1, someLiteral: "exact literal" }]-()-[r2:RelationshipName2]->({ id: $id })<-[:RelationshipName3 { relProp2: $relProp2 }]-(a3 { age: $age })-[r3 { relProp3: $relProp3 }]-(a3:`ModelB` { name: $name })-[r4:RelationshipName4 { relProp4: $relProp4 }]->(a4)-[:RELNAME]->(:`ModelA`)-[*2]->(:`ModelB`)<-[*]-(:`ModelA`)-[*2..5]-(:`ModelB`)', + 'MATCH (a1)<-[]-(:MyLabelA1)-[:RelationshipName1]->(a2:MyLabelA2)-[r1]-(:`ModelA`)<-[{ relProp1: $relProp1, someLiteral: "exact literal" }]-()-[r2:RelationshipName2]->({ id: $id })<-[:RelationshipName3 { relProp2: $relProp2 }]-(a3 { age: $age })-[r3 { relProp3: $relProp3 }]-(a3:`ModelB` { name: $name })-[r4:RelationshipName4 { relProp4: $relProp4 }]->(a4)-[:RELNAME]->(:`ModelA`)-[*2..]->(:`ModelB`)<-[*]-(:`ModelA`)-[*2..5]-(:`ModelB`)-[*..5]-(:`ModelA`)', ); expectBindParamEquals(queryBuilder, { relProp1: 1, diff --git a/src/Queries/QueryBuilder/QueryBuilder.ts b/src/Queries/QueryBuilder/QueryBuilder.ts index 0485d5c..cb1850d 100644 --- a/src/Queries/QueryBuilder/QueryBuilder.ts +++ b/src/Queries/QueryBuilder/QueryBuilder.ts @@ -662,19 +662,16 @@ export class QueryBuilder { QueryBuilder.getIdentifierWithLabel(identifier, name), ); } - if (minHops === Infinity || maxHops === Infinity) { - innerRelationshipParts.push('*'); - } else { - const variableLength = [minHops, maxHops] - .filter((v) => typeof v === 'number') - .join('..'); - - if (variableLength) { - innerRelationshipParts.push('*' + variableLength); - } - } - if (typeof minHops === 'number' || typeof maxHops === 'number') { + + const variableLength = QueryBuilder.getVariableLengthRelationshipString({ + minHops, + maxHops, + }); + + if (variableLength) { + innerRelationshipParts.push(variableLength); } + if (inner) { if (typeof inner === 'string') { innerRelationshipParts.push(inner); @@ -699,6 +696,46 @@ export class QueryBuilder { return allParts.join(''); }; + /** + * Returns the inner part of a relationship given the min and max hops. It doesn't include the brackets ([]) + * Example: minHops = 1, maxHops = 2 -> "*1..2" + * + * https://neo4j.com/docs/cypher-manual/current/patterns/reference/#variable-length-relationships-rules + */ + public static getVariableLengthRelationshipString = ({ + minHops, + maxHops, + }: { + minHops?: number | undefined; + maxHops?: number | undefined; + }): string | null => { + // infinity: * + if (minHops === Infinity || maxHops === Infinity) { + return '*'; + } + + // only min hops: *m.. + if (typeof minHops === 'number' && typeof maxHops !== 'number') { + return `*${minHops}..`; + } + + // only max hops: *..n + if (typeof minHops !== 'number' && typeof maxHops === 'number') { + return `*..${maxHops}`; + } + + // both: *m..n + if (typeof minHops === 'number' && typeof maxHops === 'number') { + if (minHops === maxHops) { + // exactly: *m + return `*${minHops}`; + } + return `*${minHops}..${maxHops}`; + } + + return null; + }; + /** returns the parts and the statement for a SET operation with the given params */ public static getSetParts = (params: { /** data to set */