Skip to content

Commit

Permalink
Utilize graphql framework to handle fragments correctly
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Andy2003 committed Nov 16, 2021
1 parent 7e45a7a commit ab0f96e
Show file tree
Hide file tree
Showing 33 changed files with 358 additions and 378 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
9 changes: 4 additions & 5 deletions core/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ import graphql.schema.GraphQLOutputType
import org.neo4j.cypherdsl.core.*
import java.util.*

fun <T> Iterable<T>.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
Expand All @@ -22,7 +18,6 @@ fun Expression.collect(type: GraphQLOutputType) = if (type.isList()) Functions.c
fun StatementBuilder.OngoingReading.withSubQueries(subQueries: List<Statement>) = 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)
Expand All @@ -35,3 +30,7 @@ fun String.toCamelCase(): String = Regex("[\\W_]([a-z])").replace(this) { it.gro
fun <T> Optional<T>.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())
28 changes: 6 additions & 22 deletions core/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -64,7 +62,7 @@ fun GraphQLFieldsContainer.relationshipFor(name: String): RelationshipInfo<Graph
(this as? GraphQLDirectiveContainer)
?.getDirective(DirectiveConstants.RELATION)?.let {
// do inverse mapping, if the current type is the `to` mapping of the relation
it to (fieldObjectType.getRelevantFieldDefinition(it.getArgument(RELATION_TO, null))?.name == typeName)
it to (fieldObjectType.getRelevantFieldDefinition(it.getArgument(RELATION_TO, null as String?))?.name == typeName)
}
?: throw IllegalStateException("Type ${this.name} needs an @relation directive")
} else {
Expand Down Expand Up @@ -111,15 +109,16 @@ fun GraphQLType.ref(): GraphQLType = when (this) {
}

fun Field.aliasOrName(): String = (this.alias ?: this.name)
fun Field.contextualize(variable: String) = variable + this.aliasOrName().capitalize()
fun Field.contextualize(variable: SymbolicName) = variable.value + this.aliasOrName().capitalize()
fun SelectedField.aliasOrName(): String = (this.alias ?: this.name)
fun SelectedField.contextualize(variable: String) = variable + this.aliasOrName().capitalize()
fun SelectedField.contextualize(variable: SymbolicName) = variable.value + this.aliasOrName().capitalize()

fun GraphQLType.innerName(): String = inner().name()
?: throw IllegalStateException("inner name cannot be retrieved for " + this.javaClass)

fun GraphQLFieldDefinition.propertyName() = getDirectiveArgument(PROPERTY, PROPERTY_NAME, this.name)!!

fun GraphQLFieldDefinition.dynamicPrefix(): String? = getDirectiveArgument(DYNAMIC, DYNAMIC_PREFIX, null)
fun GraphQLFieldDefinition.dynamicPrefix(): String? = getDirectiveArgument(DYNAMIC, DYNAMIC_PREFIX, null as String?)
fun GraphQLType.getInnerFieldsContainer() = inner() as? GraphQLFieldsContainer
?: throw IllegalArgumentException("${this.innerName()} is neither an object nor an interface")

Expand Down Expand Up @@ -168,7 +167,7 @@ fun GraphQLFieldDefinition.cypherDirective(): CypherDirective? = getDirective(CY
originalStatement, rewrittenStatement, this.name, this.definition?.sourceLocation)
}
CypherDirective(rewrittenStatement, it.getMandatoryArgument(CYPHER_PASS_THROUGH, false))
}
}

data class CypherDirective(val statement: String, val passThrough: Boolean)

Expand Down Expand Up @@ -228,21 +227,6 @@ fun TypeDefinitionRegistry.mutationTypeName() = this.getOperationType("mutation"
fun TypeDefinitionRegistry.subscriptionTypeName() = this.getOperationType("subscription") ?: "Subscription"
fun TypeDefinitionRegistry.getOperationType(name: String) = this.schemaDefinition().unwrap()?.operationTypeDefinitions?.firstOrNull { it.name == name }?.typeName?.name

fun Any?.asGraphQLValue(): Value<*> = 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()}")

Expand Down
25 changes: 11 additions & 14 deletions core/src/main/kotlin/org/neo4j/graphql/Neo4jTypes.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
}

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/kotlin/org/neo4j/graphql/NoOpCoercing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package org.neo4j.graphql
import graphql.schema.Coercing

object NoOpCoercing : Coercing<Any, Any> {
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()
}
31 changes: 16 additions & 15 deletions core/src/main/kotlin/org/neo4j/graphql/Predicates.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,17 @@ enum class FieldOperator(
queriedField: String,
propertyContainer: PropertyContainer,
field: GraphQLFieldDefinition?,
value: Any,
value: Any?,
schemaConfig: SchemaConfig,
suffix: String? = null
): List<Condition> {
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()
Expand All @@ -79,28 +79,29 @@ enum class FieldOperator(
}
}

private fun resolveNeo4jTypeConditions(variablePrefix: String, queriedField: String, propertyContainer: PropertyContainer, field: GraphQLFieldDefinition, value: ObjectValue, suffix: String?): List<Condition> {
private fun resolveNeo4jTypeConditions(variablePrefix: String, queriedField: String, propertyContainer: PropertyContainer, field: GraphQLFieldDefinition, values: Map<*, *>, suffix: String?): List<Condition> {
val neo4jTypeConverter = getNeo4jTypeConverter(field)
val conditions = mutableListOf<Condition>()
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<Condition> {
private fun resolveCondition(variablePrefix: String, queriedField: String, property: Property, value: Any?, suffix: String?): List<Condition> {
val parameter = queryParameter(value, variablePrefix, queriedField, suffix)
val condition = conditionCreator(property, parameter)
return listOf(condition)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/kotlin/org/neo4j/graphql/QueryContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package org.neo4j.graphql

data class QueryContext @JvmOverloads constructor(
/**
* if true the <code>__typename</code> will be always returned for interfaces, no matter if it was queried or not
* if true the <code>__typename</code> will always be returned for interfaces, no matter if it was queried or not
*/
var queryTypeOfInterfaces: Boolean = false,

Expand All @@ -18,4 +18,4 @@ data class QueryContext @JvmOverloads constructor(
*/
FILTER_AS_MATCH
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) })
}
Expand All @@ -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<Argument>): Array<Any> =
protected fun properties(variable: String, arguments: Map<String, Any>): Array<Any> =
preparePredicateArguments(arguments)
.flatMap { listOf(it.propertyName, it.toExpression(variable)) }
.toTypedArray()

private fun preparePredicateArguments(arguments: List<Argument>): List<PropertyAccessor> {
private fun preparePredicateArguments(arguments: Map<String, Any>): List<PropertyAccessor> {
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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit ab0f96e

Please sign in to comment.