From 823fe6b93aa39c301d9edd4809d09e8ebd24880c Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Tue, 4 May 2021 17:27:35 +0200 Subject: [PATCH] Migrate filter and field args into a where (#220) These changes are made in order to harmonize with the API of the js version of this library. resolves #181 --- .../kotlin/org/neo4j/graphql/BuildingEnv.kt | 17 +- .../kotlin/org/neo4j/graphql/SchemaBuilder.kt | 15 +- .../kotlin/org/neo4j/graphql/SchemaConfig.kt | 8 + .../neo4j/graphql/handler/BaseDataFetcher.kt | 3 +- .../handler/BaseDataFetcherForContainer.kt | 5 +- .../graphql/handler/CreateTypeHandler.kt | 7 +- .../graphql/handler/CypherDirectiveHandler.kt | 7 +- .../neo4j/graphql/handler/DeleteHandler.kt | 5 +- .../graphql/handler/MergeOrUpdateHandler.kt | 9 +- .../org/neo4j/graphql/handler/QueryHandler.kt | 20 +- .../handler/filter/OptimizedFilterHandler.kt | 24 +- .../handler/projection/ProjectionBase.kt | 62 +- .../handler/relation/BaseRelationHandler.kt | 5 +- .../handler/relation/CreateRelationHandler.kt | 7 +- .../relation/CreateRelationTypeHandler.kt | 7 +- .../handler/relation/DeleteRelationHandler.kt | 7 +- .../cypher/advanced-filtering.adoc | 655 ++++++++++++++++++ .../tck-test-files/cypher/pagination.adoc | 13 +- .../resources/tck-test-files/cypher/sort.adoc | 11 +- .../tck-test-files/cypher/where.adoc | 283 ++++++++ .../tck-test-files/schema/relationship.adoc | 82 +-- .../tck-test-files/schema/simple.adoc | 33 +- 22 files changed, 1138 insertions(+), 147 deletions(-) create mode 100644 core/src/test/resources/tck-test-files/cypher/advanced-filtering.adoc create mode 100644 core/src/test/resources/tck-test-files/cypher/where.adoc diff --git a/core/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt b/core/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt index 5b94b89c..9abd8cb8 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt @@ -6,7 +6,8 @@ import org.neo4j.graphql.handler.projection.ProjectionBase class BuildingEnv( val types: MutableMap, - private val sourceSchema: GraphQLSchema + private val sourceSchema: GraphQLSchema, + val schemaConfig: SchemaConfig ) { private val typesForRelation = types.values @@ -77,7 +78,7 @@ class BuildingEnv( } fun addFilterType(type: GraphQLFieldsContainer, createdTypes: MutableSet = mutableSetOf()): String { - val filterName = "_${type.name}Filter" + val filterName = if (schemaConfig.useWhereFilter) type.name + "Where" else "_${type.name}Filter" if (createdTypes.contains(filterName)) { return filterName } @@ -138,7 +139,7 @@ class BuildingEnv( ?: throw IllegalStateException("Ordering type $type.name is already defined but not an input type") } val sortTypeName = addSortInputType(type) - val optionsTypeBuilder = GraphQLInputObjectType.newInputObject().name(optionsName) + val optionsTypeBuilder = GraphQLInputObjectType.newInputObject().name(optionsName) if (sortTypeName != null) { optionsTypeBuilder.field(GraphQLInputObjectField.newInputObjectField() .name(ProjectionBase.SORT) @@ -147,10 +148,10 @@ class BuildingEnv( .build()) } optionsTypeBuilder.field(GraphQLInputObjectField.newInputObjectField() - .name(ProjectionBase.LIMIT) - .type(Scalars.GraphQLInt) - .description("Defines the maximum amount of records returned") - .build()) + .name(ProjectionBase.LIMIT) + .type(Scalars.GraphQLInt) + .description("Defines the maximum amount of records returned") + .build()) .field(GraphQLInputObjectField.newInputObjectField() .name(ProjectionBase.SKIP) .type(Scalars.GraphQLInt) @@ -169,7 +170,7 @@ class BuildingEnv( ?: throw IllegalStateException("Ordering type $type.name is already defined but not an input type") } val relevantFields = type.relevantFields() - if (relevantFields.isEmpty()){ + if (relevantFields.isEmpty()) { return null } val builder = GraphQLInputObjectType.newInputObject() diff --git a/core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt b/core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt index bbb2a4f1..87a48638 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt @@ -130,7 +130,7 @@ object SchemaBuilder { private fun augmentSchema(sourceSchema: GraphQLSchema, handler: List, schemaConfig: SchemaConfig): GraphQLSchema { val types = sourceSchema.typeMap.toMutableMap() - val env = BuildingEnv(types, sourceSchema) + val env = BuildingEnv(types, sourceSchema, schemaConfig) val queryTypeName = sourceSchema.queryTypeName() val mutationTypeName = sourceSchema.mutationTypeName() val subscriptionTypeName = sourceSchema.subscriptionTypeName() @@ -157,11 +157,11 @@ object SchemaBuilder { builder.clearFields().clearInterfaces() // to prevent duplicated types in schema sourceType.interfaces.forEach { builder.withInterface(GraphQLTypeReference(it.name)) } - sourceType.fieldDefinitions.forEach { f -> builder.field(enhanceRelations(f, env, schemaConfig)) } + sourceType.fieldDefinitions.forEach { f -> builder.field(enhanceRelations(f, env)) } } sourceType is GraphQLInterfaceType -> sourceType.transform { builder -> builder.clearFields() - sourceType.fieldDefinitions.forEach { f -> builder.field(enhanceRelations(f, env, schemaConfig)) } + sourceType.fieldDefinitions.forEach { f -> builder.field(enhanceRelations(f, env)) } } else -> sourceType } @@ -177,7 +177,7 @@ object SchemaBuilder { .build() } - private fun enhanceRelations(fd: GraphQLFieldDefinition, env: BuildingEnv, schemaConfig: SchemaConfig): GraphQLFieldDefinition { + private fun enhanceRelations(fd: GraphQLFieldDefinition, env: BuildingEnv): GraphQLFieldDefinition { return fd.transform { fieldBuilder -> // to prevent duplicated types in schema fieldBuilder.type(fd.type.ref() as GraphQLOutputType) @@ -188,7 +188,7 @@ object SchemaBuilder { val fieldType = fd.type.inner() as? GraphQLFieldsContainer ?: return@transform - if (schemaConfig.queryOptionStyle == SchemaConfig.InputStyle.INPUT_TYPE){ + if (env.schemaConfig.queryOptionStyle == SchemaConfig.InputStyle.INPUT_TYPE) { val optionsTypeName = env.addOptions(fieldType) val optionsType = GraphQLTypeReference(optionsTypeName) @@ -212,9 +212,10 @@ object SchemaBuilder { } - if (schemaConfig.query.enabled && !schemaConfig.query.exclude.contains(fieldType.name) && fd.getArgument(ProjectionBase.FILTER) == null) { + val filterFieldName = if (env.schemaConfig.useWhereFilter) ProjectionBase.WHERE else ProjectionBase.FILTER + if (env.schemaConfig.query.enabled && !env.schemaConfig.query.exclude.contains(fieldType.name) && fd.getArgument(filterFieldName) == null) { val filterTypeName = env.addFilterType(fieldType) - fieldBuilder.argument(input(ProjectionBase.FILTER, GraphQLTypeReference(filterTypeName))) + fieldBuilder.argument(input(filterFieldName, GraphQLTypeReference(filterTypeName))) } } } diff --git a/core/src/main/kotlin/org/neo4j/graphql/SchemaConfig.kt b/core/src/main/kotlin/org/neo4j/graphql/SchemaConfig.kt index b74be65f..c545d11f 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/SchemaConfig.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/SchemaConfig.kt @@ -3,14 +3,22 @@ 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, + /** * Defines the way the input for queries and mutations are generated */ val queryOptionStyle: InputStyle = InputStyle.ARGUMENT_PER_FIELD, + + /** + * 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, ) { data class CRUDConfig(val enabled: Boolean = true, val exclude: List = emptyList()) 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 320a434e..8a8715bb 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt @@ -9,13 +9,14 @@ import org.neo4j.cypherdsl.core.Statement import org.neo4j.cypherdsl.core.renderer.Configuration import org.neo4j.cypherdsl.core.renderer.Renderer import org.neo4j.graphql.Cypher +import org.neo4j.graphql.SchemaConfig import org.neo4j.graphql.aliasOrName import org.neo4j.graphql.handler.projection.ProjectionBase /** * The is a base class for the implementation of graphql data fetcher used in this project */ -abstract class BaseDataFetcher(val fieldDefinition: GraphQLFieldDefinition) : ProjectionBase(), DataFetcher { +abstract class BaseDataFetcher(val fieldDefinition: GraphQLFieldDefinition, schemaConfig: SchemaConfig) : ProjectionBase(schemaConfig), DataFetcher { override fun get(env: DataFetchingEnvironment?): Cypher { val field = env?.mergedField?.singleField 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 015f8a64..c406d4b5 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcherForContainer.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcherForContainer.kt @@ -14,8 +14,9 @@ import org.neo4j.graphql.* */ abstract class BaseDataFetcherForContainer( val type: GraphQLFieldsContainer, - fieldDefinition: GraphQLFieldDefinition -) : BaseDataFetcher(fieldDefinition) { + fieldDefinition: GraphQLFieldDefinition, + schemaConfig: SchemaConfig +) : BaseDataFetcher(fieldDefinition, schemaConfig) { val propertyFields: MutableMap List?> = mutableMapOf() val defaultFields: MutableMap = mutableMapOf() 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 15197e72..cbc8ca32 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt @@ -12,8 +12,9 @@ import org.neo4j.graphql.* */ class CreateTypeHandler private constructor( type: GraphQLFieldsContainer, - fieldDefinition: GraphQLFieldDefinition -) : BaseDataFetcherForContainer(type, fieldDefinition) { + fieldDefinition: GraphQLFieldDefinition, + schemaConfig: SchemaConfig +) : BaseDataFetcherForContainer(type, fieldDefinition, schemaConfig) { class Factory(schemaConfig: SchemaConfig) : AugmentationHandler(schemaConfig) { override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { @@ -41,7 +42,7 @@ class CreateTypeHandler private constructor( return null } return when { - fieldDefinition.name == "create${type.name}" -> CreateTypeHandler(type, fieldDefinition) + fieldDefinition.name == "create${type.name}" -> CreateTypeHandler(type, fieldDefinition, schemaConfig) else -> null } } 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 b23ff1b2..1b72e7fe 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt @@ -16,8 +16,9 @@ class CypherDirectiveHandler( private val type: GraphQLFieldsContainer?, private val isQuery: Boolean, private val cypherDirective: CypherDirective, - fieldDefinition: GraphQLFieldDefinition) - : BaseDataFetcher(fieldDefinition) { + fieldDefinition: GraphQLFieldDefinition, + schemaConfig: SchemaConfig) + : BaseDataFetcher(fieldDefinition, schemaConfig) { class Factory(schemaConfig: SchemaConfig) : AugmentationHandler(schemaConfig) { @@ -25,7 +26,7 @@ class CypherDirectiveHandler( val cypherDirective = fieldDefinition.cypherDirective() ?: return null val type = fieldDefinition.type.inner() as? GraphQLFieldsContainer val isQuery = operationType == OperationType.QUERY - return CypherDirectiveHandler(type, isQuery, cypherDirective, fieldDefinition) + return CypherDirectiveHandler(type, isQuery, cypherDirective, fieldDefinition, schemaConfig) } } 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 bdf61d4a..02e78182 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt @@ -16,8 +16,9 @@ class DeleteHandler private constructor( type: GraphQLFieldsContainer, private val idField: GraphQLFieldDefinition, fieldDefinition: GraphQLFieldDefinition, + schemaConfig: SchemaConfig, private val isRelation: Boolean = type.isRelationType() -) : BaseDataFetcherForContainer(type, fieldDefinition) { +) : BaseDataFetcherForContainer(type, fieldDefinition, schemaConfig) { class Factory(schemaConfig: SchemaConfig) : AugmentationHandler(schemaConfig) { override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { @@ -48,7 +49,7 @@ class DeleteHandler private constructor( } val idField = type.getIdField() ?: return null return when (fieldDefinition.name) { - "delete${type.name}" -> DeleteHandler(type, idField, fieldDefinition) + "delete${type.name}" -> DeleteHandler(type, idField, fieldDefinition, schemaConfig) else -> null } } 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 241171a9..c9496fad 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/MergeOrUpdateHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/MergeOrUpdateHandler.kt @@ -20,8 +20,9 @@ class MergeOrUpdateHandler private constructor( private val merge: Boolean, private val idField: GraphQLFieldDefinition, fieldDefinition: GraphQLFieldDefinition, + schemaConfig: SchemaConfig, private val isRelation: Boolean = type.isRelationType() -) : BaseDataFetcherForContainer(type, fieldDefinition) { +) : BaseDataFetcherForContainer(type, fieldDefinition, schemaConfig) { class Factory(schemaConfig: SchemaConfig) : AugmentationHandler(schemaConfig) { override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { @@ -55,8 +56,8 @@ class MergeOrUpdateHandler private constructor( } val idField = type.getIdField() ?: return null return when (fieldDefinition.name) { - "merge${type.name}" -> MergeOrUpdateHandler(type, true, idField, fieldDefinition) - "update${type.name}" -> MergeOrUpdateHandler(type, false, idField, fieldDefinition) + "merge${type.name}" -> MergeOrUpdateHandler(type, true, idField, fieldDefinition, schemaConfig) + "update${type.name}" -> MergeOrUpdateHandler(type, false, idField, fieldDefinition, schemaConfig) else -> null } } @@ -107,7 +108,7 @@ class MergeOrUpdateHandler private constructor( } } val properties = properties(variable, field.arguments) - val mapProjection = projectFields(propertyContainer,field, type, env) + val mapProjection = projectFields(propertyContainer, field, type, env) val update: OngoingMatchAndUpdate = 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 cdd10626..ffcb2793 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt @@ -14,8 +14,9 @@ import org.neo4j.graphql.handler.filter.OptimizedFilterHandler */ class QueryHandler private constructor( type: GraphQLFieldsContainer, - fieldDefinition: GraphQLFieldDefinition) - : BaseDataFetcherForContainer(type, fieldDefinition) { + fieldDefinition: GraphQLFieldDefinition, + schemaConfig: SchemaConfig +) : BaseDataFetcherForContainer(type, fieldDefinition, schemaConfig) { class Factory(schemaConfig: SchemaConfig) : AugmentationHandler(schemaConfig) { override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { @@ -25,15 +26,18 @@ class QueryHandler private constructor( val typeName = type.name val relevantFields = getRelevantFields(type) - // TODO not just generate the input type but use it as well - buildingEnv.addInputType("_${typeName}Input", type.relevantFields()) val filterTypeName = buildingEnv.addFilterType(type) + val arguments = if (schemaConfig.useWhereFilter) { + listOf(input(WHERE, GraphQLTypeReference(filterTypeName))) + } else { + buildingEnv.getInputValueDefinitions(relevantFields, { true }) + + input(FILTER, GraphQLTypeReference(filterTypeName)) + } val builder = GraphQLFieldDefinition .newFieldDefinition() .name(if (schemaConfig.capitalizeQueryFields) typeName else typeName.decapitalize()) - .arguments(buildingEnv.getInputValueDefinitions(relevantFields) { true }) - .argument(input(FILTER, GraphQLTypeReference(filterTypeName))) + .arguments(arguments) .type(GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLTypeReference(type.name))))) if (schemaConfig.queryOptionStyle == SchemaConfig.InputStyle.INPUT_TYPE) { @@ -68,7 +72,7 @@ class QueryHandler private constructor( if (!canHandle(type)) { return null } - return QueryHandler(type, fieldDefinition) + return QueryHandler(type, fieldDefinition, schemaConfig) } private fun canHandle(type: GraphQLFieldsContainer): Boolean { @@ -102,7 +106,7 @@ class QueryHandler private constructor( val ongoingReading = if ((env.getContext() as? QueryContext)?.optimizedQuery?.contains(QueryContext.OptimizationStrategy.FILTER_AS_MATCH) == true) { - OptimizedFilterHandler(type).generateFilterQuery(variable, fieldDefinition, field, match, propertyContainer, env.variables) + OptimizedFilterHandler(type, schemaConfig).generateFilterQuery(variable, fieldDefinition, field, match, propertyContainer, env.variables) } else { 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 37532987..1cc587cb 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 @@ -32,7 +32,7 @@ typealias ConditionBuilder = (ExposesWith) -> OrderableOngoingReadingAndWithWith * If this handler cannot generate an optimization for the passed filter, an [OptimizedQueryException] will be * thrown, so the calling site can fall back to the non-optimized logic */ -class OptimizedFilterHandler(val type: GraphQLFieldsContainer) : ProjectionBase() { +class OptimizedFilterHandler(val type: GraphQLFieldsContainer, schemaConfig: SchemaConfig) : ProjectionBase(schemaConfig) { fun generateFilterQuery(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, readingWithoutWhere: OngoingReadingWithoutWhere, rootNode: PropertyContainer, variables: Map): OngoingReading { if (type.isRelationType()) { @@ -41,23 +41,23 @@ class OptimizedFilterHandler(val type: GraphQLFieldsContainer) : ProjectionBase( var ongoingReading: OngoingReading? = null - val filteredArguments = field.arguments.filterNot { SPECIAL_FIELDS.contains(it.name) } - if (filteredArguments.isNotEmpty()) { - val parsedQuery = QueryParser.parseArguments(filteredArguments, fieldDefinition, type, variables) - val condition = handleQuery(variable, "", rootNode, parsedQuery, type, variables) - ongoingReading = readingWithoutWhere.where(condition) + if (!schemaConfig.useWhereFilter) { + val filteredArguments = field.arguments.filterNot { SPECIAL_FIELDS.contains(it.name) } + if (filteredArguments.isNotEmpty()) { + val parsedQuery = QueryParser.parseArguments(filteredArguments, fieldDefinition, type, variables) + val condition = handleQuery(variable, "", rootNode, parsedQuery, type, variables) + ongoingReading = readingWithoutWhere.where(condition) + } } - for (argument in field.arguments) { - if (argument.name == FILTER) { + return field.arguments.find { filterFieldName() == it.name } + ?.let { argument -> val parsedQuery = parseFilter(argument.value as ObjectValue, type, variables) - ongoingReading = NestingLevelHandler(parsedQuery, false, rootNode, variable, ongoingReading + NestingLevelHandler(parsedQuery, false, rootNode, variable, ongoingReading ?: readingWithoutWhere, type, argument.value, linkedSetOf(rootNode.requiredSymbolicName), variables) .parseFilter() } - } - - return ongoingReading ?: readingWithoutWhere + ?: readingWithoutWhere } /** 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 664afe18..10cc73d1 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 @@ -17,7 +17,10 @@ import org.neo4j.graphql.parser.QueryParser.parseFilter /** * This class contains the logic for projecting nodes and relations */ -open class ProjectionBase { +open class ProjectionBase( + protected val schemaConfig: SchemaConfig +) { + companion object { /* * old arguments, subject to be removed in future releases @@ -37,6 +40,7 @@ open class ProjectionBase { const val LIMIT = "limit" const val SKIP = "skip" const val SORT = "sort" + const val WHERE = "where" const val TYPE_NAME = "__typename" @@ -46,6 +50,8 @@ open class ProjectionBase { val SPECIAL_FIELDS = setOf(FIRST, OFFSET, ORDER_BY, FILTER, OPTIONS) } + fun filterFieldName() = if (schemaConfig.useWhereFilter) WHERE else FILTER + fun orderBy(node: PropertyContainer, args: MutableList, fieldDefinition: GraphQLFieldDefinition?, variables: Map): List? { val values = getOrderByArgs(args, fieldDefinition, variables) if (values.isEmpty()) { @@ -101,12 +107,15 @@ open class ProjectionBase { ): Condition { val variable = propertyContainer.requiredSymbolicName.value - val filteredArguments = field.arguments.filterNot { SPECIAL_FIELDS.contains(it.name) } + val result = if (!schemaConfig.useWhereFilter) { + val filteredArguments = field.arguments.filterNot { SPECIAL_FIELDS.contains(it.name) } - val parsedQuery = parseArguments(filteredArguments, fieldDefinition, type, variables) - val result = handleQuery(variable, "", propertyContainer, parsedQuery, type, variables) - - return field.arguments.find { FILTER == it.name } + val parsedQuery = parseArguments(filteredArguments, fieldDefinition, type, variables) + 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 @@ -116,7 +125,7 @@ open class ProjectionBase { } ?.let { parseFilter(it as ObjectValue, type, variables) } ?.let { - val filterCondition = handleQuery(normalizeName(FILTER, variable), "", propertyContainer, it, type, variables) + val filterCondition = handleQuery(normalizeName(filterFieldName(), variable), "", propertyContainer, it, type, variables) result.and(filterCondition) } ?: result @@ -154,8 +163,9 @@ open class ProjectionBase { else -> null }?.let { val targetNode = predicate.relNode.named(normalizeName(variablePrefix, predicate.relationshipInfo.typeName)) - val parsedQuery2 = parseFilter(objectField.value as ObjectValue, type, variables) - val condition = handleQuery(targetNode.requiredSymbolicName.value, "", targetNode, parsedQuery2, type, variables) + val relType = predicate.relationshipInfo.type + val parsedQuery2 = parseFilter(objectField.value as ObjectValue, relType, variables) + val condition = handleQuery(targetNode.requiredSymbolicName.value, "", targetNode, parsedQuery2, relType, variables) var where = it .`in`(listBasedOn(predicate.relationshipInfo.createRelation(propertyContainer as Node, targetNode)).returning(condition)) .where(cond.asCondition()) @@ -473,15 +483,15 @@ open class ProjectionBase { return skipLimit.slice(fieldType.isList(), comprehension) } - class SkipLimit(variable: String, arguments: List, fieldDefinition: GraphQLFieldDefinition?) { + inner class SkipLimit(variable: String, arguments: List, fieldDefinition: GraphQLFieldDefinition?) { private val skip: Parameter<*>? private val limit: Parameter<*>? init { - val options = arguments.find { it.name == OPTIONS }?.value as? ObjectValue - val defaultOptions = (fieldDefinition?.getArgument(OPTIONS)?.type as? GraphQLInputObjectType) - if (options != null || defaultOptions != null) { + if (schemaConfig.queryOptionStyle == SchemaConfig.InputStyle.INPUT_TYPE) { + val options = arguments.find { it.name == OPTIONS }?.value as? ObjectValue + val defaultOptions = (fieldDefinition?.getArgument(OPTIONS)?.type as? GraphQLInputObjectType) this.skip = convertOptionField(variable, options, defaultOptions, SKIP) this.limit = convertOptionField(variable, options, defaultOptions, LIMIT) } else { @@ -504,21 +514,19 @@ open class ProjectionBase { ?: subList(expression, literalOf(0), limit) } - companion object { - private fun convertArgument(variable: String, arguments: List, fieldDefinition: GraphQLFieldDefinition?, name: String): Parameter<*>? { - val value = arguments - .find { it.name.toLowerCase() == name }?.value - ?: fieldDefinition?.getArgument(name)?.defaultValue - ?: return null - return queryParameter(value, variable, name) - } + private fun convertArgument(variable: String, arguments: List, fieldDefinition: GraphQLFieldDefinition?, name: String): Parameter<*>? { + val value = arguments + .find { it.name.toLowerCase() == name }?.value + ?: 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 - ?: defaultOptions?.getField(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 + ?: defaultOptions?.getField(name)?.defaultValue + ?: return null + return queryParameter(value, variable, name) } } diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/relation/BaseRelationHandler.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/relation/BaseRelationHandler.kt index 25ae1fed..6810b183 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/relation/BaseRelationHandler.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/relation/BaseRelationHandler.kt @@ -16,8 +16,9 @@ abstract class BaseRelationHandler( val relation: RelationshipInfo, private val startId: RelationshipInfo.RelatedField, private val endId: RelationshipInfo.RelatedField, - fieldDefinition: GraphQLFieldDefinition) - : BaseDataFetcherForContainer(type, fieldDefinition) { + fieldDefinition: GraphQLFieldDefinition, + schemaConfig: SchemaConfig) + : BaseDataFetcherForContainer(type, fieldDefinition, schemaConfig) { init { propertyFields.remove(startId.argumentName) 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 c4dd241e..f649b220 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 @@ -18,8 +18,9 @@ class CreateRelationHandler private constructor( relation: RelationshipInfo, startId: RelationshipInfo.RelatedField, endId: RelationshipInfo.RelatedField, - fieldDefinition: GraphQLFieldDefinition) - : BaseRelationHandler(type, relation, startId, endId, fieldDefinition) { + fieldDefinition: GraphQLFieldDefinition, + schemaConfig: SchemaConfig +) : BaseRelationHandler(type, relation, startId, endId, fieldDefinition, schemaConfig) { class Factory(schemaConfig: SchemaConfig) : BaseRelationFactory("add", schemaConfig) { override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { @@ -54,7 +55,7 @@ class CreateRelationHandler private constructor( endIdField: RelationshipInfo.RelatedField, fieldDefinition: GraphQLFieldDefinition ): DataFetcher { - return CreateRelationHandler(sourceType, relation, startIdField, endIdField, fieldDefinition) + return CreateRelationHandler(sourceType, relation, startIdField, endIdField, fieldDefinition, schemaConfig) } } 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 ff828877..bbd932ae 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 @@ -16,8 +16,9 @@ class CreateRelationTypeHandler private constructor( relation: RelationshipInfo, startId: RelationshipInfo.RelatedField, endId: RelationshipInfo.RelatedField, - fieldDefinition: GraphQLFieldDefinition) - : BaseRelationHandler(type, relation, startId, endId, fieldDefinition) { + fieldDefinition: GraphQLFieldDefinition, + schemaConfig: SchemaConfig +) : BaseRelationHandler(type, relation, startId, endId, fieldDefinition, schemaConfig) { class Factory(schemaConfig: SchemaConfig) : AugmentationHandler(schemaConfig) { override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { @@ -67,7 +68,7 @@ class CreateRelationTypeHandler private constructor( val startIdField = relation.getStartFieldId() ?: return null val endIdField = relation.getEndFieldId() ?: return null - return CreateRelationTypeHandler(type, relation, startIdField, endIdField, fieldDefinition) + return CreateRelationTypeHandler(type, relation, startIdField, endIdField, fieldDefinition, schemaConfig) } private fun getRelevantFields(type: GraphQLFieldsContainer): List { 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 03717afb..6dd47689 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 @@ -18,8 +18,9 @@ class DeleteRelationHandler private constructor( relation: RelationshipInfo, startId: RelationshipInfo.RelatedField, endId: RelationshipInfo.RelatedField, - fieldDefinition: GraphQLFieldDefinition) - : BaseRelationHandler(type, relation, startId, endId, fieldDefinition) { + fieldDefinition: GraphQLFieldDefinition, + schemaConfig: SchemaConfig +) : BaseRelationHandler(type, relation, startId, endId, fieldDefinition, schemaConfig) { class Factory(schemaConfig: SchemaConfig) : BaseRelationFactory("delete", schemaConfig) { override fun augmentType(type: GraphQLFieldsContainer, buildingEnv: BuildingEnv) { @@ -41,7 +42,7 @@ class DeleteRelationHandler private constructor( endIdField: RelationshipInfo.RelatedField, fieldDefinition: GraphQLFieldDefinition ): DataFetcher { - return DeleteRelationHandler(sourceType, relation, startIdField, endIdField, fieldDefinition) + return DeleteRelationHandler(sourceType, relation, startIdField, endIdField, fieldDefinition, schemaConfig) } } diff --git a/core/src/test/resources/tck-test-files/cypher/advanced-filtering.adoc b/core/src/test/resources/tck-test-files/cypher/advanced-filtering.adoc new file mode 100644 index 00000000..3885026f --- /dev/null +++ b/core/src/test/resources/tck-test-files/cypher/advanced-filtering.adoc @@ -0,0 +1,655 @@ +:toc: + += Cypher Advanced Filtering + +Tests advanced filtering. + +== Inputs + +[source,graphql,schema=true] +---- +type Movie { + _id: ID + id: ID + title: String + actorCount: Int +# TODO support BigInt +# budget: BigInt + genres: [Genre] @relation(name: "IN_GENRE", direction: OUT) +} + +type Genre { + name: String + movies: [Movie] @relation(name: "IN_GENRE", direction: IN) +} +---- + +== Configuration + +.Configuration +[source,json,schema-config=true] +---- +{ + "queryOptionStyle": "INPUT_TYPE", + "useWhereFilter": true +} +---- + +== Tests + +=== IN + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { id_in: ["123"] }) { + id + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieIdIn" : [ "123" ] +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.id IN $whereMovieIdIn +RETURN movie { + .id +} AS movie +---- + +=== REGEX + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { id_matches: "(?i)123.*" }) { + id + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieIdMatches" : "(?i)123.*" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.id =~ $whereMovieIdMatches +RETURN movie { + .id +} AS movie +---- + +=== NOT + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { id_not: "123" }) { + id + } +} +---- + + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieIdNot" : "123" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE NOT (movie.id = $whereMovieIdNot) +RETURN movie { + .id +} AS movie +---- + +=== NOT_IN + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { id_not_in: ["123"] }) { + id + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieIdNotIn" : [ "123" ] +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE NOT (movie.id IN $whereMovieIdNotIn) +RETURN movie { + .id +} AS movie +---- + +=== CONTAINS + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { id_contains: "123" }) { + id + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieIdContains" : "123" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.id CONTAINS $whereMovieIdContains +RETURN movie { + .id +} AS movie +---- + +=== NOT_CONTAINS + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { id_not_contains: "123" }) { + id + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieIdNotContains" : "123" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE NOT (movie.id CONTAINS $whereMovieIdNotContains) +RETURN movie { + .id +} AS movie +---- + +=== STARTS_WITH + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { id_starts_with: "123" }) { + id + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieIdStartsWith" : "123" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.id STARTS WITH $whereMovieIdStartsWith +RETURN movie { + .id +} AS movie +---- + +=== NOT_STARTS_WITH + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { id_not_starts_with: "123" }) { + id + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieIdNotStartsWith" : "123" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE NOT (movie.id STARTS WITH $whereMovieIdNotStartsWith) +RETURN movie { + .id +} AS movie +---- + +=== ENDS_WITH + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { id_ends_with: "123" }) { + id + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieIdEndsWith" : "123" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.id ENDS WITH $whereMovieIdEndsWith +RETURN movie { + .id +} AS movie +---- + +=== NOT_ENDS_WITH + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { id_not_ends_with: "123" }) { + id + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieIdNotEndsWith" : "123" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE NOT (movie.id ENDS WITH $whereMovieIdNotEndsWith) +RETURN movie { + .id +} AS movie +---- + +=== LT + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { actorCount_lt: 123 }) { + actorCount + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieActorCountLt" : 123 +} +---- + + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.actorCount < $whereMovieActorCountLt +RETURN movie { + .actorCount +} AS movie +---- + +=== LT BigInt + +CAUTION: *Not yet implemented* + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { budget_lt: 9223372036854775807 }) { + budget + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "this_budget_LT": { + "low": -1, + "high": 2147483647 + } +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (this:Movie) +WHERE this.budget < $this_budget_LT +RETURN this { .budget } as this +---- + +=== LTE + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { actorCount_lte: 123 }) { + actorCount + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieActorCountLte" : 123 +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.actorCount <= $whereMovieActorCountLte +RETURN movie { + .actorCount +} AS movie +---- + +=== LTE BigInt + +CAUTION: *Not yet implemented* + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { budget_lte: 9223372036854775807 }) { + budget + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "this_budget_LTE": { + "low": -1, + "high": 2147483647 + } +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (this:Movie) +WHERE this.budget <= $this_budget_LTE +RETURN this { .budget } as this +---- + +=== GT + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { actorCount_gt: 123 }) { + actorCount + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieActorCountGt" : 123 +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.actorCount > $whereMovieActorCountGt +RETURN movie { + .actorCount +} AS movie +---- + +=== GT BigInt + +CAUTION: *Not yet implemented* + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { budget_gt: 9223372036854775000 }) { + budget + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "this_budget_GT": { + "low": -808, + "high": 2147483647 + } +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (this:Movie) +WHERE this.budget > $this_budget_GT +RETURN this { .budget } as this +---- + +=== GTE + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { actorCount_gte: 123 }) { + actorCount + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieActorCountGte" : 123 +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.actorCount >= $whereMovieActorCountGte +RETURN movie { + .actorCount +} AS movie +---- + +=== GTE BigInt + +CAUTION: *Not yet implemented* + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { budget_gte: 9223372036854775000 }) { + budget + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "this_budget_GTE": { + "low": -808, + "high": 2147483647 + } +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (this:Movie) +WHERE this.budget >= $this_budget_GTE +RETURN this { .budget } as this +---- + +=== Relationship equality + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { genres: { name: "some genre" } }) { + actorCount + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieGenreName" : "some genre" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE all(whereMovieGenreCond IN [(movie)-[:IN_GENRE]->(whereMovieGenre:Genre) | whereMovieGenre.name = $whereMovieGenreName] +WHERE whereMovieGenreCond) +RETURN movie { + .actorCount +} AS movie +---- + +=== Relationship NOT + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { genres_not: { name: "some genre" } }) { + actorCount + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieGenreName" : "some genre" +} +---- + + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE none(whereMovieGenreCond IN [(movie)-[:IN_GENRE]->(whereMovieGenre:Genre) | whereMovieGenre.name = $whereMovieGenreName] +WHERE whereMovieGenreCond) +RETURN movie { + .actorCount +} AS movie +---- 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 450fc934..2e23a9b6 100644 --- a/core/src/test/resources/tck-test-files/cypher/pagination.adoc +++ b/core/src/test/resources/tck-test-files/cypher/pagination.adoc @@ -34,7 +34,8 @@ input ActorOptions { [source,json,schema-config=true] ---- { - "queryOptionStyle": "INPUT_TYPE" + "queryOptionStyle": "INPUT_TYPE", + "useWhereFilter": true } ---- @@ -174,7 +175,10 @@ RETURN movie { [source,graphql] ---- query($skip: Int, $limit: Int, $title: String) { - movie(options: { limit: $limit, skip: $skip }, title: $title) { + movie( + options: { limit: $limit, skip: $skip }, + where: { title: $title } + ) { title } } @@ -410,7 +414,10 @@ RETURN actor { query($skip: Int, $limit: Int, $title: String) { actor { name - movies (options: { limit: $limit, skip: $skip }, title: $title) { + movies ( + options: { limit: $limit, skip: $skip }, + where: { title: $title } + ) { title } } 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 257b6a11..b6b2a0df 100644 --- a/core/src/test/resources/tck-test-files/cypher/sort.adoc +++ b/core/src/test/resources/tck-test-files/cypher/sort.adoc @@ -38,7 +38,8 @@ input GenreSort { [source,json,schema-config=true] ---- { - "queryOptionStyle": "INPUT_TYPE" + "queryOptionStyle": "INPUT_TYPE", + "useWhereFilter": true } ---- @@ -110,7 +111,9 @@ query($title: String, $skip: Int, $limit: Int, $sort: [MovieSort!]) { skip: $skip limit: $limit } - title: $title + where: { + title: $title + } ) { title } @@ -252,7 +255,9 @@ query($name: String, $skip: Int, $limit: Int, $sort: [GenreSort!]) { skip: $skip limit: $limit } - name: $name + where: { + name: $name + } ) { name } diff --git a/core/src/test/resources/tck-test-files/cypher/where.adoc b/core/src/test/resources/tck-test-files/cypher/where.adoc new file mode 100644 index 00000000..83728a1e --- /dev/null +++ b/core/src/test/resources/tck-test-files/cypher/where.adoc @@ -0,0 +1,283 @@ +:toc: + += Cypher WHERE + +Tests for queries using options.where + +== Inputs + +[source,graphql,schema=true] +---- +type Actor { + name: String + movies: [Movie] @relation(name: "ACTED_IN", direction: OUT) +} + +type Movie { + id: ID + title: String + actors: [Actor] @relation(name: "ACTED_IN", direction: IN) + isFavorite: Boolean +} +---- + +== Configuration + +.Configuration +[source,json,schema-config=true] +---- +{ + "queryOptionStyle": "INPUT_TYPE", + "useWhereFilter": true +} +---- + +== Tests + +=== Simple + +.GraphQL-Query +[source,graphql] +---- +query($title: String, $isFavorite: Boolean) { + movie(where: { title: $title, isFavorite: $isFavorite }) { + title + } +} +---- + +.GraphQL params input +[source,json,request=true] +---- +{ "title": "some title", "isFavorite": true } +---- + +.Expected Cypher params +[source,json] +---- +{ + "isFavorite" : true, + "title" : "some title" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE (movie.title = $title + AND movie.isFavorite = $isFavorite) +RETURN movie { + .title +} AS movie +---- + +=== Simple AND + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { AND: [{ title: "some title" }] }) { + title + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieTitle" : "some title" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.title = $whereMovieTitle +RETURN movie { + .title +} AS movie +---- + +=== Nested AND + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { AND: [{ AND: [{ title: "some title" }] }] }) { + title + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieTitle" : "some title" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.title = $whereMovieTitle +RETURN movie { + .title +} AS movie +---- + +=== Super Nested AND + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { AND: [{ AND: [{ AND: [{ title: "some title" }] }] }] }) { + title + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieTitle" : "some title" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.title = $whereMovieTitle +RETURN movie { + .title +} AS movie +---- + +=== Simple OR + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { OR: [{ title: "some title" }] }) { + title + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieTitle" : "some title" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.title = $whereMovieTitle +RETURN movie { + .title +} AS movie +---- + +=== Nested OR + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { OR: [{ OR: [{ title: "some title" }] }] }) { + title + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieTitle" : "some title" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.title = $whereMovieTitle +RETURN movie { + .title +} AS movie +---- + +=== Super Nested OR + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { OR: [{ OR: [{ OR: [{ title: "some title" }] }] }] }) { + title + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieTitle" : "some title" +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.title = $whereMovieTitle +RETURN movie { + .title +} AS movie +---- + +=== Simple IN + +.GraphQL-Query +[source,graphql] +---- +{ + movie(where: { title_in: ["some title"] }) { + title + } +} +---- + +.Expected Cypher params +[source,json] +---- +{ + "whereMovieTitleIn" : [ "some title" ] +} +---- + +.Expected Cypher output +[source,cypher] +---- +MATCH (movie:Movie) +WHERE movie.title IN $whereMovieTitleIn +RETURN movie { + .title +} AS movie +---- diff --git a/core/src/test/resources/tck-test-files/schema/relationship.adoc b/core/src/test/resources/tck-test-files/schema/relationship.adoc index 256077b1..13f123b6 100644 --- a/core/src/test/resources/tck-test-files/schema/relationship.adoc +++ b/core/src/test/resources/tck-test-files/schema/relationship.adoc @@ -24,7 +24,8 @@ type Movie { [source,json,schema-config=true] ---- { - "queryOptionStyle": "INPUT_TYPE" + "queryOptionStyle": "INPUT_TYPE", + "useWhereFilter": true } ---- @@ -43,7 +44,7 @@ type Actor { } type Movie { - actors(filter: _ActorFilter, options: ActorOptions): [Actor]! @relation(direction : IN, from : "from", name : "ACTED_IN", to : "to") + actors(options: ActorOptions, where: ActorWhere): [Actor]! @relation(direction : IN, from : "from", name : "ACTED_IN", to : "to") id: ID } @@ -55,8 +56,8 @@ type Mutation { } type Query { - actor(filter: _ActorFilter, name: String, options: ActorOptions): [Actor!]! - movie(filter: _MovieFilter, id: ID, options: MovieOptions): [Movie!]! + actor(options: ActorOptions, where: ActorWhere): [Actor!]! + movie(options: MovieOptions, where: MovieWhere): [Movie!]! } type _Neo4jDate { @@ -187,24 +188,10 @@ input ActorSort { name: SortDirection } -input MovieOptions { - "Defines the maximum amount of records returned" - limit: Int - "Defines the amount of records to be skipped" - skip: Int - "Specify one or more MovieSort objects to sort Movies by. The sorts will be applied in the order in which they are arranged in the array." - sort: [MovieSort!] -} - -"Fields to sort Movies by. The order in which sorts are applied is not guaranteed when specifying many fields in one MovieSort object." -input MovieSort { - id: SortDirection -} - -input _ActorFilter { - AND: [_ActorFilter!] - NOT: [_ActorFilter!] - OR: [_ActorFilter!] +input ActorWhere { + AND: [ActorWhere!] + NOT: [ActorWhere!] + OR: [ActorWhere!] name: String name_contains: String name_ends_with: String @@ -222,26 +209,36 @@ input _ActorFilter { name_starts_with: String } -input _ActorInput { - name: String +input MovieOptions { + "Defines the maximum amount of records returned" + limit: Int + "Defines the amount of records to be skipped" + skip: Int + "Specify one or more MovieSort objects to sort Movies by. The sorts will be applied in the order in which they are arranged in the array." + sort: [MovieSort!] } -input _MovieFilter { - AND: [_MovieFilter!] - NOT: [_MovieFilter!] - OR: [_MovieFilter!] +"Fields to sort Movies by. The order in which sorts are applied is not guaranteed when specifying many fields in one MovieSort object." +input MovieSort { + id: SortDirection +} + +input MovieWhere { + AND: [MovieWhere!] + NOT: [MovieWhere!] + OR: [MovieWhere!] "Filters only those `Movie` for which all `actors`-relationship matches this filter. If `null` is passed to this field, only those `Movie` will be filtered which has no `actors`-relations" - actors: _ActorFilter + actors: ActorWhere "Filters only those `Movie` for which all `actors`-relationships matches this filter" - actors_every: _ActorFilter + actors_every: ActorWhere "Filters only those `Movie` for which none of the `actors`-relationships matches this filter" - actors_none: _ActorFilter + actors_none: ActorWhere "Filters only those `Movie` for which all `actors`-relationship does not match this filter. If `null` is passed to this field, only those `Movie` will be filtered which has any `actors`-relation" - actors_not: _ActorFilter + actors_not: ActorWhere "Filters only those `Movie` for which exactly one `actors`-relationship matches this filter" - actors_single: _ActorFilter + actors_single: ActorWhere "Filters only those `Movie` for which at least one `actors`-relationship matches this filter" - actors_some: _ActorFilter + actors_some: ActorWhere id: ID id_contains: ID id_ends_with: ID @@ -259,10 +256,6 @@ input _MovieFilter { id_starts_with: ID } -input _MovieInput { - id: ID -} - input _Neo4jDateInput { day: Int formatted: String @@ -330,7 +323,18 @@ input _Neo4jTimeInput { } directive @relation(name:String, direction: RelationDirection = OUT, from: String = "from", to: String = "to") on FIELD_DEFINITION | OBJECT -directive @cypher(statement:String, passThrough: Boolean = false) on FIELD_DEFINITION + +directive @cypher( + + # a cypher statement fields or top level queries and mutations. The current node is passed to the statement as `this` + statement:String, + + # if true, passes the sole responsibility for the nested query result for the field to your Cypher query. + # You will have to provide all data/structure required by client queries. + # Otherwise, we assume if you return object-types that you will return the appropriate nodes from your statement. + passThrough: Boolean = false +) on FIELD_DEFINITION + directive @property(name:String) on FIELD_DEFINITION directive @dynamic(prefix:String = "properties.") on FIELD_DEFINITION diff --git a/core/src/test/resources/tck-test-files/schema/simple.adoc b/core/src/test/resources/tck-test-files/schema/simple.adoc index fbbd15df..c34bbd08 100644 --- a/core/src/test/resources/tck-test-files/schema/simple.adoc +++ b/core/src/test/resources/tck-test-files/schema/simple.adoc @@ -22,7 +22,8 @@ type Movie { [source,json,schema-config=true] ---- { - "queryOptionStyle": "INPUT_TYPE" + "queryOptionStyle": "INPUT_TYPE", + "useWhereFilter": true } ---- @@ -52,7 +53,7 @@ type Mutation { } type Query { - movie(actorCount: Int, averageRating: Float, filter: _MovieFilter, id: ID, isActive: Boolean, options: MovieOptions): [Movie!]! + movie(options: MovieOptions, where: MovieWhere): [Movie!]! } type _Neo4jDate { @@ -186,10 +187,10 @@ input MovieSort { isActive: SortDirection } -input _MovieFilter { - AND: [_MovieFilter!] - NOT: [_MovieFilter!] - OR: [_MovieFilter!] +input MovieWhere { + AND: [MovieWhere!] + NOT: [MovieWhere!] + OR: [MovieWhere!] actorCount: Int actorCount_gt: Int actorCount_gte: Int @@ -225,13 +226,6 @@ input _MovieFilter { isActive_not: Boolean } -input _MovieInput { - actorCount: Int - averageRating: Float - id: ID - isActive: Boolean -} - input _Neo4jDateInput { day: Int formatted: String @@ -299,7 +293,18 @@ input _Neo4jTimeInput { } directive @relation(name:String, direction: RelationDirection = OUT, from: String = "from", to: String = "to") on FIELD_DEFINITION | OBJECT -directive @cypher(statement:String) on FIELD_DEFINITION + +directive @cypher( + + # a cypher statement fields or top level queries and mutations. The current node is passed to the statement as `this` + statement:String, + + # if true, passes the sole responsibility for the nested query result for the field to your Cypher query. + # You will have to provide all data/structure required by client queries. + # Otherwise, we assume if you return object-types that you will return the appropriate nodes from your statement. + passThrough: Boolean = false +) on FIELD_DEFINITION + directive @property(name:String) on FIELD_DEFINITION directive @dynamic(prefix:String = "properties.") on FIELD_DEFINITION