Skip to content

Commit 9ce9222

Browse files
authored
Add ServerTimestampBehavior in Firestore module (#246)
* Add ServerTimestampBehavior * Remove redundant parentheses
1 parent 8d28273 commit 9ce9222

File tree

8 files changed

+160
-36
lines changed
  • firebase-firestore/src

8 files changed

+160
-36
lines changed

firebase-firestore/src/androidAndroidTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
package dev.gitlive.firebase.firestore
77

88
import androidx.test.platform.app.InstrumentationRegistry
9+
import kotlinx.coroutines.CoroutineScope
910
import kotlinx.coroutines.runBlocking
1011

1112
actual val emulatorHost: String = "10.0.2.2"
1213

1314
actual val context: Any = InstrumentationRegistry.getInstrumentation().targetContext
1415

15-
actual fun runTest(test: suspend () -> Unit) = runBlocking { test() }
16+
actual fun runTest(test: suspend CoroutineScope.() -> Unit) = runBlocking { test() }

firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt

+16-7
Original file line numberDiff line numberDiff line change
@@ -409,22 +409,32 @@ actual class DocumentSnapshot(val android: com.google.firebase.firestore.Documen
409409
actual val id get() = android.id
410410
actual val reference get() = DocumentReference(android.reference)
411411

412-
actual inline fun <reified T: Any> data() = decode<T>(value = android.data)
412+
actual inline fun <reified T: Any> data(serverTimestampBehavior: ServerTimestampBehavior): T =
413+
decode(value = android.getData(serverTimestampBehavior.toAndroid()))
413414

414-
actual fun <T> data(strategy: DeserializationStrategy<T>) = decode(strategy, android.data)
415+
actual fun <T> data(strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior): T =
416+
decode(strategy, android.getData(serverTimestampBehavior.toAndroid()))
415417

416-
actual fun dataMap(): Map<String, Any?> = android.data ?: emptyMap()
418+
actual fun dataMap(serverTimestampBehavior: ServerTimestampBehavior): Map<String, Any?> =
419+
android.getData(serverTimestampBehavior.toAndroid()) ?: emptyMap()
417420

418-
actual inline fun <reified T> get(field: String) = decode<T>(value = android.get(field))
421+
actual inline fun <reified T> get(field: String, serverTimestampBehavior: ServerTimestampBehavior): T =
422+
decode(value = android.get(field, serverTimestampBehavior.toAndroid()))
419423

420-
actual fun <T> get(field: String, strategy: DeserializationStrategy<T>) =
421-
decode(strategy, android.get(field))
424+
actual fun <T> get(field: String, strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior): T =
425+
decode(strategy, android.get(field, serverTimestampBehavior.toAndroid()))
422426

423427
actual fun contains(field: String) = android.contains(field)
424428

425429
actual val exists get() = android.exists()
426430

427431
actual val metadata: SnapshotMetadata get() = SnapshotMetadata(android.metadata)
432+
433+
fun ServerTimestampBehavior.toAndroid(): com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior = when (this) {
434+
ServerTimestampBehavior.ESTIMATE -> com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior.ESTIMATE
435+
ServerTimestampBehavior.NONE -> com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior.NONE
436+
ServerTimestampBehavior.PREVIOUS -> com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior.PREVIOUS
437+
}
428438
}
429439

430440
actual class SnapshotMetadata(val android: com.google.firebase.firestore.SnapshotMetadata) {
@@ -444,4 +454,3 @@ actual object FieldValue {
444454
actual fun arrayRemove(vararg elements: Any): Any = FieldValue.arrayRemove(*elements)
445455
actual fun delete(): Any = delete
446456
}
447-

firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt

+11-5
Original file line numberDiff line numberDiff line change
@@ -188,22 +188,28 @@ expect class DocumentChange {
188188

189189
expect class DocumentSnapshot {
190190

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

194194
fun contains(field: String): Boolean
195195

196-
inline fun <reified T: Any> data(): T
197-
fun <T> data(strategy: DeserializationStrategy<T>): T
196+
inline fun <reified T: Any> data(serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T
197+
fun <T> data(strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T
198198

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

201201
val exists: Boolean
202202
val id: String
203203
val reference: DocumentReference
204204
val metadata: SnapshotMetadata
205205
}
206206

207+
enum class ServerTimestampBehavior {
208+
ESTIMATE,
209+
NONE,
210+
PREVIOUS
211+
}
212+
207213
expect class SnapshotMetadata {
208214
val hasPendingWrites: Boolean
209215
val isFromCache: Boolean

firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt

+87-5
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,29 @@
44

55
package dev.gitlive.firebase.firestore
66

7-
import dev.gitlive.firebase.*
8-
import kotlinx.serialization.*
9-
import kotlin.test.*
7+
import dev.gitlive.firebase.Firebase
8+
import dev.gitlive.firebase.FirebaseOptions
9+
import dev.gitlive.firebase.apps
10+
import dev.gitlive.firebase.initialize
11+
import kotlinx.coroutines.CoroutineScope
12+
import kotlinx.coroutines.async
13+
import kotlinx.coroutines.delay
14+
import kotlinx.coroutines.flow.filter
15+
import kotlinx.coroutines.flow.first
16+
import kotlinx.coroutines.withTimeout
17+
import kotlinx.serialization.Serializable
18+
import kotlin.random.Random
19+
import kotlin.test.BeforeTest
20+
import kotlin.test.Test
21+
import kotlin.test.assertEquals
22+
import kotlin.test.assertNotEquals
23+
import kotlin.test.assertNotNull
24+
import kotlin.test.assertNull
25+
import kotlin.test.assertTrue
1026

1127
expect val emulatorHost: String
1228
expect val context: Any
13-
expect fun runTest(test: suspend () -> Unit)
29+
expect fun runTest(test: suspend CoroutineScope.() -> Unit)
1430

1531
class FirebaseFirestoreTest {
1632

@@ -121,7 +137,73 @@ class FirebaseFirestoreTest {
121137

122138
assertNotEquals(FieldValue.serverTimestamp, doc.get().get("time"))
123139
assertNotEquals(FieldValue.serverTimestamp, doc.get().data(FirestoreTest.serializer()).time)
140+
}
141+
142+
@Test
143+
fun testServerTimestampBehaviorNone() = runTest {
144+
val doc = Firebase.firestore
145+
.collection("testServerTimestampBehaviorNone")
146+
.document("test${Random.nextInt()}")
147+
148+
val deferredPendingWritesSnapshot = async {
149+
withTimeout(5000) {
150+
doc.snapshots.filter { it.exists }.first()
151+
}
152+
}
153+
delay(100) // makes possible to catch pending writes snapshot
154+
155+
doc.set(
156+
FirestoreTest.serializer(),
157+
FirestoreTest("ServerTimestampBehavior", FieldValue.serverTimestamp)
158+
)
159+
160+
val pendingWritesSnapshot = deferredPendingWritesSnapshot.await()
161+
assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites)
162+
assertNull(pendingWritesSnapshot.get<Double?>("time", ServerTimestampBehavior.NONE))
163+
assertNull(pendingWritesSnapshot.dataMap(ServerTimestampBehavior.NONE)["time"])
164+
}
165+
166+
@Test
167+
fun testServerTimestampBehaviorEstimate() = runTest {
168+
val doc = Firebase.firestore
169+
.collection("testServerTimestampBehaviorEstimate")
170+
.document("test${Random.nextInt()}")
171+
172+
val deferredPendingWritesSnapshot = async {
173+
withTimeout(5000) {
174+
doc.snapshots.filter { it.exists }.first()
175+
}
176+
}
177+
delay(100) // makes possible to catch pending writes snapshot
178+
179+
doc.set(FirestoreTest.serializer(), FirestoreTest("ServerTimestampBehavior", FieldValue.serverTimestamp))
180+
181+
val pendingWritesSnapshot = deferredPendingWritesSnapshot.await()
182+
assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites)
183+
assertNotNull(pendingWritesSnapshot.get<Double?>("time", ServerTimestampBehavior.ESTIMATE))
184+
assertNotNull(pendingWritesSnapshot.dataMap(ServerTimestampBehavior.ESTIMATE)["time"])
185+
assertNotEquals(0.0, pendingWritesSnapshot.data(FirestoreTest.serializer(), ServerTimestampBehavior.ESTIMATE).time)
186+
}
187+
188+
@Test
189+
fun testServerTimestampBehaviorPrevious() = runTest {
190+
val doc = Firebase.firestore
191+
.collection("testServerTimestampBehaviorPrevious")
192+
.document("test${Random.nextInt()}")
193+
194+
val deferredPendingWritesSnapshot = async {
195+
withTimeout(5000) {
196+
doc.snapshots.filter { it.exists }.first()
197+
}
198+
}
199+
delay(100) // makes possible to catch pending writes snapshot
200+
201+
doc.set(FirestoreTest.serializer(), FirestoreTest("ServerTimestampBehavior", FieldValue.serverTimestamp))
124202

203+
val pendingWritesSnapshot = deferredPendingWritesSnapshot.await()
204+
assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites)
205+
assertNull(pendingWritesSnapshot.get<Double?>("time", ServerTimestampBehavior.PREVIOUS))
206+
assertNull(pendingWritesSnapshot.dataMap(ServerTimestampBehavior.PREVIOUS)["time"])
125207
}
126208

127209
@Test
@@ -169,4 +251,4 @@ class FirebaseFirestoreTest {
169251
.document("three")
170252
.set(FirestoreTest.serializer(), FirestoreTest("ccc"))
171253
}
172-
}
254+
}

firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt

+28-6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import kotlinx.coroutines.runBlocking
1515
import kotlinx.serialization.DeserializationStrategy
1616
import kotlinx.serialization.SerializationStrategy
1717
import platform.Foundation.NSError
18+
import platform.Foundation.NSNull
1819

1920
@PublishedApi
2021
internal inline fun <reified T> decode(value: Any?): T =
@@ -377,22 +378,43 @@ actual class DocumentSnapshot(val ios: FIRDocumentSnapshot) {
377378

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

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

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

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

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

388-
actual fun <T> get(field: String, strategy: DeserializationStrategy<T>) =
389-
decode(strategy, ios.valueForField(field))
402+
actual fun <T> get(field: String, strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior): T {
403+
val value = ios.valueForField(field, serverTimestampBehavior.toIos())?.takeIf { it !is NSNull }
404+
return decode(strategy, value)
405+
}
390406

391407
actual fun contains(field: String) = ios.valueForField(field) != null
392408

393409
actual val exists get() = ios.exists
394410

395411
actual val metadata: SnapshotMetadata get() = SnapshotMetadata(ios.metadata)
412+
413+
fun ServerTimestampBehavior.toIos() : FIRServerTimestampBehavior = when (this) {
414+
ServerTimestampBehavior.ESTIMATE -> FIRServerTimestampBehavior.FIRServerTimestampBehaviorEstimate
415+
ServerTimestampBehavior.NONE -> FIRServerTimestampBehavior.FIRServerTimestampBehaviorNone
416+
ServerTimestampBehavior.PREVIOUS -> FIRServerTimestampBehavior.FIRServerTimestampBehaviorPrevious
417+
}
396418
}
397419

398420
actual class SnapshotMetadata(val ios: FIRSnapshotMetadata) {

firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ actual val emulatorHost: String = "localhost"
1111

1212
actual val context: Any = Unit
1313

14-
actual fun runTest(test: suspend () -> Unit) = runBlocking {
14+
actual fun runTest(test: suspend CoroutineScope.() -> Unit) = runBlocking {
1515
val testRun = MainScope().async { test() }
1616
while (testRun.isActive) {
1717
NSRunLoop.mainRunLoop.runMode(

firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt

+13-9
Original file line numberDiff line numberDiff line change
@@ -387,23 +387,27 @@ actual class DocumentSnapshot(val js: firebase.firestore.DocumentSnapshot) {
387387
actual val id get() = rethrow { js.id }
388388
actual val reference get() = rethrow { DocumentReference(js.ref) }
389389

390-
actual inline fun <reified T : Any> data(): T =
391-
rethrow { decode<T>(value = js.data()) }
390+
actual inline fun <reified T : Any> data(serverTimestampBehavior: ServerTimestampBehavior): T =
391+
rethrow { decode(value = js.data(getTimestampsOptions(serverTimestampBehavior))) }
392392

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

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

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

401-
actual fun <T> get(field: String, strategy: DeserializationStrategy<T>) =
402-
rethrow { decode(strategy, js.get(field)) }
402+
actual fun <T> get(field: String, strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior) =
403+
rethrow { decode(strategy, js.get(field, getTimestampsOptions(serverTimestampBehavior))) }
403404

404405
actual fun contains(field: String) = rethrow { js.get(field) != undefined }
405406
actual val exists get() = rethrow { js.exists }
406407
actual val metadata: SnapshotMetadata get() = SnapshotMetadata(js.metadata)
408+
409+
fun getTimestampsOptions(serverTimestampBehavior: ServerTimestampBehavior) =
410+
json("serverTimestamps" to serverTimestampBehavior.name.lowercase())
407411
}
408412

409413
actual class SnapshotMetadata(val js: firebase.firestore.SnapshotMetadata) {

firebase-firestore/src/jsTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44

55
package dev.gitlive.firebase.firestore
66

7+
import kotlinx.coroutines.CoroutineScope
78
import kotlinx.coroutines.GlobalScope
89
import kotlinx.coroutines.promise
910

1011
actual val emulatorHost: String = "localhost"
1112

1213
actual val context: Any = Unit
1314

14-
actual fun runTest(test: suspend () -> Unit) = GlobalScope
15+
actual fun runTest(test: suspend CoroutineScope.() -> Unit) = GlobalScope
1516
.promise {
1617
try {
1718
test()
@@ -28,4 +29,3 @@ internal fun Throwable.log() {
2829
it.log()
2930
}
3031
}
31-

0 commit comments

Comments
 (0)