Skip to content

Commit

Permalink
Merge pull request #271 from neo4j/248-use-logical-for-label-matching…
Browse files Browse the repository at this point in the history
…-in-patterns

Add labelOperator option
  • Loading branch information
angrykoala authored Jan 17, 2024
2 parents e86bb9e + 5834c61 commit ce92aeb
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 14 deletions.
24 changes: 24 additions & 0 deletions .changeset/wet-worms-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@neo4j/cypher-builder": patch
---

Add `labelOperator` option on build to change the default label `AND` operator:

```js
const node = new Cypher.Node({ labels: ["Movie", "Film"] });
const query = new Cypher.Match(node);

const queryResult = new TestClause(query).build(
undefined,
{},
{
labelOperator: "&",
}
);
```

Will return:

```cypher
MATCH (this:Movie&Film)
```
2 changes: 1 addition & 1 deletion src/Cypher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export * as db from "./namespaces/db/db";

// Types
export type { CypherEnvironment as Environment } from "./Environment";
export type { Clause } from "./clauses/Clause";
export type { BuildConfig, Clause } from "./clauses/Clause";
export type { Order } from "./clauses/sub-clauses/OrderBy";
export type { ProjectionColumn } from "./clauses/sub-clauses/Projection";
export type { SetParam } from "./clauses/sub-clauses/Set";
Expand Down
17 changes: 16 additions & 1 deletion src/Environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ export type EnvPrefix = {
variables?: string;
};

export type EnvConfig = {
labelOperator: ":" | "&";
};

const defaultConfig: EnvConfig = {
labelOperator: ":",
};

/** Hold the internal references of Cypher parameters and variables
* @group Internal
*/
Expand All @@ -36,10 +44,12 @@ export class CypherEnvironment {
private references: Map<Variable, string> = new Map();
private params: Param[] = [];

public readonly config: EnvConfig;

/**
* @internal
*/
constructor(prefix?: string | EnvPrefix) {
constructor(prefix?: string | EnvPrefix, config: Partial<EnvConfig> = {}) {
if (!prefix || typeof prefix === "string") {
this.globalPrefix = {
params: prefix ?? "",
Expand All @@ -51,6 +61,11 @@ export class CypherEnvironment {
variables: prefix.variables ?? "",
};
}

this.config = {
...defaultConfig,
...config,
};
}

public compile(element: CypherCompilable): string {
Expand Down
17 changes: 12 additions & 5 deletions src/clauses/Clause.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { CypherASTNode } from "../CypherASTNode";
import type { EnvPrefix } from "../Environment";
import type { EnvConfig, EnvPrefix } from "../Environment";
import { CypherEnvironment } from "../Environment";
import type { CypherResult } from "../types";
import { compileCypherIfExists } from "../utils/compile-cypher-if-exists";
Expand All @@ -27,16 +27,23 @@ import { toCypherParams } from "../utils/to-cypher-params";

const customInspectSymbol = Symbol.for("nodejs.util.inspect.custom");

/** Config fields for the .build method */
export type BuildConfig = Partial<EnvConfig>;

/** Represents a clause AST node
* @group Internal
*/
export abstract class Clause extends CypherASTNode {
protected nextClause: Clause | undefined;

/** Compiles a clause into Cypher and params */
public build(prefix?: string | EnvPrefix | undefined, extraParams: Record<string, unknown> = {}): CypherResult {
public build(
prefix?: string | EnvPrefix | undefined,
extraParams: Record<string, unknown> = {},
config?: BuildConfig
): CypherResult {
if (this.isRoot) {
const env = this.getEnv(prefix);
const env = this.getEnv(prefix, config);
const cypher = this.getCypher(env);

const cypherParams = toCypherParams(extraParams);
Expand All @@ -53,8 +60,8 @@ export abstract class Clause extends CypherASTNode {
throw new Error(`Cannot build root: ${root.constructor.name}`);
}

private getEnv(prefix?: string | EnvPrefix): CypherEnvironment {
return new CypherEnvironment(prefix);
private getEnv(prefix?: string | EnvPrefix, config: BuildConfig = {}): CypherEnvironment {
return new CypherEnvironment(prefix, config);
}

/** Custom string for browsers and templating
Expand Down
4 changes: 2 additions & 2 deletions src/expressions/HasLabel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(...escapedLabels);
return addLabelToken(env.config.labelOperator, ...escapedLabels);
} else {
return addLabelToken(this.expectedLabels.getCypher(env));
return addLabelToken(env.config.labelOperator, this.expectedLabels.getCypher(env));
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/pattern/Pattern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ export class Pattern extends PatternElement<NodeRef> {
if (!labelsStr) {
return "";
}
return addLabelToken(labels.getCypher(env));
return addLabelToken(env.config.labelOperator, labels.getCypher(env));
} else {
const escapedLabels = labels.map(escapeLabel);
if (escapedLabels.length === 0) {
return "";
}
return addLabelToken(...escapedLabels);
return addLabelToken(env.config.labelOperator, ...escapedLabels);
}
}
}
9 changes: 6 additions & 3 deletions src/utils/add-label-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
* limitations under the License.
*/

import { LABEL_TOKEN } from "../constants";
export function addLabelToken(andToken: ":" | "&", ...labels: string[]): string {
const firstLabel = labels.shift();
if (!firstLabel) return "";

export function addLabelToken(...labels: string[]): string {
return labels.map((label) => `${LABEL_TOKEN}${label}`).join("");
const extraLabels = labels.map((label) => `${andToken}${label}`).join("");

return `:${firstLabel}${extraLabels}`;
}
46 changes: 46 additions & 0 deletions tests/config/labelOperator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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) => {
const config: Cypher.BuildConfig = {
labelOperator,
};

test("Pattern", () => {
const node = new Cypher.Node({ labels: ["Movie", "Film"] });
const query = new Cypher.Match(node);

const queryResult = new TestClause(query).build(undefined, {}, config);

expect(queryResult.cypher).toBe(`MATCH (this0:Movie${labelOperator}Film)`);
});

test("hasLabel", () => {
const node = new Cypher.Node({ labels: ["Movie"] });
const query = new Cypher.Match(node).where(node.hasLabels("Movie", "Film"));

const queryResult = new TestClause(query).build(undefined, {}, config);

expect(queryResult.cypher).toBe(`MATCH (this0:Movie)
WHERE this0:Movie${labelOperator}Film`);
});
});

0 comments on commit ce92aeb

Please sign in to comment.