Skip to content

Commit

Permalink
Add support for pipeable query parts + variable length paths + named …
Browse files Browse the repository at this point in the history
…paths
  • Loading branch information
robak86 committed Jun 27, 2018
1 parent 6fb43fc commit d97f12b
Show file tree
Hide file tree
Showing 29 changed files with 469 additions and 124 deletions.
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,20 @@
- Create repository for batch actions ```connection.nodeBatchRepository(SomeNodeType)```
- Write more specs
- Add basic query builders for ```WHERE``` and ```ORDER BY``` statements



## 0.3.0
- Add support for more composable way of build queries
```typescript
import {buildQuery, match, returns, where, orderBy} from 'neography/cypher'

const query = buildQuery(
match(m => m.node(DummyGraphNode).as('n')),
where(w => w.literal('n.attr2 >= {val1}').params({val1: 1})),
orderBy(w => w.aliased('n').attribute('attr2').asc()),
returns('n'),
);
```

- Add support for variable length relationships
- Add support for path variables
10 changes: 10 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# ROADMAP

* add support for ```WITH```
* add support for functions like ```shortestPath```
* add support for DateTime introduced in neo4j 3.4
* create alternative api which doesn't use @decorators
* register all types explicitly in Schema object
* use typescript conditional types for getting typescript types from runtime types declaration
* ...
* take all parameters when query is run, not when it's build (performance)
23 changes: 7 additions & 16 deletions lib/cypher/builders/QueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {MatchableElement, MatchQueryPart} from "../match/MatchQueryPart";
import {MatchQueryPart} from "../match/MatchQueryPart";
import {CreateQueryPart, PersistableElement} from "../create/CreateQueryPart";
import {CypherQuery} from "../CypherQuery";
import * as _ from 'lodash';
Expand All @@ -16,6 +16,7 @@ import {OrderStatement} from "../order/OrderStatement";
import {OrderBuilder} from "./OrderBuilder";
import {IQueryPart} from "../abstract/IQueryPart";
import {literal} from "../common";
import {MatchableElement} from "../match/MatchableQueryPart";


export type MatchBuilderCallback = (q:MatchBuilder) => MatchableElement[] | MatchableElement;
Expand Down Expand Up @@ -67,21 +68,11 @@ export class QueryBuilder {
return new QueryBuilder(elements);
}

order(literal:OrderBuilderCallback<any> | OrderStatement) {
if (literal instanceof OrderStatement) {
let elements = _.clone(this.elements);
elements.push(literal);
return new QueryBuilder(elements)
} else {
if (_.isFunction(literal)) {
let whereElement:OrderStatementPart[] | OrderStatementPart = literal(new OrderBuilder());
let elements = _.clone(this.elements);
elements.push(new OrderStatement(_.castArray(whereElement) as any) as any);
return new QueryBuilder(elements)
} else {
throw new Error("Wrong type of parameter for .order()")
}
}
order(literal:OrderBuilderCallback<any>) {
const orderStatement = OrderStatement.build(literal);
let elements = _.clone(this.elements);
elements.push(orderStatement);
return new QueryBuilder(elements)
}

create(...builderOrElements:(CreateBuilderCallback | PersistableElement)[]):QueryBuilder {
Expand Down
23 changes: 23 additions & 0 deletions lib/cypher/common/CompositeQueryPart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {IQueryPart} from "../abstract/IQueryPart";
import {QueryContext} from "./QueryContext";
import {IBoundQueryPart} from "../abstract/IBoundQueryPart";

export class CompositeQueryPart implements IQueryPart {
constructor(private elements:IQueryPart[]) {}

toCypher(ctx:QueryContext):IBoundQueryPart {
return this.elements.reduce((agg:IBoundQueryPart, el:IQueryPart) => {
const bound = el.toCypher(ctx);

return {
cypherString: agg.cypherString + bound.cypherString,
params: {
...agg.params,
...bound.params
}
}

}, {cypherString: '', params: {}})
}

}
20 changes: 20 additions & 0 deletions lib/cypher/common/joinQueryParts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {IBoundQueryPart} from "../abstract/IBoundQueryPart";
import {IQueryPart} from "../abstract/IQueryPart";
import {QueryContext} from "./QueryContext";

export function joinQueryParts(separator:string, parts:IQueryPart[], ctx:QueryContext):IBoundQueryPart {
let cypherStringParts:string[] = [];
let params = {};

parts.forEach((el:IQueryPart) => {
const bound = el.toCypher(ctx);

cypherStringParts.push(bound.cypherString);
params = {...params, ...bound.params};
});

return {
cypherString: cypherStringParts.join(separator),
params
}
}
3 changes: 2 additions & 1 deletion lib/cypher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export const buildQuery = (...elements:IQueryPart[]) => new QueryBuilder(element

export * from './match';
export * from './create';
export * from './where';
export * from './where';
export * from './order';
1 change: 1 addition & 0 deletions lib/cypher/match/IRelationMatchQueryPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface IRelationMatchQueryPart<R extends RelationshipEntity> extends I
direction(direction:RelationDirectionType):IRelationMatchQueryPart<R>
params(params:Partial<R>):IRelationMatchQueryPart<R>
as(alias:string):IRelationMatchQueryPart<R>
length(minOrExactLength:number, max?:number):IRelationMatchQueryPart<R>
}
4 changes: 3 additions & 1 deletion lib/cypher/match/MatchNodeQueryPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ export class MatchNodeQueryPart<N extends NodeEntity> implements INodeMatchQuery
` {${generateMatchAssignments(paramsId, this._params)}}` :
'';

const params = this._params ? {[paramsId]: this._params} : {};

return {
cypherString: `(${alias}:${this.nodeLabels}${cypherParams})`,
params: {[paramsId]: this._params}
params
};
}
}
67 changes: 12 additions & 55 deletions lib/cypher/match/MatchQueryPart.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,30 @@
import {MatchNodeQueryPart} from "./MatchNodeQueryPart";
import {MatchRelationQueryPart} from "./MatchRelationQueryPart";
import {IQueryPart} from "../abstract/IQueryPart";
import {QueryContext} from "../common/QueryContext";
import {IBoundQueryPart} from "../abstract/IBoundQueryPart";
import {NodeRelationConcatenator} from "../utils/NodeRelationConcatenator";
import {MatchUntypedNodeQueryPart} from "./MatchUntypedNodeQueryPart";
import {MatchUntypedRelationQueryPart} from "./MatchUntypedRelationQueryPart";
import {MatchedNodeQueryPart} from "../common/MatchedNodeQueryPart";
import {INodeMatchQueryPart} from "./INodeMatchQueryPart";
import {IRelationMatchQueryPart} from "./IRelationMatchQueryPart";
import {MatchBuilderCallback} from "../builders/QueryBuilder";
import {MatchBuilder} from "../builders/MatchBuilder";
import * as _ from 'lodash';
import {MatchableElement, MatchableQueryPart} from "./MatchableQueryPart";
import {PathQueryPart} from "./PathQueryPart";

export type MatchableElement = MatchNodeQueryPart<any>
| MatchedNodeQueryPart
| INodeMatchQueryPart<any>
| IRelationMatchQueryPart<any>;
export type MatchQueryPartChild =
| PathQueryPart
| MatchableQueryPart;

export class MatchQueryPart implements IQueryPart {
static build(isOptional:boolean, ...builderOrElements:(MatchBuilderCallback | MatchableElement)[]):MatchQueryPart {
let matchableElements:MatchableElement[] = [];
const matchableQueryPart = MatchableQueryPart.build(...builderOrElements);

builderOrElements.forEach((el) => {
if (_.isFunction(el)) {
matchableElements = matchableElements.concat(el(new MatchBuilder()));
} else {
matchableElements.push(el as any);
}
});

return new MatchQueryPart(matchableElements, isOptional);
return new MatchQueryPart(matchableQueryPart, isOptional);
}

constructor(private elements:MatchableElement[], private isOptionalMatch:boolean) {}
constructor(private matchableQueryParts:MatchQueryPartChild,
private isOptionalMatch:boolean) {
}

toCypher(ctx:QueryContext):IBoundQueryPart {
let cypherPart:IBoundQueryPart = {cypherString: '', params: {}};
let concatenator:NodeRelationConcatenator = new NodeRelationConcatenator();

this.elements.forEach((el:MatchableElement) => {
if (el instanceof MatchNodeQueryPart
|| el instanceof MatchUntypedNodeQueryPart
|| el instanceof MatchedNodeQueryPart) {

let nodeQueryCypherPart = el.toCypher(ctx);
concatenator.push({cypherString: nodeQueryCypherPart.cypherString, isRelation: false});

//TODO: check if there is no collisions! Ideally params should be registered in context!!! We could implement this behaviour there
cypherPart.params = {
...cypherPart.params,
...nodeQueryCypherPart.params
}
}

if (el instanceof MatchRelationQueryPart || el instanceof MatchUntypedRelationQueryPart) {
let relationQueryCypherPart = el.toCypher(ctx);
concatenator.push({cypherString: relationQueryCypherPart.cypherString, isRelation: true});

//TODO: check if there is no collisions! Ideally params should be registered in context!!! We could implement this behaviour there
cypherPart.params = {
...cypherPart.params,
...relationQueryCypherPart.params
}
}
});

const cypherPart = this.matchableQueryParts.toCypher(ctx);
let matchKeyword = this.isOptionalMatch ? 'OPTIONAL MATCH' : 'MATCH';
cypherPart.cypherString = `${matchKeyword} ${concatenator.toString()}`;
cypherPart.cypherString = `${matchKeyword} ${cypherPart.cypherString}`;
return cypherPart;
}
}
36 changes: 31 additions & 5 deletions lib/cypher/match/MatchRelationQueryPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import {generateMatchAssignments} from "../utils/QueryHelpers";
import {IRelationMatchQueryPart, RelationDirectionType} from "./IRelationMatchQueryPart";
import {IQueryPart} from "../abstract/IQueryPart";
import {cloned} from "../../utils/core";

import * as _ from 'lodash';

export class MatchRelationQueryPart<G extends RelationshipEntity> implements IRelationMatchQueryPart<G>, IQueryPart {
protected _params:any;
protected _alias:string;
protected _direction:RelationDirectionType = '<->';
protected _minLength:number | undefined;
protected _maxLength:number | undefined;

constructor(private klass:Type<G>) {}

Expand All @@ -28,8 +30,11 @@ export class MatchRelationQueryPart<G extends RelationshipEntity> implements IRe
return cloned(this, (el:MatchRelationQueryPart<G>) => el._alias = alias);
}

private get relationType():string {
return RelationMetadata.getOrCreateForClass(this.klass).getType();
length(minOrExactLength:number, max?:number) {
return cloned(this, (el:MatchRelationQueryPart<G>) => {
el._minLength = minOrExactLength;
el._maxLength = max;
});
}

toCypher(context:QueryContext):IBoundQueryPart {
Expand All @@ -39,12 +44,33 @@ export class MatchRelationQueryPart<G extends RelationshipEntity> implements IRe
` {${generateMatchAssignments(paramsId, this._params)}}` :
'';

const lengthFragment = () => {
const lengthToString = (l:number | undefined):string => l === Infinity ? '' : l!.toString();


if (_.isUndefined(this._minLength) && _.isUndefined(this._maxLength)) {
return '';
}

if (!_.isUndefined(this._minLength) && _.isUndefined(this._maxLength)) {
return `*${lengthToString(this._minLength)}`
}

return `*${lengthToString(this._minLength)}..${lengthToString(this._maxLength)}`
};

const params = this._params ? {[paramsId]: this._params} : {};

return {
cypherString: `${this.arrowPreSign}[${alias}:${this.relationType}${cypherParams}]${this.arrowPostSign}`,
params: {[paramsId]: this._params}
cypherString: `${this.arrowPreSign}[${alias}:${this.relationType}${lengthFragment()}${cypherParams}]${this.arrowPostSign}`,
params
};
}

private get relationType():string {
return RelationMetadata.getOrCreateForClass(this.klass).getType();
}

private get arrowPreSign():string {
return (this._direction === '<->' || this._direction === '->') ? '-' : '<-';
}
Expand Down
29 changes: 28 additions & 1 deletion lib/cypher/match/MatchUntypedRelationQueryPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import {IRelationMatchQueryPart, RelationDirectionType} from "./IRelationMatchQu
import {IQueryPart} from "../abstract/IQueryPart";
import {RelationshipEntity} from "../../model";
import {cloned} from "../../utils/core";
import * as _ from 'lodash';


export class MatchUntypedRelationQueryPart<R extends RelationshipEntity> implements IRelationMatchQueryPart<R>, IQueryPart {
protected _params:any;
protected _alias:string;
protected _direction:RelationDirectionType = '<->';
protected _minLength:number | undefined;
protected _maxLength:number | undefined;

constructor() {}

Expand All @@ -26,6 +29,14 @@ export class MatchUntypedRelationQueryPart<R extends RelationshipEntity> impleme
return cloned(this, (el:MatchUntypedRelationQueryPart<R>) => el._alias = alias);
}

length(minOrExactLength:number, max?:number) {
return cloned(this, (el:MatchUntypedRelationQueryPart<R>) => {
el._minLength = minOrExactLength;
el._maxLength = max;
});
}

// TODO: toCypher is almost identical to MatchRelationQueryPart's toCypher
toCypher(context:QueryContext):IBoundQueryPart {
let alias = this._alias || context.checkoutRelationAlias();

Expand All @@ -34,8 +45,24 @@ export class MatchUntypedRelationQueryPart<R extends RelationshipEntity> impleme
` {${generateMatchAssignments(paramsId, this._params)}}` :
'';

const lengthFragment = () => {
const lengthToString = (l:number | undefined):string => l === Infinity ? '' : l!.toString();


if (_.isUndefined(this._minLength) && _.isUndefined(this._maxLength)) {
return '';
}

if (!_.isUndefined(this._minLength) && _.isUndefined(this._maxLength)) {
return `*${lengthToString(this._minLength)}`
}

return `*${lengthToString(this._minLength)}..${lengthToString(this._maxLength)}`
};


return {
cypherString: `${this.arrowPreSign}[${alias} ${cypherParams}]${this.arrowPostSign}`,
cypherString: `${this.arrowPreSign}[${alias}${lengthFragment()} ${cypherParams}]${this.arrowPostSign}`,
params: {[paramsId]: this._params}
};
}
Expand Down
Loading

0 comments on commit d97f12b

Please sign in to comment.