Skip to content

Commit

Permalink
Merge pull request #194 from neo4j/mixin-refactor
Browse files Browse the repository at this point in the history
Refactor mixins for using nextClause
  • Loading branch information
angrykoala committed Oct 20, 2023
2 parents f8f749a + e2e7c4c commit 3648e40
Show file tree
Hide file tree
Showing 16 changed files with 143 additions and 132 deletions.
23 changes: 23 additions & 0 deletions .changeset/eight-carrots-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@neo4j/cypher-builder": patch
---

Refactors mixins.
Due to this, multiple top-level clauses nested in the same clause will explicitly fail, instead of silent failing:

The following is not supported;

```javascript
const match = new Cypher.Match();

match.with();
match.return();
```

In favor of the following:

```javascript
const match = new Cypher.Match();

match.with().return();
```
2 changes: 1 addition & 1 deletion DEVELOPING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ In the Cypher Builder folder run:

In the root of the package run:

- `yarn link -p [path-to-local-cypher-builder]
- `yarn link -p [path-to-local-cypher-builder]`

To unlink, in the project using cypher-builder:

Expand Down
7 changes: 3 additions & 4 deletions src/clauses/Call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,10 @@ export class Call extends Clause {
public getCypher(env: CypherEnvironment): string {
const subQueryStr = this.subQuery.getCypher(env);
const innerWithCypher = compileCypherIfExists(this.importWith, env, { suffix: "\n" });
const returnCypher = compileCypherIfExists(this.returnStatement, env, { prefix: "\n" });
const withCypher = compileCypherIfExists(this.withStatement, env, { prefix: "\n" });
const unwindCypher = compileCypherIfExists(this.unwindStatement, env, { prefix: "\n" });
const inCallBlock = `${innerWithCypher}${subQueryStr}`;

return `CALL {\n${padBlock(inCallBlock)}\n}${withCypher}${unwindCypher}${returnCypher}`;
const nextClause = this.compileNextClause(env);

return `CALL {\n${padBlock(inCallBlock)}\n}${nextClause}`;
}
}
15 changes: 15 additions & 0 deletions src/clauses/Clause.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { CypherASTNode } from "../CypherASTNode";
import type { EnvPrefix } from "../Environment";
import { CypherEnvironment } from "../Environment";
import type { CypherResult } from "../types";
import { compileCypherIfExists } from "../utils/compile-cypher-if-exists";
import { padBlock } from "../utils/pad-block";
import { toCypherParams } from "../utils/to-cypher-params";

Expand All @@ -30,6 +31,8 @@ const customInspectSymbol = Symbol.for("nodejs.util.inspect.custom");
* @group Internal
*/
export abstract class Clause extends CypherASTNode {
protected nextClause: Clause | undefined;

/** Compiles a clause into Cypher and params */
public build(prefix?: string | EnvPrefix | undefined, extraParams: Record<string, unknown> = {}): CypherResult {
if (this.isRoot) {
Expand Down Expand Up @@ -73,4 +76,16 @@ export abstract class Clause extends CypherASTNode {
[customInspectSymbol](): string {
return this.toString();
}

protected addNextClause(clause: Clause): void {
if (this.nextClause) {
throw new Error("Cannot chain 2 top-level clauses to the same clause");
}
this.nextClause = clause;
this.addChildren(this.nextClause);
}

protected compileNextClause(env: CypherEnvironment): string {
return compileCypherIfExists(this.nextClause, env, { prefix: "\n" });
}
}
15 changes: 7 additions & 8 deletions src/clauses/Create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
*/

import type { CypherEnvironment } from "../Environment";
import type { NodeRef } from "../references/NodeRef";
import { Pattern } from "../pattern/Pattern";
import { SetClause } from "./sub-clauses/Set";
import { Clause } from "./Clause";
import type { NodeRef } from "../references/NodeRef";
import { compileCypherIfExists } from "../utils/compile-cypher-if-exists";
import { Clause } from "./Clause";
import { WithPathAssign } from "./mixins/WithPathAssign";
import { WithReturn } from "./mixins/clauses/WithReturn";
import { mixin } from "./utils/mixin";
import { WithSet } from "./mixins/sub-clauses/WithSet";
import { WithPathAssign } from "./mixins/WithPathAssign";
import { SetClause } from "./sub-clauses/Set";
import { mixin } from "./utils/mixin";

export interface Create extends WithReturn, WithSet, WithPathAssign {}

Expand Down Expand Up @@ -55,8 +55,7 @@ export class Create extends Clause {
const patternCypher = this.pattern.getCypher(env);

const setCypher = compileCypherIfExists(this.setSubClause, env, { prefix: "\n" });
const returnCypher = compileCypherIfExists(this.returnStatement, env, { prefix: "\n" });

return `CREATE ${pathCypher}${patternCypher}${setCypher}${returnCypher}`;
const nextClause = this.compileNextClause(env);
return `CREATE ${pathCypher}${patternCypher}${setCypher}${nextClause}`;
}
}
20 changes: 9 additions & 11 deletions src/clauses/Foreach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,17 @@
*/

import type { CypherEnvironment } from "../Environment";
import type { Variable } from "../references/Variable";
import type { Expr } from "../types";
import { padBlock } from "../utils/pad-block";
import { Clause } from "./Clause";
import type { Create } from "./Create";
import type { Merge } from "./Merge";
import { WithWith } from "./mixins/clauses/WithWith";
import { mixin } from "./utils/mixin";
import type { DeleteClause } from "./sub-clauses/Delete";
import type { SetClause } from "./sub-clauses/Set";
import type { RemoveClause } from "./sub-clauses/Remove";
import { padBlock } from "../utils/pad-block";
import { compileCypherIfExists } from "../utils/compile-cypher-if-exists";
import type { Create } from "./Create";
import type { Merge } from "./Merge";
import type { Variable } from "../references/Variable";
import type { Expr } from "../types";
import type { SetClause } from "./sub-clauses/Set";
import { mixin } from "./utils/mixin";

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Foreach extends WithWith {}
Expand Down Expand Up @@ -58,10 +57,9 @@ export class Foreach extends Clause {
const variableStr = this.variable.getCypher(env);
const listExpr = this.listExpr.getCypher(env);
const mapClauseStr = this.mapClause.getCypher(env);
const withStr = compileCypherIfExists(this.withStatement, env, { prefix: "\n" });

const nextClause = this.compileNextClause(env);
const foreachStr = [`FOREACH (${variableStr} IN ${listExpr} |`, padBlock(mapClauseStr), `)`].join("\n");

return `${foreachStr}${withStr}`;
return `${foreachStr}${nextClause}`;
}
}
24 changes: 12 additions & 12 deletions src/clauses/Match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,20 @@
* limitations under the License.
*/

import type { CypherEnvironment } from "../Environment";
import { Pattern } from "../pattern/Pattern";
import { Clause } from "./Clause";
import type { NodeRef } from "../references/NodeRef";
import type { PropertyRef } from "../references/PropertyRef";
import { compileCypherIfExists } from "../utils/compile-cypher-if-exists";
import { Clause } from "./Clause";
import { WithPathAssign } from "./mixins/WithPathAssign";
import { WithReturn } from "./mixins/clauses/WithReturn";
import { mixin } from "./utils/mixin";
import { WithWhere } from "./mixins/sub-clauses/WithWhere";
import { WithSet } from "./mixins/sub-clauses/WithSet";
import { WithWith } from "./mixins/clauses/WithWith";
import { WithPathAssign } from "./mixins/WithPathAssign";
import type { PropertyRef } from "../references/PropertyRef";
import { RemoveClause } from "./sub-clauses/Remove";
import type { CypherEnvironment } from "../Environment";
import type { NodeRef } from "../references/NodeRef";
import { WithDelete } from "./mixins/sub-clauses/WithDelete";
import { WithSet } from "./mixins/sub-clauses/WithSet";
import { WithWhere } from "./mixins/sub-clauses/WithWhere";
import { RemoveClause } from "./sub-clauses/Remove";
import { mixin } from "./utils/mixin";

export interface Match extends WithReturn, WithWhere, WithSet, WithWith, WithPathAssign, WithDelete {}

Expand Down Expand Up @@ -81,14 +81,14 @@ export class Match extends Clause {
const patternCypher = this.pattern.getCypher(env);

const whereCypher = compileCypherIfExists(this.whereSubClause, env, { prefix: "\n" });
const returnCypher = compileCypherIfExists(this.returnStatement, env, { prefix: "\n" });

const nextClause = this.compileNextClause(env);
const setCypher = compileCypherIfExists(this.setSubClause, env, { prefix: "\n" });
const withCypher = compileCypherIfExists(this.withStatement, env, { prefix: "\n" });
const deleteCypher = compileCypherIfExists(this.deleteClause, env, { prefix: "\n" });
const removeCypher = compileCypherIfExists(this.removeClause, env, { prefix: "\n" });
const optionalMatch = this._optional ? "OPTIONAL " : "";

return `${optionalMatch}MATCH ${pathAssignStr}${patternCypher}${whereCypher}${setCypher}${removeCypher}${deleteCypher}${withCypher}${returnCypher}`;
return `${optionalMatch}MATCH ${pathAssignStr}${patternCypher}${whereCypher}${setCypher}${removeCypher}${deleteCypher}${nextClause}`;
}
}

Expand Down
16 changes: 8 additions & 8 deletions src/clauses/Merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@

import type { CypherEnvironment } from "../Environment";
import { Pattern } from "../pattern/Pattern";
import type { NodeRef } from "../references/NodeRef";
import { compileCypherIfExists } from "../utils/compile-cypher-if-exists";
import { Clause } from "./Clause";
import { WithPathAssign } from "./mixins/WithPathAssign";
import { WithReturn } from "./mixins/clauses/WithReturn";
import { WithDelete } from "./mixins/sub-clauses/WithDelete";
import { WithSet } from "./mixins/sub-clauses/WithSet";
import type { OnCreateParam } from "./sub-clauses/OnCreate";
import { OnCreate } from "./sub-clauses/OnCreate";
import { WithReturn } from "./mixins/clauses/WithReturn";
import { mixin } from "./utils/mixin";
import { WithSet } from "./mixins/sub-clauses/WithSet";
import { compileCypherIfExists } from "../utils/compile-cypher-if-exists";
import type { NodeRef } from "../references/NodeRef";
import { WithPathAssign } from "./mixins/WithPathAssign";
import { WithDelete } from "./mixins/sub-clauses/WithDelete";

export interface Merge extends WithReturn, WithSet, WithPathAssign, WithDelete {}

Expand Down Expand Up @@ -66,9 +66,9 @@ export class Merge extends Clause {
const mergeStr = `MERGE ${pathAssignStr}${this.pattern.getCypher(env)}`;
const setCypher = compileCypherIfExists(this.setSubClause, env, { prefix: "\n" });
const onCreateStr = compileCypherIfExists(this.onCreateClause, env, { prefix: "\n" });
const returnStr = compileCypherIfExists(this.returnStatement, env, { prefix: "\n" });
const deleteStr = compileCypherIfExists(this.deleteClause, env, { prefix: "\n" });
const nextClause = this.compileNextClause(env);

return `${mergeStr}${setCypher}${onCreateStr}${deleteStr}${returnStr}`;
return `${mergeStr}${setCypher}${onCreateStr}${deleteStr}${nextClause}`;
}
}
11 changes: 6 additions & 5 deletions src/clauses/Unwind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
*/

import type { CypherEnvironment } from "../Environment";
import type { ProjectionColumn } from "./sub-clauses/Projection";
import { Projection } from "./sub-clauses/Projection";
import { compileCypherIfExists } from "../utils/compile-cypher-if-exists";
import { Clause } from "./Clause";
import { WithWith } from "./mixins/clauses/WithWith";
import { mixin } from "./utils/mixin";
import { WithDelete } from "./mixins/sub-clauses/WithDelete";
import type { ProjectionColumn } from "./sub-clauses/Projection";
import { Projection } from "./sub-clauses/Projection";
import { mixin } from "./utils/mixin";

export interface Unwind extends WithWith, WithDelete {}

Expand All @@ -48,8 +48,9 @@ export class Unwind extends Clause {
/** @internal */
public getCypher(env: CypherEnvironment): string {
const projectionStr = this.projection.getCypher(env);
const withStr = compileCypherIfExists(this.withStatement, env, { prefix: "\n" });
const deleteStr = compileCypherIfExists(this.deleteClause, env, { prefix: "\n" });
return `UNWIND ${projectionStr}${deleteStr}${withStr}`;
const nextClause = this.compileNextClause(env);

return `UNWIND ${projectionStr}${deleteStr}${nextClause}`;
}
}
8 changes: 6 additions & 2 deletions src/clauses/With.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,20 @@ export class With extends Clause {
public getCypher(env: CypherEnvironment): string {
const projectionStr = this.projection.getCypher(env);
const orderByStr = compileCypherIfExists(this.orderByStatement, env, { prefix: "\n" });
const returnStr = compileCypherIfExists(this.returnStatement, env, { prefix: "\n" });
const withStr = compileCypherIfExists(this.withStatement, env, { prefix: "\n" });
const whereStr = compileCypherIfExists(this.whereSubClause, env, { prefix: "\n" });
const deleteStr = compileCypherIfExists(this.deleteClause, env, { prefix: "\n" });
const distinctStr = this.isDistinct ? " DISTINCT" : "";

return `WITH${distinctStr} ${projectionStr}${whereStr}${orderByStr}${deleteStr}${withStr}${returnStr}`;
const nextClause = this.compileNextClause(env);

return `WITH${distinctStr} ${projectionStr}${whereStr}${orderByStr}${deleteStr}${withStr}${nextClause}`;
}

// Cannot be part of WithWith due to dependency cycles
/** Add a {@link With} clause
* @see [Cypher Documentation](https://neo4j.com/docs/cypher-manual/current/clauses/with/)
*/
public with(...columns: ("*" | WithProjection)[]): With {
if (this.withStatement) {
// This behaviour of `.with` is deprecated, use `.addColumns` instead
Expand Down
3 changes: 3 additions & 0 deletions src/clauses/mixins/Mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
* limitations under the License.
*/

import { Clause } from "../../clauses/Clause";
import { CypherASTNode } from "../../CypherASTNode";

/**
* Superclass of all mixins in CypherBuilder
*/
export abstract class Mixin extends CypherASTNode {}

export abstract class MixinClause extends Clause {}
37 changes: 14 additions & 23 deletions src/clauses/mixins/clauses/WithReturn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,30 @@
* limitations under the License.
*/

import type { ProjectionColumn } from "../../sub-clauses/Projection";
import { Return } from "../../Return";
import { Mixin } from "../Mixin";

export abstract class WithReturn extends Mixin {
protected returnStatement: Return | undefined;
import type { ProjectionColumn } from "../../sub-clauses/Projection";
import { MixinClause } from "../Mixin";

export abstract class WithReturn extends MixinClause {
/** Append a {@link Return} clause
* @see [Cypher Documentation](https://neo4j.com/docs/cypher-manual/current/clauses/return/)
*/
public return(clause: Return): Return;
public return(...columns: Array<"*" | ProjectionColumn>): Return;
public return(clauseOrColumn: Return | "*" | ProjectionColumn, ...columns: Array<"*" | ProjectionColumn>): Return {
if (clauseOrColumn instanceof Return) {
return this.addReturnStatement(clauseOrColumn);
}

return this.addColumnsToReturnClause(clauseOrColumn, ...columns);
const returnClause = this.getReturnClause(clauseOrColumn, columns);
this.addNextClause(returnClause);
return returnClause;
}

private addColumnsToReturnClause(...columns: Array<"*" | ProjectionColumn>): Return {
let returnStatement = this.returnStatement;
if (!returnStatement) {
returnStatement = this.addReturnStatement(new Return());
private getReturnClause(
clauseOrColumn: Return | "*" | ProjectionColumn,
columns: Array<"*" | ProjectionColumn>
): Return {
if (clauseOrColumn instanceof Return) {
return clauseOrColumn;
} else {
return new Return(clauseOrColumn, ...columns);
}

returnStatement.addColumns(...columns);
return returnStatement;
}

private addReturnStatement(clause: Return): Return {
this.returnStatement = clause;
this.addChildren(this.returnStatement);
return clause;
}
}
Loading

0 comments on commit 3648e40

Please sign in to comment.