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/AugmentationHandler.kt b/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt
new file mode 100644
index 00000000..b22f505d
--- /dev/null
+++ b/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt
@@ -0,0 +1,18 @@
+package org.neo4j.graphql
+
+import graphql.schema.DataFetcher
+import graphql.schema.GraphQLFieldDefinition
+import graphql.schema.GraphQLFieldsContainer
+import graphql.schema.GraphQLObjectType
+
+abstract class AugmentationHandler(val schemaConfig: SchemaConfig) {
+ companion object {
+ const val QUERY = "Query"
+ const val MUTATION = "Mutation"
+ }
+
+ open fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) {}
+
+ abstract fun createDataFetcher(rootType: GraphQLObjectType, fieldDefinition: GraphQLFieldDefinition): DataFetcher?
+
+}
\ 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..aa84f7ed
--- /dev/null
+++ b/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt
@@ -0,0 +1,197 @@
+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 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}")
+ .arguments(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(field)) {
+ (type as? GraphQLNonNull)?.wrappedType ?: type
+ } else {
+ type
+ }
+ input(field.name, type)
+ }
+ }
+
+ /**
+ * 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)
+ .value(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 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()
+ }
+
+ private 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()
+ }
+ }
+
+ 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/Cypher.kt b/src/main/kotlin/org/neo4j/graphql/Cypher.kt
index 468f7f71..9cd920dc 100644
--- a/src/main/kotlin/org/neo4j/graphql/Cypher.kt
+++ b/src/main/kotlin/org/neo4j/graphql/Cypher.kt
@@ -1,12 +1,13 @@
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("'", "\\'")
companion object {
+ @JvmStatic
val EMPTY = Cypher("")
}
}
\ No newline at end of file
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
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..0e267761 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
@@ -17,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()
@@ -27,31 +25,93 @@ 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
+@Suppress("SimplifiableCallChain")
+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 +128,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 +137,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 +147,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(): String = 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`"
@@ -127,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 + "'"
@@ -135,11 +204,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 +231,5 @@ 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 FieldDefinition.isList(): Boolean = this.type.isList()
-fun ObjectTypeDefinition.getFieldByType(typeName: String): FieldDefinition? = this.fieldDefinitions
- .firstOrNull { it.type.inner().name() == typeName }
+fun GraphQLFieldDefinition.isID() = this.type.inner() == Scalars.GraphQLID
+fun GraphQLFieldDefinition.isNativeId() = this.name == ProjectionBase.NATIVE_ID
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..6ed81026 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,13 +29,20 @@ object SchemaBuilder {
fun buildSchema(sdl: String, config: SchemaConfig = SchemaConfig(), dataFetchingInterceptor: DataFetchingInterceptor? = null): GraphQLSchema {
val schemaParser = SchemaParser()
val typeDefinitionRegistry = schemaParser.parse(sdl)
+ return buildSchema(typeDefinitionRegistry, config, dataFetchingInterceptor)
+ }
- val builder = RuntimeWiring.newRuntimeWiring()
- .scalar(DynamicProperties.INSTANCE)
+ @JvmStatic
+ @JvmOverloads
+ fun buildSchema(typeDefinitionRegistry: TypeDefinitionRegistry , config: SchemaConfig = SchemaConfig(), dataFetchingInterceptor: DataFetchingInterceptor? = null): GraphQLSchema {
+ val enhancedRegistry = typeDefinitionRegistry.merge(getNeo4jEnhancements())
+ if (!enhancedRegistry.getType(QUERY).isPresent) {
+ enhancedRegistry.add(ObjectTypeDefinition.newObjectTypeDefinition().name(QUERY).build())
+ }
- AugmentationProcessor(typeDefinitionRegistry, config, dataFetchingInterceptor, builder).augmentSchema()
+ val builder = RuntimeWiring.newRuntimeWiring() .scalar(DynamicProperties.INSTANCE)
- typeDefinitionRegistry
+ enhancedRegistry
.getTypes(InterfaceTypeDefinition::class.java)
.forEach { typeDefinition ->
builder.type(typeDefinition.name) {
@@ -44,283 +53,156 @@ object SchemaBuilder {
}
}
}
+ val sourceSchema = SchemaGenerator().makeExecutableSchema(enhancedRegistry, 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()
+ }
- /**
- * add the given operation to the corresponding ObjectTypeDefinition
- */
- private fun mergeOperation(objectTypeDefinition: ObjectTypeDefinition, dataFetcher: BaseDataFetcher?)
- : ObjectTypeDefinition {
- if (dataFetcher == null) {
- return objectTypeDefinition
+ private fun enhanceRelations(fd: GraphQLFieldDefinition): GraphQLFieldDefinition {
+ return fd.transform {
+ // to prevent duplicated types in schema
+ it.type(fd.type.ref() as GraphQLOutputType)
+
+ 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
- }
- 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))
+ private fun getNeo4jEnhancements(): TypeDefinitionRegistry {
+ val directivesSdl = javaClass.getResource("/neo4j.graphql").readText()
+ val typeDefinitionRegistry = 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.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))
- }
- }
+ return typeDefinitionRegistry
+ }
- 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
}
-
- private fun addOrdering(typeName: String, relevantFields: List): String {
- val orderingName = "_${typeName}Ordering"
- if (typeDefinitionRegistry.getType(orderingName).isPresent) {
- return orderingName
- }
- 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
- }
+ val inputType = InputObjectTypeDefinition.newInputObjectDefinition()
+ .name(inputName)
+ .inputValueDefinitions(relevantFields.map {
+ InputValueDefinition.newInputValueDefinition()
+ .name(it.name)
+ .type(it.type)
+ .build()
+ })
+ .build()
+ typeDefinitionRegistry.add(inputType)
+ return inputName
}
}
\ 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..e453b0fd 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(
@@ -64,27 +67,22 @@ 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
- fun allLabels(): String = type.allLabels()
+ fun allLabels(): String = type.label(includeAll = true)
fun label(includeAll: Boolean = false) = type.label(includeAll)
@@ -111,7 +109,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 +118,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 +143,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..2ac3c9d6 100644
--- a/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt
+++ b/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt
@@ -1,30 +1,77 @@
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 {
+ 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 9d5abd3b..3dc28c62 100644
--- a/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt
+++ b/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt
@@ -1,22 +1,30 @@
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) {
- override fun generateCypher(variable: String, field: Field, projectionProvider: () -> Cypher, env: DataFetchingEnvironment): Cypher {
- val mapProjection = projectionProvider.invoke()
+ class Factory(schemaConfig: SchemaConfig) : AugmentationHandler(schemaConfig) {
+
+ override fun createDataFetcher(rootType: GraphQLObjectType, fieldDefinition: GraphQLFieldDefinition): DataFetcher? {
+ val cypherDirective = fieldDefinition.cypherDirective() ?: 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, 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 39eeab1c..37a18d25 100644
--- a/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt
+++ b/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt
@@ -1,43 +1,65 @@
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
}
}
- 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 5183978c..fc24129f 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
}
}
@@ -32,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 b124a875..6a5884c8 100644
--- a/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt
+++ b/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt
@@ -1,53 +1,79 @@
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 {
+ return type
+ .relevantFields()
+ .filter { it.dynamicPrefix() == null } // TODO currently we do not support filtering on dynamic properties
}
}
- 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 9af88cec..6576d081 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"
@@ -25,6 +25,7 @@ open class ProjectionBase(val metaProvider: MetaProvider) {
else -> null
}
}
+ @Suppress("SimplifiableCallChain")
return if (values == null) ""
else " ORDER BY " + values
.map { it.split("_") }
@@ -32,14 +33,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 +56,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,78 +66,76 @@ 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)
+ @Suppress("SimplifiableCallChain")
val projection = queries
.map { it.query }
.joinToString(", ", "{ ", " }")
@@ -146,7 +145,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 +163,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 +173,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 +198,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,17 +207,18 @@ 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())
}
}
}
}
private fun projectNeo4jObjectType(variable: String, field: Field): Cypher {
+ @Suppress("SimplifiableCallChain")
val fieldProjection = field.selectionSet.selections
.filterIsInstance()
.map {
@@ -233,13 +233,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 +256,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 +285,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.inner() 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 +320,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 +337,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..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,53 +1,66 @@
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 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)) }
+
+ 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(
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 5413faee..43a224ee 100644
--- a/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationTypeHandler.kt
+++ b/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationTypeHandler.kt
@@ -1,53 +1,105 @@
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(
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 f1898e5b..fa5be2df 100644
--- a/src/main/kotlin/org/neo4j/graphql/handler/relation/DeleteRelationHandler.kt
+++ b/src/main/kotlin/org/neo4j/graphql/handler/relation/DeleteRelationHandler.kt
@@ -1,37 +1,50 @@
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(
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/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
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
----