diff --git a/core/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt b/core/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt index c7403973..46f50a93 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt @@ -98,13 +98,10 @@ fun GraphQLFieldsContainer.relevantFields() = fieldDefinitions fun GraphQLFieldsContainer.relationship(): RelationshipInfo? { val relDirective = (this as? GraphQLDirectiveContainer)?.getDirective(DirectiveConstants.RELATION) ?: return null - val directiveResolver: (name: String, defaultValue: String?) -> String? = { argName, defaultValue -> - relDirective.getArgument(argName, defaultValue) - } - val relType = directiveResolver(RELATION_NAME, "")!! - val startField = directiveResolver(RELATION_FROM, null) - val endField = directiveResolver(RELATION_TO, null) - val direction = directiveResolver(RELATION_DIRECTION, null)?.let { RelationDirection.valueOf(it) } + val relType = relDirective.getArgument(RELATION_NAME, "")!! + val startField = relDirective.getMandatoryArgument(RELATION_FROM) + val endField = relDirective.getMandatoryArgument(RELATION_TO) + val direction = relDirective.getArgument(RELATION_DIRECTION)?.let { RelationDirection.valueOf(it) } ?: RelationDirection.OUT return RelationshipInfo(this, relType, direction, startField, endField) } @@ -127,17 +124,17 @@ fun relDetails(type: GraphQLFieldsContainer, relDirective: GraphQLDirective): Re return RelationshipInfo(type, relType, direction, - relDirective.getArgument(RELATION_FROM, null), - relDirective.getArgument(RELATION_TO, null)) + relDirective.getMandatoryArgument(RELATION_FROM), + relDirective.getMandatoryArgument(RELATION_TO) + ) } data class RelationshipInfo( val type: GraphQLFieldsContainer, val relType: String, val direction: RelationDirection, - val startField: String? = null, - val endField: String? = null, - val isRelFromType: Boolean = false + val startField: String, + val endField: String ) { data class RelatedField( val argumentName: String, @@ -145,12 +142,6 @@ data class RelationshipInfo( val declaringType: GraphQLFieldsContainer ) - val arrows = when (direction) { - RelationDirection.IN -> "<" to "" - RelationDirection.OUT -> "" to ">" - RelationDirection.BOTH -> "" to "" - } - val typeName: String get() = this.type.name fun getStartFieldId() = getRelatedIdField(this.startField) @@ -197,7 +188,10 @@ fun GraphQLType.getInnerFieldsContainer() = inner() as? GraphQLFieldsContainer fun GraphQLDirectiveContainer.getDirectiveArgument(directiveName: String, argumentName: String, defaultValue: T?): T? = getDirective(directiveName)?.getArgument(argumentName, defaultValue) ?: defaultValue -fun GraphQLDirective.getArgument(argumentName: String, defaultValue: T?): T? { +fun GraphQLDirective.getMandatoryArgument(argumentName: String, defaultValue: T? = null): T = this.getArgument(argumentName, defaultValue) + ?: throw IllegalStateException(argumentName + " is required for @${this.name}") + +fun GraphQLDirective.getArgument(argumentName: String, defaultValue: T? = null): T? { val argument = getArgument(argumentName) @Suppress("UNCHECKED_CAST") return argument?.value as T? diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt index 17a8083b..25437476 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt @@ -82,7 +82,7 @@ open class ProjectionBase { } ?.let { parseFilter(it as ObjectValue, type) } ?.let { - val filterCondition = handleQuery(normalizeName(FILTER ,variable), "", propertyContainer, it, type) + val filterCondition = handleQuery(normalizeName(FILTER, variable), "", propertyContainer, it, type) result.and(filterCondition) } ?: result @@ -118,7 +118,7 @@ open class ProjectionBase { RelationOperator.NONE -> Predicates.none(cond) else -> null }?.let { - val targetNode = predicate.relNode.named(normalizeName(variablePrefix,predicate.relationshipInfo.typeName)) + val targetNode = predicate.relNode.named(normalizeName(variablePrefix, predicate.relationshipInfo.typeName)) val parsedQuery2 = parseFilter(objectField.value as ObjectValue, type) val condition = handleQuery(targetNode.requiredSymbolicName.value, "", targetNode, parsedQuery2, type) var where = it @@ -338,12 +338,42 @@ open class ProjectionBase { return if (inverse) relInfo0.copy(direction = relInfo0.direction.invert(), startField = relInfo0.endField, endField = relInfo0.startField) else relInfo0 } - private fun projectRelationshipParent(node: PropertyContainer, variable: SymbolicName, field: Field, fieldDefinition: GraphQLFieldDefinition, parent: GraphQLFieldsContainer, env: DataFetchingEnvironment, variableSuffix: String?): Expression { + private fun projectRelationshipParent(propertyContainer: PropertyContainer, variable: SymbolicName, field: Field, fieldDefinition: GraphQLFieldDefinition, parent: GraphQLFieldsContainer, env: DataFetchingEnvironment, variableSuffix: String?): Expression { val fieldObjectType = fieldDefinition.type.inner() as? GraphQLFieldsContainer ?: throw IllegalArgumentException("field ${fieldDefinition.name} of type ${parent.name} is not an object (fields container) and can not be handled as relationship") - val projectionEntries = projectFields(anyNode(variable), name(variable.value + (variableSuffix?.capitalize() - ?: "")), field, fieldObjectType, env, variableSuffix) - return node.project(projectionEntries) + return when (propertyContainer) { + is Node -> { + val projectionEntries = projectFields(propertyContainer, name(variable.value + (variableSuffix?.capitalize() + ?: "")), field, fieldObjectType, env, variableSuffix) + propertyContainer.project(projectionEntries) + } + is Relationship -> projectNodeFromRichRelationship(parent, fieldDefinition, variable, field, env) + else -> throw IllegalArgumentException("${propertyContainer.javaClass.name} cannot be handled for field ${fieldDefinition.name} of type ${parent.name}") + } + } + + private fun projectNodeFromRichRelationship( + parent: GraphQLFieldsContainer, + fieldDefinition: GraphQLFieldDefinition, + variable: SymbolicName, + field: Field, + env: DataFetchingEnvironment + ): Expression { + val relInfo = parent.relationship() + ?: throw IllegalStateException(parent.name + " is not an relation type") + + val node = CypherDSL.node(fieldDefinition.type.name()).named(fieldDefinition.name) + val (start, end, target) = when (fieldDefinition.name) { + relInfo.startField -> Triple(node, anyNode(), node) + relInfo.endField -> Triple(anyNode(), node, node) + else -> throw IllegalArgumentException("type ${parent.name} does not have a matching field with name ${fieldDefinition.name}") + } + val rel = when (relInfo.direction) { + RelationDirection.IN -> start.relationshipFrom(end).named(variable) + RelationDirection.OUT -> start.relationshipTo(end).named(variable) + RelationDirection.BOTH -> start.relationshipBetween(end).named(variable) + } + return head(CypherDSL.listBasedOn(rel).returning(target.project(projectFields(target, field, fieldDefinition.type as GraphQLFieldsContainer, env)))) } private fun projectRichAndRegularRelationship(variable: SymbolicName, field: Field, fieldDefinition: GraphQLFieldDefinition, parent: GraphQLFieldsContainer, env: DataFetchingEnvironment): Expression { @@ -366,7 +396,7 @@ open class ProjectionBase { val (endNodePattern, variableSuffix) = when { isRelFromType -> { - val label = nodeType.getFieldDefinition(relInfo.endField!!)!!.type.innerName() + val label = nodeType.getFieldDefinition(relInfo.endField)!!.type.innerName() node(label).named("$childVariable${relInfo.endField.capitalize()}") to relInfo.endField } else -> node(nodeType.name).named(childVariableName) to null diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/relation/BaseRelationHandler.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/relation/BaseRelationHandler.kt index c231c9cd..7c90b4ba 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/relation/BaseRelationHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/relation/BaseRelationHandler.kt @@ -130,10 +130,10 @@ abstract class BaseRelationHandler( val relFieldName: String val idField: RelationshipInfo.RelatedField if (start) { - relFieldName = relation.startField!! + relFieldName = relation.startField idField = startId } else { - relFieldName = relation.endField!! + relFieldName = relation.endField idField = endId } if (!arguments.containsKey(idField.argumentName)) { diff --git a/core/src/test/resources/issues/gh-170.adoc b/core/src/test/resources/issues/gh-170.adoc new file mode 100644 index 00000000..12aa0517 --- /dev/null +++ b/core/src/test/resources/issues/gh-170.adoc @@ -0,0 +1,84 @@ +:toc: + += Github Issue #170: wrong mapping starting query from rich relationship + +== Schema + +[source,graphql,schema=true] +---- +type Movie { + title: String + ratings: [Rated] @relation(name:"RATED") +} +interface Person { + name: String +} +type User { + name: String + rated(rating: Int): [Rated] +} +type Rated @relation(name:"RATED", from: "user", to: "movie") { + user: User + rating: Int + movie: Movie +} +---- + +[source,cypher,test-data=true] +---- +CREATE + (u:User{ name: 'Andreas' }), + (m1:Movie{ title: 'Forrest Gump' }), + (m2:Movie{ title: 'Apollo 13' }), + (m3:Movie{ title: 'Harry Potter' }), + (u)-[:RATED{ rating: 2}]->(m1), + (u)-[:RATED{ rating: 3}]->(m2), + (u)-[:RATED{ rating: 4}]->(m3); +---- + +== Query + +.GraphQL-Query +[source,graphql] +---- +query { + r: rated( rating_gte : 3) { + rating + movie { + title + } + } +} +---- + +.Cypher Params +[source,json] +---- +{ + "rRatingGte" : 3 +} +---- + +.GraphQL-Response +[source,json,response=true] +---- +{ + "r" : [ + { "rating": 3 ,"movie" : { "title" : "Apollo 13" } }, + { "rating": 4 ,"movie" : { "title" : "Harry Potter" } } + ] +} +---- + +.Cypher +[source,cypher] +---- +MATCH ()-[r:RATED]->() +WHERE r.rating >= $rRatingGte +RETURN r { + .rating, + movie: head([()-[r]->(movie:Movie) | movie { + .title + }]) +} AS r +---- diff --git a/core/src/test/resources/movie-tests.adoc b/core/src/test/resources/movie-tests.adoc index 78e15529..9c0564ee 100644 --- a/core/src/test/resources/movie-tests.adoc +++ b/core/src/test/resources/movie-tests.adoc @@ -1536,10 +1536,9 @@ RETURN deleteState AS deleteState mutation{ deleteRated(_id: 1){ rating -# TODO this does not work correctly -# from { -# name -# } + from { + name + } } } ---- @@ -1558,7 +1557,10 @@ mutation{ MATCH ()-[deleteRated:RATED]->() WHERE id(deleteRated) = toInteger($deleteRated_id) WITH deleteRated AS toDelete, deleteRated { - .rating + .rating, + from: head([(from:User)-[deleteRated]->() | from { + .name + }]) } AS deleteRated DETACH DELETE toDelete RETURN deleteRated AS deleteRated ----