diff --git a/.changeset/all-signs-live.md b/.changeset/all-signs-live.md new file mode 100644 index 00000000..a16fa26e --- /dev/null +++ b/.changeset/all-signs-live.md @@ -0,0 +1,5 @@ +--- +"@neo4j/cypher-builder": major +--- + +Remove type `Cypher.Operation` in favor of `Cypher.Expr` diff --git a/.changeset/blue-ends-relate.md b/.changeset/blue-ends-relate.md new file mode 100644 index 00000000..449b7e2a --- /dev/null +++ b/.changeset/blue-ends-relate.md @@ -0,0 +1,11 @@ +--- +"@neo4j/cypher-builder": patch +--- + +Add support for `Cypher 25` version prefix: + +```js +const { cypher } = matchQuery.build({ + cypherVersion: "25", +}); +``` diff --git a/.changeset/bright-meals-arrive.md b/.changeset/bright-meals-arrive.md new file mode 100644 index 00000000..2b00ce13 --- /dev/null +++ b/.changeset/bright-meals-arrive.md @@ -0,0 +1,5 @@ +--- +"@neo4j/cypher-builder": major +--- + +ListComprehension `.in` method no longer throws if called twice. It will instead override the expression diff --git a/.changeset/clean-items-greet.md b/.changeset/clean-items-greet.md new file mode 100644 index 00000000..3ec4da9c --- /dev/null +++ b/.changeset/clean-items-greet.md @@ -0,0 +1,23 @@ +--- +"@neo4j/cypher-builder": major +--- + +Remove second parameter of `ListComprehension` in favor of `.in` + +_Before_ + +```js +new Cypher.ListComprehension(variable, new Cypher.Literal([1, 2])); +``` + +_After_ + +```js +new Cypher.ListComprehension(variable).in(new Cypher.Literal([1, 2])); +``` + +In both cases, the same comprehension will be generated: + +```cypher +[var0 IN [1, 2]] +``` diff --git a/.changeset/common-cycles-obey.md b/.changeset/common-cycles-obey.md new file mode 100644 index 00000000..e26895e4 --- /dev/null +++ b/.changeset/common-cycles-obey.md @@ -0,0 +1,13 @@ +--- +"@neo4j/cypher-builder": major +--- + +Remove support for patterns in size. + +_No longer supported_ + +```js +Cypher.size(new Cypher.Pattern(node)); +``` + +Use `new Cypher.Count(pattern)` instead. diff --git a/.changeset/cool-signs-start.md b/.changeset/cool-signs-start.md new file mode 100644 index 00000000..9e6f18f6 --- /dev/null +++ b/.changeset/cool-signs-start.md @@ -0,0 +1,5 @@ +--- +"@neo4j/cypher-builder": major +--- + +Updates minimum node engine to 20.0.0 diff --git a/.changeset/easy-horses-repeat.md b/.changeset/easy-horses-repeat.md new file mode 100644 index 00000000..4f2a828d --- /dev/null +++ b/.changeset/easy-horses-repeat.md @@ -0,0 +1,25 @@ +--- +"@neo4j/cypher-builder": major +--- + +Remove labelOperator, all labels will now use operator `&` + +No longer supported: + +```js +const { cypher, params } = matchQuery.build({ + labelOperator: "&", +}); +``` + +_Before_ + +```cypher +MATCH (this1:Movie:Film) +``` + +_After_ + +```cypher +MATCH (this1:Movie&Film) +``` diff --git a/.changeset/fine-roses-film.md b/.changeset/fine-roses-film.md new file mode 100644 index 00000000..b6edc1f8 --- /dev/null +++ b/.changeset/fine-roses-film.md @@ -0,0 +1,30 @@ +--- +"@neo4j/cypher-builder": major +--- + +Remove all apoc functions and procedures: + +- `apoc.util.validate` +- `apoc.util.validatePredicate` +- `apoc.date.convertFormat` +- `apoc.cypher.runFirstColumnMany` +- `apoc.cypher.runFirstColumnSingle` + +In order to use apoc methods, create a custom function or procedure. For example: + +```js +function validate( + predicate: Predicate, + message: string, + params: List | Literal | Map +): VoidCypherProcedure { + return new VoidCypherProcedure("apoc.util.validate", [predicate, new Literal(message), params]); +} +``` + +```js +function validatePredicate(predicate: Predicate, message: string): CypherFunction { + return new CypherFunction("apoc.util.validatePredicate", [predicate, new Literal(message), new Literal([0])]); +} + +``` diff --git a/.changeset/floppy-llamas-teach.md b/.changeset/floppy-llamas-teach.md new file mode 100644 index 00000000..6c8b0d05 --- /dev/null +++ b/.changeset/floppy-llamas-teach.md @@ -0,0 +1,10 @@ +--- +"@neo4j/cypher-builder": major +--- + +Remove method `.children` from concat clauses: + +```js +const query = Cypher.utils.concat(clause1, clause2); +query.children; // No longer supported +``` diff --git a/.changeset/sharp-women-kick.md b/.changeset/sharp-women-kick.md new file mode 100644 index 00000000..dba542d5 --- /dev/null +++ b/.changeset/sharp-women-kick.md @@ -0,0 +1,30 @@ +--- +"@neo4j/cypher-builder": major +--- + +Remove `.importWith` from `Call` clauses in favor of constructor options + +_Before_ + +```js +const clause = new Cypher.Call(nestedClause).importWith(movieNode, actorNode); +``` + +```cypher +CALL { + WITH var0, var1 + // Nested clause +} +``` + +_After_ + +```js +const clause = new Cypher.Call(nestedClause, [movieNode, actorNode]); +``` + +```cypher +CALL (var0, var1){ + // Nested clause +} +``` diff --git a/.changeset/thin-radios-poke.md b/.changeset/thin-radios-poke.md new file mode 100644 index 00000000..bcbc0013 --- /dev/null +++ b/.changeset/thin-radios-poke.md @@ -0,0 +1,8 @@ +--- +"@neo4j/cypher-builder": major +--- + +Remove functions deprecated in Cypher 5: + +- `distance` in favor of `point.distance` +- `id` in favor of `elementId` diff --git a/.changeset/violet-sloths-hug.md b/.changeset/violet-sloths-hug.md new file mode 100644 index 00000000..f2a964f2 --- /dev/null +++ b/.changeset/violet-sloths-hug.md @@ -0,0 +1,45 @@ +--- +"@neo4j/cypher-builder": major +--- + +Remove extra parameters in `Cypher.Foreach` in favor of methods `in` and `do`. + +For example, to create the following Cypher: + +```cypher +FOREACH (var0 IN [1, 2, 3] | + CREATE (this1:Movie) + SET + this1.id = var0 + ) +``` + +_before_ + +```js +const list = new Cypher.Literal([1, 2, 3]); +const variable = new Cypher.Variable(); + +const movieNode = new Cypher.Node(); +const createMovie = new Cypher.Create(new Cypher.Pattern(movieNode, { labels: ["Movie"] })).set([ + movieNode.property("id"), + variable, +]); + +const foreachClause = new Cypher.Foreach(variable, list, createMovie); +``` + +_after_ + +```js +const list = new Cypher.Literal([1, 2, 3]); +const variable = new Cypher.Variable(); + +const movieNode = new Cypher.Node(); +const createMovie = new Cypher.Create(new Cypher.Pattern(movieNode, { labels: ["Movie"] })).set([ + movieNode.property("id"), + variable, +]); + +const foreachClause = new Cypher.Foreach(variable).in(list).do(createMovie); +``` diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6dfdeb2..c6b6f334 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [16.x, latest] + node-version: [20.x, latest] steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 diff --git a/docs/modules/ROOT/content-nav.adoc b/docs/modules/ROOT/content-nav.adoc index a1623fcd..e3a9047d 100644 --- a/docs/modules/ROOT/content-nav.adoc +++ b/docs/modules/ROOT/content-nav.adoc @@ -44,6 +44,6 @@ ** xref:how-to/update-labels.adoc[] ** xref:how-to/define-cypher-version.adoc[] * xref:compatibility.adoc[] -* xref:migration-guide-2.adoc[] +* xref:migration-guide-3.adoc[] * link:https://github.com/neo4j/cypher-builder/tree/main/examples[Examples] * link:https://neo4j.github.io/cypher-builder/reference/[Reference] diff --git a/docs/modules/ROOT/pages/build-configuration.adoc b/docs/modules/ROOT/pages/build-configuration.adoc index 6483ad9c..19f96479 100644 --- a/docs/modules/ROOT/pages/build-configuration.adoc +++ b/docs/modules/ROOT/pages/build-configuration.adoc @@ -14,8 +14,7 @@ The method `build` accepts an object with options to configure the build process [cols="2,1,1,3",options="header"] |=== | Setting | Type | Default | Description -| `labelOperator` | String | `":"` | Define the default operator to use when using multiple labels in a Pattern. It can be either `:` or `&` -| `cypherVersion` | String | `undefined` | If set, will prepend all queries with a `CYPHER [Version]` statement. The only valid value is `"5"` +| `cypherVersion` | String | `undefined` | If set, will prepend all queries with a `CYPHER [Version]` statement. The possible values are `"5"` and `"25"` | `prefix` | String | `undefined` | If set, will add a prefix to all variable names. See xref:how-to/customize-cypher.adoc#_build_prefix[Customize Cypher] | `extraParams` | Object | `{}` | Add the provided parameters to the resulting params object. See xref:variables-and-params/parameters.adoc#_adding_extra_parameters[Parameters] | `unsafeEscapeOptions` | Object | `{}` | Set of options to disable escaping from generated Cypher. See xref:_unsafe_escape_options[Unsafe escape options] diff --git a/docs/modules/ROOT/pages/clauses/with.adoc b/docs/modules/ROOT/pages/clauses/with.adoc index 3e309f6a..5977f7d0 100644 --- a/docs/modules/ROOT/pages/clauses/with.adoc +++ b/docs/modules/ROOT/pages/clauses/with.adoc @@ -2,7 +2,7 @@ :description: This page describes how to create `WITH` clauses. = With -This page describes how to create a link:https://neo4j.com/docs/cypher-manual/current/clauses/with/[`WITH`] clause using the `Cypher.With` class. Note that this is different to the xref:../subqueries/call.adoc#_importwith[ImportWith] statement inside `CALL`. +This page describes how to create a link:https://neo4j.com/docs/cypher-manual/current/clauses/with/[`WITH`] clause using the `Cypher.With` class. A `With` clause will take multiple parameters. Variables can be passed directly without aliasing: diff --git a/docs/modules/ROOT/pages/compatibility.adoc b/docs/modules/ROOT/pages/compatibility.adoc index d6e69c41..14be8ea3 100644 --- a/docs/modules/ROOT/pages/compatibility.adoc +++ b/docs/modules/ROOT/pages/compatibility.adoc @@ -1,13 +1,13 @@ [[compatibility]] -:description: This page outlines the compatibility requirements for `@neo4j/cypher-builder` version 2 with Cypher and Node.js. +:description: This page outlines the compatibility requirements for `@neo4j/cypher-builder` version 3 with Cypher and Node.js. = Compatibility -This page outlines the compatibility requirements for `@neo4j/cypher-builder` version 2 with Cypher and Node.js. +This page outlines the compatibility requirements for `@neo4j/cypher-builder` version 3 with Cypher and Node.js. -* Targets link:https://neo4j.com/docs/cypher-manual/5/introduction/[Cypher 5], with support for link:https://neo4j.com/docs/cypher-manual/4.4/introduction/[Cypher 4.4]. -* Compatible with Node.js version 16.0.0 and later. +* Targets link:https://neo4j.com/docs/cypher-manual/25/introduction/[Cypher 25], with support for link:https://neo4j.com/docs/cypher-manual/5/introduction/[Cypher 5]. +* Compatible with Node.js version 20.0.0 and later. [NOTE] ==== -`@neo4j/cypher-builder` version 1 is no longer supported. Version 2 is a full replacement and maintains the same compatibility with Cypher and Node.js. +If you need to support Neo4j 4, stay with Cypher Builder 2.x, otherwise, xref:migration-guide-3.adoc#migration[migrate to version 3]. ==== diff --git a/docs/modules/ROOT/pages/getting-started/installation.adoc b/docs/modules/ROOT/pages/getting-started/installation.adoc index 55ec1863..c292809f 100644 --- a/docs/modules/ROOT/pages/getting-started/installation.adoc +++ b/docs/modules/ROOT/pages/getting-started/installation.adoc @@ -6,7 +6,7 @@ This guide shows how to start using Cypher Builder by setting up a Node.js proje == Requirements -* link:https://nodejs.org/[Node.js] 16.0.0 or greater +* link:https://nodejs.org/[Node.js] 20.0.0 or greater * link:https://docs.npmjs.com/downloading-and-installing-node-js-and-npm[npm] * **[Optional]** A link:https://neo4j.com/cloud/platform/aura-graph-database/?ref=nav-get-started-cta[Neo4j] database to try Cypher queries. diff --git a/docs/modules/ROOT/pages/how-to/define-cypher-version.adoc b/docs/modules/ROOT/pages/how-to/define-cypher-version.adoc index cd7a5af3..34a6f8b6 100644 --- a/docs/modules/ROOT/pages/how-to/define-cypher-version.adoc +++ b/docs/modules/ROOT/pages/how-to/define-cypher-version.adoc @@ -2,7 +2,7 @@ :description: This page describes how to define the Cypher version to be used in the query. = Define Cypher version -It is possible to define the version of Cypher to use in a query by prepending that query with `CYPHER [version]`. For example: +It is possible to explicitly define the version of Cypher to use in a query by prepending that query with `CYPHER [version]`. For example: [source, cypher] @@ -13,7 +13,7 @@ RETURN this0 ---- -To add the Cypher version at the beggining of the query, pass the parameter `cypherVersion` to `.build`: +To add the Cypher version at the beggining of the query, pass the parameter `cypherVersion` to `.build`. The possible values are `"5"` and `"25"`: [source, javascript] ---- diff --git a/docs/modules/ROOT/pages/migration-guide-2.adoc b/docs/modules/ROOT/pages/migration-guide-2.adoc deleted file mode 100644 index eccec4dc..00000000 --- a/docs/modules/ROOT/pages/migration-guide-2.adoc +++ /dev/null @@ -1,450 +0,0 @@ -[[migration]] -:description: This page describes how to migrate to version 2.x -= Migration to Cypher Builder 2 - -This guide describes the changes required to migrate from Cypher Builder 1.x to 2.0, as well as some breaking changes that may affect projects using Cypher Builder. - -The full, up to date changelog can be found link:https://github.com/neo4j/cypher-builder/blob/main/CHANGELOG.md[here]. - -== Patterns - -xref:patterns.adoc[Patterns] have been reworked in version 1.x, and the old behavior have been removed in version 2.0. - - -=== Node shorthand removed from clauses - -Clauses using Patterns no longer accept a `Cypher.Node` as a shorthand. An explicit pattern must be provided: - - -Before: -[source, javascript] ----- -const movieNode = new Cypher.Node(); - -const matchQuery = new Cypher.Match(movieNode) ----- - - -Now: -[source, javascript] ----- -const movieNode = new Cypher.Node(); - -const matchQuery = new Cypher.Match( - new Cypher.Pattern(movieNode) -); ----- - -This affects all clauses using patterns: - -* `Cypher.Match` -* `Cypher.Create` -* `Cypher.Merge` - -This also affects pattern comprehensions `Cypher.PatternComprehension`. - - -=== Patterns no longer create a variable by default - -Creating a Pattern without options will no longer add variables to the generated Cypher: - -[source, javascript] ----- -const pattern = new Cypher.Pattern() ----- - -Before - -[source, cypher] ----- -(this0) ----- - -Now - -[source, cypher] ----- -() ----- - -To add a variable, it must be passed when creating the pattern: - -[source, javascript] ----- -const movieNode = new Cypher.Node(); -const pattern = new Cypher.Pattern(movieNode) ----- - - -==== `.getVariables` - -The method `.getVariables` has been removed from Patterns, as Patterns no longer create variables, `getVariables` is no longer useful. - -=== Patterns configuration - -The following methods to configure the resulting pattern have been removed from `Pattern`: - -- `.withoutLabels` -- `.withoutVariable` -- `.withProperties` -- `.withVariables` -- `.withoutType` -- `.withDirection` -- `.withLength` - -Instead, Patterns are now configured through objects in the constructor, as well as the `related` and `to` methods: - -Before -[source, javascript] ----- -const a = new Cypher.Node({ - labels: ["Person", "Actor"], -}); - -const aProperties = { - name: new Cypher.Param("Arthur"), - surname: new Cypher.Param("Dent"), -}; -const b = new Cypher.Node(); -const rel = new Cypher.Relationship({ - type: "ACTED_IN", -}); - -new Cypher.Pattern(a) - .withProperties({a: new Cypher.Param("Example")}) - .withoutVariable() - .related(rel) - .to(b) ----- - -Now - -[source, javascript] ----- -const aProperties = { - name: new Cypher.Param("Arthur"), - surname: new Cypher.Param("Dent"), -}; -const b = new Cypher.Node(); -const rel = new Cypher.Relationship(); - - -new Cypher.Pattern({ properties: aProperties, labels: ["Person", "Actor"] }) - .related(rel, { type: "ACTED_IN"}) - .to(b) ----- - - -The generated Cypher: - -[source, Cypher] ----- -(:Person:Actor { name: $param0, surname: $param1 })-[this1:ACTED_IN]->(this2) ----- - -Note that labels and types are now defined in the Pattern, not in the `Node` and `Relationship` classes. - -=== Assign to path variable - -The method `assignToPath` has been removed in the following clauses: - -- `Match` -- `Merge` -- `Create` - -Instead, the method `assignTo` in `Patterns` must be used: - -Before: - -```js -const pathVariable = new Cypher.Cypher.PathVariable() -new Cypher.Match(pattern).assignToPath(pathVariable).return(pathVariable); -``` - -Now: - -```js -const pathVariable = new Cypher.Cypher.PathVariable() -new Cypher.Match(pattern.assignTo(pathVariable)).return(pathVariable); -``` - -Generates the Cypher: - -```cypher -MATCH p = ()-[]-() -RETURN p -``` - - -== Node and Relationship variables - -`Cypher.Node` and `Cypher.Relationship` no longer hold any data about labels, or types. Making them more similar to `Cypher.Variable`. To add labels or types, these need to be passed to the `Cypher.Pattern` instead of relying on `Cypher.Node` and `Cypher.Relationship`. - -Before -[source, javascript] ----- -const a = new Cypher.Node({ - labels: ["Person", "Actor"], -}); -const b = new Cypher.Node(); -const rel = new Cypher.Relationship({ - type: "ACTED_IN", -}); - -new Cypher.Pattern(a) - .related(rel) - .to(b) ----- - -Now - -[source, javascript] ----- -const a = new Cypher.Node(); -const b = new Cypher.Node(); -const rel = new Cypher.Relationship(); - - -new Cypher.Pattern(a, { labels: ["Person", "Actor"] }) - .related(rel, { type: "ACTED_IN"}) - .to(b) ----- - -=== Path variables - -The variables used for paths `Cypher.Path` and `Cypher.NamedPath` have been removed in favor of the more accurate names: `Cypher.PathVariable` and `Cypher.NamedPathVariable` - -== Renamed features - -The following features where deprecated in favor of a different name with the same functionality. The deprecated features have been removed in version 2.0: - -* `Cypher.concat` in favor of `Cypher.utils.concat` -* `pointDistance` in favor of `point.distance` -* `Merge.onCreate` in favor of `Merge.onCreateSet` -* `Call.innerWith` in favor of `Call.importWith` -* `cdc` namespace in favor of `db.cdc` -** `db.cdc.current` -** `db.cdc.earliest` -** `db.cdc.query` -* `rTrim` and `lTrim` in favor of `rtrim` and `ltrim` respectively - -== `.build()` - -The options for `.build()` are now passed as a single object rather than parameters: - -Before: -[source, javascript] ----- -myClause.build( - "another-this", - { myParam: "hello"}, - { - labelOperator: "&" - } -); ----- - - -Now: -[source, javascript] ----- -myClause.build({ - prefix: "another-this", - extraParams: { - myParam: "hello", - }, - labelOperator: "&", -}); ----- - -All parameters are optional, and `build` can still be called without parameters. - -=== Remove support for fine-grained prefix - -The first parameter "prefix" for the `.build` method in 1.x supports passing an object with the parameters `params` and `variables` for fine grained control of what prefix to use in different kind of variables. This has been removed in 2.x, supporting only a `string` as global prefix: - -No longer supported: -[source, javascript] ----- -myClause.build({ - variable: "var_prefix_", - params: "param_prefix_" -}); ----- - -Instead, a single string can be used as prefix for both, variables and params: - -Now: -[source, javascript] ----- -myClause.build({ - prefix: "my-custom-prefix" -}); ----- - -== `With` - -The method `.with` no longer adds new columns into the existing clause. It will always create a new `WITH` statement instead. The method `.addColumns` should be used instead to add extra columns. - -Before -[source, javascript] ----- -const withQuery = new Cypher.With(node); -withQuery.with(node); -withQuery.with("*"); ----- - -Now -[source, javascript] ----- -const withQuery = new Cypher.With(node); -withQuery.with(node) -withQuery.addColumns("*"); ----- - - -The generated Cypher: - -[source, cypher] ----- -WITH this0 -WITH *, this0 ----- - -== `RawCypher` - -`Cypher.RawCypher` has been removed in favor of `Cypher.Raw`. - -=== Update callback parameter - -`Cypher.Raw` no longer exposes a `Cypher.Environment` variable. Instead, it provides an instance of `CypherRawContext` with a `compile` method to compile nested elements in custom cypher. - - -Before: -[source, typescript] ----- -const releasedParam = new Cypher.Param(1999); -const rawCypher = new Cypher.Raw((env: Cypher.Environment) => { - const releasedParamId = env.compile(releasedParam); // Gets the raw Cypher for the param - - const customCypher = `MATCH(n) WHERE n.title=$title_param AND n.released=${releasedParamId}`; - - return customCypher; -}); ----- - -Now: -[source, typescript] ----- -const releasedParam = new Cypher.Param(1999); -const rawCypher = new Cypher.Raw((ctx: Cypher.RawCypherContext) => { - const releasedParamId = ctx.compile(releasedParam); // Gets the raw Cypher for the param - - const customCypher = `MATCH(n) WHERE n.title=$title_param AND n.released=${releasedParamId}`; - - return customCypher; -}); ----- - -Note that the code itself has not changed, and just the type passed to `Cypher.Raw` callback has been changed from `Cypher.Environment` to `Cypher.RawCypherContext`. - -=== Remove `utils.compileCypher` - -The utility function `compileCypher` has been removed, in favor of using `CypherRawContext.compile`, which offers the same functionality. - -== `PatternComprehension` - -`PatternComprehension` no longer accept a node as an argument in the constructor, a Pattern must be passed instead: - -Before -[source, javascript] ----- -const node = new Cypher.Node(); -const comprehension = new Cypher.PatternComprehension(node); ----- - -Now -[source, javascript] ----- -const node = new Cypher.Node(); -const comprehension = new Cypher.PatternComprehension(new Cypher.Pattern(node)); ----- - -=== `.map` - -`PatternComprehension` no longer accepts a second argument for the Map expression. The method `.map` must be used instead: - -Before -[source, javascript] ----- -const andExpr = Cypher.eq(node.property("released"), new Cypher.Param(1999)); - -const comprehension = new Cypher.PatternComprehension(new Cypher.Pattern(node), andExpr) ----- - -Now -[source, javascript] ----- -const andExpr = Cypher.eq(node.property("released"), new Cypher.Param(1999)); - -const comprehension = new Cypher.PatternComprehension(new Cypher.Pattern(node)).map(andExpr); ----- - - -== Other Breaking changes - -These are breaking changes that do not require changes, but may affect the behaviour of projects updating to Cypher Builder 2.0. - - -=== Fix TypeScript typings for boolean operators - -The typings for the following boolean operators have been fixed to better reflect the result of these functions when spread parameters are used: - -* `Cypher.and` -* `Cypher.or` -* `Cypher.xor` - -The following: - -[source, typescript] ----- -const predicates: Cypher.Predicate[] = []; -const andPredicate = Cypher.and(...predicates); ----- - -Will now return the correct type `Cypher.Predicate | undefined`. This change means that additional checks may be needed when using boolean operators: - -[source, typescript] ----- -const predicates = [Cypher.true, Cypher.false]; -const andPredicate = Cypher.and(...predicates); // type Cypher.Predicate | undefined ----- - -Passing parameters without spread will still return a defined type. - - -=== Literals escaping - -`Cypher.Literal` will now escape strings if these contain invalid characters. This is to avoid code injection. - - -[source, javascript] ----- -new Cypher.Literal(`Hello "World"`); ----- - -Would generate the following Cypher: - -Before: -[source, cypher] ----- -"Hello "World"" ----- - -Now: -[source, cypher] ----- -"Hello \"World\"" ----- - -Note that `Cypher.Param` is still preferred over `Cypher.Literal` for dynamic values. diff --git a/docs/modules/ROOT/pages/migration-guide-3.adoc b/docs/modules/ROOT/pages/migration-guide-3.adoc new file mode 100644 index 00000000..99f6c230 --- /dev/null +++ b/docs/modules/ROOT/pages/migration-guide-3.adoc @@ -0,0 +1,282 @@ +[[migration]] +:description: This page describes how to migrate to version 3.x from version 2 += Migration to Cypher Builder 3 + +This guide details all the changes needed to migrate from Cypher Builder version 2 to version 3. + +[NOTE] +==== +If you need to support Neo4j 4, stay with Cypher Builder 2.x, otherwise, migrate to version 3. +==== + +== Compatibility changes + +=== Minimum node engine changed to 20.0.0 + +Node.js 16 is no longer supported. Update Node.js to version 20.0.0 or above. + +=== Cypher 4 no longer supported + +Cypher 4, and Neo4j 4 databases are no longer supported. Cypher Builder 3 targets Cypher 5 and 25. + +The following features are Cypher 4 only, and have been removed from Cypher Builder. + +==== Removed functions deprecated in Cypher 5 + +- `distance` in favor of `point.distance` +- `id` in favor of `elementId` + +==== Removed support for patterns in size + +_No longer supported_ +[source, javascript] +---- +Cypher.size(new Cypher.Pattern(node)); +---- + +[source, Cypher] +---- +size((this0)); +---- + +Instead, use `new Cypher.Count`: + +[source, javascript] +---- +new Cypher.Count(new Cypher.Pattern(node)); +---- + +[source, Cypher] +---- +COUNT { + (this0) +} +---- + +==== Removed `labelOperator` + +Removed option `labelOperator`. Now, the operator `&` is used by default when using multiple labels: + + +_No longer supported_ +[source, javascript] +---- +const { cypher, params } = matchQuery.build({ + labelOperator: "&", +}); +---- + + +_Before_ +[source, Cypher] +---- +MATCH (this1:Movie:Film) +---- + +_After_ +[source, Cypher] +---- +MATCH (this1:Movie&Film) +---- + + +==== Removed `.importWith` from `Call` + +Remove `.importWith` from `Call` clauses in favor of constructor options + +_No longer supported_ + +[source, Javascript] +---- +const clause = new Cypher.Call(nestedClause).importWith(movieNode, actorNode); +---- + +[source, Cypher] +---- +CALL { + WITH var0, var1 + // Nested clause +} +---- + +_After_ + +[source, Javascript] +---- +const clause = new Cypher.Call(nestedClause, [movieNode, actorNode]); +---- + +[source, Cypher] +---- +CALL (var0, var1){ + // Nested clause +} +---- + +== Removed support for apoc + +No apoc functions or procedures are supported in Cypher Builder 3. The following are no longer available: + +- `apoc.util.validate` +- `apoc.util.validatePredicate` +- `apoc.date.convertFormat` +- `apoc.cypher.runFirstColumnMany` +- `apoc.cypher.runFirstColumnSingle` + +To use apoc methods, create a xref:how-to/customize-cypher.adoc#_custom_functions_and_procedures[custom function or procedure]. For example: + +[source, Javascript] +---- +function validate( + predicate: Predicate, + message: string, + params: List | Literal | Map +): Cypher.VoidCypherProcedure { + return new Cypher.VoidCypherProcedure( + "apoc.util.validate", + [predicate, new Literal(message), params] + ); +} +---- + +[source, Javascript] +---- +function validatePredicate(predicate: Predicate, message: string): CypherFunction { + return new CypherFunction(" + apoc.util.validatePredicate", + [predicate, new Literal(message), new Literal([0])] + ); +} +---- + +== API changes +The following are changes to the API, made to improve consistency across the Cypher Builder API. + +=== ListComprehension + +==== Remove second parameter of constructor +Remove second parameter of `ListComprehension` constructor in favor of `.in` + +Before: +[source, javascript] +---- +new Cypher.ListComprehension(variable, new Cypher.Literal([1, 2])); +---- + +After: +[source, javascript] +---- +new Cypher.ListComprehension(variable).in(new Cypher.Literal([1, 2])); +---- + +In both cases, the same comprehension will be generated: + +```cypher +[var0 IN [1, 2]] +``` + +==== ListComprehension `.in` method no longer throws if called twice. + +ListComprehension `.in` method no longer throws if called twice. It will instead override the expression + +Before, it will throw: +[source, javascript] +---- +new Cypher.ListComprehension(variable).in(new Cypher.Literal([1, 2])).in(new Cypher.Literal([1])) +---- + +After, this is valid: +[source, javascript] +---- +new Cypher.ListComprehension(variable).in(new Cypher.Literal([1, 2])).in(new Cypher.Literal([1])) +---- + +Will generate the following Cypher: + +[source, cypher] +---- +[var0 IN [1]] +---- + +Note that the same Cypher is generated if we omit the first `.in`: + +[source, javascript] +---- +new Cypher.ListComprehension(variable).in(new Cypher.Literal([1])) +---- + + +=== Foreach + +==== Remove extra parameters in `Cypher.Foreach` constructor +Remove extra parameters in `Cypher.Foreach` constructor in favor of methods `in` and `do`. + +For example, to create the following Cypher: + +[source, Cypher] +---- +FOREACH (var0 IN [1, 2, 3] | + CREATE (this1:Movie) + SET + this1.id = var0 + ) +---- + +_Before_ + +[source, javascript] +---- +const list = new Cypher.Literal([1, 2, 3]); +const variable = new Cypher.Variable(); + +const movieNode = new Cypher.Node(); +const createMovie = new Cypher.Create(new Cypher.Pattern(movieNode, { labels: ["Movie"] })).set([ + movieNode.property("id"), + variable, +]); + +const foreachClause = new Cypher.Foreach(variable, list, createMovie); +---- + +_After_ + +[source, javascript] +---- +const list = new Cypher.Literal([1, 2, 3]); +const variable = new Cypher.Variable(); + +const movieNode = new Cypher.Node(); +const createMovie = new Cypher.Create(new Cypher.Pattern(movieNode, { labels: ["Movie"] })).set([ + movieNode.property("id"), + variable, +]); + +const foreachClause = new Cypher.Foreach(variable).in(list).do(createMovie); +---- + +== Other breaking changes + +=== Remove method `.children` from concat clauses + +[source, Javascript] +---- +const query = Cypher.utils.concat(clause1, clause2); +query.children; // No longer supported +---- + +=== Remove type `Operation` + +The type `Cypher.Operation` is no longer availabe, use `Cypher.Expr` instead: + +_Before_ +[source, javascript] +---- +const myOperation: Cypher.Operation = Cypher.and() +---- + +_After_ +[source, javascript] +---- +const myOperation: Cypher.Expr = Cypher.and() +---- + diff --git a/docs/modules/ROOT/pages/subqueries/call.adoc b/docs/modules/ROOT/pages/subqueries/call.adoc index 9a8641c8..313224fc 100644 --- a/docs/modules/ROOT/pages/subqueries/call.adoc +++ b/docs/modules/ROOT/pages/subqueries/call.adoc @@ -83,54 +83,6 @@ CALL (*) { } ---- -[role=label--deprecated] -== `.importWith` - -[WARNING] -==== -This method is deprecated in favor of <<_variable_scope>>. -==== - -[WARNING] -==== -`importWith` cannot be used if scope variables are defined and will throw an error. -==== - - -To add variables to a `CALL` subquery context, you need to add a `WITH` statement. This can be achieved by using the `.importWith` method. - -[source, javascript] ----- -const dog = new Cypher.Node(); -const person = new Cypher.Node(); - -const dogName = new Cypher.NamedVariable("dogName"); - -const subquery = new Cypher.Match( - new Cypher.Pattern(person, { labels: ["Person"] }) - .related(new Cypher.Relationship({ type: "HAS_DOG" })) - .to(dog, { labels: ["Dog"] }) -).return([dog.property("name"), dogName]); - -const clause = new Cypher.Match(new Cypher.Pattern(person, { labels: ["Person"] })) - .call(subquery) - .importWith(person) - .return(dogName); ----- - -[source, cypher] ----- -MATCH (this0:Person) -CALL { - WITH this0 - MATCH (this0:Person)-[this1:HAS_DOG]->(this2:Dog) - RETURN this2.name AS dogName -} -RETURN dogName ----- - -Note how this example uses `.concat` to combine the initial `MATCH` and `CALL` clauses. - == `.inTransactions` The `.inTransactions` method appends the `IN TRANSACTIONS` modifier to a `CALL` subquery, causing it to link:https://neo4j.com/docs/cypher-manual/current/subqueries/subqueries-in-transactions/[execute in separate transactions]: diff --git a/docs/modules/ROOT/pages/variables-and-params/lists.adoc b/docs/modules/ROOT/pages/variables-and-params/lists.adoc index 3f52b032..c94a228d 100644 --- a/docs/modules/ROOT/pages/variables-and-params/lists.adoc +++ b/docs/modules/ROOT/pages/variables-and-params/lists.adoc @@ -98,7 +98,7 @@ You also need an expression resulting in the original list to create a new list [source, javascript] ---- -const listComprehension = new Cypher.ListComprehension(variable, new Cypher.Literal([1,2])) +const listComprehension = new Cypher.ListComprehension(variable).in(new Cypher.Literal([1,2])) ---- [source, cypher] @@ -106,13 +106,6 @@ const listComprehension = new Cypher.ListComprehension(variable, new Cypher.Lite [var0 IN [1,2]] ---- -Alternatively, the original list expression can be passed with the method `.in`: - -[source, javascript] ----- -const listComprehension = new Cypher.ListComprehension(variable).in(new Cypher.Literal([1,2])) ----- - By using the methods `where` and `map`, you can construct the filter and mapping parts of the comprehension: diff --git a/examples/patterns/label-expressions.ts b/examples/patterns/label-expressions.ts index 75dcc67f..275f77bf 100644 --- a/examples/patterns/label-expressions.ts +++ b/examples/patterns/label-expressions.ts @@ -30,9 +30,7 @@ const matchQuery = new Cypher.Match( }) ).return(movieNode); -const { cypher, params } = matchQuery.build({ - labelOperator: "&", -}); +const { cypher, params } = matchQuery.build(); console.log("Cypher"); console.log(cypher); diff --git a/examples/patterns/length.ts b/examples/patterns/length.ts index f39bce04..7332254b 100644 --- a/examples/patterns/length.ts +++ b/examples/patterns/length.ts @@ -29,9 +29,7 @@ const pattern = new Cypher.Pattern(movie).related(actedIn, { type: "ACTED_IN", l const matchQuery = new Cypher.Match(pattern).return(movie); -const { cypher, params } = matchQuery.build({ - labelOperator: "&", -}); +const { cypher, params } = matchQuery.build(); console.log("Cypher"); console.log(cypher); diff --git a/examples/patterns/quantified-path-patterns.ts b/examples/patterns/quantified-path-patterns.ts index 31bd9f5b..4431b23e 100644 --- a/examples/patterns/quantified-path-patterns.ts +++ b/examples/patterns/quantified-path-patterns.ts @@ -41,9 +41,7 @@ const quantifiedPath = new Cypher.QuantifiedPath( const query = new Cypher.Match(quantifiedPath).return(m2); -const { cypher, params } = query.build({ - labelOperator: "&", -}); +const { cypher, params } = query.build(); console.log("Cypher"); console.log(cypher); diff --git a/package.json b/package.json index e6290264..ba6bda0f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dist/**/*.js.map" ], "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" }, "scripts": { "test": "jest", @@ -45,15 +45,15 @@ "@changesets/changelog-github": "0.5.1", "@changesets/cli": "2.29.7", "@eslint/js": "9.38.0", - "@tsconfig/node16": "16.1.6", - "@types/jest": "29.5.14", - "@types/node": "22.18.13", + "@tsconfig/node20": "20.1.6", + "@types/jest": "30.0.0", + "@types/node": "24.7.2", "@typescript-eslint/eslint-plugin": "8.46.2", "@typescript-eslint/parser": "8.46.2", "eslint": "9.38.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-tsdoc": "0.4.0", - "jest": "29.7.0", + "jest": "30.2.0", "jest-extended": "6.0.0", "prettier": "3.6.2", "ts-jest": "29.4.5", diff --git a/src/Cypher.ts b/src/Cypher.ts index d6af2af3..b5521eae 100644 --- a/src/Cypher.ts +++ b/src/Cypher.ts @@ -63,11 +63,6 @@ export { Collect } from "./expressions/subquery/Collect"; export { Count } from "./expressions/subquery/Count"; export { Exists } from "./expressions/subquery/Exists"; -/** - * @hideGroups - */ -export * as apoc from "./namespaces/apoc/apoc"; - export * as db from "./namespaces/db/db"; /** * @hideGroups @@ -191,7 +186,7 @@ export type { PathAssign } from "./pattern/PathAssign"; export type { InputArgument } from "./procedures/CypherProcedure"; export type { Yield, YieldProjectionColumn } from "./procedures/Yield"; export type { Label } from "./references/Label"; -export type { CypherResult, Expr, NormalizationType, Operation, Predicate } from "./types"; +export type { CypherResult, Expr, NormalizationType, Predicate } from "./types"; /** * Utility functions diff --git a/src/Environment.ts b/src/Environment.ts index 8c3fd522..ad4c31bd 100644 --- a/src/Environment.ts +++ b/src/Environment.ts @@ -27,13 +27,11 @@ export type EnvPrefix = { }; export type EnvConfig = { - labelOperator: NonNullable; unsafeEscapeOptions: NonNullable; cypherVersion: BuildConfig["cypherVersion"]; }; const defaultConfig: EnvConfig = { - labelOperator: ":", unsafeEscapeOptions: {}, cypherVersion: undefined, }; diff --git a/src/clauses/Call.test.ts b/src/clauses/Call.test.ts index c92c9772..c935f4f9 100644 --- a/src/clauses/Call.test.ts +++ b/src/clauses/Call.test.ts @@ -71,138 +71,6 @@ describe("CypherBuilder Call", () => { `); }); - test("CALL with import with", () => { - const node = new Cypher.Node(); - - const matchClause = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })) - .where(Cypher.eq(new Cypher.Param("aa"), new Cypher.Param("bb"))) - .return([node.property("title"), "movie"]); - - const clause = new Cypher.Call(matchClause).importWith(node); - const queryResult = clause.build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` - "CALL { - WITH this0 - MATCH (this0:Movie) - WHERE $param0 = $param1 - RETURN this0.title AS movie - }" - `); - - expect(queryResult.params).toMatchInlineSnapshot(` - { - "param0": "aa", - "param1": "bb", - } - `); - }); - - test("CALL with import with *", () => { - const node = new Cypher.Node(); - - const matchClause = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })).return([ - node.property("title"), - "movie", - ]); - - const clause = new Cypher.Call(matchClause).importWith("*"); - const queryResult = clause.build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` - "CALL { - WITH * - MATCH (this0:Movie) - RETURN this0.title AS movie - }" - `); - - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); - - test("CALL with import with * and extra fields", () => { - const node = new Cypher.Node(); - - const matchClause = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })).return([ - node.property("title"), - "movie", - ]); - - const clause = new Cypher.Call(matchClause).importWith(node, "*"); - const queryResult = clause.build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` - "CALL { - WITH *, this0 - MATCH (this0:Movie) - RETURN this0.title AS movie - }" - `); - - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); - - test("CALL with import with without parameters", () => { - const node = new Cypher.Node(); - - const matchClause = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })) - .where(Cypher.eq(new Cypher.Param("aa"), new Cypher.Param("bb"))) - .return([node.property("title"), "movie"]); - - const clause = new Cypher.Call(matchClause).importWith(); - const queryResult = clause.build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` - "CALL { - MATCH (this0:Movie) - WHERE $param0 = $param1 - RETURN this0.title AS movie - }" - `); - - expect(queryResult.params).toMatchInlineSnapshot(` - { - "param0": "aa", - "param1": "bb", - } - `); - }); - - test("CALL with import with multiple parameters", () => { - const node = new Cypher.Node(); - - const matchClause = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })) - .where(Cypher.eq(new Cypher.Param("aa"), new Cypher.Param("bb"))) - .return([node.property("title"), "movie"]); - - const clause = new Cypher.Call(matchClause).importWith(node, new Cypher.Variable()); - const queryResult = clause.build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` - "CALL { - WITH this0, var1 - MATCH (this0:Movie) - WHERE $param0 = $param1 - RETURN this0.title AS movie - }" - `); - - expect(queryResult.params).toMatchInlineSnapshot(` - { - "param0": "aa", - "param1": "bb", - } - `); - }); - - test("CALL with import with fails if import with is already set", () => { - const node = new Cypher.Node(); - - const matchClause = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })) - .where(Cypher.eq(new Cypher.Param("aa"), new Cypher.Param("bb"))) - .return([node.property("title"), "movie"]); - - const clause = new Cypher.Call(matchClause).importWith(node); - expect(() => { - clause.importWith(node); - }).toThrow(`Call import "WITH" already set`); - }); - test("CALL with external with", () => { const node = new Cypher.Node(); @@ -700,19 +568,6 @@ RETURN this0" expect(queryResult.params).toMatchInlineSnapshot(`{}`); }); - test("ImportWith fails when variable scope is set", () => { - const movieNode = new Cypher.Node(); - - const call = new Cypher.Call( - new Cypher.Create(new Cypher.Pattern(movieNode).related().to(new Cypher.Node())), - [movieNode] - ); - - expect(() => { - call.importWith("*"); - }).toThrow(`Call import cannot be used along with scope clauses "Call ()"`); - }); - test("Call with empty variable scope", () => { const movieNode = new Cypher.Node(); const actorNode = new Cypher.Node(); diff --git a/src/clauses/Call.ts b/src/clauses/Call.ts index 685592b2..d165515b 100644 --- a/src/clauses/Call.ts +++ b/src/clauses/Call.ts @@ -24,7 +24,6 @@ import { compileCypherIfExists } from "../utils/compile-cypher-if-exists"; import { isNumber } from "../utils/is-number"; import { padBlock } from "../utils/pad-block"; import { Clause } from "./Clause"; -import { Union } from "./Union"; import { WithCreate } from "./mixins/clauses/WithCreate"; import { WithMatch } from "./mixins/clauses/WithMatch"; import { WithMerge } from "./mixins/clauses/WithMerge"; @@ -34,8 +33,6 @@ import { WithWith } from "./mixins/clauses/WithWith"; import { WithDelete } from "./mixins/sub-clauses/WithDelete"; import { WithOrder } from "./mixins/sub-clauses/WithOrder"; import { WithSetRemove } from "./mixins/sub-clauses/WithSetRemove"; -import { ImportWith } from "./sub-clauses/ImportWith"; -import { CompositeClause } from "./utils/concat"; import { mixin } from "./utils/mixin"; export interface Call @@ -69,7 +66,6 @@ export type CallInTransactionOptions = { @mixin(WithReturn, WithWith, WithUnwind, WithDelete, WithSetRemove, WithMatch, WithCreate, WithMerge, WithOrder) export class Call extends Clause { private readonly subquery: CypherASTNode; - private _importWith?: ImportWith; private inTransactionsConfig?: CallInTransactionOptions; private readonly variableScope?: Variable[] | "*"; private _optional: boolean = false; @@ -82,24 +78,6 @@ export class Call extends Clause { this.variableScope = variableScope; } - /** Adds a `WITH` statement inside `CALL {`, this `WITH` can is used to import variables outside of the subquery - * @see {@link https://neo4j.com/docs/cypher-manual/current/subqueries/call-subquery/#call-importing-variables | Cypher Documentation} - * @deprecated Use constructor parameter `variableScope` instead - */ - public importWith(...params: Array): this { - if (this._importWith) { - throw new Error(`Call import "WITH" already set`); - } - if (this.variableScope) { - throw new Error(`Call import cannot be used along with scope clauses "Call ()"`); - } - if (params.length > 0) { - this._importWith = new ImportWith(this, [...params]); - this.addChildren(this._importWith); - } - return this; - } - public inTransactions(config: CallInTransactionOptions = {}): this { this.inTransactionsConfig = config; return this; @@ -116,30 +94,19 @@ export class Call extends Clause { /** @internal */ public getCypher(env: CypherEnvironment): string { - const importWithCypher = compileCypherIfExists(this._importWith, env, { suffix: "\n" }); - - const subQueryStr = this.getSubqueryCypher(env, importWithCypher); + const subQueryStr = this.subquery.getCypher(env); const setCypher = this.compileSetCypher(env); const deleteCypher = compileCypherIfExists(this.deleteClause, env, { prefix: "\n" }); const orderByCypher = compileCypherIfExists(this.orderByStatement, env, { prefix: "\n" }); const inTransactionCypher = this.generateInTransactionsStr(); - const inCallBlock = `${importWithCypher}${subQueryStr}`; const variableScopeStr = this.generateVariableScopeStr(env); const nextClause = this.compileNextClause(env); const optionalStr = this._optional ? "OPTIONAL " : ""; - return `${optionalStr}CALL${variableScopeStr} {\n${padBlock(inCallBlock)}\n}${inTransactionCypher}${setCypher}${deleteCypher}${orderByCypher}${nextClause}`; - } - - private getSubqueryCypher(env: CypherEnvironment, importWithCypher: string | undefined): string { - // This ensures the import with is added to all the union subqueries - if (this.subquery instanceof Union || this.subquery instanceof CompositeClause) { - return this.subquery.getCypher(env, importWithCypher); - } - return this.subquery.getCypher(env); + return `${optionalStr}CALL${variableScopeStr} {\n${padBlock(subQueryStr)}\n}${inTransactionCypher}${setCypher}${deleteCypher}${orderByCypher}${nextClause}`; } private generateVariableScopeStr(env: CypherEnvironment): string { diff --git a/src/clauses/Clause.ts b/src/clauses/Clause.ts index b3f6c7d4..53f2a5f4 100644 --- a/src/clauses/Clause.ts +++ b/src/clauses/Clause.ts @@ -30,22 +30,10 @@ const customInspectSymbol = Symbol.for("nodejs.util.inspect.custom"); * @group Clauses */ export type BuildConfig = Partial<{ - /** Defines the default operator for adding multiple labels in a Pattern. Defaults to `":"` - * - * If the target Cypher is version 5 or above, `"&"` is recommended - * - * @example - * `MATCH (this:Movie:Film)` - * `MATCH (this:Movie&Film)` - * - * @deprecated This will be removed in the following major release and the value `"&"` will be used in the generated Cypher - * - */ - labelOperator: ":" | "&"; /** Will prefix generated queries with the Cypher version * @example `CYPHER 5` */ - cypherVersion: "5"; + cypherVersion: "5" | "25"; /** Prefix variables with given string. * * This is useful to avoid name collisions if separate Cypher queries are generated and merged after generating the strings. @@ -85,11 +73,10 @@ export abstract class Clause extends CypherASTNode { /** Compiles a clause into Cypher and params */ public build(config?: BuildConfig): CypherResult { - const { prefix, extraParams = {}, labelOperator = ":", cypherVersion, unsafeEscapeOptions = {} } = config ?? {}; + const { prefix, extraParams = {}, cypherVersion, unsafeEscapeOptions = {} } = config ?? {}; if (this.isRoot) { const env = this.getEnv(prefix, { - labelOperator, cypherVersion, unsafeEscapeOptions, }); @@ -104,7 +91,7 @@ export abstract class Clause extends CypherASTNode { } const root = this.getRoot(); if (root instanceof Clause) { - return root.build({ prefix, extraParams, labelOperator, cypherVersion, unsafeEscapeOptions }); + return root.build({ prefix, extraParams, cypherVersion, unsafeEscapeOptions }); } throw new Error(`Cannot build root: ${root.constructor.name}`); } diff --git a/src/clauses/Foreach.ts b/src/clauses/Foreach.ts index fd27865f..c4820693 100644 --- a/src/clauses/Foreach.ts +++ b/src/clauses/Foreach.ts @@ -51,13 +51,9 @@ export class Foreach extends Clause { private mapClause: ForeachClauses | undefined; constructor(variable: Variable); - /** @deprecated Use `in` and `do` instead of passing the constructor */ - constructor(variable: Variable, listExpr: Expr, mapClause: ForeachClauses); - constructor(variable: Variable, listExpr?: Expr, mapClause?: ForeachClauses) { + constructor(variable: Variable) { super(); this.variable = variable; - this.listExpr = listExpr; - this.mapClause = mapClause; } public in(listExpr: Expr): this { diff --git a/src/clauses/Union.test.ts b/src/clauses/Union.test.ts index 081ce145..16f86c22 100644 --- a/src/clauses/Union.test.ts +++ b/src/clauses/Union.test.ts @@ -89,93 +89,4 @@ describe("CypherBuilder UNION", () => { RETURN this3 AS var1" `); }); - - test("Union in CALL statement with import with", () => { - const returnVar = new Cypher.Variable(); - const n1 = new Cypher.Node(); - const query1 = new Cypher.Match(new Cypher.Pattern(n1, { labels: ["Movie"] })).return([n1, returnVar]); - const n2 = new Cypher.Node(); - const query2 = new Cypher.Match(new Cypher.Pattern(n2, { labels: ["Movie"] })).return([n2, returnVar]); - const n3 = new Cypher.Node(); - const query3 = new Cypher.Match(new Cypher.Pattern(n3, { labels: ["Movie"] })).return([n3, returnVar]); - - const unionQuery = new Cypher.Union(query1, query2, query3); - const callQuery = new Cypher.Call(unionQuery).importWith(new Cypher.Variable()); - const queryResult = callQuery.build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` -"CALL { - WITH var0 - MATCH (this1:Movie) - RETURN this1 AS var2 - UNION - WITH var0 - MATCH (this3:Movie) - RETURN this3 AS var2 - UNION - WITH var0 - MATCH (this4:Movie) - RETURN this4 AS var2 -}" -`); - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); - test("Union in concat in CALL statement with import with", () => { - const returnVar = new Cypher.Variable(); - const n1 = new Cypher.Node(); - const query1 = new Cypher.Match(new Cypher.Pattern(n1, { labels: ["Movie"] })).return([n1, returnVar]); - const n2 = new Cypher.Node(); - const query2 = new Cypher.Match(new Cypher.Pattern(n2, { labels: ["Movie"] })).return([n2, returnVar]); - const n3 = new Cypher.Node(); - const query3 = new Cypher.Match(new Cypher.Pattern(n3, { labels: ["Movie"] })).return([n3, returnVar]); - - const unionQuery = new Cypher.Union(query1, query2, query3); - const callQuery = new Cypher.Call(Cypher.utils.concat(unionQuery)).importWith(new Cypher.Variable()); - const queryResult = callQuery.build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` -"CALL { - WITH var0 - MATCH (this1:Movie) - RETURN this1 AS var2 - UNION - WITH var0 - MATCH (this3:Movie) - RETURN this3 AS var2 - UNION - WITH var0 - MATCH (this4:Movie) - RETURN this4 AS var2 -}" -`); - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); - - test("Union in nested CALL statement should not append top import with", () => { - const returnVar = new Cypher.Variable(); - const n1 = new Cypher.Node(); - const query1 = new Cypher.Match(new Cypher.Pattern(n1, { labels: ["Movie"] })).return([n1, returnVar]); - const n2 = new Cypher.Node(); - const query2 = new Cypher.Match(new Cypher.Pattern(n2, { labels: ["Movie"] })).return([n2, returnVar]); - const n3 = new Cypher.Node(); - const query3 = new Cypher.Match(new Cypher.Pattern(n3, { labels: ["Movie"] })).return([n3, returnVar]); - - const unionQuery = new Cypher.Union(query1, query2, query3); - const callQuery = new Cypher.Call(new Cypher.Call(unionQuery)).importWith(new Cypher.Variable()); - const queryResult = callQuery.build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` -"CALL { - WITH var0 - CALL { - MATCH (this1:Movie) - RETURN this1 AS var2 - UNION - MATCH (this3:Movie) - RETURN this3 AS var2 - UNION - MATCH (this4:Movie) - RETURN this4 AS var2 - } -}" -`); - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); }); diff --git a/src/clauses/Union.ts b/src/clauses/Union.ts index 6fb87548..e5bd1edb 100644 --- a/src/clauses/Union.ts +++ b/src/clauses/Union.ts @@ -54,10 +54,10 @@ export class Union extends Clause { * If importWithCypher is provided, it will be added at the beginning of each subquery except first * @internal */ - public getCypher(env: CypherEnvironment, importWithCypher?: string): string { + public getCypher(env: CypherEnvironment): string { const subqueriesStr = this.subqueries.map((s) => s.getCypher(env)); const unionTypeStr = this.unionType ? ` ${this.unionType}` : ""; - return subqueriesStr.join(`\nUNION${unionTypeStr}\n${importWithCypher ?? ""}`); + return subqueriesStr.join(`\nUNION${unionTypeStr}\n`); } } diff --git a/src/clauses/With.test.ts b/src/clauses/With.test.ts index a0b911bc..ce7e2eb9 100644 --- a/src/clauses/With.test.ts +++ b/src/clauses/With.test.ts @@ -286,12 +286,15 @@ CALL db.labels() YIELD label" }); test("With * and cypher void procedure", () => { - const withQuery = new Cypher.With("*").callProcedure(Cypher.apoc.util.validate(Cypher.true, "message")); + const targetNode = new Cypher.Node(); + const customProcedure = new Cypher.VoidProcedure("customProcedure", [targetNode]); + + const withQuery = new Cypher.With("*").callProcedure(customProcedure); const queryResult = withQuery.build(); expect(queryResult.cypher).toMatchInlineSnapshot(` "WITH * -CALL apoc.util.validate(true, \\"message\\", [0])" +CALL customProcedure(this0)" `); expect(queryResult.params).toMatchInlineSnapshot(`{}`); }); diff --git a/src/clauses/utils/concat.ts b/src/clauses/utils/concat.ts index fe2b471e..ac07cd39 100644 --- a/src/clauses/utils/concat.ts +++ b/src/clauses/utils/concat.ts @@ -21,7 +21,6 @@ import type { CypherASTNode } from "../../CypherASTNode"; import type { CypherEnvironment } from "../../Environment"; import { filterTruthy } from "../../utils/filter-truthy"; import { Clause } from "../Clause"; -import { Union } from "../Union"; /** The result of multiple clauses concatenated with `Cypher.utils.concat` * @group Utils @@ -52,18 +51,10 @@ export class CompositeClause extends Clause { return this._children.length === 0; } - /** @deprecated Children from a composite clause should not be accessed as this will lead to unexpected behaviour */ - public get children(): Array { - return this._children; - } - /** @internal */ - public getCypher(env: CypherEnvironment, importWithCypher?: string): string { + public getCypher(env: CypherEnvironment): string { // NOTE: importWithCypher used to pass down import WITH to UNION clauses const childrenStrs = this._children.map((c) => { - if (importWithCypher && c instanceof Union) { - return c.getCypher(env, importWithCypher); - } return c.getCypher(env); }); return childrenStrs.join(this.separator); diff --git a/src/expressions/HasLabel.test.ts b/src/expressions/HasLabel.test.ts index 7b7fd1c8..4a03980a 100644 --- a/src/expressions/HasLabel.test.ts +++ b/src/expressions/HasLabel.test.ts @@ -55,9 +55,9 @@ describe("HasLabel", () => { const queryResult = new TestClause(query).build(); expect(queryResult.cypher).toMatchInlineSnapshot(` - "MATCH (this0:Movie) - WHERE this0:Movie:Film" - `); +"MATCH (this0:Movie) +WHERE this0:Movie&Film" +`); expect(queryResult.params).toMatchInlineSnapshot(`{}`); }); diff --git a/src/expressions/HasLabel.ts b/src/expressions/HasLabel.ts index 7958e265..3f29556a 100644 --- a/src/expressions/HasLabel.ts +++ b/src/expressions/HasLabel.ts @@ -58,9 +58,9 @@ export class HasLabel extends CypherASTNode { private generateLabelExpressionStr(env: CypherEnvironment): string { if (Array.isArray(this.expectedLabels)) { const escapedLabels = this.expectedLabels.map((label) => escapeLabel(label)); - return addLabelToken(env.config.labelOperator, ...escapedLabels); + return addLabelToken(...escapedLabels); } else { - return addLabelToken(env.config.labelOperator, this.expectedLabels.getCypher(env)); + return addLabelToken(this.expectedLabels.getCypher(env)); } } diff --git a/src/expressions/functions/scalar.test.ts b/src/expressions/functions/scalar.test.ts index 29c83de6..b1d6535c 100644 --- a/src/expressions/functions/scalar.test.ts +++ b/src/expressions/functions/scalar.test.ts @@ -31,7 +31,6 @@ describe("Scalar Functions", () => { // 1 parameter functions test.each([ - "id", "elementId", "endNode", "size", @@ -103,12 +102,4 @@ describe("Scalar Functions", () => { expect(queryResult.cypher).toMatchInlineSnapshot(`"size(\\"Hello\\")"`); }); - - test("size() applied to a pattern", () => { - const pattern = new Cypher.Pattern(new Cypher.Node()).related().to(); - const cypherFunction = Cypher.size(pattern); - const queryResult = new TestClause(cypherFunction).build(); - - expect(queryResult.cypher).toMatchInlineSnapshot(`"size((this0)-[]->())"`); - }); }); diff --git a/src/expressions/functions/scalar.ts b/src/expressions/functions/scalar.ts index d5aa2331..be581766 100644 --- a/src/expressions/functions/scalar.ts +++ b/src/expressions/functions/scalar.ts @@ -17,8 +17,6 @@ * limitations under the License. */ -import { Raw } from "../../clauses/Raw"; -import type { Pattern } from "../../pattern/Pattern"; import type { Expr } from "../../types"; import { CypherFunction } from "./CypherFunctions"; @@ -58,16 +56,6 @@ export function head(expr: Expr): CypherFunction { return new CypherFunction("head", [expr]); } -/** - * @see {@link https://neo4j.com/docs/cypher-manual/current/functions/scalar/#functions-id | Cypher Documentation} - * @group Functions - * @category Scalar - * @deprecated Use {@link elementId} instead - */ -export function id(variable: Expr): CypherFunction { - return new CypherFunction("id", [variable]); -} - /** * @see {@link https://neo4j.com/docs/cypher-manual/current/functions/scalar/#functions-last | Cypher Documentation} * @group Functions @@ -109,17 +97,8 @@ export function randomUUID(): CypherFunction { * @group Functions * @category Scalar */ -export function size(expr: Expr): CypherFunction; -/** @deprecated size() with pattern is deprecated in Neo4j 5 */ -export function size(expr: Pattern): CypherFunction; -export function size(expr: Expr | Pattern): CypherFunction { - // Support for patterns in size() in Neo4j 4 - // Using Raw to avoid adding Patterns to CypherFunction - const sizeParam = new Raw((env) => { - return env.compile(expr); - }); - - return new CypherFunction("size", [sizeParam]); +export function size(expr: Expr): CypherFunction { + return new CypherFunction("size", [expr]); } /** diff --git a/src/expressions/functions/spatial.test.ts b/src/expressions/functions/spatial.test.ts index 51415b6a..b53cf4c3 100644 --- a/src/expressions/functions/spatial.test.ts +++ b/src/expressions/functions/spatial.test.ts @@ -17,23 +17,10 @@ * limitations under the License. */ -import { TestClause } from "../../utils/TestClause"; import Cypher from "../.."; +import { TestClause } from "../../utils/TestClause"; describe("Spatial Functions", () => { - describe("4.x deprecated functions", () => { - test.each(["distance"] as const)("%s", (value) => { - const leftExpr = new Cypher.Variable(); - const rightExpr = new Cypher.Variable(); - const spatialFn = Cypher[value](leftExpr, rightExpr); - - const queryResult = new TestClause(spatialFn).build(); - - expect(queryResult.cypher).toBe(`${value}(var0, var1)`); - expect(queryResult.params).toEqual({}); - }); - }); - test("point function", () => { const pointFn = Cypher.point(new Cypher.Variable()); diff --git a/src/expressions/functions/spatial.ts b/src/expressions/functions/spatial.ts index b6d2e9f5..382220b5 100644 --- a/src/expressions/functions/spatial.ts +++ b/src/expressions/functions/spatial.ts @@ -29,16 +29,6 @@ export function point(variable: Expr): CypherFunction { return new CypherFunction("point", [variable]); } -/** - * @see {@link https://neo4j.com/docs/cypher-manual/4.3/functions/spatial/#functions-distance | Cypher Documentation} - * @group Functions - * @category Spatial - * @deprecated No longer supported in Neo4j 5. Use {@link point.distance} instead. - */ -export function distance(lexpr: Expr, rexpr: Expr): CypherFunction { - return new CypherFunction("distance", [lexpr, rexpr]); -} - /** * @see {@link https://neo4j.com/docs/cypher-manual/current/functions/spatial/#functions-distance | Cypher Documentation} * @group Functions diff --git a/src/expressions/list/ListComprehension.test.ts b/src/expressions/list/ListComprehension.test.ts index 57b4dff4..905d8a2a 100644 --- a/src/expressions/list/ListComprehension.test.ts +++ b/src/expressions/list/ListComprehension.test.ts @@ -25,7 +25,7 @@ describe("List comprehension", () => { const variable = new Cypher.Variable(); const exprVariable = new Cypher.Param([1, 2, 5]); - const listComprehension = new Cypher.ListComprehension(variable, exprVariable); + const listComprehension = new Cypher.ListComprehension(variable).in(exprVariable); const queryResult = new TestClause(listComprehension).build(); @@ -47,7 +47,7 @@ describe("List comprehension", () => { const exprVariable = new Cypher.Param([1, 2, 5]); const andExpr = Cypher.eq(variable, new Cypher.Param(5)); - const listComprehension = new Cypher.ListComprehension(variable, exprVariable).where(andExpr); + const listComprehension = new Cypher.ListComprehension(variable).in(exprVariable).where(andExpr); const queryResult = new TestClause(listComprehension).build(); @@ -91,16 +91,27 @@ describe("List comprehension", () => { `); }); - it("Fails to set a expression twice", () => { + test("Overrides if a expression is set twice", () => { const variable = new Cypher.Variable(); const exprVariable = new Cypher.Param([1, 2, 5]); + const exprVariable2 = new Cypher.Param([1, 3]); - expect(() => { - new Cypher.ListComprehension(variable, exprVariable).in(exprVariable); - }).toThrow("Cannot set 2 lists in list comprehension IN"); + const listComprehension = new Cypher.ListComprehension(variable).in(exprVariable).in(exprVariable2); + + const queryResult = new TestClause(listComprehension).build(); + + expect(queryResult.cypher).toMatchInlineSnapshot(`"[var0 IN $param0]"`); + expect(queryResult.params).toMatchInlineSnapshot(` + { + "param0": [ + 1, + 3, + ], + } + `); }); - it("Fails to build if no expression is set", () => { + test("Fails to build if no expression is set", () => { const variable = new Cypher.Variable(); const listComprehension = new Cypher.ListComprehension(variable); diff --git a/src/expressions/list/ListComprehension.ts b/src/expressions/list/ListComprehension.ts index 06a363f9..a62532aa 100644 --- a/src/expressions/list/ListComprehension.ts +++ b/src/expressions/list/ListComprehension.ts @@ -38,17 +38,13 @@ export class ListComprehension extends CypherASTNode { private listExpr: Expr | undefined; private mapExpr: Expr | undefined; // Expression for list mapping - constructor(variable: Variable); - /** @deprecated Use `new ListComprehension(var1).in(expr)` instead */ - constructor(variable: Variable, listExpr: Expr); - constructor(variable: Variable, listExpr?: Expr) { + constructor(variable: Variable) { super(); this.variable = variable; - this.listExpr = listExpr; } + /** Sets the list expression to be used for the comprehension. If called twice, the expression will be overriden */ public in(listExpr: Expr): this { - if (this.listExpr) throw new Error("Cannot set 2 lists in list comprehension IN"); this.listExpr = listExpr; return this; } diff --git a/src/namespaces/apoc/apoc.ts b/src/namespaces/apoc/apoc.ts deleted file mode 100644 index bd5c8859..00000000 --- a/src/namespaces/apoc/apoc.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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. - */ - -/** Functions and procedures in `apoc.date` - * @see [Apoc Documentation](https://neo4j.com/docs/apoc/current/overview/apoc.date/) - */ -export * as date from "./date"; - -/** Functions and procedures in `apoc.util` - * @see [Apoc Documentation](https://neo4j.com/docs/apoc/current/overview/apoc.util/) - */ -export * as util from "./util"; - -/** Functions and procedures in `apoc.cypher` - * @see [Apoc Documentation](https://neo4j.com/docs/apoc/current/overview/apoc.cypher/) - */ -export * as cypher from "./cypher/cypher"; diff --git a/src/namespaces/apoc/cypher/cypher.ts b/src/namespaces/apoc/cypher/cypher.ts deleted file mode 100644 index 3bd8046a..00000000 --- a/src/namespaces/apoc/cypher/cypher.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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. - */ - -export { runFirstColumnMany, runFirstColumnSingle } from "./run-first-column"; diff --git a/src/namespaces/apoc/cypher/run-first-column.test.ts b/src/namespaces/apoc/cypher/run-first-column.test.ts deleted file mode 100644 index 5d1dddd4..00000000 --- a/src/namespaces/apoc/cypher/run-first-column.test.ts +++ /dev/null @@ -1,191 +0,0 @@ -/* - * 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 Cypher from "../../.."; -import { TestClause } from "../../../utils/TestClause"; - -describe("apoc.cypher", () => { - test("runFirstColumnSingle", () => { - const node = new Cypher.Node(); - const subquery = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })).return(node); - - const apocRunFirstColum = Cypher.apoc.cypher.runFirstColumnSingle(subquery); - const queryResult = new TestClause(apocRunFirstColum).build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` - "apoc.cypher.runFirstColumnSingle(\\"MATCH (this0:Movie) - RETURN this0\\", { })" - `); - - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); - - test("runFirstColumnSingle with parameters", () => { - const node = new Cypher.Node(); - const subquery = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })).return(node); - - const apocRunFirstColum = Cypher.apoc.cypher.runFirstColumnSingle(subquery, [node]); - const queryResult = new TestClause(apocRunFirstColum).build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` - "apoc.cypher.runFirstColumnSingle(\\"MATCH (this0:Movie) - RETURN this0\\", { this0: this0 })" - `); - - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); - - test("runFirstColumnMany", () => { - const node = new Cypher.Node(); - const subquery = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })).return(node); - - const apocRunFirstColum = Cypher.apoc.cypher.runFirstColumnMany(subquery); - const queryResult = new TestClause(apocRunFirstColum).build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` - "apoc.cypher.runFirstColumnMany(\\"MATCH (this0:Movie) - RETURN this0\\", { })" - `); - - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); - - test("runFirstColumnMany with parameters", () => { - const node = new Cypher.Node(); - const subquery = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })).return(node); - - const apocRunFirstColum = Cypher.apoc.cypher.runFirstColumnMany(subquery, [node]); - const queryResult = new TestClause(apocRunFirstColum).build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` - "apoc.cypher.runFirstColumnMany(\\"MATCH (this0:Movie) - RETURN this0\\", { this0: this0 })" - `); - - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); - - test("runFirstColumn with string", () => { - const node = new Cypher.Node(); - const subquery = "MATCH (n:Film) RETURN n"; - - const apocRunFirstColum = Cypher.apoc.cypher.runFirstColumnSingle(subquery, [node]); - const queryResult = new TestClause(apocRunFirstColum).build(); - expect(queryResult.cypher).toMatchInlineSnapshot( - `"apoc.cypher.runFirstColumnSingle(\\"MATCH (n:Film) RETURN n\\", { this0: this0 })"` - ); - - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); - - test("runFirstColumn with a map for parameters", () => { - const node = new Cypher.Node(); - const subquery = "MATCH (n) RETURN n"; - - const params = new Cypher.Map({ - n: node, - param: new Cypher.Param("Test param"), - }); - - const apocRunFirstColum = Cypher.apoc.cypher.runFirstColumnSingle(subquery, params); - const queryResult = new TestClause(apocRunFirstColum).build(); - expect(queryResult.cypher).toMatchInlineSnapshot( - `"apoc.cypher.runFirstColumnSingle(\\"MATCH (n) RETURN n\\", { n: this0, param: $param0 })"` - ); - - expect(queryResult.params).toMatchInlineSnapshot(` - { - "param0": "Test param", - } - `); - }); - - test("Complex subquery with scoped env and params", () => { - const node = new Cypher.Node(); - const param1 = new Cypher.Param("The Matrix"); - - const topQuery = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })).where( - Cypher.eq(node.property("title"), param1) - ); - - const nestedPattern = new Cypher.Pattern(node); - const releasedParam = new Cypher.Param(1999); - const subQuery = new Cypher.Match(nestedPattern).set([node.property("released"), releasedParam]).return(node); - const apocCall = Cypher.apoc.cypher.runFirstColumnMany(subQuery, [node, releasedParam]); - - topQuery.return( - new Cypher.Map({ - result: apocCall, - }) - ); - - const cypherResult = topQuery.build(); - - expect(cypherResult.cypher).toMatchInlineSnapshot(` - "MATCH (this0:Movie) - WHERE this0.title = $param0 - RETURN { result: apoc.cypher.runFirstColumnMany(\\"MATCH (this0) - SET - this0.released = $param1 - RETURN this0\\", { this0: this0, param1: $param1 }) }" - `); - expect(cypherResult.params).toMatchInlineSnapshot(` - { - "param0": "The Matrix", - "param1": 1999, - } - `); - }); - - test("runFirstColumn with an object for parameters", () => { - const node = new Cypher.Node(); - const subquery = "MATCH (n) RETURN n"; - - const params = { - n: node, - param: new Cypher.Param("Test param"), - }; - - const apocRunFirstColum = Cypher.apoc.cypher.runFirstColumnSingle(subquery, params); - const queryResult = new TestClause(apocRunFirstColum).build(); - expect(queryResult.cypher).toMatchInlineSnapshot( - `"apoc.cypher.runFirstColumnSingle(\\"MATCH (n) RETURN n\\", { n: this0, param: $param0 })"` - ); - - expect(queryResult.params).toMatchInlineSnapshot(` - { - "param0": "Test param", - } - `); - }); - - test("runFirstColumnSingle", () => { - const node = new Cypher.Node(); - - const nestedSubquery = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })).return(node); - const nestedRunFirstColumn = Cypher.apoc.cypher.runFirstColumnSingle(nestedSubquery); - - const subquery = new Cypher.Return([nestedRunFirstColumn, "result"]); - - const apocRunFirstColum = Cypher.apoc.cypher.runFirstColumnSingle(subquery); - const queryResult = new TestClause(apocRunFirstColum).build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` - "apoc.cypher.runFirstColumnSingle(\\"RETURN apoc.cypher.runFirstColumnSingle(\\\\\\"MATCH (this0:Movie) - RETURN this0\\\\\\", { }) AS result\\", { })" - `); - - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); -}); diff --git a/src/namespaces/apoc/cypher/run-first-column.ts b/src/namespaces/apoc/cypher/run-first-column.ts deleted file mode 100644 index 107931a9..00000000 --- a/src/namespaces/apoc/cypher/run-first-column.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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 { CypherEnvironment } from "../../../Environment"; -import type { Clause } from "../../../clauses/Clause"; -import { CypherFunction } from "../../../expressions/functions/CypherFunctions"; -import { MapExpr } from "../../../expressions/map/MapExpr"; -import type { Variable } from "../../../references/Variable"; -import type { Expr } from "../../../types"; - -/** - * @group Functions - * @deprecated apoc methods will no longer be supported in Cypher Builder version 3 - * @see [Apoc Documentation](https://neo4j.com/docs/apoc/current/overview/apoc.cypher/apoc.cypher.runFirstColumnMany/) - */ -export function runFirstColumnMany( - clause: Clause | string, - params: Variable[] | MapExpr | Record = [] -): CypherFunction { - return new RunFirstColumnFunction(clause, params, true); -} - -/** - * @group Functions - * @deprecated apoc methods will no longer be supported in Cypher Builder version 3 - * @see [Apoc Documentation](https://neo4j.com/docs/apoc/current/overview/apoc.cypher/apoc.cypher.runFirstColumnSingle/) - */ -export function runFirstColumnSingle( - clause: Clause | string, - params: Variable[] | MapExpr | Record = [] -): CypherFunction { - return new RunFirstColumnFunction(clause, params, false); -} - -class RunFirstColumnFunction extends CypherFunction { - private readonly innerClause: Clause | string; - private readonly variables: Variable[] | MapExpr; - private readonly many: boolean; - - constructor(clause: Clause | string, variables: Variable[] | MapExpr | Record, many: boolean) { - super(`apoc.cypher.runFirstColumn${many ? "Many" : "Single"}`); // Note: this argument is never used - - this.innerClause = clause; - this.variables = this.parseVariablesInput(variables); - this.many = many; - } - - /** @internal */ - public getCypher(env: CypherEnvironment): string { - let clauseStr: string; - let paramsStr: string; - if (typeof this.innerClause === "string") { - clauseStr = this.innerClause; - } else { - clauseStr = this.innerClause.getRoot().getCypher(env); - } - - if (Array.isArray(this.variables)) { - paramsStr = this.convertArrayToParams(env, this.variables); - } else { - paramsStr = this.variables.getCypher(env); - } - - if (this.many) { - return `apoc.cypher.runFirstColumnMany("${this.escapeQuery(clauseStr)}", ${paramsStr})`; - } - - return `apoc.cypher.runFirstColumnSingle("${this.escapeQuery(clauseStr)}", ${paramsStr})`; - } - - private escapeQuery(query: string): string { - return query.replaceAll(/(["\\])/g, "\\$1"); - } - - private parseVariablesInput(variables: Variable[] | MapExpr | Record): Variable[] | MapExpr { - if (Array.isArray(variables) || variables instanceof MapExpr) return variables; - return new MapExpr(variables); - } - - private convertArrayToParams(env: CypherEnvironment, variables: Variable[]): string { - const params = variables.reduce((acc: Record, variable) => { - const globalScopeName = variable.getCypher(env); - const key = env.getReferenceId(variable); - acc[key] = globalScopeName; - return acc; - }, {}); - - const paramsStr = Object.entries(params) - .map(([key, value]) => { - return `${key}: ${value}`; - }) - .join(", "); - return `{ ${paramsStr} }`; - } -} diff --git a/src/namespaces/apoc/date.test.ts b/src/namespaces/apoc/date.test.ts deleted file mode 100644 index 06e4489c..00000000 --- a/src/namespaces/apoc/date.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 { TestClause } from "../../utils/TestClause"; -import Cypher from "../.."; - -describe("apoc.date", () => { - test("convertFormat", () => { - const convertFormat = Cypher.apoc.date.convertFormat( - new Cypher.Variable(), - "iso_zoned_date_time", - "iso_offset_date_time" - ); - - const queryResult = new TestClause(convertFormat).build(); - - expect(queryResult.cypher).toMatchInlineSnapshot( - `"apoc.date.convertFormat(toString(var0), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")"` - ); - - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); - test("convertFormat with expression", () => { - const convertFormat = Cypher.apoc.date.convertFormat( - Cypher.max(new Cypher.Variable()), - "iso_zoned_date_time", - "iso_offset_date_time" - ); - - const queryResult = new TestClause(convertFormat).build(); - - expect(queryResult.cypher).toMatchInlineSnapshot( - `"apoc.date.convertFormat(toString(max(var0)), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")"` - ); - - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); - - test("convertFormat with default convertTo", () => { - const convertFormat = Cypher.apoc.date.convertFormat(new Cypher.Variable(), "iso_zoned_date_time"); - - const queryResult = new TestClause(convertFormat).build(); - - expect(queryResult.cypher).toMatchInlineSnapshot( - `"apoc.date.convertFormat(toString(var0), \\"iso_zoned_date_time\\", \\"yyyy-MM-dd\\")"` - ); - - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); -}); diff --git a/src/namespaces/apoc/date.ts b/src/namespaces/apoc/date.ts deleted file mode 100644 index f0b77d5d..00000000 --- a/src/namespaces/apoc/date.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 { CypherFunction } from "../../expressions/functions/CypherFunctions"; -import { toString } from "../../expressions/functions/string"; -import { Literal } from "../../references/Literal"; -import type { Expr } from "../../types"; - -/** - * @group Functions - * @see [Apoc Documentation](https://neo4j.com/docs/apoc/current/overview/apoc.date/apoc.date.convertFormat/) - * @deprecated apoc methods will no longer be supported in Cypher Builder version 3 - * @example - * ```ts - * Cypher.apoc.date.convertFormat( - * new Cypher.Param("2020-11-04"), - * "date", - * "basic_date" - * ) - *``` - */ -export function convertFormat(temporalParam: Expr, currentFormat: string, convertTo = "yyyy-MM-dd"): CypherFunction { - return new CypherFunction("apoc.date.convertFormat", [ - toString(temporalParam), // NOTE: should this be `toString` by default? - new Literal(currentFormat), - new Literal(convertTo), - ]); -} diff --git a/src/namespaces/apoc/util.test.ts b/src/namespaces/apoc/util.test.ts deleted file mode 100644 index 1a23f595..00000000 --- a/src/namespaces/apoc/util.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 Cypher from "../.."; -describe("apoc.util", () => { - describe("Validate", () => { - test("Simple Validate", () => { - const validate = Cypher.apoc.util.validate( - Cypher.eq(new Cypher.Literal(1), new Cypher.Literal(2)), - "That's not how math works" - ); - expect(validate.build().cypher).toMatchInlineSnapshot( - `"CALL apoc.util.validate(1 = 2, \\"That's not how math works\\", [0])"` - ); - }); - }); - - describe("ValidatePredicate", () => { - test("Simple validatePredicate", () => { - const node = new Cypher.Node(); - const validatePredicate = Cypher.apoc.util.validatePredicate( - Cypher.eq(new Cypher.Literal(1), new Cypher.Literal(2)), - "That's not how math works" - ); - - const query = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })); - query.where(validatePredicate).return(node); - - const { cypher, params } = query.build(); - expect(cypher).toMatchInlineSnapshot(` - "MATCH (this0:Movie) - WHERE apoc.util.validatePredicate(1 = 2, \\"That's not how math works\\", [0]) - RETURN this0" - `); - expect(params).toMatchInlineSnapshot(`{}`); - }); - }); -}); diff --git a/src/namespaces/apoc/util.ts b/src/namespaces/apoc/util.ts deleted file mode 100644 index 00677ac7..00000000 --- a/src/namespaces/apoc/util.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { CypherFunction } from "../../expressions/functions/CypherFunctions"; -import type { ListExpr as List } from "../../expressions/list/ListExpr"; -import type { MapExpr as Map } from "../../expressions/map/MapExpr"; -import { VoidCypherProcedure } from "../../procedures/CypherProcedure"; -import { Literal } from "../../references/Literal"; -import type { Predicate } from "../../types"; -import { normalizeVariable } from "../../utils/normalize-variable"; - -/** - * @group Procedures - * @deprecated apoc methods will no longer be supported in Cypher Builder version 3 - * @see [Apoc Documentation](https://neo4j.com/docs/apoc/current/overview/apoc.util/apoc.util.validate/) - */ -export function validate( - predicate: Predicate, - message: string | Literal, - params: List | Literal | Map = new Literal([0]) -): VoidCypherProcedure { - const messageVar = normalizeVariable(message); - return new VoidCypherProcedure("apoc.util.validate", [predicate, messageVar, params]); -} - -/** - * @group Functions - * @deprecated apoc methods will no longer be supported in Cypher Builder version 3 - * @see [Apoc Documentation](https://neo4j.com/docs/apoc/current/overview/apoc.util/apoc.util.validatePredicate/) - */ -export function validatePredicate(predicate: Predicate, message: string): CypherFunction { - const defaultParam = new Literal([0]); - - return new CypherFunction("apoc.util.validatePredicate", [predicate, new Literal(message), defaultParam]); -} diff --git a/src/pattern/Pattern.test.ts b/src/pattern/Pattern.test.ts index 42049b0e..e7db0bff 100644 --- a/src/pattern/Pattern.test.ts +++ b/src/pattern/Pattern.test.ts @@ -192,7 +192,7 @@ describe("Patterns", () => { ); const queryResult = query.build(); expect(queryResult.cypher).toMatchInlineSnapshot( - `"(this0:Person:Actor { name: $param0, surname: $param1 })-[this1:ACTED_IN { roles: $param2 }]->(this2)"` + `"(this0:Person&Actor { name: $param0, surname: $param1 })-[this1:ACTED_IN { roles: $param2 }]->(this2)"` ); expect(queryResult.params).toMatchInlineSnapshot(` @@ -226,7 +226,7 @@ describe("Patterns", () => { ); const queryResult = query.build(); expect(queryResult.cypher).toMatchInlineSnapshot( - `"(this0:Person:Actor)-[this1:ACTED_IN { roles: (\\"The \\" + \\"Matrix\\") }]->(this2)"` + `"(this0:Person&Actor)-[this1:ACTED_IN { roles: (\\"The \\" + \\"Matrix\\") }]->(this2)"` ); expect(queryResult.params).toMatchInlineSnapshot(`{}`); @@ -283,9 +283,7 @@ describe("Patterns", () => { const a = new Cypher.Node(); const rel = new Cypher.Variable(); - const query = new TestClause( - new Cypher.Pattern(a as Cypher.Node | undefined).related(rel).to(a as Cypher.Node | undefined) - ); + const query = new TestClause(new Cypher.Pattern(a).related(rel).to(a)); const queryResult = query.build(); expect(queryResult.cypher).toMatchInlineSnapshot(`"(this0)-[var1]->(this0)"`); @@ -429,7 +427,7 @@ describe("Patterns", () => { }); describe("Where predicate", () => { - it("Node pattern with where predicate", () => { + test("Node pattern with where predicate", () => { const node = new Cypher.Node(); const pattern = new Cypher.Pattern(node, { labels: ["TestLabel"] }).where( @@ -439,7 +437,7 @@ describe("Patterns", () => { expect(queryResult.cypher).toMatchInlineSnapshot(`"(this0:TestLabel WHERE this0.name = \\"Keanu\\")"`); }); - it("Node pattern with where predicate and properties", () => { + test("Node pattern with where predicate and properties", () => { const node = new Cypher.Node(); const pattern = new Cypher.Pattern(node, { @@ -457,7 +455,7 @@ describe("Patterns", () => { ); }); - it("Node pattern with where predicate in target node", () => { + test("Node pattern with where predicate in target node", () => { const node = new Cypher.Node(); const pattern = new Cypher.Pattern(node, { labels: ["TestLabel"] }) @@ -470,7 +468,7 @@ describe("Patterns", () => { ); }); - it("Relationship pattern with where predicate", () => { + test("Relationship pattern with where predicate", () => { const node = new Cypher.Node(); const relationship = new Cypher.Relationship(); @@ -484,7 +482,7 @@ describe("Patterns", () => { ); }); - it("Relationship pattern with where predicate and properties", () => { + test("Relationship pattern with where predicate and properties", () => { const node = new Cypher.Node(); const relationship = new Cypher.Relationship(); diff --git a/src/pattern/labels-to-string.ts b/src/pattern/labels-to-string.ts index c490f1c7..6a8dabfa 100644 --- a/src/pattern/labels-to-string.ts +++ b/src/pattern/labels-to-string.ts @@ -44,7 +44,7 @@ function labelOrTypeToString( escapeFunc: (s: string) => string ): string { if (elements instanceof LabelExpr) { - return addLabelToken(env.config.labelOperator, elements.getCypher(env)); + return addLabelToken(elements.getCypher(env)); } else { const escapedLabels = asArray(elements).map((label: string | Expr) => { if (typeof label === "string") { @@ -54,6 +54,6 @@ function labelOrTypeToString( } }); - return addLabelToken(env.config.labelOperator, ...escapedLabels); + return addLabelToken(...escapedLabels); } } diff --git a/src/procedures/CypherProcedure.test.ts b/src/procedures/CypherProcedure.test.ts index ff9492ad..b6d5039d 100644 --- a/src/procedures/CypherProcedure.test.ts +++ b/src/procedures/CypherProcedure.test.ts @@ -134,7 +134,7 @@ describe("Procedures", () => { }); describe("Procedure with Yield and nested clauses", () => { - it("Procedure with Where", () => { + test("Procedure with Where", () => { const procedure = new Cypher.Procedure("custom-procedure").yield("test"); procedure.where(Cypher.true).and(Cypher.false).return("*"); @@ -148,7 +148,7 @@ RETURN *" expect(params).toMatchInlineSnapshot(`{}`); }); - it("Procedure with Delete", () => { + test("Procedure with Delete", () => { const procedure = new Cypher.Procedure("custom-procedure").yield("test"); procedure.delete(new Cypher.Node()); @@ -161,7 +161,7 @@ DELETE this0" expect(params).toMatchInlineSnapshot(`{}`); }); - it("Procedure with Detach Delete", () => { + test("Procedure with Detach Delete", () => { const procedure = new Cypher.Procedure("custom-procedure").yield("test"); procedure.detachDelete(new Cypher.Node()); @@ -174,7 +174,7 @@ DETACH DELETE this0" expect(params).toMatchInlineSnapshot(`{}`); }); - it("Procedure with Remove", () => { + test("Procedure with Remove", () => { const procedure = new Cypher.Procedure("custom-procedure").yield("test"); procedure.remove(new Cypher.Node().property("test")); @@ -187,7 +187,7 @@ REMOVE this0.test" expect(params).toMatchInlineSnapshot(`{}`); }); - it("Procedure with Set", () => { + test("Procedure with Set", () => { const procedure = new Cypher.Procedure("custom-procedure").yield("test"); procedure.set([new Cypher.Variable().property("test"), new Cypher.Literal("hello")]); @@ -201,7 +201,7 @@ SET expect(params).toMatchInlineSnapshot(`{}`); }); - it("Procedure with Unwind", () => { + test("Procedure with Unwind", () => { const yieldVar = new Cypher.Variable(); const procedure = new Cypher.Procedure("custom-procedure").yield(["test", yieldVar]); @@ -215,7 +215,7 @@ UNWIND var0 AS var1" expect(params).toMatchInlineSnapshot(`{}`); }); - it("Procedure with Merge", () => { + test("Procedure with Merge", () => { const procedure = new Cypher.Procedure("custom-procedure").yield("test"); procedure.merge(new Cypher.Pattern(new Cypher.Node())); @@ -228,7 +228,7 @@ MERGE (this0)" expect(params).toMatchInlineSnapshot(`{}`); }); - it("Procedure with Create", () => { + test("Procedure with Create", () => { const procedure = new Cypher.Procedure("custom-procedure").yield("test"); procedure.create(new Cypher.Pattern(new Cypher.Node())); @@ -241,7 +241,7 @@ CREATE (this0)" expect(params).toMatchInlineSnapshot(`{}`); }); - it("Procedure with Order by", () => { + test("Procedure with Order by", () => { const testVar = new Cypher.NamedVariable("test"); const procedure = new Cypher.Procedure("custom-procedure").yield("test").orderBy(testVar).skip(1).limit(10); diff --git a/src/references/Label.ts b/src/references/Label.ts index 3510a158..cb6b7594 100644 --- a/src/references/Label.ts +++ b/src/references/Label.ts @@ -53,12 +53,12 @@ export class Label extends CypherASTNode { /** @internal */ public getCypher(env: CypherEnvironment): string { const nodeId = this.node.getCypher(env); - const labelsStr = this.generateLabelExpressionStr(env); + const labelsStr = this.generateLabelExpressionStr(); return `${nodeId}${labelsStr}`; } - private generateLabelExpressionStr(env: CypherEnvironment): string { - return addLabelToken(env.config.labelOperator, escapeLabel(this.label)); + private generateLabelExpressionStr(): string { + return addLabelToken(escapeLabel(this.label)); } } @@ -77,7 +77,7 @@ export class DynamicLabel extends Label { public getCypher(env: CypherEnvironment): string { const nodeId = this.node.getCypher(env); const exprStr = `$(${this.expr.getCypher(env)})`; - const labelStr = addLabelToken(env.config.labelOperator, exprStr); + const labelStr = addLabelToken(exprStr); return `${nodeId}${labelStr}`; } } diff --git a/src/types.ts b/src/types.ts index 4519ceec..e3fe8902 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,12 +41,6 @@ import type { Literal } from "./references/Literal"; import type { PropertyRef } from "./references/PropertyRef"; import type { Variable } from "./references/Variable"; -/** - * @group Expressions - * @deprecated This type will no longer be exported, use {@link Expr} instead - */ -export type Operation = BooleanOp | ComparisonOp | MathOp | ConcatOp; - /** Represents a Cypher Expression * @group Expressions * @see {@link https://neo4j.com/docs/cypher-manual/current/syntax/expressions/ | Cypher Documentation} diff --git a/src/utils/add-label-token.test.ts b/src/utils/add-label-token.test.ts index 17c00b61..8814f1c0 100644 --- a/src/utils/add-label-token.test.ts +++ b/src/utils/add-label-token.test.ts @@ -19,24 +19,24 @@ import { addLabelToken } from "./add-label-token"; -describe.each([":", "&"] as const)("addLabelToken", (operator) => { - test("addLabelToken without labels using operator %s", () => { - const result = addLabelToken(operator); +describe("addLabelToken", () => { + test("addLabelToken without labels", () => { + const result = addLabelToken(); expect(result).toBe(""); }); - test("addLabelToken with a single label using operator %s", () => { - const result = addLabelToken(operator, "Movie"); + test("addLabelToken with a single label", () => { + const result = addLabelToken("Movie"); expect(result).toBe(":Movie"); }); - test("addLabelToken with two labels using operator %s", () => { - const result = addLabelToken(operator, "Movie", "Film"); - expect(result).toBe(`:Movie${operator}Film`); + test("addLabelToken with two labels", () => { + const result = addLabelToken("Movie", "Film"); + expect(result).toBe(`:Movie&Film`); }); - test("addLabelToken with multiple labels using operator %s", () => { - const result = addLabelToken(operator, "Movie", "Film", "Video"); - expect(result).toBe(`:Movie${operator}Film${operator}Video`); + test("addLabelToken with multiple labels", () => { + const result = addLabelToken("Movie", "Film", "Video"); + expect(result).toBe(`:Movie&Film&Video`); }); }); diff --git a/src/utils/add-label-token.ts b/src/utils/add-label-token.ts index 2720c975..626f90fb 100644 --- a/src/utils/add-label-token.ts +++ b/src/utils/add-label-token.ts @@ -18,11 +18,11 @@ */ /** Generates a string with all the labels. For example `:Movie&Film` */ -export function addLabelToken(andToken: ":" | "&", ...labels: string[]): string { +export function addLabelToken(...labels: string[]): string { const firstLabel = labels.shift(); if (!firstLabel) return ""; - const extraLabels = labels.map((label) => `${andToken}${label}`).join(""); + const extraLabels = labels.map((label) => `&${label}`).join(""); return `:${firstLabel}${extraLabels}`; } diff --git a/src/utils/utils.test.ts b/src/utils/utils.test.ts index 9f055031..8299a429 100644 --- a/src/utils/utils.test.ts +++ b/src/utils/utils.test.ts @@ -83,7 +83,7 @@ describe("CypherBuilder Utils", () => { }); }); - it("toCypherParams", () => { + test("toCypherParams", () => { const cypherParams = Cypher.utils.toCypherParams({ param1: "my param", param2: 5, diff --git a/tests/build-config/cypher-version.test.ts b/tests/build-config/cypher-version.test.ts index 375f87a0..17f9ffbd 100644 --- a/tests/build-config/cypher-version.test.ts +++ b/tests/build-config/cypher-version.test.ts @@ -20,7 +20,7 @@ import Cypher from "../../src"; describe("Cypher version", () => { - test("Add cypher version on .build", () => { + test("Add cypher version 5 on .build", () => { const movieNode = new Cypher.Node(); const pattern = new Cypher.Pattern(movieNode, { labels: ["Movie"] }); @@ -39,6 +39,28 @@ describe("Cypher version", () => { MATCH (this0:Movie) WHERE this0.title = $param0 RETURN this0.title" +`); + }); + + test("Add cypher version 25 on .build", () => { + const movieNode = new Cypher.Node(); + const pattern = new Cypher.Pattern(movieNode, { labels: ["Movie"] }); + + const matchQuery = new Cypher.Match(pattern) + .where(movieNode, { + title: new Cypher.Param("The Matrix"), + }) + .return(movieNode.property("title")); + + const { cypher } = matchQuery.build({ + cypherVersion: "25", + }); + + expect(cypher).toMatchInlineSnapshot(` +"CYPHER 25 +MATCH (this0:Movie) +WHERE this0.title = $param0 +RETURN this0.title" `); }); }); diff --git a/tests/build-config/label-operator.test.ts b/tests/build-config/label-operator.test.ts deleted file mode 100644 index f807b5e6..00000000 --- a/tests/build-config/label-operator.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 Cypher from "../../src"; -import { TestClause } from "../../src/utils/TestClause"; - -describe.each([":", "&"] as const)("Config.labelOperator", (labelOperator) => { - test("Pattern", () => { - const node = new Cypher.Node(); - const query = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie", "Film"] })); - - const queryResult = new TestClause(query).build({ - labelOperator, - }); - - expect(queryResult.cypher).toBe(`MATCH (this0:Movie${labelOperator}Film)`); - }); - - test("hasLabel", () => { - const node = new Cypher.Node(); - const query = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })).where( - node.hasLabels("Movie", "Film") - ); - - const queryResult = new TestClause(query).build({ - labelOperator, - }); - - expect(queryResult.cypher).toBe(`MATCH (this0:Movie) -WHERE this0:Movie${labelOperator}Film`); - }); -}); diff --git a/tests/clause-chaining.test.ts b/tests/clause-chaining.test.ts index bf73e7e4..efc637e3 100644 --- a/tests/clause-chaining.test.ts +++ b/tests/clause-chaining.test.ts @@ -99,7 +99,7 @@ describe("Clause chaining", () => { const movieNode = new Cypher.Node(); const createMovie = new Cypher.Create(new Cypher.Pattern(movieNode)).set([movieNode.property("id"), variable]); - const clause = new Cypher.Foreach(variable, list, createMovie); + const clause = new Cypher.Foreach(variable).in(list).do(createMovie); it.each(["return", "remove", "set", "delete", "detachDelete", "with", "merge", "create"] as const)( "Foreach.%s", diff --git a/tests/deprecated/Foreach.test.ts b/tests/deprecated/Foreach.test.ts deleted file mode 100644 index 3c08d81a..00000000 --- a/tests/deprecated/Foreach.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 Cypher from "../../src"; - -describe("Foreach", () => { - test("Foreach create", () => { - const list = new Cypher.Literal([1, 2, 3]); - const variable = new Cypher.Variable(); - - const movieNode = new Cypher.Node(); - const createMovie = new Cypher.Create(new Cypher.Pattern(movieNode, { labels: ["Movie"] })).set([ - movieNode.property("id"), - variable, - ]); - - const foreachClause = new Cypher.Foreach(variable, list, createMovie).with("*"); - - const queryResult = foreachClause.build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` - "FOREACH (var0 IN [1, 2, 3] | - CREATE (this1:Movie) - SET - this1.id = var0 - ) - WITH *" - `); - - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); - - test("Foreach create with set, remove and delete", () => { - const list = new Cypher.Literal([1, 2, 3]); - const variable = new Cypher.Variable(); - - const movieNode = new Cypher.Node(); - const createMovie = new Cypher.Create(new Cypher.Pattern(movieNode, { labels: ["Movie"] })); - - const foreachClause = new Cypher.Foreach(variable, list, createMovie) - .remove(movieNode.property("title")) - .set([movieNode.property("id"), variable]) - .delete(movieNode) - .with("*"); - - const queryResult = foreachClause.build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` -"FOREACH (var0 IN [1, 2, 3] | - CREATE (this1:Movie) -) -REMOVE this1.title -SET - this1.id = var0 -DELETE this1 -WITH *" -`); - - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); - - test("Foreach create detachDelete", () => { - const list = new Cypher.Literal([1, 2, 3]); - const variable = new Cypher.Variable(); - - const movieNode = new Cypher.Node(); - const createMovie = new Cypher.Create(new Cypher.Pattern(movieNode, { labels: ["Movie"] })); - - const foreachClause = new Cypher.Foreach(variable, list, createMovie).detachDelete(movieNode).with("*"); - - const queryResult = foreachClause.build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` -"FOREACH (var0 IN [1, 2, 3] | - CREATE (this1:Movie) -) -DETACH DELETE this1 -WITH *" -`); - - expect(queryResult.params).toMatchInlineSnapshot(`{}`); - }); -}); diff --git a/tests/deprecated/concat.test.ts b/tests/deprecated/concat.test.ts deleted file mode 100644 index ce35cdfe..00000000 --- a/tests/deprecated/concat.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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 Cypher from "../../src"; - -describe("CypherBuilder utils.concat", () => { - test("concatenates Match and Return", () => { - const node = new Cypher.Node(); - - const clause = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })).where( - Cypher.eq(new Cypher.Param("aa"), new Cypher.Param("bb")) - ); - const returnClause = new Cypher.Return([node.property("title"), "movie"]); - - const query = Cypher.utils.concat(clause, returnClause); - - const queryResult = query.build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` - "MATCH (this0:Movie) - WHERE $param0 = $param1 - RETURN this0.title AS movie" - `); - - expect(queryResult.params).toMatchInlineSnapshot(` - { - "param0": "aa", - "param1": "bb", - } - `); - expect(query.children).toHaveLength(2); - }); - - test("Empty composite clause", () => { - const compositeClause = Cypher.utils.concat(undefined); - expect(compositeClause.empty).toBeTrue(); - expect(compositeClause.children).toHaveLength(0); - - const queryResult = compositeClause.build(); - - expect(queryResult.cypher).toMatchInlineSnapshot(`""`); - }); - - test("Empty nested composite clause", () => { - const compositeClause = Cypher.utils.concat(Cypher.utils.concat()); - expect(compositeClause.empty).toBeTrue(); - expect(compositeClause.children).toHaveLength(0); - - const queryResult = compositeClause.build(); - - expect(queryResult.cypher).toMatchInlineSnapshot(`""`); - }); - - test("Nested composite clause with multiple elements", () => { - const compositeClause = Cypher.utils.concat( - Cypher.utils.concat( - new Cypher.Match(new Cypher.Pattern(new Cypher.Node())), - new Cypher.Match(new Cypher.Pattern(new Cypher.Node())) - ) - ); - expect(compositeClause.empty).toBeFalse(); - expect(compositeClause.children).toHaveLength(1); - - const queryResult = compositeClause.build(); - - expect(queryResult.cypher).toMatchInlineSnapshot(` - "MATCH (this0) - MATCH (this1)" - `); - }); - - test("Non-Empty composite clause", () => { - const node = new Cypher.Node(); - - const clause = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })).where( - Cypher.eq(new Cypher.Param("aa"), new Cypher.Param("bb")) - ); - const compositeClause = Cypher.utils.concat(clause); - expect(compositeClause.empty).toBeFalse(); - expect(compositeClause.children).toHaveLength(1); - }); - - test("Nested concatenation flattens the tree if composite clause has 1 element", () => { - const node = new Cypher.Node(); - - const clause = new Cypher.Match(new Cypher.Pattern(node, { labels: ["Movie"] })).where( - Cypher.eq(new Cypher.Param("aa"), new Cypher.Param("bb")) - ); - const returnClause = new Cypher.Return([node.property("title"), "movie"]); - - const nestedConcat = Cypher.utils.concat(clause); - - const topLevelConcat = Cypher.utils.concat(nestedConcat, returnClause); - - const queryResult = topLevelConcat.build(); - expect(queryResult.cypher).toMatchInlineSnapshot(` - "MATCH (this0:Movie) - WHERE $param0 = $param1 - RETURN this0.title AS movie" - `); - - expect(queryResult.params).toMatchInlineSnapshot(` - { - "param0": "aa", - "param1": "bb", - } - `); - - // Three children as nested concat was flattened - expect(topLevelConcat.children).toHaveLength(2); - }); -}); diff --git a/tests/issues/479.test.ts b/tests/issues/479.test.ts index b9ca5ad8..bd9ffd2e 100644 --- a/tests/issues/479.test.ts +++ b/tests/issues/479.test.ts @@ -116,7 +116,7 @@ RETURN count(this1)" expect(queryResult.cypher).toMatchInlineSnapshot(` "WITH $param0 AS var0 -MATCH (this1:$(var0):\`normal$Label\`:$($param1)) +MATCH (this1:$(var0)&\`normal$Label\`&$($param1)) RETURN count(this1)" `); diff --git a/tsconfig.json b/tsconfig.json index 76c5874e..6e6e4f6f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,10 @@ { - "extends": "@tsconfig/node16/tsconfig.json", + "extends": "@tsconfig/node20/tsconfig.json", "compilerOptions": { "declaration": true, "declarationMap": true, "sourceMap": true, "resolveJsonModule": true, - "experimentalDecorators": true, "noImplicitAny": true, "rootDir": ".", "baseUrl": ".",