Skip to content

Commit 56be9d4

Browse files
committed
Added Cypher Directive for nested Object type fields
1 parent 6525401 commit 56be9d4

File tree

5 files changed

+64
-18
lines changed

5 files changed

+64
-18
lines changed

src/main/kotlin/org/neo4j/graphql/Translator.kt

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,16 @@ class Translator(val schema: GraphQLSchema) {
159159
private fun projectField(variable: String, field: Field, type: GraphQLObjectType, ctx:Context) : Cypher {
160160
val fieldDefinition = type.getFieldDefinition(field.name) ?: throw IllegalStateException("No field ${field.name} in ${type.name}")
161161
val cypherDirective = fieldDefinition.cypherDirective()
162-
return cypherDirective?.let { cypherFieldDirective(variable, fieldDefinition, field, it) } ?:
163-
if (fieldDefinition.type.inner() is GraphQLObjectType) {
162+
val isObjectField = fieldDefinition.type.inner() is GraphQLObjectType
163+
return cypherDirective?.let {
164+
val directive = cypherDirective(variable, fieldDefinition, field, it, listOf(CypherArgument("this", "this", variable)))
165+
if (isObjectField) {
166+
val patternComprehensions = projectListComprehension(variable, field, fieldDefinition, ctx,directive)
167+
Cypher(field.aliasOrName() + ":" + patternComprehensions.query, patternComprehensions.params)
168+
} else
169+
Cypher(field.aliasOrName() +":" + directive.query, directive.params)
170+
171+
} ?: if (isObjectField) {
164172
val patternComprehensions = projectRelationship(variable, field, fieldDefinition, type, ctx)
165173
Cypher(field.aliasOrName() + ":" + patternComprehensions.query, patternComprehensions.params)
166174
} else
@@ -170,18 +178,13 @@ class Translator(val schema: GraphQLSchema) {
170178
Cypher(field.aliasOrName() + ":" + variable + "." + field.propertyName(fieldDefinition))
171179
}
172180

173-
private fun cypherFieldDirective(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: Cypher): Cypher {
174-
val (query,params) = cypherDirective(variable, fieldDefinition, field, cypherDirective, listOf(CypherArgument("this", "this", variable)))
175-
return Cypher(field.aliasOrName() +":" + query, params)
176-
}
177181
private fun cypherDirective(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: Cypher, additionalArgs: List<CypherArgument>): Cypher {
178182
val suffix = if (fieldDefinition.type.isList()) "Many" else "Single"
179183
val (query, args) = cypherDirectiveQuery(variable, fieldDefinition, field, cypherDirective, additionalArgs)
180184
return Cypher("apoc.cypher.runFirstColumn$suffix($query)", args)
181185
}
182186

183187
private fun cypherDirectiveQuery(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: Cypher, additionalArgs: List<CypherArgument>): Cypher {
184-
val suffix = if (fieldDefinition.type.isList()) "Many" else "Single"
185188
val args = additionalArgs + prepareFieldArguments(fieldDefinition, field.arguments)
186189
val argParams = args.map { '$' + it.name + " AS " + it.name }.joinNonEmpty(",")
187190
val query = (if (argParams.isEmpty()) "" else "WITH $argParams ") + cypherDirective.escapedQuery()
@@ -217,6 +220,20 @@ class Translator(val schema: GraphQLSchema) {
217220
}
218221
}
219222

223+
private fun projectListComprehension(variable: String, field: Field, fieldDefinition: GraphQLFieldDefinition, ctx: Context, expression:Cypher): Cypher {
224+
val fieldType = fieldDefinition.type
225+
val fieldObjectType = fieldType.inner() as GraphQLObjectType
226+
val childVariable = variable + field.name.capitalize()
227+
228+
// val where = where(childVariable, fieldDefinition, fieldObjectType, propertyArguments(field))
229+
val fieldProjection = projectFields(childVariable, field, fieldObjectType, ctx)
230+
231+
val comprehension = "[$childVariable IN ${expression.query} | ${fieldProjection.query}]"
232+
val skipLimit = skipLimit(field.arguments)
233+
val slice = slice(skipLimit, fieldType.isList())
234+
return Cypher(comprehension + slice, (expression.params + fieldProjection.params)) // + where.params
235+
236+
}
220237
private fun projectRichAndRegularRelationship(variable: String, field: Field, fieldDefinition: GraphQLFieldDefinition, ctx: Context): Cypher {
221238
val fieldType = fieldDefinition.type
222239
val fieldObjectType = fieldType.inner() as GraphQLObjectType

src/test/kotlin/GraphQLServer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ type Movie {
2828
}
2929
"""
3030

31-
fun main(args: Array<String>) {
31+
fun main() {
3232
val gson = Gson()
3333
fun render(value:Any) = gson.toJson(value)
3434
fun query(value:String) = (gson.fromJson(value, Map::class.java)["query"] as String).also { println(it) }

src/test/kotlin/org/neo4j/graphql/CypherDirectiveTest.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type Person {
1515
id: ID
1616
name: String @cypher(statement:"RETURN this.name")
1717
age(mult:Int=13) : [Int] @cypher(statement:"RETURN this.age * mult as age")
18+
friends: [Person] @cypher(statement:"MATCH (this)-[:KNOWS]-(o) RETURN o")
1819
}
1920
type Query {
2021
person : [Person]
@@ -31,13 +32,20 @@ schema {
3132
"""
3233

3334
@Test
34-
fun renderCypherFieldDirective() {
35+
fun renderCypherFieldDirectiveScalar() {
3536

3637
val expected = """MATCH (person:Person) RETURN person { name:apoc.cypher.runFirstColumnSingle('WITH ${"$"}this AS this RETURN this.name',{this:person}) } AS person"""
3738
val query = """{ person { name }}"""
3839
assertQuery(query, expected, emptyMap())
3940
}
4041

42+
@Test
43+
fun renderCypherFieldDirectiveNested() {
44+
val expected = """MATCH (person:Person) RETURN person { friends:[personFriends IN apoc.cypher.runFirstColumnMany('WITH ${"$"}this AS this MATCH (this)-[:KNOWS]-(o) RETURN o',{this:person}) | personFriends { .id }] } AS person"""
45+
val query = """{ person { friends { id } }}"""
46+
assertQuery(query, expected, emptyMap())
47+
}
48+
4149
@Test
4250
fun renderCypherFieldDirectiveWithParamsDefaults() {
4351
val expected = """MATCH (person:Person) RETURN person { age:apoc.cypher.runFirstColumnMany('WITH ${"$"}this AS this,${'$'}mult AS mult RETURN this.age * mult as age',{this:person,mult:${'$'}personMult}) } AS person"""

src/test/resources/movie-test.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
77
```
88

9-
### Basic Test
9+
### Basic Query with Parameter
1010

1111
```graphql
1212
{ Movie(title: "River Runs Through It, A") { title } }
@@ -20,7 +20,7 @@ WHERE movie.title = $movieTitle
2020
RETURN movie { .title } AS movie
2121
```
2222

23-
### Testing Paging
23+
### Paging
2424

2525
```graphql
2626
{
@@ -42,7 +42,7 @@ RETURN movie { .title , .year } AS movie
4242
SKIP $offset LIMIT $first
4343
```
4444

45-
### Testing Projection
45+
### Relationship Expansion
4646

4747
```graphql
4848
{
@@ -65,7 +65,7 @@ WHERE movie.title = $movieTitle
6565
RETURN movie { .title,actors:[(movie)<-[:ACTED_IN]-(movieActors:Actor) | movieActors { .name }] } AS movie
6666
```
6767

68-
### Testing Projection with sub-paging
68+
### Projection with sub-paging
6969

7070
```graphql
7171
{
@@ -87,3 +87,24 @@ MATCH (movie:Movie)
8787
WHERE movie.title = $movieTitle
8888
RETURN movie { .title,actors:[(movie)<-[:ACTED_IN]-(movieActors:Actor) | movieActors { .name }][0..3] } AS movie
8989
```
90+
### Subquery Cypher Directive
91+
92+
```graphql
93+
{
94+
Movie {
95+
title
96+
similar {
97+
title
98+
}
99+
}
100+
}
101+
```
102+
103+
```params
104+
{}
105+
```
106+
107+
```cypher
108+
MATCH (movie:Movie)
109+
RETURN movie { .title,similar:[movieSimilar IN apoc.cypher.runFirstColumnMany('WITH $this AS this,$first AS first,$offset AS offset MATCH (this)--(:Genre)--(o:Movie) RETURN o',{this:movie,first:$movieFirst,offset:$movieOffset}) | movieSimilar { .title }] } AS movie
110+
```

src/test/resources/movies-test-schema.graphql

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ type Movie {
88
poster: String
99
imdbRating: Float
1010
genres: [Genre] @relation(name: "IN_GENRE", direction: "OUT")
11-
similar(first: Int = 3, offset: Int = 0): [Movie] @cypher(statement: "WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o")
12-
mostSimilar: Movie @cypher(statement: "WITH {this} AS this RETURN this")
13-
degree: Int @cypher(statement: "WITH {this} AS this RETURN SIZE((this)--())")
11+
similar(first: Int = 3, offset: Int = 0): [Movie] @cypher(statement: "MATCH (this)--(:Genre)--(o:Movie) RETURN o")
12+
mostSimilar: Movie @cypher(statement: "RETURN this")
13+
degree: Int @cypher(statement: "RETURN SIZE((this)--())")
1414
actors(first: Int = 3, offset: Int = 0, name: String): [Actor] @relation(name: "ACTED_IN", direction:"IN")
1515
avgStars: Float
1616
filmedIn: State @relation(name: "FILMED_IN", direction:"OUT")
17-
scaleRating(scale: Int = 3): Float @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating")
18-
scaleRatingFloat(scale: Float = 1.5): Float @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating")
17+
scaleRating(scale: Int = 3): Float @cypher(statement: "RETURN $scale * this.imdbRating")
18+
scaleRatingFloat(scale: Float = 1.5): Float @cypher(statement: "RETURN $scale * this.imdbRating")
1919
actorMovies: [Movie] @cypher(statement: "MATCH (this)-[:ACTED_IN*2]-(other:Movie) RETURN other")
2020
ratings: [Rated]
2121
}

0 commit comments

Comments
 (0)