Skip to content

Add ServerTimestampBehavior in Firestore module #246

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
Original file line number Diff line number Diff line change
Expand Up @@ -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 <reified T: Any> data() = decode<T>(value = android.data)
actual inline fun <reified T: Any> data(serverTimestampBehavior: ServerTimestampBehavior): T =
decode(value = android.getData(serverTimestampBehavior.toAndroid()))

actual fun <T> data(strategy: DeserializationStrategy<T>) = decode(strategy, android.data)
actual fun <T> data(strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior): T =
decode(strategy, android.getData(serverTimestampBehavior.toAndroid()))

actual fun dataMap(): Map<String, Any?> = android.data ?: emptyMap()
actual fun dataMap(serverTimestampBehavior: ServerTimestampBehavior): Map<String, Any?> =
android.getData(serverTimestampBehavior.toAndroid()) ?: emptyMap()

actual inline fun <reified T> get(field: String) = decode<T>(value = android.get(field))
actual inline fun <reified T> get(field: String, serverTimestampBehavior: ServerTimestampBehavior): T =
decode(value = android.get(field, serverTimestampBehavior.toAndroid()))

actual fun <T> get(field: String, strategy: DeserializationStrategy<T>) =
decode(strategy, android.get(field))
actual fun <T> get(field: String, strategy: DeserializationStrategy<T>, 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) {
Expand All @@ -444,4 +454,3 @@ actual object FieldValue {
actual fun arrayRemove(vararg elements: Any): Any = FieldValue.arrayRemove(*elements)
actual fun delete(): Any = delete
}

Original file line number Diff line number Diff line change
Expand Up @@ -188,22 +188,28 @@ expect class DocumentChange {

expect class DocumentSnapshot {

inline fun <reified T> get(field: String): T
fun <T> get(field: String, strategy: DeserializationStrategy<T>): T
inline fun <reified T> get(field: String, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T
fun <T> get(field: String, strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T

fun contains(field: String): Boolean

inline fun <reified T: Any> data(): T
fun <T> data(strategy: DeserializationStrategy<T>): T
inline fun <reified T: Any> data(serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T
fun <T> data(strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T

fun dataMap(): Map<String, Any?>
fun dataMap(serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): Map<String, Any?>

val exists: Boolean
val id: String
val reference: DocumentReference
val metadata: SnapshotMetadata
}

enum class ServerTimestampBehavior {
ESTIMATE,
NONE,
PREVIOUS
}

expect class SnapshotMetadata {
val hasPendingWrites: Boolean
val isFromCache: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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<Double?>("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<Double?>("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<Double?>("time", ServerTimestampBehavior.PREVIOUS))
assertNull(pendingWritesSnapshot.dataMap(ServerTimestampBehavior.PREVIOUS)["time"])
}

@Test
Expand Down Expand Up @@ -169,4 +251,4 @@ class FirebaseFirestoreTest {
.document("three")
.set(FirestoreTest.serializer(), FirestoreTest("ccc"))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <reified T> decode(value: Any?): T =
Expand Down Expand Up @@ -377,22 +378,43 @@ actual class DocumentSnapshot(val ios: FIRDocumentSnapshot) {

actual val reference get() = DocumentReference(ios.reference)

actual inline fun <reified T: Any> data() = decode<T>(value = ios.data())
actual inline fun <reified T: Any> data(serverTimestampBehavior: ServerTimestampBehavior): T {
val data = ios.dataWithServerTimestampBehavior(serverTimestampBehavior.toIos())
return decode(value = data?.mapValues { (_, value) -> value?.takeIf { it !is NSNull } })
}

actual fun <T> data(strategy: DeserializationStrategy<T>) = decode(strategy, ios.data())
actual fun <T> data(strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior): T {
val data = ios.dataWithServerTimestampBehavior(serverTimestampBehavior.toIos())
return decode(strategy, data?.mapValues { (_, value) -> value?.takeIf { it !is NSNull } })
}

actual fun dataMap(): Map<String, Any?> = ios.data()?.map { it.key.toString() to it.value }?.toMap() ?: emptyMap()
actual fun dataMap(serverTimestampBehavior: ServerTimestampBehavior): Map<String, Any?> =
ios.dataWithServerTimestampBehavior(serverTimestampBehavior.toIos())
?.map { (key, value) -> key.toString() to value?.takeIf { it !is NSNull } }
?.toMap()
?: emptyMap()

actual inline fun <reified T> get(field: String) = decode<T>(value = ios.valueForField(field))
actual inline fun <reified T> get(field: String, serverTimestampBehavior: ServerTimestampBehavior): T {
val value = ios.valueForField(field, serverTimestampBehavior.toIos())?.takeIf { it !is NSNull }
return decode(value)
}

actual fun <T> get(field: String, strategy: DeserializationStrategy<T>) =
decode(strategy, ios.valueForField(field))
actual fun <T> get(field: String, strategy: DeserializationStrategy<T>, 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <reified T : Any> data(): T =
rethrow { decode<T>(value = js.data()) }
actual inline fun <reified T : Any> data(serverTimestampBehavior: ServerTimestampBehavior): T =
rethrow { decode(value = js.data(getTimestampsOptions(serverTimestampBehavior))) }

actual fun <T> data(strategy: DeserializationStrategy<T>): T =
rethrow { decode(strategy, js.data()) }
actual fun <T> data(strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior): T =
rethrow { decode(strategy, js.data(getTimestampsOptions(serverTimestampBehavior))) }

actual fun dataMap(): Map<String, Any?> = rethrow { mapOf(js.data().asDynamic()) }
actual fun dataMap(serverTimestampBehavior: ServerTimestampBehavior): Map<String, Any?> =
rethrow { mapOf(js.data(getTimestampsOptions(serverTimestampBehavior)).asDynamic()) }

actual inline fun <reified T> get(field: String) =
rethrow { decode<T>(value = js.get(field)) }
actual inline fun <reified T> get(field: String, serverTimestampBehavior: ServerTimestampBehavior) =
rethrow { decode<T>(value = js.get(field, getTimestampsOptions(serverTimestampBehavior))) }

actual fun <T> get(field: String, strategy: DeserializationStrategy<T>) =
rethrow { decode(strategy, js.get(field)) }
actual fun <T> get(field: String, strategy: DeserializationStrategy<T>, 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

package dev.gitlive.firebase.firestore

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.promise

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()
Expand All @@ -28,4 +29,3 @@ internal fun Throwable.log() {
it.log()
}
}