From 7c1beea6ef1e09c956ee66be9f2b83d49f06aefc Mon Sep 17 00:00:00 2001 From: Viktor Nyblom Date: Wed, 10 Apr 2024 07:37:53 +0200 Subject: [PATCH 1/2] Initial commit --- .../gitlive/firebase/firestore/firestore.kt | 15 ++++++-- .../gitlive/firebase/firestore/firestore.kt | 12 ++++-- .../gitlive/firebase/firestore/firestore.kt | 12 ++++-- .../firebase/firestore/externals/firestore.kt | 12 ++++++ .../gitlive/firebase/firestore/firestore.kt | 38 ++++++++----------- 5 files changed, 57 insertions(+), 32 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 fcfe4056b..38d676830 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 @@ -8,6 +8,7 @@ package dev.gitlive.firebase.firestore import com.google.firebase.firestore.MetadataChanges import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseApp +import dev.gitlive.firebase.firestore.Source.* import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -171,8 +172,8 @@ internal actual class NativeDocumentReference actual constructor(actual val nati actual fun collection(collectionPath: String) = NativeCollectionReference(android.collection(collectionPath)) - actual suspend fun get() = - NativeDocumentSnapshot(android.get().await()) + actual suspend fun get(source: Source) = + NativeDocumentSnapshot(android.get(source.toAndroidSource()).await()) actual suspend fun setEncoded(encodedData: Any, setOptions: SetOptions) { val task = (setOptions.android?.let { @@ -230,7 +231,7 @@ actual open class Query internal actual constructor(nativeQuery: NativeQuery) { open val android = nativeQuery.android - actual suspend fun get() = QuerySnapshot(android.get().await()) + actual suspend fun get(source: Source) = QuerySnapshot(android.get(source.toAndroidSource()).await()) actual fun limit(limit: Number) = Query(NativeQuery(android.limit(limit.toLong()))) @@ -428,3 +429,11 @@ actual class FieldPath private constructor(val android: com.google.firebase.fire } actual typealias EncodedFieldPath = com.google.firebase.firestore.FieldPath + +internal typealias NativeSource = com.google.firebase.firestore.Source + +private fun Source.toAndroidSource() = when(this) { + CACHE -> NativeSource.CACHE + SERVER -> NativeSource.SERVER + DEFAULT -> NativeSource.DEFAULT +} 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 5a00e0d4d..64f2a006e 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 @@ -131,7 +131,7 @@ expect open class Query internal constructor(nativeQuery: NativeQuery) { fun limit(limit: Number): Query val snapshots: Flow fun snapshots(includeMetadataChanges: Boolean = false): Flow - suspend fun get(): QuerySnapshot + suspend fun get(source: Source = Source.DEFAULT): QuerySnapshot internal fun where(filter: Filter): Query @@ -327,7 +327,7 @@ internal expect class NativeDocumentReference(nativeValue: NativeDocumentReferen fun snapshots(includeMetadataChanges: Boolean = false): Flow fun collection(collectionPath: String): NativeCollectionReference - suspend fun get(): NativeDocumentSnapshot + suspend fun get(source: Source = Source.DEFAULT): NativeDocumentSnapshot suspend fun setEncoded(encodedData: Any, setOptions: SetOptions) suspend fun updateEncoded(encodedData: Any) suspend fun updateEncodedFieldsAndValues(encodedFieldsAndValues: List>) @@ -348,7 +348,7 @@ data class DocumentReference internal constructor(@PublishedApi internal val nat fun snapshots(includeMetadataChanges: Boolean = false): Flow = native.snapshots(includeMetadataChanges).map(::DocumentSnapshot) fun collection(collectionPath: String): CollectionReference = CollectionReference(native.collection(collectionPath)) - suspend fun get(): DocumentSnapshot = DocumentSnapshot(native.get()) + suspend fun get(source: Source = Source.DEFAULT): DocumentSnapshot = DocumentSnapshot(native.get(source)) @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(data, merge) { this.encodeDefaults = encodeDefaults }")) suspend inline fun set(data: T, encodeDefaults: Boolean, merge: Boolean = false) = set(data, merge) { @@ -553,3 +553,9 @@ expect class FieldPath(vararg fieldNames: String) { } expect class EncodedFieldPath + +enum class Source { + CACHE, + SERVER, + DEFAULT +} \ 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 2dc2890a7..124c68a28 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 @@ -185,8 +185,8 @@ internal actual class NativeDocumentReference actual constructor(actual val nati actual fun collection(collectionPath: String) = NativeCollectionReference(ios.collectionWithPath(collectionPath)) - actual suspend fun get() = - NativeDocumentSnapshot(awaitResult { ios.getDocumentWithCompletion(it) }) + actual suspend fun get(source: Source) = + NativeDocumentSnapshot(awaitResult { ios.getDocumentWithSource(source.toIosSource(), it) }) actual suspend fun setEncoded(encodedData: Any, setOptions: SetOptions) = await { when (setOptions) { @@ -235,7 +235,7 @@ actual open class Query internal actual constructor(nativeQuery: NativeQuery) { open val ios: FIRQuery = nativeQuery.ios - actual suspend fun get() = QuerySnapshot(awaitResult { ios.getDocumentsWithCompletion(it) }) + actual suspend fun get(source: Source) = QuerySnapshot(awaitResult { ios.getDocumentsWithSource(source.toIosSource(),it) }) actual fun limit(limit: Number) = Query(ios.queryLimitedTo(limit.toLong()).native) @@ -485,3 +485,9 @@ suspend inline fun await(function: (callback: (NSError?) -> Unit) -> T): T { job.await() return result } + +private fun Source.toIosSource() = when (this) { + Source.CACHE -> FIRFirestoreSource.FIRFirestoreSourceCache + Source.SERVER -> FIRFirestoreSource.FIRFirestoreSourceServer + Source.DEFAULT -> FIRFirestoreSource.FIRFirestoreSourceDefault +} 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 920b95eaf..1206594ed 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 @@ -68,8 +68,20 @@ external fun getDoc( options: Any? = definedExternally ): Promise +external fun getDocFromCache( + reference: DocumentReference, +): Promise + +external fun getDocFromServer( + reference: DocumentReference, +): Promise + external fun getDocs(query: Query): Promise +external fun getDocsFromCache(query: Query): Promise + +external fun getDocsFromServer(query: Query): Promise + external fun getFirestore(app: FirebaseApp? = definedExternally): Firestore external fun increment(n: Int): FieldValue 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 0a381c8f4..05c854444 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 @@ -7,28 +7,8 @@ package dev.gitlive.firebase.firestore import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseApp import dev.gitlive.firebase.FirebaseException -import dev.gitlive.firebase.firestore.externals.Firestore -import dev.gitlive.firebase.firestore.externals.QueryConstraint -import dev.gitlive.firebase.firestore.externals.addDoc -import dev.gitlive.firebase.firestore.externals.and -import dev.gitlive.firebase.firestore.externals.clearIndexedDbPersistence -import dev.gitlive.firebase.firestore.externals.connectFirestoreEmulator -import dev.gitlive.firebase.firestore.externals.deleteDoc -import dev.gitlive.firebase.firestore.externals.doc +import dev.gitlive.firebase.firestore.externals.* import dev.gitlive.firebase.firestore.externals.documentId as jsDocumentId -import dev.gitlive.firebase.firestore.externals.enableIndexedDbPersistence -import dev.gitlive.firebase.firestore.externals.getDoc -import dev.gitlive.firebase.firestore.externals.getDocs -import dev.gitlive.firebase.firestore.externals.getFirestore -import dev.gitlive.firebase.firestore.externals.initializeFirestore -import dev.gitlive.firebase.firestore.externals.onSnapshot -import dev.gitlive.firebase.firestore.externals.or -import dev.gitlive.firebase.firestore.externals.orderBy -import dev.gitlive.firebase.firestore.externals.query -import dev.gitlive.firebase.firestore.externals.refEqual -import dev.gitlive.firebase.firestore.externals.setDoc -import dev.gitlive.firebase.firestore.externals.setLogLevel -import dev.gitlive.firebase.firestore.externals.writeBatch import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.await import kotlinx.coroutines.channels.awaitClose @@ -218,7 +198,7 @@ internal actual class NativeDocumentReference actual constructor(actual val nati actual fun collection(collectionPath: String) = rethrow { NativeCollectionReference(jsCollection(js, collectionPath)) } - actual suspend fun get() = rethrow { NativeDocumentSnapshot( getDoc(js).await()) } + actual suspend fun get(source: Source) = rethrow { NativeDocumentSnapshot( js.get(source).await()) } actual val snapshots: Flow get() = snapshots() @@ -276,7 +256,7 @@ actual open class Query internal actual constructor(nativeQuery: NativeQuery) { open val js: JsQuery = nativeQuery.js - actual suspend fun get() = rethrow { QuerySnapshot(getDocs(js).await()) } + actual suspend fun get(source: Source) = rethrow { QuerySnapshot(js.get(source).await()) } actual fun limit(limit: Number) = Query(query(js, jsLimit(limit))) @@ -543,3 +523,15 @@ fun entriesOf(jsObject: dynamic): List> = // from: https://discuss.kotlinlang.org/t/how-to-access-native-js-object-as-a-map-string-any/509/8 fun mapOf(jsObject: dynamic): Map = entriesOf(jsObject).toMap() + +private fun NativeDocumentReferenceType.get(source: Source) = when (source) { + Source.DEFAULT -> getDoc(this) + Source.CACHE -> getDocFromCache(this) + Source.SERVER -> getDocFromServer(this) +} + +private fun JsQuery.get(source: Source) = when (source) { + Source.DEFAULT -> getDocs(this) + Source.CACHE -> getDocsFromCache(this) + Source.SERVER -> getDocsFromServer(this) +} \ No newline at end of file From 8d43e478d1f256b4c986d25124c77cf8fa329b07 Mon Sep 17 00:00:00 2001 From: Viktor Nyblom Date: Thu, 18 Apr 2024 09:40:11 +0200 Subject: [PATCH 2/2] Added cache tests --- .../firebase/firestore/FirestoreSourceTest.kt | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FirestoreSourceTest.kt diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FirestoreSourceTest.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FirestoreSourceTest.kt new file mode 100644 index 000000000..439d459e7 --- /dev/null +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FirestoreSourceTest.kt @@ -0,0 +1,117 @@ +package dev.gitlive.firebase.firestore + +import dev.gitlive.firebase.* +import kotlin.test.* + +/** + * These tests are separated from other tests because + * testing Firestore Source requires toggling persistence settings per test. + */ +class FirestoreSourceTest { + lateinit var firestore: FirebaseFirestore + + companion object { + val testDoc = FirebaseFirestoreTest.FirestoreTest( + "aaa", + 0.0, + 1, + listOf("a", "aa", "aaa"), + "notNull", + ) + } + + private suspend fun setDoc() { + firestore.collection("testFirestoreQuerying").document("one").set(testDoc) + } + + private fun initializeFirebase(persistenceEnabled: Boolean = false) { + val app = Firebase.apps(context).firstOrNull() ?: Firebase.initialize( + context, + FirebaseOptions( + applicationId = "1:846484016111:ios:dd1f6688bad7af768c841a", + apiKey = "AIzaSyCK87dcMFhzCz_kJVs2cT2AVlqOTLuyWV0", + databaseUrl = "https://fir-kotlin-sdk.firebaseio.com", + storageBucket = "fir-kotlin-sdk.appspot.com", + projectId = "fir-kotlin-sdk", + gcmSenderId = "846484016111" + ) + ) + + firestore = Firebase.firestore(app).apply { + useEmulator(emulatorHost, 8080) + setSettings(persistenceEnabled = persistenceEnabled) + } + } + + @AfterTest + fun deinitializeFirebase() = runBlockingTest { + Firebase.apps(context).forEach { + it.delete() + } + } + + @Test + fun testGetFromServer_withPersistence() = runTest { + initializeFirebase(persistenceEnabled = true) + setDoc() + val doc = firestore.collection("testFirestoreQuerying").document("one").get(Source.SERVER) + assertTrue(doc.exists) + assertFalse(doc.native.metadata.isFromCache) + } + + @Test + fun testGetFromServer_withoutPersistence() = runTest { + initializeFirebase(persistenceEnabled = false) + setDoc() + val doc = firestore.collection("testFirestoreQuerying").document("one").get(Source.SERVER) + assertTrue(doc.exists) + assertFalse(doc.native.metadata.isFromCache) + } + + @Test + fun testGetFromCache() = runTest { + initializeFirebase(persistenceEnabled = true) + + // Warm up cache by setting a document + setDoc() + + val cachedDoc = firestore.collection("testFirestoreQuerying").document("one").get(Source.CACHE) + assertTrue(cachedDoc.exists) + assertTrue(cachedDoc.native.metadata.isFromCache) + } + + @Test + fun testGetFromCache_withoutPersistence() = runTest { + initializeFirebase(persistenceEnabled = false) + setDoc() + assertFailsWith(FirebaseFirestoreException::class) { + firestore.collection("testFirestoreQuerying").document("one").get(Source.CACHE) + } + } + + @Test + fun testGetDefault_withPersistence() = runTest { + initializeFirebase(persistenceEnabled = false) + val doc = firestore.collection("testFirestoreQuerying").document("one").get(Source.DEFAULT) + assertTrue(doc.exists) + assertFalse(doc.native.metadata.isFromCache) + } + @Test + fun testGet() = runTest { + initializeFirebase(persistenceEnabled = false) + val doc = firestore.collection("testFirestoreQuerying").document("one").get() + assertTrue(doc.exists) + assertFalse(doc.native.metadata.isFromCache) + } + + @Test + fun testGetDefault_withoutPersistence() = runTest { + initializeFirebase(persistenceEnabled = true) + setDoc() + val doc = firestore.collection("testFirestoreQuerying").document("one").get(Source.DEFAULT) + assertTrue(doc.exists) + // Firebase defaults to first fetching from server + assertFalse(doc.native.metadata.isFromCache) + } + +} \ No newline at end of file