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] ----