Skip to content

Commit 6458f5c

Browse files
committed
First stab at relationship-entities
1 parent 9ef809b commit 6458f5c

File tree

3 files changed

+93
-31
lines changed

3 files changed

+93
-31
lines changed

readme.adoc

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,19 @@ cypher == "MATCH (p:Person) WHERE p.name = 'Joe' RETURN p {.age}"
3434
* handle first, offset arguments
3535
* argument types: string, int, float, array
3636
* parameter support
37+
* parametrization
3738
* aliases
39+
* inline and named fragments
40+
* sorting (top-level)
3841

3942
== Next
4043

41-
* sorting
44+
* @relationship types
45+
* filters
46+
* sorting (nested)
4247
* interfaces
43-
* inline and named fragments
4448
* input types
4549
* @cypher for fields
46-
* filters
47-
* @relationship types
50+
* auto-generate queries
4851
* auto-generate mutations
4952
* unions

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

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ class Translator(val schema: GraphQLSchema) {
104104
private fun projectField(variable: String, field: Field, type: GraphQLObjectType, ctx:Context) : Cypher {
105105
val fieldDefinition = type.getFieldDefinition(field.name)
106106
return if (inner(fieldDefinition.type) is GraphQLObjectType) {
107-
val patternComprehensions = projectRelationship(variable, field, fieldDefinition, ctx)
107+
val patternComprehensions = projectRelationship(variable, field, fieldDefinition, type, ctx)
108108
Cypher(field.aliasOrName() + ":" + patternComprehensions.query, patternComprehensions.params)
109109
} else Cypher("." + field.aliasOrName())
110110
}
@@ -129,23 +129,64 @@ class Translator(val schema: GraphQLSchema) {
129129
projectFragment(fragment.typeCondition.name, type, variable, ctx, fragment.selectionSet)
130130

131131

132-
private fun projectRelationship(variable: String, field: Field, fieldDefinition: GraphQLFieldDefinition, ctx:Context): Cypher {
132+
private fun projectRelationship(variable: String, field: Field, fieldDefinition: GraphQLFieldDefinition, parent:GraphQLObjectType, ctx:Context): Cypher {
133133
val fieldType = fieldDefinition.type
134134
val innerType = inner(fieldType).name
135-
val fieldObjectType = schema.getType(innerType)
136-
val relDirective = fieldDefinition.definition.getDirective("relation")
137-
?: throw IllegalStateException("Field $field needs an @relation directive")
138-
val relType = relDirective.getArgument("name").value.toJavaValue()
139-
val relDirection = relDirective.getArgument("direction").value.toJavaValue()
140-
val (inArrow, outArrow) = if (relDirection.toString() == "IN") "<" to "" else "" to ">"
141-
val childVariable = field.name + fieldObjectType.name
142-
val childPattern = "$childVariable:$innerType"
143-
val where = where(childVariable,fieldDefinition,propertyArguments(field))
144-
val fieldProjection = projectFields(childVariable, field, fieldObjectType, ctx)
145-
val comprehension = "[($variable)$inArrow-[:${relType}]-$outArrow($childPattern)${where.query} | ${fieldProjection.query}]"
146-
val skipLimit = skipLimit(field.arguments)
147-
val slice = slice(skipLimit,fieldType.isList())
148-
return Cypher(comprehension + slice, (where.params + fieldProjection.params))
135+
val fieldObjectType = inner(fieldType) as GraphQLObjectType // schema.getType(innerType) as GraphQLObjectType
136+
val parentIsRelationship = parent.definition.directivesByName.containsKey("relation")
137+
// todo combine both if rel-entity
138+
if (!parentIsRelationship) {
139+
val (relDirective, relFromType) = fieldObjectType.definition.getDirective("relation")?.let { it to true }
140+
?: fieldDefinition.definition.getDirective("relation")?.let { it to false }
141+
?: throw IllegalStateException("Field $field needs an @relation directive")
142+
143+
val (relType, outgoing, endField) = relDetails(relDirective)
144+
val (inArrow, outArrow) = arrows(outgoing)
145+
146+
val childVariable = field.name // + fieldObjectType.name
147+
val endNodePattern = if (relFromType) {
148+
val label = inner(fieldObjectType.getFieldDefinition(endField).type).name
149+
"$childVariable${endField.capitalize()}:$label"
150+
} else "$childVariable:$innerType"
151+
val relPattern = if (relFromType) "$childVariable:${relType}" else ":${relType}"
152+
val where = where(childVariable, fieldDefinition, propertyArguments(field))
153+
val fieldProjection = projectFields(childVariable, field, fieldObjectType, ctx)
154+
155+
val comprehension = "[($variable)$inArrow-[$relPattern]-$outArrow($endNodePattern)${where.query} | ${fieldProjection.query}]"
156+
val skipLimit = skipLimit(field.arguments)
157+
val slice = slice(skipLimit, fieldType.isList())
158+
return Cypher(comprehension + slice, (where.params + fieldProjection.params))
159+
} else {
160+
val relDirective = parent.definition.directivesByName.getValue("relation")
161+
val (relType, outgoing, endField) = relDetails(relDirective)
162+
163+
val fieldProjection = projectFields(variable+endField.capitalize(), field, fieldObjectType, ctx)
164+
165+
return Cypher(fieldProjection.query)
166+
}
167+
}
168+
169+
private fun arrows(outgoing: Boolean?): Pair<String, String> {
170+
return when (outgoing) {
171+
false -> "<" to ""
172+
true -> "" to ">"
173+
null -> "" to ""
174+
}
175+
}
176+
177+
private fun relDetails(relDirective: Directive): Triple<String,Boolean?,String> {
178+
val arguments = relDirective.argumentsByName
179+
val relType = arguments.getValue("name").value.toJavaValue().toString()
180+
val relDirection = arguments.get("direction")?.value?.toJavaValue()
181+
?: schema.getDirective("relation").getArgument("direction").defaultValue
182+
val outgoing = when (relDirection.toString()) {
183+
"IN" -> false
184+
"BOTH" -> null
185+
"OUT" -> true
186+
else -> throw IllegalStateException("Unknown direction $relDirection")
187+
}
188+
val endField = if (outgoing == true) "end" else "start"
189+
return Triple(relType, outgoing, endField)
149190
}
150191

151192
private fun slice(skipLimit: Pair<Int, Int>, list: Boolean = false) =
@@ -240,7 +281,7 @@ object SchemaBuilder {
240281
.build()
241282

242283
val schemaGenerator = SchemaGenerator()
243-
val directives = setOf(GraphQLDirective.newDirective().name("relation").argument { it.name("name").type(Scalars.GraphQLString).also { it.name("direction").type(Scalars.GraphQLString) } }.build())
284+
val directives = setOf(GraphQLDirective.newDirective().name("relation").argument { it.name("name").type(Scalars.GraphQLString).also { it.name("direction").type(Scalars.GraphQLString).defaultValue("OUT") } }.build())
244285
return schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring).transform { bc -> bc.additionalDirectives(directives).build() }
245286
}
246287
}

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

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package org.neo4j.graphql
22

3-
import graphql.language.VariableReference
43
import org.antlr.v4.runtime.misc.ParseCancellationException
4+
import org.junit.Assert.assertEquals
5+
import org.junit.Assert.assertTrue
56
import org.junit.Test
67

7-
import org.junit.Assert.*
8-
98
class TranslatorTest {
109

1110
val schema =
@@ -14,9 +13,16 @@ class TranslatorTest {
1413
age: Int
1514
livesIn : Location @relation(name:"LIVES_IN", direction:"OUT")
1615
livedIn : [Location] @relation(name:"LIVED_IN", direction:"OUT")
16+
born : Birth
17+
}
18+
type Birth @relation(name:"BORN") {
19+
start: Person
20+
end: Location
21+
date: String
1722
}
1823
type Location {
1924
name: String
25+
founded: Person @relation(name:"FOUNDED", direction:"IN")
2026
}
2127
enum _PersonOrdering { name_asc, name_desc, age_asc, age_desc }
2228
enum E { pi, e }
@@ -45,37 +51,49 @@ class TranslatorTest {
4551
@Test
4652
fun nestedQuery() {
4753
val query = " { person { name age livesIn { name } } } "
48-
assertQuery(query, "MATCH (person:Person) RETURN person { .name,.age,livesIn:[(person)-[:LIVES_IN]->(livesInLocation:Location) | livesInLocation { .name }][0] } AS person")
54+
assertQuery(query, "MATCH (person:Person) RETURN person { .name,.age,livesIn:[(person)-[:LIVES_IN]->(livesIn:Location) | livesIn { .name }][0] } AS person")
55+
}
56+
57+
@Test
58+
fun nestedQuery2ndHop() {
59+
val query = " { person { name age livesIn { name founded {name}} } } "
60+
assertQuery(query, "MATCH (person:Person) RETURN person { .name,.age,livesIn:[(person)-[:LIVES_IN]->(livesIn:Location) | livesIn { .name,founded:[(livesIn)<-[:FOUNDED]-(founded:Person) | founded { .name }][0] }][0] } AS person")
61+
}
62+
63+
@Test
64+
fun richRelationship() {
65+
val query = " { person { name born { date end { name } } } } "
66+
assertQuery(query, "MATCH (person:Person) RETURN person { .name,born:[(person)-[born:BORN]->(bornEnd:Location) | born { .date,end:bornEnd { .name } }][0] } AS person")
4967
}
5068

5169
@Test
5270
fun nestedQueryParameter() {
5371
val query = """ { person { name age livesIn(name:"Berlin") { name } } } """
54-
assertQuery(query, "MATCH (person:Person) RETURN person { .name,.age,livesIn:[(person)-[:LIVES_IN]->(livesInLocation:Location) WHERE livesInLocation.name = \$livesInLocationName | livesInLocation { .name }][0] } AS person",
55-
mapOf("livesInLocationName" to "Berlin"))
72+
assertQuery(query, "MATCH (person:Person) RETURN person { .name,.age,livesIn:[(person)-[:LIVES_IN]->(livesIn:Location) WHERE livesIn.name = \$livesInName | livesIn { .name }][0] } AS person",
73+
mapOf("livesInName" to "Berlin"))
5674
}
5775

5876
@Test
5977
fun nestedQueryMulti() {
6078
val query = " { person { name age livedIn { name } } } "
61-
assertQuery(query, "MATCH (person:Person) RETURN person { .name,.age,livedIn:[(person)-[:LIVED_IN]->(livedInLocation:Location) | livedInLocation { .name }] } AS person")
79+
assertQuery(query, "MATCH (person:Person) RETURN person { .name,.age,livedIn:[(person)-[:LIVED_IN]->(livedIn:Location) | livedIn { .name }] } AS person")
6280
}
6381

6482
@Test
6583
fun nestedQuerySliceOffset() {
6684
val query = " { person { livedIn(offset:3) { name } } } "
67-
assertQuery(query, "MATCH (person:Person) RETURN person { livedIn:[(person)-[:LIVED_IN]->(livedInLocation:Location) | livedInLocation { .name }][3..] } AS person")
85+
assertQuery(query, "MATCH (person:Person) RETURN person { livedIn:[(person)-[:LIVED_IN]->(livedIn:Location) | livedIn { .name }][3..] } AS person")
6886
}
6987
@Test
7088
fun nestedQuerySliceFirstOffset() {
7189
val query = " { person { livedIn(first:2,offset:3) { name } } } "
72-
assertQuery(query, "MATCH (person:Person) RETURN person { livedIn:[(person)-[:LIVED_IN]->(livedInLocation:Location) | livedInLocation { .name }][3..5] } AS person")
90+
assertQuery(query, "MATCH (person:Person) RETURN person { livedIn:[(person)-[:LIVED_IN]->(livedIn:Location) | livedIn { .name }][3..5] } AS person")
7391
}
7492

7593
@Test
7694
fun nestedQuerySliceFirst() {
7795
val query = " { person { livedIn(first:2) { name } } } "
78-
assertQuery(query, "MATCH (person:Person) RETURN person { livedIn:[(person)-[:LIVED_IN]->(livedInLocation:Location) | livedInLocation { .name }][0..2] } AS person")
96+
assertQuery(query, "MATCH (person:Person) RETURN person { livedIn:[(person)-[:LIVED_IN]->(livedIn:Location) | livedIn { .name }][0..2] } AS person")
7997
}
8098

8199
@Test

0 commit comments

Comments
 (0)