Skip to content

Commit 1c776bd

Browse files
committed
added @cypher directive for fields
single/many call depending on cardinality of field pass this as parameter support field arguments as parameter doesn't support separate parameters yet
1 parent bbb0455 commit 1c776bd

File tree

3 files changed

+94
-13
lines changed

3 files changed

+94
-13
lines changed

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ fun Field.propertyName(fieldDefinition: GraphQLFieldDefinition) = (fieldDefiniti
3838
fun GraphQLFieldDefinition.propertyDirectiveName() =
3939
this.definition.getDirective("property")?.getArgument("name")?.value?.toJavaValue()?.toString()
4040

41+
fun GraphQLFieldDefinition.cypherDirective(): Translator.Cypher? =
42+
this.definition.getDirective("cypher")?.let {
43+
Translator.Cypher(it.getArgument("statement").value.toJavaValue().toString(),
44+
it.getArgument("params")?.value?.toJavaValue() as Map<String,Any?>? ?: emptyMap())
45+
}
46+
4147
fun String.quote() = if (isJavaIdentifier()) this else '`'+this+'`'
4248

4349
fun String.isJavaIdentifier() =
@@ -67,8 +73,8 @@ fun Value.toJavaValue(): Any? = when (this) {
6773
is EnumValue -> this.name
6874
is NullValue -> null
6975
is BooleanValue -> this.isValue
70-
is FloatValue -> this.value
71-
is IntValue -> this.value
76+
is FloatValue -> this.value.toDouble()
77+
is IntValue -> this.value.longValueExact()
7278
is VariableReference -> this
7379
is ArrayValue -> this.values.map { it.toJavaValue() }.toList()
7480
is ObjectValue -> this.objectFields.map { it.name to it.value.toJavaValue() }.toMap()

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

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class Translator(val schema: GraphQLSchema) {
2525
val EMPTY = Cypher("")
2626
}
2727
fun with(p: Map<String,Any?>) = this.copy(params = this.params + p)
28+
fun escapedQuery() = query.replace("\"","\\\"").replace("'","\\'")
2829
}
2930

3031
@JvmOverloads fun translate(query: String, params: Map<String, Any> = emptyMap(), context: Context = Context()) : List<Cypher> {
@@ -67,7 +68,7 @@ class Translator(val schema: GraphQLSchema) {
6768
}
6869

6970
private fun where(variable: String, field: GraphQLFieldDefinition, type: GraphQLType, arguments: List<Argument>) : Cypher {
70-
val all = prepareArguments(field, arguments)
71+
val all = preparePredicateArguments(field, arguments)
7172
if (all.isEmpty()) return Cypher("")
7273
val (filterExpressions, filterParams) =
7374
filterExpressions(all.find{ it.name == "filter" }?.value, type as GraphQLObjectType)
@@ -92,14 +93,14 @@ class Translator(val schema: GraphQLSchema) {
9293
}
9394

9495
private fun properties(variable: String, field: GraphQLFieldDefinition, arguments: List<Argument>) : Cypher {
95-
val all = prepareArguments(field, arguments)
96+
val all = preparePredicateArguments(field, arguments)
9697
return Cypher(all.map { (k,p, v) -> "${p.quote()}:\$${paramName(variable, k, v)}"}.joinToString(" , "," {","}") ,
9798
all.map { (k,p,v) -> paramName(variable, k, v) to v }.toMap())
9899
}
99100

100101
data class CypherArgument(val name:String, val propertyName:String, val value:Any?)
101102

102-
private fun prepareArguments(field: GraphQLFieldDefinition, arguments: List<Argument>): List<CypherArgument> {
103+
private fun preparePredicateArguments(field: GraphQLFieldDefinition, arguments: List<Argument>): List<CypherArgument> {
103104
if (arguments.isEmpty()) return emptyList()
104105
val resultObjectType = schema.getType(field.type.inner().name) as GraphQLObjectType
105106
val predicates = arguments.map { it.name to CypherArgument(it.name, resultObjectType.getFieldDefinition(it.name)?.propertyDirectiveName() ?: it.name, it.value.toJavaValue()) }.toMap()
@@ -108,6 +109,14 @@ class Translator(val schema: GraphQLSchema) {
108109
return predicates.values + defaults
109110
}
110111

112+
private fun prepareFieldArguments(field: GraphQLFieldDefinition, arguments: List<Argument>): List<CypherArgument> {
113+
// if (arguments.isEmpty()) return emptyList()
114+
val predicates = arguments.map { it.name to CypherArgument(it.name, it.name, it.value.toJavaValue()) }.toMap()
115+
val defaults = field.arguments.filter { it.defaultValue != null && !predicates.containsKey(it.name) }
116+
.map { CypherArgument(it.name, it.name, it.defaultValue) }
117+
return predicates.values + defaults
118+
}
119+
111120
private fun projectFields(variable: String, field: Field, type: GraphQLType, ctx: Context): Cypher {
112121
// todo handle non-object case
113122
val objectType = type as GraphQLObjectType
@@ -127,14 +136,25 @@ class Translator(val schema: GraphQLSchema) {
127136

128137
private fun projectField(variable: String, field: Field, type: GraphQLObjectType, ctx:Context) : Cypher {
129138
val fieldDefinition = type.getFieldDefinition(field.name) ?: throw IllegalStateException("No field ${field.name} in ${type.name}")
130-
return if (fieldDefinition.type.inner() is GraphQLObjectType) {
131-
val patternComprehensions = projectRelationship(variable, field, fieldDefinition, type, ctx)
132-
Cypher(field.aliasOrName() + ":" + patternComprehensions.query, patternComprehensions.params)
133-
} else
134-
if (field.aliasOrName() == field.propertyName(fieldDefinition))
135-
Cypher("." + field.propertyName(fieldDefinition))
136-
else
137-
Cypher(field.aliasOrName() + ":" + variable+"."+field.propertyName(fieldDefinition))
139+
val cypherDirective = fieldDefinition.cypherDirective()
140+
return cypherDirective?.let { cypherFieldDirective(variable, fieldDefinition, field, it) } ?:
141+
if (fieldDefinition.type.inner() is GraphQLObjectType) {
142+
val patternComprehensions = projectRelationship(variable, field, fieldDefinition, type, ctx)
143+
Cypher(field.aliasOrName() + ":" + patternComprehensions.query, patternComprehensions.params)
144+
} else
145+
if (field.aliasOrName() == field.propertyName(fieldDefinition))
146+
Cypher("." + field.propertyName(fieldDefinition))
147+
else
148+
Cypher(field.aliasOrName() + ":" + variable + "." + field.propertyName(fieldDefinition))
149+
}
150+
151+
private fun cypherFieldDirective(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: Cypher): Cypher {
152+
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 })
138158
}
139159

140160
fun projectNamedFragments(variable: String, fragmentSpread: FragmentSpread, type: GraphQLObjectType, ctx: Context) =
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package org.neo4j.graphql
2+
3+
import demo.org.neo4j.graphql.TckTest
4+
import org.junit.Assert.assertEquals
5+
import org.junit.Assert.assertTrue
6+
import org.junit.Ignore
7+
import org.junit.Test
8+
9+
class CypherDirectiveTest {
10+
11+
val schema = """
12+
type Person {
13+
name: String @cypher(statement:"RETURN this.name")
14+
age(mult:Int=13) : [Int] @cypher(statement:"RETURN this.age * mult as age")
15+
}
16+
type Query {
17+
person : [Person]
18+
}
19+
"""
20+
21+
@Test
22+
fun renderCypherDirective() {
23+
24+
val expected = """MATCH (person:Person) RETURN person { name:apoc.cypher.runFirstColumnSingle('WITH ${"$"}this AS this RETURN this.name',{this:person}) } AS person"""
25+
val query = """{ person { name }}"""
26+
assertQuery(query, expected, emptyMap())
27+
}
28+
29+
@Test
30+
fun renderCypherDirectiveWithParamsDefaults() {
31+
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"""
33+
val query = """{ person { age }}"""
34+
assertQuery(query, expected, mapOf("personMult" to 13))
35+
}
36+
37+
@Test
38+
fun renderCypherDirectiveWithParams() {
39+
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"""
41+
val query = """{ person { age(mult:25) }}"""
42+
assertQuery(query, expected, mapOf("personMult" to 25L))
43+
}
44+
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))
49+
}
50+
51+
@Test @Ignore
52+
fun testTck() {
53+
TckTest(schema).testTck("cypher-directive-test.md", 0)
54+
}
55+
}

0 commit comments

Comments
 (0)