Skip to content

Commit f224da6

Browse files
authored
Merge pull request #3242 from angrykoala/escape-properties-and-relationships
Fix escaping for relationships
2 parents e5dcc07 + e1a7b26 commit f224da6

File tree

12 files changed

+175
-19
lines changed

12 files changed

+175
-19
lines changed

.changeset/fluffy-suns-invite.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@neo4j/graphql": major
3+
"@neo4j/cypher-builder": minor
4+
---
5+
6+
Escape properties and relationships if needed, using | and & as part of the label is no longer supported

packages/cypher-builder/src/expressions/map/MapExpr.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe("Map Expression", () => {
5353
const queryResult = new TestClause(map).build();
5454

5555
expect(queryResult.cypher).toMatchInlineSnapshot(
56-
`"{ key: $param0, value2: \\"Override\\", value3: \\"another value\\" }"`
56+
`"{ key: $param0, \`value2\`: \\"Override\\", \`value3\`: \\"another value\\" }"`
5757
);
5858

5959
expect(queryResult.params).toMatchInlineSnapshot(`

packages/cypher-builder/src/expressions/map/MapProjection.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type { Variable } from "../../references/Variable";
2323
import { serializeMap } from "../../utils/serialize-map";
2424
import { MapExpr } from "./MapExpr";
2525
import { isString } from "../../utils/is-string";
26+
import { escapeProperty } from "../../utils/escape";
2627

2728
/** Represents a Map projection
2829
* @see [Cypher Documentation](https://neo4j.com/docs/cypher-manual/current/syntax/maps/#cypher-map-projection)
@@ -87,7 +88,7 @@ export class MapProjection implements CypherCompilable {
8788
const variableStr = this.variable.getCypher(env);
8889
const extraValuesStr = serializeMap(env, this.extraValues, true);
8990

90-
const projectionStr = this.projection.map((p) => `.${p}`).join(", ");
91+
const projectionStr = this.projection.map((p) => `.${escapeProperty(p)}`).join(", ");
9192

9293
const commaStr = extraValuesStr && projectionStr ? ", " : "";
9394

packages/cypher-builder/src/utils/serialize-map.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,18 @@ import { serializeMap } from "./serialize-map";
2424
describe("serializeMap", () => {
2525
const env = new CypherEnvironment();
2626
const map = new Map<string, Cypher.Expr>([
27-
["test1", new Cypher.Literal(10)],
27+
["test", new Cypher.Literal(10)],
28+
["test$", new Cypher.Literal(10)],
2829
["expr", Cypher.reverse(new Cypher.Literal([1]))],
2930
]);
3031

3132
test("serialize a map of expressions", () => {
3233
const result = serializeMap(env, map);
33-
expect(result).toBe("{ test1: 10, expr: reverse([1]) }");
34+
expect(result).toBe("{ test: 10, `test$`: 10, expr: reverse([1]) }");
3435
});
3536

3637
test("serialize a map of expressions without curly braces", () => {
3738
const result = serializeMap(env, map, true);
38-
expect(result).toBe("test1: 10, expr: reverse([1])");
39+
expect(result).toBe("test: 10, `test$`: 10, expr: reverse([1])");
3940
});
4041
});

packages/cypher-builder/src/utils/serialize-map.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@
1919

2020
import type { Expr } from "../types";
2121
import type { CypherEnvironment } from "../Environment";
22+
import { escapeProperty } from "./escape";
2223

2324
export function serializeMap(env: CypherEnvironment, map: Map<string, Expr>, omitCurlyBraces = false): string {
2425
const serializedFields: string[] = [];
2526

2627
for (const [key, value] of map.entries()) {
2728
if (value) {
28-
const fieldStr = `${key}: ${value.getCypher(env)}`;
29+
const fieldStr = `${escapeProperty(key)}: ${value.getCypher(env)}`;
2930
serializedFields.push(fieldStr);
3031
}
3132
}

packages/cypher-builder/src/utils/stringify-object.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
* limitations under the License.
1818
*/
1919

20+
import { escapeProperty } from "./escape";
21+
2022
/** Serializes object into a string for Cypher objects */
2123
export function stringifyObject(fields: Record<string, string | undefined | null>): string {
2224
return `{ ${Object.entries(fields)
2325
.filter(([, value]) => Boolean(value))
2426
.map(([key, value]): string | undefined => {
25-
return `${key}: ${value}`;
27+
return `${escapeProperty(key)}: ${value}`;
2628
})
2729
.join(", ")} }`;
2830
}

packages/cypher-builder/src/utils/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@
1919

2020
// Note: This file exists for exported utils to the user
2121

22-
export { escapeLabel } from "./escape";
22+
export { escapeLabel, escapeType, escapeProperty } from "./escape";

packages/graphql/src/schema/get-alias-meta.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function getAliasMeta(directive: DirectiveNode): AliasMeta | undefined {
3434
const property = (stmtArg.value as StringValueNode).value;
3535

3636
return {
37-
property: Cypher.utils.escapeLabel(property),
37+
property: Cypher.utils.escapeProperty(property),
3838
propertyUnescaped: property,
3939
};
4040
}

packages/graphql/src/translate/create-connect-or-create-and-params.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ function getAutogeneratedParams(node: Node | Relationship): Record<string, Cyphe
330330
function getCypherParameters(onCreateParams: Record<string, any> = {}, node?: Node): Record<string, Cypher.Param<any>> {
331331
const params = Object.entries(onCreateParams).reduce((acc, [key, value]) => {
332332
const nodeField = node?.constrainableFields.find((f) => f.fieldName === key);
333-
const nodeFieldName = nodeField?.dbPropertyName || nodeField?.fieldName;
333+
const nodeFieldName = nodeField?.dbPropertyNameUnescaped || nodeField?.fieldName;
334334
const fieldName = nodeFieldName || key;
335335
const valueOrArray = nodeField?.typeMeta.array ? asArray(value) : value;
336336
acc[fieldName] = valueOrArray;

packages/graphql/tests/tck/aggregations/alias-directive.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe("Cypher Aggregations Many with Alias directive", () => {
3232
type Movie {
3333
id: ID! @alias(property: "_id")
3434
title: String! @alias(property: "_title")
35-
imdbRating: Int! @alias(property: "_imdbRating")
35+
imdbRating: Int! @alias(property: "_imdb Rating")
3636
createdAt: DateTime! @alias(property: "_createdAt")
3737
}
3838
`;
@@ -74,21 +74,21 @@ describe("Cypher Aggregations Many with Alias directive", () => {
7474

7575
expect(formatCypher(result.cypher)).toMatchInlineSnapshot(`
7676
"MATCH (this:\`Movie\`)
77-
RETURN { id: { shortest: min(this.\`_id\`), longest: max(this.\`_id\`) }, title: { shortest:
78-
reduce(aggVar = collect(this.\`_title\`)[0], current IN collect(this.\`_title\`) |
77+
RETURN { id: { shortest: min(this._id), longest: max(this._id) }, title: { shortest:
78+
reduce(aggVar = collect(this._title)[0], current IN collect(this._title) |
7979
CASE
8080
WHEN size(current) < size(aggVar) THEN current
8181
ELSE aggVar
8282
END
8383
)
8484
, longest:
85-
reduce(aggVar = collect(this.\`_title\`)[0], current IN collect(this.\`_title\`) |
85+
reduce(aggVar = collect(this._title)[0], current IN collect(this._title) |
8686
CASE
8787
WHEN size(current) > size(aggVar) THEN current
8888
ELSE aggVar
8989
END
9090
)
91-
}, imdbRating: { min: min(this.\`_imdbRating\`), max: max(this.\`_imdbRating\`), average: avg(this.\`_imdbRating\`) }, createdAt: { min: apoc.date.convertFormat(toString(min(this.\`_createdAt\`)), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), max: apoc.date.convertFormat(toString(max(this.\`_createdAt\`)), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\") } }"
91+
}, imdbRating: { min: min(this.\`_imdb Rating\`), max: max(this.\`_imdb Rating\`), average: avg(this.\`_imdb Rating\`) }, createdAt: { min: apoc.date.convertFormat(toString(min(this._createdAt)), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), max: apoc.date.convertFormat(toString(max(this._createdAt)), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\") } }"
9292
`);
9393

9494
expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`);

0 commit comments

Comments
 (0)