Skip to content

Commit 612aa24

Browse files
committed
add nested relationships in groupBy
1 parent d1b1c47 commit 612aa24

File tree

4 files changed

+337
-5
lines changed

4 files changed

+337
-5
lines changed

packages/graphql/src/translate/queryAST/ast/fields/group-by/GroupByField.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import type { Clause, Expr, Variable } from "@neo4j/cypher-builder";
2121
import Cypher from "@neo4j/cypher-builder";
22+
import { wrapSubqueriesInCypherCalls } from "../../../utils/wrap-subquery-in-calls";
2223
import type { QueryASTContext } from "../../QueryASTContext";
2324
import type { QueryASTNode } from "../../QueryASTNode";
2425
import { Field } from "../Field";
@@ -54,13 +55,17 @@ export class GroupByField extends Field {
5455
this.edgeFields = fields;
5556
}
5657

57-
// NOTE: All of this is based on ConnectionReadOperation
58-
// TODO: Unify this logic
58+
// NOTE: Based on ConnectionReadOperation
5959
public getSubqueries(context: QueryASTContext): Clause[] {
6060
if (!context.target) {
6161
throw new Error("Invalid target");
6262
}
6363

64+
const subqueries = wrapSubqueriesInCypherCalls(
65+
context,
66+
[...this.nodeFields, ...this.edgeFields],
67+
[context.target]
68+
);
6469
const edgeProjectionMap = this.createProjectionMapForEdge(context as QueryASTContext<Cypher.Node>);
6570

6671
const groupByFields: Array<[Cypher.Expr, string]> = this.by.map((prop) => {
@@ -70,7 +75,7 @@ export class GroupByField extends Field {
7075
return [context.target.property(prop), prop];
7176
});
7277
const sq = Cypher.utils.concat(
73-
// unwindClause,
78+
...subqueries,
7479
new Cypher.Return(...groupByFields, [
7580
new Cypher.Map({ edges: Cypher.collect(edgeProjectionMap) }),
7681
this.resultVariable,
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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("@groupBy directive top level with nested connection", () => {
24+
const testHelper = new TestHelper();
25+
26+
let Movie: UniqueType;
27+
let Person: UniqueType;
28+
29+
beforeEach(async () => {
30+
Movie = testHelper.createUniqueType("Movie");
31+
Person = testHelper.createUniqueType("Person");
32+
33+
const typeDefs = /* GraphQL */ `
34+
type ${Movie} @node {
35+
title: String!
36+
year: Int! @groupBy
37+
actors: [${Person}!]! @relationship(type: "ACTED_IN", direction: IN)
38+
}
39+
40+
type ${Person} @node {
41+
name: String!
42+
}
43+
`;
44+
45+
await testHelper.initNeo4jGraphQL({
46+
typeDefs,
47+
});
48+
});
49+
50+
afterEach(async () => {
51+
await testHelper.close();
52+
});
53+
54+
test("groupBy in top level query with nested connection in groupBy", async () => {
55+
await testHelper.executeCypher(`
56+
CREATE (:${Movie} {title: "The Matrix", year: 1999})
57+
CREATE (:${Movie} {title: "The Matrix Reloaded", year: 2001})
58+
CREATE (:${Movie} {title: "Another Movie", year: 1999})<-[:ACTED_IN]-(:${Person} { name: "Another Keanu" })
59+
`);
60+
61+
const query = /* GraphQL */ `
62+
query {
63+
${Movie.operations.connection} {
64+
groupBy(fields: {year: true}) {
65+
edges {
66+
node {
67+
title
68+
actorsConnection {
69+
edges {
70+
node {
71+
name
72+
}
73+
}
74+
}
75+
}
76+
}
77+
}
78+
}
79+
}
80+
`;
81+
82+
const result = await testHelper.executeGraphQL(query);
83+
84+
expect(result.errors).toBeUndefined();
85+
expect(result.data).toEqual({
86+
[Movie.operations.connection]: {
87+
groupBy: expect.toIncludeSameMembers([
88+
{
89+
edges: expect.toIncludeSameMembers([
90+
{
91+
node: {
92+
title: "The Matrix",
93+
actorsConnection: { edges: [] },
94+
},
95+
},
96+
{
97+
node: {
98+
title: "Another Movie",
99+
actorsConnection: {
100+
edges: [
101+
{
102+
node: {
103+
name: "Another Keanu",
104+
},
105+
},
106+
],
107+
},
108+
},
109+
},
110+
]),
111+
},
112+
{
113+
edges: [
114+
{
115+
node: {
116+
title: "The Matrix Reloaded",
117+
actorsConnection: { edges: [] },
118+
},
119+
},
120+
],
121+
},
122+
]),
123+
},
124+
});
125+
});
126+
127+
test("groupBy in top level query with nested connection with limit in groupBy", async () => {
128+
await testHelper.executeCypher(`
129+
CREATE (:${Movie} {title: "The Matrix", year: 1999})
130+
CREATE (:${Movie} {title: "The Matrix Reloaded", year: 2001})
131+
CREATE (m:${Movie} {title: "Another Movie", year: 1999})
132+
133+
CREATE(m)<-[:ACTED_IN]-(:${Person} { name: "Actor 1" })
134+
CREATE(m)<-[:ACTED_IN]-(:${Person} { name: "Actor 2" })
135+
CREATE(m)<-[:ACTED_IN]-(:${Person} { name: "Actor 3" })
136+
`);
137+
138+
const query = /* GraphQL */ `
139+
query {
140+
${Movie.operations.connection} {
141+
groupBy(fields: {year: true}) {
142+
edges {
143+
node {
144+
title
145+
actorsConnection(first: 2, sort: {node: {name: ASC}}) {
146+
edges {
147+
node {
148+
name
149+
}
150+
}
151+
}
152+
}
153+
}
154+
}
155+
}
156+
}
157+
`;
158+
159+
const result = await testHelper.executeGraphQL(query);
160+
161+
console.log(result.data);
162+
expect(result.errors).toBeUndefined();
163+
expect(result.data).toEqual({
164+
[Movie.operations.connection]: {
165+
groupBy: expect.toIncludeSameMembers([
166+
{
167+
edges: expect.toIncludeSameMembers([
168+
{
169+
node: {
170+
title: "The Matrix",
171+
actorsConnection: { edges: [] },
172+
},
173+
},
174+
{
175+
node: {
176+
title: "Another Movie",
177+
actorsConnection: {
178+
edges: [
179+
{
180+
node: {
181+
name: "Actor 1",
182+
},
183+
},
184+
{
185+
node: {
186+
name: "Actor 2",
187+
},
188+
},
189+
],
190+
},
191+
},
192+
},
193+
]),
194+
},
195+
{
196+
edges: [
197+
{
198+
node: {
199+
title: "The Matrix Reloaded",
200+
actorsConnection: { edges: [] },
201+
},
202+
},
203+
],
204+
},
205+
]),
206+
},
207+
});
208+
});
209+
});
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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("@groupBy directive top level", () => {
24+
const testHelper = new TestHelper();
25+
26+
let Movie: UniqueType;
27+
let Person: UniqueType;
28+
29+
beforeEach(async () => {
30+
Movie = testHelper.createUniqueType("Movie");
31+
Person = testHelper.createUniqueType("Person");
32+
33+
const typeDefs = /* GraphQL */ `
34+
type ${Movie} @node {
35+
title: String!
36+
year: Int! @groupBy
37+
actors: [${Person}!]! @relationship(type: "ACTED_IN", direction: IN)
38+
}
39+
40+
type ${Person} @node {
41+
name: String!
42+
}
43+
`;
44+
45+
await testHelper.initNeo4jGraphQL({
46+
typeDefs,
47+
});
48+
});
49+
50+
afterEach(async () => {
51+
await testHelper.close();
52+
});
53+
54+
test("groupBy in top level query with nested relationship in groupBy", async () => {
55+
await testHelper.executeCypher(`
56+
CREATE (:${Movie} {title: "The Matrix", year: 1999})
57+
CREATE (:${Movie} {title: "The Matrix Reloaded", year: 2001})
58+
CREATE (:${Movie} {title: "Another Movie", year: 1999})<-[:ACTED_IN]-(:${Person} { name: "Another Keanu" })
59+
`);
60+
61+
const query = /* GraphQL */ `
62+
query {
63+
${Movie.operations.connection} {
64+
groupBy(fields: {year: true}) {
65+
edges {
66+
node {
67+
title
68+
actors {
69+
name
70+
}
71+
}
72+
}
73+
}
74+
}
75+
}
76+
`;
77+
78+
const result = await testHelper.executeGraphQL(query);
79+
80+
expect(result.errors).toBeUndefined();
81+
expect(result.data).toEqual({
82+
[Movie.operations.connection]: {
83+
groupBy: expect.toIncludeSameMembers([
84+
{
85+
edges: expect.toIncludeSameMembers([
86+
{
87+
node: {
88+
title: "The Matrix",
89+
actors: [],
90+
},
91+
},
92+
{
93+
node: {
94+
title: "Another Movie",
95+
actors: [
96+
{
97+
name: "Another Keanu",
98+
},
99+
],
100+
},
101+
},
102+
]),
103+
},
104+
{
105+
edges: [
106+
{
107+
node: {
108+
title: "The Matrix Reloaded",
109+
actors: [],
110+
},
111+
},
112+
],
113+
},
114+
]),
115+
},
116+
});
117+
});
118+
});

packages/graphql/tests/integration/directives/groupBy/top-level-group-by.int.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe("@groupBy directive top level", () => {
4444
await testHelper.close();
4545
});
4646

47-
test("group by in top level query without projection", async () => {
47+
test("groupBy in top level query without projection", async () => {
4848
await testHelper.executeCypher(`
4949
CREATE (:${Movie} {title: "The Matrix", year: 1999})
5050
CREATE (:${Movie} {title: "The Matrix Reloaded", year: 2001})
@@ -98,7 +98,7 @@ describe("@groupBy directive top level", () => {
9898
});
9999
});
100100

101-
test("group by in top level query with node projection", async () => {
101+
test("groupBy in top level query with node projection", async () => {
102102
await testHelper.executeCypher(`
103103
CREATE (:${Movie} {title: "The Matrix", year: 1999})
104104
CREATE (:${Movie} {title: "The Matrix Reloaded", year: 2001})

0 commit comments

Comments
 (0)