diff --git a/pom.xml b/pom.xml
index 3e3f98e2..dfabba42 100755
--- a/pom.xml
+++ b/pom.xml
@@ -8,7 +8,7 @@
neo4j-graphql-java
Neo4j GraphQL Java
GraphQL to Cypher Mapping
- 1.0.0-M03-SNAPSHOT
+ 1.0.0-M04-SNAPSHOT
http://github.com/neo4j-graphql/neo4j-graphql-java
@@ -22,7 +22,7 @@
UTF-8
1.8
- 1.3.21
+ 1.3.41
${java.version}
3.5.6
1.7.2
diff --git a/readme.adoc b/readme.adoc
index 0649acde..71f90c22 100644
--- a/readme.adoc
+++ b/readme.adoc
@@ -53,7 +53,7 @@ val schema =
# Optional if you use generated queries
type Query {
person : [Person]
- personByName(name:String) : Person
+ personByName(name:ID) : Person
}"""
val query = """ { p:personByName(name:"Joe") { age } } """
@@ -229,12 +229,12 @@ This example doesn't handle introspection queries but the one in the test direct
* auto-generate mutation fields for all objects to create, update, delete
* @cypher directive for top level queries and mutations, supports arguments
* date(time)
+* interfaces
=== Next
* skip limit params
* sorting (nested)
-* interfaces
* input types
* unions
* scalars
diff --git a/src/main/kotlin/org/neo4j/graphql/QueryContext.kt b/src/main/kotlin/org/neo4j/graphql/QueryContext.kt
index 7eaa73b6..6386f0a0 100644
--- a/src/main/kotlin/org/neo4j/graphql/QueryContext.kt
+++ b/src/main/kotlin/org/neo4j/graphql/QueryContext.kt
@@ -1,5 +1,8 @@
package org.neo4j.graphql
data class QueryContext @JvmOverloads constructor(
- val topLevelWhere: Boolean = true
+ /**
+ * if true the __typename
will be always returned for interfaces, no matter if it was queried or not
+ */
+ var queryTypeOfInterfaces: Boolean = false
)
\ No newline at end of file
diff --git a/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt b/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt
index 5bbd7535..24689803 100644
--- a/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt
+++ b/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt
@@ -26,6 +26,18 @@ object SchemaBuilder {
AugmentationProcessor(typeDefinitionRegistry, config, builder).augmentSchema()
+ typeDefinitionRegistry
+ .getTypes(InterfaceTypeDefinition::class.java)
+ .forEach { typeDefinition ->
+ builder.type(typeDefinition.name) {
+ it.typeResolver { env ->
+ (env.getObject() as? Map)
+ ?.let { data -> data[ProjectionBase.TYPE_NAME] as? String }
+ ?.let { typeName -> env.schema.getObjectType(typeName) }
+ }
+ }
+ }
+
val runtimeWiring = builder.build()
// todo add new queries, filters, enums etc.
diff --git a/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt b/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt
index 7073751d..e5895c03 100644
--- a/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt
+++ b/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt
@@ -10,6 +10,8 @@ open class ProjectionBase(val metaProvider: MetaProvider) {
const val FIRST = "first"
const val OFFSET = "offset"
const val FILTER = "filter"
+
+ const val TYPE_NAME = "__typename"
}
fun orderBy(variable: String, args: MutableList): String {
@@ -135,17 +137,39 @@ open class ProjectionBase(val metaProvider: MetaProvider) {
// a{.name},
// CASE WHEN a:Location THEN a { .foo } ELSE {} END
// ])
- return selectionSet.selections.flatMapTo(mutableListOf()) {
+ var hasTypeName = false
+ val projections = selectionSet.selections.flatMapTo(mutableListOf()) {
when (it) {
- is Field -> listOf(projectField(variable, it, nodeType, env, variableSuffix))
+ is Field -> {
+ hasTypeName = hasTypeName || (it.name == TYPE_NAME)
+ listOf(projectField(variable, it, nodeType, env, variableSuffix))
+ }
is InlineFragment -> projectInlineFragment(variable, it, env, variableSuffix)
is FragmentSpread -> projectNamedFragments(variable, it, env, variableSuffix)
else -> emptyList()
}
}
+ if (nodeType is InterfaceDefinitionNodeFacade
+ && !hasTypeName
+ && (env.getLocalContext() as? QueryContext)?.queryTypeOfInterfaces == true
+ ) {
+ // for interfaces the typename is required to determine the correct implementation
+ projections.add(projectField(variable, Field(TYPE_NAME), nodeType, env, variableSuffix))
+ }
+ return projections
}
private fun projectField(variable: String, field: Field, type: NodeFacade, env: DataFetchingEnvironment, variableSuffix: String?): Cypher {
+ if (field.name == TYPE_NAME) {
+ return if (type.isRelationType()) {
+ Cypher("${field.aliasOrName()}: '${type.name()}'")
+ } else {
+ val paramName = paramName(variable, "validTypes", null)
+ val validTypeLabels = metaProvider.getValidTypeLabels(type)
+ Cypher("${field.aliasOrName()}: head( [ label IN labels($variable) WHERE label IN $$paramName ] )",
+ mapOf(paramName to validTypeLabels))
+ }
+ }
val fieldDefinition = type.getFieldDefinition(field.name)
?: throw IllegalStateException("No field ${field.name} in ${type.name()}")
val cypherDirective = fieldDefinition.cypherDirective()
@@ -324,4 +348,4 @@ open class ProjectionBase(val metaProvider: MetaProvider) {
private fun numericArgument(arguments: List, name: String, defaultValue: Number = 0) =
(arguments.find { it.name.toLowerCase() == name }?.value?.toJavaValue() as Number?) ?: defaultValue
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/augmentation-tests.adoc b/src/test/resources/augmentation-tests.adoc
index 4d789ada..cb7e2fe1 100644
--- a/src/test/resources/augmentation-tests.adoc
+++ b/src/test/resources/augmentation-tests.adoc
@@ -6,13 +6,14 @@
[source,graphql,schema=true]
----
-type Person0 { name: String }
-type Person1 { name: String }
-type Person2 { name: String, age: [Int] }
-type Person3 { name: String!}
-type Person4 { id:ID!, name: String}
-type Person5 { id:ID!, movies:[Movie]}
-type Movie { id:ID!, publishedBy: Publisher }
+interface HasMovies { movies:[Movie] }
+type Person0 { name: String, born: _Neo4jTime }
+type Person1 { name: String, born: _Neo4jDate }
+type Person2 { name: String, age: [Int], born: _Neo4jDateTime }
+type Person3 { name: String!, born: _Neo4jLocalTime }
+type Person4 { id:ID!, name: String, born: _Neo4jLocalDateTime }
+type Person5 implements HasMovies { id:ID!, movies:[Movie]}
+type Movie { id:ID!, publishedBy: Publisher}
type Publisher { name:ID! }
----
@@ -44,6 +45,10 @@ schema {
mutation: Mutation
}
+interface HasMovies {
+ movies(first: Int, offset: Int): [Movie]
+}
+
type Movie {
id: ID!
publishedBy: Publisher
@@ -52,13 +57,17 @@ type Movie {
type Mutation {
createMovie(id: ID!): Movie!
deleteMovie(id: ID!): Movie
- createPerson1(name: String): Person1!
- createPerson2(age: [Int], name: String): Person2!
- createPerson3(name: String!): Person3!
- createPerson4(id: ID!, name: String): Person4!
+ createPerson1(born: _Neo4jDateInput, name: String): Person1!
+
+ createPerson2(age: [Int], born: _Neo4jDateTimeInput, name: String): Person2!
+
+ createPerson3(born: _Neo4jLocalTimeInput, name: String!): Person3!
+
+ createPerson4(born: _Neo4jLocalDateTimeInput, id: ID!, name: String): Person4!
deletePerson4(id: ID!): Person4
- mergePerson4(id: ID!, name: String): Person4!
- updatePerson4(id: ID!, name: String): Person4
+ mergePerson4(born: _Neo4jLocalDateTimeInput, id: ID!, name: String): Person4!
+ updatePerson4(born: _Neo4jLocalDateTimeInput, id: ID!, name: String): Person4
+
createPerson5(id: ID!): Person5!
deletePerson5(id: ID!): Person5
createPublisher(name: ID!): Publisher!
@@ -66,28 +75,33 @@ type Mutation {
}
type Person0 {
+ born: _Neo4jTime
name: String
}
type Person1 {
+ born: _Neo4jDate
name: String
}
type Person2 {
age: [Int]
+ born: _Neo4jDateTime
name: String
}
type Person3 {
+ born: _Neo4jLocalTime
name: String!
}
type Person4 {
+ born: _Neo4jLocalDateTime
id: ID!
name: String
}
-type Person5 {
+type Person5 implements HasMovies {
id: ID!
movies(first: Int, offset: Int): [Movie]
}
@@ -215,6 +229,9 @@ input _Neo4jTimeInput {
timezone: String
}
+directive @relation(name:String, direction: RelationDirection = OUT, from: String = "from", to: String = "to") on FIELD_DEFINITION | OBJECT
+directive @cypher(statement:String) on FIELD_DEFINITION
+directive @property(name:String) on FIELD_DEFINITION
----
=== Disable Mutations
@@ -243,6 +260,10 @@ schema {
mutation: Mutation
}
+interface HasMovies {
+ movies(first: Int, offset: Int):[Movie]
+}
+
type Movie {
id: ID!
publishedBy: Publisher
@@ -252,28 +273,33 @@ type Mutation {
}
type Person0 {
+ born: _Neo4jTime
name: String
}
type Person1 {
+ born: _Neo4jDate
name: String
}
type Person2 {
age: [Int]
+ born: _Neo4jDateTime
name: String
}
type Person3 {
+ born: _Neo4jLocalTime
name: String!
}
type Person4 {
+ born: _Neo4jLocalDateTime
id: ID!
name: String
}
-type Person5 {
+type Person5 implements HasMovies {
id: ID!
movies(first: Int, offset: Int): [Movie]
}
@@ -284,10 +310,10 @@ type Publisher {
type Query {
movie(filter: _MovieFilter, first: Int, id: ID, offset: Int, orderBy: _MovieOrdering): [Movie!]!
- person1(filter: _Person1Filter, first: Int, name: String, offset: Int, orderBy: _Person1Ordering): [Person1!]!
- person2(age: [Int], filter: _Person2Filter, first: Int, name: String, offset: Int, orderBy: _Person2Ordering): [Person2!]!
- person3(filter: _Person3Filter, first: Int, name: String, offset: Int, orderBy: _Person3Ordering): [Person3!]!
- person4(filter: _Person4Filter, first: Int, id: ID, name: String, offset: Int, orderBy: _Person4Ordering): [Person4!]!
+ person1(born: _Neo4jDateInput, filter: _Person1Filter, first: Int, name: String, offset: Int, orderBy: _Person1Ordering): [Person1!]!
+ person2(age: [Int], born: _Neo4jDateTimeInput, filter: _Person2Filter, first: Int, name: String, offset: Int, orderBy: _Person2Ordering): [Person2!]!
+ person3(born: _Neo4jLocalTimeInput, filter: _Person3Filter, first: Int, name: String, offset: Int, orderBy: _Person3Ordering): [Person3!]!
+ person4(born: _Neo4jLocalDateTimeInput, filter: _Person4Filter, first: Int, id: ID, name: String, offset: Int, orderBy: _Person4Ordering): [Person4!]!
person5(filter: _Person5Filter, first: Int, id: ID, offset: Int, orderBy: _Person5Ordering): [Person5!]!
publisher(filter: _PublisherFilter, first: Int, name: ID, offset: Int, orderBy: _PublisherOrdering): [Publisher!]!
}
@@ -347,18 +373,14 @@ type _Neo4jTime {
timezone: String
}
-enum RelationDirection {
- BOTH
- IN
- OUT
-}
-
enum _MovieOrdering {
id_asc
id_desc
}
enum _Person1Ordering {
+ born_asc
+ born_desc
name_asc
name_desc
}
@@ -366,16 +388,22 @@ enum _Person1Ordering {
enum _Person2Ordering {
age_asc
age_desc
+ born_asc
+ born_desc
name_asc
name_desc
}
enum _Person3Ordering {
+ born_asc
+ born_desc
name_asc
name_desc
}
enum _Person4Ordering {
+ born_asc
+ born_desc
id_asc
id_desc
name_asc
@@ -480,6 +508,10 @@ input _Person1Filter {
AND: [_Person1Filter!]
NOT: [_Person1Filter!]
OR: [_Person1Filter!]
+ born: _Neo4jDateInput
+ born_in: [_Neo4jDateInput]
+ born_not: _Neo4jDateInput
+ born_not_in: [_Neo4jDateInput]
name: String
name_contains: String
name_ends_with: String
@@ -497,6 +529,7 @@ input _Person1Filter {
}
input _Person1Input {
+ born: _Neo4jDateInput
name: String
}
@@ -512,6 +545,10 @@ input _Person2Filter {
age_lte: Int
age_not: Int
age_not_in: [Int]
+ born: _Neo4jDateTimeInput
+ born_in: [_Neo4jDateTimeInput]
+ born_not: _Neo4jDateTimeInput
+ born_not_in: [_Neo4jDateTimeInput]
name: String
name_contains: String
name_ends_with: String
@@ -530,6 +567,7 @@ input _Person2Filter {
input _Person2Input {
age: [Int]
+ born: _Neo4jDateTimeInput
name: String
}
@@ -537,6 +575,10 @@ input _Person3Filter {
AND: [_Person3Filter!]
NOT: [_Person3Filter!]
OR: [_Person3Filter!]
+ born: _Neo4jLocalTimeInput
+ born_in: [_Neo4jLocalTimeInput]
+ born_not: _Neo4jLocalTimeInput
+ born_not_in: [_Neo4jLocalTimeInput]
name: String
name_contains: String
name_ends_with: String
@@ -554,6 +596,7 @@ input _Person3Filter {
}
input _Person3Input {
+ born: _Neo4jLocalTimeInput
name: String
}
@@ -561,6 +604,10 @@ input _Person4Filter {
AND: [_Person4Filter!]
NOT: [_Person4Filter!]
OR: [_Person4Filter!]
+ born: _Neo4jLocalDateTimeInput
+ born_in: [_Neo4jLocalDateTimeInput]
+ born_not: _Neo4jLocalDateTimeInput
+ born_not_in: [_Neo4jLocalDateTimeInput]
id: ID
id_contains: ID
id_ends_with: ID
@@ -592,6 +639,7 @@ input _Person4Filter {
}
input _Person4Input {
+ born: _Neo4jLocalDateTimeInput
id: ID
name: String
}
@@ -648,6 +696,16 @@ input _PublisherFilter {
input _PublisherInput {
name: ID
}
+
+enum RelationDirection {
+ IN
+ OUT
+ BOTH
+}
+
+directive @relation(name:String, direction: RelationDirection = OUT, from: String = "from", to: String = "to") on FIELD_DEFINITION | OBJECT
+directive @cypher(statement:String) on FIELD_DEFINITION
+directive @property(name:String) on FIELD_DEFINITION
----
@@ -681,13 +739,17 @@ schema {
mutation: Mutation
}
+interface HasMovies {
+ movies(first: Int, offset: Int): [Movie]
+}
+
type Movie {
id: ID!
publishedBy: Publisher
}
type Mutation {
- createMovie(id: ID!): Movie!
+ createMovie(id: ID!): Movie!
deleteMovie(id: ID!): Movie
createPerson5(id: ID!): Person5!
deletePerson5(id: ID!): Person5
@@ -696,28 +758,33 @@ type Mutation {
}
type Person0 {
+ born: _Neo4jTime
name: String
}
type Person1 {
+ born: _Neo4jDate
name: String
}
type Person2 {
age: [Int]
+ born: _Neo4jDateTime
name: String
}
type Person3 {
+ born: _Neo4jLocalTime
name: String!
}
type Person4 {
+ born: _Neo4jLocalDateTime
id: ID!
name: String
}
-type Person5 {
+type Person5 implements HasMovies {
id: ID!
movies(first: Int, offset: Int): [Movie]
}
@@ -784,12 +851,6 @@ type _Neo4jTime {
timezone: String
}
-enum RelationDirection {
- BOTH
- IN
- OUT
-}
-
input _Neo4jDateInput {
day: Int
formatted: String
@@ -844,4 +905,13 @@ input _Neo4jTimeInput {
second: Int
timezone: String
}
+
+enum RelationDirection {
+ IN
+ OUT
+ BOTH
+}
+directive @relation(name:String, direction: RelationDirection = OUT, from: String = "from", to: String = "to") on FIELD_DEFINITION | OBJECT
+directive @cypher(statement:String) on FIELD_DEFINITION
+directive @property(name:String) on FIELD_DEFINITION
----
\ No newline at end of file
diff --git a/src/test/resources/movie-tests.adoc b/src/test/resources/movie-tests.adoc
index 84f1e15b..3d51689d 100644
--- a/src/test/resources/movie-tests.adoc
+++ b/src/test/resources/movie-tests.adoc
@@ -43,18 +43,18 @@ type Genre {
type State {
name: String
}
-#interface Person {
-# userId: ID!
-# name: String
-#}
-type Actor # implements Person
+interface Person {
+ userId: ID!
+ name: String
+}
+type Actor implements Person
{
userId: ID!
name: String
movies: [Movie] @relation(name: "ACTED_IN", direction:OUT)
born: _Neo4jDateTime
}
-type User # implements Person
+type User implements Person
{
userId: ID!
name: String
@@ -100,6 +100,7 @@ type Query {
}
type Mutation {
createGenre(name:String): Genre @cypher(statement:"CREATE (g:Genre) SET g.name = name RETURN g")
+ changePerson(name: String): Person
}
# scalar DateTime
----
@@ -371,8 +372,8 @@ RETURN movie {
[source,json]
----
{
- "movieFirst":3,
- "movieOffset":0
+ "movieFirst": 3,
+ "movieOffset": 0
}
----
@@ -412,7 +413,7 @@ RETURN movie {
[source,json]
----
{
- "movieFirst":3,
+ "movieFirst": 3,
"movieOffset":0
}
----
@@ -675,7 +676,7 @@ mutation {
{
"fromMovieId": 1,
"toGenres": [
- "Action",
+ "Action",
"Fantasy"
]
}
@@ -773,7 +774,7 @@ mutation {
{
"fromUserId": 1,
"toKnows": [
- 10,
+ 10,
23
]
}
@@ -819,8 +820,42 @@ ORDER BY movie.title DESC
LIMIT 10
----
-== Neo4j Data Types queryies
+=== create object with multiple labels
+.GraphQL-Query
+[source,graphql]
+----
+mutation {
+ createUser(userId:1){
+ userId,
+ __typename
+ }
+}
+----
+
+.Cypher Params
+[source,json]
+----
+{
+ "createUserUserId": 1,
+ "createUserValidTypes": [
+ "User"
+ ]
+}
+----
+
+.Cypher
+[source,cypher]
+----
+CREATE (createUser:User:Person { userId: $createUserUserId })
+WITH createUser
+RETURN createUser {
+ .userId,
+ __typename: head( [ label in labels(createUser) WHERE label in $createUserValidTypes ] )
+} AS createUser
+----
+
+== Neo4j Data Types queryies
=== User born extraction
@@ -927,13 +962,23 @@ mutation {
.Cypher Params
[source,json]
----
-{"actorUserId": "1", "actorName": "Andrea", "actorBorn": { "formatted": "2015-06-24T12:50:35.556000000+01:00" }}
+{
+ "actorUserId": "1",
+ "actorName": "Andrea",
+ "actorBorn": {
+ "formatted": "2015-06-24T12:50:35.556000000+01:00"
+ }
+}
----
.Cypher
[source,cypher]
----
-CREATE (actor:Actor {userId: $actorUserId, name: $actorName, born: datetime($actorBorn.formatted)})
+CREATE (actor:Actor:Person {
+ userId: $actorUserId,
+ name: $actorName,
+ born: datetime($actorBorn.formatted)
+})
WITH actor
RETURN actor { .name } AS actor
----
@@ -1029,7 +1074,11 @@ mutation {
.Cypher
[source,cypher]
----
-CREATE (actor:Actor {userId: $actorUserId, name: $actorName, born: datetime($actorBorn)})
+CREATE (actor:Actor:Person {
+ userId: $actorUserId,
+ name: $actorName,
+ born: datetime($actorBorn)
+})
WITH actor
RETURN actor { .name,born: { year: actor.born.year, month: actor.born.month } } AS actor
----
\ No newline at end of file
diff --git a/src/test/resources/translator-tests1.adoc b/src/test/resources/translator-tests1.adoc
index aaa99f17..4d416412 100644
--- a/src/test/resources/translator-tests1.adoc
+++ b/src/test/resources/translator-tests1.adoc
@@ -13,19 +13,32 @@ type Person {
born : Birth
died : Death
}
-type Birth @relation(name:"BORN") {
+interface Temporal {
+ date: String
+}
+type Birth implements Temporal @relation(name:"BORN") {
from: Person
to: Location
date: String
}
-type Death @relation(name:"DIED",from:"who",to:"where") {
+type Death implements Temporal @relation(name:"DIED",from:"who",to:"where") {
who: Person
where: Location
date: String
}
-type Location {
+interface Location {
+ name: String
+ founded: Person @relation(name:"FOUNDED", direction: IN)
+}
+type City implements Location {
+ name: String
+ founded: Person @relation(name:"FOUNDED", direction: IN)
+ cityArg: String
+}
+type Village implements Location {
name: String
founded: Person @relation(name:"FOUNDED", direction: IN)
+ villageArg: String
}
# enum _PersonOrdering { name_asc, name_desc, age_asc, age_desc }
enum E { pi, e }
@@ -619,13 +632,17 @@ RETURN person {
.GraphQL-Query
[source,graphql]
----
-{ location { name } }
+{ location { name __typename } }
----
.Cypher params
[source,json]
----
{
+ "locationValidTypes": [
+ "City",
+ "Village"
+ ]
}
----
@@ -634,14 +651,13 @@ RETURN person {
----
MATCH (location:Location)
RETURN location {
- .name
+ .name,
+ __typename: head( [ label in labels(location) WHERE label in $locationValidTypes ] )
} AS location
----
=== introspection
-CAUTION: Not yet implemented
-
.GraphQL-Query
[source,graphql]
----
@@ -679,8 +695,6 @@ RETURN person {
=== inline fragments on interfaces
-CAUTION: Not yet implemented
-
.GraphQL-Query
[source,graphql]
----
@@ -720,8 +734,6 @@ RETURN location {
=== fragments on interfaces
-CAUTION: Not yet implemented
-
.GraphQL-Query
[source,graphql]
----