diff --git a/.editorconfig b/.editorconfig
index 1525fadc..46940d65 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -411,6 +411,7 @@ ij_asciidoc_blank_lines_after_header = 1
ij_asciidoc_blank_lines_keep_after_header = 1
ij_asciidoc_formatting_enabled = true
ij_asciidoc_one_sentence_per_line = true
+trim_trailing_whitespace = false
[{*.ant,*.fo,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.qrc,*.rng,*.tld,*.wadl,*.wsdd,*.wsdl,*.xjb,*.xml,*.xsd,*.xsl,*.xslt,*.xul,phpunit.xml.dist}]
ij_xml_align_attributes = true
diff --git a/.github/workflows/changelog-configuration.json b/.github/workflows/changelog-configuration.json
index b6d3b74d..beb21a16 100644
--- a/.github/workflows/changelog-configuration.json
+++ b/.github/workflows/changelog-configuration.json
@@ -55,5 +55,5 @@
"tag_resolver" : {
"method" : "semver"
},
- "base_branches" : ["master"]
+ "base_branches" : ["master-v2"]
}
diff --git a/.github/workflows/pr-build.yaml b/.github/workflows/pr-build.yaml
index 3225f9d9..7d0b36cc 100644
--- a/.github/workflows/pr-build.yaml
+++ b/.github/workflows/pr-build.yaml
@@ -3,7 +3,7 @@ name: build
on:
push:
branches:
- - master
+ - master-v2
pull_request:
jobs:
diff --git a/.run/AsciidocReformater.run.xml b/.run/AsciidocReformater.run.xml
new file mode 100644
index 00000000..0bb7888c
--- /dev/null
+++ b/.run/AsciidocReformater.run.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/JsTemplateDeleter.run.xml b/.run/JsTemplateDeleter.run.xml
new file mode 100644
index 00000000..1e89f156
--- /dev/null
+++ b/.run/JsTemplateDeleter.run.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/JsTestCaseSync.run.xml b/.run/JsTestCaseSync.run.xml
new file mode 100644
index 00000000..d867530b
--- /dev/null
+++ b/.run/JsTestCaseSync.run.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/Reformat Tests.run.xml b/.run/Reformat Tests.run.xml
deleted file mode 100644
index ed6f4681..00000000
--- a/.run/Reformat Tests.run.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/core/pom.xml b/core/pom.xml
index 08036cb5..b64cbccf 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -7,7 +7,7 @@
org.neo4j
neo4j-graphql-java-parent
- 1.9.1-SNAPSHOT
+ 2.0.0-alpha
neo4j-graphql-java
@@ -15,10 +15,26 @@
GraphQL to Cypher Mapping
+
+ org.neo4j
+ neo4j-graphql-neo4j-adapter-api
+ 2.0.0-alpha
+
+
+ org.neo4j
+ neo4j-graphql-neo4j-driver-adapter
+ 2.0.0-alpha
+ test
+
+
+ org.threeten
+ threeten-extra
+ 1.7.0
+
org.neo4j.driver
neo4j-java-driver
- 5.23.0
+ ${neo4j.version}
test
@@ -111,6 +127,13 @@
2.17.2
test
+
+ org.apache.commons
+ commons-csv
+ 1.12.0
+ test
+
+
@@ -137,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/atteo/evo/inflector/EnglischInflector.kt b/core/src/main/kotlin/org/atteo/evo/inflector/EnglischInflector.kt
new file mode 100644
index 00000000..51f02215
--- /dev/null
+++ b/core/src/main/kotlin/org/atteo/evo/inflector/EnglischInflector.kt
@@ -0,0 +1,48 @@
+package org.atteo.evo.inflector
+
+object EnglischInflector : English() {
+
+ private val customRules = mutableListOf()
+
+ init {
+ workaround_irregular("person", "people")
+ // TODO
+ workaround_irregular("two", "twos")
+ workaround_irregular("aircraft", "aircraft")
+ }
+
+ private fun workaround_irregular(singular: String, plural: String) {
+ if (singular[0] == plural[0]) {
+ customRules.add(
+ RegExpRule(
+ "(?i)(" + singular[0] + ")" + singular.substring(1) + "$",
+ "$1" + plural.substring(1)
+ )
+ )
+ } else {
+ customRules.add(
+ RegExpRule(
+ singular[0].uppercaseChar().toString() + "(?i)" + singular.substring(1) + "$",
+ plural[0].uppercaseChar()
+ .toString() + plural.substring(1)
+ )
+ )
+ customRules.add(
+ RegExpRule(
+ singular[0].lowercaseChar().toString() + "(?i)" + singular.substring(1) + "$",
+ plural[0].lowercaseChar().toString() + plural.substring(1)
+ )
+ )
+ }
+ }
+
+ override fun getPlural(word: String?): String {
+ for (rule in customRules) {
+ val result = rule.getPlural(word)
+ if (result != null) {
+ return result
+ }
+ }
+ return super.getPlural(word)
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt b/core/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt
deleted file mode 100644
index 0f9ef3ac..00000000
--- a/core/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt
+++ /dev/null
@@ -1,403 +0,0 @@
-package org.neo4j.graphql
-
-import graphql.language.*
-import graphql.language.TypeDefinition
-import graphql.schema.DataFetcher
-import graphql.schema.idl.ScalarInfo
-import graphql.schema.idl.TypeDefinitionRegistry
-import org.atteo.evo.inflector.English
-import org.neo4j.graphql.handler.projection.ProjectionBase
-
-/**
- * A base class for augmenting a TypeDefinitionRegistry. There a re 2 steps in augmenting the types:
- * 1. augmenting the type by creating the relevant query / mutation fields and adding filtering and sorting to the relation fields
- * 2. generating a data fetcher based on a field definition. The field may be an augmented field (from step 1)
- * but can also be a user specified query / mutation field
- */
-abstract class AugmentationHandler(
- val schemaConfig: SchemaConfig,
- val typeDefinitionRegistry: TypeDefinitionRegistry,
- val neo4jTypeDefinitionRegistry: TypeDefinitionRegistry
-) {
- enum class OperationType {
- QUERY,
- MUTATION
- }
-
- /**
- * The 1st step in enhancing a schema. This method creates relevant query / mutation fields and / or adds filtering and sorting to the relation fields of the given type
- * @param type the type for which the schema should be enhanced / augmented
- */
- open fun augmentType(type: ImplementingTypeDefinition<*>) {}
-
- /**
- * The 2nd step is creating a data fetcher based on a field definition. The field may be an augmented field (from step 1)
- * but can also be a user specified query / mutation field
- * @param operationType the type of the field
- * @param fieldDefinition the filed to create the data fetcher for
- * @return a data fetcher for the field or null if not applicable
- */
- abstract fun createDataFetcher(operationType: OperationType, fieldDefinition: FieldDefinition): DataFetcher?
-
- protected fun buildFieldDefinition(
- prefix: String,
- resultType: ImplementingTypeDefinition<*>,
- scalarFields: List,
- nullableResult: Boolean,
- forceOptionalProvider: (field: FieldDefinition) -> Boolean = { false }
- ): FieldDefinition.Builder {
- var type: Type<*> = TypeName(resultType.name)
- if (!nullableResult) {
- type = NonNullType(type)
- }
- return FieldDefinition.newFieldDefinition()
- .name("$prefix${resultType.name}")
- .inputValueDefinitions(getInputValueDefinitions(scalarFields, false, forceOptionalProvider))
- .type(type)
- }
-
- protected fun getInputValueDefinitions(
- relevantFields: List,
- addFieldOperations: Boolean,
- forceOptionalProvider: (field: FieldDefinition) -> Boolean
- ): List {
- return relevantFields.flatMap { field ->
- var type = getInputType(field.type)
- type = if (forceOptionalProvider(field)) {
- (type as? NonNullType)?.type ?: type
- } else {
- type
- }
- if (addFieldOperations && !field.isNativeId()) {
- val typeDefinition = field.type.resolve()
- ?: throw IllegalArgumentException("type ${field.type.name()} cannot be resolved")
- FieldOperator.forType(typeDefinition, field.type.inner().isNeo4jType(), field.type.isList())
- .map { op ->
- val wrappedType: Type<*> = when {
- op.list -> ListType(NonNullType(TypeName(type.name())))
- else -> type
- }
- input(op.fieldName(field.name), wrappedType)
- }
- } else {
- listOf(input(field.name, type))
- }
- }
- }
-
- protected fun addQueryField(fieldDefinition: FieldDefinition) {
- addOperation(typeDefinitionRegistry.queryTypeName(), fieldDefinition)
- }
-
- protected fun addMutationField(fieldDefinition: FieldDefinition) {
- addOperation(typeDefinitionRegistry.mutationTypeName(), fieldDefinition)
- }
-
- protected fun isRootType(type: ImplementingTypeDefinition<*>): Boolean {
- return type.name == typeDefinitionRegistry.queryTypeName()
- || type.name == typeDefinitionRegistry.mutationTypeName()
- || type.name == typeDefinitionRegistry.subscriptionTypeName()
- }
-
- /**
- * add the given operation to the corresponding rootType
- */
- private fun addOperation(rootTypeName: String, fieldDefinition: FieldDefinition) {
- val rootType = typeDefinitionRegistry.getType(rootTypeName)?.unwrap()
- if (rootType == null) {
- typeDefinitionRegistry.add(
- ObjectTypeDefinition.newObjectTypeDefinition()
- .name(rootTypeName)
- .fieldDefinition(fieldDefinition)
- .build()
- )
- } else {
- val existingRootType = (rootType as? ObjectTypeDefinition
- ?: throw IllegalStateException("root type $rootTypeName is not an object type but ${rootType.javaClass}"))
- if (existingRootType.fieldDefinitions.find { it.name == fieldDefinition.name } != null) {
- return // definition already exists, we don't override it
- }
- typeDefinitionRegistry.remove(rootType)
- typeDefinitionRegistry.add(rootType.transform { it.fieldDefinition(fieldDefinition) })
- }
- }
-
- protected fun addFilterType(
- type: ImplementingTypeDefinition<*>,
- createdTypes: MutableSet = mutableSetOf()
- ): String {
- val filterName = if (schemaConfig.useWhereFilter) type.name + "Where" else "_${type.name}Filter"
- if (createdTypes.contains(filterName)) {
- return filterName
- }
- val existingFilterType = typeDefinitionRegistry.getType(filterName).unwrap()
- if (existingFilterType != null) {
- return (existingFilterType as? InputObjectTypeDefinition)?.name
- ?: throw IllegalStateException("Filter type $filterName is already defined but not an input type")
- }
- createdTypes.add(filterName)
- val builder = InputObjectTypeDefinition.newInputObjectDefinition()
- .name(filterName)
- listOf("AND", "OR", "NOT").forEach {
- builder.inputValueDefinition(
- InputValueDefinition.newInputValueDefinition()
- .name(it)
- .type(ListType(NonNullType(TypeName(filterName))))
- .build()
- )
- }
- type.fieldDefinitions
- .filterNot { it.isIgnored() }
- .filter { it.dynamicPrefix() == null } // TODO currently we do not support filtering on dynamic properties
- .forEach { field ->
- val typeDefinition = field.type.resolve()
- ?: throw IllegalArgumentException("type ${field.type.name()} cannot be resolved")
- val filterType = when {
- field.type.inner().isNeo4jType() -> getInputType(field.type).name()!!
- typeDefinition is ScalarTypeDefinition -> typeDefinition.name
- typeDefinition is EnumTypeDefinition -> typeDefinition.name
- typeDefinition is ImplementingTypeDefinition -> {
- when {
- field.type.inner().isNeo4jType() -> typeDefinition.name
- else -> addFilterType(typeDefinition, createdTypes)
- }
- }
-
- else -> throw IllegalArgumentException("${field.type.name()} is neither an object nor an interface")
- }
-
- if (field.isRelationship()) {
- RelationOperator.createRelationFilterFields(type, field, filterType, builder)
- } else {
- FieldOperator.forType(typeDefinition, field.type.inner().isNeo4jType(), field.type.isList())
- .forEach { op ->
- when {
- field.type.isList() -> builder.addArrayFilterField(
- op.fieldName(field.name),
- filterType,
- field.description
- )
-
- else -> builder.addFilterField(
- op.fieldName(field.name),
- op.list,
- filterType,
- field.description
- )
- }
- }
- if (typeDefinition.isNeo4jSpatialType()) {
- val distanceFilterType =
- getSpatialDistanceFilter(neo4jTypeDefinitionRegistry.getUnwrappedType(filterType) as TypeDefinition<*>)
- FieldOperator.forType(distanceFilterType, true, field.type.isList())
- .forEach { op ->
- builder.addFilterField(
- op.fieldName(field.name + NEO4j_POINT_DISTANCE_FILTER_SUFFIX),
- op.list,
- NEO4j_POINT_DISTANCE_FILTER
- )
- }
- }
- }
-
- }
- typeDefinitionRegistry.add(builder.build())
- return filterName
- }
-
- private fun getSpatialDistanceFilter(pointType: TypeDefinition<*>): InputObjectTypeDefinition {
- return addInputType(
- NEO4j_POINT_DISTANCE_FILTER, listOf(
- input("distance", NonNullType(TypeFloat)),
- input("point", NonNullType(TypeName(pointType.name)))
- )
- )
- }
-
- protected fun addOptions(type: ImplementingTypeDefinition<*>): String {
- val optionsName = "${type.name}Options"
- val optionsType = typeDefinitionRegistry.getType(optionsName)?.unwrap()
- if (optionsType != null) {
- return (optionsType as? InputObjectTypeDefinition)?.name
- ?: throw IllegalStateException("Ordering type $type.name is already defined but not an input type")
- }
- val sortTypeName = addSortInputType(type)
- val optionsTypeBuilder = InputObjectTypeDefinition.newInputObjectDefinition().name(optionsName)
- if (sortTypeName != null) {
- optionsTypeBuilder.inputValueDefinition(
- input(
- ProjectionBase.SORT,
- ListType(NonNullType(TypeName(sortTypeName))),
- "Specify one or more $sortTypeName objects to sort ${English.plural(type.name)} by. The sorts will be applied in the order in which they are arranged in the array."
- )
- )
- }
- optionsTypeBuilder
- .inputValueDefinition(
- input(
- ProjectionBase.LIMIT,
- TypeInt,
- "Defines the maximum amount of records returned"
- )
- )
- .inputValueDefinition(input(ProjectionBase.SKIP, TypeInt, "Defines the amount of records to be skipped"))
- .build()
- typeDefinitionRegistry.add(optionsTypeBuilder.build())
- return optionsName
- }
-
- private fun addSortInputType(type: ImplementingTypeDefinition<*>): String? {
- val sortTypeName = "${type.name}Sort"
- val sortType = typeDefinitionRegistry.getType(sortTypeName)?.unwrap()
- if (sortType != null) {
- return (sortType as? InputObjectTypeDefinition)?.name
- ?: throw IllegalStateException("Ordering type $type.name is already defined but not an input type")
- }
- val relevantFields = type.getScalarFields()
- if (relevantFields.isEmpty()) {
- return null
- }
- val builder = InputObjectTypeDefinition.newInputObjectDefinition()
- .name(sortTypeName)
- .description("Fields to sort ${type.name}s by. The order in which sorts are applied is not guaranteed when specifying many fields in one MovieSort object.".asDescription())
- for (relevantField in relevantFields) {
- builder.inputValueDefinition(input(relevantField.name, TypeName("SortDirection")))
- }
- typeDefinitionRegistry.add(builder.build())
- return sortTypeName
- }
-
- protected fun addOrdering(type: ImplementingTypeDefinition<*>): String? {
- val orderingName = "_${type.name}Ordering"
- var existingOrderingType = typeDefinitionRegistry.getType(orderingName)?.unwrap()
- if (existingOrderingType != null) {
- return (existingOrderingType as? EnumTypeDefinition)?.name
- ?: throw IllegalStateException("Ordering type $type.name is already defined but not an input type")
- }
- val sortingFields = type.getScalarFields()
- if (sortingFields.isEmpty()) {
- return null
- }
- existingOrderingType = EnumTypeDefinition.newEnumTypeDefinition()
- .name(orderingName)
- .enumValueDefinitions(sortingFields.flatMap { fd ->
- listOf("_asc", "_desc")
- .map {
- EnumValueDefinition
- .newEnumValueDefinition()
- .name(fd.name + it)
- .build()
- }
- })
- .build()
- typeDefinitionRegistry.add(existingOrderingType)
- return orderingName
- }
-
- private fun addInputType(inputName: String, relevantFields: List): InputObjectTypeDefinition {
- var inputType = typeDefinitionRegistry.getType(inputName)?.unwrap()
- if (inputType != null) {
- return inputType as? InputObjectTypeDefinition
- ?: throw IllegalStateException("Filter type $inputName is already defined but not an input type")
- }
- inputType = getInputType(inputName, relevantFields)
- typeDefinitionRegistry.add(inputType)
- return inputType
- }
-
- private fun getInputType(inputName: String, relevantFields: List): InputObjectTypeDefinition {
- return InputObjectTypeDefinition.newInputObjectDefinition()
- .name(inputName)
- .inputValueDefinitions(relevantFields)
- .build()
- }
-
- private fun getInputType(type: Type<*>): Type<*> {
- if (type.inner().isNeo4jType()) {
- return neo4jTypeDefinitions
- .find { it.typeDefinition == type.name() }
- ?.let { TypeName(it.inputDefinition) }
- ?: throw IllegalArgumentException("Cannot find input type for ${type.name()}")
- }
- return type
- }
-
- private fun getTypeFromAnyRegistry(name: String?): TypeDefinition<*>? =
- typeDefinitionRegistry.getUnwrappedType(name)
- ?: neo4jTypeDefinitionRegistry.getUnwrappedType(name)
-
- fun ImplementingTypeDefinition<*>.relationship(): RelationshipInfo>? =
- RelationshipInfo.create(this, neo4jTypeDefinitionRegistry)
-
- fun ImplementingTypeDefinition<*>.getScalarFields(): List = fieldDefinitions
- .filterNot { it.isIgnored() }
- .filter { it.type.inner().isScalar() || it.type.inner().isNeo4jType() || it.type.inner().isEnum() }
- .sortedByDescending { it.type.inner().isID() }
-
- fun ImplementingTypeDefinition<*>.getFieldDefinition(name: String) = this.fieldDefinitions
- .filterNot { it.isIgnored() }
- .find { it.name == name }
-
- fun ImplementingTypeDefinition<*>.getIdField() = this.fieldDefinitions
- .filterNot { it.isIgnored() }
- .find { it.type.inner().isID() }
-
- fun Type<*>.resolve(): TypeDefinition<*>? = getTypeFromAnyRegistry(name())
- fun Type<*>.isScalar(): Boolean = resolve() is ScalarTypeDefinition
- fun Type<*>.isEnum(): Boolean = resolve() is EnumTypeDefinition
- private fun Type<*>.isNeo4jType(): Boolean = name()
- ?.takeIf {
- !ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.containsKey(it)
- && it.startsWith("_Neo4j") // TODO remove this check by refactoring neo4j input types
- }
- ?.let { neo4jTypeDefinitionRegistry.getUnwrappedType(it) } != null
-
- fun Type<*>.isID(): Boolean = name() == "ID"
-
-
- fun FieldDefinition.isNativeId(): Boolean = name == ProjectionBase.NATIVE_ID
- fun FieldDefinition.dynamicPrefix(): String? =
- getDirectiveArgument(DirectiveConstants.DYNAMIC, DirectiveConstants.DYNAMIC_PREFIX, null)
-
- fun FieldDefinition.isRelationship(): Boolean =
- !type.inner().isNeo4jType() && type.resolve() is ImplementingTypeDefinition<*>
-
- fun TypeDefinitionRegistry.getUnwrappedType(name: String?): TypeDefinition>? =
- getType(name)?.unwrap()
-
- fun DirectivesContainer<*>.cypherDirective(): CypherDirective? = if (hasDirective(DirectiveConstants.CYPHER)) {
- CypherDirective(
- getMandatoryDirectiveArgument(DirectiveConstants.CYPHER, DirectiveConstants.CYPHER_STATEMENT),
- getMandatoryDirectiveArgument(DirectiveConstants.CYPHER, DirectiveConstants.CYPHER_PASS_THROUGH, false)
- )
- } else {
- null
- }
-
- fun DirectivesContainer<*>.getDirectiveArgument(
- directiveName: String,
- argumentName: String,
- defaultValue: T? = null
- ): T? =
- getDirectiveArgument(neo4jTypeDefinitionRegistry, directiveName, argumentName, defaultValue)
-
- private fun DirectivesContainer<*>.getMandatoryDirectiveArgument(
- directiveName: String,
- argumentName: String,
- defaultValue: T? = null
- ): T =
- getDirectiveArgument(directiveName, argumentName, defaultValue)
- ?: throw IllegalStateException("No default value for @${directiveName}::$argumentName")
-
- fun input(name: String, type: Type<*>, description: String? = null): InputValueDefinition {
- val input = InputValueDefinition
- .newInputValueDefinition()
- .name(name)
- .type(type)
- if (description != null) {
- input.description(description.asDescription())
- }
- return input
- .build()
- }
-}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/Constants.kt b/core/src/main/kotlin/org/neo4j/graphql/Constants.kt
new file mode 100644
index 00000000..f39ddd3f
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/Constants.kt
@@ -0,0 +1,89 @@
+package org.neo4j.graphql
+
+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"
+ const val AND = "AND"
+ const val NOT = "NOT"
+ const val OR = "OR"
+ const val LIMIT = "limit"
+ const val OFFSET = "offset"
+ const val SORT = "sort"
+ const val FIRST = "first"
+ const val AFTER = "after"
+ const val DIRECTED = "directed"
+ const val EDGE_FIELD = "edge"
+ const val PROPERTIES_FIELD = "properties"
+ const val TOTAL_COUNT = "totalCount"
+ const val PAGE_INFO = "pageInfo"
+ const val EDGES_FIELD = "edges"
+ const val CURSOR_FIELD = "cursor"
+ const val NODE_FIELD = "node"
+ const val RELATIONSHIP_FIELD = "relationship"
+ const val TYPENAME_IN = "typename"
+
+ const val RESOLVE_TYPE = TYPE_NAME
+ const val RESOLVE_ID = "__id"
+
+ const val POINT_TYPE = "Point"
+ const val CARTESIAN_POINT_TYPE = "CartesianPoint"
+ const val POINT_INPUT_TYPE = "PointInput"
+ const val CARTESIAN_POINT_INPUT_TYPE = "CartesianPointInput"
+
+ const val BOOLEAN = "Boolean"
+ const val ID = "ID"
+ const val STRING = "String"
+ const val INT = "Int"
+ const val FLOAT = "Float"
+ const val DATE = "Date"
+ const val TIME = "Time"
+ const val LOCAL_TIME = "LocalTime"
+ const val DATE_TIME = "DateTime"
+ const val LOCAL_DATE_TIME = "LocalDateTime"
+ const val BIG_INT = "BigInt"
+ const val DURATION = "Duration"
+
+ val TEMPORAL_TYPES = setOf(DATE, TIME, LOCAL_TIME, DATE_TIME, LOCAL_DATE_TIME)
+ val POINT_TYPES = setOf(POINT_TYPE, CARTESIAN_POINT_TYPE)
+
+ val RESERVED_INTERFACE_FIELDS = mapOf(
+ NODE_FIELD to "Interface field name 'node' reserved to support relay See https://relay.dev/graphql/",
+ CURSOR_FIELD to "Interface field name 'cursor' reserved to support relay See https://relay.dev/graphql/"
+ )
+
+ val FORBIDDEN_RELATIONSHIP_PROPERTY_DIRECTIVES = setOf(
+ RelationshipDirective.NAME,
+ )
+
+ const val WHERE = "where"
+
+ object Types {
+ val ID = TypeName("ID")
+ val Int = TypeName(INT)
+ val Float = TypeName(FLOAT)
+ val String = TypeName(STRING)
+ val Boolean = TypeName("Boolean")
+
+ val PageInfo = TypeName("PageInfo")
+ val SortDirection = TypeName("SortDirection")
+ val PointDistance = TypeName("PointDistance")
+ val CartesianPointDistance = TypeName("CartesianPointDistance")
+
+ val POINT = TypeName(POINT_TYPE)
+ val CARTESIAN_POINT = TypeName(CARTESIAN_POINT_TYPE)
+ }
+
+
+ object SpatialReferenceIdentifier {
+ const val wgs_84_2D = 4326
+ const val wgs_84_3D = 4979
+ const val cartesian_2D = 7203
+ const val cartesian_3D = 9157
+
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/Cypher.kt b/core/src/main/kotlin/org/neo4j/graphql/Cypher.kt
deleted file mode 100644
index d33a2b19..00000000
--- a/core/src/main/kotlin/org/neo4j/graphql/Cypher.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.neo4j.graphql
-
-import graphql.schema.GraphQLType
-
-data class Cypher @JvmOverloads constructor(
- val query: String,
- val params: Map = emptyMap(),
- var type: GraphQLType? = null,
- val variable: String
-) {
- fun with(p: Map) = this.copy(params = this.params + p)
-}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/DataFetchingInterceptor.kt b/core/src/main/kotlin/org/neo4j/graphql/DataFetchingInterceptor.kt
deleted file mode 100644
index 8f3aa24a..00000000
--- a/core/src/main/kotlin/org/neo4j/graphql/DataFetchingInterceptor.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.neo4j.graphql
-
-import graphql.schema.DataFetcher
-import graphql.schema.DataFetchingEnvironment
-
-/**
- * Interceptor to hook in database driver binding
- */
-interface DataFetchingInterceptor {
-
- /**
- * Called by the Graphql runtime for each method augmented by this library. The custom code should call the delegate
- * to get a cypher query. This query can then be forwarded to the neo4j driver to retrieve the data.
- * The method then returns the fully parsed result.
- */
- @Throws(Exception::class)
- fun fetchData(env: DataFetchingEnvironment, delegate: DataFetcher): Any?
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/org/neo4j/graphql/DirectiveConstants.kt b/core/src/main/kotlin/org/neo4j/graphql/DirectiveConstants.kt
deleted file mode 100644
index d1034331..00000000
--- a/core/src/main/kotlin/org/neo4j/graphql/DirectiveConstants.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.neo4j.graphql
-
-class DirectiveConstants {
-
- companion object {
-
- const val IGNORE = "ignore"
- const val RELATION = "relation"
- const val RELATION_NAME = "name"
- const val RELATION_DIRECTION = "direction"
- const val RELATION_FROM = "from"
- const val RELATION_TO = "to"
-
- const val CYPHER = "cypher"
- const val CYPHER_STATEMENT = "statement"
- const val CYPHER_PASS_THROUGH = "passThrough"
-
- const val PROPERTY = "property"
- const val PROPERTY_NAME = "name"
-
- const val DYNAMIC = "dynamic"
- const val DYNAMIC_PREFIX = "prefix"
- }
-}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/DynamicProperties.kt b/core/src/main/kotlin/org/neo4j/graphql/DynamicProperties.kt
deleted file mode 100644
index 2caf53ce..00000000
--- a/core/src/main/kotlin/org/neo4j/graphql/DynamicProperties.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-package org.neo4j.graphql
-
-import graphql.Assert
-import graphql.GraphQLContext
-import graphql.execution.CoercedVariables
-import graphql.language.*
-import graphql.schema.*
-import java.util.*
-
-object DynamicProperties {
-
- val INSTANCE: GraphQLScalarType = GraphQLScalarType.newScalar()
- .name("DynamicProperties")
- .coercing(object : Coercing {
- @Throws(CoercingSerializeException::class)
- override fun serialize(input: Any, graphQLContext: GraphQLContext, locale: Locale): Any {
- return input
- }
-
- @Throws(CoercingParseLiteralException::class)
- override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): Any {
- return input
- }
-
- @Throws(CoercingParseLiteralException::class)
- override fun parseLiteral(
- input: Value<*>,
- variables: CoercedVariables,
- graphQLContext: GraphQLContext,
- locale: Locale
- ): Any {
- return parse(input, emptyMap())
- }
- })
- .build()
-
-
- @Throws(CoercingParseLiteralException::class)
- private fun parse(input: Any, variables: Map): Any {
- return when (input) {
- !is Value<*> -> throw CoercingParseLiteralException("Expected AST type 'StringValue' but was '${input::class.java.simpleName}'.")
- is NullValue -> throw CoercingParseLiteralException("Expected non null value.")
- is ObjectValue -> input.objectFields.associate { it.name to parseNested(it.value, variables) }
- else -> Assert.assertShouldNeverHappen("Only maps structures are expected")
- }
- }
-
- @Throws(CoercingParseLiteralException::class)
- private fun parseNested(input: Any, variables: Map): Any? {
- return when (input) {
- !is Value<*> -> throw CoercingParseLiteralException("Expected AST type 'StringValue' but was '${input::class.java.simpleName}'.")
- is NullValue -> null
- is FloatValue -> input.value
- is StringValue -> input.value
- is IntValue -> input.value
- is BooleanValue -> input.isValue
- is EnumValue -> input.name
- is VariableReference -> variables[input.name]
- is ArrayValue -> input.values.map { v -> parseNested(v, variables) }
- is ObjectValue -> throw IllegalArgumentException("deep structures not supported for dynamic properties")
- else -> Assert.assertShouldNeverHappen("We have covered all Value types")
- }
- }
-
-}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt b/core/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt
index 9643258a..34033603 100644
--- a/core/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt
+++ b/core/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt
@@ -1,34 +1,20 @@
package org.neo4j.graphql
+import graphql.language.Argument
import graphql.language.Description
-import graphql.language.VariableReference
-import graphql.schema.GraphQLOutputType
+import graphql.language.Directive
+import graphql.language.StringValue
import org.neo4j.cypherdsl.core.*
-import org.neo4j.cypherdsl.core.Cypher
-import org.neo4j.cypherdsl.core.Cypher.elementId
+import org.neo4j.cypherdsl.core.StatementBuilder.*
+import org.neo4j.graphql.schema.model.inputs.Dict
+import org.neo4j.graphql.schema.model.inputs.connection.ConnectionSort
+import org.neo4j.graphql.schema.model.inputs.options.OptionsInput
+import org.neo4j.graphql.schema.model.inputs.options.SortInput
import java.util.*
-fun queryParameter(value: Any?, vararg parts: String?): Parameter<*> {
- val name = when (value) {
- is VariableReference -> value.name
- else -> normalizeName(*parts)
- }
- return Cypher.parameter(name).withValue(value?.toJavaValue())
-}
-
-fun Expression.collect(type: GraphQLOutputType) = if (type.isList()) Cypher.collect(this) else this
-fun StatementBuilder.OngoingReading.withSubQueries(subQueries: List) =
+fun OngoingReading.withSubQueries(subQueries: List) =
subQueries.fold(this, { it, sub -> it.call(sub) })
-fun normalizeName(vararg parts: String?) =
- parts.mapNotNull { it?.capitalize() }.filter { it.isNotBlank() }.joinToString("").decapitalize()
-
-fun PropertyContainer.id(): FunctionInvocation = when (this) {
- is Node -> elementId(this)
- is Relationship -> elementId(this)
- else -> throw IllegalArgumentException("Id can only be retrieved for Nodes or Relationships")
-}
-
fun String.toCamelCase(): String = Regex("[\\W_]([a-z])").replace(this) { it.groupValues[1].toUpperCase() }
fun Optional.unwrap(): T? = orElse(null)
@@ -41,3 +27,148 @@ fun String.capitalize(): String =
fun String.decapitalize(): String = replaceFirstChar { it.lowercase(Locale.getDefault()) }
fun String.toUpperCase(): String = uppercase(Locale.getDefault())
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() }
+ ?.let { conditions ->
+ var result = Cypher.noCondition()
+ conditions.forEach {
+ result = result and it
+ }
+ result
+ }
+
+fun Named.name(): String = this.requiredSymbolicName.value
+
+fun OngoingReading.applySortingSkipAndLimit(
+ orderAndLimit: OptionsInput?,
+ extractSortFields: (T) -> Collection,
+ queryContext: QueryContext,
+ withVars: List? = null,
+ enforceAsterix: Boolean = false,
+): OngoingReading {
+ var asterixEnforced = false
+ if (orderAndLimit == null || orderAndLimit.isEmpty()) {
+ return this
+ }
+ val ordered = orderAndLimit.sort
+ .takeIf { it.isNotEmpty() }
+ ?.let { sortItem ->
+ if (this is ExposesOrderBy) {
+ if (enforceAsterix) {
+ asterixEnforced = true
+ this.with(Cypher.asterisk())
+ } else {
+ this
+ }
+ } else {
+ if (withVars != null) {
+ this.with(*withVars.toTypedArray())
+ } else {
+ this.with(Cypher.asterisk())
+ }
+ }.orderBy(sortItem.flatMap { extractSortFields(it) })
+ }
+ ?: this
+ val skip = orderAndLimit.offset?.let {
+ if (ordered is ExposesSkip) {
+ ordered
+ } else {
+ if (withVars != null) {
+ this.with(*withVars.toTypedArray())
+ } else {
+ ordered.with(Cypher.asterisk())
+ }
+ }.skip(queryContext.getNextParam(it))
+ }
+ ?: ordered
+ return orderAndLimit.limit?.let {
+ if (skip is ExposesLimit) {
+ if (enforceAsterix && !asterixEnforced) {
+ skip.with(Cypher.asterisk())
+ } else {
+ skip
+ }
+ } else {
+ if (withVars != null) {
+ this.with(*withVars.toTypedArray())
+ } else {
+ skip.with(Cypher.asterisk())
+ }
+ }.limit(queryContext.getNextParam(it))
+ }
+ ?: skip
+}
+
+fun OngoingReading.applySortingSkipAndLimit(
+ p: PropertyAccessor,
+ optionsInput: OptionsInput?,
+ queryContext: QueryContext,
+ withVars: List? = null,
+ enforceAsterix: Boolean = false,
+ alreadyProjected: Boolean = false,
+): OngoingReading {
+ return this.applySortingSkipAndLimit(
+ optionsInput,
+ { it.getCypherSortFields(p::property, alreadyProjected) },
+ queryContext,
+ withVars = withVars,
+ enforceAsterix = enforceAsterix
+ )
+}
+
+fun OngoingReading.applySortingSkipAndLimit(
+ optionsInput: OptionsInput?,
+ node: PropertyAccessor,
+ edge: PropertyAccessor,
+ queryContext: QueryContext,
+ withVars: List? = null,
+ enforceAsterix: Boolean = false,
+ alreadyProjected: Boolean = false,
+): OngoingReading {
+ return this.applySortingSkipAndLimit(
+ optionsInput,
+ {
+ listOf(
+ it.node?.getCypherSortFields(node::property, alreadyProjected),
+ it.edge?.getCypherSortFields(edge::property, alreadyProjected),
+ )
+ .filterNotNull()
+ .flatten()
+ },
+ queryContext,
+ withVars = withVars,
+ enforceAsterix = enforceAsterix
+ )
+}
+
+
+inline fun T.wrapList(): List = when (this) {
+ is List<*> -> this
+ .filterNotNull()
+ .filterIsInstance()
+ .also { check(it.size == this.size, { "expected only elements of type " + T::class.java }) }
+
+ else -> listOf(this)
+}
+
+fun OngoingReadingWithoutWhere.optionalWhere(condition: List): OngoingReading =
+ optionalWhere(condition.filterNotNull().takeIf { it.isNotEmpty() }?.foldWithAnd())
+
+fun OngoingReadingWithoutWhere.optionalWhere(condition: Condition?): OngoingReading =
+ if (condition != null) this.where(condition) else this
+
+fun OrderableOngoingReadingAndWithWithoutWhere.optionalWhere(condition: Condition?): OngoingReading =
+ if (condition != null) this.where(condition) else this
+
+fun Any?.toDict() = Dict.create(this) ?: Dict.EMPTY
+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/GraphQLExtensions.kt b/core/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt
index fc53b825..c1be8a43 100644
--- a/core/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt
+++ b/core/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt
@@ -1,29 +1,20 @@
package org.neo4j.graphql
import graphql.GraphQLContext
-import graphql.Scalars
import graphql.language.*
-import graphql.language.TypeDefinition
import graphql.schema.*
import graphql.schema.idl.TypeDefinitionRegistry
-import org.neo4j.cypherdsl.core.SymbolicName
-import org.neo4j.graphql.DirectiveConstants.Companion.CYPHER
-import org.neo4j.graphql.DirectiveConstants.Companion.CYPHER_PASS_THROUGH
-import org.neo4j.graphql.DirectiveConstants.Companion.CYPHER_STATEMENT
-import org.neo4j.graphql.DirectiveConstants.Companion.DYNAMIC
-import org.neo4j.graphql.DirectiveConstants.Companion.DYNAMIC_PREFIX
-import org.neo4j.graphql.DirectiveConstants.Companion.PROPERTY
-import org.neo4j.graphql.DirectiveConstants.Companion.PROPERTY_NAME
-import org.neo4j.graphql.DirectiveConstants.Companion.RELATION_NAME
-import org.neo4j.graphql.DirectiveConstants.Companion.RELATION_TO
-import org.neo4j.graphql.handler.projection.ProjectionBase
-import org.slf4j.LoggerFactory
-
-fun Type<*>.name(): String? = if (this.inner() is TypeName) (this.inner() as TypeName).name else null
-fun Type<*>.inner(): Type<*> = when (this) {
+import org.neo4j.cypherdsl.core.Cypher
+import org.neo4j.graphql.domain.directives.Annotations
+import kotlin.reflect.KProperty1
+
+fun Type<*>.name(): String = this.inner().name
+
+fun Type<*>.inner(): TypeName = when (this) {
is ListType -> this.type.inner()
is NonNullType -> this.type.inner()
- else -> this
+ is TypeName -> this
+ else -> throw IllegalStateException("Inner type is expected to hava a name")
}
fun Type<*>.isList(): Boolean = when (this) {
@@ -32,176 +23,50 @@ fun Type<*>.isList(): Boolean = when (this) {
else -> false
}
-fun GraphQLType.inner(): GraphQLType = when (this) {
- is GraphQLList -> this.wrappedType.inner()
- is GraphQLNonNull -> this.wrappedType.inner()
- else -> this
-}
-
-fun GraphQLType.name(): String? = (this as? GraphQLNamedType)?.name
-fun GraphQLType.requiredName(): String =
- requireNotNull(name()) { "name is required but cannot be determined for " + this.javaClass }
-
-fun GraphQLType.isList() = this is GraphQLList || (this is GraphQLNonNull && this.wrappedType is GraphQLList)
-fun GraphQLType.isNeo4jType() = this.innerName().startsWith("_Neo4j")
-fun GraphQLType.isNeo4jTemporalType() = NEO4j_TEMPORAL_TYPES.contains(this.innerName())
-
-fun TypeDefinition<*>.isNeo4jSpatialType() = this.name.startsWith("_Neo4jPoint")
-
-fun GraphQLFieldDefinition.isNeo4jType(): Boolean = this.type.isNeo4jType()
-fun GraphQLFieldDefinition.isNeo4jTemporalType(): Boolean = this.type.isNeo4jTemporalType()
-
-fun GraphQLFieldDefinition.isRelationship() =
- !type.isNeo4jType() && this.type.inner().let { it is GraphQLFieldsContainer }
-
-fun GraphQLFieldsContainer.isRelationType() =
- (this as? GraphQLDirectiveContainer)?.getAppliedDirective(DirectiveConstants.RELATION) != null
-
-fun GraphQLFieldsContainer.relationshipFor(name: String): RelationshipInfo? {
- val field = getRelevantFieldDefinition(name)
- ?: throw IllegalArgumentException("$name is not defined on ${this.name}")
- val fieldObjectType = field.type.inner() as? GraphQLImplementingType ?: return null
-
- val (relDirective, inverse) = if (isRelationType()) {
- val typeName = this.name
- (this as? GraphQLDirectiveContainer)
- ?.getAppliedDirective(DirectiveConstants.RELATION)?.let {
- // do inverse mapping, if the current type is the `to` mapping of the relation
- it to (fieldObjectType.getRelevantFieldDefinition(
- it.getArgument(
- RELATION_TO,
- null as String?
- )
- )?.name == typeName)
- }
- ?: throw IllegalStateException("Type ${this.name} needs an @relation directive")
- } else {
- (fieldObjectType as? GraphQLDirectiveContainer)
- ?.getAppliedDirective(DirectiveConstants.RELATION)?.let { it to true }
- ?: field.getAppliedDirective(DirectiveConstants.RELATION)?.let { it to false }
- ?: throw IllegalStateException("Field $field needs an @relation directive")
- }
-
- val relInfo = RelationshipInfo.create(fieldObjectType, relDirective)
-
- return if (inverse) relInfo.copy(
- direction = relInfo.direction.invert(),
- startField = relInfo.endField,
- endField = relInfo.startField
- ) else relInfo
+fun Type<*>.isRequired(): Boolean = when (this) {
+ is NonNullType -> true
+ else -> false
}
-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 String.asRequiredType() = asType(true)
+fun String.asType(required: Boolean = false) = TypeName(this).makeRequired(required)
+fun String.wrapLike(type: Type<*>) = TypeName(this).wrapLike(type)
+fun TypeName.wrapLike(type: Type<*>): Type<*> = when (type) {
+ is ListType -> this.wrapLike(type.type).List
+ is NonNullType -> this.wrapLike(type.type).NonNull
+ is TypeName -> this
+ else -> throw IllegalStateException("Unknown type")
}
-fun GraphQLFieldsContainer.label(): String = when {
- this.isRelationType() ->
- (this as? GraphQLDirectiveContainer)
- ?.getAppliedDirective(DirectiveConstants.RELATION)
- ?.getArgument(RELATION_NAME)?.argumentValue?.value?.toJavaValue()?.toString()
- ?: this.name
+val String.NonNull get() = asType(true)
+val String.List get() = asType().List
- else -> name
-}
-
-fun GraphQLFieldsContainer.relationship(): RelationshipInfo? = RelationshipInfo.create(this)
+val Type<*>.NonNull
+ get() = when (this) {
+ is NonNullType -> this
+ else -> NonNullType(this)
+ }
-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())
-}
+val Type<*>.List
+ get() = when (this) {
+ is ListType -> this
+ else -> ListType(this)
+ }
-fun Field.aliasOrName(): String = (this.alias ?: this.name)
-fun SelectedField.aliasOrName(): String = (this.alias ?: this.name)
-fun SelectedField.contextualize(variable: String) = variable + this.aliasOrName().capitalize()
-fun SelectedField.contextualize(variable: SymbolicName) = variable.value + this.aliasOrName().capitalize()
-
-fun GraphQLType.innerName(): String = inner().name()
- ?: throw IllegalStateException("inner name cannot be retrieved for " + this.javaClass)
-
-fun GraphQLFieldDefinition.propertyName() = getDirectiveArgument(PROPERTY, PROPERTY_NAME, this.name)!!
-
-fun GraphQLFieldDefinition.dynamicPrefix(): String? = getDirectiveArgument(DYNAMIC, DYNAMIC_PREFIX, null as String?)
-fun GraphQLType.getInnerFieldsContainer() = inner() as? GraphQLFieldsContainer
- ?: throw IllegalArgumentException("${this.innerName()} is neither an object nor an interface")
-
-fun GraphQLDirectiveContainer.getDirectiveArgument(
- directiveName: String,
- argumentName: String,
- defaultValue: T?
-): T? =
- getAppliedDirective(directiveName)?.getArgument(argumentName, defaultValue) ?: defaultValue
-
-@Suppress("UNCHECKED_CAST")
-fun DirectivesContainer<*>.getDirectiveArgument(
- typeRegistry: TypeDefinitionRegistry,
- directiveName: String,
- argumentName: String,
- defaultValue: T? = null
-): T? {
- return (getDirective(directiveName) ?: return defaultValue)
- .getArgument(argumentName)?.value?.toJavaValue() as T?
- ?: typeRegistry.getDirectiveDefinition(directiveName)
- ?.unwrap()
- ?.inputValueDefinitions
- ?.find { inputValueDefinition -> inputValueDefinition.name == argumentName }
- ?.defaultValue?.toJavaValue() as T?
- ?: defaultValue
+fun TypeName.makeRequired(required: Boolean = true): Type<*> = when (required) {
+ true -> this.NonNull
+ else -> this
}
-fun DirectivesContainer<*>.getDirective(name: String): Directive? = directives.firstOrNull { it.name == name }
-
-fun DirectivesContainer<*>.getMandatoryDirectiveArgument(
- typeRegistry: TypeDefinitionRegistry,
- directiveName: String,
- argumentName: String,
- defaultValue: T? = null
-): T =
- getDirectiveArgument(typeRegistry, directiveName, argumentName, defaultValue)
- ?: throw IllegalStateException("No default value for @${directiveName}::$argumentName")
-
-fun GraphQLAppliedDirective.getMandatoryArgument(argumentName: String, defaultValue: T? = null): T =
- this.getArgument(argumentName, defaultValue)
- ?: throw IllegalStateException(argumentName + " is required for @${this.name}")
-
-fun GraphQLAppliedDirective.getArgument(argumentName: String, defaultValue: T? = null): T? {
- val argument = getArgument(argumentName)
- @Suppress("UNCHECKED_CAST")
- return when {
- argument.argumentValue.isSet && argument.argumentValue.value != null -> argument.argumentValue.value?.toJavaValue() as T?
- argument.definition?.value != null -> argument.definition?.value?.toJavaValue() as T?
- else -> defaultValue ?: throw IllegalStateException("No default value for @${this.name}::$argumentName")
- }
+fun Type<*>.isListElementRequired(): Boolean = when (this) {
+ is ListType -> type is NonNullType
+ is NonNullType -> type.isListElementRequired()
+ else -> false
}
-fun GraphQLFieldDefinition.cypherDirective(): CypherDirective? = getAppliedDirective(CYPHER)?.let {
- val originalStatement = it.getMandatoryArgument(CYPHER_STATEMENT)
- // Arguments on the field are passed to the Cypher statement and can be used by name.
- // They must not be prefixed by $ since they are no longer parameters. Just use the same name as the fields' argument.
- val rewrittenStatement = originalStatement.replace(Regex("\\\$([_a-zA-Z]\\w*)"), "$1")
- if (originalStatement != rewrittenStatement) {
- LoggerFactory.getLogger(CypherDirective::class.java)
- .warn(
- "The field arguments used in the directives statement must not contain parameters. The statement was replaced. Please adjust your GraphQl Schema.\n\tGot : {}\n\tReplaced by: {}\n\tField : {} ({})",
- originalStatement, rewrittenStatement, this.name, this.definition?.sourceLocation
- )
- }
- CypherDirective(rewrittenStatement, it.getMandatoryArgument(CYPHER_PASS_THROUGH, false))
-}
+fun GraphQLType.isList() = this is GraphQLList || (this is GraphQLNonNull && this.wrappedType is GraphQLList)
-data class CypherDirective(val statement: String, val passThrough: Boolean)
+fun SelectedField.aliasOrName(): String = (this.alias ?: this.name)
fun Any.toJavaValue() = when (this) {
is Value<*> -> this.toJavaValue()
@@ -221,77 +86,67 @@ fun Value<*>.toJavaValue(): Any? = when (this) {
else -> throw IllegalStateException("Unhandled value $this")
}
-fun GraphQLFieldDefinition.isID() = this.type.inner() == Scalars.GraphQLID
-fun GraphQLFieldDefinition.isNativeId() = this.name == ProjectionBase.NATIVE_ID
-fun GraphQLFieldDefinition.isIgnored() = getDirective(DirectiveConstants.IGNORE) != null
-fun FieldDefinition.isIgnored(): Boolean = hasDirective(DirectiveConstants.IGNORE)
-
-fun GraphQLFieldsContainer.getIdField() = this.getRelevantFieldDefinitions().find { it.isID() }
-
-/**
- * Returns the field definitions which are not ignored
- */
-fun GraphQLFieldsContainer.getRelevantFieldDefinitions() = this.fieldDefinitions.filterNot { it.isIgnored() }
-
-/**
- * Returns the field definition if it is not ignored
- */
-fun GraphQLFieldsContainer.getRelevantFieldDefinition(name: String?) =
- this.getFieldDefinition(name)?.takeIf { !it.isIgnored() }
-
-
-fun InputObjectTypeDefinition.Builder.addFilterField(
- fieldName: String,
- isList: Boolean,
- filterType: String,
- description: Description? = null
-) {
- val wrappedType: Type<*> = when {
- isList -> ListType(TypeName(filterType))
- else -> TypeName(filterType)
- }
- val inputField = InputValueDefinition.newInputValueDefinition()
- .name(fieldName)
- .type(wrappedType)
- if (description != null) {
- inputField.description(description)
- }
-
- this.inputValueDefinition(inputField.build())
-}
-
-fun InputObjectTypeDefinition.Builder.addArrayFilterField(
- fieldName: String,
- filterType: String,
- description: Description? = null
-) {
- val inputField = InputValueDefinition.newInputValueDefinition()
- .name(fieldName)
- .type(ListType(NonNullType(TypeName(filterType))))
- if (description != null) {
- inputField.description(description)
- }
-
- this.inputValueDefinition(inputField.build())
-}
+fun TypeDefinitionRegistry.queryType() = this.getTypeByName(this.queryTypeName())
+fun TypeDefinitionRegistry.mutationType() =
+ this.getTypeByName(this.mutationTypeName())
+fun TypeDefinitionRegistry.subscriptionType() = this.getTypeByName(this.subscriptionTypeName())
fun TypeDefinitionRegistry.queryTypeName() = this.getOperationType("query") ?: "Query"
fun TypeDefinitionRegistry.mutationTypeName() = this.getOperationType("mutation") ?: "Mutation"
fun TypeDefinitionRegistry.subscriptionTypeName() = this.getOperationType("subscription") ?: "Subscription"
fun TypeDefinitionRegistry.getOperationType(name: String) =
this.schemaDefinition().unwrap()?.operationTypeDefinitions?.firstOrNull { it.name == name }?.typeName?.name
-fun DataFetchingEnvironment.typeAsContainer() = this.fieldDefinition.type.inner() as? GraphQLFieldsContainer
- ?: throw IllegalStateException("expect type of field ${this.logField()} to be GraphQLFieldsContainer, but was ${this.fieldDefinition.type.name()}")
+fun DataFetchingEnvironment.queryContext(): QueryContext =
+ this.graphQlContext.get(QueryContext.KEY)
+ ?: run {
+ val queryContext = QueryContext()
+ this.graphQlContext.put(QueryContext.KEY, queryContext)
+ queryContext
+ }
-fun DataFetchingEnvironment.logField() = "${this.parentType.name()}.${this.fieldDefinition.name}"
-fun DataFetchingEnvironment.queryContext(): QueryContext = this.graphQlContext.get(QueryContext.KEY) ?: QueryContext()
fun GraphQLContext.setQueryContext(ctx: QueryContext): GraphQLContext {
this.put(QueryContext.KEY, ctx)
return this
}
-val TypeInt = TypeName("Int")
-val TypeFloat = TypeName("Float")
-val TypeBoolean = TypeName("Boolean")
-val TypeID = TypeName("ID")
+
+fun ObjectValue.get(name: String): Value>? = this.objectFields.firstOrNull { it.name == name }?.value
+
+fun Directive.readArgument(prop: KProperty1<*, T>): T? = readArgument(prop) {
+ @Suppress("UNCHECKED_CAST")
+ it.toJavaValue() as T?
+}
+
+fun Directive.readArgument(prop: KProperty1<*, T>, transformer: (v: Value<*>) -> T?): T? {
+ return this.getArgument(prop.name)?.let { transformer.invoke(it.value) }
+}
+
+fun Directive.readRequiredArgument(prop: KProperty1<*, T>): T = readRequiredArgument(prop) {
+ @Suppress("UNCHECKED_CAST")
+ it.toJavaValue() as T?
+}
+
+fun Directive.readRequiredArgument(prop: KProperty1<*, T>, transformer: (v: Value<*>) -> T?): T =
+ readArgument(prop, transformer)
+ ?: throw IllegalArgumentException("@${this.name} is missing the required argument ${prop.name}")
+
+
+fun TypeDefinitionRegistry.getUnwrappedType(name: String?): TypeDefinition>? =
+ getType(name)?.unwrap()
+
+inline fun > TypeDefinitionRegistry.getTypeByName(name: String): T? =
+ this.getType(name, T::class.java)?.unwrap()
+
+fun ImplementingTypeDefinition<*>.getField(name: String): FieldDefinition? = fieldDefinitions.find { it.name == name }
+
+fun TypeDefinitionRegistry.replace(definition: SDLDefinition<*>) {
+ this.remove(definition)
+ this.add(definition)
+}
+
+fun NodeDirectivesBuilder.addNonLibDirectives(directivesContainer: DirectivesContainer<*>) {
+ this.directives(directivesContainer.directives.filterNot { Annotations.LIBRARY_DIRECTIVES.contains(it.name) })
+}
+
+inline fun T.asCypherLiteral() = Cypher.literalOf(this)
diff --git a/core/src/main/kotlin/org/neo4j/graphql/InvalidQueryException.kt b/core/src/main/kotlin/org/neo4j/graphql/InvalidQueryException.kt
deleted file mode 100644
index fd75b1a8..00000000
--- a/core/src/main/kotlin/org/neo4j/graphql/InvalidQueryException.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.neo4j.graphql
-
-import graphql.GraphQLError
-
-class InvalidQueryException(@Suppress("MemberVisibilityCanBePrivate") val error: GraphQLError) :
- RuntimeException(error.message)
diff --git a/core/src/main/kotlin/org/neo4j/graphql/Neo4jTypes.kt b/core/src/main/kotlin/org/neo4j/graphql/Neo4jTypes.kt
deleted file mode 100644
index a2a01d1d..00000000
--- a/core/src/main/kotlin/org/neo4j/graphql/Neo4jTypes.kt
+++ /dev/null
@@ -1,145 +0,0 @@
-package org.neo4j.graphql
-
-import graphql.schema.GraphQLFieldDefinition
-import graphql.schema.SelectedField
-import org.neo4j.cypherdsl.core.*
-import org.neo4j.cypherdsl.core.Cypher
-import org.neo4j.graphql.handler.BaseDataFetcherForContainer
-
-private const val NEO4j_FORMATTED_PROPERTY_KEY = "formatted"
-const val NEO4j_POINT_DISTANCE_FILTER = "_Neo4jPointDistanceFilter"
-const val NEO4j_POINT_DISTANCE_FILTER_SUFFIX = "_distance"
-
-data class TypeDefinition(
- val name: String,
- val typeDefinition: String,
- val inputDefinition: String = typeDefinition + "Input"
-)
-
-class Neo4jTemporalConverter(name: String) : Neo4jSimpleConverter(name) {
- override fun projectField(variable: SymbolicName, field: SelectedField, name: String): Any {
- return Cypher.call("toString").withArgs(variable.property(field.name)).asFunction()
- }
-
- override fun createCondition(
- property: Property,
- parameter: Parameter<*>,
- conditionCreator: (Expression, Expression) -> Condition
- ): Condition {
- return conditionCreator(property, toExpression(parameter))
- }
-}
-
-class Neo4jTimeConverter(name: String) : Neo4jConverter(name) {
-
- override fun createCondition(
- fieldName: String,
- field: GraphQLFieldDefinition,
- parameter: Parameter<*>,
- conditionCreator: (Expression, Expression) -> Condition,
- propertyContainer: PropertyContainer
- ): Condition = if (fieldName == NEO4j_FORMATTED_PROPERTY_KEY) {
- val exp = toExpression(parameter)
- conditionCreator(propertyContainer.property(field.name), exp)
- } else {
- super.createCondition(fieldName, field, parameter, conditionCreator, propertyContainer)
- }
-
- override fun projectField(variable: SymbolicName, field: SelectedField, name: String): Any = when (name) {
- NEO4j_FORMATTED_PROPERTY_KEY -> Cypher.call("toString").withArgs(variable.property(field.name)).asFunction()
- else -> super.projectField(variable, field, name)
- }
-
- override fun getMutationExpression(
- value: Any,
- field: GraphQLFieldDefinition
- ): BaseDataFetcherForContainer.PropertyAccessor {
- val fieldName = field.name
- return (value as? Map<*, *>)
- ?.get(NEO4j_FORMATTED_PROPERTY_KEY)
- ?.let {
- BaseDataFetcherForContainer.PropertyAccessor(fieldName) { variable ->
- val param = queryParameter(value, variable, fieldName)
- toExpression(param.property(NEO4j_FORMATTED_PROPERTY_KEY))
- }
- }
- ?: super.getMutationExpression(value, field)
- }
-}
-
-class Neo4jPointConverter(name: String) : Neo4jConverter(name) {
-
- fun createDistanceCondition(
- lhs: Expression,
- rhs: Parameter<*>,
- conditionCreator: (Expression, Expression) -> Condition
- ): Condition {
- val point = Cypher.point(rhs.property("point"))
- val distance = rhs.property("distance")
- return conditionCreator(Cypher.distance(lhs, point), distance)
- }
-}
-
-open class Neo4jConverter(
- name: String,
- val prefixedName: String = "_Neo4j$name",
- val typeDefinition: TypeDefinition = TypeDefinition(name, prefixedName)
-) : Neo4jSimpleConverter(name)
-
-open class Neo4jSimpleConverter(val name: String) {
- protected fun toExpression(parameter: Expression): Expression {
- return Cypher.call(name.toLowerCase()).withArgs(parameter).asFunction()
- }
-
- open fun createCondition(
- property: Property,
- parameter: Parameter<*>,
- conditionCreator: (Expression, Expression) -> Condition
- ): Condition = conditionCreator(property, parameter)
-
- open fun createCondition(
- fieldName: String,
- field: GraphQLFieldDefinition,
- parameter: Parameter<*>,
- conditionCreator: (Expression, Expression) -> Condition,
- propertyContainer: PropertyContainer
- ): Condition = createCondition(propertyContainer.property(field.name, fieldName), parameter, conditionCreator)
-
- open fun projectField(variable: SymbolicName, field: SelectedField, name: String): Any =
- variable.property(field.name, name)
-
- open fun getMutationExpression(
- value: Any,
- field: GraphQLFieldDefinition
- ): BaseDataFetcherForContainer.PropertyAccessor {
- return BaseDataFetcherForContainer.PropertyAccessor(field.name)
- { variable -> toExpression(queryParameter(value, variable, field.name)) }
- }
-}
-
-fun getNeo4jTypeConverter(field: GraphQLFieldDefinition): Neo4jSimpleConverter =
- getNeo4jTypeConverter(field.type.innerName())
-
-private fun getNeo4jTypeConverter(name: String): Neo4jSimpleConverter =
- neo4jConverter[name] ?: neo4jScalarConverter[name] ?: throw RuntimeException("Type $name not found")
-
-private val neo4jConverter = listOf(
- Neo4jTimeConverter("LocalTime"),
- Neo4jTimeConverter("Date"),
- Neo4jTimeConverter("DateTime"),
- Neo4jTimeConverter("Time"),
- Neo4jTimeConverter("LocalDateTime"),
- Neo4jPointConverter("Point"),
-).associateBy { it.prefixedName }
-
-private val neo4jScalarConverter = listOf(
- Neo4jTemporalConverter("LocalTime"),
- Neo4jTemporalConverter("Date"),
- Neo4jTemporalConverter("DateTime"),
- Neo4jTemporalConverter("Time"),
- Neo4jTemporalConverter("LocalDateTime")
-).associateBy { it.name }
-
-val NEO4j_TEMPORAL_TYPES = neo4jScalarConverter.keys
-
-val neo4jTypeDefinitions = neo4jConverter.values.map { it.typeDefinition }
diff --git a/core/src/main/kotlin/org/neo4j/graphql/NoOpCoercing.kt b/core/src/main/kotlin/org/neo4j/graphql/NoOpCoercing.kt
index a6854a0e..992cbf98 100644
--- a/core/src/main/kotlin/org/neo4j/graphql/NoOpCoercing.kt
+++ b/core/src/main/kotlin/org/neo4j/graphql/NoOpCoercing.kt
@@ -1,16 +1,26 @@
package org.neo4j.graphql
+import graphql.GraphQLContext
+import graphql.execution.CoercedVariables
+import graphql.language.Value
import graphql.schema.Coercing
import graphql.schema.CoercingParseLiteralException
import graphql.schema.CoercingParseValueException
+import java.util.*
object NoOpCoercing : Coercing {
- override fun parseLiteral(input: Any) = input.toJavaValue()
+ override fun parseLiteral(
+ input: Value<*>,
+ variables: CoercedVariables,
+ graphQLContext: GraphQLContext,
+ locale: Locale
+ ): Any = input.toJavaValue()
?: throw CoercingParseLiteralException("literal should not be null")
- override fun serialize(dataFetcherResult: Any) = dataFetcherResult
+ override fun serialize(dataFetcherResult: Any, graphQLContext: GraphQLContext, locale: Locale): Any =
+ dataFetcherResult
- override fun parseValue(input: Any) = input.toJavaValue()
+ override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): Any = input.toJavaValue()
?: throw CoercingParseValueException("literal should not be null")
}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/OptimizedQueryException.kt b/core/src/main/kotlin/org/neo4j/graphql/OptimizedQueryException.kt
deleted file mode 100644
index ce84a8e4..00000000
--- a/core/src/main/kotlin/org/neo4j/graphql/OptimizedQueryException.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package org.neo4j.graphql
-
-class OptimizedQueryException(message: String) : Exception(message)
\ No newline at end of file
diff --git a/core/src/main/kotlin/org/neo4j/graphql/Predicates.kt b/core/src/main/kotlin/org/neo4j/graphql/Predicates.kt
deleted file mode 100644
index e5b72d7b..00000000
--- a/core/src/main/kotlin/org/neo4j/graphql/Predicates.kt
+++ /dev/null
@@ -1,296 +0,0 @@
-package org.neo4j.graphql
-
-import graphql.language.*
-import graphql.language.TypeDefinition
-import graphql.schema.GraphQLFieldDefinition
-import graphql.schema.GraphQLFieldsContainer
-import org.neo4j.cypherdsl.core.*
-import org.neo4j.cypherdsl.core.Cypher
-import org.slf4j.LoggerFactory
-
-private fun createArrayPredicate(factory: (SymbolicName) -> OngoingListBasedPredicateFunction) =
- { lhs: Expression, rhs: Expression ->
- val x: SymbolicName = Cypher.name("x")
- factory(x).`in`(lhs).where(x.`in`(rhs))
- }
-
-enum class FieldOperator(
- val suffix: String,
- private val conditionCreator: (Expression, Expression) -> Condition,
- val not: Boolean = false,
- val requireParam: Boolean = true,
- val distance: Boolean = false,
- val list: Boolean = false
-) {
- EQ("", { lhs, rhs -> lhs.isEqualTo(rhs) }),
- IS_NULL("", { lhs, _ -> lhs.isNull }, requireParam = false),
- IS_NOT_NULL("_not", { lhs, _ -> lhs.isNotNull }, true, requireParam = false),
- NEQ("_not", { lhs, rhs -> lhs.isEqualTo(rhs).not() }, not = true),
- GTE("_gte", { lhs, rhs -> lhs.gte(rhs) }),
- GT("_gt", { lhs, rhs -> lhs.gt(rhs) }),
- LTE("_lte", { lhs, rhs -> lhs.lte(rhs) }),
- LT("_lt", { lhs, rhs -> lhs.lt(rhs) }),
-
- NIN("_not_in", { lhs, rhs -> lhs.`in`(rhs).not() }, not = true, list = true),
- IN("_in", { lhs, rhs -> lhs.`in`(rhs) }, list = true),
- NC("_not_contains", { lhs, rhs -> lhs.contains(rhs).not() }, not = true),
- NSW("_not_starts_with", { lhs, rhs -> lhs.startsWith(rhs).not() }, not = true),
- NEW("_not_ends_with", { lhs, rhs -> lhs.endsWith(rhs).not() }, not = true),
- C("_contains", { lhs, rhs -> lhs.contains(rhs) }),
- SW("_starts_with", { lhs, rhs -> lhs.startsWith(rhs) }),
- EW("_ends_with", { lhs, rhs -> lhs.endsWith(rhs) }),
- MATCHES("_matches", { lhs, rhs -> lhs.matches(rhs) }),
-
- INCLUDES_ALL("_includes_all", createArrayPredicate(Cypher::all), list = true),
- INCLUDES_SOME("_includes_some", createArrayPredicate(Cypher::any), list = true),
- INCLUDES_NONE("_includes_none", createArrayPredicate(Cypher::none), list = true),
- INCLUDES_SINGLE("_includes_single", createArrayPredicate(Cypher::single), list = true),
-
- DISTANCE(NEO4j_POINT_DISTANCE_FILTER_SUFFIX, { lhs, rhs -> lhs.isEqualTo(rhs) }, distance = true),
- DISTANCE_LT(NEO4j_POINT_DISTANCE_FILTER_SUFFIX + "_lt", { lhs, rhs -> lhs.lt(rhs) }, distance = true),
- DISTANCE_LTE(NEO4j_POINT_DISTANCE_FILTER_SUFFIX + "_lte", { lhs, rhs -> lhs.lte(rhs) }, distance = true),
- DISTANCE_GT(NEO4j_POINT_DISTANCE_FILTER_SUFFIX + "_gt", { lhs, rhs -> lhs.gt(rhs) }, distance = true),
- DISTANCE_GTE(NEO4j_POINT_DISTANCE_FILTER_SUFFIX + "_gte", { lhs, rhs -> lhs.gte(rhs) }, distance = true);
-
- fun resolveCondition(
- variablePrefix: String,
- queriedField: String,
- propertyContainer: PropertyContainer,
- field: GraphQLFieldDefinition?,
- value: Any?,
- schemaConfig: SchemaConfig,
- suffix: String? = null
- ): List {
- if (schemaConfig.useTemporalScalars && field?.type?.isNeo4jTemporalType() == true) {
- val neo4jTypeConverter = getNeo4jTypeConverter(field)
- val parameter = queryParameter(value, variablePrefix, queriedField, null, suffix)
- .withValue(value)
- return listOf(
- neo4jTypeConverter.createCondition(
- propertyContainer.property(field.name),
- parameter,
- conditionCreator
- )
- )
- }
- return if (field?.type?.isNeo4jType() == true && value is Map<*, *>) {
- resolveNeo4jTypeConditions(variablePrefix, queriedField, propertyContainer, field, value, suffix)
- } else if (field?.isNativeId() == true) {
- val id = propertyContainer.id()
- val parameter = queryParameter(value, variablePrefix, queriedField, suffix)
- val condition = if (list) {
- val idVar = Cypher.name("id")
- conditionCreator(id, Cypher.listWith(idVar).`in`(parameter).returning(idVar))
- } else {
- conditionCreator(id, parameter)
- }
- listOf(condition)
- } else {
- resolveCondition(
- variablePrefix, queriedField, propertyContainer.property(
- field?.propertyName()
- ?: queriedField
- ), value, suffix
- )
- }
- }
-
- private fun resolveNeo4jTypeConditions(
- variablePrefix: String,
- queriedField: String,
- propertyContainer: PropertyContainer,
- field: GraphQLFieldDefinition,
- values: Map<*, *>,
- suffix: String?
- ): List {
- val neo4jTypeConverter = getNeo4jTypeConverter(field)
- val conditions = mutableListOf()
- if (distance) {
- val parameter = queryParameter(values, variablePrefix, queriedField, suffix)
- conditions += (neo4jTypeConverter as Neo4jPointConverter).createDistanceCondition(
- propertyContainer.property(field.propertyName()),
- parameter,
- conditionCreator
- )
- } else {
- values.entries.forEachIndexed { index, (key, value) ->
- val fieldName = key.toString()
- val parameter = queryParameter(
- value,
- variablePrefix,
- queriedField,
- if (values.size > 1) "And${index + 1}" else null,
- suffix,
- fieldName
- )
- .withValue(value)
-
- conditions += neo4jTypeConverter.createCondition(
- fieldName,
- field,
- parameter,
- conditionCreator,
- propertyContainer
- )
- }
- }
- return conditions
- }
-
- private fun resolveCondition(
- variablePrefix: String,
- queriedField: String,
- property: Property,
- value: Any?,
- suffix: String?
- ): List {
- val parameter = queryParameter(value, variablePrefix, queriedField, suffix)
- val condition = conditionCreator(property, parameter)
- return listOf(condition)
- }
-
- companion object {
-
- fun forType(type: TypeDefinition<*>, isNeo4jType: Boolean, isList: Boolean): List =
- when {
- isList -> listOf(EQ, NEQ, INCLUDES_ALL, INCLUDES_NONE, INCLUDES_SOME, INCLUDES_SINGLE)
- type.name == TypeBoolean.name -> listOf(EQ, NEQ)
- type.name == NEO4j_POINT_DISTANCE_FILTER -> listOf(EQ, LT, LTE, GT, GTE)
- type.isNeo4jSpatialType() -> listOf(EQ, NEQ)
- isNeo4jType -> listOf(EQ, NEQ, IN, NIN)
- type is ImplementingTypeDefinition<*> -> throw IllegalArgumentException("This operators are not for relations, use the RelationOperator instead")
- type is EnumTypeDefinition -> listOf(EQ, NEQ, IN, NIN)
- // todo list types
- type !is ScalarTypeDefinition -> 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,
- MATCHES
- ) else emptyList()
- }
- }
-
- fun fieldName(fieldName: String) = fieldName + suffix
-}
-
-enum class RelationOperator(val suffix: String) {
- SOME("_some"),
-
- EVERY("_every"),
-
- SINGLE("_single"),
- NONE("_none"),
-
- // `eq` if queried with an object, `not exists` if queried with null
- EQ_OR_NOT_EXISTS(""),
- NOT("_not");
-
- fun fieldName(fieldName: String) = fieldName + suffix
-
- fun harmonize(type: GraphQLFieldsContainer, field: GraphQLFieldDefinition, value: Any?, queryFieldName: String) =
- when (field.type.isList()) {
- true -> when (this) {
- NOT -> when (value) {
- null -> NOT
- else -> NONE
- }
-
- EQ_OR_NOT_EXISTS -> when (value) {
- null -> EQ_OR_NOT_EXISTS
- else -> {
- LOGGER.debug("$queryFieldName on type ${type.name} was used for filtering, consider using ${field.name}${EVERY.suffix} instead")
- EVERY
- }
- }
-
- else -> this
- }
-
- false -> when (this) {
- SINGLE -> {
- LOGGER.debug("Using $queryFieldName on type ${type.name} is deprecated, use ${field.name} directly")
- SOME
- }
-
- SOME -> {
- LOGGER.debug("Using $queryFieldName on type ${type.name} is deprecated, use ${field.name} directly")
- SOME
- }
-
- NONE -> {
- LOGGER.debug("Using $queryFieldName on type ${type.name} is deprecated, use ${field.name}${NOT.suffix} instead")
- NONE
- }
-
- NOT -> when (value) {
- null -> NOT
- else -> NONE
- }
-
- EQ_OR_NOT_EXISTS -> when (value) {
- null -> EQ_OR_NOT_EXISTS
- else -> SOME
- }
-
- else -> this
- }
- }
-
- companion object {
- private val LOGGER = LoggerFactory.getLogger(RelationOperator::class.java)
-
- fun createRelationFilterFields(
- type: TypeDefinition<*>,
- field: FieldDefinition,
- filterType: String,
- builder: InputObjectTypeDefinition.Builder
- ) {
- val list = field.type.isList()
-
- val addFilterField = { op: RelationOperator, description: String ->
- builder.addFilterField(op.fieldName(field.name), false, filterType, description.asDescription())
- }
-
- addFilterField(
- EQ_OR_NOT_EXISTS,
- "Filters only those `${type.name}` for which ${if (list) "all" else "the"} `${field.name}`-relationship matches this filter. " +
- "If `null` is passed to this field, only those `${type.name}` will be filtered which has no `${field.name}`-relations"
- )
-
- addFilterField(
- NOT,
- "Filters only those `${type.name}` for which ${if (list) "all" else "the"} `${field.name}`-relationship does not match this filter. " +
- "If `null` is passed to this field, only those `${type.name}` will be filtered which has any `${field.name}`-relation"
- )
- if (list) {
- // n..m
- addFilterField(
- EVERY,
- "Filters only those `${type.name}` for which all `${field.name}`-relationships matches this filter"
- )
- addFilterField(
- SOME,
- "Filters only those `${type.name}` for which at least one `${field.name}`-relationship matches this filter"
- )
- addFilterField(
- SINGLE,
- "Filters only those `${type.name}` for which exactly one `${field.name}`-relationship matches this filter"
- )
- addFilterField(
- NONE,
- "Filters only those `${type.name}` for which none of the `${field.name}`-relationships matches this filter"
- )
- } else {
- // n..1
- addFilterField(SINGLE, "@deprecated Use the `${field.name}`-field directly (without any suffix)")
- addFilterField(SOME, "@deprecated Use the `${field.name}`-field directly (without any suffix)")
- addFilterField(NONE, "@deprecated Use the `${field.name}${NOT.suffix}`-field")
- }
- }
- }
-}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/QueryContext.kt b/core/src/main/kotlin/org/neo4j/graphql/QueryContext.kt
index 5efda4b4..d925465a 100644
--- a/core/src/main/kotlin/org/neo4j/graphql/QueryContext.kt
+++ b/core/src/main/kotlin/org/neo4j/graphql/QueryContext.kt
@@ -1,29 +1,74 @@
package org.neo4j.graphql
-import org.neo4j.cypherdsl.core.renderer.Dialect
+import org.neo4j.cypherdsl.core.Cypher
+import org.neo4j.cypherdsl.core.Parameter
+import org.neo4j.graphql.domain.fields.RelationField
+import java.util.concurrent.atomic.AtomicInteger
data class QueryContext @JvmOverloads constructor(
- /**
- * if true the __typename
will always be returned for interfaces, no matter if it was queried or not
- */
- var queryTypeOfInterfaces: Boolean = false,
+ val contextParams: Map? = emptyMap(),
+) {
- /**
- * If set alternative approaches for query translation will be used
- */
- var optimizedQuery: Set? = null,
+ private var varCounter = mutableMapOf()
+ private var paramCounter = mutableMapOf()
+ private var paramKeysPerValues = mutableMapOf>>()
- var neo4jDialect: Dialect = Dialect.NEO4J_5
+ fun resolve(string: String): String {
+ return CONTEXT_VARIABLE_PATTERN.replace(string) {
+ val path = it.groups[1] ?: it.groups[2] ?: throw IllegalStateException("expected a group")
+ val parts = path.value.split(".")
+ var o: Any? = null
+ for ((index, part) in parts.withIndex()) {
+ if (index == 0) {
+ if (part == "context") {
+ o = contextParams
+ continue
+ } else {
+ TODO("query context does not provide a `$part`")
+ }
+ }
+ if (o is Map<*, *>) {
+ o = o[part] ?: return@replace ""
+ } else {
+ TODO("only maps are currently supported")
+ }
+ }
+ return@replace o.toString()
+ }
+ }
-) {
- enum class OptimizationStrategy {
- /**
- * If used, filter queries will be converted to cypher matches
- */
- FILTER_AS_MATCH
+ fun getNextVariable(relationField: RelationField) = getNextVariable(
+ relationField.relationType.toLowerCase().toCamelCase()
+ )
+
+ fun getNextVariable(node: org.neo4j.graphql.domain.Node) = getNextVariable(node.name.decapitalize())
+
+ fun getNextVariable(prefix: String?) = getNextVariableName(prefix).let { Cypher.name(it) }
+ fun getNextVariableName(prefix: String?) = (prefix ?: "var").let { p ->
+ varCounter.computeIfAbsent(p) { AtomicInteger(0) }.getAndIncrement()
+ .let { p + it }
}
+ fun getNextParam(value: Any?) = getNextParam("param", value)
+ private fun getNextParam(prefix: String, value: Any?, reuseKey: Boolean = false): Parameter<*> =
+ if (reuseKey) {
+ paramKeysPerValues.computeIfAbsent(prefix) { mutableMapOf() }
+ .computeIfAbsent(value) {
+ getNextParam(prefix, value, reuseKey = false)
+ }
+ } else {
+ paramCounter
+ .computeIfAbsent(prefix) { AtomicInteger(0) }.getAndIncrement()
+ .let { Cypher.parameter(prefix + it, value) }
+ }
+
+
companion object {
const val KEY = "Neo4jGraphQLQueryContext"
+
+ private const val PATH_PATTERN = "([a-zA-Z_][a-zA-Z_0-9]*(?:.[a-zA-Z_][a-zA-Z_0-9]*)*)"
+
+ // matches ${path} or $path
+ private val CONTEXT_VARIABLE_PATTERN = Regex("\\\$(?:\\{$PATH_PATTERN}|$PATH_PATTERN)")
}
}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/RelationshipInfo.kt b/core/src/main/kotlin/org/neo4j/graphql/RelationshipInfo.kt
deleted file mode 100644
index 2053b3c8..00000000
--- a/core/src/main/kotlin/org/neo4j/graphql/RelationshipInfo.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-package org.neo4j.graphql
-
-import graphql.language.ImplementingTypeDefinition
-import graphql.schema.GraphQLAppliedDirective
-import graphql.schema.GraphQLDirectiveContainer
-import graphql.schema.GraphQLFieldsContainer
-import graphql.schema.idl.TypeDefinitionRegistry
-import org.neo4j.cypherdsl.core.Node
-import org.neo4j.cypherdsl.core.Relationship
-import org.neo4j.cypherdsl.core.SymbolicName
-
-data class RelationshipInfo(
- val type: TYPE,
- val typeName: String,
- val relType: String,
- val direction: RelationDirection,
- val startField: String,
- val endField: String
-) {
-
- enum class RelationDirection {
- IN,
- OUT,
- BOTH;
-
- fun invert(): RelationDirection = when (this) {
- IN -> OUT
- OUT -> IN
- else -> this
- }
-
- }
-
- companion object {
- fun create(type: GraphQLFieldsContainer): RelationshipInfo? =
- (type as? GraphQLDirectiveContainer)
- ?.getAppliedDirective(DirectiveConstants.RELATION)
- ?.let { relDirective -> create(type, relDirective) }
-
- fun create(
- type: GraphQLFieldsContainer,
- relDirective: GraphQLAppliedDirective
- ): RelationshipInfo {
- val relType = relDirective.getArgument(DirectiveConstants.RELATION_NAME, "")!!
- val direction = relDirective.getArgument(DirectiveConstants.RELATION_DIRECTION, null)
- ?.let { RelationDirection.valueOf(it) }
- ?: RelationDirection.OUT
-
- return RelationshipInfo(
- type,
- type.name,
- relType,
- direction,
- relDirective.getMandatoryArgument(DirectiveConstants.RELATION_FROM),
- relDirective.getMandatoryArgument(DirectiveConstants.RELATION_TO)
- )
- }
-
- fun create(
- type: ImplementingTypeDefinition<*>,
- registry: TypeDefinitionRegistry
- ): RelationshipInfo>? {
- val relType = type.getDirectiveArgument(
- registry,
- DirectiveConstants.RELATION,
- DirectiveConstants.RELATION_NAME
- )
- ?: return null
- val startField = type.getMandatoryDirectiveArgument(
- registry,
- DirectiveConstants.RELATION,
- DirectiveConstants.RELATION_FROM
- )
- val endField = type.getMandatoryDirectiveArgument(
- registry,
- DirectiveConstants.RELATION,
- DirectiveConstants.RELATION_TO
- )
- val direction = type.getDirectiveArgument(
- registry,
- DirectiveConstants.RELATION,
- DirectiveConstants.RELATION_DIRECTION
- )
- ?.let { RelationDirection.valueOf(it) }
- ?: RelationDirection.OUT
- return RelationshipInfo(type, type.name, relType, direction, startField, endField)
- }
- }
-
- fun createRelation(start: Node, end: Node, addType: Boolean = true, variable: SymbolicName? = null): Relationship {
- val labels = if (addType) {
- arrayOf(this.relType)
- } else {
- emptyArray()
- }
- return when (this.direction) {
- RelationDirection.IN -> start.relationshipFrom(end, *labels)
- RelationDirection.OUT -> start.relationshipTo(end, *labels)
- RelationDirection.BOTH -> start.relationshipBetween(end, *labels)
- }
- .let { if (variable != null) it.named(variable) else it }
- }
-}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt b/core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt
index e88ec941..c8c52046 100644
--- a/core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt
+++ b/core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt
@@ -7,12 +7,21 @@ import graphql.schema.idl.ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS
import graphql.schema.idl.SchemaGenerator
import graphql.schema.idl.SchemaParser
import graphql.schema.idl.TypeDefinitionRegistry
-import org.neo4j.graphql.AugmentationHandler.OperationType
-import org.neo4j.graphql.handler.*
-import org.neo4j.graphql.handler.projection.ProjectionBase
-import org.neo4j.graphql.handler.relation.CreateRelationHandler
-import org.neo4j.graphql.handler.relation.CreateRelationTypeHandler
-import org.neo4j.graphql.handler.relation.DeleteRelationHandler
+import org.neo4j.graphql.domain.Interface
+import org.neo4j.graphql.domain.Model
+import org.neo4j.graphql.domain.directives.Annotations.Companion.LIBRARY_DIRECTIVES
+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
+import org.neo4j.graphql.schema.model.outputs.NodeSelection
/**
* A class for augmenting a type definition registry and generate the corresponding data fetcher.
@@ -23,122 +32,156 @@ import org.neo4j.graphql.handler.relation.DeleteRelationHandler
* 1. [augmentTypes]
* 2. [registerScalars]
* 3. [registerTypeNameResolver]
- * 4. [registerDataFetcher]
+ * 4. [registerNeo4jAdapter]
*
* Each of these steps can be called manually to enhance an existing [TypeDefinitionRegistry]
*/
-class SchemaBuilder(
+class SchemaBuilder @JvmOverloads constructor(
val typeDefinitionRegistry: TypeDefinitionRegistry,
- val schemaConfig: SchemaConfig = SchemaConfig()
+ val schemaConfig: SchemaConfig = SchemaConfig(),
) {
companion object {
- /**
- * @param sdl the schema to augment
- * @param config defines how the schema should get augmented
- * @param dataFetchingInterceptor since this library registers dataFetcher for its augmented methods, these data
- * fetchers may be called by other resolver. This interceptor will let you convert a cypher query into real data.
- */
+
@JvmStatic
@JvmOverloads
- fun buildSchema(
- sdl: String,
- config: SchemaConfig = SchemaConfig(),
- dataFetchingInterceptor: DataFetchingInterceptor? = null
- ): GraphQLSchema {
+ fun fromSchema(sdl: String, config: SchemaConfig = SchemaConfig()): SchemaBuilder {
val schemaParser = SchemaParser()
val typeDefinitionRegistry = schemaParser.parse(sdl)
- return buildSchema(typeDefinitionRegistry, config, dataFetchingInterceptor)
+ return SchemaBuilder(typeDefinitionRegistry, config)
}
/**
- * @param typeDefinitionRegistry a registry containing all the types, that should be 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
- * @param dataFetchingInterceptor since this library registers dataFetcher for its augmented methods, these data
- * fetchers may be called by other resolver. This interceptor will let you convert a cypher query into real data.
*/
@JvmStatic
@JvmOverloads
fun buildSchema(
- typeDefinitionRegistry: TypeDefinitionRegistry,
+ sdl: String,
config: SchemaConfig = SchemaConfig(),
- dataFetchingInterceptor: DataFetchingInterceptor? = null
- ): GraphQLSchema {
-
- val builder = RuntimeWiring.newRuntimeWiring()
- val codeRegistryBuilder = GraphQLCodeRegistry.newCodeRegistry()
- val schemaBuilder = SchemaBuilder(typeDefinitionRegistry, config)
- schemaBuilder.augmentTypes()
- schemaBuilder.registerScalars(builder)
- schemaBuilder.registerTypeNameResolver(builder)
- schemaBuilder.registerDataFetcher(codeRegistryBuilder, dataFetchingInterceptor)
-
- return SchemaGenerator().makeExecutableSchema(
- typeDefinitionRegistry,
- builder.codeRegistry(codeRegistryBuilder).build()
- )
- }
+ neo4jAdapter: Neo4jAdapter = Neo4jAdapter.NO_OP,
+ addLibraryDirectivesToSchema: Boolean = true,
+ ): GraphQLSchema = fromSchema(sdl, config)
+ .withNeo4jAdapter(neo4jAdapter)
+ .addLibraryDirectivesToSchema(addLibraryDirectivesToSchema)
+ .build()
}
private val handler: List
- private val neo4jTypeDefinitionRegistry: TypeDefinitionRegistry
+ 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 {
- neo4jTypeDefinitionRegistry = getNeo4jEnhancements()
- ensureRootQueryTypeExists(typeDefinitionRegistry)
handler = mutableListOf(
- CypherDirectiveHandler.Factory(schemaConfig, typeDefinitionRegistry, neo4jTypeDefinitionRegistry),
- AugmentFieldHandler(schemaConfig, typeDefinitionRegistry, neo4jTypeDefinitionRegistry)
+ ReadResolver.Factory(ctx),
+ ConnectionResolver.Factory(ctx),
+ ImplementingTypeConnectionFieldResolver.Factory(ctx)
)
- if (schemaConfig.query.enabled) {
- handler.add(QueryHandler.Factory(schemaConfig, typeDefinitionRegistry, neo4jTypeDefinitionRegistry))
- }
- if (schemaConfig.mutation.enabled) {
- handler += listOf(
- MergeOrUpdateHandler.Factory(schemaConfig, typeDefinitionRegistry, neo4jTypeDefinitionRegistry),
- DeleteHandler.Factory(schemaConfig, typeDefinitionRegistry, neo4jTypeDefinitionRegistry),
- CreateTypeHandler.Factory(schemaConfig, typeDefinitionRegistry, neo4jTypeDefinitionRegistry),
- DeleteRelationHandler.Factory(schemaConfig, typeDefinitionRegistry, neo4jTypeDefinitionRegistry),
- CreateRelationTypeHandler.Factory(schemaConfig, typeDefinitionRegistry, neo4jTypeDefinitionRegistry),
- CreateRelationHandler.Factory(schemaConfig, typeDefinitionRegistry, neo4jTypeDefinitionRegistry)
- )
- }
+ }
+
+ 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].
* This method will also augment relation fields, so filtering and sorting is available for them
+ *
+ * @param addLibraryDirectivesToSchema if set to true, the library directives will be added to the schema. Default: true
*/
- fun augmentTypes() {
- val queryTypeName = typeDefinitionRegistry.queryTypeName()
- val mutationTypeName = typeDefinitionRegistry.mutationTypeName()
- val subscriptionTypeName = typeDefinitionRegistry.subscriptionTypeName()
-
- typeDefinitionRegistry.types().values
- .filterIsInstance>()
- .filter { it.name != queryTypeName && it.name != mutationTypeName && it.name != subscriptionTypeName }
- .forEach { type -> handler.forEach { h -> h.augmentType(type) } }
-
- // in a second run we enhance all the root fields
- typeDefinitionRegistry.types().values
- .filterIsInstance>()
- .filter { it.name == queryTypeName || it.name == mutationTypeName || it.name == subscriptionTypeName }
- .forEach { type -> handler.forEach { h -> h.augmentType(type) } }
+ @JvmOverloads
+ fun augmentTypes(addLibraryDirectivesToSchema: Boolean = true) {
+ val model = Model.createModel(typeDefinitionRegistry, schemaConfig)
+
+ // remove type definition for node since it will be added while augmenting the schema
+ model.nodes
+ .mapNotNull { typeDefinitionRegistry.getTypeByName(it.name) }
+ .forEach { typeDefinitionRegistry.remove(it) }
+
+ model.interfaces
+ .mapNotNull { typeDefinitionRegistry.getTypeByName(it.name) }
+ .forEach { typeDefinitionRegistry.remove(it) }
+
+ model.relationship
+ .mapNotNull { it.properties?.typeName }
+ .mapNotNull { typeDefinitionRegistry.getTypeByName(it) }
+ .forEach { typeDefinitionRegistry.remove(it) }
+
+ augmentedFields += handler.filterIsInstance()
+ .flatMap { h -> model.nodes.flatMap(h::augmentNode) }
+ augmentedFields += handler.filterIsInstance()
+ .flatMap { h -> model.entities.flatMap(h::augmentEntity) }
+ augmentedFields += handler.filterIsInstance()
+ .flatMap { h -> h.augmentModel(model) }
+ removeLibraryDirectivesFromInterfaces()
+ removeLibraryDirectivesFromUnions()
+ removeLibraryDirectivesFromRootTypes()
+ removeLibraryDirectivesFromSchemaTypes()
+
+ ensureAllReferencedNodesExists(model)
+ ensureAllReferencedInterfacesExists(model)
+ ensureReferencedLibraryTypesExists(addLibraryDirectivesToSchema)
+ ensureRootQueryTypeExists()
+ }
+
+ private fun ensureReferencedLibraryTypesExists(addLibraryDirectivesToSchema: Boolean) {
val types = mutableListOf>()
- neo4jTypeDefinitionRegistry.directiveDefinitions.values
- .filterNot { typeDefinitionRegistry.getDirectiveDefinition(it.name).isPresent }
- .forEach { directiveDefinition ->
- typeDefinitionRegistry.add(directiveDefinition)
- directiveDefinition.inputValueDefinitions.forEach { types.add(it.type) }
- }
+ if (addLibraryDirectivesToSchema) {
+ neo4jTypeDefinitionRegistry.directiveDefinitions.values
+ .filterNot { typeDefinitionRegistry.getDirectiveDefinition(it.name).isPresent }
+ .forEach { directiveDefinition ->
+ typeDefinitionRegistry.add(directiveDefinition)
+ directiveDefinition.inputValueDefinitions.forEach { types.add(it.type) }
+ }
+ }
typeDefinitionRegistry.types()
.values
.flatMap { typeDefinition ->
when (typeDefinition) {
is ImplementingTypeDefinition -> typeDefinition.fieldDefinitions
- .flatMap { fieldDefinition -> fieldDefinition.inputValueDefinitions.map { it.type } + fieldDefinition.type }
+ .flatMap { fieldDefinition -> fieldDefinition.inputValueDefinitions.map { it.type } + fieldDefinition.type } +
+ typeDefinition.implements
is InputObjectTypeDefinition -> typeDefinition.inputValueDefinitions.map { it.type }
else -> emptyList()
@@ -150,19 +193,90 @@ class SchemaBuilder(
.filterNot { typeDefinitionRegistry.hasType(it) }
.mapNotNull { neo4jTypeDefinitionRegistry.getType(it).unwrap() }
.forEach { typeDefinitionRegistry.add(it) }
+ }
+ private fun ensureAllReferencedInterfacesExists(model: Model) {
+ model.nodes
+ .flatMap { node ->
+ node.interfaces +
+ node.fields.filterIsInstance()
+ .map { it.target }
+ .filterIsInstance() +
+ node.interfaces.flatMap { it.interfaces }
+ }
+ .distinctBy { it.name }
+ .forEach { InterfaceSelection.Augmentation.generateInterfaceSelection(it, ctx) }
+ }
- if (typeDefinitionRegistry.getType(mutationTypeName).isPresent) {
- typeDefinitionRegistry.schemaDefinition().ifPresent { schemaDefinition ->
- typeDefinitionRegistry.remove(schemaDefinition)
- typeDefinitionRegistry.add(schemaDefinition.transform {
- val ops = schemaDefinition.operationTypeDefinitions.toMutableList()
- if (ops.find { it.name == "mutation" } == null) {
- ops.add(OperationTypeDefinition("mutation", TypeName(mutationTypeName)))
- }
- it.operationTypeDefinitions(ops)
+ private fun removeLibraryDirectivesFromInterfaces() {
+ typeDefinitionRegistry.getTypes(InterfaceTypeDefinition::class.java).forEach { interfaceTypeDefinition ->
+ typeDefinitionRegistry.replace(interfaceTypeDefinition.transform { builder ->
+ builder.addNonLibDirectives(interfaceTypeDefinition)
+ builder.definitions(interfaceTypeDefinition.fieldDefinitions.map { field ->
+ field.transform { fieldBuilder -> fieldBuilder.addNonLibDirectives(field) }
+ })
+ })
+ }
+ }
+
+ private fun removeLibraryDirectivesFromUnions() {
+ typeDefinitionRegistry.getTypes(UnionTypeDefinition::class.java).forEach { unionTypeDefinition ->
+ typeDefinitionRegistry.replace(unionTypeDefinition.transform { builder ->
+ builder.addNonLibDirectives(unionTypeDefinition)
+ })
+ }
+ }
+
+ private fun removeLibraryDirectivesFromRootTypes() {
+ listOf(
+ typeDefinitionRegistry.queryType(),
+ typeDefinitionRegistry.mutationType(),
+ typeDefinitionRegistry.subscriptionType(),
+ )
+ .filterNotNull()
+ .forEach { obj ->
+ typeDefinitionRegistry.replace(obj.transform { objBuilder ->
+ objBuilder.addNonLibDirectives(obj)
+ objBuilder.fieldDefinitions(obj.fieldDefinitions.map { field ->
+ field.transform { fieldBuilder -> fieldBuilder.addNonLibDirectives(field) }
+ })
})
}
+ }
+
+ private fun removeLibraryDirectivesFromSchemaTypes() {
+ typeDefinitionRegistry.schemaExtensionDefinitions.forEach { obj ->
+ typeDefinitionRegistry.remove(obj)
+ typeDefinitionRegistry.add(obj.transformExtension { objBuilder ->
+ objBuilder.directives(obj.directives.filterNot { LIBRARY_DIRECTIVES.contains(it.name) })
+ })
+ }
+ typeDefinitionRegistry.schemaDefinition()?.unwrap()?.let { obj ->
+ typeDefinitionRegistry.replace(obj.transform { objBuilder ->
+ objBuilder.directives(obj.directives.filterNot { LIBRARY_DIRECTIVES.contains(it.name) })
+
+ })
+ }
+ }
+
+ private fun ensureAllReferencedNodesExists(model: Model) {
+ val nodesByName = model.nodes.associateBy { it.name }
+
+ var typesToCheck = typeDefinitionRegistry.getTypes(ImplementingTypeDefinition::class.java)
+ .map { it.name }
+ .toSet()
+ while (typesToCheck.isNotEmpty()) {
+ typesToCheck = typesToCheck
+ .mapNotNull { typeDefinitionRegistry.getUnwrappedType(it) }
+ .filterIsInstance>()
+ .flatMap { it.fieldDefinitions }
+ .asSequence()
+ .map { it.type.name() }
+ .filter { typeDefinitionRegistry.getTypeByName(it) == null }
+ .mapNotNull { nodesByName[it] }
+ .onEach { NodeSelection.Augmentation.generateNodeSelection(it, ctx) }
+ .map { it.name }
+ .toSet()
}
}
@@ -175,10 +289,20 @@ class SchemaBuilder(
.filterNot { entry -> GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.containsKey(entry.key) }
.forEach { (name, definition) ->
val scalar = when (name) {
- "DynamicProperties" -> DynamicProperties.INSTANCE
+ 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(definition.description?.getContent() ?: "Scalar $name")
+ .description(
+ definition.description?.getContent()
+ ?: definition.comments?.joinToString("\n") { it.getContent() }
+ ?.takeIf { it.isNotBlank() }
+ )
.withDirectives(*definition.directives.filterIsInstance().toTypedArray())
.definition(definition)
.coercing(NoOpCoercing)
@@ -193,13 +317,13 @@ class SchemaBuilder(
* @param builder a builder to create a runtime wiring
*/
fun registerTypeNameResolver(builder: RuntimeWiring.Builder) {
- typeDefinitionRegistry
- .getTypes(InterfaceTypeDefinition::class.java)
+ (typeDefinitionRegistry.getTypes(InterfaceTypeDefinition::class.java)
+ + typeDefinitionRegistry.getTypes(UnionTypeDefinition::class.java))
.forEach { typeDefinition ->
builder.type(typeDefinition.name) {
it.typeResolver { env ->
(env.getObject() as? Map)
- ?.let { data -> data[ProjectionBase.TYPE_NAME] as? String }
+ ?.let { data -> data[Constants.TYPE_NAME] as? String }
?.let { typeName -> env.schema.getObjectType(typeName) }
}
}
@@ -207,120 +331,46 @@ class SchemaBuilder(
}
/**
- * Register data fetcher in a [GraphQLCodeRegistry][@param codeRegistryBuilder].
- * The data fetcher of this library generate a cypher query and if provided use the dataFetchingInterceptor to run this cypher against a neo4j db.
+ * Register data fetcher in the [GraphQLCodeRegistry.Builder][@param codeRegistryBuilder]
* @param codeRegistryBuilder a builder to create a code registry
- * @param dataFetchingInterceptor a function to convert a cypher string into an object by calling the neo4j db
+ * @param neo4jAdapter the adapter to run the generated cypher queries
*/
- @JvmOverloads
- fun registerDataFetcher(
+ fun registerNeo4jAdapter(
codeRegistryBuilder: GraphQLCodeRegistry.Builder,
- dataFetchingInterceptor: DataFetchingInterceptor?,
- typeDefinitionRegistry: TypeDefinitionRegistry = this.typeDefinitionRegistry
+ neo4jAdapter: Neo4jAdapter,
) {
- if (dataFetchingInterceptor != null) {
- codeRegistryBuilder.defaultDataFetcher { AliasPropertyDataFetcher() }
+ codeRegistryBuilder.defaultDataFetcher { AliasPropertyDataFetcher() }
+ augmentedFields.forEach { (coordinates, dataFetcher) ->
+ codeRegistryBuilder.dataFetcher(coordinates, DataFetcher { env ->
+ env.graphQlContext.put(Neo4jAdapter.CONTEXT_KEY, neo4jAdapter)
+ dataFetcher.get(env)
+ })
}
- addDataFetcher(
- typeDefinitionRegistry.queryTypeName(),
- OperationType.QUERY,
- dataFetchingInterceptor,
- codeRegistryBuilder
- )
- addDataFetcher(
- typeDefinitionRegistry.mutationTypeName(),
- OperationType.MUTATION,
- dataFetchingInterceptor,
- codeRegistryBuilder
- )
}
- private fun addDataFetcher(
- parentType: String,
- operationType: OperationType,
- dataFetchingInterceptor: DataFetchingInterceptor?,
- codeRegistryBuilder: GraphQLCodeRegistry.Builder
- ) {
- typeDefinitionRegistry.getType(parentType)?.unwrap()
- ?.let { it as? ObjectTypeDefinition }
- ?.fieldDefinitions
- ?.filterNot { it.isIgnored() }
- ?.forEach { field ->
- handler.forEach { h ->
- h.createDataFetcher(operationType, field)?.let { dataFetcher ->
- val interceptedDataFetcher: DataFetcher<*> = dataFetchingInterceptor?.let {
- DataFetcher { env -> dataFetchingInterceptor.fetchData(env, dataFetcher) }
- } ?: dataFetcher
- codeRegistryBuilder.dataFetcher(
- FieldCoordinates.coordinates(parentType, field.name),
- interceptedDataFetcher
- )
- }
- }
- }
- }
-
- private fun ensureRootQueryTypeExists(enhancedRegistry: TypeDefinitionRegistry) {
- var schemaDefinition = enhancedRegistry.schemaDefinition().orElse(null)
- if (schemaDefinition?.operationTypeDefinitions?.find { it.name == "query" } != null) {
+ private fun ensureRootQueryTypeExists() {
+ if (typeDefinitionRegistry.queryType() != null) {
return
}
-
- enhancedRegistry.add(ObjectTypeDefinition.newObjectTypeDefinition().name("Query").build())
-
- if (schemaDefinition != null) {
- // otherwise, adding a transform schema would fail
- enhancedRegistry.remove(schemaDefinition)
- } else {
- schemaDefinition = SchemaDefinition.newSchemaDefinition().build()
- }
-
- enhancedRegistry.add(schemaDefinition.transform {
- it.operationTypeDefinition(
- OperationTypeDefinition
- .newOperationTypeDefinition()
- .name("query")
- .typeName(TypeName("Query"))
- .build()
- )
- })
+ typeDefinitionRegistry.add(
+ ObjectTypeDefinition.newObjectTypeDefinition().name("Query")
+ .fieldDefinition(
+ FieldDefinition
+ .newFieldDefinition()
+ .name("_empty")
+ .type(Constants.Types.Boolean)
+ .build()
+ ).build()
+ )
}
private fun getNeo4jEnhancements(): TypeDefinitionRegistry {
val directivesSdl = javaClass.getResource("/neo4j_types.graphql")?.readText() +
javaClass.getResource("/lib_directives.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)
- }
return typeDefinitionRegistry
}
- private fun addInputType(
- typeDefinitionRegistry: TypeDefinitionRegistry,
- inputName: String,
- relevantFields: List
- ): String {
- if (typeDefinitionRegistry.getType(inputName).isPresent) {
- return inputName
- }
- val inputType = InputObjectTypeDefinition.newInputObjectDefinition()
- .name(inputName)
- .inputValueDefinitions(relevantFields.map {
- InputValueDefinition.newInputValueDefinition()
- .name(it.name)
- .type(it.type)
- .build()
- })
- .build()
- typeDefinitionRegistry.add(inputType)
- return inputName
- }
-
class AliasPropertyDataFetcher : DataFetcher {
override fun get(env: DataFetchingEnvironment): Any? {
val source = env.getSource() ?: return null
diff --git a/core/src/main/kotlin/org/neo4j/graphql/SchemaConfig.kt b/core/src/main/kotlin/org/neo4j/graphql/SchemaConfig.kt
index 40aa6282..30215188 100644
--- a/core/src/main/kotlin/org/neo4j/graphql/SchemaConfig.kt
+++ b/core/src/main/kotlin/org/neo4j/graphql/SchemaConfig.kt
@@ -1,50 +1,38 @@
package org.neo4j.graphql
-data class SchemaConfig @JvmOverloads constructor(
- val query: CRUDConfig = CRUDConfig(),
- val mutation: CRUDConfig = CRUDConfig(),
-
- /**
- * if true, the top level fields of the Query-type will be capitalized
- */
- val capitalizeQueryFields: Boolean = false,
-
- /**
- * if true, the generated fields for query or mutation will use the plural of the types name
- */
- val pluralizeFields: Boolean = false,
-
- /**
- * Defines the way the input for queries and mutations are generated
- */
- val queryOptionStyle: InputStyle = InputStyle.ARGUMENT_PER_FIELD,
+import com.fasterxml.jackson.annotation.JsonProperty
- /**
- * if enabled the `filter` argument will be named `where` and the input type will be named `Where`.
- * additionally, the separated filter arguments will no longer be generated.
- */
- val useWhereFilter: Boolean = false,
-
- /**
- * if enabled the `Date`, `Time`, `LocalTime`, `DateTime` and `LocalDateTime` are used as scalars
- */
- val useTemporalScalars: Boolean = false,
+data class SchemaConfig @JvmOverloads constructor(
+ val features: Neo4jFeaturesSettings = Neo4jFeaturesSettings()
) {
- data class CRUDConfig(val enabled: Boolean = true, val exclude: List = emptyList())
-
- enum class InputStyle {
- /**
- * Separate arguments are generated for the query and / or mutation fields
- */
- @Deprecated(
- message = "Will be removed in the next major release",
- replaceWith = ReplaceWith(expression = "INPUT_TYPE")
- )
- ARGUMENT_PER_FIELD,
- /**
- * All fields are encapsulated into an input type used as one argument in query and / or mutation fields
- */
- INPUT_TYPE,
- }
+ data class Neo4jFeaturesSettings(
+ val filters: Neo4jFiltersSettings = Neo4jFiltersSettings(),
+ )
+
+ data class Neo4jFiltersSettings(
+ // TODO should we also use feature toggles for strings? https://github.com/neo4j/graphql/issues/2657#issuecomment-1369858159
+ @field:JsonProperty("String")
+ val string: Neo4jStringFiltersSettings = Neo4jStringFiltersSettings(),
+ @field:JsonProperty("ID")
+ val id: Neo4jIDFiltersSettings = Neo4jIDFiltersSettings()
+ )
+
+ data class Neo4jStringFiltersSettings(
+ @field:JsonProperty("GT")
+ val gt: Boolean = false,
+ @field:JsonProperty("GTE")
+ val gte: Boolean = false,
+ @field:JsonProperty("LT")
+ val lt: Boolean = false,
+ @field:JsonProperty("LTE")
+ val lte: Boolean = false,
+ @field:JsonProperty("MATCHES")
+ val matches: Boolean = false,
+ )
+
+ data class Neo4jIDFiltersSettings(
+ @field:JsonProperty("MATCHES")
+ val matches: Boolean = false,
+ )
}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/Translator.kt b/core/src/main/kotlin/org/neo4j/graphql/Translator.kt
deleted file mode 100644
index 3aa163ae..00000000
--- a/core/src/main/kotlin/org/neo4j/graphql/Translator.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package org.neo4j.graphql
-
-import graphql.*
-import graphql.execution.NonNullableFieldWasNullError
-import graphql.schema.GraphQLSchema
-
-class Translator(val schema: GraphQLSchema) {
-
- class CypherHolder(val cyphers: MutableList = mutableListOf())
-
- private val gql: GraphQL = GraphQL.newGraphQL(schema).build()
-
- @JvmOverloads
- @Throws(OptimizedQueryException::class)
- fun translate(
- query: String,
- params: Map = emptyMap(),
- ctx: QueryContext = QueryContext()
- ): List {
- val cypherHolder = CypherHolder()
- val executionInput = ExecutionInput.newExecutionInput()
- .query(query)
- .variables(params)
- .graphQLContext(mapOf(QueryContext.KEY to ctx))
- .localContext(cypherHolder)
- .build()
- val result = gql.execute(executionInput)
- result.errors?.forEach {
- when (it) {
- is ExceptionWhileDataFetching -> throw it.exception
-
- is TypeMismatchError, // expected since we return cypher here instead of the correct json
- is NonNullableFieldWasNullError, // expected since the returned cypher does not match the shape of the graphql type
- is SerializationError // expected since the returned cypher does not match the shape of the graphql type
- -> {
- // ignore
- }
- // generic error handling
- is GraphQLError -> throw InvalidQueryException(it)
- }
- }
-
- return cypherHolder.cyphers
- }
-}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/Entity.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/Entity.kt
new file mode 100644
index 00000000..1057445f
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/Entity.kt
@@ -0,0 +1,40 @@
+package org.neo4j.graphql.domain
+
+import org.neo4j.graphql.domain.directives.EntityAnnotations
+import org.neo4j.graphql.domain.naming.EntityNames
+
+sealed interface Entity {
+ val name: String
+ val annotations: EntityAnnotations
+
+ val namings: EntityNames
+ val plural: String get() = namings.plural
+ val pluralKeepCase: String get() = namings.pluralKeepCase
+ val pascalCasePlural: String get() = namings.pascalCasePlural
+
+ fun extractOnTarget(
+ onNode: (Node) -> NODE_RESULT,
+ onInterface: (Interface) -> INTERFACE_RESULT,
+ onUnion: (Union) -> UNION_RESULT,
+ ): RESULT = when (this) {
+ is Node -> onNode(this)
+ is Interface -> onInterface(this)
+ is Union -> onUnion(this)
+ }
+
+ fun extractOnTarget(
+ onImplementingType: (ImplementingType) -> IMPLEMENTATION_TYPE_RESULT,
+ onUnion: (Union) -> UNION_RESULT,
+ ): RESULT = extractOnTarget(
+ onNode = { onImplementingType(it) },
+ onInterface = { onImplementingType(it) },
+ onUnion
+ )
+
+ companion object {
+ @JvmStatic
+ fun leadingUnderscores(name: String): String {
+ return Regex("^(_+).+").matchEntire(name)?.groupValues?.get(1) ?: ""
+ }
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/FieldContainer.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/FieldContainer.kt
new file mode 100644
index 00000000..33bd8d80
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/FieldContainer.kt
@@ -0,0 +1,65 @@
+package org.neo4j.graphql.domain
+
+import org.neo4j.graphql.domain.fields.*
+import org.neo4j.graphql.domain.predicates.ConnectionFieldPredicate
+import org.neo4j.graphql.domain.predicates.RelationFieldPredicate
+import org.neo4j.graphql.domain.predicates.ScalarFieldPredicate
+import org.neo4j.graphql.domain.predicates.definitions.PredicateDefinition
+import org.neo4j.graphql.domain.predicates.definitions.RelationPredicateDefinition
+import org.neo4j.graphql.domain.predicates.definitions.ScalarPredicateDefinition
+import org.neo4j.graphql.schema.model.inputs.WhereInput
+import org.neo4j.graphql.schema.model.inputs.connection.ConnectionWhere
+import org.neo4j.graphql.toDict
+
+/**
+ * A container holding fields
+ */
+sealed class FieldContainer(val fields: List) {
+
+ init {
+ fields.forEach { it.owner = this }
+ }
+
+ abstract val name: String
+
+ private val fieldsByName = fields.map { it.fieldName to it }.toMap()
+
+ val sortableFields: List by lazy {
+ fields
+ .filterNot { it.isList() }
+ .filter {
+ it is PrimitiveField ||
+ it is CustomScalarField ||
+ it is CustomEnumField ||
+ it is PointField
+ }
+
+ }
+
+ private val scalarFields: List by lazy {
+ fields.filterIsInstance()
+ }
+
+ val relationBaseFields: List by lazy { fields.filterIsInstance() }
+ val relationFields: List by lazy { fields.filterIsInstance() }
+
+ private val predicateDefinitions: Map by lazy {
+ val result = mutableMapOf()
+ scalarFields.forEach { result.putAll(it.predicateDefinitions) }
+ relationBaseFields.forEach { result.putAll(it.predicateDefinitions) }
+ result
+ }
+
+
+ fun getField(name: String): BaseField? = fieldsByName[name]
+
+ fun createPredicate(key: String, value: Any?) = predicateDefinitions[key]?.let { def ->
+ when (def) {
+ is ScalarPredicateDefinition -> ScalarFieldPredicate(def, value)
+ is RelationPredicateDefinition -> when (def.connection) {
+ true -> ConnectionFieldPredicate(def, value?.let { ConnectionWhere.create(def.field, it) })
+ false -> RelationFieldPredicate(def, value?.let { WhereInput.create(def.field, it.toDict()) })
+ }
+ }
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/FieldFactory.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/FieldFactory.kt
new file mode 100644
index 00000000..44445b2a
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/FieldFactory.kt
@@ -0,0 +1,170 @@
+package org.neo4j.graphql.domain
+
+import graphql.language.*
+import graphql.schema.idl.ScalarInfo
+import graphql.schema.idl.TypeDefinitionRegistry
+import org.neo4j.graphql.*
+import org.neo4j.graphql.domain.directives.Annotations
+import org.neo4j.graphql.domain.directives.FilterableDirective
+import org.neo4j.graphql.domain.directives.RelationshipDirective
+import org.neo4j.graphql.domain.directives.SelectableDirective
+import org.neo4j.graphql.domain.fields.*
+import org.neo4j.graphql.domain.fields.ObjectField
+import kotlin.reflect.KClass
+import kotlin.reflect.safeCast
+
+/**
+ * A factory to create the internal representation of a [BaseField]
+ */
+object FieldFactory {
+
+ fun createFields(
+ obj: ImplementingTypeDefinition<*>,
+ typeDefinitionRegistry: TypeDefinitionRegistry,
+ relationshipPropertiesFactory: (name: String) -> RelationshipProperties?,
+ schemaConfig: SchemaConfig
+ ): List {
+ val result = mutableListOf()
+
+ obj.fieldDefinitions.forEach { field ->
+
+ val annotations = Annotations(field.directives, typeDefinitionRegistry, obj.name)
+ if (annotations.private != null) return@forEach
+ val typeName = field.type.name()
+
+ val fieldInterface = typeDefinitionRegistry.getTypeByName(typeName)
+ val fieldUnion = typeDefinitionRegistry.getTypeByName(typeName)
+ val fieldScalar = typeDefinitionRegistry.getTypeByName(typeName)
+ ?.takeUnless { ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.containsKey(it.name) }
+ val fieldEnum = typeDefinitionRegistry.getTypeByName(typeName)
+ val fieldObject = typeDefinitionRegistry.getTypeByName(typeName)
+
+ val baseField: BaseField
+
+ if (annotations.relationship != null) {
+
+ var connectionPrefix = obj.name
+ if (obj.implements.isNotEmpty()) {
+ val firstInterface = obj.implements
+ .firstNotNullOfOrNull { typeDefinitionRegistry.getTypeByName(it.name()) }
+ if (firstInterface?.getField(field.name) != null) {
+ // TODO Darrell why only the 1st interface?
+ connectionPrefix = firstInterface.name
+ }
+ }
+
+ baseField = run {
+ val properties = if (annotations.relationship.properties != null) {
+ relationshipPropertiesFactory(annotations.relationship.properties)
+ ?: throw IllegalArgumentException("Cannot find type specified in @${RelationshipDirective.NAME} of ${obj.name}.${field.name}")
+ } else {
+ null
+ }
+ RelationField(
+ field.name,
+ field.type,
+ annotations,
+ properties
+ )
+ }
+
+ val connectionTypeName = "${connectionPrefix}${baseField.fieldName.capitalize()}Connection"
+
+ // TODO do we really need this field?
+ val connectionField = ConnectionField(
+ fieldName = "${baseField.fieldName}Connection",
+ type = TypeName(connectionTypeName).NonNull,
+ Annotations(
+ field.directives.filter {
+ it.name == SelectableDirective.NAME ||
+ it.name == FilterableDirective.NAME ||
+ it.name == "deprecated"
+ },
+ typeDefinitionRegistry,
+ obj.name
+ ),
+ relationshipField = baseField
+ ).apply {
+ this.arguments = field.inputValueDefinitions
+ }
+ result.add(connectionField)
+
+ baseField.connectionField = connectionField
+
+
+ } else if (annotations.customResolver != null) {
+ baseField = ComputedField(field.name, field.type, annotations)
+ } else if (fieldScalar != null) {
+ baseField = CustomScalarField(field.name, field.type, annotations, schemaConfig)
+ } else if (fieldEnum != null) {
+ baseField = CustomEnumField(field.name, field.type, annotations, schemaConfig)
+ if (annotations.coalesce != null) {
+ if (field.type.isList()) {
+ (annotations.coalesce.value as ArrayValue).values
+ } else {
+ listOf(annotations.coalesce.value)
+ }
+ .forEach {
+ require(it is EnumValue) { "@coalesce value on enum fields must be an enum value" }
+ }
+ }
+ } else if (fieldUnion != null) {
+ val nodes = fieldUnion.memberTypes.map { it.name() }
+ baseField = UnionField(field.name, field.type, annotations, nodes)
+ } else if (fieldInterface != null) {
+ val implementations = fieldInterface.implements.mapNotNull { it.name() }
+ baseField = InterfaceField(field.name, field.type, annotations, implementations)
+ } else if (fieldObject != null) {
+ baseField = ObjectField(field.name, field.type, annotations)
+ } else if (Constants.TEMPORAL_TYPES.contains(typeName)) {
+ baseField = TemporalField(field.name, field.type, annotations, schemaConfig)
+ } else if (Constants.POINT_TYPES.contains(typeName)) {
+ val coordinateType = when (typeName) {
+ Constants.POINT_TYPE -> PointField.CoordinateType.GEOGRAPHIC
+ Constants.CARTESIAN_POINT_TYPE -> PointField.CoordinateType.CARTESIAN
+ else -> error("unsupported point type $typeName")
+ }
+ baseField = PointField(field.name, field.type, coordinateType, annotations, schemaConfig)
+ } else {
+ baseField = PrimitiveField(field.name, field.type, annotations, schemaConfig)
+
+ if (annotations.default != null) {
+ val value = annotations.default.value
+ fun > Value<*>.checkKind(clazz: KClass): T = clazz.safeCast(this)
+ ?: throw IllegalArgumentException("Default value for ${obj.name}.${field.name} does not have matching type $typeName")
+ when (typeName) {
+ Constants.ID, Constants.STRING -> value.checkKind(StringValue::class)
+ Constants.BOOLEAN -> value.checkKind(BooleanValue::class)
+ Constants.INT -> value.checkKind(IntValue::class)
+ Constants.FLOAT -> value.checkKind(FloatValue::class)
+ else -> throw IllegalArgumentException("@default directive can only be used on types: Int | Float | String | Boolean | ID")
+ }
+ }
+
+ if (annotations.coalesce != null) {
+ val value = annotations.coalesce.value
+ fun > Value<*>.checkKind(clazz: KClass): T = clazz.safeCast(this)
+ ?: throw IllegalArgumentException("coalesce() value for ${obj.name}.${field.name} does not have matching type $typeName")
+ when (typeName) {
+ Constants.ID, Constants.STRING -> value.checkKind(StringValue::class)
+ Constants.BOOLEAN -> value.checkKind(BooleanValue::class)
+ Constants.INT -> value.checkKind(IntValue::class)
+ Constants.FLOAT -> value.checkKind(FloatValue::class)
+ else -> throw IllegalArgumentException("@coalesce directive can only be used on types: Int | Float | String | Boolean | ID")
+ }
+ }
+
+
+ }
+
+ baseField.apply {
+ this.arguments = field.inputValueDefinitions
+ this.description = field.description
+ this.comments = field.comments
+ }
+ result.add(baseField)
+ }
+
+ return result
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/ImplementingType.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/ImplementingType.kt
new file mode 100644
index 00000000..e5514af8
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/ImplementingType.kt
@@ -0,0 +1,40 @@
+package org.neo4j.graphql.domain
+
+import graphql.language.Comment
+import graphql.language.Description
+import org.neo4j.graphql.domain.directives.ImplementingTypeAnnotations
+import org.neo4j.graphql.domain.fields.BaseField
+import org.neo4j.graphql.domain.naming.ImplementingTypeNames
+
+sealed class ImplementingType(
+ override val name: String,
+ val description: Description? = null,
+ val comments: List = emptyList(),
+ fields: List,
+ val interfaces: List,
+ override val annotations: ImplementingTypeAnnotations,
+) : FieldContainer(fields), Entity {
+
+ abstract override val namings: ImplementingTypeNames
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Interface) return false
+
+ if (name != other.name) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return name.hashCode()
+ }
+
+ fun extractOnImplementingType(
+ onNode: (Node) -> NODE_RESULT,
+ onInterface: (Interface) -> INTERFACE_RESULT,
+ ): RESULT = when (this) {
+ is Node -> onNode(this)
+ is Interface -> onInterface(this)
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/Interface.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/Interface.kt
new file mode 100644
index 00000000..e86b8626
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/Interface.kt
@@ -0,0 +1,30 @@
+package org.neo4j.graphql.domain
+
+import graphql.language.Comment
+import graphql.language.Description
+import org.neo4j.graphql.domain.directives.InterfaceAnnotations
+import org.neo4j.graphql.domain.fields.BaseField
+import org.neo4j.graphql.domain.naming.InterfaceNames
+
+class Interface(
+ name: String,
+ description: Description? = null,
+ comments: List = emptyList(),
+ fields: List,
+ interfaces: List,
+ override val annotations: InterfaceAnnotations,
+) : ImplementingType(name, description, comments, fields, interfaces, annotations), NodeResolver {
+
+ lateinit var implementations: Map
+
+ override val namings = InterfaceNames(name, annotations)
+
+ override fun getRequiredNode(name: String) = implementations[name]
+ ?: throw IllegalArgumentException("unknown implementation $name for interface ${this.name}")
+
+ override fun getNode(name: String) = implementations[name]
+
+ override fun toString(): String {
+ return "Interface('$name')"
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/InterfaceFactory.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/InterfaceFactory.kt
new file mode 100644
index 00000000..ff462bed
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/InterfaceFactory.kt
@@ -0,0 +1,39 @@
+package org.neo4j.graphql.domain
+
+import graphql.language.InterfaceTypeDefinition
+import graphql.schema.idl.TypeDefinitionRegistry
+import org.neo4j.graphql.SchemaConfig
+import org.neo4j.graphql.domain.directives.Annotations
+import org.neo4j.graphql.name
+
+/**
+ * A factory to create the internal representation of a [Node]
+ */
+object InterfaceFactory {
+
+ fun createInterface(
+ definition: InterfaceTypeDefinition,
+ typeDefinitionRegistry: TypeDefinitionRegistry,
+ relationshipPropertiesFactory: (name: String) -> RelationshipProperties?,
+ interfaceFactory: (name: String) -> Interface?,
+ schemaConfig: SchemaConfig,
+ ): Interface {
+
+ val annotations = Annotations(definition.directives, typeDefinitionRegistry, definition.name)
+ val interfaces = definition.implements.mapNotNull { interfaceFactory(it.name()) }
+
+ return Interface(
+ definition.name,
+ definition.description,
+ definition.comments,
+ FieldFactory.createFields(
+ definition,
+ typeDefinitionRegistry,
+ relationshipPropertiesFactory,
+ schemaConfig
+ ),
+ interfaces,
+ annotations,
+ )
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/Model.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/Model.kt
new file mode 100644
index 00000000..9dbe9fd3
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/Model.kt
@@ -0,0 +1,168 @@
+package org.neo4j.graphql.domain
+
+import graphql.language.InterfaceTypeDefinition
+import graphql.language.ObjectTypeDefinition
+import graphql.language.UnionTypeDefinition
+import graphql.schema.idl.TypeDefinitionRegistry
+import org.neo4j.graphql.*
+import org.neo4j.graphql.domain.directives.Annotations
+import org.neo4j.graphql.domain.fields.RelationField
+import org.neo4j.graphql.domain.fields.ScalarField
+import org.neo4j.graphql.merge.TypeDefinitionRegistryMerger
+import java.util.concurrent.ConcurrentHashMap
+
+data class Model(
+ val nodes: Collection,
+ val relationship: Collection,
+ val interfaces: Collection,
+ val unions: Collection
+) {
+
+ val entities get() = nodes + interfaces + unions
+
+ companion object {
+ fun createModel(
+ typeDefinitionRegistry: TypeDefinitionRegistry,
+ schemaConfig: SchemaConfig
+ ) =
+ ModelInitializer(typeDefinitionRegistry, schemaConfig).createModel()
+ }
+
+ private class ModelInitializer(
+ val typeDefinitionRegistry: TypeDefinitionRegistry,
+ val schemaConfig: SchemaConfig,
+ ) {
+ private val relationshipFields = mutableMapOf()
+ private val interfaces = ConcurrentHashMap()
+
+ private val schema = Schema()
+
+ init {
+ TypeDefinitionRegistryMerger.mergeExtensions(typeDefinitionRegistry)
+
+ schema.annotations = Annotations(
+ typeDefinitionRegistry.schemaDefinition().unwrap()?.directives ?: emptyList(),
+ typeDefinitionRegistry
+ )
+ }
+
+
+ fun createModel(): Model {
+ val queryTypeName = typeDefinitionRegistry.queryTypeName()
+ val mutationTypeName = typeDefinitionRegistry.mutationTypeName()
+ val subscriptionTypeName = typeDefinitionRegistry.subscriptionTypeName()
+
+
+ val reservedTypes =
+ setOf(queryTypeName, mutationTypeName, subscriptionTypeName)
+
+ val objectNodes = typeDefinitionRegistry.getTypes(ObjectTypeDefinition::class.java)
+ .filterNot { reservedTypes.contains(it.name) }
+
+
+ val nodes = objectNodes.mapNotNull { node ->
+ NodeFactory.createNode(
+ node,
+ typeDefinitionRegistry,
+ this::getOrCreateRelationshipProperties,
+ this::getOrCreateInterface,
+ schemaConfig,
+ )
+ }
+ val nodesByName = nodes.associateBy { it.name }
+ val implementations = mutableMapOf>()
+
+ val unions = parseUnions(nodesByName)
+
+ nodes.forEach { node ->
+ initRelations(node, nodesByName, unions)
+ node.interfaces.forEach {
+ implementations.computeIfAbsent(it) { mutableListOf() }.add(node)
+ }
+ }
+
+ implementations.forEach { (interfaze, impls) ->
+ interfaze.implementations = impls.sortedBy { it.name }.map { it.name to it }.toMap()
+ initRelations(interfaze, nodesByName, unions)
+ }
+ val relationships = nodes.flatMap { it.relationFields }
+
+ return Model(nodes, relationships, implementations.keys, unions.values)
+ }
+
+ private fun parseUnions(nodesByName: Map): Map {
+ val unions = mutableMapOf()
+ typeDefinitionRegistry.getTypes(UnionTypeDefinition::class.java).forEach { unionDefinition ->
+ val annotations = Annotations(unionDefinition.directives, typeDefinitionRegistry, unionDefinition.name)
+ unionDefinition.memberTypes
+ .mapNotNull { nodesByName[it.name()] }
+ .sortedBy { it.name }
+ .map { it.name to it }
+ .takeIf { it.isNotEmpty() }
+ ?.let { Union(unionDefinition.name, it.toMap(), annotations) }
+ ?.let { unions[unionDefinition.name] = it }
+ }
+ return unions
+ }
+
+ private fun initRelations(
+ type: ImplementingType,
+ nodesByName: Map,
+ unions: Map
+ ) {
+ type.relationBaseFields.forEach { field ->
+ val nodeName = field.type.name()
+ field.target = unions[nodeName] ?: nodesByName[nodeName]
+ ?: getOrCreateInterface(nodeName)
+ ?: error("Unknown type $nodeName")
+ }
+ }
+
+ private fun getOrCreateRelationshipProperties(name: String): RelationshipProperties? {
+ return relationshipFields.computeIfAbsent(name) {
+ val relationship =
+ typeDefinitionRegistry.getTypeByName(it) ?: return@computeIfAbsent null
+
+ relationship.fieldDefinitions?.forEach { field ->
+ Constants.RESERVED_INTERFACE_FIELDS[field.name]?.let { message ->
+ throw IllegalArgumentException(
+ message
+ )
+ }
+ field.directives.forEach { directive ->
+ if (Constants.FORBIDDEN_RELATIONSHIP_PROPERTY_DIRECTIVES.contains(directive.name)) {
+ throw IllegalArgumentException("Cannot have @${directive.name} directive on relationship property")
+ }
+ }
+ }
+
+ val fields = FieldFactory.createFields(
+ relationship,
+ typeDefinitionRegistry,
+ this::getOrCreateRelationshipProperties,
+ schemaConfig,
+ ).onEach { field ->
+ if (field !is ScalarField) {
+ throw IllegalStateException("Field $name.${field.fieldName} is expected to be of scalar type")
+ }
+ }
+ .filterIsInstance()
+ RelationshipProperties(name, fields)
+ }
+ }
+
+ private fun getOrCreateInterface(name: String): Interface? {
+ return interfaces.computeIfAbsent(name) {
+ val interfaze =
+ typeDefinitionRegistry.getTypeByName(it) ?: return@computeIfAbsent null
+ return@computeIfAbsent InterfaceFactory.createInterface(
+ interfaze,
+ typeDefinitionRegistry,
+ ::getOrCreateRelationshipProperties,
+ ::getOrCreateInterface,
+ schemaConfig,
+ )
+ }
+ }
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/Node.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/Node.kt
new file mode 100644
index 00000000..500df97a
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/Node.kt
@@ -0,0 +1,51 @@
+package org.neo4j.graphql.domain
+
+import graphql.language.Comment
+import graphql.language.Description
+import org.neo4j.cypherdsl.core.Cypher
+import org.neo4j.cypherdsl.core.SymbolicName
+import org.neo4j.graphql.QueryContext
+import org.neo4j.graphql.domain.directives.NodeAnnotations
+import org.neo4j.graphql.domain.fields.BaseField
+import org.neo4j.graphql.domain.naming.NodeNames
+
+class Node(
+ name: String,
+ description: Description? = null,
+ comments: List = emptyList(),
+ fields: List,
+ interfaces: List,
+ override val annotations: NodeAnnotations,
+) : ImplementingType(name, description, comments, fields, interfaces, annotations) {
+
+ override val namings = NodeNames(name, annotations)
+ val hasRelayId: Boolean get() = fields.any { it.annotations.relayId != null }
+
+ private val mainLabel: String get() = annotations.node?.labels?.firstOrNull() ?: name
+
+ private val additionalLabels: List get() = annotations.node?.labels?.drop(1) ?: emptyList()
+
+ private fun additionalLabels(queryContext: QueryContext?): List =
+ additionalLabels.map { mapLabelWithContext(it, queryContext) }
+
+ private fun mapLabelWithContext(label: String, context: QueryContext?): String {
+ return context?.resolve(label) ?: label
+ }
+
+ override fun toString(): String {
+ return "Node('$name')"
+ }
+
+ fun asCypherNode(queryContext: QueryContext?, name: String? = null) =
+ Cypher.node(mapLabelWithContext(mainLabel, queryContext), additionalLabels(queryContext)).let {
+ when {
+ name != null -> it.named(name)
+ else -> it
+ }
+ }
+
+ fun asCypherNode(queryContext: QueryContext?, name: SymbolicName) =
+ Cypher.node(mapLabelWithContext(mainLabel, queryContext), additionalLabels(queryContext)).named(name)
+
+}
+
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/NodeFactory.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/NodeFactory.kt
new file mode 100644
index 00000000..a7e0a1d5
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/NodeFactory.kt
@@ -0,0 +1,47 @@
+package org.neo4j.graphql.domain
+
+import graphql.language.ObjectTypeDefinition
+import graphql.schema.idl.TypeDefinitionRegistry
+import org.neo4j.graphql.SchemaConfig
+import org.neo4j.graphql.domain.directives.Annotations
+import org.neo4j.graphql.name
+
+/**
+ * A factory to create the internal representation of a [Node]
+ */
+object NodeFactory {
+
+ fun createNode(
+ definition: ObjectTypeDefinition,
+ typeDefinitionRegistry: TypeDefinitionRegistry,
+ relationshipPropertiesFactory: (name: String) -> RelationshipProperties?,
+ interfaceFactory: (name: String) -> Interface?,
+ schemaConfig: SchemaConfig,
+ ): Node? {
+
+ val schemeDirectives =
+ typeDefinitionRegistry.schemaExtensionDefinitions?.map { it.directives }?.flatten() ?: emptyList()
+ val annotations = Annotations(schemeDirectives + definition.directives, typeDefinitionRegistry, definition.name)
+ if (annotations.node == null || annotations.relationshipProperties != null) {
+ return null
+ }
+ val interfaces = definition.implements.mapNotNull { interfaceFactory(it.name()) }
+ val fields = FieldFactory.createFields(
+ definition,
+ typeDefinitionRegistry,
+ relationshipPropertiesFactory,
+ schemaConfig
+ )
+ annotations.limit?.validate(definition.name)
+
+ val node = Node(
+ definition.name,
+ definition.description,
+ definition.comments,
+ fields,
+ interfaces,
+ annotations,
+ )
+ return node
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/NodeResolver.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/NodeResolver.kt
new file mode 100644
index 00000000..bf154a17
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/NodeResolver.kt
@@ -0,0 +1,9 @@
+package org.neo4j.graphql.domain
+
+interface NodeResolver {
+
+ fun getRequiredNode(name: String): Node
+
+ fun getNode(name: String): Node?
+
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/RelationshipProperties.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/RelationshipProperties.kt
new file mode 100644
index 00000000..bdbc9230
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/RelationshipProperties.kt
@@ -0,0 +1,26 @@
+package org.neo4j.graphql.domain
+
+import org.neo4j.graphql.domain.fields.RelationField
+import org.neo4j.graphql.domain.fields.ScalarField
+
+/**
+ * A container holding the fields of a rich relationship (relation with properties)
+ */
+class RelationshipProperties(
+ /**
+ * The name of the interface declaring the relationships properties
+ */
+ val typeName: String,
+ /**
+ * the fields of the rich relation
+ */
+ fields: List
+) : FieldContainer(fields) {
+ override val name: String get() = typeName
+ private val mutableUsedByRelations: MutableList = mutableListOf()
+ val usedByRelations: List get() = mutableUsedByRelations
+
+ fun addUsedByRelation(relation: RelationField) {
+ mutableUsedByRelations.add(relation)
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/Schema.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/Schema.kt
new file mode 100644
index 00000000..9e3de312
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/Schema.kt
@@ -0,0 +1,7 @@
+package org.neo4j.graphql.domain
+
+import org.neo4j.graphql.domain.directives.SchemaAnnotations
+
+class Schema {
+ lateinit var annotations: SchemaAnnotations
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/Union.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/Union.kt
new file mode 100644
index 00000000..33105d18
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/Union.kt
@@ -0,0 +1,17 @@
+package org.neo4j.graphql.domain
+
+import org.neo4j.graphql.domain.directives.UnionAnnotations
+import org.neo4j.graphql.domain.naming.UnionNames
+
+class Union(
+ override val name: String,
+ val nodes: Map,
+ override val annotations: UnionAnnotations,
+) : NodeResolver, Entity {
+
+ override fun getRequiredNode(name: String) = nodes[name]
+ ?: throw IllegalArgumentException("unknown implementation $name for union ${this.name}")
+
+ override fun getNode(name: String) = nodes[name]
+ override val namings = UnionNames(name, annotations)
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/AliasDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/AliasDirective.kt
new file mode 100644
index 00000000..b9dfd111
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/AliasDirective.kt
@@ -0,0 +1,17 @@
+package org.neo4j.graphql.domain.directives
+
+import graphql.language.Directive
+import org.neo4j.graphql.readRequiredArgument
+
+class AliasDirective private constructor(
+ val property: String,
+) : Annotation {
+
+ companion object {
+ internal fun create(directive: Directive): AliasDirective {
+ return AliasDirective(directive.readRequiredArgument(AliasDirective::property))
+ }
+ }
+}
+
+
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/Annotation.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/Annotation.kt
new file mode 100644
index 00000000..f8dea0cd
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/Annotation.kt
@@ -0,0 +1,6 @@
+package org.neo4j.graphql.domain.directives
+
+/**
+ * Marker interface for all annotations.
+ */
+interface Annotation
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/Annotations.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/Annotations.kt
new file mode 100644
index 00000000..1f04facb
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/Annotations.kt
@@ -0,0 +1,116 @@
+package org.neo4j.graphql.domain.directives
+
+import graphql.language.Directive
+import graphql.schema.idl.TypeDefinitionRegistry
+import kotlin.reflect.KProperty1
+
+sealed interface CommonAnnotations {
+ val otherDirectives: List
+}
+
+interface EntityAnnotations : CommonAnnotations {
+ val plural: PluralDirective?
+ val query: QueryDirective?
+}
+
+interface ImplementingTypeAnnotations : EntityAnnotations {
+ val limit: LimitDirective?
+}
+
+interface NodeAnnotations : ImplementingTypeAnnotations {
+ val node: NodeDirective?
+}
+
+interface InterfaceAnnotations : ImplementingTypeAnnotations {
+ val relationshipProperties: RelationshipPropertiesDirective?
+}
+
+interface UnionAnnotations : EntityAnnotations
+
+interface FieldAnnotations : CommonAnnotations {
+ val alias: AliasDirective?
+ val coalesce: CoalesceDirective?
+ val customResolver: CustomResolverDirective?
+ val default: DefaultDirective?
+ val filterable: FilterableDirective?
+ val id: IdDirective?
+ val private: PrivateDirective?
+ val relationship: RelationshipDirective?
+ val relationshipBaseDirective: RelationshipBaseDirective?
+ val relayId: RelayIdDirective?
+ val selectable: SelectableDirective?
+}
+
+interface SchemaAnnotations : CommonAnnotations {
+ val query: QueryDirective?
+}
+
+class Annotations(
+ directives: Map,
+ typeDefinitionRegistry: TypeDefinitionRegistry,
+ ownerName: String? = null,
+) : ImplementingTypeAnnotations, NodeAnnotations, InterfaceAnnotations, UnionAnnotations, FieldAnnotations,
+ SchemaAnnotations {
+
+ constructor(
+ directives: Collection,
+ typeDefinitionRegistry: TypeDefinitionRegistry,
+ parentTypeName: String? = null,
+ ) : this(
+ directives.associateBy { it.name },
+ typeDefinitionRegistry,
+ parentTypeName
+ )
+
+ private val libraryDirectives = mutableSetOf()
+
+ private val unhandledDirectives: MutableMap = directives.toMutableMap()
+
+ override val otherDirectives get() = this.unhandledDirectives.values.toList()
+
+ override val alias: AliasDirective? = parse(Annotations::alias, AliasDirective::create)
+
+ override val coalesce: CoalesceDirective? = parse(Annotations::coalesce, CoalesceDirective::create)
+
+ override val customResolver: CustomResolverDirective? =
+ parse(Annotations::customResolver) { CustomResolverDirective.create(it, ownerName, typeDefinitionRegistry) }
+
+ override val default: DefaultDirective? = parse(Annotations::default, DefaultDirective::create)
+
+ override val filterable: FilterableDirective? = parse(Annotations::filterable, FilterableDirective::create)
+
+ override val id: IdDirective? = parse(Annotations::id) { IdDirective.INSTANCE }
+
+ override val limit: LimitDirective? = parse(Annotations::limit, LimitDirective::create)
+
+ override val node: NodeDirective? = parse(Annotations::node, NodeDirective::create)
+
+ override val plural: PluralDirective? = parse(Annotations::plural, PluralDirective::create)
+
+ override val private: PrivateDirective? = parse(Annotations::private) { PrivateDirective.INSTANCE }
+
+ override val query: QueryDirective? = parse(Annotations::query, QueryDirective::create)
+
+ override val relationship: RelationshipDirective? = parse(Annotations::relationship, RelationshipDirective::create)
+
+ override val relationshipBaseDirective: RelationshipBaseDirective? get() = relationship
+
+ override val relationshipProperties: RelationshipPropertiesDirective? =
+ parse(Annotations::relationshipProperties) { RelationshipPropertiesDirective.INSTANCE }
+
+ override val relayId: RelayIdDirective? = parse(Annotations::relayId) { RelayIdDirective.INSTANCE }
+
+ override val selectable: SelectableDirective? = parse(Annotations::selectable, SelectableDirective::create)
+
+ private fun parse(
+ prop: KProperty1,
+ factory: (Directive) -> T,
+ ): T? {
+ libraryDirectives.add(prop.name)
+ return unhandledDirectives.remove(prop.name)?.let { return factory(it) }
+ }
+
+ companion object {
+ val LIBRARY_DIRECTIVES = Annotations(emptyMap(), TypeDefinitionRegistry()).libraryDirectives
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/CoalesceDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/CoalesceDirective.kt
new file mode 100644
index 00000000..7996ea3d
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/CoalesceDirective.kt
@@ -0,0 +1,19 @@
+package org.neo4j.graphql.domain.directives
+
+import graphql.language.Directive
+import graphql.language.Value
+import org.neo4j.graphql.readRequiredArgument
+import kotlin.Annotation
+
+class CoalesceDirective private constructor(
+ val value: Value<*>,
+) : Annotation {
+
+ companion object {
+ internal fun create(directive: Directive): CoalesceDirective {
+ return CoalesceDirective(directive.readRequiredArgument(CoalesceDirective::value) { it })
+ }
+ }
+}
+
+
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/CustomResolverDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/CustomResolverDirective.kt
new file mode 100644
index 00000000..47f2396e
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/CustomResolverDirective.kt
@@ -0,0 +1,132 @@
+package org.neo4j.graphql.domain.directives
+
+import graphql.language.*
+import graphql.parser.Parser
+import graphql.schema.idl.TypeDefinitionRegistry
+import org.neo4j.graphql.getTypeByName
+import org.neo4j.graphql.name
+import org.neo4j.graphql.readArgument
+import org.neo4j.graphql.utils.ResolveTreeBuilder
+import org.neo4j.graphql.utils.SelectionOfType
+import kotlin.Annotation
+
+class CustomResolverDirective private constructor(
+ val requires: SelectionOfType?,
+) : Annotation {
+
+ companion object {
+
+ private const val NAME = "customResolver"
+
+ private const val INVALID_REQUIRED_FIELD_ERROR =
+ "It is not possible to require fields that use the following directives: @customResolver"
+ private const val INVALID_SELECTION_SET_ERROR = "Invalid selection set passed to @customResolver requires"
+
+
+ internal fun create(
+ directive: Directive,
+ ownerName: String?,
+ typeDefinitionRegistry: TypeDefinitionRegistry,
+ ): CustomResolverDirective {
+ val requires = directive.readArgument(CustomResolverDirective::requires) {
+ val requiresString = (it as StringValue).value
+
+ requireNotNull(ownerName) { "Parent type is required to resolve nested selection set" }
+
+ val selectionSet = (Parser().parseDocument("{$requiresString}")
+ ?.definitions?.singleOrNull() as? OperationDefinition)
+ ?.selectionSet
+ ?: error(INVALID_SELECTION_SET_ERROR)
+
+ val builder = ResolveTreeBuilder()
+ buildResolveTree(typeDefinitionRegistry, selectionSet, ownerName, builder)
+ return@readArgument builder.getFieldsByTypeName()[ownerName]
+
+ }
+ return CustomResolverDirective(requires)
+ }
+
+ private fun buildResolveTree(
+ typeDefinitionRegistry: TypeDefinitionRegistry,
+ selectionSet: SelectionSet,
+ ownerName: String,
+ builder: ResolveTreeBuilder
+ ) {
+ val fields by lazy { getObjectFields(ownerName, typeDefinitionRegistry) }
+ selectionSet.selections.forEach { selection ->
+ when (selection) {
+
+ is FragmentSpread -> error("Fragment spreads are not supported in @customResolver requires")
+
+ is InlineFragment -> {
+ if (selection.selectionSet == null) return@forEach
+ val fieldType = selection.typeCondition?.name ?: error(INVALID_SELECTION_SET_ERROR)
+ buildResolveTree(
+ typeDefinitionRegistry,
+ selection.selectionSet,
+ fieldType,
+ builder
+ )
+
+ }
+
+ is Field -> {
+ builder.select(selection.name, ownerName) {
+
+ if (selection.selectionSet != null) {
+ val field = fields.find { it.name == selection.name }
+ val fieldType = field?.type?.name() ?: error(INVALID_SELECTION_SET_ERROR)
+
+ buildResolveTree(
+ typeDefinitionRegistry,
+ selection.selectionSet,
+ fieldType,
+ this
+ )
+ }
+
+ validateRequiredField(selection, ownerName, fields, typeDefinitionRegistry)
+ }
+ }
+ }
+ }
+ }
+
+
+ private fun getObjectFields(
+ fieldType: String,
+ typeDefinitionRegistry: TypeDefinitionRegistry
+ ): List {
+ return (typeDefinitionRegistry.getTypeByName(fieldType)
+ ?: typeDefinitionRegistry.getTypeByName(fieldType))
+ ?.fieldDefinitions
+ ?: typeDefinitionRegistry.getTypeByName(fieldType)?.memberTypes?.flatMap {
+ (typeDefinitionRegistry.getTypeByName(it.name())
+ ?: typeDefinitionRegistry.getTypeByName(it.name()))
+ ?.fieldDefinitions
+ ?: emptyList()
+ }
+ ?: error(INVALID_SELECTION_SET_ERROR)
+ }
+
+ private fun validateRequiredField(
+ selection: Field,
+ ownerType: String?,
+ fields: List,
+ typeDefinitionRegistry: TypeDefinitionRegistry,
+ ) {
+ val fieldImplementations = mutableListOf(fields.find { it.name == selection.name })
+
+ typeDefinitionRegistry
+ .getTypes(ObjectTypeDefinition::class.java)
+ .filter { obj -> obj.implements.any { it.name() == ownerType } }
+ .flatMap { it.fieldDefinitions }
+ .filterTo(fieldImplementations) { it.name == selection.name }
+
+ if (fieldImplementations.filterNotNull().any { !it.getDirectives(NAME).isNullOrEmpty() }) {
+ throw IllegalArgumentException(INVALID_REQUIRED_FIELD_ERROR)
+ }
+ }
+ }
+}
+
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/DefaultDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/DefaultDirective.kt
new file mode 100644
index 00000000..bd282e15
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/DefaultDirective.kt
@@ -0,0 +1,19 @@
+package org.neo4j.graphql.domain.directives
+
+import graphql.language.Directive
+import graphql.language.Value
+import org.neo4j.graphql.readRequiredArgument
+import kotlin.Annotation
+
+class DefaultDirective private constructor(
+ val value: Value<*>,
+) : Annotation {
+
+ companion object {
+ internal fun create(directive: Directive): DefaultDirective {
+ return DefaultDirective(directive.readRequiredArgument(DefaultDirective::value) { it })
+ }
+ }
+}
+
+
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/FilterableDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/FilterableDirective.kt
new file mode 100644
index 00000000..e4f266d5
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/FilterableDirective.kt
@@ -0,0 +1,22 @@
+package org.neo4j.graphql.domain.directives
+
+import graphql.language.Directive
+import org.neo4j.graphql.readArgument
+import kotlin.Annotation
+
+class FilterableDirective private constructor(
+ val byValue: Boolean,
+) : Annotation {
+
+ companion object {
+ const val NAME = "filterable"
+
+ internal fun create(directive: Directive): FilterableDirective {
+ return FilterableDirective(
+ directive.readArgument(FilterableDirective::byValue) ?: true,
+ )
+ }
+ }
+}
+
+
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/IdDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/IdDirective.kt
new file mode 100644
index 00000000..1179be3b
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/IdDirective.kt
@@ -0,0 +1,12 @@
+package org.neo4j.graphql.domain.directives
+
+import kotlin.Annotation
+
+class IdDirective private constructor() : Annotation {
+
+ companion object {
+ internal val INSTANCE = IdDirective()
+ }
+}
+
+
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/LimitDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/LimitDirective.kt
new file mode 100644
index 00000000..d23fe2c4
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/LimitDirective.kt
@@ -0,0 +1,36 @@
+package org.neo4j.graphql.domain.directives
+
+import graphql.language.Directive
+import org.neo4j.graphql.readArgument
+import org.neo4j.graphql.toJavaValue
+import kotlin.Annotation
+
+class LimitDirective private constructor(
+ val default: Int?,
+ val max: Int?,
+) : Annotation {
+
+ fun validate(typeName: String) {
+ when {
+ default != null && default <= 0 ->
+ throw IllegalArgumentException("$typeName @queryOptions(limit: {default: ${default}}) invalid value: '${default}', it should be a number greater than 0")
+
+ max != null && max <= 0 ->
+ throw IllegalArgumentException("$typeName @queryOptions(limit: {max: ${max}}) invalid value: '${max}', it should be a number greater than 0")
+
+ max != null && default != null ->
+ require(max >= default) { "$typeName @queryOptions(limit: {max: ${max}, default: ${default}}) invalid default value, 'default' must be smaller than 'max'" }
+ }
+ }
+
+ companion object {
+ internal fun create(directive: Directive): LimitDirective {
+ return LimitDirective(
+ directive.readArgument(LimitDirective::default, { (it.toJavaValue() as? Number)?.toInt() }),
+ directive.readArgument(LimitDirective::max, { (it.toJavaValue() as? Number)?.toInt() }),
+ )
+ }
+ }
+}
+
+
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/NodeDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/NodeDirective.kt
new file mode 100644
index 00000000..491ead22
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/NodeDirective.kt
@@ -0,0 +1,18 @@
+package org.neo4j.graphql.domain.directives
+
+import graphql.language.Directive
+import org.neo4j.graphql.readArgument
+import kotlin.Annotation
+
+class NodeDirective private constructor(
+ var labels: List? = null,
+) : Annotation {
+
+ companion object {
+ internal fun create(directive: Directive): NodeDirective {
+ return NodeDirective(
+ directive.readArgument(NodeDirective::labels),
+ )
+ }
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/PluralDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/PluralDirective.kt
new file mode 100644
index 00000000..2c892d64
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/PluralDirective.kt
@@ -0,0 +1,14 @@
+package org.neo4j.graphql.domain.directives
+
+import graphql.language.Directive
+import org.neo4j.graphql.readRequiredArgument
+import kotlin.Annotation
+
+class PluralDirective private constructor(var value: String) : Annotation {
+
+ companion object {
+ internal fun create(directive: Directive): PluralDirective {
+ return PluralDirective(directive.readRequiredArgument(PluralDirective::value))
+ }
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/PrivateDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/PrivateDirective.kt
new file mode 100644
index 00000000..98b3896b
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/PrivateDirective.kt
@@ -0,0 +1,12 @@
+package org.neo4j.graphql.domain.directives
+
+import kotlin.Annotation
+
+class PrivateDirective private constructor() : Annotation {
+
+ companion object {
+ internal val INSTANCE = PrivateDirective()
+ }
+}
+
+
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/QueryDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/QueryDirective.kt
new file mode 100644
index 00000000..a01f0d48
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/QueryDirective.kt
@@ -0,0 +1,20 @@
+package org.neo4j.graphql.domain.directives
+
+import graphql.language.Directive
+import org.neo4j.graphql.readArgument
+import kotlin.Annotation
+
+class QueryDirective private constructor(
+ val read: Boolean,
+) : Annotation {
+
+ companion object {
+ internal fun create(directive: Directive): QueryDirective {
+ return QueryDirective(
+ directive.readArgument(QueryDirective::read) ?: true,
+ )
+ }
+ }
+}
+
+
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/RelationshipBaseDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/RelationshipBaseDirective.kt
new file mode 100644
index 00000000..58c342a4
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/RelationshipBaseDirective.kt
@@ -0,0 +1,3 @@
+package org.neo4j.graphql.domain.directives
+
+sealed class RelationshipBaseDirective
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/RelationshipDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/RelationshipDirective.kt
new file mode 100644
index 00000000..6ced401b
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/RelationshipDirective.kt
@@ -0,0 +1,36 @@
+package org.neo4j.graphql.domain.directives
+
+import graphql.language.Directive
+import graphql.language.EnumValue
+import org.neo4j.graphql.domain.fields.RelationField
+import org.neo4j.graphql.readArgument
+import org.neo4j.graphql.readRequiredArgument
+
+class RelationshipDirective private constructor(
+ val direction: RelationField.Direction,
+ val type: String,
+ val properties: String?,
+ val queryDirection: RelationField.QueryDirection,
+) : RelationshipBaseDirective(), Annotation {
+
+ companion object {
+ const val NAME = "relationship"
+
+ internal fun create(directive: Directive): RelationshipDirective {
+ val direction =
+ directive.readRequiredArgument(RelationshipDirective::direction) { RelationField.Direction.valueOf((it as EnumValue).name) }
+
+ val type = directive.readRequiredArgument(RelationshipDirective::type)
+
+ val properties = directive.readArgument(RelationshipDirective::properties)
+
+ val queryDirection =
+ directive.readArgument(RelationshipDirective::queryDirection) { RelationField.QueryDirection.valueOf((it as EnumValue).name) }
+ ?: RelationField.QueryDirection.DIRECTED
+
+ return RelationshipDirective(direction, type, properties, queryDirection)
+ }
+ }
+}
+
+
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/RelationshipPropertiesDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/RelationshipPropertiesDirective.kt
new file mode 100644
index 00000000..ef0df777
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/RelationshipPropertiesDirective.kt
@@ -0,0 +1,12 @@
+package org.neo4j.graphql.domain.directives
+
+import kotlin.Annotation
+
+class RelationshipPropertiesDirective private constructor() : Annotation {
+
+ companion object {
+ internal val INSTANCE = RelationshipPropertiesDirective()
+ }
+}
+
+
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/RelayIdDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/RelayIdDirective.kt
new file mode 100644
index 00000000..d50a96ee
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/RelayIdDirective.kt
@@ -0,0 +1,12 @@
+package org.neo4j.graphql.domain.directives
+
+import kotlin.Annotation
+
+class RelayIdDirective private constructor() : Annotation {
+
+ companion object {
+ internal val INSTANCE = RelayIdDirective()
+ }
+}
+
+
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/directives/SelectableDirective.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/SelectableDirective.kt
new file mode 100644
index 00000000..7ebb7262
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/directives/SelectableDirective.kt
@@ -0,0 +1,24 @@
+package org.neo4j.graphql.domain.directives
+
+import graphql.language.Directive
+import org.neo4j.graphql.readRequiredArgument
+import kotlin.Annotation
+
+class SelectableDirective private constructor(
+ val onRead: Boolean,
+ val onAggregate: Boolean,
+) : Annotation {
+
+ companion object {
+ const val NAME = "selectable"
+
+ internal fun create(directive: Directive): SelectableDirective {
+ return SelectableDirective(
+ directive.readRequiredArgument(SelectableDirective::onRead),
+ directive.readRequiredArgument(SelectableDirective::onAggregate)
+ )
+ }
+ }
+}
+
+
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/BaseField.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/BaseField.kt
new file mode 100644
index 00000000..ca8b0239
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/BaseField.kt
@@ -0,0 +1,74 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Comment
+import graphql.language.Description
+import graphql.language.InputValueDefinition
+import graphql.language.Type
+import org.neo4j.graphql.asType
+import org.neo4j.graphql.domain.FieldContainer
+import org.neo4j.graphql.domain.Interface
+import org.neo4j.graphql.domain.Node
+import org.neo4j.graphql.domain.RelationshipProperties
+import org.neo4j.graphql.domain.directives.FieldAnnotations
+import org.neo4j.graphql.isList
+import org.neo4j.graphql.isRequired
+import org.neo4j.graphql.name
+
+/**
+ * Representation a ObjectTypeDefinitionNode field.
+ */
+sealed class BaseField(
+ val fieldName: String,
+ val type: Type<*>,
+ val annotations: FieldAnnotations
+) {
+ val deprecatedDirective
+ get() = annotations.otherDirectives.firstOrNull { it.name == "deprecated" }
+ var arguments: List = emptyList()
+
+ var description: Description? = null
+ var comments: List = emptyList()
+
+ // var ignored: Boolean = false
+ open val dbPropertyName get() = annotations.alias?.property ?: fieldName
+ open lateinit var owner: FieldContainer<*>
+
+ fun isList() = type.isList()
+ fun isRequired() = type.isRequired()
+ open val whereType get() = type.name().asType()
+
+ override fun toString(): String {
+ return "Field: ${getOwnerName()}::$fieldName"
+ }
+
+ fun getOwnerName() = owner.let {
+ when (it) {
+ is Node -> it.name
+ is Interface -> it.name
+ is RelationshipProperties -> it.typeName
+ }
+ }
+
+ val interfaceField
+ get() = (owner as? Node)
+ ?.interfaces
+ ?.mapNotNull { it.getField(fieldName) }
+ ?.firstOrNull()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as BaseField
+
+ return fieldName == other.fieldName
+ }
+
+ override fun hashCode(): Int {
+ return fieldName.hashCode()
+ }
+
+ fun isCustomResolvable() = this.annotations.customResolver != null
+
+ fun isFilterableByValue() = annotations.filterable?.byValue != false
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/ComputedField.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/ComputedField.kt
new file mode 100644
index 00000000..ea1aec99
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/ComputedField.kt
@@ -0,0 +1,15 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Type
+import org.neo4j.graphql.domain.directives.FieldAnnotations
+
+/**
+ * Representation of the `@customResolver` directive and its meta.
+ */
+class ComputedField(
+ fieldName: String,
+ type: Type<*>,
+ annotations: FieldAnnotations,
+) : BaseField(
+ fieldName, type, annotations
+)
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/ConnectionField.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/ConnectionField.kt
new file mode 100644
index 00000000..afb7292d
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/ConnectionField.kt
@@ -0,0 +1,17 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Type
+import org.neo4j.graphql.domain.RelationshipProperties
+import org.neo4j.graphql.domain.directives.FieldAnnotations
+
+class ConnectionField(
+ fieldName: String,
+ type: Type<*>,
+ annotations: FieldAnnotations,
+ val relationshipField: RelationField
+) : BaseField(fieldName, type, annotations) {
+
+ val properties: RelationshipProperties? get() = (relationshipField as? RelationField)?.properties
+
+ override val dbPropertyName get() = relationshipField.dbPropertyName
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/CustomEnumField.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/CustomEnumField.kt
new file mode 100644
index 00000000..5d921c9e
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/CustomEnumField.kt
@@ -0,0 +1,21 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Type
+import graphql.language.Value
+import org.neo4j.graphql.SchemaConfig
+import org.neo4j.graphql.domain.directives.FieldAnnotations
+
+class CustomEnumField(
+ fieldName: String,
+ type: Type<*>,
+ annotations: FieldAnnotations,
+ schemaConfig: SchemaConfig,
+) : ScalarField(
+ fieldName,
+ type,
+ annotations,
+ schemaConfig,
+), HasDefaultValue, HasCoalesceValue {
+ override val defaultValue: Value<*>? get() = annotations.default?.value
+ override val coalesceValue: Value<*>? get() = annotations.coalesce?.value
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/CustomScalarField.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/CustomScalarField.kt
new file mode 100644
index 00000000..93dbac00
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/CustomScalarField.kt
@@ -0,0 +1,17 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Type
+import org.neo4j.graphql.SchemaConfig
+import org.neo4j.graphql.domain.directives.FieldAnnotations
+
+class CustomScalarField(
+ fieldName: String,
+ type: Type<*>,
+ annotations: FieldAnnotations,
+ schemaConfig: SchemaConfig,
+) : ScalarField(
+ fieldName,
+ type,
+ annotations,
+ schemaConfig,
+)
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/HasCoalesceValue.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/HasCoalesceValue.kt
new file mode 100644
index 00000000..4dca3db7
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/HasCoalesceValue.kt
@@ -0,0 +1,7 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Value
+
+interface HasCoalesceValue {
+ val coalesceValue: Value<*>?
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/HasDefaultValue.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/HasDefaultValue.kt
new file mode 100644
index 00000000..23623356
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/HasDefaultValue.kt
@@ -0,0 +1,7 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Value
+
+interface HasDefaultValue {
+ val defaultValue: Value<*>?
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/InterfaceField.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/InterfaceField.kt
new file mode 100644
index 00000000..2f24c270
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/InterfaceField.kt
@@ -0,0 +1,15 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Type
+import org.neo4j.graphql.domain.directives.FieldAnnotations
+
+class InterfaceField(
+ fieldName: String,
+ type: Type<*>,
+ annotations: FieldAnnotations,
+ val implementations: List,
+) : BaseField(
+ fieldName,
+ type,
+ annotations
+)
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/ObjectField.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/ObjectField.kt
new file mode 100644
index 00000000..e4cd26a8
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/ObjectField.kt
@@ -0,0 +1,14 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Type
+import org.neo4j.graphql.domain.directives.FieldAnnotations
+
+class ObjectField(
+ fieldName: String,
+ type: Type<*>,
+ annotations: FieldAnnotations,
+) : BaseField(
+ fieldName,
+ type,
+ annotations
+)
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
new file mode 100644
index 00000000..3f134e88
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/PointField.kt
@@ -0,0 +1,112 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Type
+import graphql.language.TypeName
+import org.neo4j.cypherdsl.core.Cypher
+import org.neo4j.cypherdsl.core.Expression
+import org.neo4j.graphql.*
+import org.neo4j.graphql.Constants.CARTESIAN_POINT_INPUT_TYPE
+import org.neo4j.graphql.Constants.POINT_INPUT_TYPE
+import org.neo4j.graphql.domain.directives.FieldAnnotations
+import org.neo4j.graphql.domain.predicates.FieldOperator
+import org.neo4j.graphql.domain.predicates.definitions.ScalarPredicateDefinition
+import org.neo4j.graphql.schema.model.outputs.point.BasePointSelection
+import org.neo4j.graphql.schema.model.outputs.point.CartesianPointSelection
+import org.neo4j.graphql.schema.model.outputs.point.PointSelection
+import org.neo4j.graphql.utils.IResolveTree
+
+class PointField(
+ fieldName: String,
+ type: Type<*>,
+ private val coordinateType: CoordinateType,
+ annotations: FieldAnnotations,
+ schemaConfig: SchemaConfig
+) : ScalarField(
+ fieldName,
+ type,
+ annotations,
+ schemaConfig,
+) {
+
+ override val predicateDefinitions: Map = initPredicates()
+ override val whereType
+ get() = when (coordinateType) {
+ CoordinateType.GEOGRAPHIC -> TypeName(POINT_INPUT_TYPE)
+ CoordinateType.CARTESIAN -> TypeName(CARTESIAN_POINT_INPUT_TYPE)
+ }
+
+ private fun initPredicates(): Map {
+ val result = mutableMapOf()
+ .add(FieldOperator.IMPLICIT_EQUAL, deprecated = "Please use the explicit _EQ version")
+ .add(FieldOperator.EQUAL)
+ if (isList()) {
+ result.addIncludesResolver(FieldOperator.INCLUDES)
+ } else {
+ result
+ .addDistanceResolver(FieldOperator.LT)
+ .addDistanceResolver(FieldOperator.LTE)
+ .addDistanceResolver(FieldOperator.GT)
+ .addDistanceResolver(FieldOperator.GTE)
+ .addDistanceResolver(FieldOperator.EQUAL, "DISTANCE")
+ .addInResolver(FieldOperator.IN)
+ }
+
+ return result
+ }
+
+ private fun MutableMap.addDistanceResolver(
+ op: FieldOperator,
+ suffix: String = op.suffix
+ ): MutableMap {
+ return this.add(
+ suffix,
+ { property, parameter ->
+ op.conditionCreator(
+ Cypher.distance(property, Cypher.point(parameter.property("point"))),
+ parameter.property("distance")
+ )
+ },
+ type = coordinateType.inputType
+ )
+ }
+
+ private fun MutableMap.addInResolver(op: FieldOperator): MutableMap {
+ return this.add(
+ op.suffix,
+ { property, parameter ->
+ val p = Cypher.name("p")
+ val paramPointArray = Cypher.listWith(p).`in`(parameter).returning(Cypher.point(p))
+ op.conditionCreator(property, paramPointArray)
+ },
+ type = whereType.makeRequired(type.isRequired()).List
+ )
+ }
+
+ private fun MutableMap.addIncludesResolver(op: FieldOperator): MutableMap {
+ return this.add(
+ op.suffix,
+ { property, parameter ->
+ val paramPoint = Cypher.point(parameter)
+ op.conditionCreator(property, paramPoint)
+ },
+ type = whereType
+ )
+ }
+
+ override fun convertInputToCypher(input: Expression): Expression = if (isList()) {
+ val point = Cypher.name("p")
+ Cypher.listWith(point).`in`(input).returning(Cypher.point(point))
+ } else {
+ Cypher.point(input)
+ }
+
+ fun parseSelection(rt: IResolveTree) = coordinateType.selectionFactory(rt)
+
+ enum class CoordinateType(
+ internal val inputType: TypeName,
+ internal val selectionFactory: (IResolveTree) -> BasePointSelection<*>
+ ) {
+ GEOGRAPHIC(Constants.Types.PointDistance, PointSelection::parse),
+ CARTESIAN(Constants.Types.CartesianPointDistance, CartesianPointSelection::parse)
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/PrimitiveField.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/PrimitiveField.kt
new file mode 100644
index 00000000..0c8ae705
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/PrimitiveField.kt
@@ -0,0 +1,26 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Type
+import graphql.language.Value
+import org.neo4j.graphql.SchemaConfig
+import org.neo4j.graphql.domain.directives.FieldAnnotations
+
+/**
+ * Representation of any field that does not have
+ * a cypher directive or relationship directive
+ * String, Int, Float, ID, Boolean... (custom scalars).
+ */
+open class PrimitiveField(
+ fieldName: String,
+ type: Type<*>,
+ annotations: FieldAnnotations,
+ schemaConfig: SchemaConfig,
+) : ScalarField(
+ fieldName,
+ type,
+ annotations,
+ schemaConfig,
+), HasDefaultValue, HasCoalesceValue {
+ override val defaultValue: Value<*>? get() = annotations.default?.value
+ override val coalesceValue: Value<*>? get() = annotations.coalesce?.value
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/RelationBaseField.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/RelationBaseField.kt
new file mode 100644
index 00000000..fef8f081
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/RelationBaseField.kt
@@ -0,0 +1,72 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Type
+import org.neo4j.graphql.domain.*
+import org.neo4j.graphql.domain.directives.FieldAnnotations
+import org.neo4j.graphql.domain.naming.RelationshipBaseNames
+import org.neo4j.graphql.domain.predicates.RelationOperator
+import org.neo4j.graphql.domain.predicates.definitions.RelationPredicateDefinition
+
+sealed class RelationBaseField(
+ fieldName: String,
+ type: Type<*>,
+ annotations: FieldAnnotations,
+) : BaseField(
+ fieldName,
+ type,
+ annotations
+), NodeResolver {
+
+ lateinit var connectionField: ConnectionField
+
+ lateinit var target: Entity
+
+ abstract val namings: RelationshipBaseNames<*>
+ val isInterface: Boolean get() = target is Interface
+ val isUnion: Boolean get() = target is Union
+
+ val implementingType get() = target as? ImplementingType
+ val node get() = target as? Node
+ val union get() = target as? Union
+ val interfaze get() = target as? Interface
+
+ open val properties: RelationshipProperties? = null
+
+ override fun getRequiredNode(name: String) = getNode(name)
+ ?: throw IllegalArgumentException("unknown implementation $name for ${this.getOwnerName()}.$fieldName")
+
+ override fun getNode(name: String) = extractOnTarget(
+ onNode = { it.takeIf { it.name == name } },
+ onInterface = { it.getRequiredNode(name) },
+ onUnion = { it.getRequiredNode(name) }
+ )
+
+ fun extractOnTarget(
+ onNode: (Node) -> NODE_RESULT,
+ onInterface: (Interface) -> INTERFACE_RESULT,
+ onUnion: (Union) -> UNION_RESULT,
+ ): RESULT = target.extractOnTarget(onNode, onInterface, onUnion)
+
+ fun extractOnTarget(
+ onImplementingType: (ImplementingType) -> IMPLEMENTATION_TYPE_RESULT,
+ onUnion: (Union) -> UNION_RESULT,
+ ): RESULT = extractOnTarget(
+ onNode = { onImplementingType(it) },
+ onInterface = { onImplementingType(it) },
+ onUnion
+ )
+
+ val predicateDefinitions: Map by lazy {
+ val result = mutableMapOf()
+ listOf(true, false).forEach { isConnection ->
+ RelationOperator.entries.forEach { op ->
+ if (op.list == this.isList()) {
+ val name = (this.fieldName.takeIf { !isConnection } ?: connectionField.fieldName) +
+ (op.suffix?.let { "_$it" } ?: "")
+ result[name] = RelationPredicateDefinition(name, this, op, isConnection)
+ }
+ }
+ }
+ result
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/RelationField.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/RelationField.kt
new file mode 100644
index 00000000..19cf030a
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/RelationField.kt
@@ -0,0 +1,62 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Type
+import org.neo4j.cypherdsl.core.Node
+import org.neo4j.cypherdsl.core.Relationship
+import org.neo4j.graphql.domain.RelationshipProperties
+import org.neo4j.graphql.domain.directives.FieldAnnotations
+import org.neo4j.graphql.domain.naming.RelationshipNames
+
+/**
+ * Representation of the `@relationship` directive and its meta.
+ */
+class RelationField(
+ fieldName: String,
+ type: Type<*>,
+ annotations: FieldAnnotations,
+ override val properties: RelationshipProperties?,
+) : RelationBaseField(
+ fieldName,
+ type,
+ annotations,
+) {
+
+ override val namings = RelationshipNames(this)
+
+ init {
+ properties?.addUsedByRelation(this)
+ }
+
+ val relationship get() = requireNotNull(annotations.relationship)
+
+ /**
+ * The type of the neo4j relation
+ */
+ val relationType get() = relationship.type
+ val direction get() = relationship.direction
+ val queryDirection get() = relationship.queryDirection
+
+ enum class Direction {
+ IN, OUT
+ }
+
+ enum class QueryDirection {
+ DIRECTED,
+ UNDIRECTED,
+ }
+
+ fun createQueryDslRelation(
+ start: Node,
+ end: Node,
+ ): Relationship {
+ return when (queryDirection) {
+ QueryDirection.DIRECTED -> when (direction) {
+ Direction.IN -> end.relationshipTo(start, relationType)
+ Direction.OUT -> start.relationshipTo(end, relationType)
+ }
+
+ QueryDirection.UNDIRECTED -> start.relationshipBetween(end, relationType)
+ }
+ }
+
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/ScalarField.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/ScalarField.kt
new file mode 100644
index 00000000..30894a9b
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/ScalarField.kt
@@ -0,0 +1,142 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Type
+import org.neo4j.cypherdsl.core.Condition
+import org.neo4j.cypherdsl.core.Cypher
+import org.neo4j.cypherdsl.core.Expression
+import org.neo4j.cypherdsl.core.Parameter
+import org.neo4j.graphql.*
+import org.neo4j.graphql.domain.directives.FieldAnnotations
+import org.neo4j.graphql.domain.predicates.FieldOperator
+import org.neo4j.graphql.domain.predicates.definitions.ScalarPredicateDefinition
+
+abstract class ScalarField(
+ fieldName: String,
+ type: Type<*>,
+ annotations: FieldAnnotations,
+ schemaConfig: SchemaConfig
+) :
+ BaseField(fieldName, type, annotations) {
+
+ open val predicateDefinitions: Map by lazy { initPredicates(schemaConfig) }
+
+ private fun initPredicates(schemaConfig: SchemaConfig): Map {
+ val fieldType = type.name()
+ val resolver: ((comparisonResolver: (Expression, Expression) -> Condition) -> (Expression, Expression) -> Condition)? =
+ if (fieldType == Constants.DURATION) {
+ { comparisonResolver ->
+ { property, param ->
+ comparisonResolver(
+ Cypher.datetime().add(property),
+ Cypher.datetime().add(param)
+ )
+ }
+ }
+ } else {
+ null
+ }
+
+ val result = mutableMapOf()
+ .add(FieldOperator.IMPLICIT_EQUAL, resolver, deprecated = "Please use the explicit _EQ version")
+ .add(FieldOperator.EQUAL, resolver)
+ if (fieldType == Constants.BOOLEAN) {
+ return result
+ }
+ if (isList()) {
+ result
+ .add(FieldOperator.INCLUDES, resolver, type.inner())
+ return result
+ }
+ result
+ .add(FieldOperator.IN, resolver, type.inner().makeRequired(isRequired()).List)
+ if (STRING_LIKE_TYPES.contains(fieldType)) {
+ result
+ .add(FieldOperator.CONTAINS)
+ .add(FieldOperator.STARTS_WITH)
+ .add(FieldOperator.ENDS_WITH)
+
+ if ((fieldType == Constants.STRING && schemaConfig.features.filters.string.matches)
+ || (fieldType == Constants.ID && schemaConfig.features.filters.id.matches)
+ ) {
+ result.add(FieldOperator.MATCHES)
+ }
+ }
+ if (COMPARABLE_TYPES.contains(fieldType)) {
+ val isString = fieldType == Constants.STRING
+ if (!isString || schemaConfig.features.filters.string.lt) result.add(FieldOperator.LT, resolver)
+ if (!isString || schemaConfig.features.filters.string.lte) result.add(FieldOperator.LTE, resolver)
+ if (!isString || schemaConfig.features.filters.string.gt) result.add(FieldOperator.GT, resolver)
+ if (!isString || schemaConfig.features.filters.string.gte) result.add(FieldOperator.GTE, resolver)
+ }
+ return result
+ }
+
+ protected fun MutableMap.add(
+ op: FieldOperator,
+ delegate: ((comparisonResolver: (Expression, Expression) -> Condition) -> (Expression, Expression) -> Condition)? = null,
+ type: Type<*>? = null,
+ deprecated: String? = null,
+ ): MutableMap {
+ val comparisonResolver: (Expression, Expression) -> Condition = { lhs, rhs ->
+ val rhs2 = when (rhs) {
+ is Parameter<*> -> convertInputToCypher(rhs)
+ else -> rhs
+ }
+ (delegate?.invoke(op.conditionCreator) ?: op.conditionCreator)(lhs, rhs2)
+ }
+
+ return this.add(op.suffix, comparisonResolver, type, deprecated)
+ }
+
+ protected fun MutableMap.add(
+ op: String,
+ comparisonResolver: (Expression, Expression) -> Condition,
+ type: Type<*>? = null, // TODO set correct type
+ deprecated: String? = null,
+ ): MutableMap {
+ val name = this@ScalarField.fieldName + (if (op.isNotBlank()) "_$op" else "")
+ this[name] = ScalarPredicateDefinition(
+ name,
+ this@ScalarField,
+ comparisonResolver,
+ type ?: when {
+ isList() -> whereType
+ .let { if (this@ScalarField.type.isListElementRequired()) it.NonNull else it }
+ .List
+
+ else -> whereType
+
+ },
+ deprecated
+ )
+ return this
+ }
+
+ companion object {
+ private val COMPARABLE_TYPES = setOf(
+ Constants.FLOAT,
+ Constants.INT,
+ Constants.STRING,
+ Constants.BIG_INT,
+ Constants.DATE_TIME,
+ Constants.DATE,
+ Constants.LOCAL_DATE_TIME,
+ Constants.TIME,
+ Constants.LOCAL_TIME,
+ Constants.DURATION,
+ )
+
+ private val STRING_LIKE_TYPES = setOf(Constants.ID, Constants.STRING)
+ }
+
+ open fun convertInputToCypher(input: Expression): Expression = when (type.name()) {
+ Constants.DURATION ->
+ if (Constants.JS_COMPATIBILITY) {
+ input
+ } else {
+ Cypher.duration(input)
+ }
+
+ else -> input
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/TemporalField.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/TemporalField.kt
new file mode 100644
index 00000000..7d5a589a
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/TemporalField.kt
@@ -0,0 +1,27 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Type
+import org.neo4j.cypherdsl.core.Cypher
+import org.neo4j.cypherdsl.core.Expression
+import org.neo4j.graphql.Constants
+import org.neo4j.graphql.SchemaConfig
+import org.neo4j.graphql.domain.directives.FieldAnnotations
+import org.neo4j.graphql.name
+
+class TemporalField(fieldName: String, type: Type<*>, annotations: FieldAnnotations, schemaConfig: SchemaConfig) :
+ PrimitiveField(fieldName, type, annotations, schemaConfig) {
+
+ override fun convertInputToCypher(input: Expression): Expression {
+ if (Constants.JS_COMPATIBILITY) {
+ return super.convertInputToCypher(input)
+ }
+ return when (type.name()) {
+ Constants.DATE_TIME -> Cypher.datetime(input)
+ Constants.LOCAL_DATE_TIME -> Cypher.localdatetime(input)
+ Constants.LOCAL_TIME -> Cypher.localtime(input)
+ Constants.DATE -> Cypher.date(input)
+ Constants.TIME -> Cypher.time(input)
+ else -> super.convertInputToCypher(input)
+ }
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/fields/UnionField.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/UnionField.kt
new file mode 100644
index 00000000..a317d077
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/fields/UnionField.kt
@@ -0,0 +1,15 @@
+package org.neo4j.graphql.domain.fields
+
+import graphql.language.Type
+import org.neo4j.graphql.domain.directives.FieldAnnotations
+
+class UnionField(
+ fieldName: String,
+ type: Type<*>,
+ annotations: FieldAnnotations,
+ val nodes: List,
+) : BaseField(
+ fieldName,
+ type,
+ annotations
+)
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/naming/BaseNames.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/BaseNames.kt
new file mode 100644
index 00000000..dd8251ee
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/BaseNames.kt
@@ -0,0 +1,17 @@
+package org.neo4j.graphql.domain.naming
+
+import org.neo4j.graphql.utils.CamelCaseUtils
+
+sealed class BaseNames(
+ val name: String,
+) {
+ protected val singular = leadingUnderscores(name) + CamelCaseUtils.camelCase(name)
+
+ open val whereInputTypeName get() = "${name}Where"
+
+ companion object {
+ protected fun leadingUnderscores(name: String): String {
+ return Regex("^(_+).+").matchEntire(name)?.groupValues?.get(1) ?: ""
+ }
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/naming/EntityNames.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/EntityNames.kt
new file mode 100644
index 00000000..5847f164
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/EntityNames.kt
@@ -0,0 +1,29 @@
+package org.neo4j.graphql.domain.naming
+
+import org.atteo.evo.inflector.EnglischInflector
+import org.neo4j.graphql.capitalize
+import org.neo4j.graphql.domain.Entity
+import org.neo4j.graphql.domain.directives.EntityAnnotations
+import org.neo4j.graphql.utils.CamelCaseUtils
+
+sealed class EntityNames(
+ name: String,
+ annotations: EntityAnnotations
+) : BaseNames(name) {
+
+ val plural = annotations.plural?.value
+ ?.let { Entity.leadingUnderscores(it) + CamelCaseUtils.camelCase(it) }
+ ?: EnglischInflector.getPlural(singular)
+ val pluralKeepCase = EnglischInflector.getPlural(name)
+
+ protected val pascalCaseSingular = singular.capitalize()
+
+ val pascalCasePlural = plural.capitalize()
+
+ open val rootTypeFieldNames get() = RootTypeFieldNames()
+
+ open inner class RootTypeFieldNames {
+ val read get() = plural
+ }
+
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/naming/ImplementingTypeNames.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/ImplementingTypeNames.kt
new file mode 100644
index 00000000..2482a737
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/ImplementingTypeNames.kt
@@ -0,0 +1,24 @@
+package org.neo4j.graphql.domain.naming
+
+import org.neo4j.graphql.domain.directives.ImplementingTypeAnnotations
+
+sealed class ImplementingTypeNames(
+ name: String,
+ annotations: ImplementingTypeAnnotations
+) : EntityNames(name, annotations) {
+
+ val connectOrCreateWhereInputTypeName get() = "${name}ConnectOrCreateWhere"
+ val sortInputTypeName get() = "${name}Sort"
+ override val rootTypeFieldNames get() = ImplementingTypeRootTypeFieldNames()
+ val rootTypeSelection get() = ImplementingTypeRootTypeSelection()
+
+ open inner class ImplementingTypeRootTypeFieldNames : RootTypeFieldNames() {
+ val connection get() = "${plural}Connection"
+ }
+
+ open inner class ImplementingTypeRootTypeSelection {
+ val connection get() = "${pascalCasePlural}Connection"
+ val edge get() = "${name}Edge"
+ }
+
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/naming/InterfaceNames.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/InterfaceNames.kt
new file mode 100644
index 00000000..e86dc0e8
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/InterfaceNames.kt
@@ -0,0 +1,11 @@
+package org.neo4j.graphql.domain.naming
+
+import org.neo4j.graphql.domain.directives.InterfaceAnnotations
+
+class InterfaceNames(
+ name: String,
+ annotations: InterfaceAnnotations
+) : ImplementingTypeNames(name, annotations) {
+
+ val implementationEnumTypename get() = "${name}Implementation"
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/naming/NodeNames.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/NodeNames.kt
new file mode 100644
index 00000000..88413e92
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/NodeNames.kt
@@ -0,0 +1,8 @@
+package org.neo4j.graphql.domain.naming
+
+import org.neo4j.graphql.domain.directives.NodeAnnotations
+
+class NodeNames(
+ name: String,
+ annotations: NodeAnnotations
+) : ImplementingTypeNames(name, annotations)
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
new file mode 100644
index 00000000..7f66e3cc
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/RelationshipBaseNames.kt
@@ -0,0 +1,43 @@
+package org.neo4j.graphql.domain.naming
+
+import org.neo4j.graphql.capitalize
+import org.neo4j.graphql.domain.ImplementingType
+import org.neo4j.graphql.domain.Union
+import org.neo4j.graphql.domain.fields.RelationBaseField
+
+sealed class RelationshipBaseNames(
+ val relationship: T,
+) : BaseNames(relationship.fieldName) {
+
+
+ protected abstract val edgePrefix: String
+ protected abstract val fieldInputPrefixForTypename: String
+ private val prefixForTypenameWithInheritance: String
+ get() {
+ val prefix = relationship.getOwnerName()
+ return prefix + relationship.fieldName.capitalize()
+ }
+
+ protected val prefixForTypename get() = "${relationship.getOwnerName()}${relationship.fieldName.capitalize()}"
+
+ val connectionFieldTypename get() = "${prefixForTypenameWithInheritance}Connection"
+
+ val connectionSortInputTypename get() = "${connectionFieldTypename}Sort"
+
+ val connectionWhereInputTypename get() = "${connectionFieldTypename}Where"
+
+ val relationshipFieldTypename get() = "${prefixForTypenameWithInheritance}Relationship"
+
+ val connectionFieldName get() = "${relationship.fieldName}Connection"
+
+ fun getConnectionWhereTypename(target: ImplementingType) =
+ "$prefixForTypenameWithInheritance${target.useNameIfFieldIsUnion()}ConnectionWhere"
+
+ val unionConnectionUnionWhereTypeName get() = "${prefixForTypenameWithInheritance.capitalize()}ConnectionWhere"
+
+ override val whereInputTypeName get() = "${edgePrefix}Where"
+
+ val sortInputTypeName get() = "${edgePrefix}Sort"
+
+ protected fun ImplementingType.useNameIfFieldIsUnion() = name.takeIf { relationship.target is Union } ?: ""
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/naming/RelationshipNames.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/RelationshipNames.kt
new file mode 100644
index 00000000..032224ac
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/RelationshipNames.kt
@@ -0,0 +1,22 @@
+package org.neo4j.graphql.domain.naming
+
+import org.neo4j.graphql.capitalize
+import org.neo4j.graphql.domain.Interface
+import org.neo4j.graphql.domain.fields.RelationField
+
+class RelationshipNames(
+ relationship: RelationField,
+) : RelationshipBaseNames(relationship) {
+
+
+ override val edgePrefix
+ get() = relationship.properties?.typeName ?: "__ERROR__" // TODO find better way to handle this
+
+ override val fieldInputPrefixForTypename: String
+ get() {
+ val prefix = (relationship.getOwnerName().takeIf { relationship.target is Interface }
+ ?: relationship.getOwnerName())
+ return prefix + relationship.fieldName.capitalize()
+ }
+
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/naming/UnionNames.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/UnionNames.kt
new file mode 100644
index 00000000..15a5084b
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/naming/UnionNames.kt
@@ -0,0 +1,8 @@
+package org.neo4j.graphql.domain.naming
+
+import org.neo4j.graphql.domain.directives.UnionAnnotations
+
+class UnionNames(
+ name: String,
+ annotations: UnionAnnotations
+) : EntityNames(name, annotations)
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/ConnectionFieldPredicate.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/ConnectionFieldPredicate.kt
new file mode 100644
index 00000000..eb38ec48
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/ConnectionFieldPredicate.kt
@@ -0,0 +1,11 @@
+package org.neo4j.graphql.domain.predicates
+
+import org.neo4j.graphql.domain.predicates.definitions.RelationPredicateDefinition
+import org.neo4j.graphql.schema.model.inputs.connection.ConnectionWhere
+
+class ConnectionFieldPredicate(
+ val def: RelationPredicateDefinition,
+ val where: ConnectionWhere?,
+) : Predicate(def.name) {
+ val field get() = def.field.connectionField
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/ConnectionPredicate.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/ConnectionPredicate.kt
new file mode 100644
index 00000000..ecc1f4e8
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/ConnectionPredicate.kt
@@ -0,0 +1,35 @@
+package org.neo4j.graphql.domain.predicates
+
+import org.neo4j.cypherdsl.core.Condition
+import org.neo4j.graphql.Constants
+import org.neo4j.graphql.schema.model.inputs.WhereInput
+
+/**
+ * Predicates on a nodes' or relations' property
+ */
+class ConnectionPredicate(
+ val key: String,
+ val target: Target,
+ val op: ConnectionOperator,
+ val value: WhereInput,
+) {
+
+
+ enum class Target(val targetName: String) {
+ NODE(Constants.NODE_FIELD),
+ EDGE(Constants.EDGE_FIELD),
+ }
+
+ enum class ConnectionOperator(
+ val suffix: String,
+ val conditionCreator: (Condition) -> Condition,
+ ) {
+ EQUAL("", { it }),
+ NOT("_NOT", { it.not() });
+ }
+
+ companion object {
+ fun getTargetOperationCombinations() = Target.entries
+ .flatMap { target -> ConnectionOperator.entries.map { target to it } }
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/ExpressionPredicate.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/ExpressionPredicate.kt
new file mode 100644
index 00000000..f5d53bc1
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/ExpressionPredicate.kt
@@ -0,0 +1,13 @@
+package org.neo4j.graphql.domain.predicates
+
+import org.neo4j.cypherdsl.core.Condition
+import org.neo4j.cypherdsl.core.Expression
+
+open class ExpressionPredicate(
+ val name: String,
+ private val comparisonResolver: (Expression, Expression) -> Condition,
+ val value: Any?,
+) : Predicate(name) {
+ fun createCondition(lhs: Expression, rhs: Expression): Condition = comparisonResolver(lhs, rhs)
+
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/FieldOperator.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/FieldOperator.kt
new file mode 100644
index 00000000..7d6ad3f3
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/FieldOperator.kt
@@ -0,0 +1,64 @@
+package org.neo4j.graphql.domain.predicates
+
+import org.neo4j.cypherdsl.core.Condition
+import org.neo4j.cypherdsl.core.Cypher
+import org.neo4j.cypherdsl.core.Expression
+
+enum class FieldOperator(
+ val suffix: String,
+ val conditionCreator: (Expression, Expression) -> Condition,
+ val conditionEvaluator: (Any?, Any?) -> Boolean,
+) {
+ IMPLICIT_EQUAL("",
+ { lhs, rhs -> if (rhs == Cypher.literalNull()) lhs.isNull else lhs.eq(rhs) },
+ { lhs, rhs -> lhs == rhs }
+ ),
+ EQUAL("EQ",
+ { lhs, rhs -> if (rhs == Cypher.literalNull()) lhs.isNull else lhs.eq(rhs) },
+ { lhs, rhs -> lhs == rhs }
+ ),
+ LT("LT",
+ Expression::lt,
+ { lhs, rhs -> if (lhs is Number && rhs is Number) lhs.toDouble() < rhs.toDouble() else TODO() }),
+ LTE("LTE",
+ Expression::lte,
+ { lhs, rhs -> if (lhs is Number && rhs is Number) lhs.toDouble() <= rhs.toDouble() else TODO() }),
+ GT(
+ "GT",
+ Expression::gt,
+ { lhs, rhs -> if (lhs is Number && rhs is Number) lhs.toDouble() > rhs.toDouble() else TODO() },
+ ),
+ GTE(
+ "GTE",
+ Expression::gte,
+ { lhs, rhs -> if (lhs is Number && rhs is Number) lhs.toDouble() >= rhs.toDouble() else TODO() },
+ ),
+
+ MATCHES("MATCHES",
+ Expression::matches,
+ { lhs, rhs -> if (lhs is String && rhs is String) lhs.matches(rhs.toRegex()) else TODO() }
+ ),
+
+ CONTAINS("CONTAINS",
+ Expression::contains,
+ { lhs, rhs -> if (lhs is String && rhs is String) lhs.contains(rhs) else TODO() }
+ ),
+ STARTS_WITH("STARTS_WITH",
+ Expression::startsWith,
+ { lhs, rhs -> if (lhs is String && rhs is String) lhs.startsWith(rhs) else TODO() }
+ ),
+ ENDS_WITH("ENDS_WITH",
+ Expression::endsWith,
+ { lhs, rhs -> if (lhs is String && rhs is String) lhs.endsWith(rhs) else TODO() }
+ ),
+
+ IN("IN",
+ Expression::`in`,
+ { lhs, rhs -> if (lhs is Collection<*>) lhs.contains(rhs) else TODO() }
+ ),
+ INCLUDES(
+ "INCLUDES",
+ { lhs, rhs -> rhs.`in`(lhs) },
+ { lhs, rhs -> if (rhs is Collection<*>) rhs.contains(lhs) else TODO() },
+ ),
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/Predicate.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/Predicate.kt
new file mode 100644
index 00000000..c11aa3fd
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/Predicate.kt
@@ -0,0 +1,3 @@
+package org.neo4j.graphql.domain.predicates
+
+sealed class Predicate(val key: String)
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/RelationFieldPredicate.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/RelationFieldPredicate.kt
new file mode 100644
index 00000000..4b9749a2
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/RelationFieldPredicate.kt
@@ -0,0 +1,11 @@
+package org.neo4j.graphql.domain.predicates
+
+import org.neo4j.graphql.domain.predicates.definitions.RelationPredicateDefinition
+import org.neo4j.graphql.schema.model.inputs.WhereInput
+
+class RelationFieldPredicate(
+ val def: RelationPredicateDefinition,
+ val where: WhereInput?,
+) : Predicate(def.name) {
+ val field get() = def.field
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/RelationOperator.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/RelationOperator.kt
new file mode 100644
index 00000000..64ef7259
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/RelationOperator.kt
@@ -0,0 +1,43 @@
+package org.neo4j.graphql.domain.predicates
+
+import org.neo4j.cypherdsl.core.Condition
+import org.neo4j.cypherdsl.core.Cypher
+import org.neo4j.cypherdsl.core.Relationship
+import org.neo4j.graphql.asCypherLiteral
+
+enum class RelationOperator(
+ val suffix: String?,
+ val list: Boolean,
+ private val wrapInNotIfNeeded: (where: Condition) -> Condition = { it },
+) {
+ ALL("ALL", list = true),
+ NONE("NONE", list = true, wrapInNotIfNeeded = { it.not() }),
+ SINGLE("SINGLE", list = true),
+ SOME("SOME", list = true),
+ EQUAL(null, list = false);
+
+ fun createRelationCondition(
+ relationship: Relationship,
+ nestedCondition: Condition?
+ ): Condition {
+ val inner = nestedCondition ?: Cypher.noCondition()
+ val match = Cypher.match(relationship)
+ val condition = when (this) {
+ ALL ->
+ match.let {
+ it.where(inner).asCondition()
+ // Testing "ALL" requires testing that at least one element exists and that no elements not matching the filter exists
+ .and(it.where(inner.not()).asCondition().not())
+ }
+
+ SINGLE ->
+ Cypher.single(Cypher.name("ignore"))
+ .`in`(Cypher.listBasedOn(relationship).where(inner).returning(1.asCypherLiteral()))
+ .where(Cypher.literalTrue().asCondition())
+
+ else -> match.where(inner).asCondition()
+ }
+ return wrapInNotIfNeeded(condition)
+ }
+
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/ScalarFieldPredicate.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/ScalarFieldPredicate.kt
new file mode 100644
index 00000000..8efe6e75
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/ScalarFieldPredicate.kt
@@ -0,0 +1,11 @@
+package org.neo4j.graphql.domain.predicates
+
+import org.neo4j.graphql.domain.predicates.definitions.ScalarPredicateDefinition
+
+class ScalarFieldPredicate(
+ private val definition: ScalarPredicateDefinition,
+ value: Any?
+) : ExpressionPredicate(definition.name, definition::createCondition, value) {
+
+ val field get() = definition.field
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/definitions/PredicateDefinition.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/definitions/PredicateDefinition.kt
new file mode 100644
index 00000000..88ba0f09
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/definitions/PredicateDefinition.kt
@@ -0,0 +1,5 @@
+package org.neo4j.graphql.domain.predicates.definitions
+
+sealed interface PredicateDefinition {
+ val name: String
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/definitions/RelationPredicateDefinition.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/definitions/RelationPredicateDefinition.kt
new file mode 100644
index 00000000..49aa4808
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/definitions/RelationPredicateDefinition.kt
@@ -0,0 +1,31 @@
+package org.neo4j.graphql.domain.predicates.definitions
+
+import graphql.language.Description
+import org.atteo.evo.inflector.EnglischInflector
+import org.neo4j.graphql.asDescription
+import org.neo4j.graphql.domain.ImplementingType
+import org.neo4j.graphql.domain.fields.RelationBaseField
+import org.neo4j.graphql.domain.predicates.RelationOperator
+
+data class RelationPredicateDefinition(
+ override val name: String,
+ val field: RelationBaseField,
+ val operator: RelationOperator,
+ val connection: Boolean
+) : PredicateDefinition {
+
+ val description: Description?
+ get() = if (operator.list) {
+ val plural = (this.field.owner as? ImplementingType)?.pluralKeepCase ?: this.field.getOwnerName()
+ val relatedName = if (connection) {
+ // TODO really this name?
+ EnglischInflector.getPlural(this.field.namings.connectionFieldTypename)
+ } else this.field.extractOnTarget(
+ onImplementingType = { it.pluralKeepCase },
+ onUnion = { it.pluralKeepCase }
+ )
+ "Return $plural where ${if (operator !== RelationOperator.SINGLE) operator.suffix!!.lowercase() else "one"} of the related $relatedName match this filter".asDescription()
+ } else {
+ null
+ }
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/definitions/ScalarPredicateDefinition.kt b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/definitions/ScalarPredicateDefinition.kt
new file mode 100644
index 00000000..0a4d5597
--- /dev/null
+++ b/core/src/main/kotlin/org/neo4j/graphql/domain/predicates/definitions/ScalarPredicateDefinition.kt
@@ -0,0 +1,16 @@
+package org.neo4j.graphql.domain.predicates.definitions
+
+import graphql.language.Type
+import org.neo4j.cypherdsl.core.Condition
+import org.neo4j.cypherdsl.core.Expression
+import org.neo4j.graphql.domain.fields.ScalarField
+
+data class ScalarPredicateDefinition(
+ override val name: String,
+ val field: ScalarField,
+ private val comparisonResolver: (Expression, Expression) -> Condition,
+ val type: Type<*>,
+ val deprecated: String? = null
+) : PredicateDefinition {
+ fun createCondition(lhs: Expression, rhs: Expression): Condition = comparisonResolver(lhs, rhs)
+}
diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/AugmentFieldHandler.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/AugmentFieldHandler.kt
deleted file mode 100644
index 9e095a09..00000000
--- a/core/src/main/kotlin/org/neo4j/graphql/handler/AugmentFieldHandler.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-package org.neo4j.graphql.handler
-
-import graphql.language.*
-import graphql.schema.DataFetcher
-import graphql.schema.idl.TypeDefinitionRegistry
-import org.neo4j.graphql.*
-import org.neo4j.graphql.handler.projection.ProjectionBase
-
-/**
- * This class augments existing fields on a type and adds filtering and sorting to these fields.
- */
-class AugmentFieldHandler(
- schemaConfig: SchemaConfig,
- typeDefinitionRegistry: TypeDefinitionRegistry,
- neo4jTypeDefinitionRegistry: TypeDefinitionRegistry
-) : AugmentationHandler(schemaConfig, typeDefinitionRegistry, neo4jTypeDefinitionRegistry) {
-
- override fun augmentType(type: ImplementingTypeDefinition<*>) {
- val enhanceRelations = {
- type.fieldDefinitions.map { fieldDef -> fieldDef.transform { augmentRelation(it, fieldDef) } }
- }
-
- val rewritten = when (type) {
- is ObjectTypeDefinition -> type.transform { it.fieldDefinitions(enhanceRelations()) }
- is InterfaceTypeDefinition -> type.transform { it.definitions(enhanceRelations()) }
- else -> return
- }
-
- typeDefinitionRegistry.remove(rewritten.name, rewritten)
- typeDefinitionRegistry.add(rewritten)
- }
-
- private fun augmentRelation(fieldBuilder: FieldDefinition.Builder, field: FieldDefinition) {
- if (!field.isRelationship() || !field.type.isList() || field.isIgnored()) {
- return
- }
-
- val fieldType = field.type.inner().resolve() as? ImplementingTypeDefinition<*> ?: return
-
- if (schemaConfig.queryOptionStyle == SchemaConfig.InputStyle.INPUT_TYPE) {
-
- val optionsTypeName = addOptions(fieldType)
- if (field.inputValueDefinitions.find { it.name == ProjectionBase.OPTIONS } == null) {
- fieldBuilder.inputValueDefinition(input(ProjectionBase.OPTIONS, TypeName(optionsTypeName)))
- }
-
- } else {
-
- if (field.inputValueDefinitions.find { it.name == ProjectionBase.FIRST } == null) {
- fieldBuilder.inputValueDefinition(input(ProjectionBase.FIRST, TypeInt))
- }
- if (field.inputValueDefinitions.find { it.name == ProjectionBase.OFFSET } == null) {
- fieldBuilder.inputValueDefinition(input(ProjectionBase.OFFSET, TypeInt))
- }
- if (field.inputValueDefinitions.find { it.name == ProjectionBase.ORDER_BY } == null) {
- addOrdering(fieldType)?.let { orderingTypeName ->
- val orderType = ListType(NonNullType(TypeName(orderingTypeName)))
- fieldBuilder.inputValueDefinition(input(ProjectionBase.ORDER_BY, orderType))
- }
- }
- if (!schemaConfig.useWhereFilter
- && schemaConfig.query.enabled
- && !schemaConfig.query.exclude.contains(fieldType.name)
- ) {
- // legacy support
- val relevantFields = fieldType
- .getScalarFields()
- .filter { scalarField -> field.inputValueDefinitions.find { it.name == scalarField.name } == null }
- .filter { it.dynamicPrefix() == null } // TODO currently we do not support filtering on dynamic properties
- getInputValueDefinitions(relevantFields, true, { true }).forEach {
- fieldBuilder.inputValueDefinition(it)
- }
- }
- }
-
- val filterFieldName = if (schemaConfig.useWhereFilter) ProjectionBase.WHERE else ProjectionBase.FILTER
- if (schemaConfig.query.enabled && !schemaConfig.query.exclude.contains(fieldType.name) && field.inputValueDefinitions.find { it.name == filterFieldName } == null) {
- val filterTypeName = addFilterType(fieldType)
- fieldBuilder.inputValueDefinition(input(filterFieldName, TypeName(filterTypeName)))
- }
-
- }
-
- override fun createDataFetcher(
- operationType: OperationType,
- fieldDefinition: FieldDefinition
- ): DataFetcher? = null
-}
-
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 b8f423d1..070fe42a 100644
--- a/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt
+++ b/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt
@@ -1,31 +1,30 @@
package org.neo4j.graphql.handler
-import graphql.language.Field
import graphql.language.VariableReference
import graphql.schema.DataFetcher
import graphql.schema.DataFetchingEnvironment
-import graphql.schema.GraphQLFieldDefinition
-import graphql.schema.GraphQLType
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.*
-import org.neo4j.graphql.handler.projection.ProjectionBase
+import org.neo4j.graphql.SchemaConfig
+import org.neo4j.graphql.driver.adapter.Neo4jAdapter
+import org.neo4j.graphql.isList
/**
* This is a base class for the implementation of graphql data fetcher used in this project
*/
-abstract class BaseDataFetcher(schemaConfig: SchemaConfig) : ProjectionBase(schemaConfig), DataFetcher {
-
- private var init = false
-
- override fun get(env: DataFetchingEnvironment): Cypher {
- val field = env.mergedField?.singleField
- ?: throw IllegalAccessException("expect one filed in environment.mergedField")
- val variable = field.aliasOrName().decapitalize()
- prepareDataFetcher(env.fieldDefinition, env.parentType)
- val statement = generateCypher(variable, field, env)
- val dialect = env.queryContext().neo4jDialect
+internal abstract class BaseDataFetcher(protected val schemaConfig: SchemaConfig) :
+ DataFetcher {
+
+ 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
+ }
val query = Renderer.getRenderer(
Configuration
.newConfig()
@@ -38,25 +37,23 @@ abstract class BaseDataFetcher(schemaConfig: SchemaConfig) : ProjectionBase(sche
val params = statement.catalog.parameters.mapValues { (_, value) ->
(value as? VariableReference)?.let { env.variables[it.name] } ?: value
}
- return Cypher(query, params, env.fieldDefinition.type, variable = field.aliasOrName())
- .also {
- (env.getLocalContext() as? Translator.CypherHolder)?.apply { this.cyphers += it }
- }
+
+ val result = neo4jAdapter.executeQuery(query, params)
+ return mapResult(env, result)
}
- /**
- * called after the schema is generated but before the 1st call
- */
- private fun prepareDataFetcher(fieldDefinition: GraphQLFieldDefinition, parentType: GraphQLType) {
- if (init) {
- return
+ protected abstract fun generateCypher(env: DataFetchingEnvironment): Statement
+
+ protected open fun mapResult(env: DataFetchingEnvironment, result: List