From dda2c81bc4f2669420817b9fc691d1bf19fdf113 Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Tue, 27 Aug 2019 15:42:28 +0200 Subject: [PATCH 1/7] provide an interceptor to integrate with spring-boot-graphql --- readme.adoc | 4 ++ src/main/kotlin/org/neo4j/graphql/Cypher.kt | 2 +- .../neo4j/graphql/DataFetchingInterceptor.kt | 18 +++++ .../kotlin/org/neo4j/graphql/SchemaBuilder.kt | 20 ++++-- src/test/kotlin/DataFetcherInterceptorDemo.kt | 67 +++++++++++++++++++ 5 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/org/neo4j/graphql/DataFetchingInterceptor.kt create mode 100644 src/test/kotlin/DataFetcherInterceptorDemo.kt diff --git a/readme.adoc b/readme.adoc index 654a38e6..edca094f 100644 --- a/readme.adoc +++ b/readme.adoc @@ -82,6 +82,10 @@ It is running against a Neo4j instance at `bolt://localhost` (username: `neo4j`, (You can also use a link:src/test/kotlin/GraphQLServer.kt[Kotlin based server example].) +In case you wand to bind the neo4j driver directly to the graphql schema you can +link:src/test/kotlin/DataFetcherInterceptorDemo.kt[use the DataFetchingInterceptor to +intercept the cypher queries]. + [source,groovy] ---- // Simplistic GraphQL Server using SparkJava diff --git a/src/main/kotlin/org/neo4j/graphql/Cypher.kt b/src/main/kotlin/org/neo4j/graphql/Cypher.kt index b5f5c85e..468f7f71 100644 --- a/src/main/kotlin/org/neo4j/graphql/Cypher.kt +++ b/src/main/kotlin/org/neo4j/graphql/Cypher.kt @@ -2,7 +2,7 @@ package org.neo4j.graphql import graphql.language.Type -data class Cypher(val query: String, val params: Map = emptyMap(), var type: Type<*>? = null) { +data class Cypher @JvmOverloads constructor(val query: String, val params: Map = emptyMap(), var type: Type<*>? = null) { fun with(p: Map) = this.copy(params = this.params + p) fun escapedQuery() = query.replace("\"", "\\\"").replace("'", "\\'") diff --git a/src/main/kotlin/org/neo4j/graphql/DataFetchingInterceptor.kt b/src/main/kotlin/org/neo4j/graphql/DataFetchingInterceptor.kt new file mode 100644 index 00000000..8f3aa24a --- /dev/null +++ b/src/main/kotlin/org/neo4j/graphql/DataFetchingInterceptor.kt @@ -0,0 +1,18 @@ +package org.neo4j.graphql + +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment + +/** + * Interceptor to hook in database driver binding + */ +interface DataFetchingInterceptor { + + /** + * Called by the Graphql runtime for each method augmented by this library. The custom code should call the delegate + * to get a cypher query. This query can then be forwarded to the neo4j driver to retrieve the data. + * The method then returns the fully parsed result. + */ + @Throws(Exception::class) + fun fetchData(env: DataFetchingEnvironment, delegate: DataFetcher): Any? +} \ 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 7091b1a5..354a3b0b 100644 --- a/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt +++ b/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt @@ -16,16 +16,22 @@ import org.neo4j.graphql.handler.relation.DeleteRelationHandler object SchemaBuilder { + /** + * @param sdl the schema to augment + * @param config defines how the schema should get augmented + * @param dataFetchingInterceptor since this library registers dataFetcher for its augmented methods, these data + * fetchers may be called by other resolver. This interceptor will let you convert a cypher query into real data. + */ @JvmStatic @JvmOverloads - fun buildSchema(sdl: String, config: SchemaConfig = SchemaConfig()): GraphQLSchema { + fun buildSchema(sdl: String, config: SchemaConfig = SchemaConfig(), dataFetchingInterceptor: DataFetchingInterceptor? = null): GraphQLSchema { val schemaParser = SchemaParser() val typeDefinitionRegistry = schemaParser.parse(sdl) val builder = RuntimeWiring.newRuntimeWiring() .scalar(DynamicProperties.INSTANCE) - AugmentationProcessor(typeDefinitionRegistry, config, builder).augmentSchema() + AugmentationProcessor(typeDefinitionRegistry, config, dataFetchingInterceptor, builder).augmentSchema() typeDefinitionRegistry .getTypes(InterfaceTypeDefinition::class.java) @@ -33,7 +39,7 @@ object SchemaBuilder { builder.type(typeDefinition.name) { it.typeResolver { env -> (env.getObject() as? Map) - ?.let { data -> data[ProjectionBase.TYPE_NAME] as? String } + ?.let { data -> data.get(ProjectionBase.TYPE_NAME) as? String } ?.let { typeName -> env.schema.getObjectType(typeName) } } } @@ -48,6 +54,7 @@ object SchemaBuilder { private class AugmentationProcessor( val typeDefinitionRegistry: TypeDefinitionRegistry, val schemaConfig: SchemaConfig, + val dataFetchingInterceptor: DataFetchingInterceptor?, val wiringBuilder: RuntimeWiring.Builder ) { private val metaProvider = TypeRegistryMetaProvider(typeDefinitionRegistry) @@ -123,7 +130,12 @@ object SchemaBuilder { } fun addDataFetcher(type: String, name: String, dataFetcher: DataFetcher) { - wiringBuilder.type(type) { runtimeWiring -> runtimeWiring.dataFetcher(name, dataFetcher) } + val df: DataFetcher<*> = dataFetchingInterceptor?.let { + DataFetcher { env -> + dataFetchingInterceptor.fetchData(env, dataFetcher) + } + } ?: dataFetcher + wiringBuilder.type(type) { runtimeWiring -> runtimeWiring.dataFetcher(name, df) } } /** diff --git a/src/test/kotlin/DataFetcherInterceptorDemo.kt b/src/test/kotlin/DataFetcherInterceptorDemo.kt new file mode 100644 index 00000000..a532db49 --- /dev/null +++ b/src/test/kotlin/DataFetcherInterceptorDemo.kt @@ -0,0 +1,67 @@ +package demo + +import graphql.GraphQL +import graphql.language.ListType +import graphql.language.NonNullType +import graphql.language.Type +import graphql.language.VariableReference +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import graphql.schema.GraphQLSchema +import org.intellij.lang.annotations.Language +import org.neo4j.driver.v1.AuthTokens +import org.neo4j.driver.v1.GraphDatabase +import org.neo4j.graphql.Cypher +import org.neo4j.graphql.DataFetchingInterceptor +import org.neo4j.graphql.SchemaBuilder +import java.math.BigDecimal +import java.math.BigInteger + + +fun initBoundSchema(schema: String): GraphQLSchema { + val driver = GraphDatabase.driver("bolt://localhost", AuthTokens.basic("neo4j", "test")) + + val dataFetchingInterceptor = object : DataFetchingInterceptor { + override fun fetchData(env: DataFetchingEnvironment, delegate: DataFetcher): Any? { + val cypher = delegate.get(env) + return driver.session().use { session -> + val result = session.run(cypher.query, cypher.params.mapValues { toBoltValue(it.value, env.variables) }) + val key = result.keys().stream().findFirst().orElse(null) + if (isListType(cypher.type)) { + result.list().map { record -> record.get(key).asObject() } + + } else { + result.list().map { record -> record.get(key).asObject() } + .firstOrNull() ?: emptyMap() + } + } + } + } + return SchemaBuilder.buildSchema(schema, dataFetchingInterceptor = dataFetchingInterceptor) +} + +fun main() { + @Language("GraphQL") val schema = initBoundSchema(""" + type Movie { + movieId: ID! + title: String + } + """.trimIndent()) + val graphql = GraphQL.newGraphQL(schema).build() + val movies = graphql.execute("{ movie { title }}") +} + +fun toBoltValue(value: Any?, params: Map) = when (value) { + is VariableReference -> params[value.name] + is BigInteger -> value.longValueExact() + is BigDecimal -> value.toDouble() + else -> value +} + +private fun isListType(type: Type<*>?): Boolean { + return when (type) { + is ListType -> true + is NonNullType -> isListType(type.type) + else -> false + } +} \ No newline at end of file From 9d3cb74c470e07b617dc9dc76fa6e86b19fdd63f Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Thu, 29 Aug 2019 21:55:19 +0200 Subject: [PATCH 2/7] split up augmentation and code wiring --- .../org/neo4j/graphql/AugmentationHandler.kt | 22 + .../kotlin/org/neo4j/graphql/BuildingEnv.kt | 198 +++++++++ src/main/kotlin/org/neo4j/graphql/Cypher.kt | 4 +- src/main/kotlin/org/neo4j/graphql/Facades.kt | 85 ---- .../org/neo4j/graphql/GraphQLExtensions.kt | 170 +++++-- .../kotlin/org/neo4j/graphql/MetaProvider.kt | 49 --- .../kotlin/org/neo4j/graphql/Neo4jTypes.kt | 14 +- .../kotlin/org/neo4j/graphql/Predicates.kt | 67 ++- .../kotlin/org/neo4j/graphql/SchemaBuilder.kt | 415 +++++++----------- .../neo4j/graphql/handler/BaseDataFetcher.kt | 83 ++-- .../graphql/handler/CreateTypeHandler.kt | 73 ++- .../graphql/handler/CypherDirectiveHandler.kt | 32 +- .../neo4j/graphql/handler/DeleteHandler.kt | 70 ++- .../graphql/handler/MergeOrUpdateHandler.kt | 67 ++- .../org/neo4j/graphql/handler/QueryHandler.kt | 89 ++-- .../handler/projection/ProjectionBase.kt | 113 +++-- .../handler/relation/BaseRelationHandler.kt | 133 ++++-- .../handler/relation/CreateRelationHandler.kt | 67 +-- .../relation/CreateRelationTypeHandler.kt | 97 +++- .../handler/relation/DeleteRelationHandler.kt | 40 +- src/test/kotlin/DataFetcherInterceptorDemo.kt | 13 +- 21 files changed, 1108 insertions(+), 793 deletions(-) create mode 100644 src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt create mode 100644 src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt delete mode 100644 src/main/kotlin/org/neo4j/graphql/Facades.kt delete mode 100644 src/main/kotlin/org/neo4j/graphql/MetaProvider.kt diff --git a/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt b/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt new file mode 100644 index 00000000..41caba82 --- /dev/null +++ b/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt @@ -0,0 +1,22 @@ +package org.neo4j.graphql + +import graphql.schema.* + +abstract class AugmentationHandler(val schemaConfig: SchemaConfig) { + companion object { + const val QUERY = "Query" + const val MUTATION = "Mutation" + } + + abstract fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) + + abstract fun createDataFetcher(rootType: GraphQLObjectType, fieldDefinition: GraphQLFieldDefinition): DataFetcher? + + fun input(name: String, type: GraphQLType): GraphQLArgument { + return GraphQLArgument + .newArgument() + .name(name) + .type((type.ref() as? GraphQLInputType) + ?: throw IllegalArgumentException("${type.innerName()} is not allowed for input")).build() + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt b/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt new file mode 100644 index 00000000..10acb7d2 --- /dev/null +++ b/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt @@ -0,0 +1,198 @@ +package org.neo4j.graphql + +import graphql.schema.* + +class BuildingEnv(val types: MutableMap) { + + private val typesForRelation = types.values + .filterIsInstance() + .filter { it.getDirective(DirectiveConstants.RELATION) != null } + .map { it.getDirectiveArgument(DirectiveConstants.RELATION, DirectiveConstants.RELATION_NAME, null)!! to it.name } + .toMap() + + fun getInnerFieldsContainer(type: GraphQLType): GraphQLFieldsContainer { + var innerType = type.inner() + if (innerType is GraphQLTypeReference) { + innerType = types[innerType.name] + ?: throw IllegalArgumentException("${innerType.name} is unknown") + } + return innerType as? GraphQLFieldsContainer + ?: throw IllegalArgumentException("${innerType.name} is neither an object nor an interface") + } + + fun buildFieldDefinition( + prefix: String, + resultType: GraphQLOutputType, + scalarFields: List, + nullableResult: Boolean, + forceOptionalProvider: (field: GraphQLFieldDefinition) -> Boolean = { false } + ): GraphQLFieldDefinition.Builder { + var type: GraphQLOutputType = resultType + if (!nullableResult) { + type = GraphQLNonNull(type) + } + return GraphQLFieldDefinition.newFieldDefinition() + .name("$prefix${resultType.name}") + .argument(getInputValueDefinitions(scalarFields, forceOptionalProvider)) + .type(type.ref() as GraphQLOutputType) + } + + fun getInputValueDefinitions( + relevantFields: List, + forceOptionalProvider: (field: GraphQLFieldDefinition) -> Boolean): List { + return relevantFields.map { field -> + var type = field.type as GraphQLType + type = getInputType(type) + type = if (forceOptionalProvider.invoke(field)) { + (type as? GraphQLNonNull)?.wrappedType ?: type + } else { + type + } + input(field.name, type) + } + } + + private fun getInputType(type: GraphQLType): GraphQLInputType { + val inner = type.inner() + if (inner is GraphQLInputType) { + return type as GraphQLInputType + } + if (inner.isNeo4jType()) { + return neo4jTypeDefinitions + .find { it.typeDefinition == inner.name } + ?.let { types[it.inputDefinition] } as? GraphQLInputType + ?: throw IllegalArgumentException("Cannot find input type for ${inner.name}") + } + return type as? GraphQLInputType + ?: throw IllegalArgumentException("${type.name} is not allowed for input") + } + + fun input(name: String, type: GraphQLType): GraphQLArgument { + return GraphQLArgument + .newArgument() + .name(name) + .type(getInputType(type).ref() as GraphQLInputType) + .build() + } + + /** + * add the given operation to the corresponding rootType + */ + fun addOperation(rootTypeName: String, fieldDefinition: GraphQLFieldDefinition) { + val rootType = types[rootTypeName] + types[rootTypeName] = if (rootType == null) { + val builder = GraphQLObjectType.newObject() + builder.name(rootTypeName) + .field(fieldDefinition) + .build() + } else { + val existingRootType = (rootType as? GraphQLObjectType + ?: throw IllegalStateException("root type $rootTypeName is not an object type")) + if (existingRootType.getFieldDefinition(fieldDefinition.name) != null) { + return // definition already exists + } + existingRootType + .transform { builder -> builder.field(fieldDefinition) } + } + } + + fun addFilterType(type: GraphQLFieldsContainer, handled: MutableSet = mutableSetOf()): String { + val filterName = "_${type.name}Filter" + if (handled.contains(filterName)) { + return filterName + } + val existingFilterType = types[filterName] + if (existingFilterType != null) { + return (existingFilterType as? GraphQLInputType)?.name + ?: throw IllegalStateException("Filter type $filterName is already defined but not an input type") + } + handled.add(filterName) + val builder = GraphQLInputObjectType.newInputObject() + .name(filterName) + listOf("AND", "OR", "NOT").forEach { + builder.field(GraphQLInputObjectField.newInputObjectField() + .name(it) + .type(GraphQLList(GraphQLNonNull(GraphQLTypeReference(filterName))))) + } + type.fieldDefinitions + .filter { it.dynamicPrefix() == null } // TODO currently we do not support filtering on dynamic properties + .forEach { field -> + val typeDefinition = field.type.inner() + val filterType = when { + typeDefinition.isNeo4jType() -> getInputType(typeDefinition).name + typeDefinition.isScalar() -> typeDefinition.innerName() + typeDefinition is GraphQLEnumType -> typeDefinition.innerName() + else -> addFilterType(getInnerFieldsContainer(typeDefinition), handled) + } + + Operators.forType(types[filterType] ?: typeDefinition).forEach { op -> + val wrappedType: GraphQLInputType = when { + op.list -> GraphQLList(GraphQLTypeReference(filterType)) + else -> GraphQLTypeReference(filterType) + } + builder.field(GraphQLInputObjectField.newInputObjectField() + .name(op.fieldName(field.name)) + .type(wrappedType)) + } + } + types[filterName] = builder.build() + return filterName + } + + fun addOrdering(type: GraphQLFieldsContainer): String? { + val orderingName = "_${type.name}Ordering" + var existingOrderingType = types[orderingName] + if (existingOrderingType != null) { + return (existingOrderingType as? GraphQLInputType)?.name + ?: throw IllegalStateException("Ordering type $type.name is already defined but not an input type") + } + val sortingFields = type.fieldDefinitions + .filter { it.type.isScalar() || it.isNeo4jType() } + .sortedByDescending { it.isID() } + if (sortingFields.isEmpty()){ + return null + } + existingOrderingType = GraphQLEnumType.newEnum() + .name(orderingName) + .values(sortingFields.flatMap { fd -> + listOf("_asc", "_desc") + .map { GraphQLEnumValueDefinition.newEnumValueDefinition().name(fd.name + it).build() } + }) + .build() + types[orderingName] = existingOrderingType + return orderingName + } + + fun addInputType(inputName: String, relevantFields: List): GraphQLInputType { + var inputType = types[inputName] + if (inputType != null) { + return inputType as? GraphQLInputType + ?: throw IllegalStateException("Filter type $inputName is already defined but not an input type") + } + inputType = getInputType(inputName, relevantFields) + types[inputName] = inputType + return inputType + } + + fun getInputType(inputName: String, relevantFields: List): GraphQLInputObjectType { + return GraphQLInputObjectType.newInputObject() + .name(inputName) + .fields(getInputValueDefinitions(relevantFields)) + .build() + } + + fun getInputValueDefinitions(relevantFields: List): List { + return relevantFields.map { + val type = (it.type as? GraphQLNonNull)?.wrappedType ?: it.type + GraphQLInputObjectField + .newInputObjectField() + .name(it.name) + .type(getInputType(type).ref() as GraphQLInputType) + .build() + } + } + + fun getTypeForRelation(nameOfRelation: String): GraphQLObjectType? { + return typesForRelation[nameOfRelation]?.let { types[it] } as? GraphQLObjectType + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/neo4j/graphql/Cypher.kt b/src/main/kotlin/org/neo4j/graphql/Cypher.kt index 468f7f71..61379c7c 100644 --- a/src/main/kotlin/org/neo4j/graphql/Cypher.kt +++ b/src/main/kotlin/org/neo4j/graphql/Cypher.kt @@ -1,8 +1,8 @@ package org.neo4j.graphql -import graphql.language.Type +import graphql.schema.GraphQLType -data class Cypher @JvmOverloads constructor(val query: String, val params: Map = emptyMap(), var type: Type<*>? = null) { +data class Cypher @JvmOverloads constructor(val query: String, val params: Map = emptyMap(), var type: GraphQLType? = null) { fun with(p: Map) = this.copy(params = this.params + p) fun escapedQuery() = query.replace("\"", "\\\"").replace("'", "\\'") diff --git a/src/main/kotlin/org/neo4j/graphql/Facades.kt b/src/main/kotlin/org/neo4j/graphql/Facades.kt deleted file mode 100644 index 33a6404a..00000000 --- a/src/main/kotlin/org/neo4j/graphql/Facades.kt +++ /dev/null @@ -1,85 +0,0 @@ -package org.neo4j.graphql - -import graphql.language.Directive -import graphql.language.FieldDefinition -import graphql.language.InterfaceTypeDefinition -import graphql.language.ObjectTypeDefinition - -interface NodeFacade { - fun name(): String - fun interfaces(): List - fun fieldDefinitions(): List - fun getDirective(directiveName: String): Directive? - fun getFieldDefinition(name: String?): FieldDefinition? = fieldDefinitions().firstOrNull { it.name == name } - fun hasRelationship(name: String, metaProvider: MetaProvider): Boolean { - val fieldDefinition = this.getFieldDefinition(name) ?: return false - return metaProvider.getNodeType(fieldDefinition.type.name()) != null - } - - fun relevantFields(): List = fieldDefinitions() - .filter { it.type.isScalar() || it.isNeo4jType() } - .sortedByDescending { it.isID() } - - fun isRelationType(): Boolean = this.getDirective(DirectiveConstants.RELATION) != null - - fun relationshipFor(name: String, metaProvider: MetaProvider): RelationshipInfo? { - val field = this.getFieldDefinition(name) - ?: throw IllegalArgumentException("$name is not defined on ${name()}") - val fieldObjectType = metaProvider.getNodeType(field.type.name()!!) ?: return null - - // TODO direction is depending on source/target type - - val (relDirective, isRelFromType) = fieldObjectType.getDirective(DirectiveConstants.RELATION)?.let { it to true } - ?: field.getDirective(DirectiveConstants.RELATION)?.let { it to false } - ?: throw IllegalStateException("Field $field needs an @relation directive") - - - val relInfo = relDetails(fieldObjectType) { argName, defaultValue -> - metaProvider.getDirectiveArgument(relDirective, argName, defaultValue) - } - - val inverse = isRelFromType && fieldObjectType.getFieldDefinition(relInfo.startField)?.name != this.name() - return if (inverse) relInfo.copy(out = relInfo.out?.let { !it }, startField = relInfo.endField, endField = relInfo.startField) else relInfo - } - - fun allLabels(): String { - return label(true) - } - - fun label(includeAll: Boolean = false) = when { - this.isRelationType() -> - getDirective(DirectiveConstants.RELATION)?.getArgument(DirectiveConstants.RELATION_NAME)?.value?.toJavaValue()?.toString()?.quote() - ?: name().quote() - else -> when { - includeAll -> (listOf(name()) + interfaces()) - .map { it.quote() } - .joinToString(":") - else -> name().quote() - } - } - - fun relationship(metaProvider: MetaProvider): RelationshipInfo? { - val relDirective = this.getDirective(DirectiveConstants.RELATION) ?: return null - val directiveResolver: (name: String, defaultValue: String?) -> String? = { argName, defaultValue -> - metaProvider.getDirectiveArgument(relDirective, argName, defaultValue) - } - val relType = directiveResolver(DirectiveConstants.RELATION_NAME, "")!! - val startField = directiveResolver(DirectiveConstants.RELATION_FROM, null) - val endField = directiveResolver(DirectiveConstants.RELATION_TO, null) - return RelationshipInfo(this, relType, null, startField, endField) - } -} - -data class ObjectDefinitionNodeFacade(private val delegate: ObjectTypeDefinition) : NodeFacade { - override fun name(): String = this.delegate.name - override fun fieldDefinitions(): List = this.delegate.fieldDefinitions - override fun getDirective(directiveName: String): Directive? = this.delegate.getDirective(directiveName) - override fun interfaces(): List = delegate.implements.mapNotNull { it.name() } -} - -data class InterfaceDefinitionNodeFacade(private val delegate: InterfaceTypeDefinition) : NodeFacade { - override fun name(): String = this.delegate.name - override fun fieldDefinitions(): List = this.delegate.fieldDefinitions - override fun getDirective(directiveName: String): Directive? = this.delegate.getDirective(directiveName) - override fun interfaces(): List = emptyList() -} diff --git a/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt b/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt index 3d7d0d6f..6d6dee4c 100644 --- a/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt +++ b/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt @@ -1,7 +1,8 @@ package org.neo4j.graphql +import graphql.Scalars import graphql.language.* -import graphql.language.TypeDefinition +import graphql.schema.* import org.neo4j.graphql.DirectiveConstants.Companion.CYPHER import org.neo4j.graphql.DirectiveConstants.Companion.CYPHER_STATEMENT import org.neo4j.graphql.DirectiveConstants.Companion.DYNAMIC @@ -27,31 +28,92 @@ fun Type>.inner(): Type> = when (this) { else -> this } -fun Type<*>.optional(): Type<*> = when (this) { - is ListType -> this - is NonNullType -> this.type - is TypeName -> this - else -> throw IllegalStateException("Can't nonNull $this") +fun GraphQLType.inner(): GraphQLType = when (this) { + is GraphQLList -> this.wrappedType.inner() + is GraphQLNonNull -> this.wrappedType.inner() + else -> this +} + +fun GraphQLType.isList() = this is GraphQLList || (this is GraphQLNonNull && this.wrappedType is GraphQLList) +fun GraphQLType.isScalar() = this.inner().let { it is GraphQLScalarType || it.name.startsWith("_Neo4j") } +fun GraphQLType.isNeo4jType() = this.inner().name?.startsWith("_Neo4j") == true +fun GraphQLFieldDefinition.isNeo4jType(): Boolean = this.type.isNeo4jType() + +fun GraphQLFieldDefinition.isRelationship() = this.type.inner().let { it is GraphQLFieldsContainer } + +fun GraphQLFieldsContainer.hasRelationship(name: String) = this.getFieldDefinition(name)?.isRelationship() ?: false +fun GraphQLDirectiveContainer.isRelationType() = getDirective(DirectiveConstants.RELATION) != null +fun GraphQLFieldsContainer.isRelationType() = (this as? GraphQLDirectiveContainer)?.getDirective(DirectiveConstants.RELATION) != null +fun GraphQLFieldsContainer.relationshipFor(name: String): RelationshipInfo? { + val field = getFieldDefinition(name) + ?: throw IllegalArgumentException("$name is not defined on ${this.name}") + val fieldObjectType = field.type.inner() as? GraphQLFieldsContainer ?: return null + + // TODO direction is depending on source/target type + + val (relDirective, isRelFromType) = (fieldObjectType as? GraphQLDirectiveContainer) + ?.getDirective(DirectiveConstants.RELATION)?.let { it to true } + ?: field.getDirective(DirectiveConstants.RELATION)?.let { it to false } + ?: throw IllegalStateException("Field $field needs an @relation directive") + + + val relInfo = relDetails(fieldObjectType) { argName, defaultValue -> relDirective.getArgument(argName, defaultValue) } + + val inverse = isRelFromType && fieldObjectType.getFieldDefinition(relInfo.startField)?.name != this.name + return if (inverse) relInfo.copy(out = relInfo.out?.let { !it }, startField = relInfo.endField, endField = relInfo.startField) else relInfo +} + +fun GraphQLFieldsContainer.getValidTypeLabels(schema: GraphQLSchema): List { + if (this is GraphQLObjectType) { + return listOf(this.label()) + } + if (this is GraphQLInterfaceType) { + return schema.getImplementations(this) + .mapNotNull { it.label() } + } + return emptyList() } -fun Type>.inputType(): Type<*> = if (this.isNeo4jType()) TypeName(this.name() + "Input") else this +fun GraphQLFieldsContainer.label(includeAll: Boolean = false) = when { + this.isRelationType() -> + (this as? GraphQLDirectiveContainer) + ?.getDirective(DirectiveConstants.RELATION) + ?.getArgument(RELATION_NAME)?.value?.toJavaValue()?.toString()?.quote() + ?: this.name.quote() + else -> when { + includeAll -> (listOf(name) + ((this as? GraphQLObjectType)?.interfaces?.map { it.name } ?: emptyList())) + .map { it.quote() } + .joinToString(":") + else -> name.quote() + } +} -fun Type<*>.isList() = this is ListType || (this is NonNullType && this.type is ListType) +fun GraphQLFieldsContainer.relevantFields() = fieldDefinitions + .filter { it.type.isScalar() || it.isNeo4jType() } + .sortedByDescending { it.isID() } -fun TypeDefinition<*>.getNodeType(): NodeFacade? { - return when (this) { - is ObjectTypeDefinition -> ObjectDefinitionNodeFacade(this) - is InterfaceTypeDefinition -> InterfaceDefinitionNodeFacade(this) - else -> null +fun GraphQLFieldsContainer.relationship(): RelationshipInfo? { + val relDirective = (this as? GraphQLDirectiveContainer)?.getDirective(DirectiveConstants.RELATION) ?: return null + val directiveResolver: (name: String, defaultValue: String?) -> String? = { argName, defaultValue -> + relDirective.getArgument(argName, defaultValue) } + val relType = directiveResolver(RELATION_NAME, "")!! + val startField = directiveResolver(RELATION_FROM, null) + val endField = directiveResolver(RELATION_TO, null) + return RelationshipInfo(this, relType, null, startField, endField) } -fun Type>.isNeo4jType(): Boolean = this.inner().name()?.startsWith("_Neo4j") == true -fun ObjectTypeDefinition.isNeo4jType(): Boolean = this.name.startsWith("_Neo4j") -fun FieldDefinition.isNeo4jType(): Boolean = this.type.isNeo4jType() -fun NodeFacade.isNeo4jType(): Boolean = this.name().startsWith("_Neo4j") +fun GraphQLType.ref(): GraphQLType = when (this) { + is GraphQLNonNull -> GraphQLNonNull(this.wrappedType.ref()) + is GraphQLList -> GraphQLList(this.wrappedType.ref()) + is GraphQLScalarType -> this + is GraphQLEnumType -> this + is GraphQLTypeReference -> this + else -> GraphQLTypeReference(name) +} -fun relDetails(type: NodeFacade, +fun relDetails(type: GraphQLFieldsContainer, + // TODO simplify uasage (no more callback) directiveResolver: (name: String, defaultValue: String?) -> String?): RelationshipInfo { val relType = directiveResolver(RELATION_NAME, "")!! val outgoing = when (directiveResolver(RELATION_DIRECTION, null)) { @@ -68,7 +130,7 @@ fun relDetails(type: NodeFacade, } data class RelationshipInfo( - val type: NodeFacade, + val type: GraphQLFieldsContainer, val relType: String, val out: Boolean?, val startField: String? = null, @@ -77,8 +139,8 @@ data class RelationshipInfo( ) { data class RelatedField( val argumentName: String, - val field: FieldDefinition, - val declaringType: NodeFacade + val field: GraphQLFieldDefinition, + val declaringType: GraphQLFieldsContainer ) val arrows = when (out) { @@ -87,39 +149,47 @@ data class RelationshipInfo( null -> "" to "" } - val typeName: String get() = this.type.name() + val typeName: String get() = this.type.name - fun getStartFieldId(metaProvider: MetaProvider) = getRelatedIdField(this.startField, metaProvider) + fun getStartFieldId() = getRelatedIdField(this.startField) - fun getEndFieldId(metaProvider: MetaProvider) = getRelatedIdField(this.endField, metaProvider) + fun getEndFieldId() = getRelatedIdField(this.endField) - private fun getRelatedIdField(relFieldName: String?, metaProvider: MetaProvider): RelatedField? { + private fun getRelatedIdField(relFieldName: String?): RelatedField? { if (relFieldName == null) return null - val relFieldDefinition = type.fieldDefinitions().find { it.name == relFieldName } - ?: throw IllegalArgumentException("field $relFieldName does not exists on ${type.name()}") - val relType = metaProvider.getNodeType(relFieldDefinition.type.name()) - ?: throw IllegalArgumentException("type ${relFieldDefinition.type.name()} not found") - return relType.fieldDefinitions().filter { it.isID() } + val relFieldDefinition = type.getFieldDefinition(relFieldName) + ?: throw IllegalArgumentException("field $relFieldName does not exists on ${type.innerName()}") + val relType = relFieldDefinition.type.inner() as? GraphQLFieldsContainer + ?: throw IllegalArgumentException("type ${relFieldDefinition.type.innerName()} not found") + return relType.fieldDefinitions.filter { it.isID() } .map { RelatedField("${relFieldName}_${it.name}", it, relType) } .firstOrNull() } } fun Field.aliasOrName() = (this.alias ?: this.name).quote() -fun Field.propertyName(fieldDefinition: FieldDefinition) = (fieldDefinition.propertyDirectiveName() - ?: this.name).quote() +fun GraphQLType.innerName() = inner().name -fun FieldDefinition.propertyDirectiveName() = - getDirective(PROPERTY)?.getArgument(PROPERTY_NAME)?.value?.toJavaValue()?.toString() +fun GraphQLFieldDefinition.propertyName() = getDirectiveArgument(PROPERTY, PROPERTY_NAME, this.name)!! -fun FieldDefinition.dynamicPrefix(metaProvider: MetaProvider): String? = metaProvider.getDirectiveArgument(getDirective(DYNAMIC), DYNAMIC_PREFIX, null) +fun GraphQLFieldDefinition.dynamicPrefix(): String? = getDirectiveArgument(DYNAMIC, DYNAMIC_PREFIX, null) +fun GraphQLType.getInnerFieldsContainer() = inner() as? GraphQLFieldsContainer + ?: throw IllegalArgumentException("${this.innerName()} is neither an object nor an interface") -fun FieldDefinition.cypherDirective(): Cypher? = - this.getDirective(CYPHER)?.let { - @Suppress("UNCHECKED_CAST") - (Cypher(it.getArgument(CYPHER_STATEMENT).value.toJavaValue().toString(), - it.getArgument("params")?.value?.toJavaValue() as Map? ?: emptyMap())) - } +fun GraphQLDirectiveContainer.getDirectiveArgument(directiveName: String, argumentName: String, defaultValue: T?): T? = + getDirective(directiveName)?.getArgument(argumentName, defaultValue) ?: defaultValue + +fun GraphQLDirective.getArgument(argumentName: String, defaultValue: T?): T? { + val argument = getArgument(argumentName) + @Suppress("UNCHECKED_CAST") + return argument?.value as T? + ?: argument.defaultValue as T? + ?: defaultValue + ?: throw IllegalStateException("No default value for @${this.name}::$argumentName") +} + +fun GraphQLFieldDefinition.cypherDirective(): Cypher? = getDirectiveArgument(CYPHER, CYPHER_STATEMENT, null) + ?.let { statement -> Cypher(statement) } fun String.quote() = if (isJavaIdentifier()) this else "`$this`" @@ -135,11 +205,16 @@ fun Value>.toCypherString(): String = when (this) { is FloatValue -> this.value.toString() is IntValue -> this.value.toString() is VariableReference -> "$" + this.name - is ArrayValue -> this.values.map{ it.toCypherString() }.joinToString(",", "[", "]") + is ArrayValue -> this.values.map { it.toCypherString() }.joinToString(",", "[", "]") else -> throw IllegalStateException("Unhandled value $this") } -fun Value>.toJavaValue(): Any? = when (this) { +fun Any.toJavaValue() = when (this) { + is Value<*> -> this.toJavaValue() + else -> this +} + +fun Value<*>.toJavaValue(): Any? = when (this) { is StringValue -> this.value is EnumValue -> this.name is NullValue -> null @@ -157,9 +232,8 @@ fun paramName(variable: String, argName: String, value: Any?): String = when (va else -> "$variable${argName.capitalize()}" } -fun FieldDefinition.isID(): Boolean = this.type.name() == "ID" -fun FieldDefinition.isNativeId() = this.name == ProjectionBase.NATIVE_ID +fun GraphQLFieldDefinition.isID() = this.type.inner() == Scalars.GraphQLID +fun GraphQLFieldDefinition.isNativeId() = this.name == ProjectionBase.NATIVE_ID -fun FieldDefinition.isList(): Boolean = this.type.isList() -fun ObjectTypeDefinition.getFieldByType(typeName: String): FieldDefinition? = this.fieldDefinitions - .firstOrNull { it.type.inner().name() == typeName } +fun GraphQLFieldsContainer.getFieldByType(typeName: String) = this.fieldDefinitions + .firstOrNull { it.type.inner().name == typeName } \ No newline at end of file diff --git a/src/main/kotlin/org/neo4j/graphql/MetaProvider.kt b/src/main/kotlin/org/neo4j/graphql/MetaProvider.kt deleted file mode 100644 index f5e7dfa6..00000000 --- a/src/main/kotlin/org/neo4j/graphql/MetaProvider.kt +++ /dev/null @@ -1,49 +0,0 @@ -package org.neo4j.graphql - -import graphql.language.Directive -import graphql.language.DirectiveDefinition -import graphql.language.InterfaceTypeDefinition -import graphql.language.ObjectTypeDefinition -import graphql.schema.idl.TypeDefinitionRegistry - -interface MetaProvider { - fun getNodeType(name: String?): NodeFacade? - fun getValidTypeLabels(node: NodeFacade): List - fun getDirectiveArgument(directive: Directive?, name: String, defaultValue: String?): String? -} - -class TypeRegistryMetaProvider(private val typeRegistry: TypeDefinitionRegistry) : MetaProvider { - override fun getValidTypeLabels(node: NodeFacade): List { - val type = typeRegistry.types()[node.name()] - if (type is ObjectTypeDefinition) { - return listOf(type.getNodeType()!!.label()) - } - if (type is InterfaceTypeDefinition) { - return typeRegistry - .getTypes(ObjectTypeDefinition::class.java) - .filter { objectTypeDefinition -> objectTypeDefinition.implements.any { it.name() == node.name() } } - .mapNotNull { it.getNodeType()?.label() } - } - return emptyList() - } - - private fun getDirectiveDefinition(name: String?): DirectiveDefinition? { - return when (name) { - null -> null - else -> typeRegistry.getDirectiveDefinition(name).orElse(null) - } - } - - override fun getNodeType(name: String?): NodeFacade? { - return typeRegistry.getType(name).orElse(null)?.getNodeType() - } - - override fun getDirectiveArgument(directive: Directive?, name: String, defaultValue: String?): String? { - if (directive == null) return null - return directive.getArgument(name)?.value?.toJavaValue()?.toString() - ?: getDirectiveDefinition(directive.name)?.inputValueDefinitions?.first { it.name == name }?.defaultValue?.toJavaValue()?.toString() - ?: defaultValue - ?: throw IllegalStateException("No default value for ${directive.name}.$name") - } - -} \ No newline at end of file diff --git a/src/main/kotlin/org/neo4j/graphql/Neo4jTypes.kt b/src/main/kotlin/org/neo4j/graphql/Neo4jTypes.kt index 81edeaaa..e791f3f6 100644 --- a/src/main/kotlin/org/neo4j/graphql/Neo4jTypes.kt +++ b/src/main/kotlin/org/neo4j/graphql/Neo4jTypes.kt @@ -1,9 +1,9 @@ package org.neo4j.graphql import graphql.language.Field -import graphql.language.FieldDefinition import graphql.language.ObjectValue -import graphql.language.Value +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLFieldsContainer import java.time.* import java.time.temporal.Temporal @@ -37,13 +37,13 @@ fun getNeo4jTypeConverter(name: String): Neo4jConverter { data class Neo4jQueryConversion(val name: String, val propertyName: String, val converter: Neo4jConverter = Neo4jConverter()) { companion object { - fun forQuery(argument: Translator.CypherArgument, field: Field, type: NodeFacade): Neo4jQueryConversion { + fun forQuery(argument: Translator.CypherArgument, field: Field, type: GraphQLFieldsContainer): Neo4jQueryConversion { val isNeo4jType = type.isNeo4jType() val name = argument.name return when (isNeo4jType) { true -> { if (name == NEO4j_FORMATTED_PROPERTY_KEY) { - Neo4jQueryConversion(field.name + NEO4j_FORMATTED_PROPERTY_KEY.capitalize(), field.name, getNeo4jTypeConverter(type.name())) + Neo4jQueryConversion(field.name + NEO4j_FORMATTED_PROPERTY_KEY.capitalize(), field.name, getNeo4jTypeConverter(type.name)) } else { Neo4jQueryConversion(field.name + name.capitalize(), field.name + ".$name") } @@ -53,13 +53,13 @@ data class Neo4jQueryConversion(val name: String, val propertyName: String, val } - fun forMutation(value: Value>, fieldDefinition: FieldDefinition): Neo4jQueryConversion { - val isNeo4jType = fieldDefinition.type.isNeo4jType() + fun forMutation(value: Any, fieldDefinition: GraphQLFieldDefinition): Neo4jQueryConversion { + val isNeo4jType = fieldDefinition.isNeo4jType() val name = fieldDefinition.name if (!isNeo4jType) { Neo4jQueryConversion(name, name) } - val converter = getNeo4jTypeConverter(fieldDefinition.type.name()!!) + val converter = getNeo4jTypeConverter(fieldDefinition.type.inner().name) val objectValue = (value as? ObjectValue) ?.objectFields ?.map { it.name to it.value } diff --git a/src/main/kotlin/org/neo4j/graphql/Predicates.kt b/src/main/kotlin/org/neo4j/graphql/Predicates.kt index 4dff5ebf..2123c3af 100644 --- a/src/main/kotlin/org/neo4j/graphql/Predicates.kt +++ b/src/main/kotlin/org/neo4j/graphql/Predicates.kt @@ -1,22 +1,17 @@ package org.neo4j.graphql import graphql.Scalars -import graphql.language.* -import graphql.language.TypeDefinition -import graphql.schema.GraphQLEnumType -import graphql.schema.GraphQLInputType -import graphql.schema.GraphQLObjectType -import graphql.schema.GraphQLTypeReference +import graphql.schema.* import org.neo4j.graphql.Predicate.Companion.resolvePredicate import org.neo4j.graphql.handler.projection.ProjectionBase interface Predicate { - fun toExpression(variable: String, metaProvider: MetaProvider): Cypher + fun toExpression(variable: String): Cypher companion object { - fun resolvePredicate(name: String, value: Any?, type: NodeFacade, metaProvider: MetaProvider): Predicate { + fun resolvePredicate(name: String, value: Any?, type: GraphQLFieldsContainer): Predicate { val (fieldName, op) = Operators.resolve(name, value) - return if (type.hasRelationship(fieldName, metaProvider)) { + return if (type.hasRelationship(fieldName)) { when (value) { is Map<*, *> -> RelationPredicate(fieldName, op, value, type) null -> IsNullPredicate(fieldName, op, type) @@ -35,27 +30,27 @@ interface Predicate { null -> "null" is String -> if (isParam(value)) value else "\"$value\"" is Map<*, *> -> "{" + value.map { it.key.toString() + ":" + formatAnyValueCypher(it.value) }.joinToString(",") + "}" - is Iterable<*> -> "[" + value.map { formatAnyValueCypher(it) }.joinToString (",") + "]" + is Iterable<*> -> "[" + value.map { formatAnyValueCypher(it) }.joinToString(",") + "]" else -> value.toString() } } } -fun toExpression(name: String, value: Any?, type: NodeFacade, metaProvider: MetaProvider): Predicate = +fun toExpression(name: String, value: Any?, type: GraphQLFieldsContainer): Predicate = if (name == "AND" || name == "OR") when (value) { - is Iterable<*> -> CompoundPredicate(value.map { toExpression("AND", it, type, metaProvider) }, name) - is Map<*, *> -> CompoundPredicate(value.map { (k, v) -> toExpression(k.toString(), v, type, metaProvider) }, name) + is Iterable<*> -> CompoundPredicate(value.map { toExpression("AND", it, type) }, name) + is Map<*, *> -> CompoundPredicate(value.map { (k, v) -> toExpression(k.toString(), v, type) }, name) else -> throw IllegalArgumentException("Unexpected value for filter: $value") } else { - resolvePredicate(name, value, type, metaProvider) + resolvePredicate(name, value, type) } data class CompoundPredicate(val parts: List, val op: String = "AND") : Predicate { - override fun toExpression(variable: String, metaProvider: MetaProvider): Cypher = - parts.map { it.toExpression(variable, metaProvider) } + override fun toExpression(variable: String): Cypher = + parts.map { it.toExpression(variable) } .let { expressions -> Cypher( expressions.map { it.query }.joinNonEmpty(" $op ", "(", ")"), @@ -64,18 +59,18 @@ data class CompoundPredicate(val parts: List, val op: String = "AND") } } -data class IsNullPredicate(val fieldName: String, val op: Operators, val type: NodeFacade) : Predicate { - override fun toExpression(variable: String, metaProvider: MetaProvider): Cypher { - val rel = type.relationshipFor(fieldName, metaProvider) ?: throw IllegalArgumentException("Not a relation") +data class IsNullPredicate(val fieldName: String, val op: Operators, val type: GraphQLFieldsContainer) : Predicate { + override fun toExpression(variable: String): Cypher { + val rel = type.relationshipFor(fieldName) ?: throw IllegalArgumentException("Not a relation") val (left, right) = rel.arrows val not = if (op.not) "" else "NOT " return Cypher("$not($variable)$left-[:${rel.relType}]-$right()") } } -data class ExpressionPredicate(val name: String, val op: Operators, val value: Any?, val fieldDefinition: FieldDefinition) : Predicate { +data class ExpressionPredicate(val name: String, val op: Operators, val value: Any?, val fieldDefinition: GraphQLFieldDefinition) : Predicate { val not = if (op.not) "NOT " else "" - override fun toExpression(variable: String, metaProvider: MetaProvider): Cypher { + override fun toExpression(variable: String): Cypher { val paramName: String = ProjectionBase.FILTER + paramName(variable, name, value).capitalize() val field = if (fieldDefinition.isNativeId()) "ID($variable)" else "$variable.${name.quote()}" return Cypher("$not$field ${op.op} \$$paramName", mapOf(paramName to value)) @@ -83,25 +78,23 @@ data class ExpressionPredicate(val name: String, val op: Operators, val value: A } -data class RelationPredicate(val fieldName: String, val op: Operators, val value: Map<*, *>, val type: NodeFacade) : Predicate { +data class RelationPredicate(val fieldName: String, val op: Operators, val value: Map<*, *>, val type: GraphQLFieldsContainer) : Predicate { val not = if (op.not) "NOT" else "" // (type)-[:TYPE]->(related) | pred] = 0/1/ > 0 | = // ALL/ANY/NONE/SINGLE(p in (type)-[:TYPE]->() WHERE pred(last(nodes(p))) // ALL/ANY/NONE/SINGLE(x IN [(type)-[:TYPE]->(o) | pred(o)] WHERE x) - override fun toExpression(variable: String, metaProvider: MetaProvider): Cypher { + override fun toExpression(variable: String): Cypher { val prefix = when (op) { Operators.EQ -> "ALL" Operators.NEQ -> "ALL" // bc of not else -> op.op } - val rel = type.relationshipFor(fieldName, metaProvider) ?: throw IllegalArgumentException("Not a relation") + val rel = type.relationshipFor(fieldName) ?: throw IllegalArgumentException("Not a relation") val (left, right) = rel.arrows val other = variable + "_" + rel.typeName val cond = other + "_Cond" - val relNodeType = metaProvider.getNodeType(rel.typeName) - ?: throw IllegalArgumentException("${rel.typeName} not found") - val (pred, params) = CompoundPredicate(value.map { resolvePredicate(it.key.toString(), it.value, relNodeType, metaProvider) }).toExpression(other, metaProvider) + val (pred, params) = CompoundPredicate(value.map { resolvePredicate(it.key.toString(), it.value, rel.type) }).toExpression(other) return Cypher("$not $prefix($cond IN [($variable)$left-[:${rel.relType.quote()}]-$right($other) | $pred] WHERE $cond)", params) } } @@ -162,15 +155,17 @@ enum class Operators(val suffix: String, val op: String, val not: Boolean = fals else listOf(EQ, NEQ, IN, NIN, LT, LTE, GT, GTE) + if (type == Scalars.GraphQLString || type == Scalars.GraphQLID) listOf(C, NC, SW, NSW, EW, NEW) else emptyList() - fun forType(type: Type>, typeDef: TypeDefinition<*>): List = - if (type.name() == "Boolean") listOf(EQ, NEQ) - else if (type.isNeo4jType()) listOf(EQ, NEQ, IN, NIN) - else if (typeDef is ObjectTypeDefinition || typeDef is InterfaceTypeDefinition) listOf(IS_NULL, IS_NOT_NULL, SOME, NONE, SINGLE) - else if (typeDef is EnumTypeDefinition) listOf(EQ, NEQ, IN, NIN) - // todo list types - else if (!type.isScalar()) listOf(EQ, NEQ, IN, NIN) - else listOf(EQ, NEQ, IN, NIN, LT, LTE, GT, GTE) + - if (type.name() == "String" || type.name() == "ID") listOf(C, NC, SW, NSW, EW, NEW) else emptyList() + fun forType(type: GraphQLType): List = + when { + type == Scalars.GraphQLBoolean -> listOf(EQ, NEQ) + type.isNeo4jType() -> listOf(EQ, NEQ, IN, NIN) + type is GraphQLFieldsContainer || type is GraphQLInputObjectType -> listOf(IS_NULL, IS_NOT_NULL, SOME, NONE, SINGLE) + type is GraphQLEnumType -> listOf(EQ, NEQ, IN, NIN) + // todo list types + !type.isScalar() -> listOf(EQ, NEQ, IN, NIN) + else -> listOf(EQ, NEQ, IN, NIN, LT, LTE, GT, GTE) + + if (type.name == "String" || type.name == "ID") listOf(C, NC, SW, NSW, EW, NEW) else emptyList() + } } diff --git a/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt b/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt index 354a3b0b..57293d5f 100644 --- a/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt +++ b/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt @@ -1,9 +1,8 @@ package org.neo4j.graphql +import graphql.Scalars import graphql.language.* -import graphql.language.TypeDefinition -import graphql.schema.DataFetcher -import graphql.schema.GraphQLSchema +import graphql.schema.* import graphql.schema.idl.RuntimeWiring import graphql.schema.idl.SchemaGenerator import graphql.schema.idl.SchemaParser @@ -15,6 +14,9 @@ import org.neo4j.graphql.handler.relation.CreateRelationTypeHandler import org.neo4j.graphql.handler.relation.DeleteRelationHandler object SchemaBuilder { + private const val MUTATION = "Mutation" + private const val SUBSCRIPTION = "Subscription" + private const val QUERY = "Query" /** * @param sdl the schema to augment @@ -27,300 +29,209 @@ object SchemaBuilder { fun buildSchema(sdl: String, config: SchemaConfig = SchemaConfig(), dataFetchingInterceptor: DataFetchingInterceptor? = null): GraphQLSchema { val schemaParser = SchemaParser() val typeDefinitionRegistry = schemaParser.parse(sdl) - + mergeNeo4jEnhancements(typeDefinitionRegistry) + if (!typeDefinitionRegistry.getType(QUERY).isPresent) { + typeDefinitionRegistry.add(ObjectTypeDefinition.newObjectTypeDefinition().name(QUERY).build()) + } val builder = RuntimeWiring.newRuntimeWiring() .scalar(DynamicProperties.INSTANCE) - - AugmentationProcessor(typeDefinitionRegistry, config, dataFetchingInterceptor, builder).augmentSchema() - typeDefinitionRegistry .getTypes(InterfaceTypeDefinition::class.java) .forEach { typeDefinition -> builder.type(typeDefinition.name) { it.typeResolver { env -> (env.getObject() as? Map) - ?.let { data -> data.get(ProjectionBase.TYPE_NAME) as? String } + ?.let { data -> data[ProjectionBase.TYPE_NAME] as? String } ?.let { typeName -> env.schema.getObjectType(typeName) } } } } + val sourceSchema = SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, builder.build()) - val runtimeWiring = builder.build() + val handler = getHandler(config) - // todo add new queries, filters, enums etc. - return SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, runtimeWiring) + var targetSchema = augmentSchema(sourceSchema, handler) + targetSchema = addDataFetcher(targetSchema, dataFetchingInterceptor, handler) + return targetSchema } - private class AugmentationProcessor( - val typeDefinitionRegistry: TypeDefinitionRegistry, - val schemaConfig: SchemaConfig, - val dataFetchingInterceptor: DataFetchingInterceptor?, - val wiringBuilder: RuntimeWiring.Builder - ) { - private val metaProvider = TypeRegistryMetaProvider(typeDefinitionRegistry) - private var schemaDefinition: SchemaDefinition - private var queryDefinition: ObjectTypeDefinition - private var mutationDefinition: ObjectTypeDefinition - - init { - this.schemaDefinition = typeDefinitionRegistry - .schemaDefinition() - .orElseGet { SchemaDefinition.newSchemaDefinition().build() } - this.queryDefinition = getOrCreateObjectTypeDefinition("Query") - this.mutationDefinition = getOrCreateObjectTypeDefinition("Mutation") + private fun getHandler(schemaConfig: SchemaConfig): List { + val handler = mutableListOf( + CypherDirectiveHandler.Factory(schemaConfig) + ) + if (schemaConfig.query.enabled) { + handler.add(QueryHandler.Factory(schemaConfig)) } - - fun augmentSchema() { - mergeNeo4jEnhancements() - - val interfaceTypeDefinitions = typeDefinitionRegistry.types().values - .filterIsInstance() - .map { enhanceRelations(it, InterfaceTypeDefinition.CHILD_DEFINITIONS) } - val objectTypeDefinitions = typeDefinitionRegistry.types().values - .filterIsInstance() - .filter { !it.isNeo4jType() } - .map { enhanceRelations(it, ObjectTypeDefinition.CHILD_FIELD_DEFINITIONS) } - - wireDefinedOperations(queryDefinition, true) - wireDefinedOperations(mutationDefinition, false) - - val relationTypes = objectTypeDefinitions - .filter { it.getDirective(DirectiveConstants.RELATION) != null } - .map { it.getDirective(DirectiveConstants.RELATION).getArgument(DirectiveConstants.RELATION_NAME).value.toJavaValue().toString() to it } - .toMap() - - val nodeDefinitions: List> = interfaceTypeDefinitions + objectTypeDefinitions - nodeDefinitions.forEach { createNodeMutation(it.getNodeType()!!) } - - objectTypeDefinitions.forEach { createRelationshipMutations(it, objectTypeDefinitions, relationTypes) } - relationTypes.values.forEach { createNodeMutation(it.getNodeType()!!) } - - recreateSchema() + if (schemaConfig.mutation.enabled) { + handler += listOf( + MergeOrUpdateHandler.Factory(schemaConfig), + DeleteHandler.Factory(schemaConfig), + CreateTypeHandler.Factory(schemaConfig), + DeleteRelationHandler.Factory(schemaConfig), + CreateRelationTypeHandler.Factory(schemaConfig), + CreateRelationHandler.Factory(schemaConfig) + ) } + return handler + } - private fun recreateSchema() { - val newSchemaDef = schemaDefinition.transform { - it.operationTypeDefinition(OperationTypeDefinition("query", TypeName(queryDefinition.name))) - .operationTypeDefinition(OperationTypeDefinition("mutation", TypeName(mutationDefinition.name))).build() + private fun augmentSchema(sourceSchema: GraphQLSchema, handler: List): GraphQLSchema { + val types = sourceSchema.typeMap.toMutableMap() + val env = BuildingEnv(types) + + types.values + .filterIsInstance() + .filter { + !it.name.startsWith("__") + && !it.isNeo4jType() + && it.name != QUERY + && it.name != MUTATION + && it.name != SUBSCRIPTION + } + .forEach { type -> + handler.forEach { h -> h.augmentType(type, env) } } - typeDefinitionRegistry.remove(schemaDefinition) - typeDefinitionRegistry.add(newSchemaDef) - schemaDefinition = newSchemaDef - } - - private fun mergeNeo4jEnhancements() { - val directivesSdl = javaClass.getResource("/neo4j.graphql").readText() - typeDefinitionRegistry.merge(SchemaParser().parse(directivesSdl)) - neo4jTypeDefinitions - .forEach { - val type = typeDefinitionRegistry.getType(it.typeDefinition) - .orElseThrow { IllegalStateException("type ${it.typeDefinition} not found") } - as ObjectTypeDefinition - addInputType(it.inputDefinition, it.inputDefinition, type.fieldDefinitions) + types.replaceAll { _, sourceType -> + when { + sourceType.name.startsWith("__") -> sourceType + sourceType is GraphQLObjectType -> sourceType.transform { builder -> + builder.clearFields().clearInterfaces() + // to prevent duplicated types in schema + sourceType.interfaces.forEach { builder.withInterface(GraphQLTypeReference(it.name)) } + sourceType.fieldDefinitions.forEach { f -> builder.field(enhanceRelations(f)) } } - } - - private fun wireDefinedOperations(queryDefinition: ObjectTypeDefinition, isQuery: Boolean) { - for (fieldDefinition in queryDefinition.fieldDefinitions) { - QueryHandler.build(fieldDefinition, isQuery, metaProvider)?.let { - addDataFetcher(queryDefinition.name, fieldDefinition.name, it) + sourceType is GraphQLInterfaceType -> sourceType.transform { builder -> + builder.clearFields() + sourceType.fieldDefinitions.forEach { f -> builder.field(enhanceRelations(f)) } } + else -> sourceType } } - fun addDataFetcher(type: String, name: String, dataFetcher: DataFetcher) { - val df: DataFetcher<*> = dataFetchingInterceptor?.let { - DataFetcher { env -> - dataFetchingInterceptor.fetchData(env, dataFetcher) - } - } ?: dataFetcher - wiringBuilder.type(type) { runtimeWiring -> runtimeWiring.dataFetcher(name, df) } - } + return GraphQLSchema + .newSchema(sourceSchema) + .clearAdditionalTypes() + .query(types[QUERY] as? GraphQLObjectType) + .mutation(types[MUTATION] as? GraphQLObjectType) + .additionalTypes(types.values.toSet()) + .build() + } + + private fun enhanceRelations(fd: GraphQLFieldDefinition): GraphQLFieldDefinition { + return fd.transform { + // to prevent duplicated types in schema + it.type(fd.type.ref() as GraphQLOutputType) - /** - * add the given operation to the corresponding ObjectTypeDefinition - */ - private fun mergeOperation(objectTypeDefinition: ObjectTypeDefinition, dataFetcher: BaseDataFetcher?) - : ObjectTypeDefinition { - if (dataFetcher == null) { - return objectTypeDefinition + if (!fd.isRelationship() || !fd.type.isList()) { + return@transform } - if (objectTypeDefinition.fieldDefinitions.any { fd -> dataFetcher.fieldDefinition.name == fd.name }) { - return objectTypeDefinition // definition already exists + + if (fd.getArgument(ProjectionBase.FIRST) == null) { + it.argument { a -> a.name(ProjectionBase.FIRST).type(Scalars.GraphQLInt) } } - typeDefinitionRegistry.remove(objectTypeDefinition) - val transformedTypeDefinition = objectTypeDefinition.transform { qdb -> - qdb.fieldDefinition(dataFetcher.fieldDefinition) - addDataFetcher(objectTypeDefinition.name, dataFetcher.fieldDefinition.name, dataFetcher) + if (fd.getArgument(ProjectionBase.OFFSET) == null) { + it.argument { a -> a.name(ProjectionBase.OFFSET).type(Scalars.GraphQLInt) } } - typeDefinitionRegistry.add(transformedTypeDefinition) - return transformedTypeDefinition + // TODO implement ordering +// if (fd.getArgument(ProjectionBase.ORDER_BY) == null) { +// val typeName = fd.type.name()!! +// val orderingType = addOrdering(typeName, metaProvider.getNodeType(typeName)!!.fieldDefinitions().filter { it.type.isScalar() }) +// it.argument { a -> a.name(ProjectionBase.ORDER_BY).type(orderingType) } +// } } + } - private fun getOrCreateObjectTypeDefinition(name: String): ObjectTypeDefinition { - val operationGroup = name.toLowerCase() - return schemaDefinition.operationTypeDefinitions - .firstOrNull { it.name == operationGroup } - ?.let { - typeDefinitionRegistry - .getType(it.typeName, ObjectTypeDefinition::class.java) - .orElseThrow { RuntimeException("Could not find type: ${it.typeName} in schema") } as ObjectTypeDefinition - } - ?: typeDefinitionRegistry.getType(name, ObjectTypeDefinition::class.java) - .orElseGet { - ObjectTypeDefinition(name).also { typeDefinitionRegistry.add(it) } - } - } + private fun addDataFetcher(sourceSchema: GraphQLSchema, dataFetchingInterceptor: DataFetchingInterceptor?, handler: List): GraphQLSchema { + val codeRegistryBuilder = GraphQLCodeRegistry.newCodeRegistry(sourceSchema.codeRegistry) + addDataFetcher(sourceSchema.queryType, dataFetchingInterceptor, handler, codeRegistryBuilder) + addDataFetcher(sourceSchema.mutationType, dataFetchingInterceptor, handler, codeRegistryBuilder) + return sourceSchema.transform { it.codeRegistry(codeRegistryBuilder.build()) } + } - private fun createRelationshipMutations( - source: ObjectTypeDefinition, - objectTypeDefinitions: List, - relationTypes: Map?) { - if (!schemaConfig.mutation.enabled || schemaConfig.mutation.exclude.contains(source.name)) { - return - } - source.fieldDefinitions - .filter { !it.type.inner().isScalar() && it.getDirective(DirectiveConstants.RELATION) != null } - .mapNotNull { targetField -> - objectTypeDefinitions.firstOrNull { it.name == targetField.type.inner().name() } - ?.let { target -> - mutationDefinition = mergeOperation(mutationDefinition, DeleteRelationHandler.build(source, target, metaProvider)) - mutationDefinition = mergeOperation(mutationDefinition, CreateRelationHandler.build(source, target, relationTypes, metaProvider)) + private fun addDataFetcher( + rootType: GraphQLObjectType?, + dataFetchingInterceptor: DataFetchingInterceptor?, + handler: List, + codeRegistryBuilder: GraphQLCodeRegistry.Builder) { + if (rootType == null) return + rootType.fieldDefinitions.forEach { field -> + handler.forEach { h -> + h.createDataFetcher(rootType, field)?.let { dataFetcher -> + val df: DataFetcher<*> = dataFetchingInterceptor?.let { + DataFetcher { env -> + dataFetchingInterceptor.fetchData(env, dataFetcher) } + } ?: dataFetcher + codeRegistryBuilder.dataFetcher(rootType, field, df) } - } - - - private fun > enhanceRelations(source: T, childName: String): T { - var enhanced: T = source - val fieldDefinitions = enhanced.namedChildren.children[childName] - ?: return enhanced - for ((index, fd) in fieldDefinitions.withIndex()) { - if (fd !is FieldDefinition - || !typeDefinitionRegistry.types().containsKey(fd.type.inner().name()) - || !fd.isList()) { - continue - } - @Suppress("UNCHECKED_CAST") - enhanced = enhanced.withNewChildren(enhanced.namedChildren.transform { b1 -> - b1.replaceChild(childName, index, fd.transform { builder -> - if (fd.inputValueDefinitions.none { it.name == ProjectionBase.FIRST }) { - builder.inputValueDefinition(BaseDataFetcher.input(ProjectionBase.FIRST, TypeName("Int"))) - } - if (fd.inputValueDefinitions.none { it.name == ProjectionBase.OFFSET }) { - builder.inputValueDefinition(BaseDataFetcher.input(ProjectionBase.OFFSET, TypeName("Int"))) - } - // TODO implement ordering -// if (fd.inputValueDefinitions.none { it.name == ProjectionBase.ORDER_BY }) { -// val typeName = fd.type.name()!! -// val orderingName = addOrdering(typeName, metaProvider.getNodeType(typeName)!!.fieldDefinitions().filter { it.type.isScalar() }) -// builder.inputValueDefinition(BaseDataFetcher.input(ProjectionBase.ORDER_BY, TypeName(orderingName))) -// } - }) - }) as T } - if (source != enhanced) { - typeDefinitionRegistry.remove(source) - typeDefinitionRegistry.add(enhanced) - } - return enhanced } + } - private fun createNodeMutation(type: NodeFacade) { - val typeName = type.name() - val idField = type.fieldDefinitions().find { it.isID() } - val relevantFields = type.fieldDefinitions() - .filter { it.type.isScalar() || it.type.isNeo4jType() } - .sortedByDescending { it == idField } - if (relevantFields.isEmpty()) { - return + private fun mergeNeo4jEnhancements(typeDefinitionRegistry: TypeDefinitionRegistry) { + val directivesSdl = javaClass.getResource("/neo4j.graphql").readText() + typeDefinitionRegistry.merge(SchemaParser().parse(directivesSdl)) + neo4jTypeDefinitions + .forEach { + val type = typeDefinitionRegistry.getType(it.typeDefinition) + .orElseThrow { IllegalStateException("type ${it.typeDefinition} not found") } + as ObjectTypeDefinition + addInputType(typeDefinitionRegistry, it.inputDefinition, type.fieldDefinitions) } - if (schemaConfig.mutation.enabled && !schemaConfig.mutation.exclude.contains(typeName)) { - if (type is ObjectDefinitionNodeFacade) { - mutationDefinition = if (type.isRelationType()) { - mergeOperation(mutationDefinition, CreateRelationTypeHandler.build(type, metaProvider)) - } else { - mergeOperation(mutationDefinition, CreateTypeHandler.build(type, metaProvider)) - } - } - mutationDefinition = mergeOperation(mutationDefinition, DeleteHandler.build(type, metaProvider)) - mutationDefinition = mergeOperation(mutationDefinition, MergeOrUpdateHandler.build(type, true, metaProvider)) - mutationDefinition = mergeOperation(mutationDefinition, MergeOrUpdateHandler.build(type, false, metaProvider)) - } - if (schemaConfig.query.enabled && !schemaConfig.query.exclude.contains(typeName)) { - addInputType(typeName, relevantFields = relevantFields) - val filterName = addFilterType(typeName, type.fieldDefinitions()) - val orderingName = addOrdering(typeName, relevantFields) - queryDefinition = mergeOperation(queryDefinition, QueryHandler.build(type, filterName, orderingName, metaProvider)) - } - } + } - private fun addInputType(typeName: String, inputName: String = "_${typeName}Input", relevantFields: List): String { - if (typeDefinitionRegistry.getType(inputName).isPresent) { - return inputName - } - val inputType = InputObjectTypeDefinition.newInputObjectDefinition() - .name(inputName) - .inputValueDefinitions(BaseDataFetcher.getInputValueDefinitions(relevantFields) { true }) - .build() - typeDefinitionRegistry.add(inputType) + private fun addInputType(typeDefinitionRegistry: TypeDefinitionRegistry, inputName: String, relevantFields: List): String { + if (typeDefinitionRegistry.getType(inputName).isPresent) { return inputName } + val inputType = InputObjectTypeDefinition.newInputObjectDefinition() + .name(inputName) + .inputValueDefinitions(relevantFields.map { + InputValueDefinition.newInputValueDefinition() + .name(it.name) + .type(it.type) + .build() + }) + .build() + typeDefinitionRegistry.add(inputType) + return inputName + } - private fun addOrdering(typeName: String, relevantFields: List): String { - val orderingName = "_${typeName}Ordering" - if (typeDefinitionRegistry.getType(orderingName).isPresent) { - return orderingName + @JvmStatic + fun mergeNeo4jEnhancements(builder: GraphQLSchema.Builder) { + val schemaParser = SchemaParser() + val directivesSdl = javaClass.getResource("/neo4j.graphql").readText() + val typeDefinitionRegistry = schemaParser.parse(directivesSdl) + val wiring = RuntimeWiring.newRuntimeWiring().scalar(DynamicProperties.INSTANCE).build() + val schema = SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, wiring) + schema.directives.forEach { builder.additionalDirective(it) } + schema.additionalTypes.forEach { builder.additionalType(it) } + neo4jTypeDefinitions + .forEach { + val type = (schema.getType(it.typeDefinition) as? GraphQLObjectType) + ?: throw IllegalStateException("type ${it.typeDefinition} not found") + + builder.additionalType(getInputType(it.inputDefinition, type.fieldDefinitions)) } - val ordering = EnumTypeDefinition.newEnumTypeDefinition() - .name(orderingName) - .enumValueDefinitions(relevantFields.flatMap { fd -> - listOf("_asc", "_desc") - .map { EnumValueDefinition.newEnumValueDefinition().name(fd.name + it).build() } - }) - .build() - typeDefinitionRegistry.add(ordering) - return orderingName - } + } - private fun addFilterType(name: String?, fieldArgs: List, handled: MutableSet = mutableSetOf()): String { - val filterName = "_${name}Filter" - if (typeDefinitionRegistry.getType(filterName).isPresent || handled.contains(filterName)) { - return filterName - } - handled.add(filterName) - val builder = InputObjectTypeDefinition.newInputObjectDefinition() - .name(filterName) - listOf("AND", "OR", "NOT") - .forEach { builder.inputValueDefinition(BaseDataFetcher.input(it, ListType(NonNullType(TypeName(filterName))))) } - fieldArgs - .filter { it.dynamicPrefix(metaProvider) == null } // TODO currently we do not support filtering on dynamic properties - .forEach { field -> - val typeDefinition = typeDefinitionRegistry.getType(field.type).orElse(null) - val type = if (field.type.isScalar() || typeDefinition is EnumTypeDefinition || field.isNeo4jType()) { - field.type.inner().inputType() - } else { - val objectName = field.type.name() - val subFilterName = addFilterType(objectName, - metaProvider.getNodeType(objectName) - ?.fieldDefinitions() - ?: throw IllegalArgumentException("type $objectName not found"), - handled) - TypeName(subFilterName) - } - Operators.forType(field.type, typeDefinition) - .forEach { op -> - val filterType = if (op.list) { - ListType(type) - } else { - type - } - builder.inputValueDefinition(BaseDataFetcher.input(op.fieldName(field.name), filterType)) - } - } - typeDefinitionRegistry.add(builder.build()) - return filterName + private fun getInputType(inputName: String, relevantFields: List): GraphQLInputObjectType { + return GraphQLInputObjectType.newInputObject() + .name(inputName) + .fields(getInputValueDefinitions(relevantFields)) + .build() + } + + private fun getInputValueDefinitions(relevantFields: List): List { + return relevantFields.map { + val type = (it.type as? GraphQLNonNull)?.wrappedType ?: it.type + GraphQLInputObjectField + .newInputObjectField() + .name(it.name) + .type(type as? GraphQLInputType + ?: throw IllegalArgumentException("${type.name} is not allowed for input")) + .build() } } } \ No newline at end of file diff --git a/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt b/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt index b661ece1..cf1de8da 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt @@ -1,33 +1,36 @@ package org.neo4j.graphql.handler -import graphql.language.* +import graphql.language.Argument +import graphql.language.ArrayValue +import graphql.language.Field +import graphql.language.ObjectValue import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLFieldsContainer import org.neo4j.graphql.* import org.neo4j.graphql.handler.projection.ProjectionBase abstract class BaseDataFetcher( - val type: NodeFacade, - val fieldDefinition: FieldDefinition, - metaProvider: MetaProvider -) : ProjectionBase(metaProvider), DataFetcher { + val type: GraphQLFieldsContainer, + val fieldDefinition: GraphQLFieldDefinition +) : ProjectionBase(), DataFetcher { - val propertyFields: MutableMap>) -> List?> = mutableMapOf() - val defaultFields: MutableMap>> = mutableMapOf() + val propertyFields: MutableMap List?> = mutableMapOf() + val defaultFields: MutableMap = mutableMapOf() init { - val fieldsOfType = type.fieldDefinitions().map { it.name to it }.toMap() fieldDefinition - .inputValueDefinitions + .arguments .filterNot { listOf(FIRST, OFFSET, ORDER_BY, NATIVE_ID).contains(it.name) } - .mapNotNull { - if (it.defaultValue != null) { - defaultFields[it.name] = it.defaultValue + .onEach { arg -> + if (arg.defaultValue != null) { + defaultFields[arg.name] = arg.defaultValue } - fieldsOfType[it.name] } - .forEach { field: FieldDefinition -> - val dynamicPrefix = field.dynamicPrefix(metaProvider) + .mapNotNull { type.getFieldDefinition(it.name) } + .forEach { field -> + val dynamicPrefix = field.dynamicPrefix() propertyFields[field.name] = when { dynamicPrefix != null -> dynamicPrefixCallback(field, dynamicPrefix) field.isNeo4jType() -> neo4jTypeCallback(field) @@ -36,21 +39,21 @@ abstract class BaseDataFetcher( } } - private fun defaultCallback(field: FieldDefinition) = - { value: Value> -> - val propertyName = field.propertyDirectiveName() ?: field.name + private fun defaultCallback(field: GraphQLFieldDefinition) = + { value: Any -> + val propertyName = field.propertyName() listOf(Translator.CypherArgument(field.name, propertyName.quote(), value.toJavaValue())) } - private fun neo4jTypeCallback(field: FieldDefinition) = - { value: Value> -> + private fun neo4jTypeCallback(field: GraphQLFieldDefinition) = + { value: Any -> val (name, propertyName, converter) = Neo4jQueryConversion - .forMutation(value, field) + .forMutation(value, field) listOf(Translator.CypherArgument(name, propertyName, value.toJavaValue(), converter, propertyName)) } - private fun dynamicPrefixCallback(field: FieldDefinition, dynamicPrefix: String) = - { value: Value> -> + private fun dynamicPrefixCallback(field: GraphQLFieldDefinition, dynamicPrefix: String) = + { value: Any -> // maps each property of the map to the node (value as? ObjectValue)?.objectFields?.map { argField -> Translator.CypherArgument( @@ -84,7 +87,7 @@ abstract class BaseDataFetcher( ): Cypher - fun allLabels(): String = type.allLabels() + fun allLabels(): String = type.label(includeAll = true) fun label(includeAll: Boolean = false) = type.label(includeAll) @@ -111,7 +114,7 @@ abstract class BaseDataFetcher( val defaults = defaultFields .filter { !predicates.containsKey(it.key) } - .flatMap { propertyFields[it.key]?.invoke(it.value) ?: emptyList() } + .flatMap { (argName, defaultValue) -> propertyFields[argName]?.invoke(defaultValue) ?: emptyList() } return predicates.values.flatten() + defaults } @@ -120,7 +123,7 @@ abstract class BaseDataFetcher( variable: String, label: String?, idProperty: Argument?, - idField: FieldDefinition, + idField: GraphQLFieldDefinition, isRelation: Boolean, paramName: String? = idProperty?.let { paramName(variable, idProperty.name, idProperty.value) } ): Cypher { @@ -145,33 +148,5 @@ abstract class BaseDataFetcher( else -> Cypher.EMPTY } } - - fun input(name: String, type: Type<*>): InputValueDefinition { - return InputValueDefinition.newInputValueDefinition().name(name).type(type).build() - } - - fun createFieldDefinition( - prefix: String, - typeName: String, - scalarFields: List, - nullableResult: Boolean, - forceOptionalProvider: (field: FieldDefinition) -> Boolean = { false } - ): FieldDefinition.Builder { - var type: Type<*> = TypeName(typeName) - if (!nullableResult) { - type = NonNullType(type) - } - return FieldDefinition.newFieldDefinition() - .name("$prefix$typeName") - .inputValueDefinitions(getInputValueDefinitions(scalarFields, forceOptionalProvider)) - .type(type) - } - - fun getInputValueDefinitions( - relevantFields: List, - forceOptionalProvider: (field: FieldDefinition) -> Boolean): List { - return relevantFields - .map { input(it.name, if (forceOptionalProvider.invoke(it)) it.type.inputType().optional() else it.type.inputType()) } - } } } \ No newline at end of file diff --git a/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt index 100d1ee7..21973cf5 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt @@ -1,25 +1,72 @@ package org.neo4j.graphql.handler import graphql.language.Field -import graphql.language.FieldDefinition -import graphql.schema.DataFetchingEnvironment +import graphql.schema.* import org.neo4j.graphql.* class CreateTypeHandler private constructor( - type: NodeFacade, - fieldDefinition: FieldDefinition, - metaProvider: MetaProvider -) : BaseDataFetcher(type, fieldDefinition, metaProvider) { - - companion object { - fun build(type: ObjectDefinitionNodeFacade, metaProvider: MetaProvider): CreateTypeHandler? { - val relevantFields = type.relevantFields() - if (relevantFields.isEmpty()) { + type: GraphQLFieldsContainer, + fieldDefinition: GraphQLFieldDefinition +) : BaseDataFetcher(type, fieldDefinition) { + + class Factory(schemaConfig: SchemaConfig) : AugmentationHandler(schemaConfig) { + override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { + if (!canHandle(type)) { + return + } + val relevantFields = getRelevantFields(type) + val fieldDefinition = buildingEnv + .buildFieldDefinition("create", type, relevantFields, nullableResult = false) + .build() + + buildingEnv.addOperation(MUTATION, fieldDefinition) + } + + override fun createDataFetcher(rootType: GraphQLObjectType, fieldDefinition: GraphQLFieldDefinition): DataFetcher? { + if (rootType.name != MUTATION){ return null } - val fieldDefinition = createFieldDefinition("create", type.name(), relevantFields.filter { !it.isNativeId() }, false).build() - return CreateTypeHandler(type, fieldDefinition, metaProvider) + if (fieldDefinition.cypherDirective() != null) { + return null + } + val type = fieldDefinition.type.inner() as? GraphQLObjectType + ?: return null + if (!canHandle(type)) { + return null + } + return when { + fieldDefinition.name == "create${type.name}" -> CreateTypeHandler(type, fieldDefinition) + else -> null + } } + + private fun getRelevantFields(type: GraphQLFieldsContainer): List { + return type + .relevantFields() + .filter { !it.isNativeId() } + } + + private fun canHandle(type: GraphQLFieldsContainer): Boolean { + val typeName = type.name + if (!schemaConfig.mutation.enabled || schemaConfig.mutation.exclude.contains(typeName)) { + return false + } + if (type !is GraphQLObjectType) { + return false + } + if ((type as GraphQLDirectiveContainer).isRelationType()) { + // relations are handled by the CreateRelationTypeHandler + return false + } + + if (getRelevantFields(type).isEmpty()) { + // nothing to create + // TODO or should we support just creating empty nodes? + return false + } + return true + } + } override fun generateCypher(variable: String, field: Field, projectionProvider: () -> Cypher, env: DataFetchingEnvironment): Cypher { diff --git a/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt index 9d5abd3b..cc751090 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt @@ -1,19 +1,33 @@ package org.neo4j.graphql.handler import graphql.language.Field -import graphql.language.FieldDefinition -import graphql.schema.DataFetchingEnvironment -import org.neo4j.graphql.Cypher -import org.neo4j.graphql.MetaProvider -import org.neo4j.graphql.NodeFacade +import graphql.schema.* +import org.neo4j.graphql.* class CypherDirectiveHandler( - type: NodeFacade, + type: GraphQLFieldsContainer, private val isQuery: Boolean, private val cypherDirective: Cypher, - fieldDefinition: FieldDefinition, - metaProvider: MetaProvider) - : BaseDataFetcher(type, fieldDefinition, metaProvider) { + fieldDefinition: GraphQLFieldDefinition) + : BaseDataFetcher(type, fieldDefinition) { + + class Factory(schemaConfig: SchemaConfig) : AugmentationHandler(schemaConfig) { + override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { + return + } + + override fun createDataFetcher(rootType: GraphQLObjectType, fieldDefinition: GraphQLFieldDefinition): DataFetcher? { + val cypherDirective = fieldDefinition.cypherDirective() + if (cypherDirective == null) { + return null + } + // TODO cypher directives can also return scalars + val type = fieldDefinition.type.inner() as? GraphQLFieldsContainer + ?: return null + val isQuery = rootType.name == QUERY + return CypherDirectiveHandler(type, isQuery, cypherDirective, fieldDefinition) + } + } override fun generateCypher(variable: String, field: Field, projectionProvider: () -> Cypher, env: DataFetchingEnvironment): Cypher { val mapProjection = projectionProvider.invoke() diff --git a/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt index 39eeab1c..96016c3c 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt @@ -1,37 +1,59 @@ package org.neo4j.graphql.handler -import graphql.language.Description import graphql.language.Field -import graphql.language.FieldDefinition -import graphql.language.TypeName -import graphql.schema.DataFetchingEnvironment -import org.neo4j.graphql.Cypher -import org.neo4j.graphql.MetaProvider -import org.neo4j.graphql.NodeFacade -import org.neo4j.graphql.isID +import graphql.schema.* +import org.neo4j.graphql.* class DeleteHandler private constructor( - type: NodeFacade, - private val idField: FieldDefinition, - fieldDefinition: FieldDefinition, - metaProvider: MetaProvider, + type: GraphQLFieldsContainer, + private val idField: GraphQLFieldDefinition, + fieldDefinition: GraphQLFieldDefinition, private val isRelation: Boolean = type.isRelationType() -) : BaseDataFetcher(type, fieldDefinition, metaProvider) { +) : BaseDataFetcher(type, fieldDefinition) { - companion object { - fun build(type: NodeFacade, metaProvider: MetaProvider): DeleteHandler? { - val idField = type.fieldDefinitions().find { it.isID() } ?: return null - val relevantFields = type.relevantFields() - if (relevantFields.isEmpty()) { - return null + class Factory(schemaConfig: SchemaConfig) : AugmentationHandler(schemaConfig) { + override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { + if (!canHandle(type)) { + return } - val typeName = type.name() + val idField = type.fieldDefinitions.find { it.isID() } ?: return - val fieldDefinition = createFieldDefinition("delete", typeName, listOf(idField), true) - .description(Description("Deletes $typeName and returns its ID on successful deletion", null, false)) - .type(TypeName(typeName)) + val fieldDefinition = buildingEnv + .buildFieldDefinition("delete", type, listOf(idField), nullableResult = true) + .description("Deletes ${type.name} and returns its ID on successful deletion") + .type(type.ref() as GraphQLOutputType) .build() - return DeleteHandler(type, idField, fieldDefinition, metaProvider) + buildingEnv.addOperation(MUTATION, fieldDefinition) + } + + override fun createDataFetcher(rootType: GraphQLObjectType, fieldDefinition: GraphQLFieldDefinition): DataFetcher? { + if (rootType.name != MUTATION){ + return null + } + if (fieldDefinition.cypherDirective() != null) { + return null + } + val type = fieldDefinition.type as? GraphQLFieldsContainer + ?: return null + if (!canHandle(type)) { + return null + } + val idField = type.fieldDefinitions.find { it.isID() } ?: return null + return when { + fieldDefinition.name == "delete${type.name}" -> DeleteHandler(type, idField, fieldDefinition) + else -> null + } + } + + private fun canHandle(type: GraphQLFieldsContainer): Boolean { + val typeName = type.name + if (!schemaConfig.mutation.enabled || schemaConfig.mutation.exclude.contains(typeName)) { + return false + } + if (type.fieldDefinitions.find { it.isID() } == null) { + return false + } + return true } } diff --git a/src/main/kotlin/org/neo4j/graphql/handler/MergeOrUpdateHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/MergeOrUpdateHandler.kt index 5183978c..59b121fc 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/MergeOrUpdateHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/MergeOrUpdateHandler.kt @@ -1,29 +1,68 @@ package org.neo4j.graphql.handler import graphql.language.Field -import graphql.language.FieldDefinition -import graphql.schema.DataFetchingEnvironment +import graphql.schema.* import org.neo4j.graphql.* class MergeOrUpdateHandler private constructor( - type: NodeFacade, + type: GraphQLFieldsContainer, private val merge: Boolean, - private val idField: FieldDefinition, - fieldDefinition: FieldDefinition, - metaProvider: MetaProvider, + private val idField: GraphQLFieldDefinition, + fieldDefinition: GraphQLFieldDefinition, private val isRelation: Boolean = type.isRelationType() -) : BaseDataFetcher(type, fieldDefinition, metaProvider) { +) : BaseDataFetcher(type, fieldDefinition) { + + class Factory(schemaConfig: SchemaConfig) : AugmentationHandler(schemaConfig) { + override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { + if (!canHandle(type)) { + return + } - companion object { - fun build(type: NodeFacade, merge: Boolean, metaProvider: MetaProvider): MergeOrUpdateHandler? { - val idField = type.fieldDefinitions().find { it.isID() } ?: return null val relevantFields = type.relevantFields() - if (relevantFields.none { !it.isID() }) { - // nothing to update (except ID) + val mergeField = buildingEnv + .buildFieldDefinition("merge", type, relevantFields, nullableResult = false) + .build() + buildingEnv.addOperation(MUTATION, mergeField) + + val updateField = buildingEnv + .buildFieldDefinition("update", type, relevantFields, nullableResult = true) + .build() + buildingEnv.addOperation(MUTATION, updateField) + } + + override fun createDataFetcher(rootType: GraphQLObjectType, fieldDefinition: GraphQLFieldDefinition): DataFetcher? { + if (rootType.name != MUTATION){ + return null + } + if (fieldDefinition.cypherDirective() != null) { return null } - val fieldDefinition = createFieldDefinition(if (merge) "merge" else "update", type.name(), relevantFields, !merge).build() - return MergeOrUpdateHandler(type, merge, idField, fieldDefinition, metaProvider) + val type = fieldDefinition.type.inner() as? GraphQLFieldsContainer + ?: return null + if (!canHandle(type)) { + return null + } + val idField = type.fieldDefinitions.find { it.isID() } ?: return null + return when { + fieldDefinition.name == "merge${type.name}" -> MergeOrUpdateHandler(type, true, idField, fieldDefinition) + fieldDefinition.name == "update${type.name}" -> MergeOrUpdateHandler(type, false, idField, fieldDefinition) + else -> null + } + } + + private fun canHandle(type: GraphQLFieldsContainer): Boolean { + val typeName = type.name + if (!schemaConfig.mutation.enabled || schemaConfig.mutation.exclude.contains(typeName)) { + return false + } + if (type.fieldDefinitions.find { it.isID() } == null) { + return false + } + if (type.relevantFields().none { !it.isID() }) { + // nothing to update (except ID) + return false + } + return true } } diff --git a/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt index b124a875..7b869ddd 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt @@ -1,47 +1,74 @@ package org.neo4j.graphql.handler -import graphql.language.* -import graphql.schema.DataFetchingEnvironment +import graphql.Scalars +import graphql.language.Field +import graphql.schema.* import org.neo4j.graphql.* class QueryHandler private constructor( - type: NodeFacade, - fieldDefinition: FieldDefinition, - metaProvider: MetaProvider) - : BaseDataFetcher(type, fieldDefinition, metaProvider) { + type: GraphQLFieldsContainer, + fieldDefinition: GraphQLFieldDefinition) + : BaseDataFetcher(type, fieldDefinition) { - companion object { - fun build(type: NodeFacade, filterTypeName: String, orderingTypename: String, metaProvider: MetaProvider): BaseDataFetcher? { - val typeName = type.name() - val relevantFields = type - .relevantFields() - .filter { it.dynamicPrefix(metaProvider) == null } // TODO currently we do not support filtering on dynamic properties + class Factory(schemaConfig: SchemaConfig) : AugmentationHandler(schemaConfig) { + override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { + if (!canHandle(type)) { + return + } + val typeName = type.name + val relevantFields = getRelevantFields(type) - val fieldDefinition = FieldDefinition + // TODO not just generate the input type but use it as well + buildingEnv.addInputType("_${typeName}Input", type.relevantFields()) + val filterTypeName = buildingEnv.addFilterType(type) + val orderingTypeName = buildingEnv.addOrdering(type) + val builder = GraphQLFieldDefinition .newFieldDefinition() .name(typeName.decapitalize()) - .inputValueDefinitions(getInputValueDefinitions(relevantFields) { true }) - .inputValueDefinition(input(FILTER, TypeName(filterTypeName))) - .inputValueDefinition(input(ORDER_BY, TypeName(orderingTypename))) - .inputValueDefinition(input(FIRST, TypeName("Int"))) - .inputValueDefinition(input(OFFSET, TypeName("Int"))) - .type(NonNullType(ListType(NonNullType(TypeName(typeName))))) - .build() - return QueryHandler(type, fieldDefinition, metaProvider) + .arguments(buildingEnv.getInputValueDefinitions(relevantFields) { true }) + .argument(input(FILTER, GraphQLTypeReference(filterTypeName))) + .argument(input(FIRST, Scalars.GraphQLInt)) + .argument(input(OFFSET, Scalars.GraphQLInt)) + .type(GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLTypeReference(type.name))))) + if (orderingTypeName != null) { + builder.argument(input(ORDER_BY, GraphQLTypeReference(orderingTypeName))) + } + val def = builder.build() + buildingEnv.addOperation(QUERY, def) } - fun build(fieldDefinition: FieldDefinition, - isQuery: Boolean, - metaProvider: MetaProvider, - type: NodeFacade = metaProvider.getNodeType(fieldDefinition.type.name()) - ?: throw IllegalStateException("cannot find type " + fieldDefinition.type.name()) - ): BaseDataFetcher? { + override fun createDataFetcher(rootType: GraphQLObjectType, fieldDefinition: GraphQLFieldDefinition): DataFetcher? { + if (rootType.name != QUERY) { + return null + } val cypherDirective = fieldDefinition.cypherDirective() - return when { - cypherDirective != null -> CypherDirectiveHandler(type, isQuery, cypherDirective, fieldDefinition, metaProvider) - isQuery -> QueryHandler(type, fieldDefinition, metaProvider) - else -> null + if (cypherDirective != null) { + return null + } + val type = fieldDefinition.type.inner() as? GraphQLFieldsContainer + ?: return null + if (!canHandle(type)) { + return null + } + return QueryHandler(type, fieldDefinition) + } + + private fun canHandle(type: GraphQLFieldsContainer): Boolean { + val typeName = type.innerName() + if (!schemaConfig.query.enabled || schemaConfig.query.exclude.contains(typeName)) { + return false } + if (getRelevantFields(type).isEmpty()) { + return false + } + return true + } + + private fun getRelevantFields(type: GraphQLFieldsContainer): List { + val relevantFields = type + .relevantFields() + .filter { it.dynamicPrefix() == null } // TODO currently we do not support filtering on dynamic properties + return relevantFields } } 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 9af88cec..00d141de 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt @@ -1,10 +1,10 @@ package org.neo4j.graphql.handler.projection import graphql.language.* -import graphql.schema.DataFetchingEnvironment +import graphql.schema.* import org.neo4j.graphql.* -open class ProjectionBase(val metaProvider: MetaProvider) { +open class ProjectionBase { companion object { const val NATIVE_ID = "_id" const val ORDER_BY = "orderBy" @@ -32,14 +32,14 @@ open class ProjectionBase(val metaProvider: MetaProvider) { .joinToString(", ") } - fun where(variable: String, fieldDefinition: FieldDefinition, type: NodeFacade, arguments: List, field: Field): Cypher { + fun where(variable: String, fieldDefinition: GraphQLFieldDefinition, type: GraphQLFieldsContainer, arguments: List, field: Field): Cypher { val all = preparePredicateArguments(fieldDefinition, arguments) .filterNot { listOf(FIRST, OFFSET, ORDER_BY).contains(it.name) } .plus(predicateForNeo4jTypes(type, field)) val (filterExpressions, filterParams) = filterExpressions(all.find { it.name == FILTER }?.value, type) - .map { it.toExpression(variable, metaProvider) } + .map { it.toExpression(variable) } .let { expressions -> expressions.map { it.query } to expressions.fold(emptyMap()) { res, exp -> res + exp.params } } @@ -55,8 +55,8 @@ open class ProjectionBase(val metaProvider: MetaProvider) { return Cypher(expression, (filterParams + noFilter.map { (k, _, v) -> paramName(variable, k, v) to v }.toMap())) } - private fun predicateForNeo4jTypes(type: NodeFacade, field: Field): Collection = - type.fieldDefinitions() + private fun predicateForNeo4jTypes(type: GraphQLFieldsContainer, field: Field): Collection = + type.fieldDefinitions .filter { it.isNeo4jType() } .map { neo4jType -> neo4jType to field.selectionSet.selections @@ -65,77 +65,74 @@ open class ProjectionBase(val metaProvider: MetaProvider) { } .groupBy({ it.first }, { it.second }) // create a map of > so we group the data by the type .mapValues { it.value.flatten() } - .flatMap { entry -> + .flatMap { (fieldDefinition , selection) -> // for each FieldWithNeo4jType of type query we create the where condition - val typeName = entry.key.type.name() - val fields = entry.value - val neo4jType = metaProvider.getNodeType(typeName) - ?: throw IllegalArgumentException("type $typeName not defined in schema") - fields.flatMap { f -> - argumentsToMap(f.arguments, neo4jType) + val neo4jType = fieldDefinition.type.getInnerFieldsContainer() + selection.flatMap { field -> + argumentsToMap(field.arguments, neo4jType) .values .map { arg -> val (nameSuffix, propertyNameSuffix, innerNeo4jConstruct) = Neo4jQueryConversion - .forQuery(arg, f, neo4jType) + .forQuery(arg, field, neo4jType) Translator.CypherArgument(nameSuffix, propertyNameSuffix, arg.value, innerNeo4jConstruct) } } } - private fun argumentsToMap(arguments: List, resultObjectType: NodeFacade? = null): Map { + private fun argumentsToMap(arguments: List, resultObjectType: GraphQLFieldsContainer? = null): Map { return arguments .map { argument -> - val propertyName = (resultObjectType?.getFieldDefinition(argument.name)?.propertyDirectiveName() + val propertyName = (resultObjectType?.getFieldDefinition(argument.name)?.propertyName() ?: argument.name).quote() argument.name to Translator.CypherArgument(argument.name.quote(), propertyName, argument.value.toJavaValue()) } .toMap() } - private fun filterExpressions(value: Any?, type: NodeFacade): List { + private fun filterExpressions(value: Any?, type: GraphQLFieldsContainer): List { // todo variable/parameter return if (value is Map<*, *>) { - CompoundPredicate(value.map { (k, v) -> toExpression(k.toString(), v, type, metaProvider) }, "AND").parts + CompoundPredicate(value.map { (k, v) -> toExpression(k.toString(), v, type) }, "AND").parts } else emptyList() } fun propertyArguments(queryField: Field) = queryField.arguments.filterNot { listOf(FIRST, OFFSET, ORDER_BY).contains(it.name) } - private fun preparePredicateArguments(field: FieldDefinition, arguments: List): List { + private fun preparePredicateArguments(field: GraphQLFieldDefinition, arguments: List): List { if (arguments.isEmpty()) return emptyList() - val resultObjectType = metaProvider.getNodeType(field.type.name()) - ?: throw IllegalArgumentException("Result of ${field.name} i.e. ${field.type.name()} is neither and object type nor interface in the schema") + val resultObjectType = field.type.getInnerFieldsContainer() val predicates = arguments.map { val fieldDefinition = resultObjectType.getFieldDefinition(it.name) - val dynamicPrefix = fieldDefinition?.dynamicPrefix(metaProvider) + val dynamicPrefix = fieldDefinition?.dynamicPrefix() val result = mutableListOf() if (dynamicPrefix != null && it.value is ObjectValue) { for (argField in (it.value as ObjectValue).objectFields) { result += Translator.CypherArgument(it.name + argField.name.capitalize(), dynamicPrefix + argField.name, argField.value.toJavaValue()) + result += Translator.CypherArgument(it.name + argField.name.capitalize(), dynamicPrefix + argField.name, argField.value.toJavaValue()) } } else { result += Translator.CypherArgument(it.name, - (fieldDefinition?.propertyDirectiveName() ?: it.name).quote(), + (fieldDefinition?.propertyName() ?: it.name).quote(), it.value.toJavaValue()) } it.name to result }.toMap() - val defaults = field.inputValueDefinitions.filter { it.defaultValue?.toJavaValue() != null && !predicates.containsKey(it.name) } + val defaults = field.arguments.filter { it.defaultValue?.toJavaValue() != null && !predicates.containsKey(it.name) } .map { Translator.CypherArgument(it.name, it.name, it.defaultValue?.toJavaValue()) } return predicates.values.flatten() + defaults } - private fun prepareFieldArguments(field: FieldDefinition, arguments: List): List { + private fun prepareFieldArguments(field: GraphQLFieldDefinition, arguments: List): List { // if (arguments.isEmpty()) return emptyList() val predicates = arguments.map { it.name to Translator.CypherArgument(it.name, it.name, it.value.toJavaValue()) }.toMap() - val defaults = field.inputValueDefinitions.filter { it.defaultValue != null && !predicates.containsKey(it.name) } + val defaults = field.arguments.filter { it.defaultValue != null && !predicates.containsKey(it.name) } .map { Translator.CypherArgument(it.name, it.name, it.defaultValue.toJavaValue()) } return predicates.values + defaults } - fun projectFields(variable: String, field: Field, nodeType: NodeFacade, env: DataFetchingEnvironment, variableSuffix: String?): Cypher { + fun projectFields(variable: String, field: Field, nodeType: GraphQLFieldsContainer, env: DataFetchingEnvironment, variableSuffix: String?): Cypher { val queries = projectSelectionSet(variable, field.selectionSet, nodeType, env, variableSuffix) val projection = queries .map { it.query } @@ -146,7 +143,7 @@ open class ProjectionBase(val metaProvider: MetaProvider) { return Cypher("$variable $projection", params) } - private fun projectSelectionSet(variable: String, selectionSet: SelectionSet, nodeType: NodeFacade, env: DataFetchingEnvironment, variableSuffix: String?): List { + private fun projectSelectionSet(variable: String, selectionSet: SelectionSet, nodeType: GraphQLFieldsContainer, env: DataFetchingEnvironment, variableSuffix: String?): List { // TODO just render fragments on valid types (Labels) by using cypher like this: // apoc.map.mergeList([ // a{.name}, @@ -164,7 +161,7 @@ open class ProjectionBase(val metaProvider: MetaProvider) { else -> emptyList() } } - if (nodeType is InterfaceDefinitionNodeFacade + if (nodeType is GraphQLInterfaceType && !hasTypeName && (env.getLocalContext() as? QueryContext)?.queryTypeOfInterfaces == true ) { @@ -174,21 +171,21 @@ open class ProjectionBase(val metaProvider: MetaProvider) { return projections } - private fun projectField(variable: String, field: Field, type: NodeFacade, env: DataFetchingEnvironment, variableSuffix: String?): Cypher { + private fun projectField(variable: String, field: Field, type: GraphQLFieldsContainer, env: DataFetchingEnvironment, variableSuffix: String?): Cypher { if (field.name == TYPE_NAME) { return if (type.isRelationType()) { - Cypher("${field.aliasOrName()}: '${type.name()}'") + Cypher("${field.aliasOrName()}: '${type.name}'") } else { val paramName = paramName(variable, "validTypes", null) - val validTypeLabels = metaProvider.getValidTypeLabels(type) + val validTypeLabels = type.getValidTypeLabels(env.graphQLSchema) 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()}") + ?: throw IllegalStateException("No field ${field.name} in ${type.name}") val cypherDirective = fieldDefinition.cypherDirective() - val isObjectField = metaProvider.getNodeType(fieldDefinition.type.name()) != null + val isObjectField = fieldDefinition.type.inner() is GraphQLFieldsContainer return cypherDirective?.let { val directive = cypherDirective(variable, fieldDefinition, field, it, listOf(Translator.CypherArgument("this", "this", variable))) if (isObjectField) { @@ -199,7 +196,7 @@ open class ProjectionBase(val metaProvider: MetaProvider) { } ?: when { isObjectField -> { - val patternComprehensions = if (fieldDefinition.type.isNeo4jType()) { + val patternComprehensions = if (fieldDefinition.isNeo4jType()) { projectNeo4jObjectType(variable, field) } else { projectRelationship(variable, field, fieldDefinition, type, env, variableSuffix) @@ -208,11 +205,11 @@ open class ProjectionBase(val metaProvider: MetaProvider) { } fieldDefinition.isNativeId() -> Cypher("${field.aliasOrName()}:ID($variable)") else -> { - val dynamicPrefix = fieldDefinition.dynamicPrefix(metaProvider) + val dynamicPrefix = fieldDefinition.dynamicPrefix() when { dynamicPrefix != null -> Cypher("${field.aliasOrName()}:apoc.map.fromPairs([key IN keys($variable) WHERE key STARTS WITH '$dynamicPrefix'| [substring(key,${dynamicPrefix.length}), $variable[key]]])") - field.aliasOrName() == field.propertyName(fieldDefinition) -> Cypher("." + field.propertyName(fieldDefinition)) - else -> Cypher(field.aliasOrName() + ":" + variable + "." + field.propertyName(fieldDefinition)) + field.aliasOrName() == fieldDefinition.propertyName() -> Cypher("." + fieldDefinition.propertyName().quote()) + else -> Cypher(field.aliasOrName() + ":" + variable + "." + fieldDefinition.propertyName().quote()) } } } @@ -233,13 +230,13 @@ open class ProjectionBase(val metaProvider: MetaProvider) { return Cypher(" { $fieldProjection }") } - fun cypherDirective(variable: String, fieldDefinition: FieldDefinition, field: Field, cypherDirective: Cypher, additionalArgs: List): Cypher { + fun cypherDirective(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: Cypher, additionalArgs: List): Cypher { val suffix = if (fieldDefinition.type.isList()) "Many" else "Single" val (query, args) = cypherDirectiveQuery(variable, fieldDefinition, field, cypherDirective, additionalArgs) return Cypher("apoc.cypher.runFirstColumn$suffix($query)", args) } - fun cypherDirectiveQuery(variable: String, fieldDefinition: FieldDefinition, field: Field, cypherDirective: Cypher, additionalArgs: List): Cypher { + fun cypherDirectiveQuery(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: Cypher, additionalArgs: List): Cypher { val args = additionalArgs + prepareFieldArguments(fieldDefinition, field.arguments) val argParams = args.map { '$' + it.name + " AS " + it.name }.joinNonEmpty(", ") val query = (if (argParams.isEmpty()) "" else "WITH $argParams ") + cypherDirective.escapedQuery() @@ -256,22 +253,22 @@ open class ProjectionBase(val metaProvider: MetaProvider) { projectFragment(fragment.typeCondition.name, variable, env, variableSuffix, fragment.selectionSet) private fun projectFragment(fragmentTypeName: String?, variable: String, env: DataFetchingEnvironment, variableSuffix: String?, selectionSet: SelectionSet): List { - val fragmentType = metaProvider.getNodeType(fragmentTypeName) ?: return emptyList() + val fragmentType = env.graphQLSchema.getType(fragmentTypeName) as? GraphQLFieldsContainer ?: return emptyList() // these are the nested fields of the fragment // it could be that we have to adapt the variable name too, and perhaps add some kind of rename return projectSelectionSet(variable, selectionSet, fragmentType, env, variableSuffix) } - private fun projectRelationship(variable: String, field: Field, fieldDefinition: FieldDefinition, parent: NodeFacade, env: DataFetchingEnvironment, variableSuffix: String?): Cypher { - return when (parent.getDirective(DirectiveConstants.RELATION) != null) { + private fun projectRelationship(variable: String, field: Field, fieldDefinition: GraphQLFieldDefinition, parent: GraphQLFieldsContainer, env: DataFetchingEnvironment, variableSuffix: String?): Cypher { + return when (parent.isRelationType()) { true -> projectRelationshipParent(variable, field, fieldDefinition, env, variableSuffix) else -> projectRichAndRegularRelationship(variable, field, fieldDefinition, parent, env) } } - private fun projectListComprehension(variable: String, field: Field, fieldDefinition: FieldDefinition, env: DataFetchingEnvironment, expression: Cypher, variableSuffix: String?): Cypher { - val fieldObjectType = metaProvider.getNodeType(fieldDefinition.type.name()) ?: return Cypher.EMPTY + private fun projectListComprehension(variable: String, field: Field, fieldDefinition: GraphQLFieldDefinition, env: DataFetchingEnvironment, expression: Cypher, variableSuffix: String?): Cypher { + val fieldObjectType = fieldDefinition.type.getInnerFieldsContainer() val fieldType = fieldDefinition.type val childVariable = variable + field.name.capitalize() @@ -285,28 +282,26 @@ open class ProjectionBase(val metaProvider: MetaProvider) { } - private fun relationshipInfoInCorrectDirection(fieldObjectType: NodeFacade, relInfo0: RelationshipInfo, parent: NodeFacade, relDirectiveField: RelationshipInfo?): RelationshipInfo { + private fun relationshipInfoInCorrectDirection(fieldObjectType: GraphQLFieldsContainer, relInfo0: RelationshipInfo, parent: GraphQLFieldsContainer, relDirectiveField: RelationshipInfo?): RelationshipInfo { val startField = fieldObjectType.getFieldDefinition(relInfo0.startField)!! val endField = fieldObjectType.getFieldDefinition(relInfo0.endField)!! - val startFieldTypeName = startField.type.inner().name() - val inverse = startFieldTypeName != parent.name() || startField.type.name() == endField.type.name() && relDirectiveField?.out != relInfo0.out + val startFieldTypeName = startField.type.inner().name + val inverse = startFieldTypeName != parent.name || startField.type.name == endField.type.name && relDirectiveField?.out != relInfo0.out return if (inverse) relInfo0.copy(out = relInfo0.out?.not(), startField = relInfo0.endField, endField = relInfo0.startField) else relInfo0 } - private fun projectRelationshipParent(variable: String, field: Field, fieldDefinition: FieldDefinition, env: DataFetchingEnvironment, variableSuffix: String?): Cypher { - val fieldObjectType = metaProvider.getNodeType(fieldDefinition.type.name()) ?: return Cypher.EMPTY + private fun projectRelationshipParent(variable: String, field: Field, fieldDefinition: GraphQLFieldDefinition, env: DataFetchingEnvironment, variableSuffix: String?): Cypher { + val fieldObjectType = fieldDefinition.type as? GraphQLFieldsContainer ?: return Cypher.EMPTY return projectFields(variable + (variableSuffix?.capitalize() ?: ""), field, fieldObjectType, env, variableSuffix) } - private fun projectRichAndRegularRelationship(variable: String, field: Field, fieldDefinition: FieldDefinition, parent: NodeFacade, env: DataFetchingEnvironment): Cypher { + private fun projectRichAndRegularRelationship(variable: String, field: Field, fieldDefinition: GraphQLFieldDefinition, parent: GraphQLFieldsContainer, env: DataFetchingEnvironment): Cypher { val fieldType = fieldDefinition.type - val fieldTypeName = fieldDefinition.type.name()!! - val nodeType = metaProvider.getNodeType(fieldTypeName) - ?: throw IllegalArgumentException("$fieldTypeName cannot be converted to a NodeGraphQlFacade") + val nodeType = fieldType.getInnerFieldsContainer() // todo combine both nestings if rel-entity - val relDirectiveObject = nodeType.getDirective(DirectiveConstants.RELATION)?.let { relDetails(nodeType, it) } + val relDirectiveObject = (nodeType as? GraphQLDirectiveContainer)?.getDirective(DirectiveConstants.RELATION)?.let { relDetails(nodeType, it) } val relDirectiveField = fieldDefinition.getDirective(DirectiveConstants.RELATION)?.let { relDetails(nodeType, it) } val (relInfo0, isRelFromType) = @@ -322,10 +317,10 @@ open class ProjectionBase(val metaProvider: MetaProvider) { val (endNodePattern, variableSuffix) = when { isRelFromType -> { - val label = nodeType.getFieldDefinition(relInfo.endField!!)!!.type.inner().name() + val label = nodeType.getFieldDefinition(relInfo.endField!!)!!.type.inner().name ("$childVariable${relInfo.endField.capitalize()}:$label" to relInfo.endField) } - else -> ("$childVariable:${nodeType.name()}" to null) + else -> ("$childVariable:${nodeType.name}" to null) } val relPattern = if (isRelFromType) "$childVariable:${relInfo.relType}" else ":${relInfo.relType}" @@ -339,8 +334,8 @@ open class ProjectionBase(val metaProvider: MetaProvider) { return Cypher(comprehension + slice.query, (where.params + fieldProjection.params + slice.params)) } - private fun relDetails(type: NodeFacade, relDirective: Directive) = - relDetails(type) { name, defaultValue -> metaProvider.getDirectiveArgument(relDirective, name, defaultValue) } + private fun relDetails(type: GraphQLFieldsContainer, relDirective: GraphQLDirective) = + relDetails(type) { name, defaultValue -> relDirective.getArgument(name, defaultValue) } class SkipLimit(variable: String, arguments: List, diff --git a/src/main/kotlin/org/neo4j/graphql/handler/relation/BaseRelationHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/relation/BaseRelationHandler.kt index 2a1a254a..b8f76a1b 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/relation/BaseRelationHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/relation/BaseRelationHandler.kt @@ -1,71 +1,128 @@ package org.neo4j.graphql.handler.relation -import graphql.language.* +import graphql.Scalars +import graphql.language.Argument +import graphql.schema.* import org.neo4j.graphql.* import org.neo4j.graphql.handler.BaseDataFetcher abstract class BaseRelationHandler( - type: NodeFacade, + type: GraphQLFieldsContainer, val relation: RelationshipInfo, private val startId: RelationshipInfo.RelatedField, private val endId: RelationshipInfo.RelatedField, - fieldDefinition: FieldDefinition, - metaProvider: MetaProvider) - : BaseDataFetcher(type, fieldDefinition, metaProvider) { + fieldDefinition: GraphQLFieldDefinition) + : BaseDataFetcher(type, fieldDefinition) { init { propertyFields.remove(startId.argumentName) propertyFields.remove(endId.argumentName) } - companion object { - @JvmStatic - protected fun build(prefix: String, source: ObjectTypeDefinition, - target: ObjectTypeDefinition, - metaProvider: MetaProvider, - nullableResult: Boolean, - handlerFactory: (sourceNodeType: NodeFacade, - relation: RelationshipInfo, - startIdField: RelationshipInfo.RelatedField, - endIdField: RelationshipInfo.RelatedField, - targetField: FieldDefinition, - fieldDefinitionBuilder: FieldDefinition.Builder) -> T): T? { - - val sourceTypeName = source.name - val targetField = source.getFieldByType(target.name) ?: return null + abstract class BaseRelationFactory(val prefix: String, schemaConfig: SchemaConfig) : AugmentationHandler(schemaConfig) { + protected fun buildFieldDefinition( + source: GraphQLFieldsContainer, + targetField: GraphQLFieldDefinition, + nullableResult: Boolean + ): GraphQLFieldDefinition.Builder? { + + val targetType = targetField.type.getInnerFieldsContainer(); val sourceIdField = source.fieldDefinitions.find { it.isID() } - val targetIdField = target.fieldDefinitions.find { it.isID() } + val targetIdField = targetType.fieldDefinitions.find { it.isID() } if (sourceIdField == null || targetIdField == null) { return null } val targetFieldName = targetField.name.capitalize() - val idType = NonNullType(TypeName("ID")) - val targetIDType = if (targetField.isList()) NonNullType(ListType(idType)) else idType + val idType = GraphQLNonNull(Scalars.GraphQLID) + val targetIDType = if (targetField.type.isList()) GraphQLNonNull(GraphQLList(idType)) else idType - val sourceNodeType = source.getNodeType()!! - val targetNodeType = target.getNodeType()!! - val relation = sourceNodeType.relationshipFor(targetField.name, metaProvider) - ?: throw IllegalArgumentException("could not resolve relation for " + source.name + "::" + targetField.name) - var type: Type<*> = TypeName(sourceTypeName) + var type: GraphQLOutputType = source if (!nullableResult) { - type = NonNullType(type) + type = GraphQLNonNull(type) } - val fieldDefinitionBuilder = FieldDefinition.newFieldDefinition() - .name("$prefix$sourceTypeName$targetFieldName") - .inputValueDefinition(input(sourceIdField.name, idType)) - .inputValueDefinition(input(targetField.name, targetIDType)) - .type(type) + return GraphQLFieldDefinition.newFieldDefinition() + .name("$prefix${source.name}$targetFieldName") + .argument(input(sourceIdField.name, idType)) + .argument(input(targetField.name, targetIDType)) + .type(type.ref() as GraphQLOutputType) + } - val startIdField = RelationshipInfo.RelatedField(sourceIdField.name, sourceIdField, sourceNodeType) - val endIdField = RelationshipInfo.RelatedField(targetField.name, targetIdField, targetNodeType) - return handlerFactory(sourceNodeType, relation, startIdField, endIdField, targetField, fieldDefinitionBuilder) + protected fun canHandleType(type: GraphQLFieldsContainer): Boolean { + if (type !is GraphQLObjectType) { + return false + } + if (!schemaConfig.mutation.enabled || schemaConfig.mutation.exclude.contains(type.name)) { + // TODO we do not mutate the node but the relation, I think this check should be different + return false + } + if (type.fieldDefinitions.find { it.isID() } == null) { + return false + } + return true } - } + protected fun canHandleField(targetField: GraphQLFieldDefinition): Boolean { + val type = targetField.type.inner() as? GraphQLObjectType ?: return false + if (targetField.getDirective(DirectiveConstants.RELATION) == null) { + return false + } + if (type.fieldDefinitions.find { it.isID() } == null) { + return false + } + return true + } + + final override fun createDataFetcher(rootType: GraphQLObjectType, fieldDefinition: GraphQLFieldDefinition): DataFetcher? { + if (rootType.name != MUTATION) { + return null + } + if (fieldDefinition.cypherDirective() != null) { + return null + } + val sourceType = fieldDefinition.type.inner() as? GraphQLFieldsContainer + ?: return null + if (!canHandleType(sourceType)) { + return null + } + + val p = "$prefix${sourceType.name}" + if (!fieldDefinition.name.startsWith(p)) { + return null + } + val targetField = fieldDefinition.name + .removePrefix(p) + .decapitalize() + .let { sourceType.getFieldDefinition(it) } + ?: return null + if (!canHandleField(targetField)) { + return null + } + val relation = sourceType.relationshipFor(targetField.name) ?: return null + + val targetType = targetField.type.getInnerFieldsContainer(); + val sourceIdField = sourceType.fieldDefinitions.find { it.isID() } + val targetIdField = targetType.fieldDefinitions.find { it.isID() } + if (sourceIdField == null || targetIdField == null) { + return null + } + val startIdField = RelationshipInfo.RelatedField(sourceIdField.name, sourceIdField, sourceType) + val endIdField = RelationshipInfo.RelatedField(targetField.name, targetIdField, targetType) + return createDataFetcher(sourceType, relation, startIdField, endIdField, fieldDefinition) + } + + abstract fun createDataFetcher( + sourceType: GraphQLFieldsContainer, + relation: RelationshipInfo, + startIdField: RelationshipInfo.RelatedField, + endIdField: RelationshipInfo.RelatedField, + fieldDefinition: GraphQLFieldDefinition + ): DataFetcher? + + } fun getRelationSelect(start: Boolean, arguments: Map): Cypher { val relFieldName: String diff --git a/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt index 5e635dab..0b6634b8 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt @@ -1,42 +1,53 @@ package org.neo4j.graphql.handler.relation import graphql.language.Field -import graphql.language.FieldDefinition -import graphql.language.ObjectTypeDefinition -import graphql.schema.DataFetchingEnvironment +import graphql.schema.* import org.neo4j.graphql.* class CreateRelationHandler private constructor( - type: NodeFacade, + type: GraphQLFieldsContainer, relation: RelationshipInfo, startId: RelationshipInfo.RelatedField, endId: RelationshipInfo.RelatedField, - fieldDefinition: FieldDefinition, - metaProvider: MetaProvider) - : BaseRelationHandler(type, relation, startId, endId, fieldDefinition, metaProvider) { - - companion object { - fun build(source: ObjectTypeDefinition, - target: ObjectTypeDefinition, - relationTypes: Map?, - metaProvider: MetaProvider): CreateRelationHandler? { - - return build("add", source, target, metaProvider, false) { sourceNodeType, relation, startIdField, endIdField, targetField, fieldDefinitionBuilder -> - - val relationType = targetField - .getDirective(DirectiveConstants.RELATION) - ?.getArgument(DirectiveConstants.RELATION_NAME) - ?.value?.toJavaValue()?.toString() - .let { relationTypes?.get(it) } - - relationType - ?.fieldDefinitions - ?.filter { it.type.isScalar() && !it.isID() } - ?.forEach { fieldDefinitionBuilder.inputValueDefinition(input(it.name, it.type)) } - - CreateRelationHandler(sourceNodeType, relation, startIdField, endIdField, fieldDefinitionBuilder.build(), metaProvider) + fieldDefinition: GraphQLFieldDefinition) + : BaseRelationHandler(type, relation, startId, endId, fieldDefinition) { + + class Factory(schemaConfig: SchemaConfig) : BaseRelationFactory("add", schemaConfig) { + override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { + if (!canHandleType(type)) { + return } + type.fieldDefinitions + .filter { canHandleField(it) } + .mapNotNull { targetField -> + buildFieldDefinition(type, targetField, nullableResult = false) + ?.let { builder -> + + val relationType = targetField + .getDirectiveArgument(DirectiveConstants.RELATION, DirectiveConstants.RELATION_NAME, null) + ?.let { buildingEnv.getTypeForRelation(it) } + + relationType + ?.fieldDefinitions + ?.filter { it.type.isScalar() && !it.isID() } + ?.forEach { builder.argument(input(it.name, it.type as GraphQLScalarType)) } + + buildingEnv.addOperation(MUTATION, builder.build()) + } + + } + } + + override fun createDataFetcher( + sourceType: GraphQLFieldsContainer, + relation: RelationshipInfo, + startIdField: RelationshipInfo.RelatedField, + endIdField: RelationshipInfo.RelatedField, + fieldDefinition: GraphQLFieldDefinition + ): DataFetcher? { + return CreateRelationHandler(sourceType, relation, startIdField, endIdField, fieldDefinition) } + } override fun generateCypher( diff --git a/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationTypeHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationTypeHandler.kt index 5413faee..46e774fe 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationTypeHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationTypeHandler.kt @@ -1,43 +1,96 @@ package org.neo4j.graphql.handler.relation import graphql.language.Field -import graphql.language.FieldDefinition -import graphql.schema.DataFetchingEnvironment +import graphql.schema.* import org.neo4j.graphql.* class CreateRelationTypeHandler private constructor( - type: NodeFacade, + type: GraphQLFieldsContainer, relation: RelationshipInfo, startId: RelationshipInfo.RelatedField, endId: RelationshipInfo.RelatedField, - fieldDefinition: FieldDefinition, - metaProvider: MetaProvider) - : BaseRelationHandler(type, relation, startId, endId, fieldDefinition, metaProvider) { - - companion object { - fun build(type: ObjectDefinitionNodeFacade, metaProvider: MetaProvider): CreateRelationTypeHandler? { - val relevantFields = type.relevantFields() - if (relevantFields.isEmpty()) { - return null + fieldDefinition: GraphQLFieldDefinition) + : BaseRelationHandler(type, relation, startId, endId, fieldDefinition) { + + class Factory(schemaConfig: SchemaConfig) : AugmentationHandler(schemaConfig) { + override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { + if (!canHandle(type)) { + return } - val relation = type.relationship(metaProvider)!! - val startIdField = relation.getStartFieldId(metaProvider) - val endIdField = relation.getEndFieldId(metaProvider) + val relation = type.relationship()!! + val startIdField = relation.getStartFieldId() + val endIdField = relation.getEndFieldId() if (startIdField == null || endIdField == null) { - return null + return } - val createArgs = relevantFields + val relevantFields = getRelevantFields(type) + + val createArgs = getRelevantFields(type) .filter { !it.isNativeId() } .filter { it.name != startIdField.argumentName } .filter { it.name != endIdField.argumentName } - val builder = createFieldDefinition("create", type.name(), emptyList(), false) - .inputValueDefinition(input(startIdField.argumentName, startIdField.field.type)) - .inputValueDefinition(input(endIdField.argumentName, endIdField.field.type)) - createArgs.forEach { builder.inputValueDefinition(input(it.name, it.type)) } + val builder = buildingEnv + .buildFieldDefinition("create", type, relevantFields, nullableResult = false) + .argument(input(startIdField.argumentName, startIdField.field.type)) + .argument(input(endIdField.argumentName, endIdField.field.type)) + + createArgs.forEach { builder.argument(input(it.name, it.type)) } + + buildingEnv.addOperation(MUTATION, builder.build()) + } + + override fun createDataFetcher(rootType: GraphQLObjectType, fieldDefinition: GraphQLFieldDefinition): DataFetcher? { + if (rootType.name != MUTATION) { + return null + } + if (fieldDefinition.cypherDirective() != null) { + return null + } + val type = fieldDefinition.type.inner() as? GraphQLObjectType + ?: return null + if (!canHandle(type)) { + return null + } + if (fieldDefinition.name != "create${type.name}") { + return null + } + + val relation = type.relationship() ?: return null + val startIdField = relation.getStartFieldId() ?: return null + val endIdField = relation.getEndFieldId() ?: return null + + return CreateRelationTypeHandler(type, relation, startIdField, endIdField, fieldDefinition) + } + + private fun getRelevantFields(type: GraphQLFieldsContainer): List { + return type + .relevantFields() + .filter { !it.isNativeId() } + } - return CreateRelationTypeHandler(type, relation, startIdField, endIdField, builder.build(), metaProvider) + private fun canHandle(type: GraphQLFieldsContainer): Boolean { + val typeName = type.name + if (!schemaConfig.mutation.enabled || schemaConfig.mutation.exclude.contains(typeName)) { + return false + } + if (type !is GraphQLObjectType) { + return false + } + val relation = type.relationship() ?: return false + val startIdField = relation.getStartFieldId() + val endIdField = relation.getEndFieldId() + if (startIdField == null || endIdField == null) { + return false + } + if (getRelevantFields(type).isEmpty()) { + // nothing to create + // TODO or should we support just creating empty nodes? + return false + } + return true } + } override fun generateCypher( diff --git a/src/main/kotlin/org/neo4j/graphql/handler/relation/DeleteRelationHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/relation/DeleteRelationHandler.kt index f1898e5b..be1b9e79 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/relation/DeleteRelationHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/relation/DeleteRelationHandler.kt @@ -1,29 +1,43 @@ package org.neo4j.graphql.handler.relation import graphql.language.Field -import graphql.language.FieldDefinition -import graphql.language.ObjectTypeDefinition +import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLFieldsContainer import org.neo4j.graphql.* class DeleteRelationHandler private constructor( - type: NodeFacade, + type: GraphQLFieldsContainer, relation: RelationshipInfo, startId: RelationshipInfo.RelatedField, endId: RelationshipInfo.RelatedField, - fieldDefinition: FieldDefinition, - metaProvider: MetaProvider) - : BaseRelationHandler(type, relation, startId, endId, fieldDefinition, metaProvider) { + fieldDefinition: GraphQLFieldDefinition) + : BaseRelationHandler(type, relation, startId, endId, fieldDefinition) { - companion object { - fun build(source: ObjectTypeDefinition, - target: ObjectTypeDefinition, - metaProvider: MetaProvider): DeleteRelationHandler? { - - return build("delete", source, target, metaProvider, true) { sourceNodeType, relation, startIdField, endIdField, _, fieldDefinitionBuilder -> - DeleteRelationHandler(sourceNodeType, relation, startIdField, endIdField, fieldDefinitionBuilder.build(), metaProvider) + class Factory(schemaConfig: SchemaConfig) : BaseRelationFactory("delete", schemaConfig) { + override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { + if (!canHandleType(type)) { + return } + type.fieldDefinitions + .filter { canHandleField(it) } + .mapNotNull { targetField -> + buildFieldDefinition(type, targetField, true) + ?.let { builder -> buildingEnv.addOperation(MUTATION, builder.build()) } + } + } + + override fun createDataFetcher( + sourceType: GraphQLFieldsContainer, + relation: RelationshipInfo, + startIdField: RelationshipInfo.RelatedField, + endIdField: RelationshipInfo.RelatedField, + fieldDefinition: GraphQLFieldDefinition + ): DataFetcher? { + return DeleteRelationHandler(sourceType, relation, startIdField, endIdField, fieldDefinition) } + } override fun generateCypher( diff --git a/src/test/kotlin/DataFetcherInterceptorDemo.kt b/src/test/kotlin/DataFetcherInterceptorDemo.kt index a532db49..2a2dec60 100644 --- a/src/test/kotlin/DataFetcherInterceptorDemo.kt +++ b/src/test/kotlin/DataFetcherInterceptorDemo.kt @@ -1,13 +1,8 @@ package demo import graphql.GraphQL -import graphql.language.ListType -import graphql.language.NonNullType -import graphql.language.Type import graphql.language.VariableReference -import graphql.schema.DataFetcher -import graphql.schema.DataFetchingEnvironment -import graphql.schema.GraphQLSchema +import graphql.schema.* import org.intellij.lang.annotations.Language import org.neo4j.driver.v1.AuthTokens import org.neo4j.driver.v1.GraphDatabase @@ -58,10 +53,10 @@ fun toBoltValue(value: Any?, params: Map) = when (value) { else -> value } -private fun isListType(type: Type<*>?): Boolean { +private fun isListType(type: GraphQLType?): Boolean { return when (type) { - is ListType -> true - is NonNullType -> isListType(type.type) + is GraphQLType -> true + is GraphQLNonNull -> isListType(type.wrappedType) else -> false } } \ No newline at end of file From 41e5b2760b6b7dde7793387675fc37fe0d46820a Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Thu, 29 Aug 2019 22:37:27 +0200 Subject: [PATCH 3/7] clean up --- pom.xml | 1 + .../kotlin/org/neo4j/graphql/BuildingEnv.kt | 61 ++++++++--------- .../org/neo4j/graphql/GraphQLExtensions.kt | 10 +-- .../kotlin/org/neo4j/graphql/SchemaBuilder.kt | 65 +++++++------------ .../neo4j/graphql/handler/BaseDataFetcher.kt | 7 +- .../graphql/handler/CreateTypeHandler.kt | 4 +- .../graphql/handler/CypherDirectiveHandler.kt | 9 +-- .../neo4j/graphql/handler/DeleteHandler.kt | 4 +- .../graphql/handler/MergeOrUpdateHandler.kt | 4 +- .../org/neo4j/graphql/handler/QueryHandler.kt | 7 +- .../handler/projection/ProjectionBase.kt | 3 + .../handler/relation/CreateRelationHandler.kt | 3 +- .../relation/CreateRelationTypeHandler.kt | 3 +- .../handler/relation/DeleteRelationHandler.kt | 3 +- .../neo4j/graphql/utils/AsciiDocTestSuite.kt | 3 +- src/test/resources/augmentation-tests.adoc | 4 -- src/test/resources/movie-tests.adoc | 16 ++--- src/test/resources/translator-tests1.adoc | 8 +-- 18 files changed, 92 insertions(+), 123 deletions(-) diff --git a/pom.xml b/pom.xml index dfabba42..2624e3b4 100755 --- a/pom.xml +++ b/pom.xml @@ -195,6 +195,7 @@ + packages.md diff --git a/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt b/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt index 10acb7d2..ad56fa37 100644 --- a/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt +++ b/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt @@ -10,16 +10,6 @@ class BuildingEnv(val types: MutableMap) { .map { it.getDirectiveArgument(DirectiveConstants.RELATION, DirectiveConstants.RELATION_NAME, null)!! to it.name } .toMap() - fun getInnerFieldsContainer(type: GraphQLType): GraphQLFieldsContainer { - var innerType = type.inner() - if (innerType is GraphQLTypeReference) { - innerType = types[innerType.name] - ?: throw IllegalArgumentException("${innerType.name} is unknown") - } - return innerType as? GraphQLFieldsContainer - ?: throw IllegalArgumentException("${innerType.name} is neither an object nor an interface") - } - fun buildFieldDefinition( prefix: String, resultType: GraphQLOutputType, @@ -33,7 +23,7 @@ class BuildingEnv(val types: MutableMap) { } return GraphQLFieldDefinition.newFieldDefinition() .name("$prefix${resultType.name}") - .argument(getInputValueDefinitions(scalarFields, forceOptionalProvider)) + .arguments(getInputValueDefinitions(scalarFields, forceOptionalProvider)) .type(type.ref() as GraphQLOutputType) } @@ -52,21 +42,6 @@ class BuildingEnv(val types: MutableMap) { } } - private fun getInputType(type: GraphQLType): GraphQLInputType { - val inner = type.inner() - if (inner is GraphQLInputType) { - return type as GraphQLInputType - } - if (inner.isNeo4jType()) { - return neo4jTypeDefinitions - .find { it.typeDefinition == inner.name } - ?.let { types[it.inputDefinition] } as? GraphQLInputType - ?: throw IllegalArgumentException("Cannot find input type for ${inner.name}") - } - return type as? GraphQLInputType - ?: throw IllegalArgumentException("${type.name} is not allowed for input") - } - fun input(name: String, type: GraphQLType): GraphQLArgument { return GraphQLArgument .newArgument() @@ -174,14 +149,18 @@ class BuildingEnv(val types: MutableMap) { return inputType } - fun getInputType(inputName: String, relevantFields: List): GraphQLInputObjectType { + fun getTypeForRelation(nameOfRelation: String): GraphQLObjectType? { + return typesForRelation[nameOfRelation]?.let { types[it] } as? GraphQLObjectType + } + + private fun getInputType(inputName: String, relevantFields: List): GraphQLInputObjectType { return GraphQLInputObjectType.newInputObject() .name(inputName) .fields(getInputValueDefinitions(relevantFields)) .build() } - fun getInputValueDefinitions(relevantFields: List): List { + private fun getInputValueDefinitions(relevantFields: List): List { return relevantFields.map { val type = (it.type as? GraphQLNonNull)?.wrappedType ?: it.type GraphQLInputObjectField @@ -192,7 +171,29 @@ class BuildingEnv(val types: MutableMap) { } } - fun getTypeForRelation(nameOfRelation: String): GraphQLObjectType? { - return typesForRelation[nameOfRelation]?.let { types[it] } as? GraphQLObjectType + private fun getInnerFieldsContainer(type: GraphQLType): GraphQLFieldsContainer { + var innerType = type.inner() + if (innerType is GraphQLTypeReference) { + innerType = types[innerType.name] + ?: throw IllegalArgumentException("${innerType.name} is unknown") + } + return innerType as? GraphQLFieldsContainer + ?: throw IllegalArgumentException("${innerType.name} is neither an object nor an interface") + } + + private fun getInputType(type: GraphQLType): GraphQLInputType { + val inner = type.inner() + if (inner is GraphQLInputType) { + return type as GraphQLInputType + } + if (inner.isNeo4jType()) { + return neo4jTypeDefinitions + .find { it.typeDefinition == inner.name } + ?.let { types[it.inputDefinition] } as? GraphQLInputType + ?: throw IllegalArgumentException("Cannot find input type for ${inner.name}") + } + return type as? GraphQLInputType + ?: throw IllegalArgumentException("${type.name} is not allowed for input") } + } \ No newline at end of file diff --git a/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt b/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt index 6d6dee4c..0e267761 100644 --- a/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt +++ b/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt @@ -18,9 +18,6 @@ import org.neo4j.graphql.DirectiveConstants.Companion.RELATION_NAME import org.neo4j.graphql.DirectiveConstants.Companion.RELATION_TO import org.neo4j.graphql.handler.projection.ProjectionBase -val SCALAR_TYPES = listOf("String", "ID", "Boolean", "Int", "Float", "DynamicProperties") - -fun Type>.isScalar() = this.inner().name()?.let { SCALAR_TYPES.contains(it) } ?: false fun Type>.name(): String? = if (this.inner() is TypeName) (this.inner() as TypeName).name else null fun Type>.inner(): Type> = when (this) { is ListType -> this.type.inner() @@ -74,6 +71,7 @@ fun GraphQLFieldsContainer.getValidTypeLabels(schema: GraphQLSchema): List (this as? GraphQLDirectiveContainer) @@ -168,7 +166,7 @@ data class RelationshipInfo( } fun Field.aliasOrName() = (this.alias ?: this.name).quote() -fun GraphQLType.innerName() = inner().name +fun GraphQLType.innerName(): String = inner().name fun GraphQLFieldDefinition.propertyName() = getDirectiveArgument(PROPERTY, PROPERTY_NAME, this.name)!! @@ -197,6 +195,7 @@ fun String.isJavaIdentifier() = this[0].isJavaIdentifierStart() && this.substring(1).all { it.isJavaIdentifierPart() } +@Suppress("SimplifiableCallChain") fun Value>.toCypherString(): String = when (this) { is StringValue -> "'" + this.value + "'" is EnumValue -> "'" + this.name + "'" @@ -234,6 +233,3 @@ fun paramName(variable: String, argName: String, value: Any?): String = when (va fun GraphQLFieldDefinition.isID() = this.type.inner() == Scalars.GraphQLID fun GraphQLFieldDefinition.isNativeId() = this.name == ProjectionBase.NATIVE_ID - -fun GraphQLFieldsContainer.getFieldByType(typeName: String) = this.fieldDefinitions - .firstOrNull { it.type.inner().name == typeName } \ 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 57293d5f..62dd1bc0 100644 --- a/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt +++ b/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt @@ -28,11 +28,11 @@ object SchemaBuilder { @JvmOverloads fun buildSchema(sdl: String, config: SchemaConfig = SchemaConfig(), dataFetchingInterceptor: DataFetchingInterceptor? = null): GraphQLSchema { val schemaParser = SchemaParser() - val typeDefinitionRegistry = schemaParser.parse(sdl) - mergeNeo4jEnhancements(typeDefinitionRegistry) + val typeDefinitionRegistry = schemaParser.parse(sdl).merge(getNeo4jEnhancements()) if (!typeDefinitionRegistry.getType(QUERY).isPresent) { typeDefinitionRegistry.add(ObjectTypeDefinition.newObjectTypeDefinition().name(QUERY).build()) } + val builder = RuntimeWiring.newRuntimeWiring() .scalar(DynamicProperties.INSTANCE) typeDefinitionRegistry @@ -55,6 +55,16 @@ object SchemaBuilder { return targetSchema } +// @JvmStatic +// @JvmOverloads +// fun enhanceSchema(builder: GraphQLSchema.Builder, config: SchemaConfig = SchemaConfig(), dataFetchingInterceptor: DataFetchingInterceptor? = null): GraphQLSchema { +// mergeNeo4jEnhancements(builder) +// val handler = getHandler(config) +// +// var targetSchema = augmentSchema(sourceSchema, handler) +// targetSchema = addDataFetcher(targetSchema, dataFetchingInterceptor, handler) +// } + private fun getHandler(schemaConfig: SchemaConfig): List { val handler = mutableListOf( CypherDirectiveHandler.Factory(schemaConfig) @@ -169,9 +179,18 @@ object SchemaBuilder { } } - private fun mergeNeo4jEnhancements(typeDefinitionRegistry: TypeDefinitionRegistry) { + private fun mergeNeo4jEnhancements(builder: GraphQLSchema.Builder) { + val typeDefinitionRegistry = getNeo4jEnhancements() + val wiring = RuntimeWiring.newRuntimeWiring().scalar(DynamicProperties.INSTANCE).build() + val schema = SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, wiring) + + schema.directives.forEach { builder.additionalDirective(it) } + schema.additionalTypes.forEach { builder.additionalType(it) } + } + + private fun getNeo4jEnhancements(): TypeDefinitionRegistry { val directivesSdl = javaClass.getResource("/neo4j.graphql").readText() - typeDefinitionRegistry.merge(SchemaParser().parse(directivesSdl)) + val typeDefinitionRegistry = SchemaParser().parse(directivesSdl) neo4jTypeDefinitions .forEach { val type = typeDefinitionRegistry.getType(it.typeDefinition) @@ -179,6 +198,7 @@ object SchemaBuilder { as ObjectTypeDefinition addInputType(typeDefinitionRegistry, it.inputDefinition, type.fieldDefinitions) } + return typeDefinitionRegistry } private fun addInputType(typeDefinitionRegistry: TypeDefinitionRegistry, inputName: String, relevantFields: List): String { @@ -197,41 +217,4 @@ object SchemaBuilder { typeDefinitionRegistry.add(inputType) return inputName } - - @JvmStatic - fun mergeNeo4jEnhancements(builder: GraphQLSchema.Builder) { - val schemaParser = SchemaParser() - val directivesSdl = javaClass.getResource("/neo4j.graphql").readText() - val typeDefinitionRegistry = schemaParser.parse(directivesSdl) - val wiring = RuntimeWiring.newRuntimeWiring().scalar(DynamicProperties.INSTANCE).build() - val schema = SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, wiring) - schema.directives.forEach { builder.additionalDirective(it) } - schema.additionalTypes.forEach { builder.additionalType(it) } - neo4jTypeDefinitions - .forEach { - val type = (schema.getType(it.typeDefinition) as? GraphQLObjectType) - ?: throw IllegalStateException("type ${it.typeDefinition} not found") - - builder.additionalType(getInputType(it.inputDefinition, type.fieldDefinitions)) - } - } - - private fun getInputType(inputName: String, relevantFields: List): GraphQLInputObjectType { - return GraphQLInputObjectType.newInputObject() - .name(inputName) - .fields(getInputValueDefinitions(relevantFields)) - .build() - } - - private fun getInputValueDefinitions(relevantFields: List): List { - return relevantFields.map { - val type = (it.type as? GraphQLNonNull)?.wrappedType ?: it.type - GraphQLInputObjectField - .newInputObjectField() - .name(it.name) - .type(type as? GraphQLInputType - ?: throw IllegalArgumentException("${type.name} is not allowed for input")) - .build() - } - } } \ No newline at end of file diff --git a/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt b/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt index cf1de8da..e453b0fd 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt @@ -67,22 +67,17 @@ abstract class BaseDataFetcher( override fun get(env: DataFetchingEnvironment?): Cypher { val field = env?.mergedField?.singleField ?: throw IllegalAccessException("expect one filed in environment.mergedField") - if (field.name != fieldDefinition.name) - throw IllegalArgumentException("Handler for ${fieldDefinition.name} cannot handle ${field.name}") + require(field.name == fieldDefinition.name) { "Handler for ${fieldDefinition.name} cannot handle ${field.name}" } val variable = field.aliasOrName().decapitalize() return generateCypher( variable, field, - { - projectFields(variable, field, type, env, null) - }, env) .copy(type = fieldDefinition.type) } protected abstract fun generateCypher(variable: String, field: Field, - projectionProvider: () -> Cypher, env: DataFetchingEnvironment ): Cypher diff --git a/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt index 21973cf5..2ac3c9d6 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt @@ -69,9 +69,9 @@ class CreateTypeHandler private constructor( } - override fun generateCypher(variable: String, field: Field, projectionProvider: () -> Cypher, env: DataFetchingEnvironment): Cypher { + override fun generateCypher(variable: String, field: Field, env: DataFetchingEnvironment): Cypher { val properties = properties(variable, field.arguments) - val mapProjection = projectionProvider.invoke() + val mapProjection = projectFields(variable, field, type, env, null) return Cypher("CREATE ($variable:${allLabels()}${properties.query})" + " WITH $variable" + " RETURN ${mapProjection.query} AS $variable", diff --git a/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt index cc751090..63d40b19 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt @@ -17,10 +17,7 @@ class CypherDirectiveHandler( } override fun createDataFetcher(rootType: GraphQLObjectType, fieldDefinition: GraphQLFieldDefinition): DataFetcher? { - val cypherDirective = fieldDefinition.cypherDirective() - if (cypherDirective == null) { - return null - } + val cypherDirective = fieldDefinition.cypherDirective() ?: return null // TODO cypher directives can also return scalars val type = fieldDefinition.type.inner() as? GraphQLFieldsContainer ?: return null @@ -29,8 +26,8 @@ class CypherDirectiveHandler( } } - override fun generateCypher(variable: String, field: Field, projectionProvider: () -> Cypher, env: DataFetchingEnvironment): Cypher { - val mapProjection = projectionProvider.invoke() + override fun generateCypher(variable: String, field: Field, env: DataFetchingEnvironment): Cypher { + val mapProjection = projectFields(variable, field, type, env, null) val ordering = orderBy(variable, field.arguments) val skipLimit = SkipLimit(variable, field.arguments).format() diff --git a/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt index 96016c3c..37a18d25 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt @@ -57,9 +57,9 @@ class DeleteHandler private constructor( } } - override fun generateCypher(variable: String, field: Field, projectionProvider: () -> Cypher, env: DataFetchingEnvironment): Cypher { + override fun generateCypher(variable: String, field: Field, env: DataFetchingEnvironment): Cypher { val idArg = field.arguments.first { it.name == idField.name } - val mapProjection = projectionProvider.invoke() + val mapProjection = projectFields(variable, field, type, env, null) val select = getSelectQuery(variable, label(), idArg, idField, isRelation) return Cypher("MATCH " + select.query + diff --git a/src/main/kotlin/org/neo4j/graphql/handler/MergeOrUpdateHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/MergeOrUpdateHandler.kt index 59b121fc..fc24129f 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/MergeOrUpdateHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/MergeOrUpdateHandler.kt @@ -71,11 +71,11 @@ class MergeOrUpdateHandler private constructor( propertyFields.remove(idField.name) // id should not be updated } - override fun generateCypher(variable: String, field: Field, projectionProvider: () -> Cypher, env: DataFetchingEnvironment): Cypher { + override fun generateCypher(variable: String, field: Field, env: DataFetchingEnvironment): Cypher { val idArg = field.arguments.first { it.name == idField.name } val properties = properties(variable, field.arguments) - val mapProjection = projectionProvider.invoke() + val mapProjection = projectFields(variable, field, type, env, null) val op = if (merge) "+" else "" val select = getSelectQuery(variable, label(), idArg, idField, isRelation) diff --git a/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt index 7b869ddd..6a5884c8 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt @@ -65,16 +65,15 @@ class QueryHandler private constructor( } private fun getRelevantFields(type: GraphQLFieldsContainer): List { - val relevantFields = type + return type .relevantFields() .filter { it.dynamicPrefix() == null } // TODO currently we do not support filtering on dynamic properties - return relevantFields } } - override fun generateCypher(variable: String, field: Field, projectionProvider: () -> Cypher, env: DataFetchingEnvironment): Cypher { + override fun generateCypher(variable: String, field: Field, env: DataFetchingEnvironment): Cypher { - val mapProjection = projectionProvider.invoke() + val mapProjection = projectFields(variable, field, type, env, null) val ordering = orderBy(variable, field.arguments) val skipLimit = SkipLimit(variable, field.arguments).format() 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 00d141de..1e2ad099 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt @@ -25,6 +25,7 @@ open class ProjectionBase { else -> null } } + @Suppress("SimplifiableCallChain") return if (values == null) "" else " ORDER BY " + values .map { it.split("_") } @@ -134,6 +135,7 @@ open class ProjectionBase { fun projectFields(variable: String, field: Field, nodeType: GraphQLFieldsContainer, env: DataFetchingEnvironment, variableSuffix: String?): Cypher { val queries = projectSelectionSet(variable, field.selectionSet, nodeType, env, variableSuffix) + @Suppress("SimplifiableCallChain") val projection = queries .map { it.query } .joinToString(", ", "{ ", " }") @@ -216,6 +218,7 @@ open class ProjectionBase { } private fun projectNeo4jObjectType(variable: String, field: Field): Cypher { + @Suppress("SimplifiableCallChain") val fieldProjection = field.selectionSet.selections .filterIsInstance() .map { diff --git a/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt index 0b6634b8..443a0cc6 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt @@ -53,12 +53,11 @@ class CreateRelationHandler private constructor( override fun generateCypher( variable: String, field: Field, - projectionProvider: () -> Cypher, env: DataFetchingEnvironment ): Cypher { val properties = properties(variable, field.arguments) - val mapProjection = projectionProvider.invoke() + val mapProjection = projectFields(variable, field, type, env, null) val arguments = field.arguments.map { it.name to it }.toMap() val startSelect = getRelationSelect(true, arguments) diff --git a/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationTypeHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationTypeHandler.kt index 46e774fe..43a224ee 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationTypeHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationTypeHandler.kt @@ -96,11 +96,10 @@ class CreateRelationTypeHandler private constructor( override fun generateCypher( variable: String, field: Field, - projectionProvider: () -> Cypher, env: DataFetchingEnvironment ): Cypher { val properties = properties(variable, field.arguments) - val mapProjection = projectionProvider.invoke() + val mapProjection = projectFields(variable, field, type, env, null) val arguments = field.arguments.map { it.name to it }.toMap() val startSelect = getRelationSelect(true, arguments) diff --git a/src/main/kotlin/org/neo4j/graphql/handler/relation/DeleteRelationHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/relation/DeleteRelationHandler.kt index be1b9e79..fa5be2df 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/relation/DeleteRelationHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/relation/DeleteRelationHandler.kt @@ -43,9 +43,8 @@ class DeleteRelationHandler private constructor( override fun generateCypher( variable: String, field: Field, - projectionProvider: () -> Cypher, env: DataFetchingEnvironment): Cypher { - val mapProjection = projectionProvider.invoke() + val mapProjection = projectFields(variable, field, type, env, null) val arguments = field.arguments.map { it.name to it }.toMap() val startSelect = getRelationSelect(true, arguments) val endSelect = getRelationSelect(false, arguments) diff --git a/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt b/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt index 9be4f306..21b139f8 100644 --- a/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt +++ b/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt @@ -67,7 +67,8 @@ open class AsciiDocTestSuite { } } } - return ParsedFile(schema ?: throw IllegalStateException("no schema found"), file, tests) + return ParsedFile(schema ?: throw IllegalStateException("no schema found"), + File("src/test/resources/$fileName").absoluteFile, tests) } private fun fixNumber(v: Any?): Any? = when (v) { diff --git a/src/test/resources/augmentation-tests.adoc b/src/test/resources/augmentation-tests.adoc index 08599467..0d1b4812 100644 --- a/src/test/resources/augmentation-tests.adoc +++ b/src/test/resources/augmentation-tests.adoc @@ -316,7 +316,6 @@ scalar DynamicProperties ---- schema { query: Query - mutation: Mutation } interface HasMovies { @@ -348,9 +347,6 @@ type Movie { publishedBy: Publisher @relation(direction : OUT, from : "from", name : "PUBLISHED_BY", to : "to") } -type Mutation { -} - type Person0 { born: _Neo4jTime name: String diff --git a/src/test/resources/movie-tests.adoc b/src/test/resources/movie-tests.adoc index 3b5beb2d..5ba3aa49 100644 --- a/src/test/resources/movie-tests.adoc +++ b/src/test/resources/movie-tests.adoc @@ -83,14 +83,14 @@ enum BookGenre { type Book { genre: BookGenre } -# enum _MovieOrdering { -# title_desc, -# title_asc -# } -# enum _GenreOrdering { -# name_desc, -# name_asc -# } +enum _MovieOrdering { + title_desc, + title_asc +} +enum _GenreOrdering { + name_desc, + name_asc +} type Query { Movie(_id: String, movieId: ID, title: String, year: Int, plot: String, poster: String, imdbRating: Float, first: Int, offset: Int, orderBy: _MovieOrdering): [Movie] MoviesByYear(year: Int): [Movie] diff --git a/src/test/resources/translator-tests1.adoc b/src/test/resources/translator-tests1.adoc index 21a0e59a..a81a024b 100644 --- a/src/test/resources/translator-tests1.adoc +++ b/src/test/resources/translator-tests1.adoc @@ -476,12 +476,12 @@ query($_param:String) { p:values(_param:$_param) { age } } ---- MATCH (p:Person) WHERE p._param = $_param - AND p._string = $p_string - AND p._int = $p_int - AND p._float = $p_float AND p._array = $p_array - AND p._enum = $p_enum AND p._boolean = $p_boolean + AND p._enum = $p_enum + AND p._float = $p_float + AND p._int = $p_int + AND p._string = $p_string RETURN p { .age } AS p ---- From cbb3f9324375fb3507aed2ce642ef70de31ba98a Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Mon, 16 Sep 2019 09:12:41 +0200 Subject: [PATCH 4/7] clean up --- .../kotlin/org/neo4j/graphql/BuildingEnv.kt | 10 ++++- src/main/kotlin/org/neo4j/graphql/Cypher.kt | 1 + .../kotlin/org/neo4j/graphql/SchemaBuilder.kt | 40 +++++++------------ .../handler/projection/ProjectionBase.kt | 2 +- .../handler/relation/CreateRelationHandler.kt | 7 +++- 5 files changed, 29 insertions(+), 31 deletions(-) diff --git a/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt b/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt index ad56fa37..939d1b33 100644 --- a/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt +++ b/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt @@ -124,14 +124,20 @@ class BuildingEnv(val types: MutableMap) { val sortingFields = type.fieldDefinitions .filter { it.type.isScalar() || it.isNeo4jType() } .sortedByDescending { it.isID() } - if (sortingFields.isEmpty()){ + if (sortingFields.isEmpty()) { return null } existingOrderingType = GraphQLEnumType.newEnum() .name(orderingName) .values(sortingFields.flatMap { fd -> listOf("_asc", "_desc") - .map { GraphQLEnumValueDefinition.newEnumValueDefinition().name(fd.name + it).build() } + .map { + GraphQLEnumValueDefinition + .newEnumValueDefinition() + .name(fd.name + it) + .value(fd.name + it) + .build() + } }) .build() types[orderingName] = existingOrderingType diff --git a/src/main/kotlin/org/neo4j/graphql/Cypher.kt b/src/main/kotlin/org/neo4j/graphql/Cypher.kt index 61379c7c..9cd920dc 100644 --- a/src/main/kotlin/org/neo4j/graphql/Cypher.kt +++ b/src/main/kotlin/org/neo4j/graphql/Cypher.kt @@ -7,6 +7,7 @@ data class Cypher @JvmOverloads constructor(val query: String, val params: Map builder.type(typeDefinition.name) { @@ -46,7 +53,7 @@ object SchemaBuilder { } } } - val sourceSchema = SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, builder.build()) + val sourceSchema = SchemaGenerator().makeExecutableSchema(enhancedRegistry, builder.build()) val handler = getHandler(config) @@ -55,16 +62,6 @@ object SchemaBuilder { return targetSchema } -// @JvmStatic -// @JvmOverloads -// fun enhanceSchema(builder: GraphQLSchema.Builder, config: SchemaConfig = SchemaConfig(), dataFetchingInterceptor: DataFetchingInterceptor? = null): GraphQLSchema { -// mergeNeo4jEnhancements(builder) -// val handler = getHandler(config) -// -// var targetSchema = augmentSchema(sourceSchema, handler) -// targetSchema = addDataFetcher(targetSchema, dataFetchingInterceptor, handler) -// } - private fun getHandler(schemaConfig: SchemaConfig): List { val handler = mutableListOf( CypherDirectiveHandler.Factory(schemaConfig) @@ -179,15 +176,6 @@ object SchemaBuilder { } } - private fun mergeNeo4jEnhancements(builder: GraphQLSchema.Builder) { - val typeDefinitionRegistry = getNeo4jEnhancements() - val wiring = RuntimeWiring.newRuntimeWiring().scalar(DynamicProperties.INSTANCE).build() - val schema = SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, wiring) - - schema.directives.forEach { builder.additionalDirective(it) } - schema.additionalTypes.forEach { builder.additionalType(it) } - } - private fun getNeo4jEnhancements(): TypeDefinitionRegistry { val directivesSdl = javaClass.getResource("/neo4j.graphql").readText() val typeDefinitionRegistry = SchemaParser().parse(directivesSdl) 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 1e2ad099..6576d081 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt @@ -294,7 +294,7 @@ open class ProjectionBase { } private fun projectRelationshipParent(variable: String, field: Field, fieldDefinition: GraphQLFieldDefinition, env: DataFetchingEnvironment, variableSuffix: String?): Cypher { - val fieldObjectType = fieldDefinition.type as? GraphQLFieldsContainer ?: return Cypher.EMPTY + val fieldObjectType = fieldDefinition.type.inner() as? GraphQLFieldsContainer ?: return Cypher.EMPTY return projectFields(variable + (variableSuffix?.capitalize() ?: ""), field, fieldObjectType, env, variableSuffix) } diff --git a/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt index 443a0cc6..384f1637 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt @@ -1,7 +1,10 @@ package org.neo4j.graphql.handler.relation import graphql.language.Field -import graphql.schema.* +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLFieldsContainer import org.neo4j.graphql.* class CreateRelationHandler private constructor( @@ -30,7 +33,7 @@ class CreateRelationHandler private constructor( relationType ?.fieldDefinitions ?.filter { it.type.isScalar() && !it.isID() } - ?.forEach { builder.argument(input(it.name, it.type as GraphQLScalarType)) } + ?.forEach { builder.argument(input(it.name, it.type)) } buildingEnv.addOperation(MUTATION, builder.build()) } From a7d4574db37cd2b6799efa1987029ba501109a34 Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Mon, 23 Sep 2019 08:36:21 +0200 Subject: [PATCH 5/7] merge master in + extract input method --- .../kotlin/org/neo4j/graphql/AugmentationHandler.kt | 12 ++++-------- src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt | 8 -------- .../kotlin/org/neo4j/graphql/ExtensionFunctions.kt | 11 +++++++++++ 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt b/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt index 41caba82..187ce0f9 100644 --- a/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt @@ -1,6 +1,9 @@ package org.neo4j.graphql -import graphql.schema.* +import graphql.schema.DataFetcher +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLFieldsContainer +import graphql.schema.GraphQLObjectType abstract class AugmentationHandler(val schemaConfig: SchemaConfig) { companion object { @@ -12,11 +15,4 @@ abstract class AugmentationHandler(val schemaConfig: SchemaConfig) { abstract fun createDataFetcher(rootType: GraphQLObjectType, fieldDefinition: GraphQLFieldDefinition): DataFetcher? - fun input(name: String, type: GraphQLType): GraphQLArgument { - return GraphQLArgument - .newArgument() - .name(name) - .type((type.ref() as? GraphQLInputType) - ?: throw IllegalArgumentException("${type.innerName()} is not allowed for input")).build() - } } \ No newline at end of file diff --git a/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt b/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt index 939d1b33..bc4d61f2 100644 --- a/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt +++ b/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt @@ -42,14 +42,6 @@ class BuildingEnv(val types: MutableMap) { } } - fun input(name: String, type: GraphQLType): GraphQLArgument { - return GraphQLArgument - .newArgument() - .name(name) - .type(getInputType(type).ref() as GraphQLInputType) - .build() - } - /** * add the given operation to the corresponding rootType */ diff --git a/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt b/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt index a2456f79..9d7963d5 100644 --- a/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt +++ b/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt @@ -1,5 +1,8 @@ package org.neo4j.graphql +import graphql.schema.GraphQLArgument +import graphql.schema.GraphQLInputType +import graphql.schema.GraphQLType import java.io.PrintWriter import java.io.StringWriter @@ -12,3 +15,11 @@ fun Throwable.stackTraceAsString(): String { fun Iterable.joinNonEmpty(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String { return if (iterator().hasNext()) joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString() else "" } + +fun input(name: String, type: GraphQLType): GraphQLArgument { + return GraphQLArgument + .newArgument() + .name(name) + .type((type.ref() as? GraphQLInputType) + ?: throw IllegalArgumentException("${type.innerName()} is not allowed for input")).build() +} \ No newline at end of file From 4ebc7abbafb6f25a4dbadba53da638701fb94e02 Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Tue, 24 Sep 2019 22:20:46 +0200 Subject: [PATCH 6/7] make augmentType not abstract --- src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt | 2 +- .../kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt b/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt index 187ce0f9..b22f505d 100644 --- a/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt @@ -11,7 +11,7 @@ abstract class AugmentationHandler(val schemaConfig: SchemaConfig) { const val MUTATION = "Mutation" } - abstract fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) + open fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) {} abstract fun createDataFetcher(rootType: GraphQLObjectType, fieldDefinition: GraphQLFieldDefinition): DataFetcher? diff --git a/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt b/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt index 63d40b19..3dc28c62 100644 --- a/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt +++ b/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt @@ -12,9 +12,6 @@ class CypherDirectiveHandler( : BaseDataFetcher(type, fieldDefinition) { class Factory(schemaConfig: SchemaConfig) : AugmentationHandler(schemaConfig) { - override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { - return - } override fun createDataFetcher(rootType: GraphQLObjectType, fieldDefinition: GraphQLFieldDefinition): DataFetcher? { val cypherDirective = fieldDefinition.cypherDirective() ?: return null From 5c17939de695d4aaa76c246a608a05e497a593ee Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Mon, 30 Sep 2019 07:44:14 +0200 Subject: [PATCH 7/7] simplify invocation --- src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt b/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt index bc4d61f2..aa84f7ed 100644 --- a/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt +++ b/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt @@ -33,7 +33,7 @@ class BuildingEnv(val types: MutableMap) { return relevantFields.map { field -> var type = field.type as GraphQLType type = getInputType(type) - type = if (forceOptionalProvider.invoke(field)) { + type = if (forceOptionalProvider(field)) { (type as? GraphQLNonNull)?.wrappedType ?: type } else { type