Skip to content

Commit 23b5201

Browse files
committed
added @cypher directive for mutations
support field arguments as parameter supports further drill-down in graphql query mutation cypher query has to return the data if there is a conflict between generated and manual mutations or queries the manual one overrides
1 parent 07a66af commit 23b5201

File tree

2 files changed

+62
-23
lines changed

2 files changed

+62
-23
lines changed

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

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,39 +32,52 @@ class Translator(val schema: GraphQLSchema) {
3232
val ast = parse(query) // todo preparsedDocumentProvider
3333
val ctx = context.copy(fragments = ast.definitions.filterIsInstance<FragmentDefinition>().map { it.name to it }.toMap())
3434
val queries = ast.definitions.filterIsInstance<OperationDefinition>()
35-
.filter { it.operation == OperationDefinition.Operation.QUERY } // todo variabledefinitions, directives, name
35+
.filter { it.operation == OperationDefinition.Operation.QUERY || it.operation == OperationDefinition.Operation.MUTATION } // todo variabledefinitions, directives, name
3636
.flatMap { it.selectionSet.selections }
3737
.filterIsInstance<Field>() // FragmentSpread, InlineFragment
3838
.map { toQuery(it, ctx).with(params) } // arguments, alias, directives, selectionSet
3939
return queries
4040
}
4141

42-
private fun toQuery(queryField: Field, ctx:Context = Context()): Cypher {
43-
val name = queryField.name
44-
val queryType = schema.queryType.fieldDefinitions.filter { it.name == name }.firstOrNull() ?: throw IllegalArgumentException("Unknown Query $name available queries: " + schema.queryType.fieldDefinitions.map { it.name }.joinToString())
45-
val returnType = queryType.type.inner()
42+
private fun toQuery(field: Field, ctx:Context = Context()): Cypher {
43+
val name = field.name
44+
val queryType = schema.queryType.fieldDefinitions.filter { it.name == name }.firstOrNull()
45+
val mutationType = schema.mutationType.fieldDefinitions.filter { it.name == name }.firstOrNull()
46+
val fieldDefinition = queryType ?: mutationType
47+
?: throw IllegalArgumentException("Unknown Query $name available queries: " + schema.queryType.fieldDefinitions.map { it.name }.joinToString())
48+
val isQuery = queryType != null
49+
val returnType = fieldDefinition.type.inner()
4650
// println(returnType)
4751
val type = schema.getType(returnType.name)
4852
val label = type.name.quote()
49-
val variable = queryField.aliasOrName().decapitalize()
50-
val cypherDirective = queryType.cypherDirective()
51-
val mapProjection = projectFields(variable, queryField, type, ctx)
52-
val skipLimit = format(skipLimit(queryField.arguments))
53-
val ordering = orderBy(variable, queryField.arguments)
53+
val variable = field.aliasOrName().decapitalize()
54+
val cypherDirective = fieldDefinition.cypherDirective()
55+
val mapProjection = projectFields(variable, field, type, ctx)
56+
val skipLimit = format(skipLimit(field.arguments))
57+
val ordering = orderBy(variable, field.arguments)
5458
if (cypherDirective != null) {
5559
// 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))
60+
return cypherQueryOrMutation(variable, fieldDefinition, field, cypherDirective, mapProjection, ordering, skipLimit, isQuery)
5961

6062
} 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+
val where = if (ctx.topLevelWhere) where(variable, fieldDefinition, type, propertyArguments(field)) else Cypher.EMPTY
64+
val properties = if (ctx.topLevelWhere) Cypher.EMPTY else properties(variable, fieldDefinition, propertyArguments(field))
6365
return Cypher("MATCH ($variable:$label${properties.query})${where.query} RETURN ${mapProjection.query} AS $variable$ordering$skipLimit" ,
6466
(mapProjection.params + properties.params + where.params))
6567
}
6668
}
6769

70+
private fun cypherQueryOrMutation(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: Cypher, mapProjection: Cypher, ordering: String, skipLimit: String, isQuery: Boolean) =
71+
if (isQuery) {
72+
val (query, params) = cypherDirective(variable, fieldDefinition, field, cypherDirective, emptyList())
73+
Cypher("UNWIND $query AS $variable RETURN ${mapProjection.query} AS $variable$ordering$skipLimit",
74+
(params + mapProjection.params))
75+
} else {
76+
val (query, params) = cypherDirectiveQuery(variable, fieldDefinition, field, cypherDirective, emptyList())
77+
Cypher("CALL apoc.cypher.doIt($query) YIELD value WITH value[head(keys(value))] AS $variable RETURN ${mapProjection.query} AS $variable$ordering$skipLimit",
78+
(params + mapProjection.params))
79+
}
80+
6881
private fun propertyArguments(queryField: Field) =
6982
queryField.arguments.filterNot { listOf("first", "offset", "orderBy").contains(it.name) }
7083

@@ -162,12 +175,18 @@ class Translator(val schema: GraphQLSchema) {
162175
return Cypher(field.aliasOrName() +":" + query, params)
163176
}
164177
private fun cypherDirective(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: Cypher, additionalArgs: List<CypherArgument>): Cypher {
178+
val suffix = if (fieldDefinition.type.isList()) "Many" else "Single"
179+
val (query, args) = cypherDirectiveQuery(variable, fieldDefinition, field, cypherDirective, additionalArgs)
180+
return Cypher("apoc.cypher.runFirstColumn$suffix($query)", args)
181+
}
182+
183+
private fun cypherDirectiveQuery(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: Cypher, additionalArgs: List<CypherArgument>): Cypher {
165184
val suffix = if (fieldDefinition.type.isList()) "Many" else "Single"
166185
val args = additionalArgs + prepareFieldArguments(fieldDefinition, field.arguments)
167186
val argParams = args.map { '$' + it.name + " AS " + it.name }.joinNonEmpty(",")
168187
val query = (if (argParams.isEmpty()) "" else "WITH $argParams ") + cypherDirective.escapedQuery()
169188
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 })
189+
return Cypher("'$query',$argString", args.filter { it.name != "this" }.associate { paramName(variable, it.name, it.value) to it.value })
171190
}
172191

173192
fun projectNamedFragments(variable: String, fragmentSpread: FragmentSpread, type: GraphQLObjectType, ctx: Context) =
@@ -329,23 +348,29 @@ object SchemaBuilder {
329348

330349
val queryDefinition = operations.getOrElse("query") {
331350
ObjectTypeDefinition("Query").also {
332-
schemaDefinition.operationTypeDefinitions.add(OperationTypeDefinition("query",TypeName(it.name)))
351+
schemaDefinition.operationTypeDefinitions.add(OperationTypeDefinition("query", TypeName(it.name)))
333352
typeDefinitionRegistry.add(it)
334353
}
335354
}
336355
val mutationDefinition = operations.getOrElse("mutation") {
337-
ObjectTypeDefinition("Mutation").also { schemaDefinition.operationTypeDefinitions.add(OperationTypeDefinition("mutation",TypeName(it.name)))
338-
typeDefinitionRegistry.add(it)
356+
ObjectTypeDefinition("Mutation").also {
357+
schemaDefinition.operationTypeDefinitions.add(OperationTypeDefinition("mutation", TypeName(it.name)))
358+
typeDefinitionRegistry.add(it)
339359
}
340360
}
341-
342-
augmentations.filter { it.query.isNotBlank() }.map { it.query }.let {
361+
// todo better filter
362+
augmentations
363+
.filter { it.query.isNotBlank() && queryDefinition.fieldDefinitions.none { fd -> it.query.startsWith(fd.name+"(") } }
364+
.map { it.query }.let {
343365
if (!it.isEmpty()) {
344366
val newQueries = schemaParser.parse("type AugmentedQuery { ${it.joinToString("\n")} }").getType("AugmentedQuery").get() as ObjectTypeDefinition
345367
queryDefinition.fieldDefinitions.addAll(newQueries.fieldDefinitions)
346368
}
347369
}
348-
augmentations.flatMap { listOf(it.create,it.update,it.delete).filter{ it.isNotBlank() }}.let {
370+
augmentations
371+
.flatMap { listOf(it.create,it.update,it.delete)
372+
.filter{ it.isNotBlank() && mutationDefinition.fieldDefinitions.none { fd -> it.startsWith(fd.name+"(") } }}
373+
.let {
349374
if (!it.isEmpty()) {
350375
val newQueries = schemaParser.parse("type AugmentedMutation { ${it.joinToString("\n")} }").getType("AugmentedMutation").get() as ObjectTypeDefinition
351376
mutationDefinition.fieldDefinitions.addAll(newQueries.fieldDefinitions)

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ type Query {
2121
p2: [Person] @cypher(statement:"MATCH (p:Person) RETURN p")
2222
p3(name:String): Person @cypher(statement:"MATCH (p:Person) WHERE p.name = name RETURN p LIMIT 1")
2323
}
24+
type Mutation {
25+
createPerson(name:String): Person @cypher(statement:"CREATE (p:Person) SET p.name = name RETURN p")
26+
}
27+
schema {
28+
query: Query
29+
mutation: Mutation
30+
}
2431
"""
2532

2633
@Test
@@ -33,7 +40,6 @@ type Query {
3340

3441
@Test
3542
fun renderCypherFieldDirectiveWithParamsDefaults() {
36-
3743
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"""
3844
val query = """{ person { age }}"""
3945
assertQuery(query, expected, mapOf("personMult" to 13))
@@ -66,6 +72,14 @@ type Query {
6672
assertQuery(query, expected, mapOf("pname" to VariableReference("pname")))
6773
}
6874

75+
@Test
76+
fun renderCypherMutationDirective() {
77+
val expected = """CALL apoc.cypher.doIt('WITH ${'$'}name AS name CREATE (p:Person) SET p.name = name RETURN p',{name:${'$'}personName}) YIELD value WITH value[head(keys(value))] AS person RETURN person { .id } AS person"""
78+
val query = """mutation { person: createPerson(name:"Joe") { id }}"""
79+
assertQuery(query, expected, mapOf("personName" to "Joe"))
80+
}
81+
82+
6983
@Test @Ignore
7084
fun testTck() {
7185
TckTest(schema).testTck("cypher-directive-test.md", 0)

0 commit comments

Comments
 (0)