diff --git a/.github/actions/setup_test_action/action.yml b/.github/actions/setup_test_action/action.yml
index 1b18c9db4..ba5441480 100644
--- a/.github/actions/setup_test_action/action.yml
+++ b/.github/actions/setup_test_action/action.yml
@@ -24,7 +24,9 @@ runs:
run: chmod +x gradlew
- name: Install Firebase tools
shell: bash
- run: npm install -g firebase-tools
+ run: npm install -g firebase-tools wait-on
- name: Start Firebase emulator
shell: bash
- run: "firebase emulators:start --config=./test/firebase.json &"
\ No newline at end of file
+ run: |
+ firebase emulators:start --config=./test/firebase.json &
+ wait-on http://127.0.0.1:9099
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 31d9d58e2..63fb749a0 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -66,6 +66,7 @@ jobs:
- uses: actions/checkout@v3
- name: Setup test environment
uses: ./.github/actions/setup_test_action
+ timeout-minutes: 10
- name: Run JS Tests
run: ./gradlew cleanTest jsTest
- name: Upload JS test artifact
@@ -89,14 +90,13 @@ jobs:
- uses: actions/checkout@v3
- name: Cocoapods cache
uses: actions/cache@v3
- id: cocoapods-cache
with:
path: |
~/.cocoapods
~/Library/Caches/CocoaPods
*/build/cocoapods
*/build/classes
- key: cocoapods-cache
+ key: cocoapods-cache-v2
- name: Setup test environment
uses: ./.github/actions/setup_test_action
- name: Run iOS Tests
@@ -112,4 +112,4 @@ jobs:
if: failure()
with:
name: "Firebase Debug Log"
- path: "**/firebase-debug.log"
+ path: "**/firebase-debug.log"
\ No newline at end of file
diff --git a/README.md b/README.md
index 0f7c6ec41..2b0797c8d 100644
--- a/README.md
+++ b/README.md
@@ -14,8 +14,8 @@ Firebase as a backend for Named arguments
-To improve readability functions such as the Cloud Firestore query operators use named arguments:
+
+
+
+To improve readability and reduce boilerplate for functions such as the Cloud Firestore query operators are built with infix notation:
```kotlin
citiesRef.whereEqualTo("state", "CA")
citiesRef.whereArrayContains("regions", "west_coast")
+citiesRef.where(Filter.and(
+ Filter.equalTo("state", "CA"),
+ Filter.or(
+ Filter.equalTo("capital", true),
+ Filter.greaterThanOrEqualTo("population", 1000000)
+ )
+))
//...becomes...
-citiesRef.where("state", equalTo = "CA")
-citiesRef.where("regions", arrayContains = "west_coast")
+citiesRef.where { "state" equalTo "CA" }
+citiesRef.where { "regions" contains "west_coast" }
+citiesRef.where {
+ all(
+ "state" equalTo "CA",
+ any(
+ "capital" equalTo true,
+ "population" greaterThanOrEqualTo 1000000
+ )
+ )
+}
```
diff --git a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt
index 1ba855b90..57306fe6b 100644
--- a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt
+++ b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt
@@ -22,6 +22,10 @@ import kotlinx.serialization.SerializationStrategy
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
+import com.google.firebase.firestore.Query as AndroidQuery
+import com.google.firebase.firestore.FieldPath as AndroidFieldPath
+import com.google.firebase.firestore.Filter as AndroidFilter
+
actual val Firebase.firestore get() =
FirebaseFirestore(com.google.firebase.firestore.FirebaseFirestore.getInstance())
@@ -342,7 +346,7 @@ actual class DocumentReference actual constructor(internal actual val nativeValu
}
}
-actual open class Query(open val android: com.google.firebase.firestore.Query) {
+actual open class Query(open val android: AndroidQuery) {
actual suspend fun get() = QuerySnapshot(android.get().await())
@@ -358,39 +362,72 @@ actual open class Query(open val android: com.google.firebase.firestore.Query) {
exception?.let { close(exception) }
}
- internal actual fun _where(field: String, equalTo: Any?) = Query(android.whereEqualTo(field, equalTo))
- internal actual fun _where(path: FieldPath, equalTo: Any?) = Query(android.whereEqualTo(path.android, equalTo))
-
- internal actual fun _where(field: String, equalTo: DocumentReference) = Query(android.whereEqualTo(field, equalTo.android))
- internal actual fun _where(path: FieldPath, equalTo: DocumentReference) = Query(android.whereEqualTo(path.android, equalTo.android))
-
- internal actual fun _where(field: String, lessThan: Any?, greaterThan: Any?, arrayContains: Any?) = Query(
- (lessThan?.let { android.whereLessThan(field, it) } ?: android).let { android2 ->
- (greaterThan?.let { android2.whereGreaterThan(field, it) } ?: android2).let { android3 ->
- arrayContains?.let { android3.whereArrayContains(field, it) } ?: android3
- }
- }
+ internal actual fun where(filter: Filter) = Query(
+ android.where(filter.toAndroidFilter())
)
- internal actual fun _where(path: FieldPath, lessThan: Any?, greaterThan: Any?, arrayContains: Any?) = Query(
- (lessThan?.let { android.whereLessThan(path.android, it) } ?: android).let { android2 ->
- (greaterThan?.let { android2.whereGreaterThan(path.android, it) } ?: android2).let { android3 ->
- arrayContains?.let { android3.whereArrayContains(path.android, it) } ?: android3
+ private fun Filter.toAndroidFilter(): AndroidFilter = when (this) {
+ is Filter.And -> AndroidFilter.and(*filters.map { it.toAndroidFilter() }.toTypedArray())
+ is Filter.Or -> AndroidFilter.or(*filters.map { it.toAndroidFilter() }.toTypedArray())
+ is Filter.Field -> {
+ when (constraint) {
+ is WhereConstraint.ForNullableObject -> {
+ val modifier: (String, Any?) -> AndroidFilter = when (constraint) {
+ is WhereConstraint.EqualTo -> AndroidFilter::equalTo
+ is WhereConstraint.NotEqualTo -> AndroidFilter::notEqualTo
+ }
+ modifier.invoke(field, constraint.safeValue)
+ }
+ is WhereConstraint.ForObject -> {
+ val modifier: (String, Any) -> AndroidFilter = when (constraint) {
+ is WhereConstraint.LessThan -> AndroidFilter::lessThan
+ is WhereConstraint.GreaterThan -> AndroidFilter::greaterThan
+ is WhereConstraint.LessThanOrEqualTo -> AndroidFilter::lessThanOrEqualTo
+ is WhereConstraint.GreaterThanOrEqualTo -> AndroidFilter::greaterThanOrEqualTo
+ is WhereConstraint.ArrayContains -> AndroidFilter::arrayContains
+ }
+ modifier.invoke(field, constraint.safeValue)
+ }
+ is WhereConstraint.ForArray -> {
+ val modifier: (String, List) -> AndroidFilter = when (constraint) {
+ is WhereConstraint.InArray -> AndroidFilter::inArray
+ is WhereConstraint.ArrayContainsAny -> AndroidFilter::arrayContainsAny
+ is WhereConstraint.NotInArray -> AndroidFilter::notInArray
+ }
+ modifier.invoke(field, constraint.safeValues)
+ }
}
}
- )
-
- internal actual fun _where(field: String, inArray: List?, arrayContainsAny: List?) = Query(
- (inArray?.let { android.whereIn(field, it) } ?: android).let { android2 ->
- arrayContainsAny?.let { android2.whereArrayContainsAny(field, it) } ?: android2
- }
- )
-
- internal actual fun _where(path: FieldPath, inArray: List?, arrayContainsAny: List?) = Query(
- (inArray?.let { android.whereIn(path.android, it) } ?: android).let { android2 ->
- arrayContainsAny?.let { android2.whereArrayContainsAny(path.android, it) } ?: android2
+ is Filter.Path -> {
+ when (constraint) {
+ is WhereConstraint.ForNullableObject -> {
+ val modifier: (AndroidFieldPath, Any?) -> AndroidFilter = when (constraint) {
+ is WhereConstraint.EqualTo -> AndroidFilter::equalTo
+ is WhereConstraint.NotEqualTo -> AndroidFilter::notEqualTo
+ }
+ modifier.invoke(path.android, constraint.safeValue)
+ }
+ is WhereConstraint.ForObject -> {
+ val modifier: (AndroidFieldPath, Any) -> AndroidFilter = when (constraint) {
+ is WhereConstraint.LessThan -> AndroidFilter::lessThan
+ is WhereConstraint.GreaterThan -> AndroidFilter::greaterThan
+ is WhereConstraint.LessThanOrEqualTo -> AndroidFilter::lessThanOrEqualTo
+ is WhereConstraint.GreaterThanOrEqualTo -> AndroidFilter::greaterThanOrEqualTo
+ is WhereConstraint.ArrayContains -> AndroidFilter::arrayContains
+ }
+ modifier.invoke(path.android, constraint.safeValue)
+ }
+ is WhereConstraint.ForArray -> {
+ val modifier: (AndroidFieldPath, List) -> AndroidFilter = when (constraint) {
+ is WhereConstraint.InArray -> AndroidFilter::inArray
+ is WhereConstraint.ArrayContainsAny -> AndroidFilter::arrayContainsAny
+ is WhereConstraint.NotInArray -> AndroidFilter::notInArray
+ }
+ modifier.invoke(path.android, constraint.safeValues)
+ }
+ }
}
- )
+ }
internal actual fun _orderBy(field: String, direction: Direction) = Query(android.orderBy(field, direction))
internal actual fun _orderBy(field: FieldPath, direction: Direction) = Query(android.orderBy(field.android, direction))
diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Filter.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Filter.kt
new file mode 100644
index 000000000..e6897c6d7
--- /dev/null
+++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Filter.kt
@@ -0,0 +1,154 @@
+package dev.gitlive.firebase.firestore
+
+sealed interface WhereConstraint {
+
+ sealed interface ForNullableObject : WhereConstraint {
+ val value: Any?
+ val safeValue get() = value?.safeValue
+ }
+
+ sealed interface ForObject : WhereConstraint {
+ val value: Any
+ val safeValue get() = value.safeValue
+ }
+ sealed interface ForArray : WhereConstraint {
+ val values: List
+ val safeValues get() = values.map { it.safeValue }
+ }
+
+ data class EqualTo internal constructor(override val value: Any?) : ForNullableObject
+ data class NotEqualTo internal constructor(override val value: Any?) : ForNullableObject
+ data class LessThan internal constructor(override val value: Any) : ForObject
+ data class GreaterThan internal constructor(override val value: Any) : ForObject
+ data class LessThanOrEqualTo internal constructor(override val value: Any) : ForObject
+ data class GreaterThanOrEqualTo internal constructor(override val value: Any) : ForObject
+ data class ArrayContains internal constructor(override val value: Any) : ForObject
+ data class ArrayContainsAny internal constructor(override val values: List) : ForArray
+ data class InArray internal constructor(override val values: List) : ForArray
+ data class NotInArray internal constructor(override val values: List) : ForArray
+}
+
+sealed class Filter {
+ data class And internal constructor(val filters: List) : Filter()
+ data class Or internal constructor(val filters: List) : Filter()
+ sealed class WithConstraint : Filter() {
+ abstract val constraint: WhereConstraint
+ }
+
+ data class Field internal constructor(val field: String, override val constraint: WhereConstraint) : WithConstraint()
+ data class Path internal constructor(val path: FieldPath, override val constraint: WhereConstraint) : WithConstraint()
+}
+
+class FilterBuilder internal constructor() {
+
+ infix fun String.equalTo(value: Any?): Filter.WithConstraint {
+ return Filter.Field(this, WhereConstraint.EqualTo(value))
+ }
+
+ infix fun FieldPath.equalTo(value: Any?): Filter.WithConstraint {
+ return Filter.Path(this, WhereConstraint.EqualTo(value))
+ }
+
+ infix fun String.notEqualTo(value: Any?): Filter.WithConstraint {
+ return Filter.Field(this, WhereConstraint.NotEqualTo(value))
+ }
+
+ infix fun FieldPath.notEqualTo(value: Any?): Filter.WithConstraint {
+ return Filter.Path(this, WhereConstraint.NotEqualTo(value))
+ }
+
+ infix fun String.lessThan(value: Any): Filter.WithConstraint {
+ return Filter.Field(this, WhereConstraint.LessThan(value))
+ }
+
+ infix fun FieldPath.lessThan(value: Any): Filter.WithConstraint {
+ return Filter.Path(this, WhereConstraint.LessThan(value))
+ }
+
+ infix fun String.greaterThan(value: Any): Filter.WithConstraint {
+ return Filter.Field(this, WhereConstraint.GreaterThan(value))
+ }
+
+ infix fun FieldPath.greaterThan(value: Any): Filter.WithConstraint {
+ return Filter.Path(this, WhereConstraint.GreaterThan(value))
+ }
+
+ infix fun String.lessThanOrEqualTo(value: Any): Filter.WithConstraint {
+ return Filter.Field(this, WhereConstraint.LessThanOrEqualTo(value))
+ }
+
+ infix fun FieldPath.lessThanOrEqualTo(value: Any): Filter.WithConstraint {
+ return Filter.Path(this, WhereConstraint.LessThanOrEqualTo(value))
+ }
+
+ infix fun String.greaterThanOrEqualTo(value: Any): Filter.WithConstraint {
+ return Filter.Field(this, WhereConstraint.GreaterThanOrEqualTo(value))
+ }
+
+ infix fun FieldPath.greaterThanOrEqualTo(value: Any): Filter.WithConstraint {
+ return Filter.Path(this, WhereConstraint.GreaterThanOrEqualTo(value))
+ }
+
+ infix fun String.contains(value: Any): Filter.WithConstraint {
+ return Filter.Field(this, WhereConstraint.ArrayContains(value))
+ }
+
+ infix fun FieldPath.contains(value: Any): Filter.WithConstraint {
+ return Filter.Path(this, WhereConstraint.ArrayContains(value))
+ }
+
+ infix fun String.containsAny(values: List): Filter.WithConstraint {
+ return Filter.Field(this, WhereConstraint.ArrayContainsAny(values))
+ }
+
+ infix fun FieldPath.containsAny(values: List): Filter.WithConstraint {
+ return Filter.Path(this, WhereConstraint.ArrayContainsAny(values))
+ }
+
+ infix fun String.inArray(values: List): Filter.WithConstraint {
+ return Filter.Field(this, WhereConstraint.InArray(values))
+ }
+
+ infix fun FieldPath.inArray(values: List): Filter.WithConstraint {
+ return Filter.Path(this, WhereConstraint.InArray(values))
+ }
+
+ infix fun String.notInArray(values: List): Filter.WithConstraint {
+ return Filter.Field(this, WhereConstraint.NotInArray(values))
+ }
+
+ infix fun FieldPath.notInArray(values: List): Filter.WithConstraint {
+ return Filter.Path(this, WhereConstraint.NotInArray(values))
+ }
+
+ infix fun Filter.and(right: Filter): Filter.And {
+ val leftList = when (this) {
+ is Filter.And -> filters
+ else -> listOf(this)
+ }
+ val rightList = when (right) {
+ is Filter.And -> right.filters
+ else -> listOf(right)
+ }
+ return Filter.And(leftList + rightList)
+ }
+
+ infix fun Filter.or(right: Filter): Filter.Or {
+ val leftList = when (this) {
+ is Filter.Or -> filters
+ else -> listOf(this)
+ }
+ val rightList = when (right) {
+ is Filter.Or -> right.filters
+ else -> listOf(right)
+ }
+ return Filter.Or(leftList + rightList)
+ }
+
+ fun all(vararg filters: Filter): Filter? = filters.toList().combine { left, right -> left and right }
+ fun any(vararg filters: Filter): Filter? = filters.toList().combine { left, right -> left or right }
+
+ private fun Collection.combine(over: (Filter, Filter) -> Filter): Filter? = fold(null) { acc, filter ->
+ acc?.let { over(acc, filter) } ?: filter
+ }
+}
diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt
index 5398029ef..237ad8500 100644
--- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt
+++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt
@@ -86,14 +86,8 @@ expect open class Query {
val snapshots: Flow
fun snapshots(includeMetadataChanges: Boolean = false): Flow
suspend fun get(): QuerySnapshot
- internal fun _where(field: String, equalTo: Any?): Query
- internal fun _where(path: FieldPath, equalTo: Any?): Query
- internal fun _where(field: String, equalTo: DocumentReference): Query
- internal fun _where(path: FieldPath, equalTo: DocumentReference): Query
- internal fun _where(field: String, lessThan: Any? = null, greaterThan: Any? = null, arrayContains: Any? = null): Query
- internal fun _where(path: FieldPath, lessThan: Any? = null, greaterThan: Any? = null, arrayContains: Any? = null): Query
- internal fun _where(field: String, inArray: List? = null, arrayContainsAny: List? = null): Query
- internal fun _where(path: FieldPath, inArray: List? = null, arrayContainsAny: List? = null): Query
+
+ internal fun where(filter: Filter): Query
internal fun _orderBy(field: String, direction: Direction): Query
internal fun _orderBy(field: FieldPath, direction: Direction): Query
@@ -109,35 +103,81 @@ expect open class Query {
internal fun _endAt(vararg fieldValues: Any): Query
}
-/** @return a native value of a wrapper or self. */
-private val Any.value get() = when (this) {
- is Timestamp -> nativeValue
- is GeoPoint -> nativeValue
- is DocumentReference -> nativeValue
- else -> this
+fun Query.where(builder: FilterBuilder.() -> Filter?) = builder(FilterBuilder())?.let { where(it) } ?: this
+
+@Deprecated("Deprecated in favor of using a [FilterBuilder]", replaceWith = ReplaceWith("where { field equalTo equalTo }", "dev.gitlive.firebase.firestore"))
+fun Query.where(field: String, equalTo: Any?) = where {
+ field equalTo equalTo
}
-fun Query.where(field: String, equalTo: Any?) = _where(field, equalTo?.value)
-fun Query.where(path: FieldPath, equalTo: Any?) = _where(path, equalTo?.value)
-fun Query.where(field: String, equalTo: DocumentReference) = _where(field, equalTo.value)
-fun Query.where(path: FieldPath, equalTo: DocumentReference) = _where(path, equalTo.value)
-fun Query.where(field: String, lessThan: Any? = null, greaterThan: Any? = null, arrayContains: Any? = null) = _where(field, lessThan?.value, greaterThan?.value, arrayContains?.value)
-fun Query.where(path: FieldPath, lessThan: Any? = null, greaterThan: Any? = null, arrayContains: Any? = null) = _where(path, lessThan?.value, greaterThan?.value, arrayContains?.value)
-fun Query.where(field: String, inArray: List? = null, arrayContainsAny: List? = null) = _where(field, inArray?.value, arrayContainsAny?.value)
-fun Query.where(path: FieldPath, inArray: List? = null, arrayContainsAny: List? = null) = _where(path, inArray?.value, arrayContainsAny?.value)
+@Deprecated("Deprecated in favor of using a [FilterBuilder]", replaceWith = ReplaceWith("where { path equalTo equalTo }", "dev.gitlive.firebase.firestore"))
+fun Query.where(path: FieldPath, equalTo: Any?) = where {
+ path equalTo equalTo
+}
+
+@Deprecated("Deprecated in favor of using a [FilterBuilder]", replaceWith = ReplaceWith("where { }", "dev.gitlive.firebase.firestore"))
+fun Query.where(field: String, lessThan: Any? = null, greaterThan: Any? = null, arrayContains: Any? = null) = where {
+ all(
+ *listOfNotNull(
+ lessThan?.let { field lessThan it },
+ greaterThan?.let { field greaterThan it },
+ arrayContains?.let { field contains it }
+ ).toTypedArray()
+ )
+}
+
+@Deprecated("Deprecated in favor of using a [FilterBuilder]", replaceWith = ReplaceWith("where { }", "dev.gitlive.firebase.firestore"))
+fun Query.where(path: FieldPath, lessThan: Any? = null, greaterThan: Any? = null, arrayContains: Any? = null) = where {
+ all(
+ *listOfNotNull(
+ lessThan?.let { path lessThan it },
+ greaterThan?.let { path greaterThan it },
+ arrayContains?.let { path contains it }
+ ).toTypedArray()
+ )
+}
+
+@Deprecated("Deprecated in favor of using a [FilterBuilder]", replaceWith = ReplaceWith("where { }", "dev.gitlive.firebase.firestore"))
+fun Query.where(field: String, inArray: List? = null, arrayContainsAny: List? = null) = where {
+ all(
+ *listOfNotNull(
+ inArray?.let { field inArray it },
+ arrayContainsAny?.let { field containsAny it },
+ ).toTypedArray()
+ )
+}
+
+@Deprecated("Deprecated in favor of using a [FilterBuilder]", replaceWith = ReplaceWith("where { }", "dev.gitlive.firebase.firestore"))
+fun Query.where(path: FieldPath, inArray: List? = null, arrayContainsAny: List? = null) = where {
+ all(
+ *listOfNotNull(
+ inArray?.let { path inArray it },
+ arrayContainsAny?.let { path containsAny it },
+ ).toTypedArray()
+ )
+}
fun Query.orderBy(field: String, direction: Direction = Direction.ASCENDING) = _orderBy(field, direction)
fun Query.orderBy(field: FieldPath, direction: Direction = Direction.ASCENDING) = _orderBy(field, direction)
fun Query.startAfter(document: DocumentSnapshot) = _startAfter(document)
-fun Query.startAfter(vararg fieldValues: Any) = _startAfter(*(fieldValues.map { it.value }.toTypedArray()))
+fun Query.startAfter(vararg fieldValues: Any) = _startAfter(*(fieldValues.mapNotNull { it.safeValue }.toTypedArray()))
fun Query.startAt(document: DocumentSnapshot) = _startAt(document)
-fun Query.startAt(vararg fieldValues: Any) = _startAt(*(fieldValues.map { it.value }.toTypedArray()))
+fun Query.startAt(vararg fieldValues: Any) = _startAt(*(fieldValues.mapNotNull { it.safeValue }.toTypedArray()))
fun Query.endBefore(document: DocumentSnapshot) = _endBefore(document)
-fun Query.endBefore(vararg fieldValues: Any) = _endBefore(*(fieldValues.map { it.value }.toTypedArray()))
+fun Query.endBefore(vararg fieldValues: Any) = _endBefore(*(fieldValues.mapNotNull { it.safeValue }.toTypedArray()))
fun Query.endAt(document: DocumentSnapshot) = _endAt(document)
-fun Query.endAt(vararg fieldValues: Any) = _endAt(*(fieldValues.map { it.value }.toTypedArray()))
+fun Query.endAt(vararg fieldValues: Any) = _endAt(*(fieldValues.mapNotNull { it.safeValue }.toTypedArray()))
+
+internal val Any.safeValue: Any get() = when (this) {
+ is Timestamp -> nativeValue
+ is GeoPoint -> nativeValue
+ is DocumentReference -> nativeValue
+ is Map<*, *> -> this.mapNotNull { (key, value) -> key?.let { it.safeValue to value?.safeValue } }
+ is Collection<*> -> this.mapNotNull { it?.safeValue }
+ else -> this
+}
expect class WriteBatch {
inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean = true, merge: Boolean = false): WriteBatch
diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FieldValueTests.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FieldValueTests.kt
index 9ed088172..6dc5fa45f 100644
--- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FieldValueTests.kt
+++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FieldValueTests.kt
@@ -1,6 +1,5 @@
package dev.gitlive.firebase.firestore
-import dev.gitlive.firebase.firebaseSerializer
import dev.gitlive.firebase.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt
index ada32568d..2030e844a 100644
--- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt
+++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt
@@ -10,14 +10,13 @@ import dev.gitlive.firebase.apps
import dev.gitlive.firebase.initialize
import dev.gitlive.firebase.runBlockingTest
import dev.gitlive.firebase.runTest
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.test.TestResult
import kotlinx.coroutines.withContext
+import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.nullable
import kotlin.random.Random
@@ -43,6 +42,7 @@ class FirebaseFirestoreTest {
val time: Double = 0.0,
val count: Int = 0,
val list: List = emptyList(),
+ val optional: String? = null,
)
@Serializable
@@ -51,6 +51,29 @@ class FirebaseFirestoreTest {
val time: BaseTimestamp?
)
+ companion object {
+ val testOne = FirestoreTest(
+ "aaa",
+ 0.0,
+ 1,
+ listOf("a", "aa", "aaa"),
+ "notNull",
+ )
+ val testTwo = FirestoreTest(
+ "bbb",
+ 0.0,
+ 2,
+ listOf("b", "bb", "ccc")
+ )
+ val testThree = FirestoreTest(
+ "ccc",
+ 1.0,
+ 3,
+ listOf("c", "cc", "ccc"),
+ "notNull",
+ )
+ }
+
lateinit var firestore: FirebaseFirestore
@BeforeTest
@@ -510,33 +533,257 @@ class FirebaseFirestoreTest {
collection.add(DocumentWithTimestamp.serializer(), DocumentWithTimestamp(pastTimestamp))
collection.add(DocumentWithTimestamp.serializer(), DocumentWithTimestamp(futureTimestamp))
- val equalityQueryResult = collection.where(
- path = FieldPath(DocumentWithTimestamp::time.name),
- equalTo = pastTimestamp
- ).get().documents.map { it.data(DocumentWithTimestamp.serializer()) }.toSet()
+ val equalityQueryResult = collection.where {
+ FieldPath(DocumentWithTimestamp::time.name) equalTo pastTimestamp
+ }.get().documents.map { it.data(DocumentWithTimestamp.serializer()) }.toSet()
assertEquals(setOf(DocumentWithTimestamp(pastTimestamp)), equalityQueryResult)
- val gtQueryResult = collection.where(
- path = FieldPath(DocumentWithTimestamp::time.name),
- greaterThan = timestamp
- ).get().documents.map { it.data(DocumentWithTimestamp.serializer()) }.toSet()
+ val gtQueryResult = collection.where {
+ FieldPath(DocumentWithTimestamp::time.name) greaterThan timestamp
+ }.get().documents.map { it.data(DocumentWithTimestamp.serializer()) }.toSet()
assertEquals(setOf(DocumentWithTimestamp(futureTimestamp)), gtQueryResult)
}
- private suspend fun setupFirestoreData() {
+ @Test
+ fun testQueryEqualTo() = runTest {
+ setupFirestoreData()
+
+ val fieldQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { "prop1" equalTo testOne.prop1 }
+
+ fieldQuery.assertDocuments(FirestoreTest.serializer(), testOne)
+
+ val pathQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { FieldPath(FirestoreTest::prop1.name) equalTo testTwo.prop1 }
+
+ pathQuery.assertDocuments(FirestoreTest.serializer(), testTwo)
+
+ val nullableQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { FieldPath(FirestoreTest::optional.name) equalTo null }
+
+ nullableQuery.assertDocuments(FirestoreTest.serializer(), testTwo)
+ }
+
+ @Test
+ fun testQueryNotEqualTo() = runTest {
+ setupFirestoreData()
+
+ val fieldQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { "prop1" notEqualTo testOne.prop1 }
+
+ fieldQuery.assertDocuments(FirestoreTest.serializer(), testTwo, testThree)
+
+ val pathQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { FieldPath(FirestoreTest::prop1.name) notEqualTo testTwo.prop1 }
+
+ pathQuery.assertDocuments(FirestoreTest.serializer(), testOne, testThree)
+
+ val nullableQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { FieldPath(FirestoreTest::optional.name) notEqualTo null }
+
+ nullableQuery.assertDocuments(FirestoreTest.serializer(), testOne, testThree)
+ }
+
+ @Test
+ fun testQueryLessThan() = runTest {
+ setupFirestoreData()
+
+ val fieldQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { "count" lessThan testThree.count }
+
+ fieldQuery.assertDocuments(FirestoreTest.serializer(), testOne, testTwo)
+
+ val pathQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { FieldPath(FirestoreTest::count.name) lessThan testTwo.count }
+
+ pathQuery.assertDocuments(FirestoreTest.serializer(), testOne)
+ }
+
+ @Test
+ fun testQueryGreaterThan() = runTest {
+ setupFirestoreData()
+
+ val fieldQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { "count" greaterThan testOne.count }
+
+ fieldQuery.assertDocuments(FirestoreTest.serializer(), testTwo, testThree)
+
+ val pathQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { FieldPath(FirestoreTest::count.name) greaterThan testTwo.count }
+
+ pathQuery.assertDocuments(FirestoreTest.serializer(), testThree)
+ }
+
+ @Test
+ fun testQueryLessThanOrEqualTo() = runTest {
+ setupFirestoreData()
+
+ val fieldQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { "count" lessThanOrEqualTo testOne.count }
+
+ fieldQuery.assertDocuments(FirestoreTest.serializer(), testOne)
+
+ val pathQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { FieldPath(FirestoreTest::count.name) lessThanOrEqualTo testTwo.count }
+
+ pathQuery.assertDocuments(FirestoreTest.serializer(), testOne, testTwo)
+ }
+
+ @Test
+ fun testQueryGreaterThanOrEqualTo() = runTest {
+ setupFirestoreData()
+
+ val fieldQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { "count" greaterThanOrEqualTo testThree.count }
+
+ fieldQuery.assertDocuments(FirestoreTest.serializer(), testThree)
+
+ val pathQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { FieldPath(FirestoreTest::count.name) greaterThanOrEqualTo testTwo.count }
+
+ pathQuery.assertDocuments(FirestoreTest.serializer(), testTwo, testThree)
+ }
+
+ @Test
+ fun testQueryArrayContains() = runTest {
+ setupFirestoreData()
+
+ val fieldQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { "list" contains "a" }
+
+ fieldQuery.assertDocuments(FirestoreTest.serializer(), testOne)
+
+ val pathQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { FieldPath(FirestoreTest::list.name) contains "ccc" }
+
+ pathQuery.assertDocuments(FirestoreTest.serializer(), testThree, testTwo)
+ }
+
+ @Test
+ fun testQueryArrayContainsAny() = runTest {
+ setupFirestoreData()
+
+ val fieldQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { "list" containsAny listOf("a", "b") }
+
+ fieldQuery.assertDocuments(FirestoreTest.serializer(), testOne, testTwo)
+
+ val pathQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { FieldPath(FirestoreTest::list.name) containsAny listOf("c", "d") }
+
+ pathQuery.assertDocuments(FirestoreTest.serializer(), testThree)
+ }
+
+ @Test
+ fun testQueryInArray() = runTest {
+ setupFirestoreData()
+
+ val fieldQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { "prop1" inArray listOf("aaa", "bbb") }
+
+ fieldQuery.assertDocuments(FirestoreTest.serializer(), testOne, testTwo)
+
+ val pathQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { FieldPath(FirestoreTest::prop1.name) inArray listOf("ccc", "ddd") }
+
+ pathQuery.assertDocuments(FirestoreTest.serializer(), testThree)
+ }
+
+ @Test
+ fun testQueryNotInArray() = runTest {
+ setupFirestoreData()
+
+ val fieldQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { "prop1" notInArray listOf("aaa", "bbb") }
+
+ fieldQuery.assertDocuments(FirestoreTest.serializer(), testThree)
+
+ val pathQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where { FieldPath(FirestoreTest::prop1.name) notInArray listOf("ccc", "ddd") }
+
+ pathQuery.assertDocuments(FirestoreTest.serializer(), testOne, testTwo)
+ }
+
+ @Test
+ fun testCompoundQuery() = runTest {
+ setupFirestoreData()
+
+ val andQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where {
+ FieldPath(FirestoreTest::prop1.name) inArray listOf("aaa", "bbb") and (FieldPath(FirestoreTest::count.name) equalTo 1)
+ }
+ andQuery.assertDocuments(FirestoreTest.serializer(), testOne)
+
+ val orQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where {
+ FieldPath(FirestoreTest::prop1.name) equalTo "aaa" or (FieldPath(FirestoreTest::count.name) equalTo 2)
+ }
+ orQuery.assertDocuments(FirestoreTest.serializer(), testOne, testTwo)
+
+ val andOrQuery = firestore
+ .collection("testFirestoreQuerying")
+ .where {
+ all(
+ any(
+ FieldPath(FirestoreTest::prop1.name) equalTo "aaa",
+ FieldPath(FirestoreTest::count.name) equalTo 2,
+ )!!,
+ FieldPath(FirestoreTest::list.name) contains "a"
+ )
+ }
+ andOrQuery.assertDocuments(FirestoreTest.serializer(), testOne)
+ }
+
+ private suspend fun setupFirestoreData(
+ documentOne: FirestoreTest = testOne,
+ documentTwo: FirestoreTest = testTwo,
+ documentThree: FirestoreTest = testThree
+ ) {
firestore.collection("testFirestoreQuerying")
.document("one")
- .set(FirestoreTest.serializer(), FirestoreTest("aaa"))
+ .set(FirestoreTest.serializer(), documentOne)
firestore.collection("testFirestoreQuerying")
.document("two")
- .set(FirestoreTest.serializer(), FirestoreTest("bbb"))
+ .set(FirestoreTest.serializer(), documentTwo)
firestore.collection("testFirestoreQuerying")
.document("three")
- .set(FirestoreTest.serializer(), FirestoreTest("ccc"))
+ .set(FirestoreTest.serializer(), documentThree)
}
-
+
+ private suspend fun Query.assertDocuments(serializer: KSerializer, vararg expected: T) {
+ val documents = get().documents
+ assertEquals(expected.size, documents.size)
+ documents.forEachIndexed { index, documentSnapshot ->
+ assertEquals(expected[index], documentSnapshot.data(serializer))
+ }
+ }
+
private suspend fun nonSkippedDelay(timeout: Long) = withContext(Dispatchers.Default) {
delay(timeout)
}
diff --git a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt
index 12fa0936f..48a900271 100644
--- a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt
+++ b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt
@@ -302,39 +302,38 @@ actual open class Query(open val ios: FIRQuery) {
awaitClose { listener.remove() }
}
- internal actual fun _where(field: String, equalTo: Any?) = Query(ios.queryWhereField(field, isEqualTo = equalTo!!))
- internal actual fun _where(path: FieldPath, equalTo: Any?) = Query(ios.queryWhereFieldPath(path.ios, isEqualTo = equalTo!!))
-
- internal actual fun _where(field: String, equalTo: DocumentReference) = Query(ios.queryWhereField(field, isEqualTo = equalTo.ios))
- internal actual fun _where(path: FieldPath, equalTo: DocumentReference) = Query(ios.queryWhereFieldPath(path.ios, isEqualTo = equalTo.ios))
-
- internal actual fun _where(field: String, lessThan: Any?, greaterThan: Any?, arrayContains: Any?) = Query(
- (lessThan?.let { ios.queryWhereField(field, isLessThan = it) } ?: ios).let { ios2 ->
- (greaterThan?.let { ios2.queryWhereField(field, isGreaterThan = it) } ?: ios2).let { ios3 ->
- arrayContains?.let { ios3.queryWhereField(field, arrayContains = it) } ?: ios3
- }
- }
- )
-
- internal actual fun _where(path: FieldPath, lessThan: Any?, greaterThan: Any?, arrayContains: Any?) = Query(
- (lessThan?.let { ios.queryWhereFieldPath(path.ios, isLessThan = it) } ?: ios).let { ios2 ->
- (greaterThan?.let { ios2.queryWhereFieldPath(path.ios, isGreaterThan = it) } ?: ios2).let { ios3 ->
- arrayContains?.let { ios3.queryWhereFieldPath(path.ios, arrayContains = it) } ?: ios3
- }
- }
+ internal actual fun where(filter: Filter): Query = Query(
+ ios.queryWhereFilter(filter.toFIRFilter())
)
- internal actual fun _where(field: String, inArray: List?, arrayContainsAny: List?) = Query(
- (inArray?.let { ios.queryWhereField(field, `in` = it) } ?: ios).let { ios2 ->
- arrayContainsAny?.let { ios2.queryWhereField(field, arrayContainsAny = arrayContainsAny) } ?: ios2
+ private fun Filter.toFIRFilter(): FIRFilter = when (this) {
+ is Filter.And -> FIRFilter.andFilterWithFilters(filters.map { it.toFIRFilter() })
+ is Filter.Or -> FIRFilter.orFilterWithFilters(filters.map { it.toFIRFilter() })
+ is Filter.Field -> when (constraint) {
+ is WhereConstraint.EqualTo -> FIRFilter.filterWhereField(field, isEqualTo = constraint.safeValue ?: NSNull.`null`())
+ is WhereConstraint.NotEqualTo -> FIRFilter.filterWhereField(field, isNotEqualTo = constraint.safeValue ?: NSNull.`null`())
+ is WhereConstraint.LessThan -> FIRFilter.filterWhereField(field, isLessThan = constraint.safeValue)
+ is WhereConstraint.GreaterThan -> FIRFilter.filterWhereField(field, isGreaterThan = constraint.safeValue)
+ is WhereConstraint.LessThanOrEqualTo -> FIRFilter.filterWhereField(field, isLessThanOrEqualTo = constraint.safeValue)
+ is WhereConstraint.GreaterThanOrEqualTo -> FIRFilter.filterWhereField(field, isGreaterThanOrEqualTo = constraint.safeValue)
+ is WhereConstraint.ArrayContains -> FIRFilter.filterWhereField(field, arrayContains = constraint.safeValue)
+ is WhereConstraint.ArrayContainsAny -> FIRFilter.filterWhereField(field, arrayContainsAny = constraint.safeValues)
+ is WhereConstraint.InArray -> FIRFilter.filterWhereField(field, `in` = constraint.safeValues)
+ is WhereConstraint.NotInArray -> FIRFilter.filterWhereField(field, notIn = constraint.safeValues)
}
- )
-
- internal actual fun _where(path: FieldPath, inArray: List?, arrayContainsAny: List?) = Query(
- (inArray?.let { ios.queryWhereFieldPath(path.ios, `in` = it) } ?: ios).let { ios2 ->
- arrayContainsAny?.let { ios2.queryWhereFieldPath(path.ios, arrayContainsAny = arrayContainsAny) } ?: ios2
+ is Filter.Path -> when (constraint) {
+ is WhereConstraint.EqualTo -> FIRFilter.filterWhereFieldPath(path.ios, isEqualTo = constraint.safeValue ?: NSNull.`null`())
+ is WhereConstraint.NotEqualTo -> FIRFilter.filterWhereFieldPath(path.ios, isNotEqualTo = constraint.safeValue ?: NSNull.`null`())
+ is WhereConstraint.LessThan -> FIRFilter.filterWhereFieldPath(path.ios, isLessThan = constraint.safeValue)
+ is WhereConstraint.GreaterThan -> FIRFilter.filterWhereFieldPath(path.ios, isGreaterThan = constraint.safeValue)
+ is WhereConstraint.LessThanOrEqualTo -> FIRFilter.filterWhereFieldPath(path.ios, isLessThanOrEqualTo = constraint.safeValue)
+ is WhereConstraint.GreaterThanOrEqualTo -> FIRFilter.filterWhereFieldPath(path.ios, isGreaterThanOrEqualTo = constraint.safeValue)
+ is WhereConstraint.ArrayContains -> FIRFilter.filterWhereFieldPath(path.ios, arrayContains = constraint.safeValue)
+ is WhereConstraint.ArrayContainsAny -> FIRFilter.filterWhereFieldPath(path.ios, arrayContainsAny = constraint.safeValues)
+ is WhereConstraint.InArray -> FIRFilter.filterWhereFieldPath(path.ios, `in` = constraint.safeValues)
+ is WhereConstraint.NotInArray -> FIRFilter.filterWhereFieldPath(path.ios, notIn = constraint.safeValues)
}
- )
+ }
internal actual fun _orderBy(field: String, direction: Direction) = Query(ios.queryOrderedByField(field, direction == Direction.DESCENDING))
internal actual fun _orderBy(field: FieldPath, direction: Direction) = Query(ios.queryOrderedByFieldPath(field.ios, direction == Direction.DESCENDING))
diff --git a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/externals/firestore.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/externals/firestore.kt
index 6fa365353..7f5065a40 100644
--- a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/externals/firestore.kt
+++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/externals/firestore.kt
@@ -155,6 +155,10 @@ external fun where(field: String, opStr: String, value: Any?): QueryConstraint
external fun where(field: FieldPath, opStr: String, value: Any?): QueryConstraint
+external fun and(vararg queryConstraints: QueryConstraint): QueryConstraint
+
+external fun or(vararg queryConstraints: QueryConstraint): QueryConstraint
+
external fun writeBatch(firestore: Firestore): WriteBatch
external interface Firestore {
diff --git a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt
index b419a5ab5..e63142aa6 100644
--- a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt
+++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt
@@ -317,43 +317,43 @@ actual open class Query(open val js: JsQuery) {
actual fun limit(limit: Number) = Query(query(js, jsLimit(limit)))
- internal actual fun _where(field: String, equalTo: Any?) = rethrow { Query(query(js, jsWhere(field, "==", equalTo))) }
- internal actual fun _where(path: FieldPath, equalTo: Any?) = rethrow { Query(query(js, jsWhere(path.js, "==", equalTo))) }
-
- internal actual fun _where(field: String, equalTo: DocumentReference) = rethrow { Query(query(js, jsWhere(field, "==", equalTo.js))) }
- internal actual fun _where(path: FieldPath, equalTo: DocumentReference) = rethrow { Query(query(js, jsWhere(path.js, "==", equalTo.js))) }
+ internal actual fun where(filter: Filter): Query = Query(
+ query(js, filter.toQueryConstraint())
+ )
- internal actual fun _where(field: String, lessThan: Any?, greaterThan: Any?, arrayContains: Any?) = rethrow {
- Query(
- (lessThan?.let { query(js, jsWhere(field, "<", it)) } ?: js).let { js2 ->
- (greaterThan?.let { query(js2, jsWhere(field, ">", it)) } ?: js2).let { js3 ->
- arrayContains?.let { query(js3, jsWhere(field, "array-contains", it)) } ?: js3
- }
+ private fun Filter.toQueryConstraint(): QueryConstraint = when (this) {
+ is Filter.And -> and(*filters.map { it.toQueryConstraint() }.toTypedArray())
+ is Filter.Or -> or(*filters.map { it.toQueryConstraint() }.toTypedArray())
+ is Filter.Field -> {
+ val value = when (constraint) {
+ is WhereConstraint.ForNullableObject -> constraint.safeValue
+ is WhereConstraint.ForObject -> constraint.safeValue
+ is WhereConstraint.ForArray -> constraint.safeValues.toTypedArray()
}
- )
- }
-
- internal actual fun _where(path: FieldPath, lessThan: Any?, greaterThan: Any?, arrayContains: Any?) = rethrow {
- Query(
- (lessThan?.let { query(js, jsWhere(path.js, "<", it)) } ?: js).let { js2 ->
- (greaterThan?.let { query(js2, jsWhere(path.js, ">", it)) } ?: js2).let { js3 ->
- arrayContains?.let { query(js3, jsWhere(path.js, "array-contains", it)) } ?: js3
- }
+ jsWhere(field, constraint.filterOp, value)
+ }
+ is Filter.Path -> {
+ val value = when (constraint) {
+ is WhereConstraint.ForNullableObject -> constraint.safeValue
+ is WhereConstraint.ForObject -> constraint.safeValue
+ is WhereConstraint.ForArray -> constraint.safeValues.toTypedArray()
}
- )
- }
-
- internal actual fun _where(field: String, inArray: List?, arrayContainsAny: List?) = Query(
- (inArray?.let { query(js, jsWhere(field, "in", it.toTypedArray())) } ?: js).let { js2 ->
- arrayContainsAny?.let { query(js2, jsWhere(field, "array-contains-any", it.toTypedArray())) } ?: js2
+ jsWhere(path.js, constraint.filterOp, value)
}
- )
+ }
- internal actual fun _where(path: FieldPath, inArray: List?, arrayContainsAny: List?) = Query(
- (inArray?.let { query(js, jsWhere(path.js, "in", it.toTypedArray())) } ?: js).let { js2 ->
- arrayContainsAny?.let { query(js2, jsWhere(path.js, "array-contains-any", it.toTypedArray())) } ?: js2
- }
- )
+ private val WhereConstraint.filterOp: String get() = when (this) {
+ is WhereConstraint.EqualTo -> "=="
+ is WhereConstraint.NotEqualTo -> "!="
+ is WhereConstraint.LessThan -> "<"
+ is WhereConstraint.LessThanOrEqualTo -> "<="
+ is WhereConstraint.GreaterThan -> ">"
+ is WhereConstraint.GreaterThanOrEqualTo -> ">="
+ is WhereConstraint.ArrayContains -> "array-contains"
+ is WhereConstraint.ArrayContainsAny -> "array-contains-any"
+ is WhereConstraint.InArray -> "in"
+ is WhereConstraint.NotInArray -> "not-in"
+ }
internal actual fun _orderBy(field: String, direction: Direction) = rethrow {
Query(query(js, orderBy(field, direction.jsString)))
diff --git a/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/Geopoint.kt b/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt
similarity index 100%
rename from firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/Geopoint.kt
rename to firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt
diff --git a/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt
index 7ad3937bc..590cdb234 100644
--- a/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt
+++ b/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt
@@ -2,13 +2,13 @@
* Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license.
*/
-@file:JvmName("JVM")
+@file:JvmName("android")
package dev.gitlive.firebase.firestore
-import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.TaskExecutors
import com.google.firebase.firestore.*
import dev.gitlive.firebase.*
+import dev.gitlive.firebase.firestore.FirebaseFirestoreException
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@@ -21,6 +21,10 @@ import kotlinx.serialization.SerializationStrategy
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
+import com.google.firebase.firestore.Query as AndroidQuery
+import com.google.firebase.firestore.FieldPath as AndroidFieldPath
+import com.google.firebase.firestore.Filter as AndroidFilter
+
actual val Firebase.firestore get() =
FirebaseFirestore(com.google.firebase.firestore.FirebaseFirestore.getInstance())
@@ -308,6 +312,7 @@ actual class DocumentReference actual constructor(internal actual val nativeValu
snapshot?.let { trySend(DocumentSnapshot(snapshot)) }
exception?.let { close(exception) }
}
+
override fun equals(other: Any?): Boolean =
this === other || other is DocumentReference && nativeValue == other.nativeValue
override fun hashCode(): Int = nativeValue.hashCode()
@@ -326,7 +331,7 @@ actual class DocumentReference actual constructor(internal actual val nativeValu
}
}
-actual open class Query(open val android: com.google.firebase.firestore.Query) {
+actual open class Query(open val android: AndroidQuery) {
actual suspend fun get() = QuerySnapshot(android.get().await())
@@ -342,40 +347,78 @@ actual open class Query(open val android: com.google.firebase.firestore.Query) {
exception?.let { close(exception) }
}
- internal actual fun _where(field: String, equalTo: Any?) = Query(android.whereEqualTo(field, equalTo))
- internal actual fun _where(path: FieldPath, equalTo: Any?) = Query(android.whereEqualTo(path.android, equalTo))
-
- internal actual fun _where(field: String, equalTo: DocumentReference) = Query(android.whereEqualTo(field, equalTo.android))
- internal actual fun _where(path: FieldPath, equalTo: DocumentReference) = Query(android.whereEqualTo(path.android, equalTo.android))
-
- internal actual fun _where(field: String, lessThan: Any?, greaterThan: Any?, arrayContains: Any?) = Query(
- (lessThan?.let { android.whereLessThan(field, it) } ?: android).let { android2 ->
- (greaterThan?.let { android2.whereGreaterThan(field, it) } ?: android2).let { android3 ->
- arrayContains?.let { android3.whereArrayContains(field, it) } ?: android3
- }
- }
+ internal actual fun where(filter: Filter) = Query(
+ filter.parseForQuery(android)
)
- internal actual fun _where(path: FieldPath, lessThan: Any?, greaterThan: Any?, arrayContains: Any?) = Query(
- (lessThan?.let { android.whereLessThan(path.android, it) } ?: android).let { android2 ->
- (greaterThan?.let { android2.whereGreaterThan(path.android, it) } ?: android2).let { android3 ->
- arrayContains?.let { android3.whereArrayContains(path.android, it) } ?: android3
- }
+ private fun Filter.parseForQuery(query: AndroidQuery): AndroidQuery = when (this) {
+ is Filter.And -> filters.fold(query) { acc, andFilter ->
+ andFilter.parseForQuery(acc)
}
- )
-
- internal actual fun _where(field: String, inArray: List?, arrayContainsAny: List?) = Query(
- (inArray?.let { android.whereIn(field, it) } ?: android).let { android2 ->
- arrayContainsAny?.let { android2.whereArrayContainsAny(field, it) } ?: android2
+ is Filter.Or -> throw FirebaseFirestoreException(
+ "Filter.Or not supported on JVM",
+ com.google.firebase.firestore.FirebaseFirestoreException.Code.INVALID_ARGUMENT
+ )
+ is Filter.Field -> {
+ when (constraint) {
+ is WhereConstraint.ForNullableObject -> {
+ val modifier: AndroidQuery.(String, Any?) -> AndroidQuery = when (constraint) {
+ is WhereConstraint.EqualTo -> AndroidQuery::whereEqualTo
+ is WhereConstraint.NotEqualTo -> AndroidQuery::whereNotEqualTo
+ }
+ modifier.invoke(query, field, constraint.safeValue)
+ }
+ is WhereConstraint.ForObject -> {
+ val modifier: AndroidQuery.(String, Any) -> AndroidQuery = when (constraint) {
+ is WhereConstraint.LessThan -> AndroidQuery::whereLessThan
+ is WhereConstraint.GreaterThan -> AndroidQuery::whereGreaterThan
+ is WhereConstraint.LessThanOrEqualTo -> AndroidQuery::whereLessThanOrEqualTo
+ is WhereConstraint.GreaterThanOrEqualTo -> AndroidQuery::whereGreaterThanOrEqualTo
+ is WhereConstraint.ArrayContains -> AndroidQuery::whereArrayContains
+ }
+ modifier.invoke(query, field, constraint.safeValue)
+ }
+ is WhereConstraint.ForArray -> {
+ val modifier: AndroidQuery.(String, List) -> AndroidQuery = when (constraint) {
+ is WhereConstraint.InArray -> AndroidQuery::whereIn
+ is WhereConstraint.ArrayContainsAny -> AndroidQuery::whereArrayContainsAny
+ is WhereConstraint.NotInArray -> AndroidQuery::whereNotIn
+ }
+ modifier.invoke(query, field, constraint.safeValues)
+ }
+ }
}
- )
-
- internal actual fun _where(path: FieldPath, inArray: List?, arrayContainsAny: List?) = Query(
- (inArray?.let { android.whereIn(path.android, it) } ?: android).let { android2 ->
- arrayContainsAny?.let { android2.whereArrayContainsAny(path.android, it) } ?: android2
+ is Filter.Path -> {
+ when (constraint) {
+ is WhereConstraint.ForNullableObject -> {
+ val modifier: AndroidQuery.(AndroidFieldPath, Any?) -> AndroidQuery = when (constraint) {
+ is WhereConstraint.EqualTo -> AndroidQuery::whereEqualTo
+ is WhereConstraint.NotEqualTo -> AndroidQuery::whereNotEqualTo
+ }
+ modifier.invoke(query, path.android, constraint.safeValue)
+ }
+ is WhereConstraint.ForObject -> {
+ val modifier: AndroidQuery.(AndroidFieldPath, Any) -> AndroidQuery = when (constraint) {
+ is WhereConstraint.LessThan -> AndroidQuery::whereLessThan
+ is WhereConstraint.GreaterThan -> AndroidQuery::whereGreaterThan
+ is WhereConstraint.LessThanOrEqualTo -> AndroidQuery::whereLessThanOrEqualTo
+ is WhereConstraint.GreaterThanOrEqualTo -> AndroidQuery::whereGreaterThanOrEqualTo
+ is WhereConstraint.ArrayContains -> AndroidQuery::whereArrayContains
+ }
+ modifier.invoke(query, path.android, constraint.safeValue)
+ }
+ is WhereConstraint.ForArray -> {
+ val modifier: AndroidQuery.(AndroidFieldPath, List) -> AndroidQuery = when (constraint) {
+ is WhereConstraint.InArray -> AndroidQuery::whereIn
+ is WhereConstraint.ArrayContainsAny -> AndroidQuery::whereArrayContainsAny
+ is WhereConstraint.NotInArray -> AndroidQuery::whereNotIn
+ }
+ modifier.invoke(query, path.android, constraint.safeValues)
+ }
+ }
}
- )
-
+ }
+
internal actual fun _orderBy(field: String, direction: Direction) = Query(android.orderBy(field, direction))
internal actual fun _orderBy(field: FieldPath, direction: Direction) = Query(android.orderBy(field.android, direction))
diff --git a/gradle.properties b/gradle.properties
index a4ff5b364..1c00629dd 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -64,6 +64,6 @@ gradlePluginVersion=8.1.3
kotlinVersion=1.9.21
coroutinesVersion=1.7.3
serializationVersion=1.6.0
-firebaseBoMVersion=32.5.0
+firebaseBoMVersion=32.7.0
apiVersion=1.8
languageVersion=1.9