From dd9992e74326962c403505a07ddc26ee2c0a88d1 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 24 Oct 2021 16:56:34 +0300 Subject: [PATCH 1/2] Add ServerTimestampBehavior --- .../gitlive/firebase/firestore/firestore.kt | 3 +- .../gitlive/firebase/firestore/firestore.kt | 23 +++-- .../gitlive/firebase/firestore/firestore.kt | 16 +++- .../gitlive/firebase/firestore/firestore.kt | 92 ++++++++++++++++++- .../gitlive/firebase/firestore/firestore.kt | 34 +++++-- .../gitlive/firebase/firestore/firestore.kt | 2 +- .../gitlive/firebase/firestore/firestore.kt | 22 +++-- .../gitlive/firebase/firestore/firestore.kt | 4 +- 8 files changed, 160 insertions(+), 36 deletions(-) diff --git a/firebase-firestore/src/androidAndroidTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/androidAndroidTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 34a69d9fd..9c3f485e2 100644 --- a/firebase-firestore/src/androidAndroidTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/androidAndroidTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -6,10 +6,11 @@ package dev.gitlive.firebase.firestore import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.runBlocking actual val emulatorHost: String = "10.0.2.2" actual val context: Any = InstrumentationRegistry.getInstrumentation().targetContext -actual fun runTest(test: suspend () -> Unit) = runBlocking { test() } +actual fun runTest(test: suspend CoroutineScope.() -> Unit) = runBlocking { test() } 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 67fb451d8..970bea996 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 @@ -409,22 +409,32 @@ actual class DocumentSnapshot(val android: com.google.firebase.firestore.Documen actual val id get() = android.id actual val reference get() = DocumentReference(android.reference) - actual inline fun data() = decode(value = android.data) + actual inline fun data(serverTimestampBehavior: ServerTimestampBehavior): T = + decode(value = android.getData(serverTimestampBehavior.toAndroid())) - actual fun data(strategy: DeserializationStrategy) = decode(strategy, android.data) + actual fun data(strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior): T = + decode(strategy, android.getData(serverTimestampBehavior.toAndroid())) - actual fun dataMap(): Map = android.data ?: emptyMap() + actual fun dataMap(serverTimestampBehavior: ServerTimestampBehavior): Map = + android.getData(serverTimestampBehavior.toAndroid()) ?: emptyMap() - actual inline fun get(field: String) = decode(value = android.get(field)) + actual inline fun get(field: String, serverTimestampBehavior: ServerTimestampBehavior): T = + decode(value = android.get(field, serverTimestampBehavior.toAndroid())) - actual fun get(field: String, strategy: DeserializationStrategy) = - decode(strategy, android.get(field)) + 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) { @@ -444,4 +454,3 @@ actual object FieldValue { actual fun arrayRemove(vararg elements: Any): Any = FieldValue.arrayRemove(*elements) actual fun delete(): Any = delete } - 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 11c34996b..12565196d 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 @@ -188,15 +188,15 @@ expect class DocumentChange { expect class DocumentSnapshot { - inline fun get(field: String): T - fun get(field: String, strategy: DeserializationStrategy): T + inline fun get(field: String, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T + fun get(field: String, strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T fun contains(field: String): Boolean - inline fun data(): T - fun data(strategy: DeserializationStrategy): T + inline fun data(serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T + fun data(strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T - fun dataMap(): Map + fun dataMap(serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): Map val exists: Boolean val id: String @@ -204,6 +204,12 @@ expect class DocumentSnapshot { val metadata: SnapshotMetadata } +enum class ServerTimestampBehavior { + ESTIMATE, + NONE, + PREVIOUS +} + expect class SnapshotMetadata { val hasPendingWrites: Boolean val isFromCache: Boolean 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 8655373cc..e8ef8bd2e 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 @@ -4,13 +4,29 @@ package dev.gitlive.firebase.firestore -import dev.gitlive.firebase.* -import kotlinx.serialization.* -import kotlin.test.* +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseOptions +import dev.gitlive.firebase.apps +import dev.gitlive.firebase.initialize +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.Serializable +import kotlin.random.Random +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue expect val emulatorHost: String expect val context: Any -expect fun runTest(test: suspend () -> Unit) +expect fun runTest(test: suspend CoroutineScope.() -> Unit) class FirebaseFirestoreTest { @@ -121,7 +137,73 @@ class FirebaseFirestoreTest { assertNotEquals(FieldValue.serverTimestamp, doc.get().get("time")) assertNotEquals(FieldValue.serverTimestamp, doc.get().data(FirestoreTest.serializer()).time) + } + + @Test + fun testServerTimestampBehaviorNone() = runTest { + val doc = Firebase.firestore + .collection("testServerTimestampBehaviorNone") + .document("test${Random.nextInt()}}") + + val deferredPendingWritesSnapshot = async { + withTimeout(5000) { + doc.snapshots.filter { it.exists }.first() + } + } + delay(100) // makes possible to catch pending writes snapshot + + doc.set( + FirestoreTest.serializer(), + FirestoreTest("ServerTimestampBehavior", FieldValue.serverTimestamp) + ) + + val pendingWritesSnapshot = deferredPendingWritesSnapshot.await() + assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites) + assertNull(pendingWritesSnapshot.get("time", ServerTimestampBehavior.NONE)) + assertNull(pendingWritesSnapshot.dataMap(ServerTimestampBehavior.NONE)["time"]) + } + + @Test + fun testServerTimestampBehaviorEstimate() = runTest { + val doc = Firebase.firestore + .collection("testServerTimestampBehaviorEstimate") + .document("test${Random.nextInt()}") + + val deferredPendingWritesSnapshot = async { + withTimeout(5000) { + doc.snapshots.filter { it.exists }.first() + } + } + delay(100) // makes possible to catch pending writes snapshot + + doc.set(FirestoreTest.serializer(), FirestoreTest("ServerTimestampBehavior", FieldValue.serverTimestamp)) + + val pendingWritesSnapshot = deferredPendingWritesSnapshot.await() + assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites) + assertNotNull(pendingWritesSnapshot.get("time", ServerTimestampBehavior.ESTIMATE)) + assertNotNull(pendingWritesSnapshot.dataMap(ServerTimestampBehavior.ESTIMATE)["time"]) + assertNotEquals(0.0, pendingWritesSnapshot.data(FirestoreTest.serializer(), ServerTimestampBehavior.ESTIMATE).time) + } + + @Test + fun testServerTimestampBehaviorPrevious() = runTest { + val doc = Firebase.firestore + .collection("testServerTimestampBehaviorPrevious") + .document("test${Random.nextInt()}") + + val deferredPendingWritesSnapshot = async { + withTimeout(5000) { + doc.snapshots.filter { it.exists }.first() + } + } + delay(100) // makes possible to catch pending writes snapshot + + doc.set(FirestoreTest.serializer(), FirestoreTest("ServerTimestampBehavior", FieldValue.serverTimestamp)) + val pendingWritesSnapshot = deferredPendingWritesSnapshot.await() + assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites) + assertNull(pendingWritesSnapshot.get("time", ServerTimestampBehavior.PREVIOUS)) + assertNull(pendingWritesSnapshot.dataMap(ServerTimestampBehavior.PREVIOUS)["time"]) } @Test @@ -169,4 +251,4 @@ class FirebaseFirestoreTest { .document("three") .set(FirestoreTest.serializer(), FirestoreTest("ccc")) } -} \ No newline at end of file +} 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 c59e9f148..5d928c290 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 @@ -15,6 +15,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy import platform.Foundation.NSError +import platform.Foundation.NSNull @PublishedApi internal inline fun decode(value: Any?): T = @@ -377,22 +378,43 @@ actual class DocumentSnapshot(val ios: FIRDocumentSnapshot) { actual val reference get() = DocumentReference(ios.reference) - actual inline fun data() = decode(value = ios.data()) + actual inline fun data(serverTimestampBehavior: ServerTimestampBehavior): T { + val data = ios.dataWithServerTimestampBehavior(serverTimestampBehavior.toIos()) + return decode(value = data?.mapValues { (_, value) -> value?.takeIf { it !is NSNull } }) + } - actual fun data(strategy: DeserializationStrategy) = decode(strategy, ios.data()) + actual fun data(strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior): T { + val data = ios.dataWithServerTimestampBehavior(serverTimestampBehavior.toIos()) + return decode(strategy, data?.mapValues { (_, value) -> value?.takeIf { it !is NSNull } }) + } - actual fun dataMap(): Map = ios.data()?.map { it.key.toString() to it.value }?.toMap() ?: emptyMap() + actual fun dataMap(serverTimestampBehavior: ServerTimestampBehavior): Map = + ios.dataWithServerTimestampBehavior(serverTimestampBehavior.toIos()) + ?.map { (key, value) -> key.toString() to value?.takeIf { it !is NSNull } } + ?.toMap() + ?: emptyMap() - actual inline fun get(field: String) = decode(value = ios.valueForField(field)) + actual inline fun get(field: String, serverTimestampBehavior: ServerTimestampBehavior): T { + val value = ios.valueForField(field, serverTimestampBehavior.toIos())?.takeIf { it !is NSNull } + return decode(value) + } - actual fun get(field: String, strategy: DeserializationStrategy) = - decode(strategy, ios.valueForField(field)) + actual fun get(field: String, strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior): T { + val value = ios.valueForField(field, serverTimestampBehavior.toIos())?.takeIf { it !is NSNull } + return decode(strategy, value) + } actual fun contains(field: String) = ios.valueForField(field) != null actual val exists get() = ios.exists actual val metadata: SnapshotMetadata get() = SnapshotMetadata(ios.metadata) + + fun ServerTimestampBehavior.toIos() : FIRServerTimestampBehavior = when (this) { + ServerTimestampBehavior.ESTIMATE -> FIRServerTimestampBehavior.FIRServerTimestampBehaviorEstimate + ServerTimestampBehavior.NONE -> FIRServerTimestampBehavior.FIRServerTimestampBehaviorNone + ServerTimestampBehavior.PREVIOUS -> FIRServerTimestampBehavior.FIRServerTimestampBehaviorPrevious + } } actual class SnapshotMetadata(val ios: FIRSnapshotMetadata) { diff --git a/firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt index d4915f9d4..294182dd3 100644 --- a/firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -11,7 +11,7 @@ actual val emulatorHost: String = "localhost" actual val context: Any = Unit -actual fun runTest(test: suspend () -> Unit) = runBlocking { +actual fun runTest(test: suspend CoroutineScope.() -> Unit) = runBlocking { val testRun = MainScope().async { test() } while (testRun.isActive) { NSRunLoop.mainRunLoop.runMode( 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 526f1acb9..c078ccde3 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 @@ -387,23 +387,27 @@ actual class DocumentSnapshot(val js: firebase.firestore.DocumentSnapshot) { actual val id get() = rethrow { js.id } actual val reference get() = rethrow { DocumentReference(js.ref) } - actual inline fun data(): T = - rethrow { decode(value = js.data()) } + actual inline fun data(serverTimestampBehavior: ServerTimestampBehavior): T = + rethrow { decode(value = js.data(getTimestampsOptions(serverTimestampBehavior))) } - actual fun data(strategy: DeserializationStrategy): T = - rethrow { decode(strategy, js.data()) } + actual fun data(strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior): T = + rethrow { decode(strategy, js.data(getTimestampsOptions(serverTimestampBehavior))) } - actual fun dataMap(): Map = rethrow { mapOf(js.data().asDynamic()) } + actual fun dataMap(serverTimestampBehavior: ServerTimestampBehavior): Map = + rethrow { mapOf(js.data(getTimestampsOptions(serverTimestampBehavior)).asDynamic()) } - actual inline fun get(field: String) = - rethrow { decode(value = js.get(field)) } + actual inline fun get(field: String, serverTimestampBehavior: ServerTimestampBehavior) = + rethrow { decode(value = js.get(field, getTimestampsOptions(serverTimestampBehavior))) } - actual fun get(field: String, strategy: DeserializationStrategy) = - rethrow { decode(strategy, js.get(field)) } + actual fun get(field: String, strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior) = + rethrow { decode(strategy, js.get(field, getTimestampsOptions(serverTimestampBehavior))) } actual fun contains(field: String) = rethrow { js.get(field) != undefined } actual val exists get() = rethrow { js.exists } actual val metadata: SnapshotMetadata get() = SnapshotMetadata(js.metadata) + + fun getTimestampsOptions(serverTimestampBehavior: ServerTimestampBehavior) = + json("serverTimestamps" to serverTimestampBehavior.name.lowercase()) } actual class SnapshotMetadata(val js: firebase.firestore.SnapshotMetadata) { diff --git a/firebase-firestore/src/jsTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/jsTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt index b3ed41815..2f4951baa 100644 --- a/firebase-firestore/src/jsTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/jsTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -4,6 +4,7 @@ package dev.gitlive.firebase.firestore +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.promise @@ -11,7 +12,7 @@ actual val emulatorHost: String = "localhost" actual val context: Any = Unit -actual fun runTest(test: suspend () -> Unit) = GlobalScope +actual fun runTest(test: suspend CoroutineScope.() -> Unit) = GlobalScope .promise { try { test() @@ -28,4 +29,3 @@ internal fun Throwable.log() { it.log() } } - From 04fbbe9008bd2fb6e5c011f340942fc63e960472 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 24 Oct 2021 17:42:03 +0300 Subject: [PATCH 2/2] Remove redundant parentheses --- .../kotlin/dev/gitlive/firebase/firestore/firestore.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e8ef8bd2e..65d2ff576 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 @@ -143,7 +143,7 @@ class FirebaseFirestoreTest { fun testServerTimestampBehaviorNone() = runTest { val doc = Firebase.firestore .collection("testServerTimestampBehaviorNone") - .document("test${Random.nextInt()}}") + .document("test${Random.nextInt()}") val deferredPendingWritesSnapshot = async { withTimeout(5000) {