Skip to content

Commit

Permalink
Add getVariableLengthRelationshipString to fix bug
Browse files Browse the repository at this point in the history
  • Loading branch information
themetalfleece committed Sep 3, 2023
1 parent 35a2392 commit fe01200
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 25 deletions.
55 changes: 43 additions & 12 deletions documentation/md/docs/QueryBuilder/Helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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"

Expand All @@ -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({
Expand Down Expand Up @@ -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,
Expand All @@ -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); // "*"
```

13 changes: 12 additions & 1 deletion src/Queries/QueryBuilder/QueryBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,33 +541,44 @@ describe.only('QueryBuilder', () => {
model: ModelA,
},
{
// only min hops
direction: 'out',
minHops: 2,
},
{
model: ModelB,
},
{
// infinity hops
direction: 'in',
maxHops: Infinity,
},
{
model: ModelA,
},
{
// both min and max hops
direction: 'none',
minHops: 2,
maxHops: 5,
},
{
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,
Expand Down
61 changes: 49 additions & 12 deletions src/Queries/QueryBuilder/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 */
Expand Down

0 comments on commit fe01200

Please sign in to comment.