Skip to content

Commit 6c56bb0

Browse files
committed
Added first stab of filter support
1 parent 61f8f97 commit 6c56bb0

File tree

6 files changed

+350
-84
lines changed

6 files changed

+350
-84
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.neo4j.graphql
2+
3+
import java.io.PrintWriter
4+
import java.io.StringWriter
5+
6+
fun Throwable.stackTraceAsString(): String {
7+
val sw = StringWriter()
8+
this.printStackTrace(PrintWriter(sw))
9+
return sw.toString()
10+
}
11+
12+
fun <T> Iterable<T>.joinNonEmpty(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String {
13+
return if (iterator().hasNext()) joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString() else ""
14+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.neo4j.graphql
2+
3+
import graphql.language.*
4+
import graphql.schema.GraphQLList
5+
import graphql.schema.GraphQLNonNull
6+
import graphql.schema.GraphQLSchema
7+
import graphql.schema.GraphQLType
8+
9+
fun GraphQLType.inner() : GraphQLType = when(this) {
10+
is GraphQLList -> this.wrappedType.inner()
11+
is GraphQLNonNull -> this.wrappedType.inner()
12+
else -> this
13+
}
14+
15+
fun GraphQLType.isList() = this is GraphQLList || (this is GraphQLNonNull && this.wrappedType is GraphQLList)
16+
17+
fun Field.aliasOrName() = (this.alias ?: this.name).quote()
18+
19+
fun String.quote() = this // do we actually need this? if (isJavaIdentifier()) this else '`'+this+'`'
20+
21+
fun String.isJavaIdentifier() =
22+
this[0].isJavaIdentifierStart() &&
23+
this.substring(1).all { it.isJavaIdentifierPart() }
24+
25+
fun Directive.argumentString(name:String, schema:GraphQLSchema) : String {
26+
return this.getArgument(name)?.value?.toJavaValue()?.toString()
27+
?: schema.getDirective(this.name).getArgument(name)?.defaultValue?.toString()
28+
?: throw IllegalStateException("No default value for ${this.name}.${name}")
29+
}
30+
31+
fun Value.toCypherString(): String = when (this) {
32+
is StringValue -> "'"+this.value+"'"
33+
is EnumValue -> "'"+this.name+"'"
34+
is NullValue -> "null"
35+
is BooleanValue -> this.isValue.toString()
36+
is FloatValue -> this.value.toString()
37+
is IntValue -> this.value.toString()
38+
is VariableReference -> "$"+this.name
39+
is ArrayValue -> this.values.map { it.toCypherString() }.joinToString(",","[","]")
40+
else -> throw IllegalStateException("Unhandled value "+this)
41+
}
42+
43+
fun Value.toJavaValue(): Any? = when (this) {
44+
is StringValue -> this.value
45+
is EnumValue -> this.name
46+
is NullValue -> null
47+
is BooleanValue -> this.isValue
48+
is FloatValue -> this.value
49+
is IntValue -> this.value
50+
is VariableReference -> this
51+
is ArrayValue -> this.values.map { it.toJavaValue() }.toList()
52+
is ObjectValue -> this.objectFields.map { it.name to it.value.toJavaValue() }.toMap()
53+
else -> throw IllegalStateException("Unhandled value "+this)
54+
}
55+
fun paramName(variable: String, argName: String, value: Any?) = when (value) {
56+
is VariableReference -> value.name
57+
else -> "$variable${argName.capitalize()}"
58+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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

Comments
 (0)