diff --git a/demo/src/main/kotlin/Main.kt b/demo/src/main/kotlin/Main.kt index f16c5c1..0dfbf74 100644 --- a/demo/src/main/kotlin/Main.kt +++ b/demo/src/main/kotlin/Main.kt @@ -1,6 +1,7 @@ package fr.qsh.ktmongo.demo import com.mongodb.kotlin.client.MongoClient +import fr.qsh.ktmongo.dsl.path.div import fr.qsh.ktmongo.sync.asKtMongo import fr.qsh.ktmongo.sync.filter @@ -8,6 +9,11 @@ data class Jedi( val name: String, val age: Int, val level: Int, + val friends: List, +) + +data class Friend( + val name: String, ) fun main() { @@ -30,4 +36,8 @@ fun main() { Jedi::age set 19 Jedi::level inc 1 } + + collection.find { + Jedi::friends.items() / Friend::name eq "Foo" + } } diff --git a/dsl/src/main/kotlin/expr/FilterExpression.kt b/dsl/src/main/kotlin/expr/FilterExpression.kt index 51d6383..0497916 100644 --- a/dsl/src/main/kotlin/expr/FilterExpression.kt +++ b/dsl/src/main/kotlin/expr/FilterExpression.kt @@ -4,6 +4,7 @@ import fr.qsh.ktmongo.dsl.KtMongoDsl import fr.qsh.ktmongo.dsl.LowLevelApi import fr.qsh.ktmongo.dsl.expr.common.CompoundExpression import fr.qsh.ktmongo.dsl.expr.common.Expression +import fr.qsh.ktmongo.dsl.path.PropertyPath import fr.qsh.ktmongo.dsl.path.path import fr.qsh.ktmongo.dsl.writeArray import fr.qsh.ktmongo.dsl.writeDocument @@ -280,6 +281,9 @@ class FilterExpression( * ### External resources * * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/query/eq/) + * + * @see eqNotNull To only filter when the value is non-null. + * @see contains To make an equality check on one of the elements of a collection. */ @KtMongoDsl infix fun <@OnlyInputTypes V> KProperty1.eq(value: V) { @@ -351,6 +355,78 @@ class FilterExpression( this { ne(value) } } + // endregion + // region Predicates on array elements + + /** + * Allows to declare filters on the items of the specified field. + * + * ### Example + * + * This example will return all users who have at least one grade above 10: + * ```kotlin + * class User( + * val name: String, + * val grades: List, + * ) + * + * collection.find { + * User::grades.items() gte 10 + * } + * ``` + * + * ### Behavior with non-array fields + * + * TODO + * + * ### Using multiple criteria + * + * @see fr.qsh.ktmongo.dsl.path.get Refer to a specific item by its index. + */ + @OptIn(LowLevelApi::class) + fun <@OnlyInputTypes V> KProperty1>.items(): KProperty1 = + PropertyPath( + path = this.path(), + backingProperty = this, + ) + + /** + * Matches documents where one of the items in the specified field is [value]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val addresses: List, + * ) + * + * collection.find { + * User::addresses contains "Some address" + * } + * ``` + * + * All documents for which one of the `addresses` is equal to "Some address" are returned. + * + * ### Behavior with non-array fields + * + * MongoDB doesn't make a difference between "checking if one of the elements of an array matches the predicate" and "checking if the field matches the predicate". + * + * In the previous example, if a document had a field named `addresses` that was a `String` (**not** a `List`), and its value was "Some address", it would be returned as well. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/query/eq/#array-element-equals-a-value) + * + * @see eq To make an equality check on the array itself, instead of one its elements. + * @see items Perform other kinds of filters on one of the items of an array. + * @see UpdateExpression.matched Update the element item matched by this function. + */ + @KtMongoDsl + infix fun <@OnlyInputTypes V> KProperty1>.contains(value: V) { + this.items() eq value + } + // endregion // region $exists diff --git a/dsl/src/main/kotlin/expr/PredicateExpression.kt b/dsl/src/main/kotlin/expr/PredicateExpression.kt index 282e743..07e3af2 100644 --- a/dsl/src/main/kotlin/expr/PredicateExpression.kt +++ b/dsl/src/main/kotlin/expr/PredicateExpression.kt @@ -11,6 +11,13 @@ import org.bson.codecs.configuration.CodecRegistry * DSL for MongoDB operators that are used as predicates in conditions in a context where the targeted field is already * specified. */ +// TODO: PredicateExpression should allow further property nesting +// { +// "foo": { +// "$gte": 10, +// "bar": 11 +// } +// } @KtMongoDsl class PredicateExpression( codec: CodecRegistry, diff --git a/dsl/src/main/kotlin/expr/UpdateExpression.kt b/dsl/src/main/kotlin/expr/UpdateExpression.kt index 3808268..bf56bee 100644 --- a/dsl/src/main/kotlin/expr/UpdateExpression.kt +++ b/dsl/src/main/kotlin/expr/UpdateExpression.kt @@ -5,8 +5,7 @@ import fr.qsh.ktmongo.dsl.LowLevelApi import fr.qsh.ktmongo.dsl.expr.common.CompoundExpression import fr.qsh.ktmongo.dsl.expr.common.Expression import fr.qsh.ktmongo.dsl.expr.common.acceptAll -import fr.qsh.ktmongo.dsl.path.Path -import fr.qsh.ktmongo.dsl.path.path +import fr.qsh.ktmongo.dsl.path.* import fr.qsh.ktmongo.dsl.writeDocument import fr.qsh.ktmongo.dsl.writeObject import org.bson.BsonWriter @@ -324,6 +323,40 @@ class UpdateExpression( } } + // endregion + // region Array indexing operators: $, $[] + + /** + * Selects the item matched by [contains][FilterExpression.contains]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val friends: List, + * ) + * + * class Friend( + * val name: String, + * val score: Int, + * ) + * + * collection.filter { + * User::name eq "Foo" + * User::friends contains { + * Friend:: TODO + * } + * } + * ``` + */ + @OptIn(LowLevelApi::class) + fun KProperty1>.matched(): KProperty1 = + PropertyPath( + path = this.path() + PathSegment.Positional, + backingProperty = this, + ) + // endregion companion object { diff --git a/dsl/src/main/kotlin/path/PropertyPath.kt b/dsl/src/main/kotlin/path/PropertyPath.kt index 22642b0..e27dcc1 100644 --- a/dsl/src/main/kotlin/path/PropertyPath.kt +++ b/dsl/src/main/kotlin/path/PropertyPath.kt @@ -18,7 +18,7 @@ import kotlin.reflect.* */ @LowLevelApi @Suppress("NO_REFLECTION_IN_CLASS_PATH") // None of the functions are called by our code. The caller is responsible for fixing this. -private class PropertyPath( +internal class PropertyPath( /** * The path of this property. * diff --git a/dsl/src/test/kotlin/expr/FilterExpressionTest.kt b/dsl/src/test/kotlin/expr/FilterExpressionTest.kt index 32e526d..d8a1efd 100644 --- a/dsl/src/test/kotlin/expr/FilterExpressionTest.kt +++ b/dsl/src/test/kotlin/expr/FilterExpressionTest.kt @@ -23,6 +23,10 @@ class FilterExpressionTest : FunSpec({ val type = "\$type" val not = "\$not" val isOneOf = "\$in" + val gt = "\$gt" + val gte = "\$gte" + val lt = "\$lt" + val lte = "\$lte" context("Operator $eq") { test("Integer") { @@ -376,11 +380,6 @@ class FilterExpressionTest : FunSpec({ } context("Comparison operators") { - val gt = "\$gt" - val gte = "\$gte" - val lt = "\$lt" - val lte = "\$lte" - test("int $gt") { filter { User::age gt 12 @@ -429,4 +428,39 @@ class FilterExpressionTest : FunSpec({ """.trimIndent() } } + + context("Array operators") { + class Grades( + val userId: String, + val grades: List, + ) + + test("Search for a specific grade") { + filter { + Grades::grades contains 12 + } shouldBeBson """ + { + "grades": { + "$eq": 12 + } + } + """.trimIndent() + } + + test("Search for a set of grades") { + filter { + Grades::grades contains { + gt(10) + lte(12) + } + } shouldBeBson """ + { + "grades": { + "$gt": 10, + "$lte": 12 + } + } + """.trimIndent() + } + } })