Skip to content

Commit 9ef809b

Browse files
committed
Added named and inline fragment support
1 parent 3ca4066 commit 9ef809b

File tree

3 files changed

+54
-20
lines changed

3 files changed

+54
-20
lines changed

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

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,37 @@ import graphql.schema.idl.SchemaParser
1010
import org.antlr.v4.runtime.misc.ParseCancellationException
1111

1212
class Translator(val schema: GraphQLSchema) {
13-
companion object {
14-
val EMPTY_RESULT = "" to emptyMap<String,Any>()
15-
}
16-
data class Config( val topLevelWhere: Boolean = true)
13+
data class Context(val topLevelWhere: Boolean = true, val fragments : Map<String,FragmentDefinition> = emptyMap())
1714
data class Cypher( val query: String, val params : Map<String,Any?> = emptyMap()) {
1815
companion object {
1916
val EMPTY = Cypher("")
2017
}
2118
fun with(p: Map<String,Any?>) = this.copy(params = this.params + p)
2219
}
2320

24-
fun translate(query: String, params: Map<String, Any> = emptyMap(), config: Config = Config()) : List<Cypher> {
21+
fun translate(query: String, params: Map<String, Any> = emptyMap(), context: Context = Context()) : List<Cypher> {
2522
val ast = parse(query) // todo preparsedDocumentProvider
23+
val ctx = context.copy(fragments = ast.definitions.filterIsInstance<FragmentDefinition>().map { it.name to it }.toMap())
2624
val queries = ast.definitions.filterIsInstance<OperationDefinition>()
2725
.filter { it.operation == OperationDefinition.Operation.QUERY } // todo variabledefinitions, directives, name
2826
.flatMap { it.selectionSet.selections }
2927
.filterIsInstance<Field>() // FragmentSpread, InlineFragment
3028
.map { println(it);it }
31-
.map { toQuery(it, config).with(params) } // arguments, alias, directives, selectionSet
29+
.map { toQuery(it, ctx).with(params) } // arguments, alias, directives, selectionSet
3230
return queries
3331
}
3432

35-
private fun toQuery(queryField: Field, config:Config = Config()): Cypher {
33+
private fun toQuery(queryField: Field, ctx:Context = Context()): Cypher {
3634
val name = queryField.name
3735
val queryType = schema.queryType.fieldDefinitions.filter { it.name == name }.firstOrNull() ?: throw IllegalArgumentException("Unknown Query $name available queries: " + schema.queryType.fieldDefinitions.map { it.name }.joinToString())
3836
val returnType = inner(queryType.type)
3937
// println(returnType)
4038
val type = schema.getType(returnType.name)
4139
val label = type.name.quote()
4240
val variable = queryField.aliasOrName().decapitalize()
43-
val mapProjection = projectFields(variable, queryField, type)
44-
val where = if (config.topLevelWhere) where(variable, queryType, propertyArguments(queryField)) else Cypher.EMPTY
45-
val properties = if (config.topLevelWhere) Cypher.EMPTY else properties(variable, queryType, propertyArguments(queryField))
41+
val mapProjection = projectFields(variable, queryField, type, ctx)
42+
val where = if (ctx.topLevelWhere) where(variable, queryType, propertyArguments(queryField)) else Cypher.EMPTY
43+
val properties = if (ctx.topLevelWhere) Cypher.EMPTY else properties(variable, queryType, propertyArguments(queryField))
4644
val skipLimit = format(skipLimit(queryField.arguments))
4745
val ordering = orderBy(variable, queryField.arguments)
4846
return Cypher("MATCH ($variable:$label${properties.query})${where.query} RETURN ${mapProjection.query} AS $variable$ordering$skipLimit" ,
@@ -86,27 +84,52 @@ class Translator(val schema: GraphQLSchema) {
8684
return predicates + defaults
8785
}
8886

89-
private fun projectFields(variable: String, field: Field, type: GraphQLType): Cypher {
87+
private fun projectFields(variable: String, field: Field, type: GraphQLType, ctx: Context): Cypher {
9088
// todo handle non-object case
9189
val objectType = type as GraphQLObjectType
92-
val properties = field.selectionSet.selections
93-
.filterIsInstance<Field>()
94-
.map { resolveField(variable, it, objectType) }
90+
val properties = field.selectionSet.selections.flatMap {
91+
when (it) {
92+
is Field -> listOf(projectField(variable, it, objectType, ctx))
93+
is InlineFragment -> projectInlineFragment(variable, it, objectType, ctx)
94+
is FragmentSpread -> projectNamedFragments(variable, it, objectType, ctx)
95+
else -> emptyList()
96+
}
97+
}
9598

9699
val projection = properties.map { it.query }.joinToString(",", "{ ", " }")
97-
val params = properties.map{ it.params }.reduce{ res,map -> res + map }
100+
val params = properties.map{ it.params }.fold(emptyMap<String,Any?>()) { res, map -> res + map }
98101
return Cypher("$variable $projection",params)
99102
}
100103

101-
private fun resolveField(variable: String, field: Field, type: GraphQLObjectType) : Cypher {
104+
private fun projectField(variable: String, field: Field, type: GraphQLObjectType, ctx:Context) : Cypher {
102105
val fieldDefinition = type.getFieldDefinition(field.name)
103106
return if (inner(fieldDefinition.type) is GraphQLObjectType) {
104-
val patternComprehensions = projectRelationship(variable, field, fieldDefinition)
107+
val patternComprehensions = projectRelationship(variable, field, fieldDefinition, ctx)
105108
Cypher(field.aliasOrName() + ":" + patternComprehensions.query, patternComprehensions.params)
106109
} else Cypher("." + field.aliasOrName())
107110
}
108111

109-
private fun projectRelationship(variable: String, field: Field, fieldDefinition: GraphQLFieldDefinition): Cypher {
112+
fun projectNamedFragments(variable: String, fragmentSpread: FragmentSpread, type: GraphQLObjectType, ctx: Context) =
113+
ctx.fragments.getValue(fragmentSpread.name).let {
114+
projectFragment(it.typeCondition.name, type, variable, ctx, it.selectionSet)
115+
}
116+
117+
private fun projectFragment(fragmentTypeName: String?, type: GraphQLObjectType, variable: String, ctx: Context, selectionSet: SelectionSet): List<Cypher> {
118+
val fragmentType = schema.getType(fragmentTypeName)!! as GraphQLObjectType
119+
if (fragmentType == type) {
120+
// these are the nested fields of the fragment
121+
// it could be that we have to adapt the variable name too, and perhaps add some kind of rename
122+
return selectionSet.selections.filterIsInstance<Field>().map { projectField(variable, it, fragmentType, ctx) }
123+
} else {
124+
return emptyList()
125+
}
126+
}
127+
128+
fun projectInlineFragment(variable: String, fragment: InlineFragment, type: GraphQLObjectType, ctx: Context) =
129+
projectFragment(fragment.typeCondition.name, type, variable, ctx, fragment.selectionSet)
130+
131+
132+
private fun projectRelationship(variable: String, field: Field, fieldDefinition: GraphQLFieldDefinition, ctx:Context): Cypher {
110133
val fieldType = fieldDefinition.type
111134
val innerType = inner(fieldType).name
112135
val fieldObjectType = schema.getType(innerType)
@@ -118,7 +141,7 @@ class Translator(val schema: GraphQLSchema) {
118141
val childVariable = field.name + fieldObjectType.name
119142
val childPattern = "$childVariable:$innerType"
120143
val where = where(childVariable,fieldDefinition,propertyArguments(field))
121-
val fieldProjection = projectFields(childVariable, field, fieldObjectType)
144+
val fieldProjection = projectFields(childVariable, field, fieldObjectType, ctx)
122145
val comprehension = "[($variable)$inArrow-[:${relType}]-$outArrow($childPattern)${where.query} | ${fieldProjection.query}]"
123146
val skipLimit = skipLimit(field.arguments)
124147
val slice = slice(skipLimit,fieldType.isList())

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class MovieSchemaTest {
1212
val schema = InputStreamReader(javaClass.getResourceAsStream("/movies-test-schema.graphql")).readText()
1313

1414
fun testTranslation(graphQLQuery: String, expectedCypherQuery:String, params: Map<String,Any> = emptyMap()) {
15-
val query = Translator(SchemaBuilder.buildSchema(schema)).translate(graphQLQuery, emptyMap(), config = Translator.Config(topLevelWhere = false)).first()
15+
val query = Translator(SchemaBuilder.buildSchema(schema)).translate(graphQLQuery, emptyMap(), context = Translator.Context(topLevelWhere = false)).first()
1616
assertEquals(expectedCypherQuery.normalizeWhitespace(), query.query.normalizeWhitespace())
1717
}
1818
fun String.normalizeWhitespace() = this.replace("\\s+".toRegex()," ")

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,17 @@ class TranslatorTest {
127127
assertQuery(query, "MATCH (foo:Person) RETURN foo { .n } AS foo")
128128
}
129129

130+
@Test
131+
fun namedFragment() {
132+
val query = " query { person { ...name } } fragment name on Person { name } "
133+
assertQuery(query, "MATCH (person:Person) RETURN person { .name } AS person")
134+
}
135+
@Test
136+
fun inlineFragment() {
137+
val query = " query { person { ... on Person { name } } }"
138+
assertQuery(query, "MATCH (person:Person) RETURN person { .name } AS person")
139+
}
140+
130141
private fun assertQuery(query: String, expected: String, params : Map<String,Any> = emptyMap()) {
131142
val result = Translator(SchemaBuilder.buildSchema(schema)).translate(query).first()
132143
assertEquals(expected, result.query)

0 commit comments

Comments
 (0)