Skip to content

Commit 8b112fc

Browse files
committed
Added configurable schema augmentation for each type
- mutation: create, delete, update - query - ordering enum - input type - filter input type Added GraphQLServer.kt example in tests TODO: docs
1 parent 6355fd2 commit 8b112fc

File tree

11 files changed

+443
-25
lines changed

11 files changed

+443
-25
lines changed

pom.xml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<kotlin.version>1.3.11</kotlin.version>
2626
<kotlin.compiler.jvmTarget>${java.version}</kotlin.compiler.jvmTarget>
2727
<neo4j.version>3.4.1</neo4j.version>
28-
<driver.version>1.6.2</driver.version>
28+
<driver.version>1.7.2</driver.version>
2929
</properties>
3030

3131
<organization>
@@ -55,6 +55,18 @@
5555
<version>${driver.version}</version>
5656
<scope>test</scope>
5757
</dependency>
58+
<dependency>
59+
<groupId>com.sparkjava</groupId>
60+
<artifactId>spark-core</artifactId>
61+
<version>2.7.2</version>
62+
<scope>test</scope>
63+
</dependency>
64+
<dependency>
65+
<groupId>com.google.code.gson</groupId>
66+
<artifactId>gson</artifactId>
67+
<version>2.8.5</version>
68+
<scope>test</scope>
69+
</dependency>
5870
<dependency>
5971
<groupId>org.neo4j.test</groupId>
6072
<artifactId>neo4j-harness</artifactId>

readme.adoc

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,39 @@ And can run queries like:
154154

155155
image::docs/graphiql.jpg[]
156156

157+
You can also test it with `curl`
158+
159+
----
160+
curl -XPOST http://localhost:4567/graphql -d'{"query":"{person {name}}"}'
161+
----
162+
163+
== Advanced Queries
164+
165+
----
166+
{
167+
person(filter: {name_starts_with: "L"}, orderBy: "born_asc", first: 5, offset: 2) {
168+
name
169+
born
170+
actedIn(first: 1) {
171+
title
172+
}
173+
}
174+
}
175+
----
176+
177+
----
178+
{
179+
person(filter: {name_starts_with: "J", born_gte: 1970}, first:2) {
180+
name
181+
born
182+
actedIn(first:1) {
183+
title
184+
released
185+
}
186+
}
187+
}
188+
----
189+
157190
== Features
158191

159192
=== Current
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.neo4j.graphql
2+
3+
import graphql.language.FieldDefinition
4+
import graphql.language.ObjectTypeDefinition
5+
6+
data class Augmentation(val create: String = "", val update: String = "", val delete: String = "",
7+
val inputType: String = "", val ordering: String = "", val filterType: String = "", val query: String = "")
8+
9+
fun augmentedSchema(ctx: Translator.Context, type: ObjectTypeDefinition): Augmentation {
10+
val typeName = type.name
11+
val idField = type.fieldDefinitions.find { it.type.name() == "ID" }
12+
val scalarFields = type.fieldDefinitions.filter { it.type.isScalar() }.sortedByDescending { it == idField }
13+
val idFieldArg = idField?.let { it.name + ":" + it.type.render() }
14+
15+
val result = if (ctx.mutation.enabled && !ctx.mutation.exclude.contains(typeName) && scalarFields.isNotEmpty()) {
16+
val fieldArgs = scalarFields.map { it.name + ":" + it.type.render() }.joinToString(", ")
17+
Augmentation().copy(create = """create$typeName($fieldArgs) : $typeName """)
18+
.let { aug ->
19+
if (idField != null) aug.copy(
20+
delete = """delete$typeName($idFieldArg) : Boolean """,
21+
update = """update$typeName($fieldArgs) : $typeName """)
22+
else aug
23+
}
24+
} else Augmentation()
25+
26+
return if (ctx.query.enabled && !ctx.query.exclude.contains(typeName) && scalarFields.isNotEmpty()) {
27+
val fieldArgs = scalarFields.map { it.name + ":" + it.type.render(false) }.joinToString(", ")
28+
result.copy(inputType = """input _${typeName}Input { $fieldArgs } """,
29+
ordering = """enum _${typeName}Ordering { ${scalarFields.map { it.name + "_asc ," + it.name + "_desc" }.joinToString(",")} } """,
30+
filterType = filterType(typeName, scalarFields), // TODO
31+
query = """${typeName.decapitalize()}(${fieldArgs} , _id: Int, filter:_${typeName}Filter, orderBy:_${typeName}Ordering, first:Int, offset:Int) : [$typeName] """)
32+
} else result
33+
}
34+
35+
private fun filterType(name: String?, fieldArgs: List<FieldDefinition>) : String {
36+
val fName = """_${name}Filter"""
37+
val fields = (listOf("AND","OR","NOT").map { "$it:[$fName!]" } +
38+
fieldArgs.flatMap { field -> Operators.forType(field.type).map { op -> op.fieldName(field.name) + ":" + field.type.render(false) } }).joinToString(", ")
39+
return """input $fName { $fields } """
40+
}
41+
42+

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

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

33
import graphql.language.*
4-
import graphql.schema.GraphQLList
5-
import graphql.schema.GraphQLNonNull
6-
import graphql.schema.GraphQLSchema
7-
import graphql.schema.GraphQLType
4+
import graphql.schema.*
85

96
fun GraphQLType.inner() : GraphQLType = when(this) {
107
is GraphQLList -> this.wrappedType.inner()
118
is GraphQLNonNull -> this.wrappedType.inner()
129
else -> this
1310
}
11+
val SCALAR_TYPES = listOf("String","ID","Boolean","Int","Float")
12+
13+
fun Type.isScalar() = this.inner().name()?.let { SCALAR_TYPES.contains(it) } ?: false
14+
fun Type.name() : String? = if (this.inner() is TypeName) (this.inner() as TypeName).name else null
15+
16+
fun Type.inner() : Type = when(this) {
17+
is ListType -> this.type.inner()
18+
is NonNullType -> this.type.inner()
19+
else -> this
20+
}
21+
fun Type.render(nonNull:Boolean = true) : String = when(this) {
22+
is ListType -> "[${this.type.render()}]"
23+
is NonNullType -> this.type.render()+ (if (nonNull) "!" else "")
24+
is TypeName -> this.name
25+
else -> throw IllegalStateException("Can't render $this")
26+
}
1427

1528
fun GraphQLType.isList() = this is GraphQLList || (this is GraphQLNonNull && this.wrappedType is GraphQLList)
29+
fun GraphQLType.isScalar() = this.inner().let { it is GraphQLScalarType || it.name.startsWith("_Neo4j") }
30+
fun GraphQLType.isRelationship() = this.inner().let { it is GraphQLObjectType } // && relationship directive
31+
32+
fun GraphQLObjectType.hasRelationship(name:String) = this.getFieldDefinition(name)?.isRelationship() ?: false
33+
fun GraphQLFieldDefinition.isRelationship() = this.type.isRelationship()
1634

1735
fun Field.aliasOrName() = (this.alias ?: this.name).quote()
1836

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.neo4j.graphql
22

33
import graphql.Scalars
44
import graphql.language.Directive
5+
import graphql.language.Type
56
import graphql.schema.*
67
import org.neo4j.graphql.Predicate.Companion.resolvePredicate
78

@@ -47,8 +48,6 @@ fun toExpression(name: String, value: Any?, type: GraphQLObjectType): Predicate
4748
resolvePredicate(name, value, type)
4849
}
4950

50-
fun GraphQLObjectType.hasRelationship(name:String) = this.getFieldDefinition(name)?.let { it.type is GraphQLObjectType } ?: false
51-
5251
fun GraphQLObjectType.relationshipFor(name:String, schema: GraphQLSchema) : RelationshipInfo {
5352
val field = this.getFieldDefinition(name)
5453
val fieldObjectType = schema.getType(field.type.inner().name) as GraphQLObjectType
@@ -196,6 +195,14 @@ enum class Operators(val suffix:String, val op:String, val not :Boolean = false)
196195
else listOf(EQ, NEQ, IN, NIN,LT,LTE,GT,GTE) +
197196
if (type == Scalars.GraphQLString || type == Scalars.GraphQLID) listOf(C,NC, SW, NSW,EW,NEW) else emptyList()
198197

198+
fun forType(type: Type) : List<Operators> =
199+
if (type.name() == "Boolean") listOf(EQ, NEQ)
200+
// todo list types
201+
// todo proper enum + object types and reference types
202+
else if (!type.isScalar()) listOf(EQ, NEQ, IN, NIN)
203+
else listOf(EQ, NEQ, IN, NIN,LT,LTE,GT,GTE) +
204+
if (type.name() == "String" || type.name() == "ID") listOf(C,NC, SW, NSW,EW,NEW) else emptyList()
205+
199206
}
200207

201208
fun fieldName(fieldName: String) = if (this == EQ) fieldName else fieldName + "_" + suffix

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

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@ import graphql.schema.*
88
import graphql.schema.idl.RuntimeWiring
99
import graphql.schema.idl.SchemaGenerator
1010
import graphql.schema.idl.SchemaParser
11+
import graphql.schema.idl.TypeDefinitionRegistry
1112
import org.antlr.v4.runtime.misc.ParseCancellationException
13+
import java.lang.RuntimeException
1214
import java.util.*
1315

1416
class Translator(val schema: GraphQLSchema) {
15-
data class Context @JvmOverloads constructor(val topLevelWhere: Boolean = true, val fragments : Map<String,FragmentDefinition> = emptyMap(),val temporal : Boolean = false)
17+
data class Context @JvmOverloads constructor(val topLevelWhere: Boolean = true,
18+
val fragments : Map<String,FragmentDefinition> = emptyMap(),
19+
val temporal : Boolean = false,
20+
val query: CRUDConfig = CRUDConfig(),
21+
val mutation: CRUDConfig = CRUDConfig())
22+
data class CRUDConfig(val enabled:Boolean = true, val exclude: List<String> = emptyList())
1623
data class Cypher( val query: String, val params : Map<String,Any?> = emptyMap()) {
1724
companion object {
1825
val EMPTY = Cypher("")
@@ -239,22 +246,68 @@ class Translator(val schema: GraphQLSchema) {
239246

240247

241248
object SchemaBuilder {
242-
@JvmStatic fun buildSchema(sdl: String) : GraphQLSchema {
249+
@JvmStatic fun buildSchema(sdl: String, ctx: Translator.Context = Translator.Context() ) : GraphQLSchema {
243250
val schemaParser = SchemaParser()
244-
val typeDefinitionRegistry = schemaParser.parse(sdl)
251+
val typeDefinitionRegistry = augmentSchema(schemaParser.parse(sdl), schemaParser, ctx)
245252

246253
val runtimeWiring = RuntimeWiring.newRuntimeWiring()
247254
.type("Query")
248255
{ it.dataFetcher("hello") { env -> "Hello ${env.getArgument<Any>("what")}!" } }
249256
.build()
250257

251258
val schemaGenerator = SchemaGenerator()
259+
return schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring)
260+
.transform { bc -> bc.additionalDirectives(additionalDirectives()).build() }
261+
.transform { sc -> sc.build() } // todo add new queries, filters, enums etc.
262+
}
263+
264+
private fun additionalDirectives(): Set<GraphQLDirective> {
252265
val directives = setOf(GraphQLDirective("relation", "relation directive",
253-
EnumSet.of(Introspection.DirectiveLocation.FIELD,Introspection.DirectiveLocation.OBJECT),
254-
listOf(GraphQLArgument("name",Scalars.GraphQLString),
255-
GraphQLArgument("direction","relationship direction",Scalars.GraphQLString,"OUT"),
256-
GraphQLArgument("from","from field name",Scalars.GraphQLString,"from"),
257-
GraphQLArgument("to","to field name",Scalars.GraphQLString,"to")),false,false,true))
258-
return schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring).transform { bc -> bc.additionalDirectives(directives).build() }
266+
EnumSet.of(Introspection.DirectiveLocation.FIELD, Introspection.DirectiveLocation.OBJECT),
267+
listOf(GraphQLArgument("name", Scalars.GraphQLString),
268+
GraphQLArgument("direction", "relationship direction", Scalars.GraphQLString, "OUT"),
269+
GraphQLArgument("from", "from field name", Scalars.GraphQLString, "from"),
270+
GraphQLArgument("to", "to field name", Scalars.GraphQLString, "to")), false, false, true))
271+
return directives
272+
}
273+
274+
private fun augmentSchema(typeDefinitionRegistry: TypeDefinitionRegistry, schemaParser: SchemaParser, ctx: Translator.Context): TypeDefinitionRegistry {
275+
276+
val augmentations = typeDefinitionRegistry.types().values.filterIsInstance<ObjectTypeDefinition>().map { augmentedSchema(ctx, it) }
277+
278+
val augmentedTypesSdl = augmentations.flatMap { listOf(it.filterType, it.ordering, it.inputType).filter { it.isNotBlank() } }.joinToString("\n")
279+
typeDefinitionRegistry.merge(schemaParser.parse(augmentedTypesSdl))
280+
281+
val schemaDefinition = typeDefinitionRegistry.schemaDefinition().orElseGet { SchemaDefinition() }
282+
if (!typeDefinitionRegistry.schemaDefinition().isPresent) typeDefinitionRegistry.add(schemaDefinition).ifPresent { throw RuntimeException(it.toSpecification().toString()) }
283+
284+
val operations = schemaDefinition.operationTypeDefinitions.associate { it.name to typeDefinitionRegistry.getType(it.type).get() as ObjectTypeDefinition }.toMap()
285+
286+
val queryDefinition = operations.getOrElse("query") {
287+
ObjectTypeDefinition("Query").also {
288+
schemaDefinition.operationTypeDefinitions.add(OperationTypeDefinition("query",TypeName(it.name)))
289+
typeDefinitionRegistry.add(it)
290+
}
291+
}
292+
val mutationDefinition = operations.getOrElse("mutation") {
293+
ObjectTypeDefinition("Mutation").also { schemaDefinition.operationTypeDefinitions.add(OperationTypeDefinition("mutation",TypeName(it.name)))
294+
typeDefinitionRegistry.add(it)
295+
}
296+
}
297+
298+
augmentations.filter { it.query.isNotBlank() }.map { it.query }.let {
299+
if (!it.isEmpty()) {
300+
val newQueries = schemaParser.parse("type AugmentedQuery { ${it.joinToString("\n")} }").getType("AugmentedQuery").get() as ObjectTypeDefinition
301+
queryDefinition.fieldDefinitions.addAll(newQueries.fieldDefinitions)
302+
}
303+
}
304+
augmentations.flatMap { listOf(it.create,it.update,it.delete).filter{ it.isNotBlank() }}.let {
305+
if (!it.isEmpty()) {
306+
val newQueries = schemaParser.parse("type AugmentedMutation { ${it.joinToString("\n")} }").getType("AugmentedMutation").get() as ObjectTypeDefinition
307+
mutationDefinition.fieldDefinitions.addAll(newQueries.fieldDefinitions)
308+
}
309+
}
310+
311+
return typeDefinitionRegistry
259312
}
260313
}

src/test/kotlin/GraphQLServer.kt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package demo
2+
3+
// Simplistic GraphQL Server using SparkJava
4+
5+
import com.google.gson.Gson
6+
import org.neo4j.driver.v1.AuthTokens
7+
import org.neo4j.driver.v1.GraphDatabase
8+
import org.neo4j.driver.v1.Values
9+
import org.neo4j.graphql.SchemaBuilder
10+
import org.neo4j.graphql.Translator
11+
import spark.Request
12+
import spark.Response
13+
import spark.Spark
14+
import graphql.*
15+
import java.math.BigDecimal
16+
import java.math.BigInteger
17+
18+
val schema = """
19+
type Person {
20+
name: String
21+
born: Int
22+
actedIn: [Movie] @relation(name:"ACTED_IN")
23+
}
24+
type Movie {
25+
title: String
26+
released: Int
27+
tagline: String
28+
}
29+
"""
30+
31+
fun main(args: Array<String>) {
32+
val gson = Gson()
33+
fun render(value:Any) = gson.toJson(value)
34+
fun query(value:String) = (gson.fromJson(value, Map::class.java)["query"] as String).also { println(it) }
35+
36+
val graphQLSchema = SchemaBuilder.buildSchema(schema)
37+
println(graphQLSchema)
38+
val build = GraphQL.newGraphQL(graphQLSchema).build()
39+
val graphql = Translator(graphQLSchema)
40+
fun translate(query:String) = graphql.translate(query)
41+
42+
val driver = GraphDatabase.driver("bolt://localhost",AuthTokens.basic("neo4j","password"))
43+
fun run(cypher:Translator.Cypher) = driver.session().use {
44+
println(cypher.query)
45+
println(cypher.params)
46+
try {
47+
// todo fix parameter mapping in translator
48+
val result = it.run(cypher.query, Values.value(cypher.params.mapValues {
49+
it.value.let {
50+
when (it) {
51+
is BigInteger -> it.longValueExact()
52+
is BigDecimal -> it.toDouble()
53+
else -> it
54+
}
55+
}
56+
}))
57+
// result.list{ it.asMap().toList() }.flatten().groupBy({ it.first },{it.second})
58+
result.keys().map { key -> key to result.list().map { it.get(key).asObject() } }.toMap(LinkedHashMap())
59+
} catch(e:Exception) {
60+
e.printStackTrace()
61+
}
62+
}
63+
64+
65+
fun handler(req: Request, res: Response) = query(req.body()).let { query ->
66+
if (query.contains("__schema"))
67+
build.execute(query).let { println(it.errors);it.getData<Any>() }
68+
else run(translate(query).first()) }
69+
70+
Spark.post("/graphql","application/json", ::handler, ::render)
71+
}
72+

0 commit comments

Comments
 (0)