Skip to content

Commit 54a483c

Browse files
authored
Use scalars for temporal types (#235)
With these changes a new configuration param `useTemporalScalars` was introduced. With this configuration enabled, neo4js' temporales like `Date`, `Time`, `LocalTime`, `DateTime` and `LocalDateTime` are used as scalars (strings). resolves #223 resolves #109 resolves #108 resolves #93
1 parent 5ddd00e commit 54a483c

File tree

12 files changed

+477
-79
lines changed

12 files changed

+477
-79
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,12 @@ fun GraphQLType.requiredName(): String = requireNotNull(name()) { "name is requi
4343

4444
fun GraphQLType.isList() = this is GraphQLList || (this is GraphQLNonNull && this.wrappedType is GraphQLList)
4545
fun GraphQLType.isNeo4jType() = this.innerName().startsWith("_Neo4j")
46+
fun GraphQLType.isNeo4jTemporalType() = NEO4j_TEMPORAL_TYPES.contains(this.innerName())
4647

47-
fun GraphQLType.isNeo4jSpatialType() = this.innerName().startsWith("_Neo4jPoint")
4848
fun TypeDefinition<*>.isNeo4jSpatialType() = this.name.startsWith("_Neo4jPoint")
4949

5050
fun GraphQLFieldDefinition.isNeo4jType(): Boolean = this.type.isNeo4jType()
51+
fun GraphQLFieldDefinition.isNeo4jTemporalType(): Boolean = this.type.isNeo4jTemporalType()
5152

5253
fun GraphQLFieldDefinition.isRelationship() = !type.isNeo4jType() && this.type.inner().let { it is GraphQLFieldsContainer }
5354

@@ -184,7 +185,7 @@ fun Value<*>.toJavaValue(): Any? = when (this) {
184185

185186
fun GraphQLFieldDefinition.isID() = this.type.inner() == Scalars.GraphQLID
186187
fun GraphQLFieldDefinition.isNativeId() = this.name == ProjectionBase.NATIVE_ID
187-
fun GraphQLFieldDefinition.isIgnored() = getDirective(DirectiveConstants.IGNORE) != null
188+
fun GraphQLFieldDefinition.isIgnored() = getDirective(DirectiveConstants.IGNORE) != null
188189
fun FieldDefinition.isIgnored(): Boolean = hasDirective(DirectiveConstants.IGNORE)
189190

190191
fun GraphQLFieldsContainer.getIdField() = this.getRelevantFieldDefinitions().find { it.isID() }

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

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ data class TypeDefinition(
1818
val inputDefinition: String = typeDefinition + "Input"
1919
)
2020

21+
class Neo4jTemporalConverter(name: String) : Neo4jSimpleConverter(name) {
22+
override fun projectField(variable: SymbolicName, field: Field, name: String): Any {
23+
return Cypher.call("toString").withArgs(variable.property(field.name)).asFunction()
24+
}
25+
26+
override fun createCondition(property: Property, parameter: Parameter<Any>, conditionCreator: (Expression, Expression) -> Condition): Condition {
27+
return conditionCreator(property, toExpression(parameter))
28+
}
29+
}
30+
2131
class Neo4jTimeConverter(name: String) : Neo4jConverter(name) {
2232

2333
override fun createCondition(
@@ -63,23 +73,30 @@ class Neo4jPointConverter(name: String) : Neo4jConverter(name) {
6373
}
6474

6575
open class Neo4jConverter(
66-
val name: String,
76+
name: String,
6777
val prefixedName: String = "_Neo4j$name",
6878
val typeDefinition: TypeDefinition = TypeDefinition(name, prefixedName)
69-
) {
79+
) : Neo4jSimpleConverter(name) {
80+
}
7081

82+
open class Neo4jSimpleConverter(val name: String) {
7183
protected fun toExpression(parameter: Expression): Expression {
7284
return Cypher.call(name.toLowerCase()).withArgs(parameter).asFunction()
7385
}
7486

87+
open fun createCondition(
88+
property: Property,
89+
parameter: Parameter<Any>,
90+
conditionCreator: (Expression, Expression) -> Condition
91+
): Condition = conditionCreator(property, parameter)
92+
7593
open fun createCondition(
7694
objectField: ObjectField,
7795
field: GraphQLFieldDefinition,
7896
parameter: Parameter<Any>,
7997
conditionCreator: (Expression, Expression) -> Condition,
8098
propertyContainer: PropertyContainer
81-
): Condition = conditionCreator(propertyContainer.property(field.name, objectField.name), parameter)
82-
99+
): Condition = createCondition(propertyContainer.property(field.name, objectField.name), parameter, conditionCreator)
83100

84101
open fun projectField(variable: SymbolicName, field: Field, name: String): Any = variable.property(field.name, name)
85102

@@ -89,10 +106,10 @@ open class Neo4jConverter(
89106
}
90107
}
91108

92-
fun getNeo4jTypeConverter(field: GraphQLFieldDefinition): Neo4jConverter = getNeo4jTypeConverter(field.type.innerName())
109+
fun getNeo4jTypeConverter(field: GraphQLFieldDefinition): Neo4jSimpleConverter = getNeo4jTypeConverter(field.type.innerName())
93110

94-
fun getNeo4jTypeConverter(name: String): Neo4jConverter =
95-
neo4jConverter[name] ?: throw RuntimeException("Type $name not found")
111+
private fun getNeo4jTypeConverter(name: String): Neo4jSimpleConverter =
112+
neo4jConverter[name] ?: neo4jScalarConverter[name] ?: throw RuntimeException("Type $name not found")
96113

97114
private val neo4jConverter = listOf(
98115
Neo4jTimeConverter("LocalTime"),
@@ -105,4 +122,16 @@ private val neo4jConverter = listOf(
105122
.map { it.prefixedName to it }
106123
.toMap()
107124

125+
private val neo4jScalarConverter = listOf(
126+
Neo4jTemporalConverter("LocalTime"),
127+
Neo4jTemporalConverter("Date"),
128+
Neo4jTemporalConverter("DateTime"),
129+
Neo4jTemporalConverter("Time"),
130+
Neo4jTemporalConverter("LocalDateTime")
131+
)
132+
.map { it.name to it }
133+
.toMap()
134+
135+
val NEO4j_TEMPORAL_TYPES = neo4jScalarConverter.keys
136+
108137
val neo4jTypeDefinitions = neo4jConverter.values.map { it.typeDefinition }

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

Lines changed: 50 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -14,41 +14,53 @@ typealias CypherDSL = org.neo4j.cypherdsl.core.Cypher
1414

1515
enum class FieldOperator(
1616
val suffix: String,
17-
val op: String,
1817
private val conditionCreator: (Expression, Expression) -> Condition,
1918
val not: Boolean = false,
2019
val requireParam: Boolean = true,
21-
val distance: Boolean = false
20+
val distance: Boolean = false,
21+
val list: Boolean = false
2222
) {
23-
EQ("", "=", { lhs, rhs -> lhs.isEqualTo(rhs) }),
24-
IS_NULL("", "", { lhs, _ -> lhs.isNull }, requireParam = false),
25-
IS_NOT_NULL("_not", "", { lhs, _ -> lhs.isNotNull }, true, requireParam = false),
26-
NEQ("_not", "=", { lhs, rhs -> lhs.isEqualTo(rhs).not() }, true),
27-
GTE("_gte", ">=", { lhs, rhs -> lhs.gte(rhs) }),
28-
GT("_gt", ">", { lhs, rhs -> lhs.gt(rhs) }),
29-
LTE("_lte", "<=", { lhs, rhs -> lhs.lte(rhs) }),
30-
LT("_lt", "<", { lhs, rhs -> lhs.lt(rhs) }),
31-
32-
NIN("_not_in", "IN", { lhs, rhs -> lhs.`in`(rhs).not() }, true),
33-
IN("_in", "IN", { lhs, rhs -> lhs.`in`(rhs) }),
34-
NC("_not_contains", "CONTAINS", { lhs, rhs -> lhs.contains(rhs).not() }, true),
35-
NSW("_not_starts_with", "STARTS WITH", { lhs, rhs -> lhs.startsWith(rhs).not() }, true),
36-
NEW("_not_ends_with", "ENDS WITH", { lhs, rhs -> lhs.endsWith(rhs).not() }, true),
37-
C("_contains", "CONTAINS", { lhs, rhs -> lhs.contains(rhs) }),
38-
SW("_starts_with", "STARTS WITH", { lhs, rhs -> lhs.startsWith(rhs) }),
39-
EW("_ends_with", "ENDS WITH", { lhs, rhs -> lhs.endsWith(rhs) }),
40-
MATCHES("_matches", "=~", { lhs, rhs -> lhs.matches(rhs) }),
41-
42-
43-
DISTANCE(NEO4j_POINT_DISTANCE_FILTER_SUFFIX, "=", { lhs, rhs -> lhs.isEqualTo(rhs) }, distance = true),
44-
DISTANCE_LT(NEO4j_POINT_DISTANCE_FILTER_SUFFIX + "_lt", "<", { lhs, rhs -> lhs.lt(rhs) }, distance = true),
45-
DISTANCE_LTE(NEO4j_POINT_DISTANCE_FILTER_SUFFIX + "_lte", "<=", { lhs, rhs -> lhs.lte(rhs) }, distance = true),
46-
DISTANCE_GT(NEO4j_POINT_DISTANCE_FILTER_SUFFIX + "_gt", ">", { lhs, rhs -> lhs.gt(rhs) }, distance = true),
47-
DISTANCE_GTE(NEO4j_POINT_DISTANCE_FILTER_SUFFIX + "_gte", ">=", { lhs, rhs -> lhs.gte(rhs) }, distance = true);
48-
49-
val list = op == "IN"
50-
51-
fun resolveCondition(variablePrefix: String, queriedField: String, propertyContainer: PropertyContainer, field: GraphQLFieldDefinition?, value: Any, suffix: String? = null): List<Condition> {
23+
EQ("", { lhs, rhs -> lhs.isEqualTo(rhs) }),
24+
IS_NULL("", { lhs, _ -> lhs.isNull }, requireParam = false),
25+
IS_NOT_NULL("_not", { lhs, _ -> lhs.isNotNull }, true, requireParam = false),
26+
NEQ("_not", { lhs, rhs -> lhs.isEqualTo(rhs).not() }, not = true),
27+
GTE("_gte", { lhs, rhs -> lhs.gte(rhs) }),
28+
GT("_gt", { lhs, rhs -> lhs.gt(rhs) }),
29+
LTE("_lte", { lhs, rhs -> lhs.lte(rhs) }),
30+
LT("_lt", { lhs, rhs -> lhs.lt(rhs) }),
31+
32+
NIN("_not_in", { lhs, rhs -> lhs.`in`(rhs).not() }, not = true, list = true),
33+
IN("_in", { lhs, rhs -> lhs.`in`(rhs) }, list = true),
34+
NC("_not_contains", { lhs, rhs -> lhs.contains(rhs).not() }, not = true),
35+
NSW("_not_starts_with", { lhs, rhs -> lhs.startsWith(rhs).not() }, not = true),
36+
NEW("_not_ends_with", { lhs, rhs -> lhs.endsWith(rhs).not() }, not = true),
37+
C("_contains", { lhs, rhs -> lhs.contains(rhs) }),
38+
SW("_starts_with", { lhs, rhs -> lhs.startsWith(rhs) }),
39+
EW("_ends_with", { lhs, rhs -> lhs.endsWith(rhs) }),
40+
MATCHES("_matches", { lhs, rhs -> lhs.matches(rhs) }),
41+
42+
43+
DISTANCE(NEO4j_POINT_DISTANCE_FILTER_SUFFIX, { lhs, rhs -> lhs.isEqualTo(rhs) }, distance = true),
44+
DISTANCE_LT(NEO4j_POINT_DISTANCE_FILTER_SUFFIX + "_lt", { lhs, rhs -> lhs.lt(rhs) }, distance = true),
45+
DISTANCE_LTE(NEO4j_POINT_DISTANCE_FILTER_SUFFIX + "_lte", { lhs, rhs -> lhs.lte(rhs) }, distance = true),
46+
DISTANCE_GT(NEO4j_POINT_DISTANCE_FILTER_SUFFIX + "_gt", { lhs, rhs -> lhs.gt(rhs) }, distance = true),
47+
DISTANCE_GTE(NEO4j_POINT_DISTANCE_FILTER_SUFFIX + "_gte", { lhs, rhs -> lhs.gte(rhs) }, distance = true);
48+
49+
fun resolveCondition(
50+
variablePrefix: String,
51+
queriedField: String,
52+
propertyContainer: PropertyContainer,
53+
field: GraphQLFieldDefinition?,
54+
value: Any,
55+
schemaConfig: SchemaConfig,
56+
suffix: String? = null
57+
): List<Condition> {
58+
if (schemaConfig.useTemporalScalars && field?.type?.isNeo4jTemporalType() == true) {
59+
val neo4jTypeConverter = getNeo4jTypeConverter(field)
60+
val parameter = queryParameter(value, variablePrefix, queriedField, null, suffix)
61+
.withValue(value.toJavaValue())
62+
return listOf(neo4jTypeConverter.createCondition(propertyContainer.property(field.name), parameter, conditionCreator))
63+
}
5264
return if (field?.type?.isNeo4jType() == true && value is ObjectValue) {
5365
resolveNeo4jTypeConditions(variablePrefix, queriedField, propertyContainer, field, value, suffix)
5466
} else if (field?.isNativeId() == true) {
@@ -96,20 +108,6 @@ enum class FieldOperator(
96108

97109
companion object {
98110

99-
fun resolve(queriedField: String, field: GraphQLFieldDefinition, value: Any?): FieldOperator? {
100-
val fieldName = field.name
101-
if (value == null) {
102-
return listOf(IS_NULL, IS_NOT_NULL).find { queriedField == fieldName + it.suffix }
103-
}
104-
val ops = enumValues<FieldOperator>().filterNot { it == IS_NULL || it == IS_NOT_NULL }
105-
return ops.find { queriedField == fieldName + it.suffix }
106-
?: if (field.type.isNeo4jSpatialType()) {
107-
ops.find { queriedField == fieldName + NEO4j_POINT_DISTANCE_FILTER_SUFFIX + it.suffix }
108-
} else {
109-
null
110-
}
111-
}
112-
113111
fun forType(type: TypeDefinition<*>, isNeo4jType: Boolean): List<FieldOperator> =
114112
when {
115113
type.name == TypeBoolean.name -> listOf(EQ, NEQ)
@@ -128,17 +126,17 @@ enum class FieldOperator(
128126
fun fieldName(fieldName: String) = fieldName + suffix
129127
}
130128

131-
enum class RelationOperator(val suffix: String, val op: String) {
132-
SOME("_some", "ANY"),
129+
enum class RelationOperator(val suffix: String) {
130+
SOME("_some"),
133131

134-
EVERY("_every", "ALL"),
132+
EVERY("_every"),
135133

136-
SINGLE("_single", "SINGLE"),
137-
NONE("_none", "NONE"),
134+
SINGLE("_single"),
135+
NONE("_none"),
138136

139137
// `eq` if queried with an object, `not exists` if queried with null
140-
EQ_OR_NOT_EXISTS("", ""),
141-
NOT("_not", "");
138+
EQ_OR_NOT_EXISTS(""),
139+
NOT("_not");
142140

143141
fun fieldName(fieldName: String) = fieldName + suffix
144142

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ data class SchemaConfig @JvmOverloads constructor(
2424
* additionally the separated filter arguments will no longer be generated.
2525
*/
2626
val useWhereFilter: Boolean = false,
27+
28+
/**
29+
* if enabled the `Date`, `Time`, `LocalTime`, `DateTime` and `LocalDateTime` are used as scalars
30+
*/
31+
val useTemporalScalars: Boolean = false,
2732
) {
2833
data class CRUDConfig(val enabled: Boolean = true, val exclude: List<String> = emptyList())
2934

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ abstract class BaseDataFetcherForContainer(schemaConfig: SchemaConfig) : BaseDat
3535
val dynamicPrefix = field.dynamicPrefix()
3636
propertyFields[field.name] = when {
3737
dynamicPrefix != null -> dynamicPrefixCallback(field, dynamicPrefix)
38-
field.isNeo4jType() -> neo4jTypeCallback(field)
38+
field.isNeo4jType() || (schemaConfig.useTemporalScalars && field.isNeo4jTemporalType()) -> neo4jTypeCallback(field)
3939
else -> defaultCallback(field)
4040
}
4141
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class OptimizedFilterHandler(val type: GraphQLFieldsContainer, schemaConfig: Sch
6969
* @param value the value passed to the graphQL field
7070
* @param parentPassThroughWiths all the nodes, required to be passed through via WITH
7171
*/
72-
class NestingLevelHandler(
72+
inner class NestingLevelHandler(
7373
private val parsedQuery: ParsedQuery,
7474
private val useDistinct: Boolean,
7575
private val current: PropertyContainer,
@@ -113,7 +113,7 @@ class OptimizedFilterHandler(val type: GraphQLFieldsContainer, schemaConfig: Sch
113113
}
114114

115115
// WHERE MATCH all predicates for current
116-
val condition = parsedQuery.getFieldConditions(current, variablePrefix, "")
116+
val condition = parsedQuery.getFieldConditions(current, variablePrefix, "", schemaConfig)
117117
val matchQueryWithWhere = matchQueryWithoutWhere.where(condition)
118118

119119
return if (additionalConditions != null) {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ open class ProjectionBase(
139139
type: GraphQLFieldsContainer,
140140
variables: Map<String, Any>
141141
): Condition {
142-
var result = parsedQuery.getFieldConditions(propertyContainer, variablePrefix, variableSuffix)
142+
var result = parsedQuery.getFieldConditions(propertyContainer, variablePrefix, variableSuffix, schemaConfig)
143143

144144
for (predicate in parsedQuery.relationPredicates) {
145145
val objectField = predicate.queryField
@@ -269,6 +269,9 @@ open class ProjectionBase(
269269
}
270270

271271
} else when {
272+
schemaConfig.useTemporalScalars && fieldDefinition.isNeo4jTemporalType() -> {
273+
projections += getNeo4jTypeConverter(fieldDefinition).projectField(variable, field, "")
274+
}
272275
isObjectField -> {
273276
if (fieldDefinition.isNeo4jType()) {
274277
if (propertiesToSkipDeepProjection.contains(fieldDefinition.name)) {

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ class ParsedQuery(
2020
val and: List<Value<*>>? = null
2121
) {
2222

23-
fun getFieldConditions(propertyContainer: PropertyContainer, variablePrefix: String, variableSuffix: String): Condition =
23+
fun getFieldConditions(propertyContainer: PropertyContainer, variablePrefix: String, variableSuffix: String, schemaConfig: SchemaConfig): Condition =
2424
fieldPredicates
25-
.flatMap { it.createCondition(propertyContainer, variablePrefix, variableSuffix) }
25+
.flatMap { it.createCondition(propertyContainer, variablePrefix, variableSuffix, schemaConfig) }
2626
.reduceOrNull { result, condition -> result.and(condition) }
2727
?: Conditions.noCondition()
2828
}
@@ -43,13 +43,14 @@ class FieldPredicate(
4343
index: Int
4444
) : Predicate<FieldOperator>(op, queryField, normalizeName(fieldDefinition.name, op.suffix.toCamelCase()), index) {
4545

46-
fun createCondition(propertyContainer: PropertyContainer, variablePrefix: String, variableSuffix: String) =
46+
fun createCondition(propertyContainer: PropertyContainer, variablePrefix: String, variableSuffix: String, schemaConfig: SchemaConfig) =
4747
op.resolveCondition(
4848
variablePrefix,
4949
normalizedName,
5050
propertyContainer,
5151
fieldDefinition,
5252
queryField.value,
53+
schemaConfig,
5354
variableSuffix
5455
)
5556

core/src/main/resources/neo4j_types.graphql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,9 @@ type _Neo4jPoint{
110110
# * `9157`: represents CRS `cartesian-3d`
111111
srid: Int
112112
}
113+
114+
scalar Date
115+
scalar Time
116+
scalar LocalTime
117+
scalar DateTime
118+
scalar LocalDateTime

core/src/test/kotlin/org/neo4j/graphql/utils/GraphQLSchemaTestSuite.kt

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
package org.neo4j.graphql.utils
22

33
import graphql.language.InterfaceTypeDefinition
4+
import graphql.language.ScalarTypeDefinition
5+
import graphql.schema.GraphQLScalarType
46
import graphql.schema.GraphQLSchema
57
import graphql.schema.GraphQLType
68
import graphql.schema.diff.DiffSet
79
import graphql.schema.diff.SchemaDiff
810
import graphql.schema.diff.reporting.CapturingReporter
9-
import graphql.schema.idl.RuntimeWiring
10-
import graphql.schema.idl.SchemaGenerator
11-
import graphql.schema.idl.SchemaParser
12-
import graphql.schema.idl.SchemaPrinter
11+
import graphql.schema.idl.*
1312
import org.assertj.core.api.Assertions.assertThat
1413
import org.junit.jupiter.api.Assertions
1514
import org.junit.jupiter.api.Assumptions
1615
import org.junit.jupiter.api.DynamicNode
1716
import org.junit.jupiter.api.DynamicTest
18-
import org.neo4j.graphql.DynamicProperties
19-
import org.neo4j.graphql.SchemaBuilder
20-
import org.neo4j.graphql.SchemaConfig
21-
import org.neo4j.graphql.requiredName
17+
import org.neo4j.graphql.*
2218
import org.opentest4j.AssertionFailedError
2319
import java.util.*
2420
import java.util.regex.Pattern
@@ -51,9 +47,18 @@ class GraphQLSchemaTestSuite(fileName: String) : AsciiDocTestSuite(
5147
reg
5248
.getTypes(InterfaceTypeDefinition::class.java)
5349
.forEach { typeDefinition -> runtimeWiring.type(typeDefinition.name) { it.typeResolver { null } } }
54-
expectedSchema = schemaGenerator.makeExecutableSchema(reg, runtimeWiring
55-
.scalar(DynamicProperties.INSTANCE)
56-
.build())
50+
reg
51+
.scalars()
52+
.filterNot { entry -> ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.containsKey(entry.key) }
53+
.forEach { (name, definition) ->
54+
runtimeWiring.scalar(GraphQLScalarType.newScalar()
55+
.name(name)
56+
.definition(definition)
57+
.coercing(NoOpCoercing)
58+
.build()
59+
)
60+
}
61+
expectedSchema = schemaGenerator.makeExecutableSchema(reg, runtimeWiring.build())
5762

5863
diff(expectedSchema, augmentedSchema)
5964
diff(augmentedSchema, expectedSchema)

0 commit comments

Comments
 (0)