diff --git a/core/pom.xml b/core/pom.xml
index c64f6ca4..089cea95 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -26,6 +26,11 @@
2.0.0-SNAPSHOT
test
+
+ org.threeten
+ threeten-extra
+ 1.7.0
+
org.neo4j.driver
neo4j-java-driver
@@ -122,6 +127,13 @@
2.17.2
test
+
+ org.apache.commons
+ commons-csv
+ 1.12.0
+ test
+
+
@@ -148,4 +160,27 @@
+
+
+
+ create-test-file-diff
+
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.0
+
+
+ true
+
+
+
+
+
+
+
diff --git a/core/src/main/kotlin/org/neo4j/graphql/Constants.kt b/core/src/main/kotlin/org/neo4j/graphql/Constants.kt
index da41f60f..cad50286 100644
--- a/core/src/main/kotlin/org/neo4j/graphql/Constants.kt
+++ b/core/src/main/kotlin/org/neo4j/graphql/Constants.kt
@@ -4,6 +4,7 @@ import graphql.language.TypeName
import org.neo4j.graphql.domain.directives.RelationshipDirective
object Constants {
+ const val TYPE_NAME = "__typename"
const val JS_COMPATIBILITY: Boolean = true
const val ID_FIELD = "id"
@@ -26,18 +27,9 @@ object Constants {
const val RELATIONSHIP_FIELD = "relationship"
const val TYPENAME_IN = "typename_IN"
- const val RESOLVE_TYPE = "__resolveType"
+ const val RESOLVE_TYPE = TYPE_NAME
const val RESOLVE_ID = "__id"
- const val X = "x"
- const val Y = "y"
- const val Z = "z"
- const val LONGITUDE = "longitude"
- const val LATITUDE = "latitude"
- const val HEIGHT = "height"
- const val CRS = "crs"
- const val SRID = "srid"
-
const val POINT_TYPE = "Point"
const val CARTESIAN_POINT_TYPE = "CartesianPoint"
const val POINT_INPUT_TYPE = "PointInput"
@@ -68,8 +60,6 @@ object Constants {
RelationshipDirective.NAME,
)
- const val TYPE_NAME = "__typename"
-
const val OPTIONS = "options"
const val WHERE = "where"
@@ -85,6 +75,9 @@ object Constants {
val SortDirection = TypeName("SortDirection")
val PointDistance = TypeName("PointDistance")
val CartesianPointDistance = TypeName("CartesianPointDistance")
+
+ val POINT = TypeName(POINT_TYPE)
+ val CARTESIAN_POINT = TypeName(CARTESIAN_POINT_TYPE)
}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/CypherDataFetcherResult.kt b/core/src/main/kotlin/org/neo4j/graphql/CypherDataFetcherResult.kt
deleted file mode 100644
index 0ba52a8a..00000000
--- a/core/src/main/kotlin/org/neo4j/graphql/CypherDataFetcherResult.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.neo4j.graphql
-
-import graphql.schema.GraphQLType
-
-internal data class CypherDataFetcherResult @JvmOverloads constructor(
- val query: String,
- val params: Map = emptyMap(),
- var type: GraphQLType? = null,
- val variable: String
-)
diff --git a/core/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt b/core/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt
index d01aa74d..34033603 100644
--- a/core/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt
+++ b/core/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt
@@ -30,6 +30,8 @@ fun String.toLowerCase(): String = lowercase(Locale.getDefault())
infix fun Condition?.and(rhs: Condition) = this?.and(rhs) ?: rhs
infix fun Condition?.or(rhs: Condition) = this?.or(rhs) ?: rhs
+infix fun Condition?.xor(rhs: Condition) = this?.xor(rhs) ?: rhs
+
fun Collection.foldWithAnd(): Condition? = this
.filterNotNull()
.takeIf { it.isNotEmpty() }
@@ -169,3 +171,4 @@ fun Iterable.toDict(): List = this.mapNotNull { Dict.create(it) }
fun String.toDeprecatedDirective() = Directive("deprecated", listOf(Argument("reason", StringValue(this))))
+fun Collection.union(): Statement = if (this.size == 1) this.first() else Cypher.union(this)
diff --git a/core/src/main/kotlin/org/neo4j/graphql/QueryContext.kt b/core/src/main/kotlin/org/neo4j/graphql/QueryContext.kt
index 4dfabde9..3e1caba6 100644
--- a/core/src/main/kotlin/org/neo4j/graphql/QueryContext.kt
+++ b/core/src/main/kotlin/org/neo4j/graphql/QueryContext.kt
@@ -3,12 +3,9 @@ package org.neo4j.graphql
import org.neo4j.cypherdsl.core.Cypher
import org.neo4j.cypherdsl.core.Parameter
import org.neo4j.graphql.domain.fields.RelationField
-import org.neo4j.graphql.driver.adapter.Neo4jAdapter.Dialect
import java.util.concurrent.atomic.AtomicInteger
data class QueryContext @JvmOverloads constructor(
- var neo4jDialect: Dialect = Dialect.NEO4J_5,
-
val contextParams: Map? = emptyMap(),
) {
diff --git a/core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt b/core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt
index afb6a3bc..c8c52046 100644
--- a/core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt
+++ b/core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt
@@ -13,8 +13,11 @@ import org.neo4j.graphql.domain.directives.Annotations.Companion.LIBRARY_DIRECTI
import org.neo4j.graphql.domain.fields.RelationField
import org.neo4j.graphql.driver.adapter.Neo4jAdapter
import org.neo4j.graphql.handler.ConnectionResolver
+import org.neo4j.graphql.handler.ImplementingTypeConnectionFieldResolver
import org.neo4j.graphql.handler.ReadResolver
import org.neo4j.graphql.scalars.BigIntScalar
+import org.neo4j.graphql.scalars.DurationScalar
+import org.neo4j.graphql.scalars.TemporalScalar
import org.neo4j.graphql.schema.AugmentationContext
import org.neo4j.graphql.schema.AugmentationHandler
import org.neo4j.graphql.schema.model.outputs.InterfaceSelection
@@ -35,69 +38,90 @@ import org.neo4j.graphql.schema.model.outputs.NodeSelection
*/
class SchemaBuilder @JvmOverloads constructor(
val typeDefinitionRegistry: TypeDefinitionRegistry,
- val schemaConfig: SchemaConfig = SchemaConfig()
+ val schemaConfig: SchemaConfig = SchemaConfig(),
) {
companion object {
- /**
- * @param sdl the schema to augment
- * @param neo4jAdapter the adapter to run the generated cypher queries
- * @param config defines how the schema should get augmented
- */
+
@JvmStatic
@JvmOverloads
- fun buildSchema(
- sdl: String,
- config: SchemaConfig = SchemaConfig(),
- neo4jAdapter: Neo4jAdapter = Neo4jAdapter.NO_OP,
- addLibraryDirectivesToSchema: Boolean = true,
- ): GraphQLSchema {
+ fun fromSchema(sdl: String, config: SchemaConfig = SchemaConfig()): SchemaBuilder {
val schemaParser = SchemaParser()
val typeDefinitionRegistry = schemaParser.parse(sdl)
- return buildSchema(typeDefinitionRegistry, config, neo4jAdapter, addLibraryDirectivesToSchema)
+ return SchemaBuilder(typeDefinitionRegistry, config)
}
/**
- * @param typeDefinitionRegistry a registry containing all the types, that should be augmented
- * @param config defines how the schema should get augmented
+ * @param sdl the schema to augment
* @param neo4jAdapter the adapter to run the generated cypher queries
+ * @param config defines how the schema should get augmented
*/
@JvmStatic
@JvmOverloads
fun buildSchema(
- typeDefinitionRegistry: TypeDefinitionRegistry,
+ sdl: String,
config: SchemaConfig = SchemaConfig(),
- neo4jAdapter: Neo4jAdapter,
+ neo4jAdapter: Neo4jAdapter = Neo4jAdapter.NO_OP,
addLibraryDirectivesToSchema: Boolean = true,
- ): GraphQLSchema {
-
- val builder = RuntimeWiring.newRuntimeWiring()
- val codeRegistryBuilder = GraphQLCodeRegistry.newCodeRegistry()
- val schemaBuilder = SchemaBuilder(typeDefinitionRegistry, config)
- schemaBuilder.augmentTypes(addLibraryDirectivesToSchema)
- schemaBuilder.registerScalars(builder)
- schemaBuilder.registerTypeNameResolver(builder)
- schemaBuilder.registerNeo4jAdapter(codeRegistryBuilder, neo4jAdapter)
-
- return SchemaGenerator().makeExecutableSchema(
- typeDefinitionRegistry,
- builder.codeRegistry(codeRegistryBuilder).build()
- )
- }
+ ): GraphQLSchema = fromSchema(sdl, config)
+ .withNeo4jAdapter(neo4jAdapter)
+ .addLibraryDirectivesToSchema(addLibraryDirectivesToSchema)
+ .build()
}
private val handler: List
private val neo4jTypeDefinitionRegistry: TypeDefinitionRegistry = getNeo4jEnhancements()
private val augmentedFields = mutableListOf()
private val ctx = AugmentationContext(schemaConfig, typeDefinitionRegistry)
+ private var addLibraryDirectivesToSchema: Boolean = false;
+ private var codeRegistryBuilder: GraphQLCodeRegistry.Builder? = null
+ private var runtimeWiringBuilder: RuntimeWiring.Builder? = null
+ private var neo4jAdapter: Neo4jAdapter = Neo4jAdapter.NO_OP
init {
handler = mutableListOf(
ReadResolver.Factory(ctx),
ConnectionResolver.Factory(ctx),
+ ImplementingTypeConnectionFieldResolver.Factory(ctx)
)
}
+ fun addLibraryDirectivesToSchema(addLibraryDirectivesToSchema: Boolean): SchemaBuilder {
+ this.addLibraryDirectivesToSchema = addLibraryDirectivesToSchema
+ return this
+ }
+
+ fun withCodeRegistryBuilder(codeRegistryBuilder: GraphQLCodeRegistry.Builder): SchemaBuilder {
+ this.codeRegistryBuilder = codeRegistryBuilder
+ return this
+ }
+
+ fun withRuntimeWiringBuilder(runtimeWiring: RuntimeWiring.Builder): SchemaBuilder {
+ this.runtimeWiringBuilder = runtimeWiring
+ return this
+ }
+
+ fun withNeo4jAdapter(neo4jAdapter: Neo4jAdapter): SchemaBuilder {
+ this.neo4jAdapter = neo4jAdapter
+ return this
+ }
+
+ fun build(): GraphQLSchema {
+ augmentTypes(addLibraryDirectivesToSchema)
+ val runtimeWiringBuilder = this.runtimeWiringBuilder ?: RuntimeWiring.newRuntimeWiring()
+ registerScalars(runtimeWiringBuilder)
+ registerTypeNameResolver(runtimeWiringBuilder)
+
+ val codeRegistryBuilder = this.codeRegistryBuilder ?: GraphQLCodeRegistry.newCodeRegistry()
+ registerNeo4jAdapter(codeRegistryBuilder, neo4jAdapter)
+
+ return SchemaGenerator().makeExecutableSchema(
+ typeDefinitionRegistry,
+ runtimeWiringBuilder.codeRegistry(codeRegistryBuilder).build()
+ )
+
+ }
+
/**
* Generated additionally query and mutation fields according to the types present in the [typeDefinitionRegistry].
@@ -266,6 +290,12 @@ class SchemaBuilder @JvmOverloads constructor(
.forEach { (name, definition) ->
val scalar = when (name) {
Constants.BIG_INT -> BigIntScalar.INSTANCE
+ Constants.DATE -> TemporalScalar.DATE
+ Constants.TIME -> TemporalScalar.TIME
+ Constants.LOCAL_TIME -> TemporalScalar.LOCAL_TIME
+ Constants.DATE_TIME -> TemporalScalar.DATE_TIME
+ Constants.LOCAL_DATE_TIME -> TemporalScalar.LOCAL_DATE_TIME
+ Constants.DURATION -> DurationScalar.INSTANCE
else -> GraphQLScalarType.newScalar()
.name(name)
.description(
@@ -310,20 +340,11 @@ class SchemaBuilder @JvmOverloads constructor(
neo4jAdapter: Neo4jAdapter,
) {
codeRegistryBuilder.defaultDataFetcher { AliasPropertyDataFetcher() }
- augmentedFields.forEach { augmentedField ->
- val interceptedDataFetcher: DataFetcher<*> = DataFetcher { env ->
- val neo4jDialect = neo4jAdapter.getDialect()
- env.graphQlContext.setQueryContext(QueryContext(neo4jDialect = neo4jDialect))
- val (cypher, params, type, variable) = augmentedField.dataFetcher.get(env)
- val result = neo4jAdapter.executeQuery(cypher, params)
- return@DataFetcher if (type?.isList() == true) {
- result.map { it[variable] }
- } else {
- result.map { it[variable] }
- .firstOrNull() ?: emptyMap()
- }
- }
- codeRegistryBuilder.dataFetcher(augmentedField.coordinates, interceptedDataFetcher)
+ augmentedFields.forEach { (coordinates, dataFetcher) ->
+ codeRegistryBuilder.dataFetcher(coordinates, DataFetcher { env ->
+ env.graphQlContext.put(Neo4jAdapter.CONTEXT_KEY, neo4jAdapter)
+ dataFetcher.get(env)
+ })
}
}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/PointField.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/PointField.kt
index e1810b00..cf2ed16f 100644
--- a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/PointField.kt
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/PointField.kt
@@ -103,9 +103,9 @@ class PointField(
enum class CoordinateType(
internal val inputType: TypeName,
- internal val selectionFactory: (IResolveTree) -> BasePointSelection
+ internal val selectionFactory: (IResolveTree) -> BasePointSelection<*>
) {
- GEOGRAPHIC(Constants.Types.PointDistance, ::PointSelection),
- CARTESIAN(Constants.Types.CartesianPointDistance, ::CartesianPointSelection)
+ GEOGRAPHIC(Constants.Types.PointDistance, PointSelection::parse),
+ CARTESIAN(Constants.Types.CartesianPointDistance, CartesianPointSelection::parse)
}
}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/naming/RelationshipBaseNames.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/RelationshipBaseNames.kt
index c288c588..7f66e3cc 100644
--- a/core/src/main/kotlin/org/neo4j/graphql/domain/naming/RelationshipBaseNames.kt
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/RelationshipBaseNames.kt
@@ -28,7 +28,7 @@ sealed class RelationshipBaseNames(
val relationshipFieldTypename get() = "${prefixForTypenameWithInheritance}Relationship"
- val connectionFieldName get() = "${prefixForTypenameWithInheritance}Connection"
+ val connectionFieldName get() = "${relationship.fieldName}Connection"
fun getConnectionWhereTypename(target: ImplementingType) =
"$prefixForTypenameWithInheritance${target.useNameIfFieldIsUnion()}ConnectionWhere"
diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt
index 946e250d..e4518710 100644
--- a/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt
+++ b/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt
@@ -7,21 +7,20 @@ import org.neo4j.cypherdsl.core.Statement
import org.neo4j.cypherdsl.core.renderer.Configuration
import org.neo4j.cypherdsl.core.renderer.Dialect
import org.neo4j.cypherdsl.core.renderer.Renderer
-import org.neo4j.graphql.CypherDataFetcherResult
import org.neo4j.graphql.SchemaConfig
import org.neo4j.graphql.driver.adapter.Neo4jAdapter
-import org.neo4j.graphql.queryContext
+import org.neo4j.graphql.isList
/**
* This is a base class for the implementation of graphql data fetcher used in this project
*/
internal abstract class BaseDataFetcher(protected val schemaConfig: SchemaConfig) :
- DataFetcher {
+ DataFetcher {
- final override fun get(env: DataFetchingEnvironment): CypherDataFetcherResult {
- val variable = "this"
- val statement = generateCypher(variable, env)
- val dialect = when (env.queryContext().neo4jDialect) {
+ final override fun get(env: DataFetchingEnvironment): Any {
+ val statement = generateCypher(env)
+ val neo4jAdapter = env.graphQlContext.get(Neo4jAdapter.CONTEXT_KEY)
+ val dialect = when (neo4jAdapter.getDialect()) {
Neo4jAdapter.Dialect.NEO4J_4 -> Dialect.NEO4J_4
Neo4jAdapter.Dialect.NEO4J_5 -> Dialect.NEO4J_5
Neo4jAdapter.Dialect.NEO4J_5_23 -> Dialect.NEO4J_5_23
@@ -38,8 +37,19 @@ internal abstract class BaseDataFetcher(protected val schemaConfig: SchemaConfig
val params = statement.catalog.parameters.mapValues { (_, value) ->
(value as? VariableReference)?.let { env.variables[it.name] } ?: value
}
- return CypherDataFetcherResult(query, params, env.fieldDefinition.type, variable = variable)
+
+ val result = neo4jAdapter.executeQuery(query, params)
+ return if (env.fieldDefinition.type?.isList() == true) {
+ result.map { it[RESULT_VARIABLE] }
+ } else {
+ result.map { it[RESULT_VARIABLE] }
+ .firstOrNull() ?: emptyMap()
+ }
}
- protected abstract fun generateCypher(variable: String, env: DataFetchingEnvironment): Statement
+ protected abstract fun generateCypher(env: DataFetchingEnvironment): Statement
+
+ companion object {
+ const val RESULT_VARIABLE = "this"
+ }
}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/ConnectionResolver.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/ConnectionResolver.kt
index 122708c0..503c9316 100644
--- a/core/src/main/kotlin/org/neo4j/graphql/handler/ConnectionResolver.kt
+++ b/core/src/main/kotlin/org/neo4j/graphql/handler/ConnectionResolver.kt
@@ -83,7 +83,7 @@ internal class ConnectionResolver private constructor(
}
}
- override fun generateCypher(variable: String, env: DataFetchingEnvironment): Statement {
+ override fun generateCypher(env: DataFetchingEnvironment): Statement {
val queryContext = env.queryContext()
if (implementingType !is Node) {
TODO()
@@ -95,7 +95,7 @@ internal class ConnectionResolver private constructor(
val input = InputArguments(node, resolveTree.args)
- val dslNode = node.asCypherNode(queryContext, variable)
+ val dslNode = node.asCypherNode(queryContext, RESULT_VARIABLE)
val ongoingReading = TopLevelMatchTranslator(schemaConfig, env.variables, queryContext)
.translateTopLevelMatch(
@@ -184,7 +184,7 @@ internal class ConnectionResolver private constructor(
)
.with(edges, Cypher.size(edges).`as`(totalCount))
.withSubQueries(subQueries)
- .returning(Cypher.mapOf(*topProjection.toTypedArray()).`as`(variable))
+ .returning(Cypher.mapOf(*topProjection.toTypedArray()).`as`(RESULT_VARIABLE))
.build()
}
}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/ImplementingTypeConnectionFieldResolver.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/ImplementingTypeConnectionFieldResolver.kt
new file mode 100644
index 00000000..2a0f6197
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/handler/ImplementingTypeConnectionFieldResolver.kt
@@ -0,0 +1,83 @@
+package org.neo4j.graphql.handler
+
+import graphql.schema.DataFetcher
+import graphql.schema.DataFetchingEnvironment
+import graphql.schema.FieldCoordinates
+import org.neo4j.graphql.Constants
+import org.neo4j.graphql.domain.Entity
+import org.neo4j.graphql.domain.ImplementingType
+import org.neo4j.graphql.domain.fields.RelationBaseField
+import org.neo4j.graphql.schema.AugmentationContext
+import org.neo4j.graphql.schema.AugmentationHandler
+import org.neo4j.graphql.schema.model.outputs.NodeConnectionEdgeFieldSelection
+import org.neo4j.graphql.schema.model.outputs.NodeConnectionFieldSelection
+import org.neo4j.graphql.schema.model.outputs.PageInfoSelection
+import org.neo4j.graphql.utils.PagingUtils
+import org.neo4j.graphql.utils.ResolveTree
+
+internal class ImplementingTypeConnectionFieldResolver(
+ private val relationBaseField: RelationBaseField
+) : DataFetcher {
+
+ class Factory(ctx: AugmentationContext) : AugmentationHandler(ctx), AugmentationHandler.EntityAugmentation {
+
+ override fun augmentEntity(entity: Entity): List {
+ if (entity !is ImplementingType) {
+ return emptyList()
+ }
+ return entity.relationBaseFields.map {
+ AugmentedField(
+ FieldCoordinates.coordinates(entity.name, it.namings.connectionFieldName),
+ ImplementingTypeConnectionFieldResolver(it)
+ )
+ }
+ }
+ }
+
+ override fun get(env: DataFetchingEnvironment): Any {
+ val resolveTree = ResolveTree.resolve(env)
+
+ val source = env.getSource