Skip to content

Commit 17d3190

Browse files
committed
Separate sorting and paging into own input type
This feature is part of migrating our API to the one generated by `@neo4j/graphql`. Here we move the fields for paging and sorting into a options input type, which can be passed to queries and projected relational fields resolves #196
1 parent bc88551 commit 17d3190

File tree

18 files changed

+1699
-55
lines changed

18 files changed

+1699
-55
lines changed

core/src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.neo4j.graphql
22

33
import graphql.Scalars
44
import graphql.schema.*
5+
import org.neo4j.graphql.handler.projection.ProjectionBase
56

67
class BuildingEnv(
78
val types: MutableMap<String, GraphQLNamedType>,
@@ -129,6 +130,61 @@ class BuildingEnv(
129130
))
130131
}
131132

133+
fun addOptions(type: GraphQLFieldsContainer): String {
134+
val optionsName = "${type.name}Options"
135+
val optionsType = types[optionsName]
136+
if (optionsType != null) {
137+
return (optionsType as? GraphQLInputType)?.requiredName()
138+
?: throw IllegalStateException("Ordering type $type.name is already defined but not an input type")
139+
}
140+
val sortTypeName = addSortInputType(type)
141+
val optionsTypeBuilder = GraphQLInputObjectType.newInputObject().name(optionsName)
142+
if (sortTypeName != null) {
143+
optionsTypeBuilder.field(GraphQLInputObjectField.newInputObjectField()
144+
.name(ProjectionBase.SORT)
145+
.type(GraphQLList(GraphQLNonNull(GraphQLTypeReference(sortTypeName))))
146+
.description("Specify one or more $sortTypeName objects to sort ${type.name}s by. The sorts will be applied in the order in which they are arranged in the array.")
147+
.build())
148+
}
149+
optionsTypeBuilder.field(GraphQLInputObjectField.newInputObjectField()
150+
.name(ProjectionBase.LIMIT)
151+
.type(Scalars.GraphQLInt)
152+
.description("Defines the maximum amount of records returned")
153+
.build())
154+
.field(GraphQLInputObjectField.newInputObjectField()
155+
.name(ProjectionBase.SKIP)
156+
.type(Scalars.GraphQLInt)
157+
.description("Defines the amount of records to be skipped")
158+
.build())
159+
.build()
160+
types[optionsName] = optionsTypeBuilder.build()
161+
return optionsName
162+
}
163+
164+
private fun addSortInputType(type: GraphQLFieldsContainer): String? {
165+
val sortTypeName = "${type.name}Sort"
166+
val sortType = types[sortTypeName]
167+
if (sortType != null) {
168+
return (sortType as? GraphQLInputType)?.requiredName()
169+
?: throw IllegalStateException("Ordering type $type.name is already defined but not an input type")
170+
}
171+
val relevantFields = type.relevantFields()
172+
if (relevantFields.isEmpty()){
173+
return null
174+
}
175+
val builder = GraphQLInputObjectType.newInputObject()
176+
.name(sortTypeName)
177+
.description("Fields to sort ${type.name}s by. The order in which sorts are applied is not guaranteed when specifying many fields in one MovieSort object.")
178+
for (relevantField in relevantFields) {
179+
builder.field(GraphQLInputObjectField.newInputObjectField()
180+
.name(relevantField.name)
181+
.type(GraphQLTypeReference("SortDirection"))
182+
.build())
183+
}
184+
types[sortTypeName] = builder.build()
185+
return sortTypeName
186+
}
187+
132188
fun addOrdering(type: GraphQLFieldsContainer): String? {
133189
val orderingName = "_${type.name}Ordering"
134190
var existingOrderingType = types[orderingName]

core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -186,21 +186,30 @@ object SchemaBuilder {
186186
return@transform
187187
}
188188

189-
if (fd.getArgument(ProjectionBase.FIRST) == null) {
190-
fieldBuilder.argument { a -> a.name(ProjectionBase.FIRST).type(Scalars.GraphQLInt) }
191-
}
192-
if (fd.getArgument(ProjectionBase.OFFSET) == null) {
193-
fieldBuilder.argument { a -> a.name(ProjectionBase.OFFSET).type(Scalars.GraphQLInt) }
194-
}
195-
196189
val fieldType = fd.type.inner() as? GraphQLFieldsContainer ?: return@transform
197190

198-
if (fd.getArgument(ProjectionBase.ORDER_BY) == null) {
199-
env.addOrdering(fieldType)?.let { orderingTypeName ->
200-
val orderType = GraphQLList(GraphQLNonNull(GraphQLTypeReference(orderingTypeName)))
201-
fieldBuilder.argument { a -> a.name(ProjectionBase.ORDER_BY).type(orderType) }
191+
if (schemaConfig.queryOptionStyle == SchemaConfig.InputStyle.INPUT_TYPE){
192+
193+
val optionsTypeName = env.addOptions(fieldType)
194+
val optionsType = GraphQLTypeReference(optionsTypeName)
195+
fieldBuilder.argument(input(ProjectionBase.OPTIONS, optionsType))
196+
197+
} else {
202198

199+
if (fd.getArgument(ProjectionBase.FIRST) == null) {
200+
fieldBuilder.argument { a -> a.name(ProjectionBase.FIRST).type(Scalars.GraphQLInt) }
203201
}
202+
if (fd.getArgument(ProjectionBase.OFFSET) == null) {
203+
fieldBuilder.argument { a -> a.name(ProjectionBase.OFFSET).type(Scalars.GraphQLInt) }
204+
}
205+
if (fd.getArgument(ProjectionBase.ORDER_BY) == null) {
206+
env.addOrdering(fieldType)?.let { orderingTypeName ->
207+
val orderType = GraphQLList(GraphQLNonNull(GraphQLTypeReference(orderingTypeName)))
208+
fieldBuilder.argument { a -> a.name(ProjectionBase.ORDER_BY).type(orderType) }
209+
210+
}
211+
}
212+
204213
}
205214

206215
if (schemaConfig.query.enabled && !schemaConfig.query.exclude.contains(fieldType.name) && fd.getArgument(ProjectionBase.FILTER) == null) {

core/src/main/kotlin/org/neo4j/graphql/SchemaConfig.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,24 @@ data class SchemaConfig @JvmOverloads constructor(
66
/**
77
* if true, the top level fields of the Query-type will be capitalized
88
*/
9-
val capitalizeQueryFields: Boolean = false
9+
val capitalizeQueryFields: Boolean = false,
10+
/**
11+
* Defines the way the input for queries and mutations are generated
12+
*/
13+
val queryOptionStyle: InputStyle = InputStyle.ARGUMENT_PER_FIELD,
1014
) {
1115
data class CRUDConfig(val enabled: Boolean = true, val exclude: List<String> = emptyList())
16+
17+
enum class InputStyle {
18+
/**
19+
* Separate arguments are generated for the query and / or mutation fields
20+
*/
21+
@Deprecated(message = "Will be removed in the next major release", replaceWith = ReplaceWith(expression = "INPUT_TYPE"))
22+
ARGUMENT_PER_FIELD,
23+
24+
/**
25+
* All fields are encapsulated into an input type used as one argument in query and / or mutation fields
26+
*/
27+
INPUT_TYPE,
28+
}
1229
}

core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcherForContainer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ abstract class BaseDataFetcherForContainer(
2323
init {
2424
fieldDefinition
2525
.arguments
26-
.filterNot { listOf(FIRST, OFFSET, ORDER_BY, NATIVE_ID).contains(it.name) }
26+
.filterNot { listOf(FIRST, OFFSET, ORDER_BY, NATIVE_ID, OPTIONS).contains(it.name) }
2727
.onEach { arg ->
2828
if (arg.defaultValue != null) {
2929
defaultFields[arg.name] = arg.defaultValue

core/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class CypherDirectiveHandler(
5050
} else {
5151
query.returning(node.`as`(field.aliasOrName()))
5252
}
53-
val ordering = orderBy(node, field.arguments, fieldDefinition)
53+
val ordering = orderBy(node, field.arguments, fieldDefinition, env.variables)
5454
val skipLimit = SkipLimit(variable, field.arguments, fieldDefinition)
5555

5656
val resultWithSkipLimit = readingWithWhere

core/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,28 @@ class QueryHandler private constructor(
2828
// TODO not just generate the input type but use it as well
2929
buildingEnv.addInputType("_${typeName}Input", type.relevantFields())
3030
val filterTypeName = buildingEnv.addFilterType(type)
31-
val orderingTypeName = buildingEnv.addOrdering(type)
31+
3232
val builder = GraphQLFieldDefinition
3333
.newFieldDefinition()
3434
.name(if (schemaConfig.capitalizeQueryFields) typeName else typeName.decapitalize())
3535
.arguments(buildingEnv.getInputValueDefinitions(relevantFields) { true })
3636
.argument(input(FILTER, GraphQLTypeReference(filterTypeName)))
37-
.argument(input(FIRST, Scalars.GraphQLInt))
38-
.argument(input(OFFSET, Scalars.GraphQLInt))
3937
.type(GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLTypeReference(type.name)))))
40-
if (orderingTypeName != null) {
41-
val orderType = GraphQLList(GraphQLNonNull(GraphQLTypeReference(orderingTypeName)))
42-
builder.argument(input(ORDER_BY, orderType))
38+
39+
if (schemaConfig.queryOptionStyle == SchemaConfig.InputStyle.INPUT_TYPE) {
40+
val optionsTypeName = buildingEnv.addOptions(type)
41+
val optionsType = GraphQLTypeReference(optionsTypeName)
42+
builder.argument(input(OPTIONS, optionsType))
43+
} else {
44+
builder
45+
.argument(input(FIRST, Scalars.GraphQLInt))
46+
.argument(input(OFFSET, Scalars.GraphQLInt))
47+
48+
val orderingTypeName = buildingEnv.addOrdering(type)
49+
if (orderingTypeName != null) {
50+
val orderType = GraphQLList(GraphQLNonNull(GraphQLTypeReference(orderingTypeName)))
51+
builder.argument(input(ORDER_BY, orderType))
52+
}
4353
}
4454
val def = builder.build()
4555
buildingEnv.addQueryField(def)
@@ -100,7 +110,7 @@ class QueryHandler private constructor(
100110
match.where(where)
101111
}
102112

103-
val ordering = orderBy(propertyContainer, field.arguments, fieldDefinition)
113+
val ordering = orderBy(propertyContainer, field.arguments, fieldDefinition, env.variables)
104114
val skipLimit = SkipLimit(variable, field.arguments, fieldDefinition)
105115

106116
val projectionEntries = projectFields(propertyContainer, field, type, env)

core/src/main/kotlin/org/neo4j/graphql/handler/filter/OptimizedFilterHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class OptimizedFilterHandler(val type: GraphQLFieldsContainer) : ProjectionBase(
4141

4242
var ongoingReading: OngoingReading? = null
4343

44-
val filteredArguments = field.arguments.filterNot { setOf(FIRST, OFFSET, ORDER_BY, FILTER).contains(it.name) }
44+
val filteredArguments = field.arguments.filterNot { SPECIAL_FIELDS.contains(it.name) }
4545
if (filteredArguments.isNotEmpty()) {
4646
val parsedQuery = QueryParser.parseArguments(filteredArguments, fieldDefinition, type, variables)
4747
val condition = handleQuery(variable, "", rootNode, parsedQuery, type, variables)

core/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,35 @@ import org.neo4j.graphql.parser.QueryParser.parseFilter
1919
*/
2020
open class ProjectionBase {
2121
companion object {
22+
/*
23+
* old arguments, subject to be removed in future releases
24+
*/
25+
2226
const val NATIVE_ID = "_id"
2327
const val ORDER_BY = "orderBy"
2428
const val FIRST = "first"
2529
const val OFFSET = "offset"
2630
const val FILTER = "filter"
2731

32+
/*
33+
* new arguments compatible with @neo4j/graphql
34+
*/
35+
36+
const val OPTIONS = "options"
37+
const val LIMIT = "limit"
38+
const val SKIP = "skip"
39+
const val SORT = "sort"
40+
2841
const val TYPE_NAME = "__typename"
42+
43+
/**
44+
* Fields with special treatments
45+
*/
46+
val SPECIAL_FIELDS = setOf(FIRST, OFFSET, ORDER_BY, FILTER, OPTIONS)
2947
}
3048

31-
fun orderBy(node: PropertyContainer, args: MutableList<Argument>, fieldDefinition: GraphQLFieldDefinition?): List<SortItem>? {
32-
val values = getOrderByArgs(args, fieldDefinition)
49+
fun orderBy(node: PropertyContainer, args: MutableList<Argument>, fieldDefinition: GraphQLFieldDefinition?, variables: Map<String, Any>): List<SortItem>? {
50+
val values = getOrderByArgs(args, fieldDefinition, variables)
3351
if (values.isEmpty()) {
3452
return null
3553
}
@@ -40,24 +58,38 @@ open class ProjectionBase {
4058
}
4159
}
4260

43-
private fun getOrderByArgs(args: MutableList<Argument>, fieldDefinition: GraphQLFieldDefinition?): List<Pair<String, Sort>> {
44-
val orderBy = args.find { it.name == ORDER_BY }?.value
45-
?: fieldDefinition?.getArgument(ORDER_BY)?.defaultValue?.asGraphQLValue()
46-
return orderBy
47-
?.let { it ->
48-
when (it) {
49-
is ArrayValue -> it.values.map { it.toJavaValue().toString() }
50-
is EnumValue -> listOf(it.name)
51-
is StringValue -> listOf(it.value)
52-
else -> null
61+
private fun getOrderByArgs(args: MutableList<Argument>, fieldDefinition: GraphQLFieldDefinition?, variables: Map<String, Any>): List<Pair<String, Sort>> {
62+
val options = args.find { it.name == OPTIONS }?.value as? ObjectValue
63+
val defaultOptions = (fieldDefinition?.getArgument(OPTIONS)?.type as? GraphQLInputObjectType)
64+
return if (options != null || defaultOptions != null) {
65+
val sortArray = (options?.objectFields?.find { it.name == SORT }?.value
66+
?.let { value -> (value as? VariableReference)?.let { variables[it.name] } ?: value }?.toJavaValue()
67+
?: defaultOptions?.getField(SORT)?.defaultValue?.toJavaValue()
68+
) as? List<*> ?: return emptyList()
69+
sortArray
70+
.mapNotNull { it as? Map<*, *> }
71+
.flatMap { it.entries }
72+
.filter { (key, sort) -> key is String && sort is String }
73+
.map { (key, sort) -> key as String to Sort.valueOf(sort as String) }
74+
} else {
75+
val orderBy = args.find { it.name == ORDER_BY }?.value
76+
?: fieldDefinition?.getArgument(ORDER_BY)?.defaultValue?.asGraphQLValue()
77+
orderBy
78+
?.let { it ->
79+
when (it) {
80+
is ArrayValue -> it.values.map { it.toJavaValue().toString() }
81+
is EnumValue -> listOf(it.name)
82+
is StringValue -> listOf(it.value)
83+
else -> null
84+
}
5385
}
54-
}
55-
?.map {
56-
val index = it.lastIndexOf('_')
57-
val property = it.substring(0, index)
58-
val direction = Sort.valueOf(it.substring(index + 1).toUpperCase())
59-
property to direction
60-
} ?: emptyList()
86+
?.map {
87+
val index = it.lastIndexOf('_')
88+
val property = it.substring(0, index)
89+
val direction = Sort.valueOf(it.substring(index + 1).toUpperCase())
90+
property to direction
91+
} ?: emptyList()
92+
}
6193
}
6294

6395
fun where(
@@ -69,7 +101,7 @@ open class ProjectionBase {
69101
): Condition {
70102
val variable = propertyContainer.requiredSymbolicName.value
71103

72-
val filteredArguments = field.arguments.filterNot { setOf(FIRST, OFFSET, ORDER_BY, FILTER).contains(it.name) }
104+
val filteredArguments = field.arguments.filterNot { SPECIAL_FIELDS.contains(it.name) }
73105

74106
val parsedQuery = parseArguments(filteredArguments, fieldDefinition, type, variables)
75107
val result = handleQuery(variable, "", propertyContainer, parsedQuery, type, variables)
@@ -406,7 +438,7 @@ open class ProjectionBase {
406438
}
407439

408440
val skipLimit = SkipLimit(childVariable, field.arguments, fieldDefinition)
409-
val orderBy = getOrderByArgs(field.arguments, fieldDefinition)
441+
val orderBy = getOrderByArgs(field.arguments, fieldDefinition, env.variables)
410442
val sortByNeo4jTypeFields = orderBy
411443
.filter { (property, _) -> nodeType.getFieldDefinition(property)?.isNeo4jType() == true }
412444
.map { (property, _) -> property }
@@ -441,12 +473,22 @@ open class ProjectionBase {
441473
return skipLimit.slice(fieldType.isList(), comprehension)
442474
}
443475

444-
class SkipLimit(variable: String,
445-
arguments: List<Argument>,
446-
fieldDefinition: GraphQLFieldDefinition?,
447-
private val skip: Parameter<*>? = convertArgument(variable, arguments, fieldDefinition, OFFSET),
448-
private val limit: Parameter<*>? = convertArgument(variable, arguments, fieldDefinition, FIRST)) {
476+
class SkipLimit(variable: String, arguments: List<Argument>, fieldDefinition: GraphQLFieldDefinition?) {
449477

478+
private val skip: Parameter<*>?
479+
private val limit: Parameter<*>?
480+
481+
init {
482+
val options = arguments.find { it.name == OPTIONS }?.value as? ObjectValue
483+
val defaultOptions = (fieldDefinition?.getArgument(OPTIONS)?.type as? GraphQLInputObjectType)
484+
if (options != null || defaultOptions != null) {
485+
this.skip = convertOptionField(variable, options, defaultOptions, SKIP)
486+
this.limit = convertOptionField(variable, options, defaultOptions, LIMIT)
487+
} else {
488+
this.skip = convertArgument(variable, arguments, fieldDefinition, OFFSET)
489+
this.limit = convertArgument(variable, arguments, fieldDefinition, FIRST)
490+
}
491+
}
450492

451493
fun <T> format(returning: T): StatementBuilder.BuildableStatement where T : TerminalExposesSkip, T : TerminalExposesLimit {
452494
val result = skip?.let { returning.skip(it) } ?: returning
@@ -470,6 +512,13 @@ open class ProjectionBase {
470512
?: return null
471513
return queryParameter(value, variable, name)
472514
}
515+
516+
private fun convertOptionField(variable: String, options: ObjectValue?, defaultOptions: GraphQLInputObjectType?, name: String): Parameter<*>? {
517+
val value = options?.objectFields?.find { it.name == name }?.value
518+
?: defaultOptions?.getField(name)?.defaultValue
519+
?: return null
520+
return queryParameter(value, variable, name)
521+
}
473522
}
474523
}
475524

core/src/main/kotlin/org/neo4j/graphql/parser/QueryParser.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ object QueryParser {
125125
fieldDefinition.arguments
126126
.filter { it.defaultValue != null }
127127
.filterNot { queriedFields.containsKey(it.name) }
128-
.filterNot { setOf(ProjectionBase.FIRST, ProjectionBase.OFFSET, ProjectionBase.ORDER_BY, ProjectionBase.FILTER).contains(it.name) }
128+
.filterNot { ProjectionBase.SPECIAL_FIELDS.contains(it.name) }
129129
.forEach { argument ->
130130
queriedFields[argument.name] = index++ to ObjectField(argument.name, argument.defaultValue.asGraphQLValue())
131131
}

core/src/main/resources/neo4j_types.graphql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ enum RelationDirection {
44
BOTH
55
}
66

7+
enum SortDirection {
8+
"""Sort by field values in ascending order."""
9+
ASC
10+
"""Sort by field values in descending order."""
11+
DESC
12+
}
13+
714
scalar DynamicProperties
815

916
type _Neo4jTime {

0 commit comments

Comments
 (0)