diff --git a/firebase-auth/src/jvmTest/kotlin/dev/gitlive/firebase/auth/auth.kt b/firebase-auth/src/jvmTest/kotlin/dev/gitlive/firebase/auth/auth.kt index 55c7b7cea..d83d421fb 100644 --- a/firebase-auth/src/jvmTest/kotlin/dev/gitlive/firebase/auth/auth.kt +++ b/firebase-auth/src/jvmTest/kotlin/dev/gitlive/firebase/auth/auth.kt @@ -9,3 +9,6 @@ package dev.gitlive.firebase.auth actual val emulatorHost: String = "10.0.2.2" actual val context: Any = Unit + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +actual annotation class IgnoreForAndroidUnitTest diff --git a/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt index 0ce0e0d12..2451e40d0 100644 --- a/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -2,6 +2,7 @@ * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. */ +@file:JvmName("databaseAndroid") package dev.gitlive.firebase.database import com.google.android.gms.tasks.Task @@ -9,7 +10,6 @@ import com.google.firebase.database.* import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseApp import dev.gitlive.firebase.database.ChildEvent.Type -import dev.gitlive.firebase.database.FirebaseDatabase.Companion.FirebaseDatabase import dev.gitlive.firebase.decode import dev.gitlive.firebase.encode import kotlinx.coroutines.CompletableDeferred @@ -37,27 +37,37 @@ suspend fun Task.awaitWhileOnline(): T = actual val Firebase.database - by lazy { FirebaseDatabase(com.google.firebase.database.FirebaseDatabase.getInstance()) } + by lazy { FirebaseDatabase.getInstance(com.google.firebase.database.FirebaseDatabase.getInstance()) } actual fun Firebase.database(url: String) = - FirebaseDatabase(com.google.firebase.database.FirebaseDatabase.getInstance(url)) + FirebaseDatabase.getInstance(com.google.firebase.database.FirebaseDatabase.getInstance(url)) actual fun Firebase.database(app: FirebaseApp) = - FirebaseDatabase(com.google.firebase.database.FirebaseDatabase.getInstance(app.android)) + FirebaseDatabase.getInstance(com.google.firebase.database.FirebaseDatabase.getInstance(app.android)) actual fun Firebase.database(app: FirebaseApp, url: String) = - FirebaseDatabase(com.google.firebase.database.FirebaseDatabase.getInstance(app.android, url)) + FirebaseDatabase.getInstance(com.google.firebase.database.FirebaseDatabase.getInstance(app.android, url)) -actual class FirebaseDatabase private constructor(val android: com.google.firebase.database.FirebaseDatabase) { +actual class FirebaseDatabase internal constructor(val android: com.google.firebase.database.FirebaseDatabase) { companion object { private val instances = WeakHashMap() - internal fun FirebaseDatabase( + internal fun getInstance( android: com.google.firebase.database.FirebaseDatabase ) = instances.getOrPut(android) { dev.gitlive.firebase.database.FirebaseDatabase(android) } } + actual data class Settings( + actual val persistenceEnabled: Boolean = false, + actual val persistenceCacheSizeBytes: Long? = null, + ) { + + actual companion object { + actual fun createSettings(persistenceEnabled: Boolean, persistenceCacheSizeBytes: Long?) = Settings(persistenceEnabled, persistenceCacheSizeBytes) + } + } + private var persistenceEnabled = true actual fun reference(path: String) = @@ -66,8 +76,11 @@ actual class FirebaseDatabase private constructor(val android: com.google.fireba actual fun reference() = DatabaseReference(android.reference, persistenceEnabled) - actual fun setPersistenceEnabled(enabled: Boolean) = - android.setPersistenceEnabled(enabled).also { persistenceEnabled = enabled } + actual fun setSettings(settings: Settings) { + android.setPersistenceEnabled(settings.persistenceEnabled) + persistenceEnabled = settings.persistenceEnabled + settings.persistenceCacheSizeBytes?.let { android.setPersistenceCacheSizeBytes(it) } + } actual fun setLoggingEnabled(enabled: Boolean) = android.setLogLevel(Logger.Level.DEBUG.takeIf { enabled } ?: Logger.Level.NONE) diff --git a/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt index d5bbf6aee..ca4cd2a51 100644 --- a/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -25,13 +25,25 @@ expect fun Firebase.database(app: FirebaseApp): FirebaseDatabase expect fun Firebase.database(app: FirebaseApp, url: String): FirebaseDatabase expect class FirebaseDatabase { + + class Settings { + val persistenceEnabled: Boolean + val persistenceCacheSizeBytes: Long? + + companion object { + fun createSettings(persistenceEnabled: Boolean = false, persistenceCacheSizeBytes: Long? = null): Settings + } + } + fun reference(path: String): DatabaseReference fun reference(): DatabaseReference - fun setPersistenceEnabled(enabled: Boolean) + fun setSettings(settings: Settings) fun setLoggingEnabled(enabled: Boolean) fun useEmulator(host: String, port: Int) } +fun FirebaseDatabase.setPersistenceEnabled(enabled: Boolean) = setSettings(FirebaseDatabase.Settings.createSettings(persistenceEnabled = enabled)) + data class ChildEvent internal constructor( val snapshot: DataSnapshot, val type: Type, diff --git a/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt index 128d7b6c2..a717eaceb 100644 --- a/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -25,6 +25,7 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationStrategy import platform.Foundation.NSError import platform.Foundation.allObjects +import platform.darwin.dispatch_queue_t import kotlin.collections.component1 import kotlin.collections.component2 @@ -44,14 +45,27 @@ actual fun Firebase.database(app: FirebaseApp, url: String): FirebaseDatabase = actual class FirebaseDatabase internal constructor(val ios: FIRDatabase) { + actual data class Settings( + actual val persistenceEnabled: Boolean = false, + actual val persistenceCacheSizeBytes: Long? = null, + val callbackQueue: dispatch_queue_t = null + ) { + + actual companion object { + actual fun createSettings(persistenceEnabled: Boolean, persistenceCacheSizeBytes: Long?) = Settings(persistenceEnabled, persistenceCacheSizeBytes) + } + } + actual fun reference(path: String) = DatabaseReference(ios.referenceWithPath(path), ios.persistenceEnabled) actual fun reference() = DatabaseReference(ios.reference(), ios.persistenceEnabled) - actual fun setPersistenceEnabled(enabled: Boolean) { - ios.persistenceEnabled = enabled + actual fun setSettings(settings: Settings) { + ios.persistenceEnabled = settings.persistenceEnabled + settings.persistenceCacheSizeBytes?.let { ios.setPersistenceCacheSizeBytes(it.toULong()) } + settings.callbackQueue?.let { ios.callbackQueue = it } } actual fun setLoggingEnabled(enabled: Boolean) = diff --git a/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt index d4d47b6be..4139a553e 100644 --- a/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -44,9 +44,20 @@ actual fun Firebase.database(app: FirebaseApp, url: String) = rethrow { FirebaseDatabase(getDatabase(app = app.js, url = url)) } actual class FirebaseDatabase internal constructor(val js: Database) { + + actual data class Settings( + actual val persistenceEnabled: Boolean = false, + actual val persistenceCacheSizeBytes: Long? = null, + ) { + + actual companion object { + actual fun createSettings(persistenceEnabled: Boolean, persistenceCacheSizeBytes: Long?) = Settings(persistenceEnabled, persistenceCacheSizeBytes) + } + } + actual fun reference(path: String) = rethrow { DatabaseReference(ref(js, path), js) } actual fun reference() = rethrow { DatabaseReference(ref(js), js) } - actual fun setPersistenceEnabled(enabled: Boolean) {} + actual fun setSettings(settings: Settings) {} actual fun setLoggingEnabled(enabled: Boolean) = rethrow { enableLogging(enabled) } actual fun useEmulator(host: String, port: Int) = rethrow { connectDatabaseEmulator(js, host, port) } } 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/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index dcc6a2ebc..1ba855b90 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,9 +5,12 @@ @file:JvmName("android") package dev.gitlive.firebase.firestore -import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.TaskExecutors import com.google.firebase.firestore.* +import com.google.firebase.firestore.util.Executors import dev.gitlive.firebase.* +import dev.gitlive.firebase.firestore.FirebaseFirestoreException +import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -16,6 +19,8 @@ import kotlinx.coroutines.tasks.await import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationStrategy +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executor actual val Firebase.firestore get() = FirebaseFirestore(com.google.firebase.firestore.FirebaseFirestore.getInstance()) @@ -37,8 +42,40 @@ private fun performUpdate( update: (com.google.firebase.firestore.FieldPath, Any?, Array) -> R ) = performUpdate(fieldsAndValues, { it.android }, { encode(it, true) }, update) +val LocalCacheSettings.android: com.google.firebase.firestore.LocalCacheSettings get() = when (this) { + is LocalCacheSettings.Persistent -> persistentCacheSettings { + sizeBytes?.let { setSizeBytes(it) } + } + is LocalCacheSettings.Memory -> memoryCacheSettings { + setGcSettings( + when (garbaseCollectorSettings) { + is LocalCacheSettings.Memory.GarbageCollectorSettings.Eager -> memoryEagerGcSettings { } + is LocalCacheSettings.Memory.GarbageCollectorSettings.LRUGC -> memoryLruGcSettings { + garbaseCollectorSettings.sizeBytes?.let { + setSizeBytes(it) + } + } + } + ) + } +} + +// Since on iOS Callback threads are set as settings, we store the settings explicitly here as well +private val callbackExecutorMap = ConcurrentHashMap() + actual class FirebaseFirestore(val android: com.google.firebase.firestore.FirebaseFirestore) { + actual data class Settings( + actual val sslEnabled: Boolean? = null, + actual val host: String? = null, + actual val cacheSettings: LocalCacheSettings? = null, + val callbackExecutor: Executor = TaskExecutors.MAIN_THREAD, + ) { + actual companion object { + actual fun create(sslEnabled: Boolean?, host: String?, cacheSettings: LocalCacheSettings?) = Settings(sslEnabled, host, cacheSettings) + } + } + actual fun collection(collectionPath: String) = CollectionReference(android.collection(collectionPath)) actual fun collectionGroup(collectionId: String) = Query(android.collectionGroup(collectionId)) @@ -58,19 +95,33 @@ actual class FirebaseFirestore(val android: com.google.firebase.firestore.Fireba actual fun useEmulator(host: String, port: Int) { android.useEmulator(host, port) - android.firestoreSettings = com.google.firebase.firestore.FirebaseFirestoreSettings.Builder() - .setPersistenceEnabled(false) - .build() + android.firestoreSettings = firestoreSettings { } } - 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 fun setSettings(settings: Settings) { + android.firestoreSettings = firestoreSettings { + settings.sslEnabled?.let { isSslEnabled = it } + settings.host?.let { host = it } + settings.cacheSettings?.let { setLocalCacheSettings(it.android) } } + callbackExecutorMap[android] = settings.callbackExecutor + } + + @Suppress("DEPRECATION") + actual fun updateSettings(settings: Settings) { + android.firestoreSettings = firestoreSettings { + isSslEnabled = settings.sslEnabled ?: android.firestoreSettings.isSslEnabled + host = settings.host ?: android.firestoreSettings.host + val cacheSettings = settings.cacheSettings?.android ?: android.firestoreSettings.cacheSettings + cacheSettings?.let { + setLocalCacheSettings(it) + } ?: kotlin.run { + isPersistenceEnabled = android.firestoreSettings.isPersistenceEnabled + setCacheSizeBytes(android.firestoreSettings.cacheSizeBytes) + } + } + callbackExecutorMap[android] = settings.callbackExecutor + } actual suspend fun disableNetwork() = android.disableNetwork().await().run { } @@ -269,18 +320,26 @@ actual class DocumentReference actual constructor(internal actual val nativeValu 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() } + actual fun snapshots(includeMetadataChanges: Boolean) = addSnapshotListener(includeMetadataChanges) { snapshot, exception -> + snapshot?.let { trySend(DocumentSnapshot(snapshot)) } + exception?.let { close(exception) } } 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() + + private fun addSnapshotListener( + includeMetadataChanges: Boolean = false, + listener: ProducerScope.(com.google.firebase.firestore.DocumentSnapshot?, com.google.firebase.firestore.FirebaseFirestoreException?) -> Unit + ) = callbackFlow { + val executor = callbackExecutorMap[android.firestore] ?: TaskExecutors.MAIN_THREAD + val metadataChanges = if(includeMetadataChanges) MetadataChanges.INCLUDE else MetadataChanges.EXCLUDE + val registration = android.addSnapshotListener(executor, metadataChanges) { snapshots, exception -> + listener(snapshots, exception) + } + awaitClose { registration.remove() } + } } actual open class Query(open val android: com.google.firebase.firestore.Query) { @@ -289,21 +348,14 @@ actual open class Query(open val android: com.google.firebase.firestore.Query) { 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 val snapshots get() = addSnapshotListener { snapshot, exception -> + snapshot?.let { trySend(QuerySnapshot(snapshot)) } + exception?.let { close(exception) } } - 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() } + actual fun snapshots(includeMetadataChanges: Boolean) = addSnapshotListener(includeMetadataChanges) { snapshot, exception -> + snapshot?.let { trySend(QuerySnapshot(snapshot)) } + exception?.let { close(exception) } } internal actual fun _where(field: String, equalTo: Any?) = Query(android.whereEqualTo(field, equalTo)) @@ -352,6 +404,18 @@ actual open class Query(open val android: com.google.firebase.firestore.Query) { 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)) + + private fun addSnapshotListener( + includeMetadataChanges: Boolean = false, + listener: ProducerScope.(com.google.firebase.firestore.QuerySnapshot?, com.google.firebase.firestore.FirebaseFirestoreException?) -> Unit + ) = callbackFlow { + val executor = callbackExecutorMap[android.firestore] ?: TaskExecutors.MAIN_THREAD + val metadataChanges = if(includeMetadataChanges) MetadataChanges.INCLUDE else MetadataChanges.EXCLUDE + val registration = android.addSnapshotListener(executor, metadataChanges) { snapshots, exception -> + listener(snapshots, exception) + } + awaitClose { registration.remove() } + } } actual typealias Direction = com.google.firebase.firestore.Query.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..5398029ef 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 @@ -18,7 +18,29 @@ expect val Firebase.firestore: FirebaseFirestore /** Returns the [FirebaseFirestore] instance of a given [FirebaseApp]. */ expect fun Firebase.firestore(app: FirebaseApp): FirebaseFirestore +sealed class LocalCacheSettings { + data class Persistent(val sizeBytes: Long? = null) : LocalCacheSettings() + data class Memory(val garbaseCollectorSettings: GarbageCollectorSettings) : LocalCacheSettings() { + sealed class GarbageCollectorSettings { + data object Eager : GarbageCollectorSettings() + data class LRUGC(val sizeBytes: Long? = null) : GarbageCollectorSettings() + } + } +} + expect class FirebaseFirestore { + + class Settings { + + companion object { + fun create(sslEnabled: Boolean? = null, host: String? = null, cacheSettings: LocalCacheSettings? = null): Settings + } + + val sslEnabled: Boolean? + val host: String? + val cacheSettings: LocalCacheSettings? + } + fun collection(collectionPath: String): CollectionReference fun collectionGroup(collectionId: String): Query fun document(documentPath: String): DocumentReference @@ -27,11 +49,18 @@ expect class FirebaseFirestore { suspend fun clearPersistence() suspend fun runTransaction(func: suspend Transaction.() -> T): T fun useEmulator(host: String, port: Int) - fun setSettings(persistenceEnabled: Boolean? = null, sslEnabled: Boolean? = null, host: String? = null, cacheSizeBytes: Long? = null) + fun setSettings(settings: Settings) + fun updateSettings(settings: Settings) suspend fun disableNetwork() suspend fun enableNetwork() } +fun FirebaseFirestore.setSettings( + sslEnabled: Boolean? = null, + host: String? = null, + cacheSettings: LocalCacheSettings? = null +) = FirebaseFirestore.Settings.create(sslEnabled, host, cacheSettings) + expect class Transaction { fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean = true, merge: Boolean = false): Transaction 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..ada32568d 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 @@ -69,7 +69,7 @@ class FirebaseFirestoreTest { firestore = Firebase.firestore(app).apply { useEmulator(emulatorHost, 8080) - setSettings(persistenceEnabled = false) + setSettings(FirebaseFirestore.Settings.create(cacheSettings = LocalCacheSettings.Memory(LocalCacheSettings.Memory.GarbageCollectorSettings.Eager))) } } 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..12fa0936f 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 @@ -17,6 +17,9 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationStrategy import platform.Foundation.NSError import platform.Foundation.NSNull +import platform.Foundation.NSNumber +import platform.Foundation.numberWithLong +import platform.darwin.dispatch_queue_t actual val Firebase.firestore get() = FirebaseFirestore(FIRFirestore.firestore()) @@ -25,9 +28,30 @@ actual fun Firebase.firestore(app: FirebaseApp): FirebaseFirestore = FirebaseFir FIRFirestore.firestoreForApp(app.ios as objcnames.classes.FIRApp) ) +val LocalCacheSettings.ios: FIRLocalCacheSettingsProtocol get() = when (this) { + is LocalCacheSettings.Persistent -> sizeBytes?.let { FIRPersistentCacheSettings(NSNumber.numberWithLong(it)) } ?: FIRPersistentCacheSettings() + is LocalCacheSettings.Memory -> FIRMemoryCacheSettings( + when (garbaseCollectorSettings) { + is LocalCacheSettings.Memory.GarbageCollectorSettings.Eager -> FIRMemoryEagerGCSettings() + is LocalCacheSettings.Memory.GarbageCollectorSettings.LRUGC -> garbaseCollectorSettings.sizeBytes?.let { FIRMemoryLRUGCSettings(NSNumber.numberWithLong(it)) } ?: FIRMemoryLRUGCSettings() + } + ) +} + @Suppress("UNCHECKED_CAST") actual class FirebaseFirestore(val ios: FIRFirestore) { + actual data class Settings( + actual val sslEnabled: Boolean? = null, + actual val host: String? = null, + actual val cacheSettings: LocalCacheSettings? = null, + val dispatchQueue: dispatch_queue_t = null + ) { + actual companion object { + actual fun create(sslEnabled: Boolean?, host: String?, cacheSettings: LocalCacheSettings?) = Settings(sslEnabled, host, cacheSettings) + } + } + actual fun collection(collectionPath: String) = CollectionReference(ios.collectionWithPath(collectionPath)) actual fun collectionGroup(collectionId: String) = Query(ios.collectionGroupWithID(collectionId)) @@ -46,20 +70,26 @@ actual class FirebaseFirestore(val ios: FIRFirestore) { await { ios.clearPersistenceWithCompletion(it) } actual fun useEmulator(host: String, port: Int) { + ios.useEmulatorWithHost(host, port.toLong()) ios.settings = ios.settings.apply { - this.host = "$host:$port" - persistenceEnabled = false + cacheSettings = FIRMemoryCacheSettings() sslEnabled = false } } - actual fun setSettings(persistenceEnabled: Boolean?, sslEnabled: Boolean?, host: String?, cacheSizeBytes: Long?) { - ios.settings = FIRFirestoreSettings().also { settings -> - persistenceEnabled?.let { settings.persistenceEnabled = it } - sslEnabled?.let { settings.sslEnabled = it } - host?.let { settings.host = it } - cacheSizeBytes?.let { settings.cacheSizeBytes = it } - } + actual fun setSettings(settings: Settings) { + ios.settings = FIRFirestoreSettings().applySettings(settings) + } + + actual fun updateSettings(settings: Settings) { + ios.settings = ios.settings.applySettings(settings) + } + + private fun FIRFirestoreSettings.applySettings(settings: Settings): FIRFirestoreSettings = apply { + settings.cacheSettings?.let { cacheSettings = it.ios } + settings.sslEnabled?.let { sslEnabled = it } + settings.host?.let { host = it } + settings.dispatchQueue?.let { dispatchQueue = it } } actual suspend fun disableNetwork() { 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..b419a5ab5 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 @@ -61,6 +61,18 @@ private fun performUpdate( actual class FirebaseFirestore(jsFirestore: Firestore) { + actual data class Settings( + actual val sslEnabled: Boolean? = null, + actual val host: String? = null, + actual val cacheSettings: LocalCacheSettings? = null + ) { + actual companion object { + actual fun create(sslEnabled: Boolean?, host: String?, cacheSettings: LocalCacheSettings?) = Settings(sslEnabled, host, cacheSettings) + } + } + + private var lastSettings = Settings() + var js: Firestore = jsFirestore private set @@ -83,17 +95,29 @@ actual class FirebaseFirestore(jsFirestore: Firestore) { actual fun useEmulator(host: String, port: Int) = rethrow { connectFirestoreEmulator(js, host, port) } - actual fun setSettings(persistenceEnabled: Boolean?, sslEnabled: Boolean?, host: String?, cacheSizeBytes: Long?) { - if(persistenceEnabled == true) enableIndexedDbPersistence(js) - - val settings = json().apply { - sslEnabled?.let { set("ssl", it) } - host?.let { set("host", it) } - cacheSizeBytes?.let { set("cacheSizeBytes", it) } + actual fun setSettings(settings: Settings) { + lastSettings = settings + if(settings.cacheSettings is LocalCacheSettings.Persistent) enableIndexedDbPersistence(js) + + val jsSettings = json().apply { + settings.sslEnabled?.let { set("ssl", it) } + settings.host?.let { set("host", it) } + when (val cacheSettings = settings.cacheSettings) { + is LocalCacheSettings.Persistent -> cacheSettings.sizeBytes + is LocalCacheSettings.Memory -> when (val garbageCollectorSettings = cacheSettings.garbaseCollectorSettings) { + is LocalCacheSettings.Memory.GarbageCollectorSettings.Eager -> null + is LocalCacheSettings.Memory.GarbageCollectorSettings.LRUGC -> garbageCollectorSettings.sizeBytes + } + null -> null + }?.let { set("cacheSizeBytes", it) } } - js = initializeFirestore(js.app, settings) + js = initializeFirestore(js.app, jsSettings) } + actual fun updateSettings(settings: Settings) = setSettings( + Settings(settings.sslEnabled ?: lastSettings.sslEnabled, settings.host ?: lastSettings.host, settings.cacheSettings ?: lastSettings.cacheSettings) + ) + actual suspend fun disableNetwork() { rethrow { disableNetwork(js).await() } } 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..7ad3937bc --- /dev/null +++ b/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -0,0 +1,521 @@ +/* + * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmName("JVM") +package dev.gitlive.firebase.firestore + +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.TaskExecutors +import com.google.firebase.firestore.* +import dev.gitlive.firebase.* +import kotlinx.coroutines.channels.ProducerScope +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 java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executor + +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) + +// Since on iOS Callback threads are set as settings, we store the settings explicitly here as well +private val callbackExecutorMap = ConcurrentHashMap() + +actual data class FirebaseFirestore(val android: com.google.firebase.firestore.FirebaseFirestore) { + + actual data class Settings( + actual val sslEnabled: Boolean? = null, + actual val host: String? = null, + actual val cacheSettings: LocalCacheSettings? = null, + val callbackExecutor: Executor = TaskExecutors.MAIN_THREAD, + ) { + actual companion object { + actual fun create(sslEnabled: Boolean?, host: String?, cacheSettings: LocalCacheSettings?) = Settings(sslEnabled, host, cacheSettings) + } + } + + private var lastSettings = Settings() + + actual fun collection(collectionPath: String) = CollectionReference(android.collection(collectionPath)) + + actual fun document(documentPath: String) = DocumentReference(android.document(documentPath)) + + actual fun collectionGroup(collectionId: String) = Query(android.collectionGroup(collectionId)) + + 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): 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(settings: Settings) { + lastSettings = settings + android.firestoreSettings = com.google.firebase.firestore.FirebaseFirestoreSettings.Builder().also { builder -> + if (settings.cacheSettings is LocalCacheSettings.Persistent) { + builder.isPersistenceEnabled = true + } + settings.sslEnabled?.let { builder.isSslEnabled = it } + settings.host?.let { builder.host = it } + when (val cacheSettings = settings.cacheSettings) { + is LocalCacheSettings.Persistent -> cacheSettings.sizeBytes + is LocalCacheSettings.Memory -> when (val garbageCollectorSettings = cacheSettings.garbaseCollectorSettings) { + is LocalCacheSettings.Memory.GarbageCollectorSettings.Eager -> null + is LocalCacheSettings.Memory.GarbageCollectorSettings.LRUGC -> garbageCollectorSettings.sizeBytes + } + null -> null + }?.let { builder.cacheSizeBytes = it } + }.build() + callbackExecutorMap[android] = settings.callbackExecutor + } + + actual fun updateSettings(settings: Settings) = setSettings( + Settings(settings.sslEnabled ?: lastSettings.sslEnabled, settings.host ?: lastSettings.host, settings.cacheSettings ?: lastSettings.cacheSettings) + ) + + 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) = addSnapshotListener(includeMetadataChanges) { snapshot, exception -> + snapshot?.let { trySend(DocumentSnapshot(snapshot)) } + exception?.let { close(exception) } + } + 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() + + private fun addSnapshotListener( + includeMetadataChanges: Boolean = false, + listener: ProducerScope.(com.google.firebase.firestore.DocumentSnapshot?, com.google.firebase.firestore.FirebaseFirestoreException?) -> Unit + ) = callbackFlow { + val executor = callbackExecutorMap[android.firestore] ?: TaskExecutors.MAIN_THREAD + val metadataChanges = if(includeMetadataChanges) MetadataChanges.INCLUDE else MetadataChanges.EXCLUDE + val registration = android.addSnapshotListener(executor, metadataChanges) { snapshots, exception -> + listener(snapshots, exception) + } + awaitClose { registration.remove() } + } +} + +actual open class Query(open val android: com.google.firebase.firestore.Query) { + + actual suspend fun get() = QuerySnapshot(android.get().await()) + + actual fun limit(limit: Number) = Query(android.limit(limit.toLong())) + + actual val snapshots get() = addSnapshotListener { snapshot, exception -> + snapshot?.let { trySend(QuerySnapshot(snapshot)) } + exception?.let { close(exception) } + } + + actual fun snapshots(includeMetadataChanges: Boolean) = addSnapshotListener(includeMetadataChanges) { snapshot, exception -> + snapshot?.let { trySend(QuerySnapshot(snapshot)) } + exception?.let { close(exception) } + } + + 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(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(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 + } + ) + + 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)) + + private fun addSnapshotListener( + includeMetadataChanges: Boolean = false, + listener: ProducerScope.(com.google.firebase.firestore.QuerySnapshot?, com.google.firebase.firestore.FirebaseFirestoreException?) -> Unit + ) = callbackFlow { + val executor = callbackExecutorMap[android.firestore] ?: TaskExecutors.MAIN_THREAD + val metadataChanges = if(includeMetadataChanges) MetadataChanges.INCLUDE else MetadataChanges.EXCLUDE + val registration = android.addSnapshotListener(executor, metadataChanges) { snapshots, exception -> + listener(snapshots, exception) + } + awaitClose { registration.remove() } + } +} + +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)) + } +} diff --git a/test-utils/src/commonMain/kotlin/dev/gitlive/firebase/TestUtils.kt b/test-utils/src/commonMain/kotlin/dev/gitlive/firebase/TestUtils.kt index bf9fcdc19..52ff28a51 100644 --- a/test-utils/src/commonMain/kotlin/dev/gitlive/firebase/TestUtils.kt +++ b/test-utils/src/commonMain/kotlin/dev/gitlive/firebase/TestUtils.kt @@ -1,4 +1,3 @@ -@file:JvmName("TestUtilsJVM") /* * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. */ 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) -}