|
| 1 | +package org.neo4j.graphql |
| 2 | + |
| 3 | +import graphql.Scalars |
| 4 | +import graphql.language.Directive |
| 5 | +import graphql.schema.* |
| 6 | +import org.neo4j.graphql.Predicate.Companion.resolvePredicate |
| 7 | + |
| 8 | +interface Predicate { |
| 9 | + fun toExpression(variable: String, schema: GraphQLSchema) : Pair<String,Map<String,Any?>> |
| 10 | + |
| 11 | + companion object { |
| 12 | + fun resolvePredicate(name: String, value: Any?, type: GraphQLObjectType): Predicate { |
| 13 | + val (fieldName, op) = Operators.resolve(name, value) |
| 14 | + return if (type.hasRelationship(fieldName)) { |
| 15 | + if (value is Map<*, *>) RelationPredicate(fieldName, op, value, type) |
| 16 | + else if (value is IsNullOperator) IsNullPredicate(fieldName, op, type) |
| 17 | + else throw IllegalArgumentException("Input for $fieldName must be an filter-InputType") |
| 18 | + } else { |
| 19 | + ExpressionPredicate(fieldName, op, value) |
| 20 | + } |
| 21 | + } |
| 22 | + |
| 23 | + private fun isParam(value: String) = value.startsWith("{") && value.endsWith("}") || value.startsWith("\$") |
| 24 | + |
| 25 | + fun formatAnyValueCypher(value: Any?): String = |
| 26 | + when (value) { |
| 27 | + null -> "null" |
| 28 | + is String -> if (isParam(value)) value else "\"${value}\"" |
| 29 | + is Map<*, *> -> "{" + value.map { it.key.toString() + ":" + formatAnyValueCypher(it.value) }.joinToString(",") + "}" |
| 30 | + is Iterable<*> -> "[" + value.map { formatAnyValueCypher(it) }.joinToString(",") + "]" |
| 31 | + else -> value.toString() |
| 32 | + |
| 33 | + } |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +fun toExpression(name: String, value: Any?, type: GraphQLObjectType): Predicate = |
| 38 | + if (name == "AND" || name == "OR") |
| 39 | + if (value is Iterable<*>) { |
| 40 | + CompoundPredicate(value.map { toExpression("AND", it, type) }, name) |
| 41 | + } else if (value is Map<*,*>){ |
| 42 | + CompoundPredicate(value.map { (k,v) -> toExpression(k.toString(), v, type) }, name) |
| 43 | + } else { |
| 44 | + throw IllegalArgumentException("Unexpected value for filter: $value") |
| 45 | + } |
| 46 | + else { |
| 47 | + resolvePredicate(name, value, type) |
| 48 | + } |
| 49 | + |
| 50 | +fun GraphQLObjectType.hasRelationship(name:String) = this.getFieldDefinition(name)?.let { it.type is GraphQLObjectType } ?: false |
| 51 | + |
| 52 | +fun GraphQLObjectType.relationshipFor(name:String, schema: GraphQLSchema) : RelationshipInfo { |
| 53 | + val field = this.getFieldDefinition(name) |
| 54 | + val fieldObjectType = schema.getType(field.type.inner().name) as GraphQLObjectType |
| 55 | + // direction |
| 56 | + // label |
| 57 | + // out |
| 58 | + |
| 59 | + val (relDirective, relFromType) = fieldObjectType.definition.getDirective("relation")?.let { it to true } |
| 60 | + ?: field.definition.getDirective("relation")?.let { it to false } |
| 61 | + ?: throw IllegalStateException("Field $field needs an @relation directive") |
| 62 | + |
| 63 | + val (relType, outgoing, endField) = relDetails(relDirective, schema) |
| 64 | + |
| 65 | + return RelationshipInfo(fieldObjectType, relDirective, relType, outgoing,endField, relFromType) |
| 66 | +} |
| 67 | + |
| 68 | +fun relDetails(relDirective: Directive, schema: GraphQLSchema): Triple<String,Boolean?,String> { |
| 69 | + val relType = relDirective.argumentString("name", schema) |
| 70 | + val outgoing = when (relDirective.argumentString("direction", schema)) { |
| 71 | + "IN" -> false |
| 72 | + "BOTH" -> null |
| 73 | + "OUT" -> true |
| 74 | + else -> throw IllegalStateException("Unknown direction ${relDirective.argumentString("direction",schema)}") |
| 75 | + } |
| 76 | + val endField = if (outgoing == true) relDirective.argumentString("to", schema) |
| 77 | + else relDirective.argumentString("from", schema) |
| 78 | + return Triple(relType, outgoing, endField) |
| 79 | +} |
| 80 | + |
| 81 | +fun arrows(outgoing: Boolean?): Pair<String, String> { |
| 82 | + return when (outgoing) { |
| 83 | + false -> "<" to "" |
| 84 | + true -> "" to ">" |
| 85 | + null -> "" to "" |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | + |
| 90 | +data class RelationshipInfo(val objectType:GraphQLObjectType, val directive:Directive, val type:String, val out:Boolean?, val endField: String? = null, val relFromType: Boolean = false) { |
| 91 | + val arrows = arrows(out) |
| 92 | + val label = objectType.name |
| 93 | +} |
| 94 | + |
| 95 | +data class CompoundPredicate(val parts : List<Predicate>, val op : String = "AND") : Predicate { |
| 96 | + override fun toExpression(variable: String, schema: GraphQLSchema) : Pair<String,Map<String,Any?>> = |
| 97 | + parts.map { it.toExpression(variable,schema) } |
| 98 | + .let { expressions -> |
| 99 | + expressions.map { it.first }.joinNonEmpty(" "+op+" ","(",")") to |
| 100 | + expressions.fold(emptyMap<String,Any?>()) { res, exp -> res + exp.second } |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +data class IsNullPredicate(val name:String, val op: Operators, val type: GraphQLObjectType) : Predicate { |
| 105 | + override fun toExpression(variable: String, schema: GraphQLSchema) : Pair<String,Map<String,Any?>> { |
| 106 | + val rel = type.relationshipFor(name, schema) |
| 107 | + val (left,right) = rel.arrows |
| 108 | + val not = if (op.not) "" else "NOT " |
| 109 | + return "$not($variable)$left-[:${rel.type}]-$right()" to emptyMap() |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | +data class ExpressionPredicate(val name:String, val op: Operators, val value:Any?) : Predicate { |
| 114 | + val not = if (op.not) "NOT " else "" |
| 115 | + override fun toExpression(variable: String, schema: GraphQLSchema) : Pair<String,Map<String,Any?>> { |
| 116 | + val paramName : String = "filter" + paramName(variable, name, value).capitalize() |
| 117 | + return "$not${variable}.$name ${op.op} \$${paramName}" to mapOf(paramName to value) |
| 118 | + } |
| 119 | +} |
| 120 | + |
| 121 | + |
| 122 | +data class RelationPredicate(val name: String, val op: Operators, val value: Map<*,*>, val type: GraphQLObjectType) : Predicate { |
| 123 | + val not = if (op.not) "NOT" else "" |
| 124 | + // (type)-[:TYPE]->(related) | pred] = 0/1/ > 0 | = |
| 125 | + // ALL/ANY/NONE/SINGLE(p in (type)-[:TYPE]->() WHERE pred(last(nodes(p))) |
| 126 | + // ALL/ANY/NONE/SINGLE(x IN [(type)-[:TYPE]->(o) | pred(o)] WHERE x) |
| 127 | + |
| 128 | + override fun toExpression(variable: String, schema: GraphQLSchema) : Pair<String,Map<String,Any?>> { |
| 129 | + val prefix = when (op) { |
| 130 | + Operators.EQ -> "ALL" |
| 131 | + Operators.NEQ -> "ALL" // bc of not |
| 132 | + else -> op.op |
| 133 | + } |
| 134 | + val rel = type.relationshipFor(name, schema) |
| 135 | + val (left,right) = rel.arrows |
| 136 | + val other = variable+"_"+rel.label |
| 137 | + val cond = other + "_Cond" |
| 138 | + val relGraphQLObjectType = schema.getType(rel.label) as GraphQLObjectType |
| 139 | + val (pred, params) = CompoundPredicate(value.map { it -> resolvePredicate(it.key.toString(), it.value,relGraphQLObjectType)}).toExpression(other, schema) |
| 140 | + return "$not $prefix(${cond} IN [($variable)$left-[:${rel.type}]-$right($other) | $pred] WHERE ${cond})" to params |
| 141 | + } |
| 142 | +} |
| 143 | + |
| 144 | +abstract class UnaryOperator |
| 145 | +class IsNullOperator : UnaryOperator() |
| 146 | + |
| 147 | +enum class Operators(val suffix:String, val op:String, val not :Boolean = false) { |
| 148 | + EQ("","="), |
| 149 | + IS_NULL("", ""), |
| 150 | + IS_NOT_NULL("", "", true), |
| 151 | + NEQ("not","=", true), |
| 152 | + GTE("gte",">="), |
| 153 | + GT("gt",">"), |
| 154 | + LTE("lte","<="), |
| 155 | + LT("lt","<"), |
| 156 | + |
| 157 | + NIN("not_in","IN", true), |
| 158 | + IN("in","IN"), |
| 159 | + NC("not_contains","CONTAINS", true), |
| 160 | + NSW("not_starts_with","STARTS WITH", true), |
| 161 | + NEW("not_ends_with","ENDS WITH", true), |
| 162 | + C("contains","CONTAINS"), |
| 163 | + SW("starts_with","STARTS WITH"), |
| 164 | + EW("ends_with","ENDS WITH"), |
| 165 | + |
| 166 | + SOME("some","ANY"), |
| 167 | + NONE("none","NONE"), |
| 168 | + ALL("every","ALL"), |
| 169 | + SINGLE("single","SINGLE") |
| 170 | + ; |
| 171 | + |
| 172 | + val list = op == "IN" |
| 173 | + |
| 174 | + companion object { |
| 175 | + val ops = enumValues<Operators>().sortedWith(Comparator.comparingInt<Operators> { it.suffix.length }).reversed() |
| 176 | + val allNames = ops.map { it.suffix } |
| 177 | + val allOps = ops.map { it.op } |
| 178 | + |
| 179 | + fun resolve(field: String, value: Any?) : Pair<String, Operators> { |
| 180 | + val fieldOperator = ops.find { field.endsWith("_" + it.suffix) } |
| 181 | + val unaryOperator = if (value is UnaryOperator) unaryOperatorOf(field, value) else Operators.EQ |
| 182 | + val op = fieldOperator ?: unaryOperator |
| 183 | + val name = if (op.suffix.isEmpty()) field else field.substring(0, field.length - op.suffix.length - 1) |
| 184 | + return name to op |
| 185 | + } |
| 186 | + |
| 187 | + private fun unaryOperatorOf(field: String, value: Any?): Operators = |
| 188 | + when (value) { |
| 189 | + is IsNullOperator -> if (field.endsWith("_not")) IS_NOT_NULL else IS_NULL |
| 190 | + else -> throw IllegalArgumentException("Unknown unary operator $value") |
| 191 | + } |
| 192 | + |
| 193 | + fun forType(type: GraphQLInputType) : List<Operators> = |
| 194 | + if (type == Scalars.GraphQLBoolean) listOf(EQ, NEQ) |
| 195 | + else if (type is GraphQLEnumType || type is GraphQLObjectType || type is GraphQLTypeReference) listOf(EQ, NEQ, IN, NIN) |
| 196 | + else listOf(EQ, NEQ, IN, NIN,LT,LTE,GT,GTE) + |
| 197 | + if (type == Scalars.GraphQLString || type == Scalars.GraphQLID) listOf(C,NC, SW, NSW,EW,NEW) else emptyList() |
| 198 | + |
| 199 | + } |
| 200 | + |
| 201 | + fun fieldName(fieldName: String) = if (this == EQ) fieldName else fieldName + "_" + suffix |
| 202 | +} |
0 commit comments