Skip to content

Commit 31ea6b3

Browse files
authored
Merge pull request #6675 from neo4j/cypher-ignoreGeneratedLimit
Add ignoreGeneratedLimit flag
2 parents 5406985 + 6dc90e9 commit 31ea6b3

File tree

7 files changed

+696
-1
lines changed

7 files changed

+696
-1
lines changed

.changeset/orange-apes-ring.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@neo4j/graphql": patch
3+
---
4+
5+
Adds feature flag `cypherDirective.disableGeneratedLimit` to disable default limits to be applied on queries using `@cypher` directive

packages/graphql/src/translate/queryAST/factory/OperationFactory.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { filterTruthy, isRecord } from "../../../utils/utils";
3232
import type { Filter } from "../ast/filters/Filter";
3333
import type { AggregationOperation } from "../ast/operations/AggregationOperation";
3434
import type { ConnectionReadOperation } from "../ast/operations/ConnectionReadOperation";
35-
import type { CypherOperation } from "../ast/operations/CypherOperation";
35+
import { CypherOperation } from "../ast/operations/CypherOperation";
3636
import type { CypherScalarOperation } from "../ast/operations/CypherScalarOperation";
3737
import type { ReadOperation } from "../ast/operations/ReadOperation";
3838
import type { CompositeAggregationOperation } from "../ast/operations/composite/CompositeAggregationOperation";
@@ -351,6 +351,11 @@ export class OperationsFactory {
351351
const sort = this.sortAndPaginationFactory.createSortFields(sortOptions, entity, context);
352352
operation.addSort(...sort);
353353

354+
// We don't want to generate the limit operation on custom cypher fields if the flag "disableGeneratedLimit" is set
355+
if (operation instanceof CypherOperation && context.features.cypherDirective?.disableGeneratedLimit) {
356+
return operation;
357+
}
358+
354359
const pagination = this.sortAndPaginationFactory.createPagination(sortOptions);
355360
if (pagination) {
356361
operation.addPagination(pagination);

packages/graphql/src/types/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,11 @@ export type Neo4jFeaturesSettings = {
504504
disableRelationshipTypeEscaping?: boolean;
505505
};
506506
vector?: Neo4jVectorSettings;
507+
/** Settings for the `@cypher` directive */
508+
cypherDirective?: {
509+
/** Enabling this setting, the limit generated by the directive `@limit` won't be added to fields with custom cypher */
510+
disableGeneratedLimit?: boolean;
511+
};
507512
};
508513

509514
/** Parsed features used in context */
@@ -513,6 +518,9 @@ export type ContextFeatures = {
513518
authorization?: Neo4jAuthorizationSettings;
514519
subscriptions?: Neo4jGraphQLSubscriptionsEngine;
515520
unsafeEscapeOptions?: Neo4jFeaturesSettings["unsafeEscapeOptions"];
521+
cypherDirective?: {
522+
disableGeneratedLimit?: boolean;
523+
};
516524
};
517525

518526
export type PredicateReturn = {
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import type { UniqueType } from "../../../../utils/graphql-types";
21+
import { TestHelper } from "../../../../utils/tests-helper";
22+
23+
describe("Cypher directive with limit disableGeneratedLimit flag", () => {
24+
const testHelper = new TestHelper();
25+
let typeDefs: string;
26+
27+
let Tag: UniqueType;
28+
let TagGroup: UniqueType;
29+
30+
beforeEach(async () => {
31+
Tag = testHelper.createUniqueType("Tag");
32+
TagGroup = testHelper.createUniqueType("TagGroup");
33+
34+
typeDefs = /* GraphQL */ `
35+
type ${Tag} @limit(default: 2, max: 100) @node {
36+
name: String!
37+
}
38+
39+
type ${TagGroup} @node {
40+
id: ID
41+
tags(tagNames: [String]!, limit: Int): [${Tag}]
42+
@cypher(
43+
statement: """
44+
MATCH (t:${Tag})
45+
WHERE ANY(name IN $tagNames WHERE t.displayName = toLower(name))
46+
RETURN t
47+
LIMIT $limit
48+
"""
49+
columnName: "t"
50+
)
51+
}
52+
53+
type Query {
54+
getTagsByNames(tagNames: [String]!, limit: Int): [${Tag}]
55+
@cypher(
56+
statement: """
57+
MATCH (t:${Tag})
58+
WHERE ANY(name IN $tagNames
59+
WHERE t.displayName = toLower(name))
60+
RETURN t
61+
LIMIT $limit
62+
"""
63+
columnName: "t"
64+
)
65+
}
66+
`;
67+
68+
await testHelper.initNeo4jGraphQL({
69+
typeDefs,
70+
features: {
71+
cypherDirective: {
72+
disableGeneratedLimit: true,
73+
},
74+
},
75+
});
76+
});
77+
78+
afterEach(async () => {
79+
await testHelper.close();
80+
});
81+
82+
test("Top level cypher with limit param does not apply default limit", async () => {
83+
const query = /* GraphQL */ `
84+
query {
85+
getTagsByNames(tagNames: ["a", "b", "c"], limit: 50) {
86+
name
87+
}
88+
}
89+
`;
90+
91+
await testHelper.executeCypher(`
92+
CREATE(:${Tag} {name: "a", displayName: "a"})
93+
CREATE(:${Tag} {name: "a", displayName: "a"})
94+
CREATE(:${Tag} {name: "a", displayName: "a"})
95+
`);
96+
97+
const queryResult = await testHelper.executeGraphQL(query);
98+
99+
expect(queryResult.errors).toBeUndefined();
100+
expect(queryResult.data).toEqual({
101+
getTagsByNames: [
102+
{
103+
name: "a",
104+
},
105+
{ name: "a" },
106+
{ name: "a" },
107+
],
108+
});
109+
});
110+
111+
test("Nested cypher with limit param does not apply default limit", async () => {
112+
const query = /* GraphQL */ `
113+
query {
114+
${TagGroup.plural} {
115+
id
116+
tags(tagNames: ["a"], limit: 14) {
117+
name
118+
}
119+
}
120+
}
121+
`;
122+
123+
await testHelper.executeCypher(`
124+
CREATE(:${Tag} {name: "a", displayName: "a"})
125+
CREATE(:${Tag} {name: "a", displayName: "a"})
126+
CREATE(:${Tag} {name: "a", displayName: "a"})
127+
CREATE(:${TagGroup} {id: "1"})
128+
129+
`);
130+
131+
const queryResult = await testHelper.executeGraphQL(query);
132+
133+
expect(queryResult.errors).toBeUndefined();
134+
expect(queryResult.data).toEqual({
135+
[TagGroup.plural]: [
136+
{
137+
id: "1",
138+
tags: [
139+
{
140+
name: "a",
141+
},
142+
{ name: "a" },
143+
{ name: "a" },
144+
],
145+
},
146+
],
147+
});
148+
});
149+
});
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import type { UniqueType } from "../../../../utils/graphql-types";
21+
import { TestHelper } from "../../../../utils/tests-helper";
22+
23+
describe("Cypher directive with limit", () => {
24+
const testHelper = new TestHelper();
25+
let typeDefs: string;
26+
27+
let Tag: UniqueType;
28+
let TagGroup: UniqueType;
29+
30+
beforeEach(async () => {
31+
Tag = testHelper.createUniqueType("Tag");
32+
TagGroup = testHelper.createUniqueType("TagGroup");
33+
34+
typeDefs = /* GraphQL */ `
35+
type ${Tag} @limit(default: 2, max: 100) @node {
36+
name: String!
37+
}
38+
39+
type ${TagGroup} @node {
40+
id: ID
41+
tags(tagNames: [String]!, limit: Int): [${Tag}]
42+
@cypher(
43+
statement: """
44+
MATCH (t:${Tag})
45+
WHERE ANY(name IN $tagNames WHERE t.displayName = toLower(name))
46+
RETURN t
47+
LIMIT $limit
48+
"""
49+
columnName: "t"
50+
)
51+
}
52+
53+
type Query {
54+
getTagsByNames(tagNames: [String]!, limit: Int): [${Tag}]
55+
@cypher(
56+
statement: """
57+
MATCH (t:${Tag})
58+
WHERE ANY(name IN $tagNames
59+
WHERE t.displayName = toLower(name))
60+
RETURN t
61+
LIMIT $limit
62+
"""
63+
columnName: "t"
64+
)
65+
}
66+
`;
67+
68+
await testHelper.initNeo4jGraphQL({
69+
typeDefs,
70+
});
71+
});
72+
73+
afterEach(async () => {
74+
await testHelper.close();
75+
});
76+
77+
test("Top level cypher with limit param applies default limit", async () => {
78+
const query = /* GraphQL */ `
79+
query {
80+
getTagsByNames(tagNames: ["a", "b", "c"], limit: 50) {
81+
name
82+
}
83+
}
84+
`;
85+
86+
await testHelper.executeCypher(`
87+
CREATE(:${Tag} {name: "a", displayName: "a"})
88+
CREATE(:${Tag} {name: "a", displayName: "a"})
89+
CREATE(:${Tag} {name: "a", displayName: "a"})
90+
`);
91+
92+
const queryResult = await testHelper.executeGraphQL(query);
93+
94+
expect(queryResult.errors).toBeUndefined();
95+
expect(queryResult.data).toEqual({
96+
getTagsByNames: [
97+
{
98+
name: "a",
99+
},
100+
{ name: "a" },
101+
],
102+
});
103+
});
104+
105+
test("Nested cypher with limit param applies default limit", async () => {
106+
const query = /* GraphQL */ `
107+
query {
108+
${TagGroup.plural} {
109+
id
110+
tags(tagNames: ["a"], limit: 14) {
111+
name
112+
}
113+
}
114+
}
115+
`;
116+
117+
await testHelper.executeCypher(`
118+
CREATE(:${Tag} {name: "a", displayName: "a"})
119+
CREATE(:${Tag} {name: "a", displayName: "a"})
120+
CREATE(:${Tag} {name: "a", displayName: "a"})
121+
CREATE(:${TagGroup} {id: "1"})
122+
123+
`);
124+
125+
const queryResult = await testHelper.executeGraphQL(query);
126+
127+
expect(queryResult.errors).toBeUndefined();
128+
expect(queryResult.data).toEqual({
129+
[TagGroup.plural]: [
130+
{
131+
id: "1",
132+
tags: [
133+
{
134+
name: "a",
135+
},
136+
{ name: "a" },
137+
],
138+
},
139+
],
140+
});
141+
});
142+
});

0 commit comments

Comments
 (0)