From ab109f543a08f5241afb77d1161d8b2a334fcad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Sk=C3=A1la?= Date: Sat, 16 Dec 2023 09:55:45 +0100 Subject: [PATCH 01/17] Wait for emulator to start --- .github/actions/setup_test_action/action.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/actions/setup_test_action/action.yml b/.github/actions/setup_test_action/action.yml index 1b18c9db4..1fc129725 100644 --- a/.github/actions/setup_test_action/action.yml +++ b/.github/actions/setup_test_action/action.yml @@ -24,7 +24,12 @@ 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://localhost:9099 + wait-on http://localhost:8080 + wait-on http://localhost:9000 + wait-on http://localhost:9199 \ No newline at end of file From 6def555f0b4bac195c7101bc967d5f17d30505fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Sk=C3=A1la?= Date: Sat, 16 Dec 2023 10:16:02 +0100 Subject: [PATCH 02/17] Change host to 127.0.0.1, add timeout and debug logs --- .github/actions/setup_test_action/action.yml | 12 ++++++++---- .github/workflows/pull_request.yml | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/actions/setup_test_action/action.yml b/.github/actions/setup_test_action/action.yml index 1fc129725..6c5eb7d47 100644 --- a/.github/actions/setup_test_action/action.yml +++ b/.github/actions/setup_test_action/action.yml @@ -29,7 +29,11 @@ runs: shell: bash run: | firebase emulators:start --config=./test/firebase.json & - wait-on http://localhost:9099 - wait-on http://localhost:8080 - wait-on http://localhost:9000 - wait-on http://localhost:9199 \ No newline at end of file + echo "Waiting for Firebase Auth emulator to start..." + wait-on http://127.0.0.1:9099 + echo "Waiting for Firebase Firestore emulator to start..." + wait-on http://127.0.0.1:8080 + echo "Waiting for Firebase Database emulator to start..." + wait-on http://127.0.0.1:9000 + echo "Waiting for Firebase Storage emulator to start..." + wait-on http://127.0.0.1:9199 \ No newline at end of file diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 31d9d58e2..55e4c9e21 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 From 57c1391177afdea1e2c74006d1284de2f0415524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Sk=C3=A1la?= Date: Sat, 16 Dec 2023 10:24:49 +0100 Subject: [PATCH 03/17] Split start and wait into 2 steps --- .github/actions/setup_test_action/action.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup_test_action/action.yml b/.github/actions/setup_test_action/action.yml index 6c5eb7d47..be2743da9 100644 --- a/.github/actions/setup_test_action/action.yml +++ b/.github/actions/setup_test_action/action.yml @@ -26,9 +26,11 @@ runs: shell: bash run: npm install -g firebase-tools wait-on - name: Start Firebase emulator + shell: bash + run: firebase emulators:start --config=./test/firebase.json & + - name: Wait for Firebase emulator to start shell: bash run: | - firebase emulators:start --config=./test/firebase.json & echo "Waiting for Firebase Auth emulator to start..." wait-on http://127.0.0.1:9099 echo "Waiting for Firebase Firestore emulator to start..." From 798a99f671062293f422c28195037d9fda5d9597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Sk=C3=A1la?= Date: Sat, 16 Dec 2023 11:03:41 +0100 Subject: [PATCH 04/17] Wait only for auth emulator --- .github/actions/setup_test_action/action.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/actions/setup_test_action/action.yml b/.github/actions/setup_test_action/action.yml index be2743da9..ba5441480 100644 --- a/.github/actions/setup_test_action/action.yml +++ b/.github/actions/setup_test_action/action.yml @@ -26,16 +26,7 @@ runs: shell: bash run: npm install -g firebase-tools wait-on - name: Start Firebase emulator - shell: bash - run: firebase emulators:start --config=./test/firebase.json & - - name: Wait for Firebase emulator to start shell: bash run: | - echo "Waiting for Firebase Auth emulator to start..." + firebase emulators:start --config=./test/firebase.json & wait-on http://127.0.0.1:9099 - echo "Waiting for Firebase Firestore emulator to start..." - wait-on http://127.0.0.1:8080 - echo "Waiting for Firebase Database emulator to start..." - wait-on http://127.0.0.1:9000 - echo "Waiting for Firebase Storage emulator to start..." - wait-on http://127.0.0.1:9199 \ No newline at end of file From 9963accae440b72986ccfff1555bb1dfae5c350d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Sk=C3=A1la?= Date: Tue, 19 Dec 2023 22:24:06 +0100 Subject: [PATCH 05/17] Restart tests --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 55e4c9e21..dcfd8346c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -113,4 +113,4 @@ jobs: if: failure() with: name: "Firebase Debug Log" - path: "**/firebase-debug.log" + path: "**/firebase-debug.log" \ No newline at end of file From bd145c4783e391a0f8ff701ffe9ae12e626332df Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Wed, 20 Dec 2023 13:07:10 +0100 Subject: [PATCH 06/17] IMprovements to Where queries --- .../gitlive/firebase/firestore/firestore.kt | 102 +++++++++++++----- .../gitlive/firebase/firestore/firestore.kt | 61 +++++++---- .../gitlive/firebase/firestore/firestore.kt | 52 ++++----- .../gitlive/firebase/firestore/firestore.kt | 60 +++++------ 4 files changed, 167 insertions(+), 108 deletions(-) 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 dcc6a2ebc..014303922 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 @@ -5,7 +5,6 @@ @file:JvmName("android") package dev.gitlive.firebase.firestore -import com.google.android.gms.tasks.Task import com.google.firebase.firestore.* import dev.gitlive.firebase.* import kotlinx.coroutines.channels.awaitClose @@ -17,6 +16,9 @@ import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationStrategy +import com.google.firebase.firestore.Query as AndroidQuery +import com.google.firebase.firestore.FieldPath as AndroidFieldPath + actual val Firebase.firestore get() = FirebaseFirestore(com.google.firebase.firestore.FirebaseFirestore.getInstance()) @@ -283,7 +285,7 @@ actual class DocumentReference actual constructor(internal actual val nativeValu override fun toString(): String = nativeValue.toString() } -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()) @@ -306,39 +308,85 @@ actual open class Query(open val android: com.google.firebase.firestore.Query) { awaitClose { listener.remove() } } - 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(field: String, vararg clauses: WhereClause) = Query( + clauses.fold(android) { query, clause -> + when (clause) { + is WhereClause.ForNullableObject -> { + val modifier: AndroidQuery.(String, Any?) -> AndroidQuery = when (clause) { + is WhereClause.EqualTo -> AndroidQuery::whereEqualTo + is WhereClause.NotEqualTo -> AndroidQuery::whereNotEqualTo + } + modifier.invoke(query, field, clause.safeValue) + } + is WhereClause.ForObject -> { + val modifier: AndroidQuery.(String, Any) -> AndroidQuery = when (clause) { + is WhereClause.LessThan -> AndroidQuery::whereLessThan + is WhereClause.GreaterThan -> AndroidQuery::whereGreaterThan + is WhereClause.LessThanOrEqualTo -> AndroidQuery::whereLessThanOrEqualTo + is WhereClause.GreaterThanOrEqualTo -> AndroidQuery::whereGreaterThanOrEqualTo + is WhereClause.ArrayContains -> AndroidQuery::whereArrayContains + } + modifier.invoke(query, field, clause.safeValue) + } + is WhereClause.ForArray -> { + val modifier: AndroidQuery.(String, List) -> AndroidQuery = when (clause) { + is WhereClause.InArray -> AndroidQuery::whereIn + is WhereClause.ArrayContainsAny -> AndroidQuery::whereArrayContainsAny + is WhereClause.NotInArray -> AndroidQuery::whereNotIn + } + modifier.invoke(query, field, clause.safeValues) + } } } ) - 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 + internal actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( + clauses.fold(android) { query, clause -> + when (clause) { + is WhereClause.ForNullableObject -> { + val modifier: AndroidQuery.(AndroidFieldPath, Any?) -> AndroidQuery = when (clause) { + is WhereClause.EqualTo -> AndroidQuery::whereEqualTo + is WhereClause.NotEqualTo -> AndroidQuery::whereNotEqualTo + } + modifier.invoke(query, path.android, clause.safeValue) + } + is WhereClause.ForObject -> { + val modifier: AndroidQuery.(AndroidFieldPath, Any) -> AndroidQuery = when (clause) { + is WhereClause.LessThan -> AndroidQuery::whereLessThan + is WhereClause.GreaterThan -> AndroidQuery::whereGreaterThan + is WhereClause.LessThanOrEqualTo -> AndroidQuery::whereLessThanOrEqualTo + is WhereClause.GreaterThanOrEqualTo -> AndroidQuery::whereGreaterThanOrEqualTo + is WhereClause.ArrayContains -> AndroidQuery::whereArrayContains + } + modifier.invoke(query, path.android, clause.safeValue) + } + is WhereClause.ForArray -> { + val modifier: AndroidQuery.(AndroidFieldPath, List) -> AndroidQuery = when (clause) { + is WhereClause.InArray -> AndroidQuery::whereIn + is WhereClause.ArrayContainsAny -> AndroidQuery::whereArrayContainsAny + is WhereClause.NotInArray -> AndroidQuery::whereNotIn + } + modifier.invoke(query, path.android, clause.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 - } - ) + private fun AndroidQuery.whereField( + field: String, + nullable: T?, + modified: AndroidQuery.(String, T) -> AndroidQuery + ) : AndroidQuery = nullable?.let { + modified(field, it) + } ?: this + + private fun AndroidQuery.wherePath( + path: FieldPath, + nullable: T?, + modified: AndroidQuery.(com.google.firebase.firestore.FieldPath, T) -> AndroidQuery + ) : AndroidQuery = nullable?.let { + modified(path.android, it) + } ?: this 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/firestore.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 8dc81fedc..c76975c0c 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 @@ -52,19 +52,42 @@ expect class Transaction { suspend fun get(documentRef: DocumentReference): DocumentSnapshot } +sealed class WhereClause { + + sealed class ForNullableObject : WhereClause() { + abstract val value: Any? + val safeValue get() = value?.safeValue + } + + sealed class ForObject : WhereClause() { + abstract val value: Any + val safeValue get() = value.safeValue + } + sealed class ForArray : WhereClause() { + abstract val values: List + val safeValues get() = values.map { it.safeValue } + } + + data class EqualTo(override val value: Any) : ForNullableObject() + data class NotEqualTo(override val value: Any) : ForNullableObject() + data class LessThan(override val value: Any) : ForObject() + data class GreaterThan(override val value: Any) : ForObject() + data class LessThanOrEqualTo(override val value: Any) : ForObject() + data class GreaterThanOrEqualTo(override val value: Any) : ForObject() + data class ArrayContains(override val value: Any) : ForObject() + data class InArray(override val values: List) : ForArray() + data class ArrayContainsAny(override val values: List) : ForArray() + data class NotInArray(override val values: List) : ForArray() +} + expect open class Query { fun limit(limit: Number): 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(field: String, vararg clauses: WhereClause): Query + internal fun where(path: FieldPath, vararg clauses: WhereClause): Query internal fun _orderBy(field: String, direction: Direction): Query internal fun _orderBy(field: FieldPath, direction: Direction): Query @@ -80,35 +103,27 @@ 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) { +private 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 } -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) - 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())) expect class WriteBatch { inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean = true, merge: Boolean = false): WriteBatch 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 e7baf2583..95603d5d0 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 @@ -272,40 +272,40 @@ 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(field: String, vararg clauses: WhereClause) = Query( + clauses.fold(ios) { query, clause -> + when (clause) { + is WhereClause.EqualTo -> query.queryWhereField(field, isEqualTo = clause.safeValue ?: NSNull) + is WhereClause.NotEqualTo -> query.queryWhereField(field, isNotEqualTo = clause.safeValue ?: NSNull) + is WhereClause.LessThan -> query.queryWhereField(field, isLessThan = clause.safeValue) + is WhereClause.GreaterThan -> query.queryWhereField(field, isGreaterThan = clause.safeValue) + is WhereClause.LessThanOrEqualTo -> query.queryWhereField(field, isLessThanOrEqualTo = clause.safeValue) + is WhereClause.GreaterThanOrEqualTo -> query.queryWhereField(field, isGreaterThanOrEqualTo = clause.safeValue) + is WhereClause.ArrayContains -> query.queryWhereField(field, arrayContains = clause.safeValue) + is WhereClause.InArray -> query.queryWhereField(field, `in` = clause.safeValues) + is WhereClause.ArrayContainsAny -> query.queryWhereField(field, arrayContainsAny = clause.safeValues) + is WhereClause.NotInArray -> query.queryWhereField(field, notIn = clause.safeValues) } } ) - 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(path: FieldPath, vararg clauses: WhereClause) = Query( + clauses.fold(ios) { query, clause -> + when (clause) { + is WhereClause.EqualTo -> query.queryWhereFieldPath(path.ios, isEqualTo = clause.safeValue ?: NSNull) + is WhereClause.NotEqualTo -> query.queryWhereFieldPath(path.ios, isNotEqualTo = clause.safeValue ?: NSNull) + is WhereClause.LessThan -> query.queryWhereFieldPath(path.ios, isLessThan = clause.safeValue) + is WhereClause.GreaterThan -> query.queryWhereFieldPath(path.ios, isGreaterThan = clause.safeValue) + is WhereClause.LessThanOrEqualTo -> query.queryWhereFieldPath(path.ios, isLessThanOrEqualTo = clause.safeValue) + is WhereClause.GreaterThanOrEqualTo -> query.queryWhereFieldPath(path.ios, isGreaterThanOrEqualTo = clause.safeValue) + is WhereClause.ArrayContains -> query.queryWhereFieldPath(path.ios, arrayContains = clause.safeValue) + is WhereClause.InArray -> query.queryWhereFieldPath(path.ios, `in` = clause.safeValues) + is WhereClause.ArrayContainsAny -> query.queryWhereFieldPath(path.ios, arrayContainsAny = clause.safeValues) + is WhereClause.NotInArray -> query.queryWhereFieldPath(path.ios, notIn = clause.safeValues) } } ) - 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 - } - ) - - 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 - } - ) - 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/firestore.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 46833d684..465870369 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 @@ -293,44 +293,40 @@ 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(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 - } + internal actual fun where(field: String, vararg clauses: WhereClause) = Query( + clauses.fold(js) { query, clause -> + val value = when (clause) { + is WhereClause.ForNullableObject -> clause.safeValue + is WhereClause.ForObject -> clause.safeValue + is WhereClause.ForArray -> clause.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 - } - } - ) - } - - 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 + query(query, jsWhere(field, clause.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 + internal actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( + clauses.fold(js) { query, clause -> + val value = when (clause) { + is WhereClause.ForNullableObject -> clause.safeValue + is WhereClause.ForObject -> clause.safeValue + is WhereClause.ForArray -> clause.safeValues.toTypedArray() + } + query(query, jsWhere(path.js, clause.filterOp, value)) } ) + private val WhereClause.filterOp: String get() = when (this) { + is WhereClause.EqualTo -> "==" + is WhereClause.NotEqualTo -> "!=" + is WhereClause.LessThan -> "<" + is WhereClause.LessThanOrEqualTo -> "<=" + is WhereClause.GreaterThan -> ">" + is WhereClause.GreaterThanOrEqualTo -> ">=" + is WhereClause.ArrayContains -> "array-contains" + is WhereClause.ArrayContainsAny -> "array-contains-any" + is WhereClause.InArray -> "in" + is WhereClause.NotInArray -> "not-in" + } + internal actual fun _orderBy(field: String, direction: Direction) = rethrow { Query(query(js, orderBy(field, direction.jsString))) } From f4afeec18f3caef866da98ee4ca93ec81731a406 Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Wed, 20 Dec 2023 13:34:36 +0100 Subject: [PATCH 07/17] Added shortcuts --- .../gitlive/firebase/firestore/firestore.kt | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) 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 c76975c0c..d00e9706d 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 @@ -11,6 +11,7 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationStrategy +import kotlin.jvm.JvmName /** Returns the [FirebaseFirestore] instance of the default [FirebaseApp]. */ expect val Firebase.firestore: FirebaseFirestore @@ -68,8 +69,8 @@ sealed class WhereClause { val safeValues get() = values.map { it.safeValue } } - data class EqualTo(override val value: Any) : ForNullableObject() - data class NotEqualTo(override val value: Any) : ForNullableObject() + data class EqualTo(override val value: Any?) : ForNullableObject() + data class NotEqualTo(override val value: Any?) : ForNullableObject() data class LessThan(override val value: Any) : ForObject() data class GreaterThan(override val value: Any) : ForObject() data class LessThanOrEqualTo(override val value: Any) : ForObject() @@ -112,6 +113,59 @@ private val Any.safeValue: Any get() = when (this) { else -> this } +fun Query.where(field: String, equalTo: Any?) = where(field, WhereClause.EqualTo(equalTo)) +fun Query.where(path: FieldPath, equalTo: Any?) = where(path, WhereClause.EqualTo(equalTo)) +fun Query.whereNot(field: String, notEqualTo: Any?) = where(field, WhereClause.NotEqualTo(notEqualTo)) +fun Query.whereNot(path: FieldPath, notEqualTo: Any?) = where(path, WhereClause.NotEqualTo(notEqualTo)) +fun Query.where(field: String, + lessThan: Any? = null, + greaterThan: Any? = null, + lessThanOrEqualTo: Any? = null, + greaterThanOrEqualTo: Any? = null, + arrayContains: Any? = null, + arrayContainsAny: List? = null, + inArray: List? = null, + notInArray: List? = null, +) = + where( + field, + listOfNotNull( + lessThan?.let { WhereClause.LessThan(it) }, + greaterThan?.let { WhereClause.GreaterThan(it) }, + lessThanOrEqualTo?.let { WhereClause.LessThanOrEqualTo(it) }, + greaterThanOrEqualTo?.let { WhereClause.GreaterThanOrEqualTo(it) }, + arrayContains?.let { WhereClause.ArrayContains(it) }, + arrayContainsAny?.let { WhereClause.ArrayContainsAny(it) }, + inArray?.let { WhereClause.InArray(it) }, + notInArray?.let { WhereClause.NotInArray(it) } + ) + ) + +fun Query.where(path: FieldPath, + lessThan: Any? = null, + greaterThan: Any? = null, + lessThanOrEqualTo: Any? = null, + greaterThanOrEqualTo: Any? = null, + arrayContains: Any? = null, + arrayContainsAny: List? = null, + inArray: List? = null, + notInArray: List? = null, +) = + where( + path, + listOfNotNull( + lessThan?.let { WhereClause.LessThan(it) }, + greaterThan?.let { WhereClause.GreaterThan(it) }, + lessThanOrEqualTo?.let { WhereClause.LessThanOrEqualTo(it) }, + greaterThanOrEqualTo?.let { WhereClause.GreaterThanOrEqualTo(it) }, + arrayContains?.let { WhereClause.ArrayContains(it) }, + arrayContainsAny?.let { WhereClause.ArrayContainsAny(it) }, + inArray?.let { WhereClause.InArray(it) }, + notInArray?.let { WhereClause.NotInArray(it) } + ) + ) + + fun Query.orderBy(field: String, direction: Direction = Direction.ASCENDING) = _orderBy(field, direction) fun Query.orderBy(field: FieldPath, direction: Direction = Direction.ASCENDING) = _orderBy(field, direction) From b8be819baaeafb4851498c1a20f20861ffdbe635 Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Wed, 20 Dec 2023 13:40:46 +0100 Subject: [PATCH 08/17] Cleanup --- .../dev/gitlive/firebase/firestore/firestore.kt | 16 ---------------- 1 file changed, 16 deletions(-) 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 014303922..9e3c47f2d 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 @@ -372,22 +372,6 @@ actual open class Query(open val android: AndroidQuery) { } ) - private fun AndroidQuery.whereField( - field: String, - nullable: T?, - modified: AndroidQuery.(String, T) -> AndroidQuery - ) : AndroidQuery = nullable?.let { - modified(field, it) - } ?: this - - private fun AndroidQuery.wherePath( - path: FieldPath, - nullable: T?, - modified: AndroidQuery.(com.google.firebase.firestore.FieldPath, T) -> AndroidQuery - ) : AndroidQuery = nullable?.let { - modified(path.android, it) - } ?: this - 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)) From 50b3b61ee8fe87bef999b45be8bdfe8244261b6a Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Wed, 20 Dec 2023 14:30:17 +0100 Subject: [PATCH 09/17] Initialization fixes --- .../gitlive/firebase/firestore/firestore.kt | 4 ++-- .../gitlive/firebase/firestore/firestore.kt | 23 ++++++++++--------- .../gitlive/firebase/firestore/firestore.kt | 4 ++-- .../gitlive/firebase/firestore/firestore.kt | 4 ++-- 4 files changed, 18 insertions(+), 17 deletions(-) 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 9e3c47f2d..cf51134fc 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 @@ -308,7 +308,7 @@ actual open class Query(open val android: AndroidQuery) { awaitClose { listener.remove() } } - internal actual fun where(field: String, vararg clauses: WhereClause) = Query( + actual fun where(field: String, vararg clauses: WhereClause) = Query( clauses.fold(android) { query, clause -> when (clause) { is WhereClause.ForNullableObject -> { @@ -340,7 +340,7 @@ actual open class Query(open val android: AndroidQuery) { } ) - internal actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( + actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( clauses.fold(android) { query, clause -> when (clause) { is WhereClause.ForNullableObject -> { 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 d00e9706d..10e385cad 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 @@ -87,8 +87,8 @@ expect open class Query { fun snapshots(includeMetadataChanges: Boolean = false): Flow suspend fun get(): QuerySnapshot - internal fun where(field: String, vararg clauses: WhereClause): Query - internal fun where(path: FieldPath, vararg clauses: WhereClause): Query + fun where(field: String, vararg clauses: WhereClause): Query + fun where(path: FieldPath, vararg clauses: WhereClause): Query internal fun _orderBy(field: String, direction: Direction): Query internal fun _orderBy(field: FieldPath, direction: Direction): Query @@ -113,10 +113,12 @@ private val Any.safeValue: Any get() = when (this) { else -> this } -fun Query.where(field: String, equalTo: Any?) = where(field, WhereClause.EqualTo(equalTo)) -fun Query.where(path: FieldPath, equalTo: Any?) = where(path, WhereClause.EqualTo(equalTo)) -fun Query.whereNot(field: String, notEqualTo: Any?) = where(field, WhereClause.NotEqualTo(notEqualTo)) -fun Query.whereNot(path: FieldPath, notEqualTo: Any?) = where(path, WhereClause.NotEqualTo(notEqualTo)) +fun Query.where(field: String, equalTo: Any?) = where(field, clause = WhereClause.EqualTo(equalTo)) +fun Query.where(path: FieldPath, equalTo: Any?) = where(path, clause = WhereClause.EqualTo(equalTo)) +fun Query.where(field: String, clause: WhereClause): Query = where(field, clauses = listOf(clause).toTypedArray()) +fun Query.where(path: FieldPath, clause: WhereClause): Query = where(path, clauses = listOf(clause).toTypedArray()) +fun Query.whereNot(field: String, notEqualTo: Any?) = where(field, clause = WhereClause.NotEqualTo(notEqualTo)) +fun Query.whereNot(path: FieldPath, notEqualTo: Any?) = where(path, clause = WhereClause.NotEqualTo(notEqualTo)) fun Query.where(field: String, lessThan: Any? = null, greaterThan: Any? = null, @@ -129,7 +131,7 @@ fun Query.where(field: String, ) = where( field, - listOfNotNull( + clauses = listOfNotNull( lessThan?.let { WhereClause.LessThan(it) }, greaterThan?.let { WhereClause.GreaterThan(it) }, lessThanOrEqualTo?.let { WhereClause.LessThanOrEqualTo(it) }, @@ -138,7 +140,7 @@ fun Query.where(field: String, arrayContainsAny?.let { WhereClause.ArrayContainsAny(it) }, inArray?.let { WhereClause.InArray(it) }, notInArray?.let { WhereClause.NotInArray(it) } - ) + ).toTypedArray() ) fun Query.where(path: FieldPath, @@ -153,7 +155,7 @@ fun Query.where(path: FieldPath, ) = where( path, - listOfNotNull( + clauses = listOfNotNull( lessThan?.let { WhereClause.LessThan(it) }, greaterThan?.let { WhereClause.GreaterThan(it) }, lessThanOrEqualTo?.let { WhereClause.LessThanOrEqualTo(it) }, @@ -162,10 +164,9 @@ fun Query.where(path: FieldPath, arrayContainsAny?.let { WhereClause.ArrayContainsAny(it) }, inArray?.let { WhereClause.InArray(it) }, notInArray?.let { WhereClause.NotInArray(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) 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 95603d5d0..ad144dc66 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 @@ -272,7 +272,7 @@ actual open class Query(open val ios: FIRQuery) { awaitClose { listener.remove() } } - internal actual fun where(field: String, vararg clauses: WhereClause) = Query( + actual fun where(field: String, vararg clauses: WhereClause) = Query( clauses.fold(ios) { query, clause -> when (clause) { is WhereClause.EqualTo -> query.queryWhereField(field, isEqualTo = clause.safeValue ?: NSNull) @@ -289,7 +289,7 @@ actual open class Query(open val ios: FIRQuery) { } ) - internal actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( + actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( clauses.fold(ios) { query, clause -> when (clause) { is WhereClause.EqualTo -> query.queryWhereFieldPath(path.ios, isEqualTo = clause.safeValue ?: NSNull) 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 465870369..20d316935 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 @@ -293,7 +293,7 @@ actual open class Query(open val js: JsQuery) { actual fun limit(limit: Number) = Query(query(js, jsLimit(limit))) - internal actual fun where(field: String, vararg clauses: WhereClause) = Query( + actual fun where(field: String, vararg clauses: WhereClause) = Query( clauses.fold(js) { query, clause -> val value = when (clause) { is WhereClause.ForNullableObject -> clause.safeValue @@ -303,7 +303,7 @@ actual open class Query(open val js: JsQuery) { query(query, jsWhere(field, clause.filterOp, value)) } ) - internal actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( + actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( clauses.fold(js) { query, clause -> val value = when (clause) { is WhereClause.ForNullableObject -> clause.safeValue From e06ca79c33cc7dfc2ced431c87e3e3645853098a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Sk=C3=A1la?= Date: Wed, 20 Dec 2023 14:36:58 +0100 Subject: [PATCH 10/17] Clear cocoapods cache --- .github/workflows/pull_request.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index dcfd8346c..63fb749a0 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -90,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 From e277eb3e1e3a4b2129cd1a186e656dbc40d3f966 Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Thu, 21 Dec 2023 11:22:49 +0100 Subject: [PATCH 11/17] PR remarks --- .../gitlive/firebase/firestore/firestore.kt | 4 +- .../gitlive/firebase/firestore/firestore.kt | 37 ++++++++++--------- .../gitlive/firebase/firestore/firestore.kt | 4 +- .../gitlive/firebase/firestore/firestore.kt | 5 ++- 4 files changed, 26 insertions(+), 24 deletions(-) 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 cf51134fc..9e3c47f2d 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 @@ -308,7 +308,7 @@ actual open class Query(open val android: AndroidQuery) { awaitClose { listener.remove() } } - actual fun where(field: String, vararg clauses: WhereClause) = Query( + internal actual fun where(field: String, vararg clauses: WhereClause) = Query( clauses.fold(android) { query, clause -> when (clause) { is WhereClause.ForNullableObject -> { @@ -340,7 +340,7 @@ actual open class Query(open val android: AndroidQuery) { } ) - actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( + internal actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( clauses.fold(android) { query, clause -> when (clause) { is WhereClause.ForNullableObject -> { 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 10e385cad..1f166ab89 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 @@ -11,6 +11,7 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationStrategy +import kotlin.jvm.JvmInline import kotlin.jvm.JvmName /** Returns the [FirebaseFirestore] instance of the default [FirebaseApp]. */ @@ -53,32 +54,32 @@ expect class Transaction { suspend fun get(documentRef: DocumentReference): DocumentSnapshot } -sealed class WhereClause { +internal sealed interface WhereClause { - sealed class ForNullableObject : WhereClause() { + sealed interface ForNullableObject : WhereClause { abstract val value: Any? val safeValue get() = value?.safeValue } - sealed class ForObject : WhereClause() { + sealed interface ForObject : WhereClause { abstract val value: Any val safeValue get() = value.safeValue } - sealed class ForArray : WhereClause() { + sealed interface ForArray : WhereClause { abstract val values: List val safeValues get() = values.map { it.safeValue } } - data class EqualTo(override val value: Any?) : ForNullableObject() - data class NotEqualTo(override val value: Any?) : ForNullableObject() - data class LessThan(override val value: Any) : ForObject() - data class GreaterThan(override val value: Any) : ForObject() - data class LessThanOrEqualTo(override val value: Any) : ForObject() - data class GreaterThanOrEqualTo(override val value: Any) : ForObject() - data class ArrayContains(override val value: Any) : ForObject() - data class InArray(override val values: List) : ForArray() - data class ArrayContainsAny(override val values: List) : ForArray() - data class NotInArray(override val values: List) : ForArray() + data class EqualTo(override val value: Any?) : ForNullableObject + data class NotEqualTo(override val value: Any?) : ForNullableObject + data class LessThan(override val value: Any) : ForObject + data class GreaterThan(override val value: Any) : ForObject + data class LessThanOrEqualTo(override val value: Any) : ForObject + data class GreaterThanOrEqualTo(override val value: Any) : ForObject + data class ArrayContains(override val value: Any) : ForObject + data class InArray(override val values: List) : ForArray + data class ArrayContainsAny(override val values: List) : ForArray + data class NotInArray(override val values: List) : ForArray } expect open class Query { @@ -87,8 +88,8 @@ expect open class Query { fun snapshots(includeMetadataChanges: Boolean = false): Flow suspend fun get(): QuerySnapshot - fun where(field: String, vararg clauses: WhereClause): Query - fun where(path: FieldPath, vararg clauses: WhereClause): Query + internal fun where(field: String, vararg clauses: WhereClause): Query + internal fun where(path: FieldPath, vararg clauses: WhereClause): Query internal fun _orderBy(field: String, direction: Direction): Query internal fun _orderBy(field: FieldPath, direction: Direction): Query @@ -115,8 +116,8 @@ private val Any.safeValue: Any get() = when (this) { fun Query.where(field: String, equalTo: Any?) = where(field, clause = WhereClause.EqualTo(equalTo)) fun Query.where(path: FieldPath, equalTo: Any?) = where(path, clause = WhereClause.EqualTo(equalTo)) -fun Query.where(field: String, clause: WhereClause): Query = where(field, clauses = listOf(clause).toTypedArray()) -fun Query.where(path: FieldPath, clause: WhereClause): Query = where(path, clauses = listOf(clause).toTypedArray()) +private fun Query.where(field: String, clause: WhereClause): Query = where(field, clauses = listOf(clause).toTypedArray()) +private fun Query.where(path: FieldPath, clause: WhereClause): Query = where(path, clauses = listOf(clause).toTypedArray()) fun Query.whereNot(field: String, notEqualTo: Any?) = where(field, clause = WhereClause.NotEqualTo(notEqualTo)) fun Query.whereNot(path: FieldPath, notEqualTo: Any?) = where(path, clause = WhereClause.NotEqualTo(notEqualTo)) fun Query.where(field: String, 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 ad144dc66..95603d5d0 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 @@ -272,7 +272,7 @@ actual open class Query(open val ios: FIRQuery) { awaitClose { listener.remove() } } - actual fun where(field: String, vararg clauses: WhereClause) = Query( + internal actual fun where(field: String, vararg clauses: WhereClause) = Query( clauses.fold(ios) { query, clause -> when (clause) { is WhereClause.EqualTo -> query.queryWhereField(field, isEqualTo = clause.safeValue ?: NSNull) @@ -289,7 +289,7 @@ actual open class Query(open val ios: FIRQuery) { } ) - actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( + internal actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( clauses.fold(ios) { query, clause -> when (clause) { is WhereClause.EqualTo -> query.queryWhereFieldPath(path.ios, isEqualTo = clause.safeValue ?: NSNull) 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 20d316935..1b8477624 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 @@ -293,7 +293,7 @@ actual open class Query(open val js: JsQuery) { actual fun limit(limit: Number) = Query(query(js, jsLimit(limit))) - actual fun where(field: String, vararg clauses: WhereClause) = Query( + internal actual fun where(field: String, vararg clauses: WhereClause) = Query( clauses.fold(js) { query, clause -> val value = when (clause) { is WhereClause.ForNullableObject -> clause.safeValue @@ -303,7 +303,8 @@ actual open class Query(open val js: JsQuery) { query(query, jsWhere(field, clause.filterOp, value)) } ) - actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( + + internal actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( clauses.fold(js) { query, clause -> val value = when (clause) { is WhereClause.ForNullableObject -> clause.safeValue From 6c72df48fdda57f9bf5e1b56426f7a45373e4564 Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Thu, 21 Dec 2023 20:44:51 +0100 Subject: [PATCH 12/17] Moving to new Query builder --- .../gitlive/firebase/firestore/firestore.kt | 98 ++++++------ .../dev/gitlive/firebase/firestore/Filter.kt | 147 +++++++++++++++++ .../gitlive/firebase/firestore/firestore.kt | 151 +++++++----------- .../gitlive/firebase/firestore/firestore.kt | 59 ++++--- .../firebase/firestore/externals/firestore.kt | 4 + .../gitlive/firebase/firestore/firestore.kt | 59 +++---- gradle.properties | 2 +- .../kotlin/dev/gitlive/firebase/TestUtils.kt | 17 -- 8 files changed, 325 insertions(+), 212 deletions(-) create mode 100644 firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Filter.kt delete mode 100644 test-utils/src/jvmMain/kotlin/dev/gitlive/firebase/TestUtils.kt 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 9e3c47f2d..99d5ca298 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 @@ -18,6 +18,7 @@ import kotlinx.serialization.SerializationStrategy 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,69 +309,72 @@ actual open class Query(open val android: AndroidQuery) { awaitClose { listener.remove() } } - internal actual fun where(field: String, vararg clauses: WhereClause) = Query( - clauses.fold(android) { query, clause -> - when (clause) { - is WhereClause.ForNullableObject -> { - val modifier: AndroidQuery.(String, Any?) -> AndroidQuery = when (clause) { - is WhereClause.EqualTo -> AndroidQuery::whereEqualTo - is WhereClause.NotEqualTo -> AndroidQuery::whereNotEqualTo + internal actual fun where(filter: Filter) = Query( + android.where(filter.toAndroidFilter()) + ) + + 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(query, field, clause.safeValue) + modifier.invoke(field, constraint.safeValue) } - is WhereClause.ForObject -> { - val modifier: AndroidQuery.(String, Any) -> AndroidQuery = when (clause) { - is WhereClause.LessThan -> AndroidQuery::whereLessThan - is WhereClause.GreaterThan -> AndroidQuery::whereGreaterThan - is WhereClause.LessThanOrEqualTo -> AndroidQuery::whereLessThanOrEqualTo - is WhereClause.GreaterThanOrEqualTo -> AndroidQuery::whereGreaterThanOrEqualTo - is WhereClause.ArrayContains -> AndroidQuery::whereArrayContains + 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(query, field, clause.safeValue) + modifier.invoke(field, constraint.safeValue) } - is WhereClause.ForArray -> { - val modifier: AndroidQuery.(String, List) -> AndroidQuery = when (clause) { - is WhereClause.InArray -> AndroidQuery::whereIn - is WhereClause.ArrayContainsAny -> AndroidQuery::whereArrayContainsAny - is WhereClause.NotInArray -> AndroidQuery::whereNotIn + 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(query, field, clause.safeValues) + modifier.invoke(field, constraint.safeValues) } } } - ) - - internal actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( - clauses.fold(android) { query, clause -> - when (clause) { - is WhereClause.ForNullableObject -> { - val modifier: AndroidQuery.(AndroidFieldPath, Any?) -> AndroidQuery = when (clause) { - is WhereClause.EqualTo -> AndroidQuery::whereEqualTo - is WhereClause.NotEqualTo -> AndroidQuery::whereNotEqualTo + 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(query, path.android, clause.safeValue) + modifier.invoke(path.android, constraint.safeValue) } - is WhereClause.ForObject -> { - val modifier: AndroidQuery.(AndroidFieldPath, Any) -> AndroidQuery = when (clause) { - is WhereClause.LessThan -> AndroidQuery::whereLessThan - is WhereClause.GreaterThan -> AndroidQuery::whereGreaterThan - is WhereClause.LessThanOrEqualTo -> AndroidQuery::whereLessThanOrEqualTo - is WhereClause.GreaterThanOrEqualTo -> AndroidQuery::whereGreaterThanOrEqualTo - is WhereClause.ArrayContains -> AndroidQuery::whereArrayContains + 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(query, path.android, clause.safeValue) + modifier.invoke(path.android, constraint.safeValue) } - is WhereClause.ForArray -> { - val modifier: AndroidQuery.(AndroidFieldPath, List) -> AndroidQuery = when (clause) { - is WhereClause.InArray -> AndroidQuery::whereIn - is WhereClause.ArrayContainsAny -> AndroidQuery::whereArrayContainsAny - is WhereClause.NotInArray -> AndroidQuery::whereNotIn + 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(query, path.android, clause.safeValues) + 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..e51071e3e --- /dev/null +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Filter.kt @@ -0,0 +1,147 @@ +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.`in`(values: List): Filter.WithConstraint { + return Filter.Field(this, WhereConstraint.InArray(values)) + } + + infix fun FieldPath.`in`(values: List): Filter.WithConstraint { + return Filter.Path(this, WhereConstraint.InArray(values)) + } + + infix fun String.notIn(values: List): Filter.WithConstraint { + return Filter.Field(this, WhereConstraint.NotInArray(values)) + } + + infix fun FieldPath.notIn(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) + } +} 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 1f166ab89..2ae80161d 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 @@ -11,8 +11,6 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationStrategy -import kotlin.jvm.JvmInline -import kotlin.jvm.JvmName /** Returns the [FirebaseFirestore] instance of the default [FirebaseApp]. */ expect val Firebase.firestore: FirebaseFirestore @@ -54,42 +52,13 @@ expect class Transaction { suspend fun get(documentRef: DocumentReference): DocumentSnapshot } -internal sealed interface WhereClause { - - sealed interface ForNullableObject : WhereClause { - abstract val value: Any? - val safeValue get() = value?.safeValue - } - - sealed interface ForObject : WhereClause { - abstract val value: Any - val safeValue get() = value.safeValue - } - sealed interface ForArray : WhereClause { - abstract val values: List - val safeValues get() = values.map { it.safeValue } - } - - data class EqualTo(override val value: Any?) : ForNullableObject - data class NotEqualTo(override val value: Any?) : ForNullableObject - data class LessThan(override val value: Any) : ForObject - data class GreaterThan(override val value: Any) : ForObject - data class LessThanOrEqualTo(override val value: Any) : ForObject - data class GreaterThanOrEqualTo(override val value: Any) : ForObject - data class ArrayContains(override val value: Any) : ForObject - data class InArray(override val values: List) : ForArray - data class ArrayContainsAny(override val values: List) : ForArray - data class NotInArray(override val values: List) : ForArray -} - expect open class Query { fun limit(limit: Number): Query val snapshots: Flow fun snapshots(includeMetadataChanges: Boolean = false): Flow suspend fun get(): QuerySnapshot - internal fun where(field: String, vararg clauses: WhereClause): Query - internal fun where(path: FieldPath, vararg clauses: WhereClause): Query + internal fun where(filter: Filter): Query internal fun _orderBy(field: String, direction: Direction): Query internal fun _orderBy(field: FieldPath, direction: Direction): Query @@ -105,68 +74,63 @@ expect open class Query { internal fun _endAt(vararg fieldValues: Any): Query } -private 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 +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, clause = WhereClause.EqualTo(equalTo)) -fun Query.where(path: FieldPath, equalTo: Any?) = where(path, clause = WhereClause.EqualTo(equalTo)) -private fun Query.where(field: String, clause: WhereClause): Query = where(field, clauses = listOf(clause).toTypedArray()) -private fun Query.where(path: FieldPath, clause: WhereClause): Query = where(path, clauses = listOf(clause).toTypedArray()) -fun Query.whereNot(field: String, notEqualTo: Any?) = where(field, clause = WhereClause.NotEqualTo(notEqualTo)) -fun Query.whereNot(path: FieldPath, notEqualTo: Any?) = where(path, clause = WhereClause.NotEqualTo(notEqualTo)) -fun Query.where(field: String, - lessThan: Any? = null, - greaterThan: Any? = null, - lessThanOrEqualTo: Any? = null, - greaterThanOrEqualTo: Any? = null, - arrayContains: Any? = null, - arrayContainsAny: List? = null, - inArray: List? = null, - notInArray: List? = null, -) = - where( - field, - clauses = listOfNotNull( - lessThan?.let { WhereClause.LessThan(it) }, - greaterThan?.let { WhereClause.GreaterThan(it) }, - lessThanOrEqualTo?.let { WhereClause.LessThanOrEqualTo(it) }, - greaterThanOrEqualTo?.let { WhereClause.GreaterThanOrEqualTo(it) }, - arrayContains?.let { WhereClause.ArrayContains(it) }, - arrayContainsAny?.let { WhereClause.ArrayContainsAny(it) }, - inArray?.let { WhereClause.InArray(it) }, - notInArray?.let { WhereClause.NotInArray(it) } - ).toTypedArray() +@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 { + val filters = listOfNotNull( + lessThan?.let { field lessThan it }, + greaterThan?.let { field greaterThan it }, + arrayContains?.let { field contains it } ) + filters.fold(null) { acc, filter -> + acc?.let { it and filter } ?: filter + } +} -fun Query.where(path: FieldPath, - lessThan: Any? = null, - greaterThan: Any? = null, - lessThanOrEqualTo: Any? = null, - greaterThanOrEqualTo: Any? = null, - arrayContains: Any? = null, - arrayContainsAny: List? = null, - inArray: List? = null, - notInArray: List? = null, -) = - where( - path, - clauses = listOfNotNull( - lessThan?.let { WhereClause.LessThan(it) }, - greaterThan?.let { WhereClause.GreaterThan(it) }, - lessThanOrEqualTo?.let { WhereClause.LessThanOrEqualTo(it) }, - greaterThanOrEqualTo?.let { WhereClause.GreaterThanOrEqualTo(it) }, - arrayContains?.let { WhereClause.ArrayContains(it) }, - arrayContainsAny?.let { WhereClause.ArrayContainsAny(it) }, - inArray?.let { WhereClause.InArray(it) }, - notInArray?.let { WhereClause.NotInArray(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 { + val filters = listOfNotNull( + lessThan?.let { path lessThan it }, + greaterThan?.let { path greaterThan it }, + arrayContains?.let { path contains it } ) + filters.fold(null) { acc, filter -> + acc?.let { it and filter } ?: filter + } +} + +@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 { + val filters = listOfNotNull( + inArray?.let { field `in` it }, + arrayContainsAny?.let { field containsAny it }, + ) + filters.fold(null) { acc, filter -> + acc?.let { it and filter } ?: filter + } +} + +@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 { + val filters = listOfNotNull( + inArray?.let { path `in` it }, + arrayContainsAny?.let { path containsAny it }, + ) + filters.fold(null) { acc, filter -> + acc?.let { it and filter } ?: filter + } +} fun Query.orderBy(field: String, direction: Direction = Direction.ASCENDING) = _orderBy(field, direction) fun Query.orderBy(field: FieldPath, direction: Direction = Direction.ASCENDING) = _orderBy(field, direction) @@ -181,6 +145,15 @@ fun Query.endBefore(vararg fieldValues: Any) = _endBefore(*(fieldValues.mapNotNu fun Query.endAt(document: DocumentSnapshot) = _endAt(document) 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 inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean = true, vararg mergeFields: String): WriteBatch 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 95603d5d0..6a7289ad6 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 @@ -272,39 +272,38 @@ actual open class Query(open val ios: FIRQuery) { awaitClose { listener.remove() } } - internal actual fun where(field: String, vararg clauses: WhereClause) = Query( - clauses.fold(ios) { query, clause -> - when (clause) { - is WhereClause.EqualTo -> query.queryWhereField(field, isEqualTo = clause.safeValue ?: NSNull) - is WhereClause.NotEqualTo -> query.queryWhereField(field, isNotEqualTo = clause.safeValue ?: NSNull) - is WhereClause.LessThan -> query.queryWhereField(field, isLessThan = clause.safeValue) - is WhereClause.GreaterThan -> query.queryWhereField(field, isGreaterThan = clause.safeValue) - is WhereClause.LessThanOrEqualTo -> query.queryWhereField(field, isLessThanOrEqualTo = clause.safeValue) - is WhereClause.GreaterThanOrEqualTo -> query.queryWhereField(field, isGreaterThanOrEqualTo = clause.safeValue) - is WhereClause.ArrayContains -> query.queryWhereField(field, arrayContains = clause.safeValue) - is WhereClause.InArray -> query.queryWhereField(field, `in` = clause.safeValues) - is WhereClause.ArrayContainsAny -> query.queryWhereField(field, arrayContainsAny = clause.safeValues) - is WhereClause.NotInArray -> query.queryWhereField(field, notIn = clause.safeValues) - } - } + internal actual fun where(filter: Filter): Query = Query( + ios.queryWhereFilter(filter.toFIRFilter()) ) - internal actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( - clauses.fold(ios) { query, clause -> - when (clause) { - is WhereClause.EqualTo -> query.queryWhereFieldPath(path.ios, isEqualTo = clause.safeValue ?: NSNull) - is WhereClause.NotEqualTo -> query.queryWhereFieldPath(path.ios, isNotEqualTo = clause.safeValue ?: NSNull) - is WhereClause.LessThan -> query.queryWhereFieldPath(path.ios, isLessThan = clause.safeValue) - is WhereClause.GreaterThan -> query.queryWhereFieldPath(path.ios, isGreaterThan = clause.safeValue) - is WhereClause.LessThanOrEqualTo -> query.queryWhereFieldPath(path.ios, isLessThanOrEqualTo = clause.safeValue) - is WhereClause.GreaterThanOrEqualTo -> query.queryWhereFieldPath(path.ios, isGreaterThanOrEqualTo = clause.safeValue) - is WhereClause.ArrayContains -> query.queryWhereFieldPath(path.ios, arrayContains = clause.safeValue) - is WhereClause.InArray -> query.queryWhereFieldPath(path.ios, `in` = clause.safeValues) - is WhereClause.ArrayContainsAny -> query.queryWhereFieldPath(path.ios, arrayContainsAny = clause.safeValues) - is WhereClause.NotInArray -> query.queryWhereFieldPath(path.ios, notIn = clause.safeValues) - } + 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) + is WhereConstraint.NotEqualTo -> FIRFilter.filterWhereField(field, isNotEqualTo = constraint.safeValue ?: NSNull) + 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) } - ) + is Filter.Path -> when (constraint) { + is WhereConstraint.EqualTo -> FIRFilter.filterWhereFieldPath(path.ios, isEqualTo = constraint.safeValue ?: NSNull) + is WhereConstraint.NotEqualTo -> FIRFilter.filterWhereFieldPath(path.ios, isNotEqualTo = constraint.safeValue ?: NSNull) + 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 1b8477624..998cdfe31 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 @@ -293,39 +293,42 @@ actual open class Query(open val js: JsQuery) { actual fun limit(limit: Number) = Query(query(js, jsLimit(limit))) - internal actual fun where(field: String, vararg clauses: WhereClause) = Query( - clauses.fold(js) { query, clause -> - val value = when (clause) { - is WhereClause.ForNullableObject -> clause.safeValue - is WhereClause.ForObject -> clause.safeValue - is WhereClause.ForArray -> clause.safeValues.toTypedArray() - } - query(query, jsWhere(field, clause.filterOp, value)) - } + internal actual fun where(filter: Filter): Query = Query( + query(js, filter.toQueryConstraint()) ) - internal actual fun where(path: FieldPath, vararg clauses: WhereClause) = Query( - clauses.fold(js) { query, clause -> - val value = when (clause) { - is WhereClause.ForNullableObject -> clause.safeValue - is WhereClause.ForObject -> clause.safeValue - is WhereClause.ForArray -> clause.safeValues.toTypedArray() + 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() } - query(query, jsWhere(path.js, clause.filterOp, value)) + 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() + } + jsWhere(path.js, constraint.filterOp, value) + } + } - private val WhereClause.filterOp: String get() = when (this) { - is WhereClause.EqualTo -> "==" - is WhereClause.NotEqualTo -> "!=" - is WhereClause.LessThan -> "<" - is WhereClause.LessThanOrEqualTo -> "<=" - is WhereClause.GreaterThan -> ">" - is WhereClause.GreaterThanOrEqualTo -> ">=" - is WhereClause.ArrayContains -> "array-contains" - is WhereClause.ArrayContainsAny -> "array-contains-any" - is WhereClause.InArray -> "in" - is WhereClause.NotInArray -> "not-in" + 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 { 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 diff --git a/test-utils/src/jvmMain/kotlin/dev/gitlive/firebase/TestUtils.kt b/test-utils/src/jvmMain/kotlin/dev/gitlive/firebase/TestUtils.kt deleted file mode 100644 index 67528d98f..000000000 --- a/test-utils/src/jvmMain/kotlin/dev/gitlive/firebase/TestUtils.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. - */package dev.gitlive.firebase - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.runBlocking - -package dev.gitlive.firebase - -actual fun runTest(test: suspend CoroutineScope.() -> Unit) = kotlinx.coroutines.test.runTest { test() } -actual fun runBlockingTest(action: suspend CoroutineScope.() -> Unit) = runBlocking(block = action) - -actual fun nativeMapOf(vararg pairs: Pair): Any = mapOf(*pairs) -actual fun nativeListOf(vararg elements: Any): Any = listOf(*elements) -actual fun nativeAssertEquals(expected: Any?, actual: Any?) { - kotlin.test.assertEquals(expected, actual) -} From a35a34ef0c7690c708e0c07819b88d7d6b1eceb2 Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Thu, 21 Dec 2023 21:16:04 +0100 Subject: [PATCH 13/17] JVM support --- firebase-firestore/build.gradle.kts | 4 - .../firebase/firestore/FieldValueTests.kt | 1 - .../gitlive/firebase/firestore/firestore.kt | 14 +- .../gitlive/firebase/firestore/GeoPoint.kt | 19 + .../gitlive/firebase/firestore/Timestamp.kt | 35 ++ .../gitlive/firebase/firestore/firestore.kt | 515 ++++++++++++++++++ 6 files changed, 575 insertions(+), 13 deletions(-) create mode 100644 firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt create mode 100644 firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt create mode 100644 firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt diff --git a/firebase-firestore/build.gradle.kts b/firebase-firestore/build.gradle.kts index 5679694a4..921ccd57f 100644 --- a/firebase-firestore/build.gradle.kts +++ b/firebase-firestore/build.gradle.kts @@ -171,10 +171,6 @@ kotlin { api("com.google.firebase:firebase-firestore") } } - - getByName("jvmMain") { - kotlin.srcDir("src/androidMain/kotlin") - } } } 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 8b2956682..5e85d0304 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 @@ -510,17 +510,15 @@ 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) } 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 new file mode 100644 index 000000000..7523619f5 --- /dev/null +++ b/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt @@ -0,0 +1,19 @@ +package dev.gitlive.firebase.firestore + +import kotlinx.serialization.Serializable + +/** A class representing a platform specific Firebase GeoPoint. */ +actual typealias NativeGeoPoint = com.google.firebase.firestore.GeoPoint + +/** A class representing a Firebase GeoPoint. */ +@Serializable(with = GeoPointSerializer::class) +actual class GeoPoint internal actual constructor(internal actual val nativeValue: NativeGeoPoint) { + actual constructor(latitude: Double, longitude: Double) : this(NativeGeoPoint(latitude, longitude)) + actual val latitude: Double = nativeValue.latitude + actual val longitude: Double = nativeValue.longitude + + override fun equals(other: Any?): Boolean = + this === other || other is GeoPoint && nativeValue == other.nativeValue + override fun hashCode(): Int = nativeValue.hashCode() + override fun toString(): String = nativeValue.toString() +} diff --git a/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt b/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt new file mode 100644 index 000000000..cc9a2ddb9 --- /dev/null +++ b/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt @@ -0,0 +1,35 @@ +@file:JvmName("androidTimestamp") +package dev.gitlive.firebase.firestore + +import kotlinx.serialization.Serializable + +/** A class representing a platform specific Firebase Timestamp. */ +actual typealias NativeTimestamp = com.google.firebase.Timestamp + +/** A base class that could be used to combine [Timestamp] and [Timestamp.ServerTimestamp] in the same field. */ +@Serializable(with = BaseTimestampSerializer::class) +actual sealed class BaseTimestamp + +/** A class representing a Firebase Timestamp. */ +@Serializable(with = TimestampSerializer::class) +actual class Timestamp internal actual constructor( + internal actual val nativeValue: NativeTimestamp +): BaseTimestamp() { + actual constructor(seconds: Long, nanoseconds: Int) : this(NativeTimestamp(seconds, nanoseconds)) + + actual val seconds: Long = nativeValue.seconds + actual val nanoseconds: Int = nativeValue.nanoseconds + + override fun equals(other: Any?): Boolean = + this === other || other is Timestamp && nativeValue == other.nativeValue + override fun hashCode(): Int = nativeValue.hashCode() + override fun toString(): String = nativeValue.toString() + + actual companion object { + actual fun now(): Timestamp = Timestamp(NativeTimestamp.now()) + } + + /** A server time timestamp. */ + @Serializable(with = ServerTimestampSerializer::class) + actual object ServerTimestamp: BaseTimestamp() +} 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 new file mode 100644 index 000000000..13a16df0c --- /dev/null +++ b/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -0,0 +1,515 @@ +/* + * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmName("android") +package dev.gitlive.firebase.firestore + +import com.google.firebase.firestore.* +import dev.gitlive.firebase.* +import dev.gitlive.firebase.firestore.FirebaseFirestoreException +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.tasks.await +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationStrategy + +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()) + +actual fun Firebase.firestore(app: FirebaseApp) = + FirebaseFirestore(com.google.firebase.firestore.FirebaseFirestore.getInstance(app.android)) + +/** Helper method to perform an update operation. */ +@JvmName("performUpdateFields") +private fun performUpdate( + fieldsAndValues: Array>, + update: (String, Any?, Array) -> R +) = performUpdate(fieldsAndValues, { it }, { encode(it, true) }, update) + +/** Helper method to perform an update operation. */ +@JvmName("performUpdateFieldPaths") +private fun performUpdate( + fieldsAndValues: Array>, + update: (com.google.firebase.firestore.FieldPath, Any?, Array) -> R +) = performUpdate(fieldsAndValues, { it.android }, { encode(it, true) }, update) + +actual class FirebaseFirestore(val android: com.google.firebase.firestore.FirebaseFirestore) { + + actual fun collection(collectionPath: String) = CollectionReference(android.collection(collectionPath)) + + actual fun collectionGroup(collectionId: String) = Query(android.collectionGroup(collectionId)) + + actual fun document(documentPath: String) = DocumentReference(android.document(documentPath)) + + actual fun batch() = WriteBatch(android.batch()) + + actual fun setLoggingEnabled(loggingEnabled: Boolean) = + com.google.firebase.firestore.FirebaseFirestore.setLoggingEnabled(loggingEnabled) + + actual suspend fun runTransaction(func: suspend Transaction.() -> T) = + android.runTransaction { runBlocking { Transaction(it).func() } }.await() + + actual suspend fun clearPersistence() = + android.clearPersistence().await().run { } + + actual fun useEmulator(host: String, port: Int) { + android.useEmulator(host, port) + android.firestoreSettings = com.google.firebase.firestore.FirebaseFirestoreSettings.Builder() + .setPersistenceEnabled(false) + .build() + } + + actual fun setSettings(persistenceEnabled: Boolean?, sslEnabled: Boolean?, host: String?, cacheSizeBytes: Long?) { + android.firestoreSettings = com.google.firebase.firestore.FirebaseFirestoreSettings.Builder().also { builder -> + persistenceEnabled?.let { builder.setPersistenceEnabled(it) } + sslEnabled?.let { builder.isSslEnabled = it } + host?.let { builder.host = it } + cacheSizeBytes?.let { builder.cacheSizeBytes = it } + }.build() + } + + actual suspend fun disableNetwork() = + android.disableNetwork().await().run { } + + actual suspend fun enableNetwork() = + android.enableNetwork().await().run { } + +} + +actual class WriteBatch(val android: com.google.firebase.firestore.WriteBatch) { + + actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, merge: Boolean) = when(merge) { + true -> android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.merge()) + false -> android.set(documentRef.android, encode(data, encodeDefaults)!!) + }.let { this } + + actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = + android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) + .let { this } + + actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = + android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) + .let { this } + + actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean) = when(merge) { + true -> android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.merge()) + false -> android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!) + }.let { this } + + actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = + android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) + .let { this } + + actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = + android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) + .let { this } + + @Suppress("UNCHECKED_CAST") + actual inline fun update(documentRef: DocumentReference, data: T, encodeDefaults: Boolean) = + android.update(documentRef.android, encode(data, encodeDefaults) as Map).let { this } + + @Suppress("UNCHECKED_CAST") + actual fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = + android.update(documentRef.android, encode(strategy, data, encodeDefaults) as Map).let { this } + + @JvmName("updateFields") + actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = + performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + android.update(documentRef.android, field, value, *moreFieldsAndValues) + }.let { this } + + @JvmName("updateFieldPaths") + actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = + performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + android.update(documentRef.android, field, value, *moreFieldsAndValues) + }.let { this } + + actual fun delete(documentRef: DocumentReference) = + android.delete(documentRef.android).let { this } + + actual suspend fun commit() = android.commit().await().run { Unit } + +} + +actual class Transaction(val android: com.google.firebase.firestore.Transaction) { + + actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, merge: Boolean) = when(merge) { + true -> android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.merge()) + false -> android.set(documentRef.android, encode(data, encodeDefaults)!!) + }.let { this } + + actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, vararg mergeFields: String) = + android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) + .let { this } + + actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = + android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) + .let { this } + + actual fun set( + documentRef: DocumentReference, + strategy: SerializationStrategy, + data: T, + encodeDefaults: Boolean, + merge: Boolean + ) = when(merge) { + true -> android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.merge()) + false -> android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!) + }.let { this } + + actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = + android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) + .let { this } + + actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = + android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) + .let { this } + + @Suppress("UNCHECKED_CAST") + actual fun update(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean) = + android.update(documentRef.android, encode(data, encodeDefaults) as Map).let { this } + + @Suppress("UNCHECKED_CAST") + actual fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = + android.update(documentRef.android, encode(strategy, data, encodeDefaults) as Map).let { this } + + @JvmName("updateFields") + actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = + performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + android.update(documentRef.android, field, value, *moreFieldsAndValues) + }.let { this } + + @JvmName("updateFieldPaths") + actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = + performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + android.update(documentRef.android, field, value, *moreFieldsAndValues) + }.let { this } + + actual fun delete(documentRef: DocumentReference) = + android.delete(documentRef.android).let { this } + + actual suspend fun get(documentRef: DocumentReference) = + DocumentSnapshot(android.get(documentRef.android)) +} + +/** A class representing a platform specific Firebase DocumentReference. */ +actual typealias NativeDocumentReference = com.google.firebase.firestore.DocumentReference + +@Serializable(with = DocumentReferenceSerializer::class) +actual class DocumentReference actual constructor(internal actual val nativeValue: NativeDocumentReference) { + val android: NativeDocumentReference by ::nativeValue + actual val id: String + get() = android.id + + actual val path: String + get() = android.path + + actual val parent: CollectionReference + get() = CollectionReference(android.parent) + + actual fun collection(collectionPath: String) = CollectionReference(android.collection(collectionPath)) + + actual suspend inline fun set(data: T, encodeDefaults: Boolean, merge: Boolean) = when(merge) { + true -> android.set(encode(data, encodeDefaults)!!, SetOptions.merge()) + false -> android.set(encode(data, encodeDefaults)!!) + }.await().run { Unit } + + actual suspend inline fun set(data: T, encodeDefaults: Boolean, vararg mergeFields: String) = + android.set(encode(data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) + .await().run { Unit } + + actual suspend inline fun set(data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = + android.set(encode(data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) + .await().run { Unit } + + actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean) = when(merge) { + true -> android.set(encode(strategy, data, encodeDefaults)!!, SetOptions.merge()) + false -> android.set(encode(strategy, data, encodeDefaults)!!) + }.await().run { Unit } + + actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = + android.set(encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) + .await().run { Unit } + + actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = + android.set(encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) + .await().run { Unit } + + @Suppress("UNCHECKED_CAST") + actual suspend inline fun update(data: T, encodeDefaults: Boolean) = + android.update(encode(data, encodeDefaults) as Map).await().run { Unit } + + @Suppress("UNCHECKED_CAST") + actual suspend fun update(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = + android.update(encode(strategy, data, encodeDefaults) as Map).await().run { Unit } + + @JvmName("updateFields") + actual suspend fun update(vararg fieldsAndValues: Pair) = + performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + android.update(field, value, *moreFieldsAndValues) + }?.await() + .run { Unit } + + @JvmName("updateFieldPaths") + actual suspend fun update(vararg fieldsAndValues: Pair) = + performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + android.update(field, value, *moreFieldsAndValues) + }?.await() + .run { Unit } + + actual suspend fun delete() = + android.delete().await().run { Unit } + + actual suspend fun get() = + DocumentSnapshot(android.get().await()) + + actual val snapshots: Flow get() = snapshots() + + actual fun snapshots(includeMetadataChanges: Boolean) = callbackFlow { + val metadataChanges = if(includeMetadataChanges) MetadataChanges.INCLUDE else MetadataChanges.EXCLUDE + val listener = android.addSnapshotListener(metadataChanges) { snapshot, exception -> + snapshot?.let { trySend(DocumentSnapshot(snapshot)) } + exception?.let { close(exception) } + } + awaitClose { listener.remove() } + } + override fun equals(other: Any?): Boolean = + this === other || other is DocumentReference && nativeValue == other.nativeValue + override fun hashCode(): Int = nativeValue.hashCode() + override fun toString(): String = nativeValue.toString() +} + +actual open class Query(open val android: AndroidQuery) { + + actual suspend fun get() = QuerySnapshot(android.get().await()) + + actual fun limit(limit: Number) = Query(android.limit(limit.toLong())) + + actual val snapshots get() = callbackFlow { + val listener = android.addSnapshotListener { snapshot, exception -> + snapshot?.let { trySend(QuerySnapshot(snapshot)) } + exception?.let { close(exception) } + } + awaitClose { listener.remove() } + } + + actual fun snapshots(includeMetadataChanges: Boolean) = callbackFlow { + val metadataChanges = if(includeMetadataChanges) MetadataChanges.INCLUDE else MetadataChanges.EXCLUDE + val listener = android.addSnapshotListener(metadataChanges) { snapshot, exception -> + snapshot?.let { trySend(QuerySnapshot(snapshot)) } + exception?.let { close(exception) } + } + awaitClose { listener.remove() } + } + + internal actual fun where(filter: Filter) = Query( + filter.parseForQuery(android) + ) + + private fun Filter.parseForQuery(query: AndroidQuery): AndroidQuery = when (this) { + is Filter.And -> filters.fold(query) { acc, andFilter -> + andFilter.parseForQuery(acc) + } + 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) + } + } + } + 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)) + + internal actual fun _startAfter(document: DocumentSnapshot) = Query(android.startAfter(document.android)) + internal actual fun _startAfter(vararg fieldValues: Any) = Query(android.startAfter(*fieldValues)) + internal actual fun _startAt(document: DocumentSnapshot) = Query(android.startAt(document.android)) + internal actual fun _startAt(vararg fieldValues: Any) = Query(android.startAt(*fieldValues)) + + internal actual fun _endBefore(document: DocumentSnapshot) = Query(android.endBefore(document.android)) + internal actual fun _endBefore(vararg fieldValues: Any) = Query(android.endBefore(*fieldValues)) + internal actual fun _endAt(document: DocumentSnapshot) = Query(android.endAt(document.android)) + internal actual fun _endAt(vararg fieldValues: Any) = Query(android.endAt(*fieldValues)) +} + +actual typealias Direction = com.google.firebase.firestore.Query.Direction +actual typealias ChangeType = com.google.firebase.firestore.DocumentChange.Type + +actual class CollectionReference(override val android: com.google.firebase.firestore.CollectionReference) : Query(android) { + + actual val path: String + get() = android.path + + actual val document: DocumentReference + get() = DocumentReference(android.document()) + + actual val parent: DocumentReference? + get() = android.parent?.let{DocumentReference(it)} + + actual fun document(documentPath: String) = DocumentReference(android.document(documentPath)) + + actual suspend inline fun add(data: T, encodeDefaults: Boolean) = + DocumentReference(android.add(encode(data, encodeDefaults)!!).await()) + + actual suspend fun add(data: T, strategy: SerializationStrategy, encodeDefaults: Boolean) = + DocumentReference(android.add(encode(strategy, data, encodeDefaults)!!).await()) + actual suspend fun add(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = + DocumentReference(android.add(encode(strategy, data, encodeDefaults)!!).await()) +} + +actual typealias FirebaseFirestoreException = com.google.firebase.firestore.FirebaseFirestoreException + +actual val FirebaseFirestoreException.code: FirestoreExceptionCode get() = code + +actual typealias FirestoreExceptionCode = com.google.firebase.firestore.FirebaseFirestoreException.Code + +actual class QuerySnapshot(val android: com.google.firebase.firestore.QuerySnapshot) { + actual val documents + get() = android.documents.map { DocumentSnapshot(it) } + actual val documentChanges + get() = android.documentChanges.map { DocumentChange(it) } + actual val metadata: SnapshotMetadata get() = SnapshotMetadata(android.metadata) +} + +actual class DocumentChange(val android: com.google.firebase.firestore.DocumentChange) { + actual val document: DocumentSnapshot + get() = DocumentSnapshot(android.document) + actual val newIndex: Int + get() = android.newIndex + actual val oldIndex: Int + get() = android.oldIndex + actual val type: ChangeType + get() = android.type +} + +@Suppress("UNCHECKED_CAST") +actual class DocumentSnapshot(val android: com.google.firebase.firestore.DocumentSnapshot) { + + actual val id get() = android.id + actual val reference get() = DocumentReference(android.reference) + + actual inline fun data(serverTimestampBehavior: ServerTimestampBehavior): T = + decode(value = android.getData(serverTimestampBehavior.toAndroid())) + + actual fun data(strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior): T = + decode(strategy, android.getData(serverTimestampBehavior.toAndroid())) + + actual inline fun get(field: String, serverTimestampBehavior: ServerTimestampBehavior): T = + decode(value = android.get(field, serverTimestampBehavior.toAndroid())) + + actual fun get(field: String, strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior): T = + decode(strategy, android.get(field, serverTimestampBehavior.toAndroid())) + + actual fun contains(field: String) = android.contains(field) + + actual val exists get() = android.exists() + + actual val metadata: SnapshotMetadata get() = SnapshotMetadata(android.metadata) + + fun ServerTimestampBehavior.toAndroid(): com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior = when (this) { + ServerTimestampBehavior.ESTIMATE -> com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior.ESTIMATE + ServerTimestampBehavior.NONE -> com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior.NONE + ServerTimestampBehavior.PREVIOUS -> com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior.PREVIOUS + } +} + +actual class SnapshotMetadata(val android: com.google.firebase.firestore.SnapshotMetadata) { + actual val hasPendingWrites: Boolean get() = android.hasPendingWrites() + actual val isFromCache: Boolean get() = android.isFromCache() +} + +actual class FieldPath private constructor(val android: com.google.firebase.firestore.FieldPath) { + actual constructor(vararg fieldNames: String) : this(com.google.firebase.firestore.FieldPath.of(*fieldNames)) + actual val documentId: FieldPath get() = FieldPath(com.google.firebase.firestore.FieldPath.documentId()) + + override fun equals(other: Any?): Boolean = other is FieldPath && android == other.android + override fun hashCode(): Int = android.hashCode() + override fun toString(): String = android.toString() +} + +/** Represents a platform specific Firebase FieldValue. */ +private typealias NativeFieldValue = com.google.firebase.firestore.FieldValue + +/** Represents a Firebase FieldValue. */ +@Serializable(with = FieldValueSerializer::class) +actual class FieldValue internal actual constructor(internal actual val nativeValue: Any) { + init { + require(nativeValue is NativeFieldValue) + } + override fun equals(other: Any?): Boolean = + this === other || other is FieldValue && nativeValue == other.nativeValue + override fun hashCode(): Int = nativeValue.hashCode() + override fun toString(): String = nativeValue.toString() + + actual companion object { + actual val serverTimestamp: FieldValue get() = FieldValue(NativeFieldValue.serverTimestamp()) + actual val delete: FieldValue get() = FieldValue(NativeFieldValue.delete()) + actual fun increment(value: Int): FieldValue = FieldValue(NativeFieldValue.increment(value.toDouble())) + actual fun arrayUnion(vararg elements: Any): FieldValue = FieldValue(NativeFieldValue.arrayUnion(*elements)) + actual fun arrayRemove(vararg elements: Any): FieldValue = FieldValue(NativeFieldValue.arrayRemove(*elements)) + } +} From fda06c306ac39c90f7278b33895a9dfb1251c44a Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Thu, 21 Dec 2023 23:12:30 +0100 Subject: [PATCH 14/17] Added tests & fixed ios nullability --- .../gitlive/firebase/firestore/firestore.kt | 258 +++++++++++++++++- .../gitlive/firebase/firestore/firestore.kt | 8 +- 2 files changed, 257 insertions(+), 9 deletions(-) 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 5e85d0304..fac4fc701 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 @@ -18,6 +18,7 @@ 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 +44,7 @@ class FirebaseFirestoreTest { val time: Double = 0.0, val count: Int = 0, val list: List = emptyList(), + val optional: String? = null, ) @Serializable @@ -51,6 +53,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 @@ -523,18 +548,241 @@ class FirebaseFirestoreTest { 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" `in` listOf("aaa", "bbb") } + + fieldQuery.assertDocuments(FirestoreTest.serializer(), testOne, testTwo) + + val pathQuery = firestore + .collection("testFirestoreQuerying") + .where { FieldPath(FirestoreTest::prop1.name) `in` listOf("ccc", "ddd") } + + pathQuery.assertDocuments(FirestoreTest.serializer(), testThree) + } + + @Test + fun testQueryNotInArray() = runTest { + setupFirestoreData() + + val fieldQuery = firestore + .collection("testFirestoreQuerying") + .where { "prop1" notIn listOf("aaa", "bbb") } + + fieldQuery.assertDocuments(FirestoreTest.serializer(), testThree) + + val pathQuery = firestore + .collection("testFirestoreQuerying") + .where { FieldPath(FirestoreTest::prop1.name) notIn listOf("ccc", "ddd") } + + pathQuery.assertDocuments(FirestoreTest.serializer(), testOne, testTwo) + } + + @Test + fun testCompoundQuery() = runTest { + setupFirestoreData() + + val andQuery = firestore + .collection("testFirestoreQuerying") + .where { + FieldPath(FirestoreTest::prop1.name) `in` 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 { + ( + FieldPath(FirestoreTest::prop1.name) equalTo "aaa" or + (FieldPath(FirestoreTest::count.name) equalTo 2) + ) and (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 6a7289ad6..6beefc629 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 @@ -280,8 +280,8 @@ actual open class Query(open val ios: FIRQuery) { 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) - is WhereConstraint.NotEqualTo -> FIRFilter.filterWhereField(field, isNotEqualTo = constraint.safeValue ?: NSNull) + 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) @@ -292,8 +292,8 @@ actual open class Query(open val ios: FIRQuery) { is WhereConstraint.NotInArray -> FIRFilter.filterWhereField(field, notIn = constraint.safeValues) } is Filter.Path -> when (constraint) { - is WhereConstraint.EqualTo -> FIRFilter.filterWhereFieldPath(path.ios, isEqualTo = constraint.safeValue ?: NSNull) - is WhereConstraint.NotEqualTo -> FIRFilter.filterWhereFieldPath(path.ios, isNotEqualTo = constraint.safeValue ?: NSNull) + 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) From 4fbfc1ddf7f625b046d8ae005d5814e4deeeac30 Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Fri, 22 Dec 2023 09:29:21 +0100 Subject: [PATCH 15/17] COnvenience methods for and/or --- .../dev/gitlive/firebase/firestore/Filter.kt | 7 +++ .../gitlive/firebase/firestore/firestore.kt | 48 +++++++++---------- .../gitlive/firebase/firestore/firestore.kt | 11 +++-- 3 files changed, 36 insertions(+), 30 deletions(-) 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 index e51071e3e..e0cf44c42 100644 --- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Filter.kt +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Filter.kt @@ -144,4 +144,11 @@ class FilterBuilder internal constructor() { } 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 2ae80161d..e4e71c1cf 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 @@ -88,48 +88,44 @@ fun Query.where(path: FieldPath, equalTo: Any?) = where { @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 { - val filters = listOfNotNull( - lessThan?.let { field lessThan it }, - greaterThan?.let { field greaterThan it }, - arrayContains?.let { field contains it } + all( + *listOfNotNull( + lessThan?.let { field lessThan it }, + greaterThan?.let { field greaterThan it }, + arrayContains?.let { field contains it } + ).toTypedArray() ) - filters.fold(null) { acc, filter -> - acc?.let { it and filter } ?: filter - } } @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 { - val filters = listOfNotNull( - lessThan?.let { path lessThan it }, - greaterThan?.let { path greaterThan it }, - arrayContains?.let { path contains it } + all( + *listOfNotNull( + lessThan?.let { path lessThan it }, + greaterThan?.let { path greaterThan it }, + arrayContains?.let { path contains it } + ).toTypedArray() ) - filters.fold(null) { acc, filter -> - acc?.let { it and filter } ?: filter - } } @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 { - val filters = listOfNotNull( - inArray?.let { field `in` it }, - arrayContainsAny?.let { field containsAny it }, + all( + *listOfNotNull( + inArray?.let { field `in` it }, + arrayContainsAny?.let { field containsAny it }, + ).toTypedArray() ) - filters.fold(null) { acc, filter -> - acc?.let { it and filter } ?: filter - } } @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 { - val filters = listOfNotNull( - inArray?.let { path `in` it }, - arrayContainsAny?.let { path containsAny it }, + all( + *listOfNotNull( + inArray?.let { path `in` it }, + arrayContainsAny?.let { path containsAny it }, + ).toTypedArray() ) - filters.fold(null) { acc, filter -> - acc?.let { it and filter } ?: filter - } } fun Query.orderBy(field: String, direction: Direction = Direction.ASCENDING) = _orderBy(field, direction) 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 fac4fc701..4f9a67099 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 @@ -751,10 +751,13 @@ class FirebaseFirestoreTest { val andOrQuery = firestore .collection("testFirestoreQuerying") .where { - ( - FieldPath(FirestoreTest::prop1.name) equalTo "aaa" or - (FieldPath(FirestoreTest::count.name) equalTo 2) - ) and (FieldPath(FirestoreTest::list.name) contains "a") + 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) } From 5f4cc8d043ab43fadfc2ed9d5f9f5c7f51adbbbf Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Fri, 22 Dec 2023 09:44:21 +0100 Subject: [PATCH 16/17] PR remark --- .../kotlin/dev/gitlive/firebase/firestore/Filter.kt | 8 ++++---- .../dev/gitlive/firebase/firestore/firestore.kt | 4 ++-- .../dev/gitlive/firebase/firestore/firestore.kt | 12 +++++------- 3 files changed, 11 insertions(+), 13 deletions(-) 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 index e0cf44c42..e6897c6d7 100644 --- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Filter.kt +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Filter.kt @@ -105,19 +105,19 @@ class FilterBuilder internal constructor() { return Filter.Path(this, WhereConstraint.ArrayContainsAny(values)) } - infix fun String.`in`(values: List): Filter.WithConstraint { + infix fun String.inArray(values: List): Filter.WithConstraint { return Filter.Field(this, WhereConstraint.InArray(values)) } - infix fun FieldPath.`in`(values: List): Filter.WithConstraint { + infix fun FieldPath.inArray(values: List): Filter.WithConstraint { return Filter.Path(this, WhereConstraint.InArray(values)) } - infix fun String.notIn(values: List): Filter.WithConstraint { + infix fun String.notInArray(values: List): Filter.WithConstraint { return Filter.Field(this, WhereConstraint.NotInArray(values)) } - infix fun FieldPath.notIn(values: List): Filter.WithConstraint { + infix fun FieldPath.notInArray(values: List): Filter.WithConstraint { return Filter.Path(this, WhereConstraint.NotInArray(values)) } 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 e4e71c1cf..97b4e50e9 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 @@ -112,7 +112,7 @@ fun Query.where(path: FieldPath, lessThan: Any? = null, greaterThan: Any? = null fun Query.where(field: String, inArray: List? = null, arrayContainsAny: List? = null) = where { all( *listOfNotNull( - inArray?.let { field `in` it }, + inArray?.let { field inArray it }, arrayContainsAny?.let { field containsAny it }, ).toTypedArray() ) @@ -122,7 +122,7 @@ fun Query.where(field: String, inArray: List? = null, arrayContainsAny: Lis fun Query.where(path: FieldPath, inArray: List? = null, arrayContainsAny: List? = null) = where { all( *listOfNotNull( - inArray?.let { path `in` it }, + inArray?.let { path inArray it }, arrayContainsAny?.let { path containsAny it }, ).toTypedArray() ) 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 4f9a67099..1f5f57693 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,13 +10,11 @@ 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 @@ -702,13 +700,13 @@ class FirebaseFirestoreTest { val fieldQuery = firestore .collection("testFirestoreQuerying") - .where { "prop1" `in` listOf("aaa", "bbb") } + .where { "prop1" inArray listOf("aaa", "bbb") } fieldQuery.assertDocuments(FirestoreTest.serializer(), testOne, testTwo) val pathQuery = firestore .collection("testFirestoreQuerying") - .where { FieldPath(FirestoreTest::prop1.name) `in` listOf("ccc", "ddd") } + .where { FieldPath(FirestoreTest::prop1.name) inArray listOf("ccc", "ddd") } pathQuery.assertDocuments(FirestoreTest.serializer(), testThree) } @@ -719,13 +717,13 @@ class FirebaseFirestoreTest { val fieldQuery = firestore .collection("testFirestoreQuerying") - .where { "prop1" notIn listOf("aaa", "bbb") } + .where { "prop1" notInArray listOf("aaa", "bbb") } fieldQuery.assertDocuments(FirestoreTest.serializer(), testThree) val pathQuery = firestore .collection("testFirestoreQuerying") - .where { FieldPath(FirestoreTest::prop1.name) notIn listOf("ccc", "ddd") } + .where { FieldPath(FirestoreTest::prop1.name) notInArray listOf("ccc", "ddd") } pathQuery.assertDocuments(FirestoreTest.serializer(), testOne, testTwo) } @@ -737,7 +735,7 @@ class FirebaseFirestoreTest { val andQuery = firestore .collection("testFirestoreQuerying") .where { - FieldPath(FirestoreTest::prop1.name) `in` listOf("aaa", "bbb") and (FieldPath(FirestoreTest::count.name) equalTo 1) + FieldPath(FirestoreTest::prop1.name) inArray listOf("aaa", "bbb") and (FieldPath(FirestoreTest::count.name) equalTo 1) } andQuery.assertDocuments(FirestoreTest.serializer(), testOne) From 504d6f2e238e3d65819eacf161843e78931c3fb6 Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Tue, 2 Jan 2024 11:15:51 +0100 Subject: [PATCH 17/17] Updated readme --- README.md | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) 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