diff --git a/.changeset/afraid-ears-chew.md b/.changeset/afraid-ears-chew.md new file mode 100644 index 00000000..884ba409 --- /dev/null +++ b/.changeset/afraid-ears-chew.md @@ -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"); +``` diff --git a/docs/modules/ROOT/pages/procedures.adoc b/docs/modules/ROOT/pages/procedures.adoc index 7a8963d8..68612701 100644 --- a/docs/modules/ROOT/pages/procedures.adoc +++ b/docs/modules/ROOT/pages/procedures.adoc @@ -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: diff --git a/src/clauses/Match.test.ts b/src/clauses/Match.test.ts index b1ff93a1..a339b644 100644 --- a/src/clauses/Match.test.ts +++ b/src/clauses/Match.test.ts @@ -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", +} `); }); }); diff --git a/src/clauses/Match.ts b/src/clauses/Match.ts index 9a6c3510..5033e37f 100644 --- a/src/clauses/Match.ts +++ b/src/clauses/Match.ts @@ -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"; @@ -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/) @@ -63,7 +65,8 @@ export interface Match WithUnwind, WithCreate, WithMerge, - WithFinish + WithFinish, + WithCallProcedure ) export class Match extends Clause { private pattern: Pattern; diff --git a/src/clauses/With.test.ts b/src/clauses/With.test.ts index d59dfa4f..33b313c8 100644 --- a/src/clauses/With.test.ts +++ b/src/clauses/With.test.ts @@ -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(`{}`); }); diff --git a/src/clauses/With.ts b/src/clauses/With.ts index 737a8c3e..bf15d29e 100644 --- a/src/clauses/With.ts +++ b/src/clauses/With.ts @@ -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"; @@ -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; @@ -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/) @@ -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}`; + } } diff --git a/src/clauses/mixins/clauses/WithCallProcedure.ts b/src/clauses/mixins/clauses/WithCallProcedure.ts new file mode 100644 index 00000000..443b7303 --- /dev/null +++ b/src/clauses/mixins/clauses/WithCallProcedure.ts @@ -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(procedure: T): T { + this.addNextClause(procedure); + return procedure; + } +}