From 7ac1c04d5607d816fd0cab610d1c7994eae542a4 Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Tue, 16 Nov 2021 23:55:55 +0100 Subject: [PATCH] Utilize graphql framework to handle fragments correctly (#248) With this change the way we handle projections is changed. We now rely on the graphql framework to parse fragments an arguments. This will simplify handling nested result fields like they are used in the javascript version of this library. --- .../org/neo4j/graphql/DynamicProperties.kt | 2 +- .../org/neo4j/graphql/ExtensionFunctions.kt | 9 +- .../org/neo4j/graphql/GraphQLExtensions.kt | 28 +-- .../kotlin/org/neo4j/graphql/Neo4jTypes.kt | 25 +- .../kotlin/org/neo4j/graphql/NoOpCoercing.kt | 4 +- .../kotlin/org/neo4j/graphql/Predicates.kt | 31 +-- .../kotlin/org/neo4j/graphql/QueryContext.kt | 4 +- .../handler/BaseDataFetcherForContainer.kt | 18 +- .../graphql/handler/CreateTypeHandler.kt | 4 +- .../graphql/handler/CypherDirectiveHandler.kt | 4 +- .../neo4j/graphql/handler/DeleteHandler.kt | 2 +- .../graphql/handler/MergeOrUpdateHandler.kt | 4 +- .../org/neo4j/graphql/handler/QueryHandler.kt | 8 +- .../handler/filter/OptimizedFilterHandler.kt | 40 ++-- .../handler/projection/ProjectionBase.kt | 217 ++++++++---------- .../handler/relation/CreateRelationHandler.kt | 4 +- .../relation/CreateRelationTypeHandler.kt | 6 +- .../handler/relation/DeleteRelationHandler.kt | 2 +- .../org/neo4j/graphql/parser/QueryParser.kt | 68 +++--- .../neo4j/graphql/utils/AsciiDocTestSuite.kt | 2 + .../resources/cypher-directive-tests.adoc | 6 +- core/src/test/resources/filter-tests.adoc | 68 +++--- core/src/test/resources/issues/gh-112.adoc | 4 +- core/src/test/resources/issues/gh-149.adoc | 2 +- core/src/test/resources/issues/gh-47.adoc | 2 +- core/src/test/resources/logback-test.xml | 1 + core/src/test/resources/movie-tests.adoc | 12 +- .../resources/optimized-query-for-filter.adoc | 32 +-- .../tck-test-files/cypher/pagination.adoc | 37 ++- .../resources/tck-test-files/cypher/sort.adoc | 20 +- .../tck-test-files/cypher/types/datetime.adoc | 4 +- .../tck-test-files/cypher/where.adoc | 8 +- .../src/test/resources/translator-tests1.adoc | 58 ++++- 33 files changed, 358 insertions(+), 378 deletions(-) diff --git a/core/src/main/kotlin/org/neo4j/graphql/DynamicProperties.kt b/core/src/main/kotlin/org/neo4j/graphql/DynamicProperties.kt index 07477d7b..befe6e76 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/DynamicProperties.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/DynamicProperties.kt @@ -48,7 +48,7 @@ object DynamicProperties { is BooleanValue -> input.isValue is EnumValue -> input.name is VariableReference -> variables[input.name] - is ArrayValue -> input.values.map { v -> parse(v, variables) } + 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 cc78a31b..16bc570c 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt @@ -6,10 +6,6 @@ import graphql.schema.GraphQLOutputType import org.neo4j.cypherdsl.core.* import java.util.* -fun Iterable.joinNonEmpty(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String { - return if (iterator().hasNext()) joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString() else "" -} - fun queryParameter(value: Any?, vararg parts: String?): Parameter<*> { val name = when (value) { is VariableReference -> value.name @@ -22,7 +18,6 @@ fun Expression.collect(type: GraphQLOutputType) = if (type.isList()) Functions.c fun StatementBuilder.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 normalizeName(vararg parts: String?) = parts.filterNot { it.isNullOrBlank() }.joinToString("_") fun PropertyContainer.id(): FunctionInvocation = when (this) { is Node -> Functions.id(this) @@ -35,3 +30,7 @@ fun String.toCamelCase(): String = Regex("[\\W_]([a-z])").replace(this) { it.gro fun Optional.unwrap(): T? = orElse(null) fun String.asDescription() = Description(this, null, this.contains("\n")) + +fun String.capitalize(): String = replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } +fun String.decapitalize(): String = replaceFirstChar { it.lowercase(Locale.getDefault()) } +fun String.toUpperCase(): String = uppercase(Locale.getDefault()) diff --git a/core/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt b/core/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt index 62d6c502..5712245f 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt @@ -17,8 +17,6 @@ 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 -import java.math.BigDecimal -import java.math.BigInteger fun Type<*>.name(): String? = if (this.inner() is TypeName) (this.inner() as TypeName).name else null fun Type<*>.inner(): Type<*> = when (this) { @@ -64,7 +62,7 @@ fun GraphQLFieldsContainer.relationshipFor(name: String): RelationshipInfo = when (this) { - null -> NullValue.newNullValue().build() - is Value<*> -> this - is Array<*> -> ArrayValue.newArrayValue().values(this.map { it.asGraphQLValue() }).build() - is Iterable<*> -> ArrayValue.newArrayValue().values(this.map { it.asGraphQLValue() }).build() - is Map<*, *> -> ObjectValue.newObjectValue().objectFields(this.map { entry -> ObjectField(entry.key as String, entry.value.asGraphQLValue()) }).build() - is Enum<*> -> EnumValue.newEnumValue().name(this.name).build() - is Int -> IntValue.newIntValue(BigInteger.valueOf(this.toLong())).build() - is Long -> IntValue.newIntValue(BigInteger.valueOf(this)).build() - is Number -> FloatValue.newFloatValue(BigDecimal.valueOf(this as Double)).build() - is Boolean -> BooleanValue.newBooleanValue(this).build() - is String -> StringValue.newStringValue(this).build() - else -> throw IllegalStateException("Cannot convert ${this.javaClass.name} into an graphql type") -} - 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()}") diff --git a/core/src/main/kotlin/org/neo4j/graphql/Neo4jTypes.kt b/core/src/main/kotlin/org/neo4j/graphql/Neo4jTypes.kt index c1377ae2..886199a2 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/Neo4jTypes.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/Neo4jTypes.kt @@ -1,9 +1,7 @@ package org.neo4j.graphql -import graphql.language.Field -import graphql.language.ObjectField -import graphql.language.ObjectValue 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 @@ -19,7 +17,7 @@ data class TypeDefinition( ) class Neo4jTemporalConverter(name: String) : Neo4jSimpleConverter(name) { - override fun projectField(variable: SymbolicName, field: Field, name: String): Any { + override fun projectField(variable: SymbolicName, field: SelectedField, name: String): Any { return Cypher.call("toString").withArgs(variable.property(field.name)).asFunction() } @@ -31,28 +29,27 @@ class Neo4jTemporalConverter(name: String) : Neo4jSimpleConverter(name) { class Neo4jTimeConverter(name: String) : Neo4jConverter(name) { override fun createCondition( - objectField: ObjectField, + fieldName: String, field: GraphQLFieldDefinition, parameter: Parameter<*>, conditionCreator: (Expression, Expression) -> Condition, propertyContainer: PropertyContainer - ): Condition = if (objectField.name == NEO4j_FORMATTED_PROPERTY_KEY) { + ): Condition = if (fieldName == NEO4j_FORMATTED_PROPERTY_KEY) { val exp = toExpression(parameter) conditionCreator(propertyContainer.property(field.name), exp) } else { - super.createCondition(objectField, field, parameter, conditionCreator, propertyContainer) + super.createCondition(fieldName, field, parameter, conditionCreator, propertyContainer) } - override fun projectField(variable: SymbolicName, field: Field, name: String): Any = when (name) { + 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? ObjectValue) - ?.objectFields - ?.find { it.name == NEO4j_FORMATTED_PROPERTY_KEY } + return (value as? Map<*, *>) + ?.get(NEO4j_FORMATTED_PROPERTY_KEY) ?.let { BaseDataFetcherForContainer.PropertyAccessor(fieldName) { variable -> val param = queryParameter(value, variable, fieldName) @@ -91,14 +88,14 @@ open class Neo4jSimpleConverter(val name: String) { ): Condition = conditionCreator(property, parameter) open fun createCondition( - objectField: ObjectField, + fieldName: String, field: GraphQLFieldDefinition, parameter: Parameter<*>, conditionCreator: (Expression, Expression) -> Condition, propertyContainer: PropertyContainer - ): Condition = createCondition(propertyContainer.property(field.name, objectField.name), parameter, conditionCreator) + ): Condition = createCondition(propertyContainer.property(field.name, fieldName), parameter, conditionCreator) - open fun projectField(variable: SymbolicName, field: Field, name: String): Any = variable.property(field.name, name) + 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) diff --git a/core/src/main/kotlin/org/neo4j/graphql/NoOpCoercing.kt b/core/src/main/kotlin/org/neo4j/graphql/NoOpCoercing.kt index 742ce17f..e0bce29a 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/NoOpCoercing.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/NoOpCoercing.kt @@ -3,9 +3,9 @@ package org.neo4j.graphql import graphql.schema.Coercing object NoOpCoercing : Coercing { - override fun parseLiteral(input: Any?) = input + override fun parseLiteral(input: Any?) = input?.toJavaValue() override fun serialize(dataFetcherResult: Any?) = dataFetcherResult - override fun parseValue(input: Any?) = input + override fun parseValue(input: Any) = input.toJavaValue() } diff --git a/core/src/main/kotlin/org/neo4j/graphql/Predicates.kt b/core/src/main/kotlin/org/neo4j/graphql/Predicates.kt index 158c027e..d3dab3b6 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/Predicates.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/Predicates.kt @@ -51,17 +51,17 @@ enum class FieldOperator( queriedField: String, propertyContainer: PropertyContainer, field: GraphQLFieldDefinition?, - value: Any, + 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.toJavaValue()) + .withValue(value) return listOf(neo4jTypeConverter.createCondition(propertyContainer.property(field.name), parameter, conditionCreator)) } - return if (field?.type?.isNeo4jType() == true && value is ObjectValue) { + 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() @@ -79,28 +79,29 @@ enum class FieldOperator( } } - private fun resolveNeo4jTypeConditions(variablePrefix: String, queriedField: String, propertyContainer: PropertyContainer, field: GraphQLFieldDefinition, value: ObjectValue, suffix: String?): List { + 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(value, variablePrefix, queriedField, suffix) + val parameter = queryParameter(values, variablePrefix, queriedField, suffix) conditions += (neo4jTypeConverter as Neo4jPointConverter).createDistanceCondition( propertyContainer.property(field.propertyName()), parameter, conditionCreator ) } else { - value.objectFields.forEachIndexed { index, objectField -> - val parameter = queryParameter(value, variablePrefix, queriedField, if (value.objectFields.size > 1) "And${index + 1}" else null, suffix, objectField.name) - .withValue(objectField.value.toJavaValue()) + 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(objectField, field, parameter, conditionCreator, propertyContainer) + conditions += neo4jTypeConverter.createCondition(fieldName, field, parameter, conditionCreator, propertyContainer) } } return conditions } - private fun resolveCondition(variablePrefix: String, queriedField: String, property: Property, value: Any, suffix: String?): List { + 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) @@ -140,14 +141,14 @@ enum class RelationOperator(val suffix: String) { fun fieldName(fieldName: String) = fieldName + suffix - fun harmonize(type: GraphQLFieldsContainer, field: GraphQLFieldDefinition, value: Value<*>, queryFieldName: String) = when (field.type.isList()) { + fun harmonize(type: GraphQLFieldsContainer, field: GraphQLFieldDefinition, value: Any?, queryFieldName: String) = when (field.type.isList()) { true -> when (this) { NOT -> when (value) { - is NullValue -> NOT + null -> NOT else -> NONE } EQ_OR_NOT_EXISTS -> when (value) { - is NullValue -> EQ_OR_NOT_EXISTS + 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 @@ -169,11 +170,11 @@ enum class RelationOperator(val suffix: String) { NONE } NOT -> when (value) { - is NullValue -> NOT + null -> NOT else -> NONE } EQ_OR_NOT_EXISTS -> when (value) { - is NullValue -> EQ_OR_NOT_EXISTS + null -> EQ_OR_NOT_EXISTS else -> SOME } else -> this diff --git a/core/src/main/kotlin/org/neo4j/graphql/QueryContext.kt b/core/src/main/kotlin/org/neo4j/graphql/QueryContext.kt index 288d2446..5e9110fd 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/QueryContext.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/QueryContext.kt @@ -2,7 +2,7 @@ package org.neo4j.graphql data class QueryContext @JvmOverloads constructor( /** - * if true the __typename will be always returned for interfaces, no matter if it was queried or not + * if true the __typename will always be returned for interfaces, no matter if it was queried or not */ var queryTypeOfInterfaces: Boolean = false, @@ -18,4 +18,4 @@ data class QueryContext @JvmOverloads constructor( */ FILTER_AS_MATCH } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcherForContainer.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcherForContainer.kt index b0a17b53..b4791385 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcherForContainer.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcherForContainer.kt @@ -2,7 +2,6 @@ package org.neo4j.graphql.handler import graphql.language.Argument import graphql.language.ArrayValue -import graphql.language.ObjectValue import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLFieldsContainer import graphql.schema.GraphQLType @@ -42,7 +41,7 @@ abstract class BaseDataFetcherForContainer(schemaConfig: SchemaConfig) : BaseDat } private fun defaultCallback(field: GraphQLFieldDefinition) = - { value: Any -> + { value: Any? -> val propertyName = field.propertyName() listOf(PropertyAccessor(propertyName) { variable -> queryParameter(value, variable, field.name) }) } @@ -55,23 +54,24 @@ abstract class BaseDataFetcherForContainer(schemaConfig: SchemaConfig) : BaseDat private fun dynamicPrefixCallback(field: GraphQLFieldDefinition, dynamicPrefix: String) = { value: Any -> // maps each property of the map to the node - (value as? ObjectValue)?.objectFields?.map { argField -> + (value as? Map<*, *>)?.map { (key, value) -> PropertyAccessor( - "$dynamicPrefix${argField.name}" - ) { variable -> queryParameter(argField.value, variable, "${field.name}${argField.name.capitalize()}") } + "$dynamicPrefix${key}" + ) { variable -> queryParameter(value, variable, "${field.name}${(key as String).capitalize()}") } } } - protected fun properties(variable: String, arguments: List): Array = + protected fun properties(variable: String, arguments: Map): Array = preparePredicateArguments(arguments) .flatMap { listOf(it.propertyName, it.toExpression(variable)) } .toTypedArray() - private fun preparePredicateArguments(arguments: List): List { + private fun preparePredicateArguments(arguments: Map): List { val predicates = arguments - .mapNotNull { argument -> - propertyFields[argument.name]?.invoke(argument.value)?.let { argument.name to it } + .entries + .mapNotNull { (key, value) -> + propertyFields[key]?.invoke(value)?.let { key to it } } .toMap() diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt index f965b609..90bdbbe4 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt @@ -84,8 +84,8 @@ class CreateTypeHandler private constructor(schemaConfig: SchemaConfig) : BaseDa val additionalTypes = (type as? GraphQLObjectType)?.interfaces?.map { it.name } ?: emptyList() val node = org.neo4j.cypherdsl.core.Cypher.node(type.name, *additionalTypes.toTypedArray()).named(variable) - val properties = properties(variable, field.arguments) - val (mapProjection, subQueries) = projectFields(node, field, type, env) + val properties = properties(variable, env.arguments) + val (mapProjection, subQueries) = projectFields(node, type, env) return org.neo4j.cypherdsl.core.Cypher.create(node.withProperties(*properties)) .with(node) diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt index be5d7c2b..223a635f 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt @@ -33,14 +33,14 @@ class CypherDirectiveHandler(schemaConfig: SchemaConfig) : BaseDataFetcher(schem val node = org.neo4j.cypherdsl.core.Cypher.anyNode(variable) val ctxVariable = node.requiredSymbolicName - val nestedQuery = cypherDirective(ctxVariable, fieldDefinition, field, cypherDirective, null, env) + val nestedQuery = cypherDirective(ctxVariable, fieldDefinition, env.arguments, cypherDirective, null) return org.neo4j.cypherdsl.core.Cypher.call(nestedQuery) .let { reading -> if (type == null || cypherDirective.passThrough) { reading.returning(ctxVariable.`as`(field.aliasOrName())) } else { - val (fieldProjection, nestedSubQueries) = projectFields(node, field, type, env) + val (fieldProjection, nestedSubQueries) = projectFields(node, type, env) reading .withSubQueries(nestedSubQueries) .returning(ctxVariable.project(fieldProjection).`as`(field.aliasOrName())) diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt index 4f9be141..5335f216 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt @@ -90,7 +90,7 @@ class DeleteHandler private constructor(schemaConfig: SchemaConfig) : BaseDataFe .where(where) } val deletedElement = propertyContainer.requiredSymbolicName.`as`("toDelete") - val (mapProjection, subQueries) = projectFields(propertyContainer, field, type, env) + val (mapProjection, subQueries) = projectFields(propertyContainer, type, env) val projection = propertyContainer.project(mapProjection).`as`(variable) return select diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/MergeOrUpdateHandler.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/MergeOrUpdateHandler.kt index 90afd687..d2ef33c9 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/MergeOrUpdateHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/MergeOrUpdateHandler.kt @@ -115,8 +115,8 @@ class MergeOrUpdateHandler private constructor(private val merge: Boolean, schem org.neo4j.cypherdsl.core.Cypher.match(node).where(where) } } - val properties = properties(variable, field.arguments) - val (mapProjection, subQueries) = projectFields(propertyContainer, field, type, env) + val properties = properties(variable, env.arguments) + val (mapProjection, subQueries) = projectFields(propertyContainer, type, env) return select .mutate(propertyContainer, org.neo4j.cypherdsl.core.Cypher.mapOf(*properties)) diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt index 7bf3124a..f7e50d4e 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt @@ -113,20 +113,20 @@ class QueryHandler private constructor(schemaConfig: SchemaConfig) : BaseDataFet val ongoingReading = if ((env.getContext() as? QueryContext)?.optimizedQuery?.contains(QueryContext.OptimizationStrategy.FILTER_AS_MATCH) == true) { - OptimizedFilterHandler(type, schemaConfig).generateFilterQuery(variable, fieldDefinition, field, match, propertyContainer, env.variables) + OptimizedFilterHandler(type, schemaConfig).generateFilterQuery(variable, fieldDefinition, env.arguments, match, propertyContainer, env.variables) } else { - val where = where(propertyContainer, fieldDefinition, type, field, env.variables) + val where = where(propertyContainer, fieldDefinition, type, env.arguments, env.variables) match.where(where) } - val (projectionEntries, subQueries) = projectFields(propertyContainer, field, type, env) + val (projectionEntries, subQueries) = projectFields(propertyContainer, type, env) val mapProjection = propertyContainer.project(projectionEntries).`as`(field.aliasOrName()) return ongoingReading .withSubQueries(subQueries) .returning(mapProjection) - .skipLimitOrder(propertyContainer.requiredSymbolicName, fieldDefinition, field, env) + .skipLimitOrder(propertyContainer.requiredSymbolicName, fieldDefinition, env.arguments) .build() } } diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/filter/OptimizedFilterHandler.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/filter/OptimizedFilterHandler.kt index 38037ce2..4d47c6fe 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/filter/OptimizedFilterHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/filter/OptimizedFilterHandler.kt @@ -1,9 +1,5 @@ package org.neo4j.graphql.handler.filter -import graphql.language.Field -import graphql.language.NullValue -import graphql.language.ObjectValue -import graphql.language.Value import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLFieldsContainer import org.neo4j.cypherdsl.core.* @@ -34,7 +30,7 @@ typealias ConditionBuilder = (ExposesWith) -> OrderableOngoingReadingAndWithWith */ class OptimizedFilterHandler(val type: GraphQLFieldsContainer, schemaConfig: SchemaConfig) : ProjectionBase(schemaConfig) { - fun generateFilterQuery(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, readingWithoutWhere: OngoingReadingWithoutWhere, rootNode: PropertyContainer, variables: Map): OngoingReading { + fun generateFilterQuery(variable: String, fieldDefinition: GraphQLFieldDefinition, arguments: Map, readingWithoutWhere: OngoingReadingWithoutWhere, rootNode: PropertyContainer, variables: Map): OngoingReading { if (type.isRelationType()) { throw OptimizedQueryException("Optimization for relationship entity type is not implemented. Please provide a test case to help adding further cases.") } @@ -42,19 +38,20 @@ class OptimizedFilterHandler(val type: GraphQLFieldsContainer, schemaConfig: Sch var ongoingReading: OngoingReading? = null if (!schemaConfig.useWhereFilter) { - val filteredArguments = field.arguments.filterNot { SPECIAL_FIELDS.contains(it.name) } + val filteredArguments = arguments.filterKeys { !SPECIAL_FIELDS.contains(it) } if (filteredArguments.isNotEmpty()) { - val parsedQuery = QueryParser.parseArguments(filteredArguments, fieldDefinition, type, variables) + val parsedQuery = QueryParser.parseArguments(filteredArguments, fieldDefinition, type) val condition = handleQuery(variable, "", rootNode, parsedQuery, type, variables) ongoingReading = readingWithoutWhere.where(condition) } } - return field.arguments.find { filterFieldName() == it.name } - ?.let { argument -> - val parsedQuery = parseFilter(argument.value as ObjectValue, type, variables) + return arguments[filterFieldName()] + ?.let { it as Map<*, *> } + ?.let { + val parsedQuery = parseFilter(it, type) NestingLevelHandler(parsedQuery, false, rootNode, variable, ongoingReading ?: readingWithoutWhere, - type, argument.value, linkedSetOf(rootNode.requiredSymbolicName), variables) + type, it, linkedSetOf(rootNode.requiredSymbolicName), variables) .parseFilter() } ?: readingWithoutWhere @@ -76,7 +73,7 @@ class OptimizedFilterHandler(val type: GraphQLFieldsContainer, schemaConfig: Sch private val variablePrefix: String, private val matchQueryWithoutWhere: OngoingReading, private val type: GraphQLFieldsContainer, - private val value: Value<*>?, + private val value: Map<*, *>, private val parentPassThroughWiths: Collection, private val variables: Map ) { @@ -88,9 +85,6 @@ class OptimizedFilterHandler(val type: GraphQLFieldsContainer, schemaConfig: Sch * @param additionalConditions additional conditions to be applied to the where */ fun parseFilter(additionalConditions: ConditionBuilder? = null): OrderableOngoingReadingAndWithWithoutWhere { - if (value !is ObjectValue) { - throw IllegalArgumentException("Only object values are supported by the OptimizedFilterHandler") - } // WHERE MATCH all predicates for current // WITH x var query = addWhere(additionalConditions) @@ -117,14 +111,14 @@ class OptimizedFilterHandler(val type: GraphQLFieldsContainer, schemaConfig: Sch val matchQueryWithWhere = matchQueryWithoutWhere.where(condition) return if (additionalConditions != null) { - additionalConditions(matchQueryWithWhere ?: matchQueryWithoutWhere) + additionalConditions(matchQueryWithWhere) } else { val withs = if (parsedQuery.relationPredicates.isNotEmpty() && parentPassThroughWiths.none { it == current.requiredSymbolicName }) { parentPassThroughWiths + current.requiredSymbolicName } else { parentPassThroughWiths } - withClauseWithOptionalDistinct(matchQueryWithWhere ?: matchQueryWithoutWhere, withs, useDistinct) + withClauseWithOptionalDistinct(matchQueryWithWhere, withs, useDistinct) } } @@ -135,15 +129,15 @@ class OptimizedFilterHandler(val type: GraphQLFieldsContainer, schemaConfig: Sch val levelPassThroughWiths = parentPassThroughWiths.toCollection(LinkedHashSet()) for ((index, relFilter) in parsedQuery.relationPredicates.withIndex()) { - val objectField = relFilter.queryField + val value = relFilter.value - if (objectField.value is NullValue) { + if (value == null) { // EXISTS + NOT EXISTS val existsCondition = relFilter.createExistsCondition(currentNode()) query = withClauseWithOptionalDistinct(query.where(existsCondition), levelPassThroughWiths) continue } - if (objectField.value !is ObjectValue) { + if (value !is Map<*, *>) { throw IllegalArgumentException("Only object values are supported by the OptimizedFilterHandler") } @@ -164,8 +158,8 @@ class OptimizedFilterHandler(val type: GraphQLFieldsContainer, schemaConfig: Sch relFilter: RelationPredicate, levelPassThroughWiths: LinkedHashSet ): OrderableOngoingReadingAndWithWithoutWhere { - val objectField = relFilter.queryField - val nestedParsedQuery = parseFilter(objectField.value as ObjectValue, relFilter.fieldDefinition.type.getInnerFieldsContainer(), variables) + val objectField = relFilter.value + val nestedParsedQuery = parseFilter(objectField as Map<*, *>, relFilter.fieldDefinition.type.getInnerFieldsContainer()) val hasPredicates = nestedParsedQuery.fieldPredicates.isNotEmpty() || nestedParsedQuery.relationPredicates.isNotEmpty() var queryWithoutWhere = query @@ -186,7 +180,7 @@ class OptimizedFilterHandler(val type: GraphQLFieldsContainer, schemaConfig: Sch } val nestingLevelHandler = NestingLevelHandler(nestedParsedQuery, true, relVariable, relVariableName, - readingWithoutWhere, relFilter.relationshipInfo.type, objectField.value, levelPassThroughWiths, variables) + readingWithoutWhere, relFilter.relationshipInfo.type, objectField, levelPassThroughWiths, variables) when (relFilter.op) { RelationOperator.SOME -> queryWithoutWhere = nestingLevelHandler.parseFilter() diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt index 1e8daffb..cd74cbc6 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt @@ -1,11 +1,9 @@ package org.neo4j.graphql.handler.projection -import graphql.language.* import graphql.schema.* import org.neo4j.cypherdsl.core.* import org.neo4j.cypherdsl.core.Cypher.* import org.neo4j.cypherdsl.core.Functions.* -import org.neo4j.cypherdsl.core.Node import org.neo4j.cypherdsl.core.StatementBuilder.* import org.neo4j.graphql.* import org.neo4j.graphql.parser.ParsedQuery @@ -46,34 +44,45 @@ open class ProjectionBase( * Fields with special treatments */ val SPECIAL_FIELDS = setOf(FIRST, OFFSET, ORDER_BY, FILTER, OPTIONS) + + val TYPE_NAME_SELECTED_FIELD = object : SelectedField { + override fun getName(): String = TYPE_NAME + override fun getQualifiedName(): String = TYPE_NAME + override fun getFullyQualifiedName(): String = TYPE_NAME + override fun getObjectType(): GraphQLObjectType? = null + override fun getFieldDefinition(): GraphQLFieldDefinition? = null + override fun getArguments(): Map = emptyMap() + override fun getLevel(): Int = 0 + override fun isConditional(): Boolean = false + override fun getAlias(): String? = null + override fun getResultKey(): String? = null + override fun getParentField(): SelectedField? = null + override fun getSelectionSet(): DataFetchingFieldSelectionSet? = null + } } fun filterFieldName() = if (schemaConfig.useWhereFilter) WHERE else FILTER private fun orderBy( node: SymbolicName, - args: MutableList, - fieldDefinition: GraphQLFieldDefinition?, - variables: Map + args: Map, + fieldDefinition: GraphQLFieldDefinition? ): List? = if (schemaConfig.queryOptionStyle == SchemaConfig.InputStyle.INPUT_TYPE) { - extractSortFromOptions(node, args, fieldDefinition, variables) + extractSortFromOptions(node, args, fieldDefinition) } else { extractSortFromArgs(node, args, fieldDefinition) } private fun extractSortFromOptions( node: SymbolicName, - args: MutableList, - fieldDefinition: GraphQLFieldDefinition?, - variables: Map + args: Map, + fieldDefinition: GraphQLFieldDefinition? ): List? { - val options = args.find { it.name == OPTIONS }?.value as? ObjectValue + val options = args[OPTIONS] as? Map<*, *> val defaultOptions = (fieldDefinition?.getArgument(OPTIONS)?.type as? GraphQLInputObjectType) - val sortArray = (options?.objectFields?.find { it.name == SORT }?.value - ?.let { value -> (value as? VariableReference)?.let { variables[it.name] } ?: value }?.toJavaValue() - ?: defaultOptions?.getField(SORT)?.defaultValue?.toJavaValue() - ) as? List<*> ?: return null + val sortArray = (options?.get(SORT) ?: defaultOptions?.getField(SORT)?.defaultValue?.toJavaValue()) + as? List<*> ?: return null return sortArray .mapNotNull { it as? Map<*, *> } @@ -86,19 +95,18 @@ open class ProjectionBase( private fun extractSortFromArgs( node: SymbolicName, - args: MutableList, + args: Map, fieldDefinition: GraphQLFieldDefinition? ): List? { - val orderBy = args.find { it.name == ORDER_BY }?.value - ?: fieldDefinition?.getArgument(ORDER_BY)?.defaultValue?.asGraphQLValue() + val orderBy = args[ORDER_BY] + ?: fieldDefinition?.getArgument(ORDER_BY)?.defaultValue return orderBy ?.let { it -> when (it) { - is ArrayValue -> it.values.map { it.toJavaValue().toString() } - is EnumValue -> listOf(it.name) - is StringValue -> listOf(it.value) - else -> null + is List<*> -> it.map { it.toString() } + is String -> listOf(it) + else -> throw IllegalArgumentException("invalid type ${it.javaClass.name} for ${fieldDefinition?.name}.$ORDER_BY") } } ?.map { @@ -109,7 +117,6 @@ open class ProjectionBase( } } - private fun createSort(node: SymbolicName, property: String, direction: String) = sort(node.property(property)) .let { if (Sort.valueOf((direction).toUpperCase()) == Sort.ASC) it.ascending() else it.descending() } @@ -118,30 +125,29 @@ open class ProjectionBase( propertyContainer: PropertyContainer, fieldDefinition: GraphQLFieldDefinition, type: GraphQLFieldsContainer, - field: Field, + arguments: Map, variables: Map ): Condition { val variable = propertyContainer.requiredSymbolicName.value val result = if (!schemaConfig.useWhereFilter) { - val filteredArguments = field.arguments.filterNot { SPECIAL_FIELDS.contains(it.name) } - - val parsedQuery = parseArguments(filteredArguments, fieldDefinition, type, variables) + val filteredArguments = arguments.filterKeys { !SPECIAL_FIELDS.contains(it) } + val parsedQuery = parseArguments(filteredArguments, fieldDefinition, type) handleQuery(variable, "", propertyContainer, parsedQuery, type, variables) } else { Conditions.noCondition() } - return field.arguments.find { filterFieldName() == it.name } - ?.let { arg -> - when (arg.value) { - is ObjectValue -> arg.value as ObjectValue - is VariableReference -> variables[arg.name]?.let { it.asGraphQLValue() } + val filterFieldName = filterFieldName() + return arguments[filterFieldName] + ?.let { + when (it) { + is Map<*, *> -> it else -> throw IllegalArgumentException("") } } - ?.let { parseFilter(it as ObjectValue, type, variables) } + ?.let { parseFilter(it, type) } ?.let { - val filterCondition = handleQuery(normalizeName(filterFieldName(), variable), "", propertyContainer, it, type, variables) + val filterCondition = handleQuery(normalizeName(filterFieldName, variable), "", propertyContainer, it, type, variables) result.and(filterCondition) } ?: result @@ -158,15 +164,15 @@ open class ProjectionBase( var result = parsedQuery.getFieldConditions(propertyContainer, variablePrefix, variableSuffix, schemaConfig) for (predicate in parsedQuery.relationPredicates) { - val objectField = predicate.queryField + val value = predicate.value - if (objectField.value is NullValue) { + if (value == null) { val existsCondition = predicate.createExistsCondition(propertyContainer) result = result.and(existsCondition) continue } - if (objectField.value !is ObjectValue) { - throw IllegalArgumentException("Only object values are supported for filtering on queried relation ${predicate.queryField}, but got ${objectField.value.javaClass.name}") + if (value !is Map<*, *>) { + throw IllegalArgumentException("Only object values are supported for filtering on queried relation ${predicate.value}, but got ${value.javaClass.name}") } val cond = name(normalizeName(variablePrefix, predicate.relationshipInfo.typeName, "Cond")) @@ -180,7 +186,7 @@ open class ProjectionBase( }?.let { val targetNode = predicate.relNode.named(normalizeName(variablePrefix, predicate.relationshipInfo.typeName)) val relType = predicate.relationshipInfo.type - val parsedQuery2 = parseFilter(objectField.value as ObjectValue, relType, variables) + val parsedQuery2 = parseFilter(value, relType) val condition = handleQuery(targetNode.requiredSymbolicName.value, "", targetNode, parsedQuery2, relType, variables) var where = it .`in`(listBasedOn(predicate.relationshipInfo.createRelation(propertyContainer as Node, targetNode)).returning(condition)) @@ -193,15 +199,15 @@ open class ProjectionBase( } } - fun handleLogicalOperator(value: Value<*>, classifier: String, variables: Map): Condition { - val objectValue = value as? ObjectValue - ?: throw IllegalArgumentException("Only object values are supported for logical operations, but got ${value.javaClass.name}") + fun handleLogicalOperator(value: Any?, classifier: String, variables: Map): Condition { + val objectValue = value as? Map<*, *> + ?: throw IllegalArgumentException("Only object values are supported for logical operations, but got ${value?.javaClass?.name}") - val parsedNestedQuery = parseFilter(objectValue, type, variables) + val parsedNestedQuery = parseFilter(objectValue, type) return handleQuery(variablePrefix + classifier, variableSuffix, propertyContainer, parsedNestedQuery, type, variables) } - fun handleLogicalOperators(values: List>?, classifier: String): List { + fun handleLogicalOperators(values: List<*>?, classifier: String): List { return when { values?.isNotEmpty() == true -> when { values.size > 1 -> values.mapIndexed { index, value -> handleLogicalOperator(value, "${classifier}${index + 1}", variables) } @@ -218,29 +224,20 @@ open class ProjectionBase( fun projectFields( propertyContainer: PropertyContainer, - field: Field, nodeType: GraphQLFieldsContainer, env: DataFetchingEnvironment, - variableSuffix: String? = null + variable: SymbolicName = propertyContainer.requiredSymbolicName, + variableSuffix: String? = null, + selectionSet: DataFetchingFieldSelectionSet = env.selectionSet, ): Pair, List> { - return projectFields(propertyContainer, propertyContainer.requiredSymbolicName, field, nodeType, env, variableSuffix) - } - - fun projectFields( - propertyContainer: PropertyContainer, - variable: SymbolicName, - field: Field, - nodeType: GraphQLFieldsContainer, - env: DataFetchingEnvironment, - variableSuffix: String? = null - ): Pair, List> { - return projectSelection(propertyContainer, variable, field.selectionSet.selections, nodeType, env, variableSuffix) + val selectedFields = selectionSet.immediateFields.distinct() + return projectSelection(propertyContainer, variable, selectedFields, nodeType, env, variableSuffix) } private fun projectSelection( propertyContainer: PropertyContainer, variable: SymbolicName, - selection: List>, + selection: List, nodeType: GraphQLFieldsContainer, env: DataFetchingEnvironment, variableSuffix: String? @@ -250,28 +247,27 @@ open class ProjectionBase( // a{.name}, // CASE WHEN a:Location THEN a { .foo } ELSE {} END // ]) - var hasTypeName = false val projections = mutableListOf() val subQueries = mutableListOf() + val handledFields = mutableSetOf() selection.forEach { - val (pro, sub) = when (it) { - is Field -> { - hasTypeName = hasTypeName || (it.name == TYPE_NAME) - projectField(propertyContainer, variable, it, nodeType, env, variableSuffix) + val (pro, sub) = if ((nodeType is GraphQLInterfaceType && nodeType.getFieldDefinition(it.name) != null) || it.name == TYPE_NAME) { + if (!handledFields.add(it.name)) { + return@forEach } - is InlineFragment -> projectInlineFragment(propertyContainer, variable, it, env, variableSuffix) - is FragmentSpread -> projectNamedFragments(propertyContainer, variable, it, env, variableSuffix) - else -> emptyList() to emptyList() + projectField(propertyContainer, variable, it, nodeType, env, variableSuffix) + } else { + projectField(propertyContainer, variable, it, it.objectType, env, variableSuffix) } projections.addAll(pro) subQueries += sub } if (nodeType is GraphQLInterfaceType - && !hasTypeName + && !handledFields.contains(TYPE_NAME) && (env.getContext() as? QueryContext)?.queryTypeOfInterfaces == true ) { // for interfaces the typename is required to determine the correct implementation - val (pro, sub) = projectField(propertyContainer, variable, Field(TYPE_NAME), nodeType, env, variableSuffix) + val (pro, sub) = projectField(propertyContainer, variable, TYPE_NAME_SELECTED_FIELD, nodeType, env, variableSuffix) projections.addAll(pro) subQueries += sub } @@ -281,7 +277,7 @@ open class ProjectionBase( private fun projectField( propertyContainer: PropertyContainer, variable: SymbolicName, - field: Field, + field: SelectedField, type: GraphQLFieldsContainer, env: DataFetchingEnvironment, variableSuffix: String? @@ -314,10 +310,10 @@ open class ProjectionBase( if (cypherDirective != null) { val ctxVariable = name(field.contextualize(variable)) - val innerSubQuery = cypherDirective(ctxVariable, fieldDefinition, field, cypherDirective, variable, env) + val innerSubQuery = cypherDirective(ctxVariable, fieldDefinition, field.arguments, cypherDirective, variable) subQueries += if (isObjectField && !cypherDirective.passThrough) { val fieldObjectType = fieldDefinition.type.getInnerFieldsContainer() - val (fieldProjection, nestedSubQueries) = projectFields(anyNode(ctxVariable), ctxVariable, field, fieldObjectType, env, variableSuffix) + val (fieldProjection, nestedSubQueries) = projectFields(anyNode(ctxVariable), fieldObjectType, env, ctxVariable, variableSuffix, field.selectionSet) with(variable) .call(innerSubQuery) .withSubQueries(nestedSubQueries) @@ -376,11 +372,10 @@ open class ProjectionBase( return projections to subQueries } - private fun projectNeo4jObjectType(variable: SymbolicName, field: Field, fieldDefinition: GraphQLFieldDefinition): Expression { + private fun projectNeo4jObjectType(variable: SymbolicName, field: SelectedField, fieldDefinition: GraphQLFieldDefinition): Expression { val converter = getNeo4jTypeConverter(fieldDefinition) val projections = mutableListOf() - field.selectionSet.selections - .filterIsInstance() + field.selectionSet.immediateFields .forEach { projections += it.name projections += converter.projectField(variable, field, it.name) @@ -388,12 +383,12 @@ open class ProjectionBase( return mapOf(*projections.toTypedArray()) } - fun cypherDirective(ctxVariable: SymbolicName, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: CypherDirective, thisValue: SymbolicName?, env: DataFetchingEnvironment): ResultStatement { + fun cypherDirective(ctxVariable: SymbolicName, fieldDefinition: GraphQLFieldDefinition, arguments: Map, cypherDirective: CypherDirective, thisValue: SymbolicName?): ResultStatement { val args = sortedMapOf() if (thisValue != null) args["this"] = thisValue.`as`("this") - field.arguments - .filterNot { SPECIAL_FIELDS.contains(it.name) } - .forEach { args[it.name] = queryParameter(it.value, ctxVariable.value, it.name).`as`(it.name) } + arguments + .filterNot { (name, _) -> SPECIAL_FIELDS.contains(name) } + .forEach { (name, value) -> args[name] = queryParameter(value, ctxVariable.value, name).`as`(name) } fieldDefinition.arguments .filterNot { SPECIAL_FIELDS.contains(it.name) } .filter { it.defaultValue != null && !args.containsKey(it.name) } @@ -409,52 +404,27 @@ open class ProjectionBase( val expression = raw(cypherDirective.statement).`as`(ctxVariable) return (reading?.returningRaw(expression) ?: returningRaw(expression)) - .skipLimitOrder(ctxVariable, fieldDefinition, field, env) + .skipLimitOrder(ctxVariable, fieldDefinition, arguments) .build() } fun OngoingReadingAndReturn.skipLimitOrder( ctxVariable: SymbolicName, fieldDefinition: GraphQLFieldDefinition, - field: Field, - env: DataFetchingEnvironment + arguments: Map ) = if (fieldDefinition.type.isList()) { - val ordering = orderBy(ctxVariable, field.arguments, fieldDefinition, env.variables) + val ordering = orderBy(ctxVariable, arguments, fieldDefinition) val orderedResult = ordering?.let { o -> this.orderBy(*o.toTypedArray()) } ?: this - val skipLimit = SkipLimit(ctxVariable.value, field.arguments, fieldDefinition) + val skipLimit = SkipLimit(ctxVariable.value, arguments, fieldDefinition) skipLimit.format(orderedResult) } else { this.limit(1) } - private fun projectNamedFragments(node: PropertyContainer, variable: SymbolicName, fragmentSpread: FragmentSpread, env: DataFetchingEnvironment, variableSuffix: String?) = - env.fragmentsByName.getValue(fragmentSpread.name).let { - projectFragment(node, it.typeCondition.name, variable, env, variableSuffix, it.selectionSet) - } - - private fun projectInlineFragment(node: PropertyContainer, variable: SymbolicName, fragment: InlineFragment, env: DataFetchingEnvironment, variableSuffix: String?) = - projectFragment(node, fragment.typeCondition.name, variable, env, variableSuffix, fragment.selectionSet) - - private fun projectFragment( - node: PropertyContainer, - fragmentTypeName: String?, - variable: SymbolicName, - env: DataFetchingEnvironment, - variableSuffix: String?, - selectionSet: SelectionSet - ): Pair, List> { - val fragmentType = env.graphQLSchema.getType(fragmentTypeName) as? GraphQLFieldsContainer - ?: return emptyList() to emptyList() - // these are the nested fields of the fragment - // it could be that we have to adapt the variable name too, and perhaps add some kind of rename - return projectSelection(node, variable, selectionSet.selections, fragmentType, env, variableSuffix) - } - - private fun projectRelationship( node: PropertyContainer, variable: SymbolicName, - field: Field, + field: SelectedField, fieldDefinition: GraphQLFieldDefinition, parent: GraphQLFieldsContainer, env: DataFetchingEnvironment, @@ -484,7 +454,7 @@ open class ProjectionBase( private fun projectRelationshipParent( propertyContainer: PropertyContainer, variable: SymbolicName, - field: Field, + field: SelectedField, fieldDefinition: GraphQLFieldDefinition, parent: GraphQLFieldsContainer, env: DataFetchingEnvironment, @@ -494,8 +464,8 @@ open class ProjectionBase( ?: throw IllegalArgumentException("field ${fieldDefinition.name} of type ${parent.name} is not an object (fields container) and can not be handled as relationship") return when (propertyContainer) { is Node -> { - val (projectionEntries, subQueries) = projectFields(propertyContainer, name(variable.value + (variableSuffix?.capitalize() - ?: "")), field, fieldObjectType, env, variableSuffix) + val (projectionEntries, subQueries) = projectFields(propertyContainer, fieldObjectType, env, name(variable.value + (variableSuffix?.capitalize() + ?: "")), variableSuffix, field.selectionSet) propertyContainer.project(projectionEntries) to subQueries } is Relationship -> projectNodeFromRichRelationship(parent, fieldDefinition, variable, field, env) @@ -507,7 +477,7 @@ open class ProjectionBase( parent: GraphQLFieldsContainer, fieldDefinition: GraphQLFieldDefinition, variable: SymbolicName, - field: Field, + field: SelectedField, env: DataFetchingEnvironment ): Pair> { val relInfo = parent.relationship() @@ -520,7 +490,7 @@ open class ProjectionBase( else -> throw IllegalArgumentException("type ${parent.name} does not have a matching field with name ${fieldDefinition.name}") } val rel = relInfo.createRelation(start, end, false, variable) - val (projectFields, subQueries) = projectFields(target, field, fieldDefinition.type as GraphQLFieldsContainer, env) + val (projectFields, subQueries) = projectFields(target, fieldDefinition.type as GraphQLFieldsContainer, env, target.requiredSymbolicName, selectionSet = field.selectionSet) val match = with(variable) .match(rel) @@ -533,7 +503,7 @@ open class ProjectionBase( private fun projectRichAndRegularRelationship( variable: SymbolicName, - field: Field, + field: SelectedField, fieldDefinition: GraphQLFieldDefinition, parent: GraphQLFieldsContainer, env: DataFetchingEnvironment @@ -563,7 +533,7 @@ open class ProjectionBase( else -> node(nodeType.name).named(childVariableName) to null } - val (projectionEntries, sub) = projectFields(endNodePattern, name(childVariable), field, nodeType, env, variableSuffix) + val (projectionEntries, sub) = projectFields(endNodePattern, nodeType, env, name(childVariable), variableSuffix, field.selectionSet) val withPassThrough = mutableListOf(endNodePattern.requiredSymbolicName) var relationship = relInfo.createRelation(anyNode(variable), endNodePattern) @@ -573,7 +543,7 @@ open class ProjectionBase( } val with = with(variable) - val where = where(anyNode(childVariableName), fieldDefinition, nodeType, field, env.variables) + val where = where(anyNode(childVariableName), fieldDefinition, nodeType, field.arguments, env.variables) var reading: OngoingReading = when { fieldDefinition.type.isList() -> with.match(relationship) @@ -581,7 +551,7 @@ open class ProjectionBase( }.where(where) val subQuery = if (fieldDefinition.type.isList()) { - val ordering = orderBy(childVariableName, field.arguments, fieldDefinition, env.variables) + val ordering = orderBy(childVariableName, field.arguments, fieldDefinition) val skipLimit = SkipLimit(childVariable, field.arguments, fieldDefinition) reading = when { ordering != null -> skipLimit.format(reading.with(*withPassThrough.toTypedArray()).orderBy(*ordering.toTypedArray())) @@ -595,14 +565,14 @@ open class ProjectionBase( return childVariableName to listOf(subQuery.build()) } - inner class SkipLimit(variable: String, arguments: List, fieldDefinition: GraphQLFieldDefinition?) { + inner class SkipLimit(variable: String, arguments: Map, fieldDefinition: GraphQLFieldDefinition?) { private val skip: Parameter<*>? private val limit: Parameter<*>? init { if (schemaConfig.queryOptionStyle == SchemaConfig.InputStyle.INPUT_TYPE) { - val options = arguments.find { it.name == OPTIONS }?.value as? ObjectValue + val options = arguments[OPTIONS] as? Map<*, *> val defaultOptions = (fieldDefinition?.getArgument(OPTIONS)?.type as? GraphQLInputObjectType) this.skip = convertOptionField(variable, options, defaultOptions, SKIP) this.limit = convertOptionField(variable, options, defaultOptions, LIMIT) @@ -622,16 +592,15 @@ open class ProjectionBase( return limit?.let { result.limit(it) } ?: result } - private fun convertArgument(variable: String, arguments: List, fieldDefinition: GraphQLFieldDefinition?, name: String): Parameter<*>? { - val value = arguments - .find { it.name.toLowerCase() == name }?.value + private fun convertArgument(variable: String, arguments: Map, fieldDefinition: GraphQLFieldDefinition?, name: String): Parameter<*>? { + val value = arguments[name] ?: fieldDefinition?.getArgument(name)?.defaultValue ?: return null return queryParameter(value, variable, name) } - private fun convertOptionField(variable: String, options: ObjectValue?, defaultOptions: GraphQLInputObjectType?, name: String): Parameter<*>? { - val value = options?.objectFields?.find { it.name == name }?.value + private fun convertOptionField(variable: String, options: Map<*, *>?, defaultOptions: GraphQLInputObjectType?, name: String): Parameter<*>? { + val value = options?.get(name) ?: defaultOptions?.getField(name)?.defaultValue ?: return null return queryParameter(value, variable, name) diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt index 3a18b749..1ca80440 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt @@ -61,14 +61,14 @@ class CreateRelationHandler private constructor(schemaConfig: SchemaConfig) : Ba override fun generateCypher(variable: String, field: Field, env: DataFetchingEnvironment): Statement { - val properties = properties(variable, field.arguments) + val properties = properties(variable, env.arguments) val arguments = field.arguments.associateBy { it.name } val (startNode, startWhere) = getRelationSelect(true, arguments) val (endNode, endWhere) = getRelationSelect(false, arguments) val withAlias = startNode.`as`(variable) - val (mapProjection, subQueries) = projectFields(startNode, withAlias.asName(), field, type, env) + val (mapProjection, subQueries) = projectFields(startNode, type, env, withAlias.asName()) return org.neo4j.cypherdsl.core.Cypher.match(startNode).where(startWhere) .match(endNode).where(endWhere) .merge(relation.createRelation(startNode, endNode).withProperties(*properties)) diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationTypeHandler.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationTypeHandler.kt index e935f4e1..e2794cf2 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationTypeHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationTypeHandler.kt @@ -132,13 +132,13 @@ class CreateRelationTypeHandler private constructor(schemaConfig: SchemaConfig) } override fun generateCypher(variable: String, field: Field, env: DataFetchingEnvironment): Statement { - val properties = properties(variable, field.arguments) + val properties = properties(variable, env.arguments) val arguments = field.arguments.associateBy { it.name } val (startNode, startWhere) = getRelationSelect(true, arguments) val (endNode, endWhere) = getRelationSelect(false, arguments) val relName = name(variable) - val (mapProjection, subQueries) = projectFields(startNode, relName, field, type, env) + val (mapProjection, subQueries) = projectFields(startNode, type, env, relName) return org.neo4j.cypherdsl.core.Cypher.match(startNode).where(startWhere) .match(endNode).where(endWhere) @@ -151,7 +151,7 @@ class CreateRelationTypeHandler private constructor(schemaConfig: SchemaConfig) companion object { private fun normalizeFieldName(relFieldName: String?, name: String): String { - // TODO b/c we need to stay backwards compatible this is not caml case but with underscore + // TODO b/c we need to stay backwards compatible this is not camel-case but with underscore //val filedName = normalizeName(relFieldName, name) return "${relFieldName}_${name}" } diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/relation/DeleteRelationHandler.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/relation/DeleteRelationHandler.kt index 2f21730b..839e4235 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/relation/DeleteRelationHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/relation/DeleteRelationHandler.kt @@ -46,7 +46,7 @@ class DeleteRelationHandler private constructor(schemaConfig: SchemaConfig) : Ba val relName = org.neo4j.cypherdsl.core.Cypher.name("r") val withAlias = startNode.`as`(variable) - val (mapProjection, subQueries) = projectFields(startNode, withAlias.asName(), field, type, env) + val (mapProjection, subQueries) = projectFields(startNode, type, env, withAlias.asName()) return org.neo4j.cypherdsl.core.Cypher .match(startNode).where(startWhere) diff --git a/core/src/main/kotlin/org/neo4j/graphql/parser/QueryParser.kt b/core/src/main/kotlin/org/neo4j/graphql/parser/QueryParser.kt index 84c95046..531e39cb 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/parser/QueryParser.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/parser/QueryParser.kt @@ -16,8 +16,8 @@ typealias CypherDSL = org.neo4j.cypherdsl.core.Cypher class ParsedQuery( val fieldPredicates: List, val relationPredicates: List, - val or: List>? = null, - val and: List>? = null + val or: List<*>? = null, + val and: List<*>? = null ) { fun getFieldConditions(propertyContainer: PropertyContainer, variablePrefix: String, variableSuffix: String, schemaConfig: SchemaConfig): Condition = @@ -29,7 +29,7 @@ class ParsedQuery( abstract class Predicate( val op: T, - val queryField: ObjectField, + val value: Any?, val normalizedName: String, val index: Int) @@ -38,10 +38,10 @@ abstract class Predicate( */ class FieldPredicate( op: FieldOperator, - queryField: ObjectField, + value: Any?, val fieldDefinition: GraphQLFieldDefinition, index: Int -) : Predicate(op, queryField, normalizeName(fieldDefinition.name, op.suffix.toCamelCase()), index) { +) : Predicate(op, value, normalizeName(fieldDefinition.name, op.suffix.toCamelCase()), index) { fun createCondition(propertyContainer: PropertyContainer, variablePrefix: String, variableSuffix: String, schemaConfig: SchemaConfig) = op.resolveCondition( @@ -49,7 +49,7 @@ class FieldPredicate( normalizedName, propertyContainer, fieldDefinition, - queryField.value, + value, schemaConfig, variableSuffix ) @@ -63,10 +63,10 @@ class FieldPredicate( class RelationPredicate( type: GraphQLFieldsContainer, op: RelationOperator, - queryField: ObjectField, + value: Any?, val fieldDefinition: GraphQLFieldDefinition, index: Int -) : Predicate(op, queryField, normalizeName(fieldDefinition.name, op.suffix.toCamelCase()), index) { +) : Predicate(op, value, normalizeName(fieldDefinition.name, op.suffix.toCamelCase()), index) { val relationshipInfo = type.relationshipFor(fieldDefinition.name)!! val relNode: Node = CypherDSL.node(fieldDefinition.type.getInnerFieldsContainer().label()) @@ -88,38 +88,33 @@ class RelationPredicate( object QueryParser { - /** - * This parser takes an filter object an transform it to the internal [ParsedQuery]-representation - */ - fun parseFilter(objectValue: ObjectValue, type: GraphQLFieldsContainer, variables: Map): ParsedQuery { + fun parseFilter(filter: Map<*, *>, type: GraphQLFieldsContainer): ParsedQuery { // Map of all queried fields // we remove all matching fields from this map, so we can ensure that only known fields got queried - val queriedFields = objectValue.objectFields - .mapIndexed { index, field -> field.name to (index to field) } + val queriedFields = filter + .entries + .mapIndexed { index, (key, value) -> key as String to (index to value) } .toMap(mutableMapOf()) - val or = queriedFields.remove("OR")?.second?.value?.let { - (it as ArrayValue).values + val or = queriedFields.remove("OR")?.second?.let { + (it as? List<*>) ?: throw IllegalArgumentException("OR on type `${type.name}` is expected to be a list") } - - - val and = queriedFields.remove("AND")?.second?.value?.let { - (it as ArrayValue).values + val and = queriedFields.remove("AND")?.second?.let { + (it as? List<*>) ?: throw IllegalArgumentException("AND on type `${type.name}` is expected to be a list") } - - return createParsedQuery(queriedFields, type, variables, null, or, and) + return createParsedQuery(queriedFields, type, or, and) } /** * This parser takes all non-filter arguments of a graphql-field an transform it to the internal [ParsedQuery]-representation */ - fun parseArguments(arguments: List, fieldDefinition: GraphQLFieldDefinition, type: GraphQLFieldsContainer, variables: Map): ParsedQuery { - // TODO we should check if the argument is defined on the field definition and throw an error otherwise + fun parseArguments(arguments: Map, fieldDefinition: GraphQLFieldDefinition, type: GraphQLFieldsContainer): ParsedQuery { // Map of all queried fields // we remove all matching fields from this map, so we can ensure that only known fields got queried val queriedFields = arguments - .mapIndexed { index, argument -> argument.name to (index to ObjectField(argument.name, argument.value)) } + .entries + .mapIndexed { index, (key, value) -> key to (index to value as Any?) } .toMap(mutableMapOf()) var index = queriedFields.size fieldDefinition.arguments @@ -127,20 +122,18 @@ object QueryParser { .filterNot { queriedFields.containsKey(it.name) } .filterNot { ProjectionBase.SPECIAL_FIELDS.contains(it.name) } .forEach { argument -> - queriedFields[argument.name] = index++ to ObjectField(argument.name, argument.defaultValue.asGraphQLValue()) + queriedFields[argument.name] = index++ to argument.defaultValue } - return createParsedQuery(queriedFields, type, variables, fieldDefinition) + return createParsedQuery(queriedFields, type) } private fun createParsedQuery( - queriedFields: MutableMap>, + queriedFields: MutableMap>, type: GraphQLFieldsContainer, - variables: Map, - fieldDefinition: GraphQLFieldDefinition? = null, - or: List>>? = null, - and: List>>? = null + or: List<*>? = null, + and: List<*>? = null ): ParsedQuery { // find all matching fields val fieldPredicates = mutableListOf() @@ -150,9 +143,9 @@ object QueryParser { RelationOperator.values() .map { it to definedField.name + it.suffix } .mapNotNull { (queryOp, queryFieldName) -> - queriedFields.remove(queryFieldName)?.let { (index, objectField) -> - val harmonizedOperator = queryOp.harmonize(type, definedField, objectField.value, queryFieldName) - RelationPredicate(type, harmonizedOperator, objectField, definedField, index) + queriedFields.remove(queryFieldName)?.let { (index, filter) -> + val harmonizedOperator = queryOp.harmonize(type, definedField, filter, queryFieldName) + RelationPredicate(type, harmonizedOperator, filter, definedField, index) } } .forEach { relationPredicates.add(it) } @@ -161,7 +154,7 @@ object QueryParser { .map { it to definedField.name + it.suffix } .mapNotNull { (predicate, queryFieldName) -> queriedFields[queryFieldName]?.let { (index, objectField) -> - if (predicate.requireParam xor (!objectField.value.isNullValue(variables))) { + if (predicate.requireParam xor (objectField != null)) { // if we got a value but the predicate requires none // or we got a no value but the predicate requires one // we skip this operator @@ -188,8 +181,5 @@ object QueryParser { } } -private fun Value<*>.isNullValue(variables: Map): Boolean = - this is NullValue || (this is VariableReference && variables[this.name] == null) - diff --git a/core/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt b/core/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt index 84f63a4f..93950fb3 100644 --- a/core/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt +++ b/core/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt @@ -7,6 +7,7 @@ import org.junit.jupiter.api.DynamicNode import org.junit.jupiter.api.DynamicTest import java.io.File import java.io.FileWriter +import java.math.BigInteger import java.net.URI import java.util.* import java.util.regex.Pattern @@ -289,6 +290,7 @@ open class AsciiDocTestSuite( fun fixNumber(v: Any?): Any? = when (v) { is Float -> v.toDouble() is Int -> v.toLong() + is BigInteger -> v.toLong() is Iterable<*> -> v.map { fixNumber(it) } is Sequence<*> -> v.map { fixNumber(it) } is Map<*, *> -> v.mapValues { fixNumber(it.value) } diff --git a/core/src/test/resources/cypher-directive-tests.adoc b/core/src/test/resources/cypher-directive-tests.adoc index 0c2699fd..f8e53019 100644 --- a/core/src/test/resources/cypher-directive-tests.adoc +++ b/core/src/test/resources/cypher-directive-tests.adoc @@ -89,14 +89,16 @@ query($pname:String) { p3(name:$pname) { id }} .Cypher params [source,json] ---- -{"pname":"foo"} +{ + "p3Name" : "foo" +} ---- .Cypher [source,cypher] ---- CALL { - WITH $pname AS name + WITH $p3Name AS name MATCH (p:Person) WHERE p.name = name RETURN p AS p3 LIMIT 1 } RETURN p3 { diff --git a/core/src/test/resources/filter-tests.adoc b/core/src/test/resources/filter-tests.adoc index aa2dfc93..05aa7bb3 100644 --- a/core/src/test/resources/filter-tests.adoc +++ b/core/src/test/resources/filter-tests.adoc @@ -694,7 +694,7 @@ RETURN person { [source,json] ---- { - "filterCompany_id" : 1 + "filterCompany_id" : "1" } ---- @@ -721,7 +721,7 @@ RETURN company { [source,json] ---- { - "filterCompany_idIn" : [ 1, 2 ] + "filterCompany_idIn" : [ "1", "2" ] } ---- @@ -748,7 +748,7 @@ RETURN company { [source,json] ---- { - "filterCompany_idNotIn" : [ 1, 2 ] + "filterCompany_idNotIn" : [ "1", "2" ] } ---- @@ -775,7 +775,7 @@ RETURN company { [source,json] ---- { - "filterCompany_idNot" : 1 + "filterCompany_idNot" : "1" } ---- @@ -2135,8 +2135,8 @@ RETURN person { [source,json] ---- { - "filterPersonLocationAnd1Longitude" : 1, - "filterPersonLocationAnd2Latitude" : 2 + "filterPersonLocationAnd1Longitude" : 1.0, + "filterPersonLocationAnd2Latitude" : 2.0 } ---- @@ -2173,8 +2173,8 @@ RETURN person { [source,json] ---- { - "filterPersonLocationNotAnd1Longitude" : 1, - "filterPersonLocationNotAnd2Latitude" : 2 + "filterPersonLocationNotAnd1Longitude" : 1.0, + "filterPersonLocationNotAnd2Latitude" : 2.0 } ---- @@ -2280,11 +2280,11 @@ RETURN person { .name } AS person ---- { "filterPersonLocationDistance" : { - "distance" : 3, + "distance" : 3.0, "point" : { - "longitude" : 1, - "latitude" : 2, - "height" : 3 + "longitude" : 1.0, + "latitude" : 2.0, + "height" : 3.0 } } } @@ -2327,11 +2327,11 @@ RETURN person { ---- { "filterPersonLocationDistanceLt" : { - "distance" : 3, + "distance" : 3.0, "point" : { - "longitude" : 1, - "latitude" : 2, - "height" : 3 + "longitude" : 1.0, + "latitude" : 2.0, + "height" : 3.0 } } } @@ -2374,11 +2374,11 @@ RETURN person { ---- { "filterPersonLocationDistanceLte" : { - "distance" : 3, + "distance" : 3.0, "point" : { - "longitude" : 1, - "latitude" : 2, - "height" : 3 + "longitude" : 1.0, + "latitude" : 2.0, + "height" : 3.0 } } } @@ -2421,11 +2421,11 @@ RETURN person { ---- { "filterPersonLocationDistanceGt" : { - "distance" : 3, + "distance" : 3.0, "point" : { - "longitude" : 1, - "latitude" : 2, - "height" : 3 + "longitude" : 1.0, + "latitude" : 2.0, + "height" : 3.0 } } } @@ -2468,11 +2468,11 @@ RETURN person { ---- { "filterPersonLocationDistanceGte" : { - "distance" : 3, + "distance" : 3.0, "point" : { - "longitude" : 1, - "latitude" : 2, - "height" : 3 + "longitude" : 1.0, + "latitude" : 2.0, + "height" : 3.0 } } } @@ -2585,8 +2585,8 @@ RETURN person { [source,cypher] ---- MATCH (person:Person) -WHERE (person.fun = $filterPersonFun - AND person.name = $filterPersonName) +WHERE (person.name = $filterPersonName + AND person.fun = $filterPersonFun) RETURN person { .name } AS person @@ -2663,8 +2663,8 @@ RETURN person { [source,cypher] ---- MATCH (person:Person) -WHERE (person.fun = $filterPersonFun - AND NOT (person.name = $filterPersonNameNot)) +WHERE (NOT (person.name = $filterPersonNameNot) + AND person.fun = $filterPersonFun) RETURN person { .name } AS person @@ -3334,7 +3334,7 @@ query filterQuery($name: String) { person(filter: {name : $name}) { name }} [source,json] ---- { - "name" : "Jane" + "filterPersonName" : "Jane" } ---- @@ -3342,7 +3342,7 @@ query filterQuery($name: String) { person(filter: {name : $name}) { name }} [source,cypher] ---- MATCH (person:Person) -WHERE person.name = $name +WHERE person.name = $filterPersonName RETURN person { .name } AS person diff --git a/core/src/test/resources/issues/gh-112.adoc b/core/src/test/resources/issues/gh-112.adoc index d431b4d9..9b0a81d6 100644 --- a/core/src/test/resources/issues/gh-112.adoc +++ b/core/src/test/resources/issues/gh-112.adoc @@ -70,7 +70,7 @@ query user( $uuid: ID ){ [source,json] ---- { - "uuid" : "2" + "userUuid" : "2" } ---- @@ -78,7 +78,7 @@ query user( $uuid: ID ){ [source,cypher] ---- MATCH (user:User) -WHERE user.uuid = $uuid +WHERE user.uuid = $userUuid CALL { WITH user MATCH (user)-[:ASSOCIATES_WITH]-(userAssociates:User) diff --git a/core/src/test/resources/issues/gh-149.adoc b/core/src/test/resources/issues/gh-149.adoc index cac6449b..e84a1c68 100644 --- a/core/src/test/resources/issues/gh-149.adoc +++ b/core/src/test/resources/issues/gh-149.adoc @@ -6,7 +6,7 @@ [source,graphql,schema=true] ---- -interface Person { +type Person { name: ID! } ---- diff --git a/core/src/test/resources/issues/gh-47.adoc b/core/src/test/resources/issues/gh-47.adoc index 4b5e8229..cf69e374 100644 --- a/core/src/test/resources/issues/gh-47.adoc +++ b/core/src/test/resources/issues/gh-47.adoc @@ -6,7 +6,7 @@ [source,graphql,schema=true] ---- -interface Company { +type Company { name: String } ---- diff --git a/core/src/test/resources/logback-test.xml b/core/src/test/resources/logback-test.xml index 2ae59bb2..14577d19 100644 --- a/core/src/test/resources/logback-test.xml +++ b/core/src/test/resources/logback-test.xml @@ -9,4 +9,5 @@ + diff --git a/core/src/test/resources/movie-tests.adoc b/core/src/test/resources/movie-tests.adoc index 3802ce0e..1daf5925 100644 --- a/core/src/test/resources/movie-tests.adoc +++ b/core/src/test/resources/movie-tests.adoc @@ -1145,7 +1145,7 @@ RETURN user { [source,json] ---- { - "rated_id" : 1 + "rated_id" : "1" } ---- @@ -1289,7 +1289,7 @@ mutation { [source,json] ---- { - "createUserUserId" : 1, + "createUserUserId" : "1", "createUserValidTypes" : [ "User" ] } ---- @@ -1355,8 +1355,8 @@ query { [source,json] ---- { - "userBornAnd1Formatted" : "2015-06-24T12:50:35.556000000+01:00", - "userBornAnd2Year" : 2015 + "userBornAnd1Year" : 2015, + "userBornAnd2Formatted" : "2015-06-24T12:50:35.556000000+01:00" } ---- @@ -1364,8 +1364,8 @@ query { [source,cypher] ---- MATCH (user:User) -WHERE (user.born = datetime($userBornAnd1Formatted) - AND user.born.year = $userBornAnd2Year) +WHERE (user.born.year = $userBornAnd1Year + AND user.born = datetime($userBornAnd2Formatted)) RETURN user { born: { year: user.born.year diff --git a/core/src/test/resources/optimized-query-for-filter.adoc b/core/src/test/resources/optimized-query-for-filter.adoc index 5b2a60b2..76b95b25 100644 --- a/core/src/test/resources/optimized-query-for-filter.adoc +++ b/core/src/test/resources/optimized-query-for-filter.adoc @@ -274,16 +274,16 @@ RETURN movie { ---- MATCH (movie:Movie) WITH movie -MATCH (movie)<-[:DIRECTED]-(movieDirectedByEvery:Person) -WHERE movieDirectedByEvery.name = $movieDirectedByEveryName -WITH movie, size((movie)<-[:DIRECTED]-(:Person)) AS movieDirectedByEveryTotal, count(DISTINCT movieDirectedByEvery) AS movieDirectedByEveryCount -WHERE movieDirectedByEveryTotal = movieDirectedByEveryCount -WITH DISTINCT movie MATCH (movie)<-[:REVIEWED]-(movieReviewedByEvery:Person) WHERE movieReviewedByEvery.name = $movieReviewedByEveryName WITH movie, size((movie)<-[:REVIEWED]-(:Person)) AS movieReviewedByEveryTotal, count(DISTINCT movieReviewedByEvery) AS movieReviewedByEveryCount WHERE movieReviewedByEveryTotal = movieReviewedByEveryCount WITH DISTINCT movie +MATCH (movie)<-[:DIRECTED]-(movieDirectedByEvery:Person) +WHERE movieDirectedByEvery.name = $movieDirectedByEveryName +WITH movie, size((movie)<-[:DIRECTED]-(:Person)) AS movieDirectedByEveryTotal, count(DISTINCT movieDirectedByEvery) AS movieDirectedByEveryCount +WHERE movieDirectedByEveryTotal = movieDirectedByEveryCount +WITH DISTINCT movie RETURN movie { .title } AS movie @@ -451,11 +451,6 @@ RETURN movie { .title } AS movie ---- MATCH (movie:Movie) WITH movie -MATCH (movie)<-[:DIRECTED]-(movieDirectedByEvery:Person) -WHERE movieDirectedByEvery.name = $movieDirectedByEveryName -WITH movie, size((movie)<-[:DIRECTED]-(:Person)) AS movieDirectedByEveryTotal, count(DISTINCT movieDirectedByEvery) AS movieDirectedByEveryCount -WHERE movieDirectedByEveryTotal = movieDirectedByEveryCount -WITH DISTINCT movie MATCH (movie)<-[:REVIEWED]-(movieReviewedBySome:Person) WHERE movieReviewedBySome.name = $movieReviewedBySomeName WITH movie, movieReviewedBySome @@ -464,6 +459,11 @@ WITH movie, movieReviewedBySomeFollowedBySome MATCH (movieReviewedBySomeFollowedBySome)-[:REVIEWED]->(movieReviewedBySomeFollowedBySomeReviewedMoviesSome:Movie) WHERE movieReviewedBySomeFollowedBySomeReviewedMoviesSome.released >= $movieReviewedBySomeFollowedBySomeReviewedMoviesSomeReleasedGte WITH DISTINCT movie +MATCH (movie)<-[:DIRECTED]-(movieDirectedByEvery:Person) +WHERE movieDirectedByEvery.name = $movieDirectedByEveryName +WITH movie, size((movie)<-[:DIRECTED]-(:Person)) AS movieDirectedByEveryTotal, count(DISTINCT movieDirectedByEvery) AS movieDirectedByEveryCount +WHERE movieDirectedByEveryTotal = movieDirectedByEveryCount +WITH DISTINCT movie RETURN movie { .title } AS movie @@ -974,8 +974,8 @@ RETURN person { [source,json] ---- { - "personLocationNotAnd1Longitude" : 3, - "personLocationNotAnd2Latitude" : 3 + "personLocationNotAnd1Longitude" : 3.0, + "personLocationNotAnd2Latitude" : 3.0 } ---- @@ -1014,11 +1014,11 @@ RETURN person { ---- { "personLocationDistanceLt" : { - "distance" : 3, + "distance" : 3.0, "point" : { - "longitude" : 1, - "latitude" : 2, - "height" : 3 + "longitude" : 1.0, + "latitude" : 2.0, + "height" : 3.0 } } } diff --git a/core/src/test/resources/tck-test-files/cypher/pagination.adoc b/core/src/test/resources/tck-test-files/cypher/pagination.adoc index 8183e897..de40e1a9 100644 --- a/core/src/test/resources/tck-test-files/cypher/pagination.adoc +++ b/core/src/test/resources/tck-test-files/cypher/pagination.adoc @@ -151,13 +151,12 @@ query($skip: Int, $limit: Int) { } ---- - .Expected Cypher params [source,json] ---- { - "limit" : 0, - "skip" : 0 + "moviesLimit" : 0, + "moviesSkip" : 0 } ---- @@ -167,7 +166,7 @@ query($skip: Int, $limit: Int) { MATCH (movies:Movie) RETURN movies { .title -} AS movies SKIP $skip LIMIT $limit +} AS movies SKIP $moviesSkip LIMIT $moviesLimit ---- === Skip + Limit with other variables @@ -199,9 +198,9 @@ query($skip: Int, $limit: Int, $title: String) { [source,json] ---- { - "limit" : 1, - "skip" : 2, - "title" : "some title" + "moviesLimit" : 1, + "moviesSkip" : 2, + "whereMoviesTitle" : "some title" } ---- @@ -209,10 +208,10 @@ query($skip: Int, $limit: Int, $title: String) { [source,cypher] ---- MATCH (movies:Movie) -WHERE movies.title = $title +WHERE movies.title = $whereMoviesTitle RETURN movies { .title -} AS movies SKIP $skip LIMIT $limit +} AS movies SKIP $moviesSkip LIMIT $moviesLimit ---- === Default values @@ -401,15 +400,14 @@ query($skip: Int, $limit: Int) { } ---- - .Expected Cypher params [source,json] ---- { "actorsLimit" : 10, - "actorsSkip" : 0, - "limit" : 0, - "skip" : 0 + "actorsMoviesLimit" : 0, + "actorsMoviesSkip" : 0, + "actorsSkip" : 0 } ---- @@ -420,7 +418,7 @@ MATCH (actors:Actor) CALL { WITH actors MATCH (actors)-[:ACTS_IN]->(actorsMovies:Movie) - WITH actorsMovies SKIP $skip LIMIT $limit + WITH actorsMovies SKIP $actorsMoviesSkip LIMIT $actorsMoviesLimit RETURN collect(actorsMovies { .title }) AS actorsMovies @@ -459,16 +457,15 @@ query($skip: Int, $limit: Int, $title: String) { } ---- - .Expected Cypher params [source,json] ---- { "actorsLimit" : 10, + "actorsMoviesLimit" : 1, + "actorsMoviesSkip" : 2, "actorsSkip" : 0, - "limit" : 1, - "skip" : 2, - "title" : "some title" + "whereActorsMoviesTitle" : "some title" } ---- @@ -479,8 +476,8 @@ MATCH (actors:Actor) CALL { WITH actors MATCH (actors)-[:ACTS_IN]->(actorsMovies:Movie) - WHERE actorsMovies.title = $title - WITH actorsMovies SKIP $skip LIMIT $limit + WHERE actorsMovies.title = $whereActorsMoviesTitle + WITH actorsMovies SKIP $actorsMoviesSkip LIMIT $actorsMoviesLimit RETURN collect(actorsMovies { .title }) AS actorsMovies diff --git a/core/src/test/resources/tck-test-files/cypher/sort.adoc b/core/src/test/resources/tck-test-files/cypher/sort.adoc index ce06f05e..13db83e3 100644 --- a/core/src/test/resources/tck-test-files/cypher/sort.adoc +++ b/core/src/test/resources/tck-test-files/cypher/sort.adoc @@ -136,9 +136,9 @@ query($title: String, $skip: Int, $limit: Int, $sort: [MovieSort!]) { [source,json] ---- { - "limit" : 0, - "skip" : 0, - "title" : "some title" + "moviesLimit" : 0, + "moviesSkip" : 0, + "whereMoviesTitle" : "some title" } ---- @@ -146,10 +146,10 @@ query($title: String, $skip: Int, $limit: Int, $sort: [MovieSort!]) { [source,cypher] ---- MATCH (movies:Movie) -WHERE movies.title = $title +WHERE movies.title = $whereMoviesTitle RETURN movies { .title -} AS movies ORDER BY movies.id DESC, movies.title ASC SKIP $skip LIMIT $limit +} AS movies ORDER BY movies.id DESC, movies.title ASC SKIP $moviesSkip LIMIT $moviesLimit ---- === Default values @@ -294,9 +294,9 @@ query($name: String, $skip: Int, $limit: Int, $sort: [GenreSort!]) { [source,json] ---- { - "limit" : 2, - "name" : "some name", - "skip" : 1 + "moviesGenresLimit" : 2, + "moviesGenresSkip" : 1, + "whereMoviesGenresName" : "some name" } ---- @@ -307,8 +307,8 @@ MATCH (movies:Movie) CALL { WITH movies MATCH (movies)-[:HAS_GENRE]->(moviesGenres:Genre) - WHERE moviesGenres.name = $name - WITH moviesGenres ORDER BY moviesGenres.id DESC, moviesGenres.name ASC SKIP $skip LIMIT $limit + WHERE moviesGenres.name = $whereMoviesGenresName + WITH moviesGenres ORDER BY moviesGenres.id DESC, moviesGenres.name ASC SKIP $moviesGenresSkip LIMIT $moviesGenresLimit RETURN collect(moviesGenres { .name }) AS moviesGenres diff --git a/core/src/test/resources/tck-test-files/cypher/types/datetime.adoc b/core/src/test/resources/tck-test-files/cypher/types/datetime.adoc index b7ba5903..104cb5a8 100644 --- a/core/src/test/resources/tck-test-files/cypher/types/datetime.adoc +++ b/core/src/test/resources/tck-test-files/cypher/types/datetime.adoc @@ -171,8 +171,8 @@ mutation { [source,cypher] ---- CREATE (createMovie:Movie { - datetime: datetime($createMovieDatetime), - id: $createMovieId + id: $createMovieId, + datetime: datetime($createMovieDatetime) }) WITH createMovie RETURN createMovie { diff --git a/core/src/test/resources/tck-test-files/cypher/where.adoc b/core/src/test/resources/tck-test-files/cypher/where.adoc index 83728a1e..7e2243e8 100644 --- a/core/src/test/resources/tck-test-files/cypher/where.adoc +++ b/core/src/test/resources/tck-test-files/cypher/where.adoc @@ -56,8 +56,8 @@ query($title: String, $isFavorite: Boolean) { [source,json] ---- { - "isFavorite" : true, - "title" : "some title" + "whereMovieIsFavorite" : true, + "whereMovieTitle" : "some title" } ---- @@ -65,8 +65,8 @@ query($title: String, $isFavorite: Boolean) { [source,cypher] ---- MATCH (movie:Movie) -WHERE (movie.title = $title - AND movie.isFavorite = $isFavorite) +WHERE (movie.title = $whereMovieTitle + AND movie.isFavorite = $whereMovieIsFavorite) RETURN movie { .title } AS movie diff --git a/core/src/test/resources/translator-tests1.adoc b/core/src/test/resources/translator-tests1.adoc index 5d88c39e..0cc8c4c4 100644 --- a/core/src/test/resources/translator-tests1.adoc +++ b/core/src/test/resources/translator-tests1.adoc @@ -210,7 +210,7 @@ query getPersons($offset: Int){ [source,json] ---- { - "offset" : 10 + "personOffset" : 10 } ---- @@ -220,7 +220,7 @@ query getPersons($offset: Int){ MATCH (person:Person) RETURN person { .age -} AS person SKIP $offset +} AS person SKIP $personOffset ---- === nested query @@ -957,8 +957,8 @@ query { [source,json] ---- { - "personLocationAnd1Longitude" : 1, - "personLocationAnd2Latitude" : 2 + "personLocationAnd1Longitude" : 1.0, + "personLocationAnd2Latitude" : 2.0 } ---- @@ -1003,9 +1003,9 @@ mutation{ ---- { "createPersonLocation" : { - "x" : 1, - "y" : 2, - "z" : 3, + "x" : 1.0, + "y" : 2.0, + "z" : 3.0, "crs" : "wgs-84-3d" }, "createPersonName" : "Test2" @@ -1031,3 +1031,47 @@ RETURN createPerson { } } AS createPerson ---- + +=== enforce typeName on interfaces + +.Query configuration +[source,json,query-config=true] +---- +{ "queryTypeOfInterfaces": true } +---- + +.GraphQL-Query +[source,graphql] +---- +{ + location { + name + ... on City { + city_Arg + } + ... on Village { + villageArg + } + } +} +---- + +.Cypher params +[source,json] +---- +{ + "locationValidTypes" : [ "City", "Village" ] +} +---- + +.Cypher +[source,cypher] +---- +MATCH (location:Location) +RETURN location { + .name, + .city_Arg, + .villageArg, + __typename: head([label IN labels(location) WHERE label IN $locationValidTypes]) +} AS location +----