Skip to content

Commit 944c8f6

Browse files
committed
Support for non node types on cypher directives
1 parent b4cd562 commit 944c8f6

File tree

4 files changed

+350
-6
lines changed

4 files changed

+350
-6
lines changed

.changeset/plain-ants-build.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@neo4j/graphql": minor
3+
---
4+
5+
Add support for `@cypher` directive of fields targeting types that do not use the `@node` directive. For example:
6+
7+
```graphql
8+
type Movie @node {
9+
title: String
10+
id: String!
11+
link: Link!
12+
@cypher(
13+
statement: """
14+
MATCH(l:${Link})
15+
WHERE l.movieId=this.id
16+
RETURN l {.name, .url} as link
17+
"""
18+
columnName: "link"
19+
)
20+
}
21+
22+
type Link {
23+
movieId: String!
24+
url: String!
25+
name: String!
26+
}
27+
```

packages/graphql/src/schema-model/generate-model.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,10 @@ function getCypherTarget(schema: Neo4jGraphQLSchemaModel, attributeType: Attribu
135135
if (attributeType instanceof ObjectType) {
136136
const foundConcreteEntity = schema.getConcreteEntity(attributeType.name);
137137
if (!foundConcreteEntity) {
138-
throw new Neo4jGraphQLSchemaValidationError(
139-
`@cypher field must target type annotated with the @node directive${attributeType.name}, `
140-
);
138+
return undefined;
141139
}
142140
return schema.getConcreteEntity(attributeType.name);
143141
}
144-
if (attributeType instanceof InterfaceEntity || attributeType instanceof UnionEntity) {
145-
throw new Error("@cypher field target cannot be an interface or an union");
146-
}
147142
}
148143

149144
// TODO: currently the below is used only for Filtering purposes, and therefore the target is set only for ObjectTypes but in the future we might want to use it for other types as well

packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeCypherOperation.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ export class CompositeCypherOperation extends Operation {
5555

5656
const returnVariable = new Cypher.Variable();
5757
const partialContext = nestedContext.setReturn(returnVariable);
58+
59+
if (!this.partials.length) {
60+
return {
61+
clauses: [Cypher.utils.concat(matchClause, new Cypher.Return(nestedContext.returnVariable))],
62+
projectionExpr: nestedContext.returnVariable,
63+
};
64+
}
5865
const partialClauses = this.partials.map((partial) => {
5966
const { clauses } = partial.transpile(partialContext);
6067
return Cypher.utils.concat(new Cypher.With("*"), ...clauses);
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
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 { TestHelper } from "../../../utils/tests-helper";
21+
22+
describe("cypher directive targeting non node types", () => {
23+
const testHelper = new TestHelper();
24+
25+
afterEach(async () => {
26+
await testHelper.close();
27+
});
28+
29+
test("field", async () => {
30+
const Movie = testHelper.createUniqueType("Movie");
31+
const Link = testHelper.createUniqueType("Link");
32+
33+
const typeDefs = /* GraphQL */ `
34+
type ${Movie} @node {
35+
title: String
36+
id: String!
37+
link: ${Link}! @cypher(statement: """
38+
MATCH(l:${Link})
39+
WHERE l.movieId=this.id
40+
RETURN l {.name, .url} as link
41+
""", columnName: "link")
42+
}
43+
44+
type ${Link} {
45+
movieId: String!
46+
url: String!
47+
name: String!
48+
}
49+
`;
50+
51+
await testHelper.initNeo4jGraphQL({ typeDefs });
52+
53+
await testHelper.executeCypher(`
54+
CREATE (:${Movie} { id: "matrix", title: "The Matrix"})
55+
CREATE (:${Link} {movieId: "matrix", url: "the-matrix.org", name: "Main Website"})
56+
`);
57+
58+
const query = /* GraphQL */ `
59+
{
60+
${Movie.plural} {
61+
title
62+
link {
63+
url
64+
name
65+
}
66+
}
67+
}
68+
`;
69+
70+
const gqlResult = await testHelper.executeGraphQL(query);
71+
72+
expect(gqlResult.errors).toBeUndefined();
73+
expect(gqlResult.data).toEqual({
74+
[Movie.plural]: [
75+
{
76+
title: "The Matrix",
77+
link: { name: "Main Website", url: "the-matrix.org" },
78+
},
79+
],
80+
});
81+
});
82+
83+
test("array field", async () => {
84+
const Movie = testHelper.createUniqueType("Movie");
85+
const Link = testHelper.createUniqueType("Link");
86+
87+
const typeDefs = /* GraphQL */ `
88+
type ${Movie} @node {
89+
title: String
90+
id: String!
91+
links: [${Link}!]! @cypher(statement: """
92+
MATCH(l:${Link})
93+
WHERE l.movieId=this.id
94+
RETURN l {.name, .url} as links
95+
""", columnName: "links")
96+
}
97+
98+
type ${Link} {
99+
movieId: String!
100+
url: String!
101+
name: String!
102+
}
103+
`;
104+
105+
await testHelper.initNeo4jGraphQL({ typeDefs });
106+
107+
await testHelper.executeCypher(`
108+
CREATE (:${Movie} { id: "matrix", title: "The Matrix"})
109+
CREATE (:${Link} {movieId: "matrix", url: "the-matrix.org", name: "Main Website"})
110+
CREATE (:${Link} {movieId: "matrix", url: "not-imdb.com", name: "Public Database" })
111+
`);
112+
113+
const query = /* GraphQL */ `
114+
{
115+
${Movie.plural} {
116+
title
117+
links {
118+
url
119+
name
120+
}
121+
}
122+
}
123+
`;
124+
125+
const gqlResult = await testHelper.executeGraphQL(query);
126+
127+
expect(gqlResult.errors).toBeUndefined();
128+
expect(gqlResult.data).toEqual({
129+
[Movie.plural]: [
130+
{
131+
title: "The Matrix",
132+
links: expect.toIncludeSameMembers([
133+
{ name: "Main Website", url: "the-matrix.org" },
134+
{ name: "Public Database", url: "not-imdb.com" },
135+
]),
136+
},
137+
],
138+
});
139+
});
140+
141+
test("nested field", async () => {
142+
const Movie = testHelper.createUniqueType("Movie");
143+
const Actor = testHelper.createUniqueType("Actor");
144+
const Link = testHelper.createUniqueType("Link");
145+
146+
const typeDefs = /* GraphQL */ `
147+
type ${Actor} @node {
148+
name: String
149+
movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT)
150+
}
151+
152+
type ${Movie} @node {
153+
title: String
154+
id: String!
155+
link: ${Link}! @cypher(statement: """
156+
MATCH(l:${Link})
157+
WHERE l.movieId=this.id
158+
RETURN l {.name, .url} as link
159+
""", columnName: "link")
160+
}
161+
162+
type ${Link} {
163+
movieId: String!
164+
url: String!
165+
name: String!
166+
}
167+
`;
168+
169+
await testHelper.initNeo4jGraphQL({ typeDefs });
170+
171+
await testHelper.executeCypher(`
172+
CREATE (:${Actor} {name: "Keanu"})-[:ACTED_IN]->(:${Movie} { id: "matrix", title: "The Matrix"})
173+
CREATE (:${Link} {movieId: "matrix", url: "the-matrix.org", name: "Main Website"})
174+
`);
175+
176+
const query = /* GraphQL */ `
177+
{
178+
${Actor.plural} {
179+
name
180+
movies {
181+
title
182+
link {
183+
url
184+
name
185+
}
186+
}
187+
}
188+
}
189+
`;
190+
191+
const gqlResult = await testHelper.executeGraphQL(query);
192+
193+
expect(gqlResult.errors).toBeUndefined();
194+
expect(gqlResult.data).toEqual({
195+
[Actor.plural]: [
196+
{
197+
name: "Keanu",
198+
movies: [
199+
{
200+
title: "The Matrix",
201+
link: { name: "Main Website", url: "the-matrix.org" },
202+
},
203+
],
204+
},
205+
],
206+
});
207+
});
208+
209+
/**
210+
* Interfaces without `@node` are not supported yet because there is no way to access "__resolveTree", as the information of the concrete type returned by the cypher field is not available
211+
*/
212+
test.skip("interface field", async () => {
213+
const Movie = testHelper.createUniqueType("Movie");
214+
const Link = testHelper.createUniqueType("Link");
215+
216+
const typeDefs = /* GraphQL */ `
217+
type ${Movie} @node {
218+
title: String
219+
id: String!
220+
link: Link! @cypher(statement: """
221+
MATCH(l:${Link})
222+
WHERE l.movieId=this.id
223+
RETURN l {.name, .url} as link
224+
""", columnName: "link")
225+
}
226+
227+
interface Link {
228+
movieId: String!
229+
url: String!
230+
name: String!
231+
}
232+
233+
type ${Link} implements Link {
234+
movieId: String!
235+
url: String!
236+
name: String!
237+
}
238+
`;
239+
240+
await testHelper.initNeo4jGraphQL({ typeDefs });
241+
242+
await testHelper.executeCypher(`
243+
CREATE (:${Movie} { id: "matrix", title: "The Matrix"})
244+
CREATE (:${Link} {movieId: "matrix", url: "the-matrix.org", name: "Main Website"})
245+
`);
246+
247+
const query = /* GraphQL */ `
248+
{
249+
${Movie.plural} {
250+
title
251+
link {
252+
url
253+
name
254+
}
255+
}
256+
}
257+
`;
258+
259+
const gqlResult = await testHelper.executeGraphQL(query);
260+
261+
expect(gqlResult.errors).toBeUndefined();
262+
expect(gqlResult.data).toEqual({
263+
[Movie.plural]: [
264+
{
265+
title: "The Matrix",
266+
link: { name: "Main Website", url: "the-matrix.org" },
267+
},
268+
],
269+
});
270+
});
271+
272+
test("query", async () => {
273+
const Movie = testHelper.createUniqueType("Movie");
274+
275+
const typeDefs = /* GraphQL */ `
276+
type Movie @node {
277+
id: String
278+
}
279+
280+
type Query {
281+
movie: Movie
282+
@cypher(
283+
statement: """
284+
MATCH (m:${Movie})
285+
RETURN m { .id } as m
286+
"""
287+
columnName: "m"
288+
)
289+
}
290+
`;
291+
292+
await testHelper.initNeo4jGraphQL({ typeDefs });
293+
294+
await testHelper.executeCypher(`
295+
CREATE (:${Movie} { id: "matrix", title: "The Matrix"})
296+
`);
297+
298+
const query = /* GraphQL */ `
299+
{
300+
movie {
301+
id
302+
}
303+
}
304+
`;
305+
306+
const gqlResult = await testHelper.executeGraphQL(query);
307+
308+
expect(gqlResult.errors).toBeUndefined();
309+
expect(gqlResult.data).toEqual({
310+
movie: {
311+
id: "matrix",
312+
},
313+
});
314+
});
315+
});

0 commit comments

Comments
 (0)