Skip to content

Commit 7e6804c

Browse files
authored
allow projection starting query from rich relationship (#175)
resolves #170
1 parent 3ccea67 commit 7e6804c

File tree

5 files changed

+143
-33
lines changed

5 files changed

+143
-33
lines changed

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

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,10 @@ fun GraphQLFieldsContainer.relevantFields() = fieldDefinitions
9898

9999
fun GraphQLFieldsContainer.relationship(): RelationshipInfo? {
100100
val relDirective = (this as? GraphQLDirectiveContainer)?.getDirective(DirectiveConstants.RELATION) ?: return null
101-
val directiveResolver: (name: String, defaultValue: String?) -> String? = { argName, defaultValue ->
102-
relDirective.getArgument(argName, defaultValue)
103-
}
104-
val relType = directiveResolver(RELATION_NAME, "")!!
105-
val startField = directiveResolver(RELATION_FROM, null)
106-
val endField = directiveResolver(RELATION_TO, null)
107-
val direction = directiveResolver(RELATION_DIRECTION, null)?.let { RelationDirection.valueOf(it) }
101+
val relType = relDirective.getArgument(RELATION_NAME, "")!!
102+
val startField = relDirective.getMandatoryArgument<String>(RELATION_FROM)
103+
val endField = relDirective.getMandatoryArgument<String>(RELATION_TO)
104+
val direction = relDirective.getArgument<String>(RELATION_DIRECTION)?.let { RelationDirection.valueOf(it) }
108105
?: RelationDirection.OUT
109106
return RelationshipInfo(this, relType, direction, startField, endField)
110107
}
@@ -127,30 +124,24 @@ fun relDetails(type: GraphQLFieldsContainer, relDirective: GraphQLDirective): Re
127124
return RelationshipInfo(type,
128125
relType,
129126
direction,
130-
relDirective.getArgument<String>(RELATION_FROM, null),
131-
relDirective.getArgument<String>(RELATION_TO, null))
127+
relDirective.getMandatoryArgument(RELATION_FROM),
128+
relDirective.getMandatoryArgument(RELATION_TO)
129+
)
132130
}
133131

134132
data class RelationshipInfo(
135133
val type: GraphQLFieldsContainer,
136134
val relType: String,
137135
val direction: RelationDirection,
138-
val startField: String? = null,
139-
val endField: String? = null,
140-
val isRelFromType: Boolean = false
136+
val startField: String,
137+
val endField: String
141138
) {
142139
data class RelatedField(
143140
val argumentName: String,
144141
val field: GraphQLFieldDefinition,
145142
val declaringType: GraphQLFieldsContainer
146143
)
147144

148-
val arrows = when (direction) {
149-
RelationDirection.IN -> "<" to ""
150-
RelationDirection.OUT -> "" to ">"
151-
RelationDirection.BOTH -> "" to ""
152-
}
153-
154145
val typeName: String get() = this.type.name
155146

156147
fun getStartFieldId() = getRelatedIdField(this.startField)
@@ -197,7 +188,10 @@ fun GraphQLType.getInnerFieldsContainer() = inner() as? GraphQLFieldsContainer
197188
fun <T> GraphQLDirectiveContainer.getDirectiveArgument(directiveName: String, argumentName: String, defaultValue: T?): T? =
198189
getDirective(directiveName)?.getArgument(argumentName, defaultValue) ?: defaultValue
199190

200-
fun <T> GraphQLDirective.getArgument(argumentName: String, defaultValue: T?): T? {
191+
fun <T> GraphQLDirective.getMandatoryArgument(argumentName: String, defaultValue: T? = null): T = this.getArgument(argumentName, defaultValue)
192+
?: throw IllegalStateException(argumentName + " is required for @${this.name}")
193+
194+
fun <T> GraphQLDirective.getArgument(argumentName: String, defaultValue: T? = null): T? {
201195
val argument = getArgument(argumentName)
202196
@Suppress("UNCHECKED_CAST")
203197
return argument?.value as T?

core/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ open class ProjectionBase {
8282
}
8383
?.let { parseFilter(it as ObjectValue, type) }
8484
?.let {
85-
val filterCondition = handleQuery(normalizeName(FILTER ,variable), "", propertyContainer, it, type)
85+
val filterCondition = handleQuery(normalizeName(FILTER, variable), "", propertyContainer, it, type)
8686
result.and(filterCondition)
8787
}
8888
?: result
@@ -118,7 +118,7 @@ open class ProjectionBase {
118118
RelationOperator.NONE -> Predicates.none(cond)
119119
else -> null
120120
}?.let {
121-
val targetNode = predicate.relNode.named(normalizeName(variablePrefix,predicate.relationshipInfo.typeName))
121+
val targetNode = predicate.relNode.named(normalizeName(variablePrefix, predicate.relationshipInfo.typeName))
122122
val parsedQuery2 = parseFilter(objectField.value as ObjectValue, type)
123123
val condition = handleQuery(targetNode.requiredSymbolicName.value, "", targetNode, parsedQuery2, type)
124124
var where = it
@@ -338,12 +338,42 @@ open class ProjectionBase {
338338
return if (inverse) relInfo0.copy(direction = relInfo0.direction.invert(), startField = relInfo0.endField, endField = relInfo0.startField) else relInfo0
339339
}
340340

341-
private fun projectRelationshipParent(node: PropertyContainer, variable: SymbolicName, field: Field, fieldDefinition: GraphQLFieldDefinition, parent: GraphQLFieldsContainer, env: DataFetchingEnvironment, variableSuffix: String?): Expression {
341+
private fun projectRelationshipParent(propertyContainer: PropertyContainer, variable: SymbolicName, field: Field, fieldDefinition: GraphQLFieldDefinition, parent: GraphQLFieldsContainer, env: DataFetchingEnvironment, variableSuffix: String?): Expression {
342342
val fieldObjectType = fieldDefinition.type.inner() as? GraphQLFieldsContainer
343343
?: throw IllegalArgumentException("field ${fieldDefinition.name} of type ${parent.name} is not an object (fields container) and can not be handled as relationship")
344-
val projectionEntries = projectFields(anyNode(variable), name(variable.value + (variableSuffix?.capitalize()
345-
?: "")), field, fieldObjectType, env, variableSuffix)
346-
return node.project(projectionEntries)
344+
return when (propertyContainer) {
345+
is Node -> {
346+
val projectionEntries = projectFields(propertyContainer, name(variable.value + (variableSuffix?.capitalize()
347+
?: "")), field, fieldObjectType, env, variableSuffix)
348+
propertyContainer.project(projectionEntries)
349+
}
350+
is Relationship -> projectNodeFromRichRelationship(parent, fieldDefinition, variable, field, env)
351+
else -> throw IllegalArgumentException("${propertyContainer.javaClass.name} cannot be handled for field ${fieldDefinition.name} of type ${parent.name}")
352+
}
353+
}
354+
355+
private fun projectNodeFromRichRelationship(
356+
parent: GraphQLFieldsContainer,
357+
fieldDefinition: GraphQLFieldDefinition,
358+
variable: SymbolicName,
359+
field: Field,
360+
env: DataFetchingEnvironment
361+
): Expression {
362+
val relInfo = parent.relationship()
363+
?: throw IllegalStateException(parent.name + " is not an relation type")
364+
365+
val node = CypherDSL.node(fieldDefinition.type.name()).named(fieldDefinition.name)
366+
val (start, end, target) = when (fieldDefinition.name) {
367+
relInfo.startField -> Triple(node, anyNode(), node)
368+
relInfo.endField -> Triple(anyNode(), node, node)
369+
else -> throw IllegalArgumentException("type ${parent.name} does not have a matching field with name ${fieldDefinition.name}")
370+
}
371+
val rel = when (relInfo.direction) {
372+
RelationDirection.IN -> start.relationshipFrom(end).named(variable)
373+
RelationDirection.OUT -> start.relationshipTo(end).named(variable)
374+
RelationDirection.BOTH -> start.relationshipBetween(end).named(variable)
375+
}
376+
return head(CypherDSL.listBasedOn(rel).returning(target.project(projectFields(target, field, fieldDefinition.type as GraphQLFieldsContainer, env))))
347377
}
348378

349379
private fun projectRichAndRegularRelationship(variable: SymbolicName, field: Field, fieldDefinition: GraphQLFieldDefinition, parent: GraphQLFieldsContainer, env: DataFetchingEnvironment): Expression {
@@ -366,7 +396,7 @@ open class ProjectionBase {
366396

367397
val (endNodePattern, variableSuffix) = when {
368398
isRelFromType -> {
369-
val label = nodeType.getFieldDefinition(relInfo.endField!!)!!.type.innerName()
399+
val label = nodeType.getFieldDefinition(relInfo.endField)!!.type.innerName()
370400
node(label).named("$childVariable${relInfo.endField.capitalize()}") to relInfo.endField
371401
}
372402
else -> node(nodeType.name).named(childVariableName) to null

core/src/main/kotlin/org/neo4j/graphql/handler/relation/BaseRelationHandler.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,10 @@ abstract class BaseRelationHandler(
130130
val relFieldName: String
131131
val idField: RelationshipInfo.RelatedField
132132
if (start) {
133-
relFieldName = relation.startField!!
133+
relFieldName = relation.startField
134134
idField = startId
135135
} else {
136-
relFieldName = relation.endField!!
136+
relFieldName = relation.endField
137137
idField = endId
138138
}
139139
if (!arguments.containsKey(idField.argumentName)) {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
:toc:
2+
3+
= Github Issue #170: wrong mapping starting query from rich relationship
4+
5+
== Schema
6+
7+
[source,graphql,schema=true]
8+
----
9+
type Movie {
10+
title: String
11+
ratings: [Rated] @relation(name:"RATED")
12+
}
13+
interface Person {
14+
name: String
15+
}
16+
type User {
17+
name: String
18+
rated(rating: Int): [Rated]
19+
}
20+
type Rated @relation(name:"RATED", from: "user", to: "movie") {
21+
user: User
22+
rating: Int
23+
movie: Movie
24+
}
25+
----
26+
27+
[source,cypher,test-data=true]
28+
----
29+
CREATE
30+
(u:User{ name: 'Andreas' }),
31+
(m1:Movie{ title: 'Forrest Gump' }),
32+
(m2:Movie{ title: 'Apollo 13' }),
33+
(m3:Movie{ title: 'Harry Potter' }),
34+
(u)-[:RATED{ rating: 2}]->(m1),
35+
(u)-[:RATED{ rating: 3}]->(m2),
36+
(u)-[:RATED{ rating: 4}]->(m3);
37+
----
38+
39+
== Query
40+
41+
.GraphQL-Query
42+
[source,graphql]
43+
----
44+
query {
45+
r: rated( rating_gte : 3) {
46+
rating
47+
movie {
48+
title
49+
}
50+
}
51+
}
52+
----
53+
54+
.Cypher Params
55+
[source,json]
56+
----
57+
{
58+
"rRatingGte" : 3
59+
}
60+
----
61+
62+
.GraphQL-Response
63+
[source,json,response=true]
64+
----
65+
{
66+
"r" : [
67+
{ "rating": 3 ,"movie" : { "title" : "Apollo 13" } },
68+
{ "rating": 4 ,"movie" : { "title" : "Harry Potter" } }
69+
]
70+
}
71+
----
72+
73+
.Cypher
74+
[source,cypher]
75+
----
76+
MATCH ()-[r:RATED]->()
77+
WHERE r.rating >= $rRatingGte
78+
RETURN r {
79+
.rating,
80+
movie: head([()-[r]->(movie:Movie) | movie {
81+
.title
82+
}])
83+
} AS r
84+
----

core/src/test/resources/movie-tests.adoc

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,10 +1536,9 @@ RETURN deleteState AS deleteState
15361536
mutation{
15371537
deleteRated(_id: 1){
15381538
rating
1539-
# TODO this does not work correctly
1540-
# from {
1541-
# name
1542-
# }
1539+
from {
1540+
name
1541+
}
15431542
}
15441543
}
15451544
----
@@ -1558,7 +1557,10 @@ mutation{
15581557
MATCH ()-[deleteRated:RATED]->()
15591558
WHERE id(deleteRated) = toInteger($deleteRated_id)
15601559
WITH deleteRated AS toDelete, deleteRated {
1561-
.rating
1560+
.rating,
1561+
from: head([(from:User)-[deleteRated]->() | from {
1562+
.name
1563+
}])
15621564
} AS deleteRated DETACH DELETE toDelete
15631565
RETURN deleteRated AS deleteRated
15641566
----

0 commit comments

Comments
 (0)