Skip to content

Commit

Permalink
feat: add callProcedure method to With and Match clauses (#346)
Browse files Browse the repository at this point in the history
  • Loading branch information
mjfwebb authored May 28, 2024
1 parent 589ddba commit 65661c3
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 18 deletions.
9 changes: 9 additions & 0 deletions .changeset/afraid-ears-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@neo4j/cypher-builder": patch
---

Add callProcedure method to With and Match clauses

```js
const withQuery = new Cypher.With("*").callProcedure(Cypher.db.labels()).yield("label");
```
8 changes: 8 additions & 0 deletions docs/modules/ROOT/pages/procedures.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ const { cypher, params } = dbLabels.build();

Cypher Builder has several built-in procedures such as `db.*` procedures that can be called as JavaScript functions.

You can add procedures following other clauses using the `callProcedure` method:

[source, javascript]
----
const withQuery = new Cypher.With("*").callProcedure(Cypher.db.labels()).yield("label");
const { cypher, params } = withQuery.build();
----

== Custom Procedures

Most procedures depend on the Neo4j setup, and such need to be defined with the `Procedure` class:
Expand Down
30 changes: 30 additions & 0 deletions src/clauses/Match.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,36 @@ WHERE this0.title = $param1"
"param0": "movie1",
"param1": "movie2",
}
`);
});
});

describe("Match.callProcedure", () => {
test("Match.match()", () => {
const movie1 = new Cypher.Node({
labels: ["Movie"],
});

const labels = new Cypher.Variable();

const matchQuery = new Cypher.Match(movie1)
.where(Cypher.eq(movie1.property("title"), new Cypher.Param("movie1")))
.callProcedure(new Cypher.Procedure("db.labels"))
.yield(["label", labels])
.return(labels);

const queryResult = matchQuery.build();
expect(queryResult.cypher).toMatchInlineSnapshot(`
"MATCH (this0:Movie)
WHERE this0.title = $param0
CALL db.labels() YIELD label AS var1
RETURN var1"
`);

expect(queryResult.params).toMatchInlineSnapshot(`
{
"param0": "movie1",
}
`);
});
});
Expand Down
7 changes: 5 additions & 2 deletions src/clauses/Match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { NodeRef } from "../references/NodeRef";
import { compileCypherIfExists } from "../utils/compile-cypher-if-exists";
import { Clause } from "./Clause";
import { WithPathAssign } from "./mixins/WithPathAssign";
import { WithCallProcedure } from "./mixins/clauses/WithCallProcedure";
import { WithCreate } from "./mixins/clauses/WithCreate";
import { WithFinish } from "./mixins/clauses/WithFinish";
import { WithMerge } from "./mixins/clauses/WithMerge";
Expand All @@ -46,7 +47,8 @@ export interface Match
WithUnwind,
WithCreate,
WithMerge,
WithFinish {}
WithFinish,
WithCallProcedure {}

/**
* @see [Cypher Documentation](https://neo4j.com/docs/cypher-manual/current/clauses/match/)
Expand All @@ -63,7 +65,8 @@ export interface Match
WithUnwind,
WithCreate,
WithMerge,
WithFinish
WithFinish,
WithCallProcedure
)
export class Match extends Clause {
private pattern: Pattern;
Expand Down
24 changes: 24 additions & 0 deletions src/clauses/With.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,30 @@ MERGE (this0:Movie)"
expect(queryResult.cypher).toMatchInlineSnapshot(`
"WITH *
MERGE (this0:Movie)"
`);
expect(queryResult.params).toMatchInlineSnapshot(`{}`);
});
});

describe("Chained Procedure", () => {
test("With * and cypher procedure", () => {
const withQuery = new Cypher.With("*").callProcedure(Cypher.db.labels()).yield("label");

const queryResult = withQuery.build();
expect(queryResult.cypher).toMatchInlineSnapshot(`
"WITH *
CALL db.labels() YIELD label"
`);
expect(queryResult.params).toMatchInlineSnapshot(`{}`);
});

test("With * and cypher void procedure", () => {
const withQuery = new Cypher.With("*").callProcedure(Cypher.apoc.util.validate(Cypher.true, "message"));

const queryResult = withQuery.build();
expect(queryResult.cypher).toMatchInlineSnapshot(`
"WITH *
CALL apoc.util.validate(true, \\"message\\", [0])"
`);
expect(queryResult.params).toMatchInlineSnapshot(`{}`);
});
Expand Down
34 changes: 18 additions & 16 deletions src/clauses/With.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { Variable } from "../references/Variable";
import type { Expr } from "../types";
import { compileCypherIfExists } from "../utils/compile-cypher-if-exists";
import { Clause } from "./Clause";
import { WithCallProcedure } from "./mixins/clauses/WithCallProcedure";
import { WithCreate } from "./mixins/clauses/WithCreate";
import { WithMatch } from "./mixins/clauses/WithMatch";
import { WithMerge } from "./mixins/clauses/WithMerge";
Expand All @@ -45,13 +46,14 @@ export interface With
WithMatch,
WithUnwind,
WithCreate,
WithMerge {}
WithMerge,
WithCallProcedure {}

/**
* @see [Cypher Documentation](https://neo4j.com/docs/cypher-manual/current/clauses/with/)
* @category Clauses
*/
@mixin(WithOrder, WithReturn, WithWhere, WithDelete, WithMatch, WithUnwind, WithCreate, WithMerge)
@mixin(WithOrder, WithReturn, WithWhere, WithDelete, WithMatch, WithUnwind, WithCreate, WithMerge, WithCallProcedure)
export class With extends Clause {
private projection: Projection;
private isDistinct = false;
Expand All @@ -72,20 +74,6 @@ export class With extends Clause {
return this;
}

/** @internal */
public getCypher(env: CypherEnvironment): string {
const projectionStr = this.projection.getCypher(env);
const orderByStr = compileCypherIfExists(this.orderByStatement, 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" : "";

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/)
Expand All @@ -100,4 +88,18 @@ export class With extends Clause {
}
return this.withStatement;
}

/** @internal */
public getCypher(env: CypherEnvironment): string {
const projectionStr = this.projection.getCypher(env);
const orderByStr = compileCypherIfExists(this.orderByStatement, 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" : "";

const nextClause = this.compileNextClause(env);

return `WITH${distinctStr} ${projectionStr}${whereStr}${orderByStr}${deleteStr}${withStr}${nextClause}`;
}
}
31 changes: 31 additions & 0 deletions src/clauses/mixins/clauses/WithCallProcedure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { CypherProcedure, VoidCypherProcedure } from "../../../procedures/CypherProcedure";
import { MixinClause } from "../Mixin";

export abstract class WithCallProcedure extends MixinClause {
/** Add a call {@link Procedure} clause
* @see [Cypher Documentation](https://neo4j.com/docs/cypher-manual/current/clauses/call/)
*/
public callProcedure<T extends CypherProcedure | VoidCypherProcedure>(procedure: T): T {
this.addNextClause(procedure);
return procedure;
}
}

0 comments on commit 65661c3

Please sign in to comment.