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: + +

Infix notation

+ +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 + ) + ) +} ```

Operator overloading

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