Skip to content

Commit 07a66af

Browse files
committed
added @cypher directive for queries
single/many call depending on cardinality of field support field arguments as parameter supports further drill-down in graphql query
1 parent 1c776bd commit 07a66af

File tree

2 files changed

+56
-19
lines changed

2 files changed

+56
-19
lines changed

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

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,22 @@ class Translator(val schema: GraphQLSchema) {
4747
val type = schema.getType(returnType.name)
4848
val label = type.name.quote()
4949
val variable = queryField.aliasOrName().decapitalize()
50+
val cypherDirective = queryType.cypherDirective()
5051
val mapProjection = projectFields(variable, queryField, type, ctx)
51-
val where = if (ctx.topLevelWhere) where(variable, queryType, type, propertyArguments(queryField)) else Cypher.EMPTY
52-
val properties = if (ctx.topLevelWhere) Cypher.EMPTY else properties(variable, queryType, propertyArguments(queryField))
5352
val skipLimit = format(skipLimit(queryField.arguments))
5453
val ordering = orderBy(variable, queryField.arguments)
55-
return Cypher("MATCH ($variable:$label${properties.query})${where.query} RETURN ${mapProjection.query} AS $variable$ordering$skipLimit" ,
56-
(mapProjection.params + properties.params + where.params))
54+
if (cypherDirective != null) {
55+
// todo filters and such from nested fields
56+
val (query, params) = cypherDirective(variable, queryType, queryField, cypherDirective, emptyList())
57+
return Cypher("UNWIND $query AS $variable RETURN ${mapProjection.query} AS $variable$ordering$skipLimit" ,
58+
(params + mapProjection.params))
59+
60+
} else {
61+
val where = if (ctx.topLevelWhere) where(variable, queryType, type, propertyArguments(queryField)) else Cypher.EMPTY
62+
val properties = if (ctx.topLevelWhere) Cypher.EMPTY else properties(variable, queryType, propertyArguments(queryField))
63+
return Cypher("MATCH ($variable:$label${properties.query})${where.query} RETURN ${mapProjection.query} AS $variable$ordering$skipLimit" ,
64+
(mapProjection.params + properties.params + where.params))
65+
}
5766
}
5867

5968
private fun propertyArguments(queryField: Field) =
@@ -149,12 +158,16 @@ class Translator(val schema: GraphQLSchema) {
149158
}
150159

151160
private fun cypherFieldDirective(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: Cypher): Cypher {
161+
val (query,params) = cypherDirective(variable, fieldDefinition, field, cypherDirective, listOf(CypherArgument("this", "this", variable)))
162+
return Cypher(field.aliasOrName() +":" + query, params)
163+
}
164+
private fun cypherDirective(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: Cypher, additionalArgs: List<CypherArgument>): Cypher {
152165
val suffix = if (fieldDefinition.type.isList()) "Many" else "Single"
153-
val args = prepareFieldArguments(fieldDefinition, field.arguments)
154-
val argParams = args.map { '$' + it.name + " AS " + it.name }.joinNonEmpty(",", ",")
155-
val query = "WITH \$this AS this $argParams " + cypherDirective.escapedQuery()
156-
val argString = (listOf("this:" + variable) + args.map { it.name + ':' + '$' + paramName(variable, it.name, it.value) }).joinToString(",", "{", "}")
157-
return Cypher(field.aliasOrName() + ":apoc.cypher.runFirstColumn$suffix('$query',$argString)", args.associate { paramName(variable, it.name, it.value) to it.value })
166+
val args = additionalArgs + prepareFieldArguments(fieldDefinition, field.arguments)
167+
val argParams = args.map { '$' + it.name + " AS " + it.name }.joinNonEmpty(",")
168+
val query = (if (argParams.isEmpty()) "" else "WITH $argParams ") + cypherDirective.escapedQuery()
169+
val argString = (args.map { it.name + ':' + if (it.name == "this") it.value else ('$' + paramName(variable, it.name, it.value)) }).joinToString(",", "{", "}")
170+
return Cypher("apoc.cypher.runFirstColumn$suffix('$query',$argString)", args.filter { it.name != "this" }.associate { paramName(variable, it.name, it.value) to it.value })
158171
}
159172

160173
fun projectNamedFragments(variable: String, fragmentSpread: FragmentSpread, type: GraphQLObjectType, ctx: Context) =
Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.neo4j.graphql
22

33
import demo.org.neo4j.graphql.TckTest
4+
import graphql.language.Node
5+
import graphql.language.VariableReference
46
import org.junit.Assert.assertEquals
57
import org.junit.Assert.assertTrue
68
import org.junit.Ignore
@@ -10,46 +12,68 @@ class CypherDirectiveTest {
1012

1113
val schema = """
1214
type Person {
15+
id: ID
1316
name: String @cypher(statement:"RETURN this.name")
1417
age(mult:Int=13) : [Int] @cypher(statement:"RETURN this.age * mult as age")
1518
}
1619
type Query {
1720
person : [Person]
21+
p2: [Person] @cypher(statement:"MATCH (p:Person) RETURN p")
22+
p3(name:String): Person @cypher(statement:"MATCH (p:Person) WHERE p.name = name RETURN p LIMIT 1")
1823
}
1924
"""
2025

2126
@Test
22-
fun renderCypherDirective() {
27+
fun renderCypherFieldDirective() {
2328

24-
val expected = """MATCH (person:Person) RETURN person { name:apoc.cypher.runFirstColumnSingle('WITH ${"$"}this AS this RETURN this.name',{this:person}) } AS person"""
29+
val expected = """MATCH (person:Person) RETURN person { name:apoc.cypher.runFirstColumnSingle('WITH ${"$"}this AS this RETURN this.name',{this:person}) } AS person"""
2530
val query = """{ person { name }}"""
2631
assertQuery(query, expected, emptyMap())
2732
}
2833

2934
@Test
30-
fun renderCypherDirectiveWithParamsDefaults() {
35+
fun renderCypherFieldDirectiveWithParamsDefaults() {
3136

32-
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"""
37+
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"""
3338
val query = """{ person { age }}"""
3439
assertQuery(query, expected, mapOf("personMult" to 13))
3540
}
3641

3742
@Test
38-
fun renderCypherDirectiveWithParams() {
43+
fun renderCypherFieldDirectiveWithParams() {
3944

40-
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"""
45+
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"""
4146
val query = """{ person { age(mult:25) }}"""
4247
assertQuery(query, expected, mapOf("personMult" to 25L))
4348
}
4449

45-
private fun assertQuery(query: String, expected: String, params : Map<String,Any?> = emptyMap()) {
46-
val result = Translator(SchemaBuilder.buildSchema(schema)).translate(query).first()
47-
assertEquals(expected, result.query)
48-
assertTrue("${params} IN ${result.params}", result.params.entries.containsAll(params.entries))
50+
@Test
51+
fun renderCypherQueryDirective() {
52+
val expected = """UNWIND apoc.cypher.runFirstColumnMany('MATCH (p:Person) RETURN p',{}) AS p2 RETURN p2 { .id } AS p2"""
53+
val query = """{ p2 { id }}"""
54+
assertQuery(query, expected, emptyMap())
55+
}
56+
@Test
57+
fun renderCypherQueryDirectiveParams() {
58+
val expected = """UNWIND apoc.cypher.runFirstColumnSingle('WITH ${'$'}name AS name MATCH (p:Person) WHERE p.name = name RETURN p LIMIT 1',{name:${'$'}p3Name}) AS p3 RETURN p3 { .id } AS p3"""
59+
val query = """{ p3(name:"Jane") { id }}"""
60+
assertQuery(query, expected, mapOf("p3Name" to "Jane"))
61+
}
62+
@Test
63+
fun renderCypherQueryDirectiveParamsArgs() {
64+
val expected = """UNWIND apoc.cypher.runFirstColumnSingle('WITH ${'$'}name AS name MATCH (p:Person) WHERE p.name = name RETURN p LIMIT 1',{name:${'$'}pname}) AS p3 RETURN p3 { .id } AS p3"""
65+
val query = """query(${'$'}pname:String) { p3(name:${'$'}pname) { id }}"""
66+
assertQuery(query, expected, mapOf("pname" to VariableReference("pname")))
4967
}
5068

5169
@Test @Ignore
5270
fun testTck() {
5371
TckTest(schema).testTck("cypher-directive-test.md", 0)
5472
}
73+
74+
private fun assertQuery(query: String, expected: String, params : Map<String,Any?> = emptyMap()) {
75+
val result = Translator(SchemaBuilder.buildSchema(schema)).translate(query).first()
76+
assertEquals(expected, result.query)
77+
assertTrue("${params} IN ${result.params}", params.all { val v=result.params[it.key]; when (v) { is Node -> v.isEqualTo(it.value as Node) else -> v == it.value}})
78+
}
5579
}

0 commit comments

Comments
 (0)