diff --git a/firebase-app/src/androidMain/kotlin/dev/gitlive/firebase/firebase.kt b/firebase-app/src/androidMain/kotlin/dev/gitlive/firebase/firebase.kt index ca86a96ee..0527e2e92 100644 --- a/firebase-app/src/androidMain/kotlin/dev/gitlive/firebase/firebase.kt +++ b/firebase-app/src/androidMain/kotlin/dev/gitlive/firebase/firebase.kt @@ -34,6 +34,8 @@ actual class FirebaseApp internal constructor(val android: com.google.firebase.F get() = android.name actual val options: FirebaseOptions get() = android.options.run { FirebaseOptions(applicationId, apiKey, databaseUrl, gaTrackingId, storageBucket, projectId) } + + actual fun delete() = android.delete() } actual fun Firebase.apps(context: Any?) = com.google.firebase.FirebaseApp.getApps(context as Context) diff --git a/firebase-app/src/commonMain/kotlin/dev/gitlive/firebase/firebase.kt b/firebase-app/src/commonMain/kotlin/dev/gitlive/firebase/firebase.kt index 871b60b59..75e3df31d 100644 --- a/firebase-app/src/commonMain/kotlin/dev/gitlive/firebase/firebase.kt +++ b/firebase-app/src/commonMain/kotlin/dev/gitlive/firebase/firebase.kt @@ -19,6 +19,7 @@ object Firebase expect class FirebaseApp { val name: String val options: FirebaseOptions + fun delete() } /** Returns the default firebase app instance. */ diff --git a/firebase-app/src/iosMain/kotlin/dev/gitlive/firebase/firebase.kt b/firebase-app/src/iosMain/kotlin/dev/gitlive/firebase/firebase.kt index b4e6819ef..c8488259f 100644 --- a/firebase-app/src/iosMain/kotlin/dev/gitlive/firebase/firebase.kt +++ b/firebase-app/src/iosMain/kotlin/dev/gitlive/firebase/firebase.kt @@ -5,6 +5,7 @@ package dev.gitlive.firebase import cocoapods.FirebaseCore.* +import kotlinx.coroutines.CompletableDeferred actual open class FirebaseException(message: String) : Exception(message) actual open class FirebaseNetworkException(message: String) : FirebaseException(message) @@ -31,6 +32,8 @@ actual class FirebaseApp internal constructor(val ios: FIRApp) { get() = ios.name actual val options: FirebaseOptions get() = ios.options.run { FirebaseOptions(bundleID, APIKey!!, databaseURL!!, trackingID, storageBucket, projectID) } + + actual fun delete() { } } actual fun Firebase.apps(context: Any?) = FIRApp.allApps() diff --git a/firebase-app/src/jsMain/kotlin/dev/gitlive/firebase/firebase.kt b/firebase-app/src/jsMain/kotlin/dev/gitlive/firebase/firebase.kt index 16214a1c4..1d911c290 100644 --- a/firebase-app/src/jsMain/kotlin/dev/gitlive/firebase/firebase.kt +++ b/firebase-app/src/jsMain/kotlin/dev/gitlive/firebase/firebase.kt @@ -28,6 +28,8 @@ actual class FirebaseApp internal constructor(val js: firebase.App) { get() = js.options.run { FirebaseOptions(applicationId, apiKey, databaseUrl, gaTrackingId, storageBucket, projectId, messagingSenderId, authDomain) } + + actual fun delete() = js.delete() } actual fun Firebase.apps(context: Any?) = firebase.apps.map { FirebaseApp(it) } diff --git a/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/externals.kt b/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/externals.kt index 670ddeab4..1927178d9 100644 --- a/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/externals.kt +++ b/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/externals.kt @@ -15,6 +15,7 @@ external object firebase { open class App { val name: String val options: Options + fun delete() fun functions(region: String? = definedExternally): functions.Functions fun database(url: String? = definedExternally): database.Database fun firestore(): firestore.Firestore @@ -320,6 +321,11 @@ external object firebase { fun update(value: Any?): Promise fun set(value: Any?): Promise fun push(): ThenableReference + fun transaction( + transactionUpdate: (currentData: T) -> T, + onComplete: (error: Error?, committed: Boolean, snapshot: DataSnapshot?) -> Unit, + applyLocally: Boolean? + ): Promise } open class DataSnapshot { diff --git a/firebase-database/build.gradle.kts b/firebase-database/build.gradle.kts index 30bbfedb0..16a1ba893 100644 --- a/firebase-database/build.gradle.kts +++ b/firebase-database/build.gradle.kts @@ -23,17 +23,11 @@ android { defaultConfig { minSdk = property("minSdkVersion") as Int targetSdk = property("targetSdkVersion") as Int - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - multiDexEnabled = true } sourceSets { getByName("main") { manifest.srcFile("src/androidMain/AndroidManifest.xml") } - getByName("androidTest"){ - java.srcDir(file("src/androidAndroidTest/kotlin")) - manifest.srcFile("src/androidAndroidTest/AndroidManifest.xml") - } } testOptions { unitTests.apply { @@ -135,9 +129,9 @@ kotlin { apiVersion = "1.6" languageVersion = "1.6" progressiveMode = true - optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") - optIn("kotlinx.coroutines.FlowPreview") - optIn("kotlinx.serialization.InternalSerializationApi") +// optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") +// optIn("kotlinx.coroutines.FlowPreview") +// optIn("kotlinx.serialization.InternalSerializationApi") } } 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 5af09fafc..7e51e4fdd 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 @@ -5,10 +5,8 @@ package dev.gitlive.firebase.database import com.google.android.gms.tasks.Task -import com.google.firebase.database.ChildEventListener -import com.google.firebase.database.Logger +import com.google.firebase.database.* import com.google.firebase.database.ServerValue -import com.google.firebase.database.ValueEventListener import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseApp import dev.gitlive.firebase.database.ChildEvent.Type @@ -22,7 +20,9 @@ import kotlinx.coroutines.flow.produceIn import kotlinx.coroutines.selects.select import kotlinx.coroutines.tasks.asDeferred import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.CompletableDeferred import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationStrategy @PublishedApi @@ -189,8 +189,34 @@ actual class DatabaseReference internal constructor( actual suspend fun removeValue() = android.removeValue() .run { if(persistenceEnabled) await() else awaitWhileOnline() } .run { Unit } -} + actual suspend fun runTransaction(strategy: KSerializer, transactionUpdate: (currentData: T) -> T): DataSnapshot { + val deferred = CompletableDeferred() + android.runTransaction(object : Transaction.Handler { + + override fun doTransaction(currentData: MutableData): Transaction.Result { + currentData.value = currentData.value?.let { + transactionUpdate(decode(strategy, it)) + } + return Transaction.success(currentData) + } + + override fun onComplete( + error: DatabaseError?, + committed: Boolean, + snapshot: com.google.firebase.database.DataSnapshot? + ) { + if (error != null) { + deferred.completeExceptionally(error.toException()) + } else { + deferred.complete(DataSnapshot(snapshot!!)) + } + } + + }) + return deferred.await() + } +} @Suppress("UNCHECKED_CAST") actual class DataSnapshot internal constructor(val android: com.google.firebase.database.DataSnapshot) { 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 5860a8be8..0936c1123 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 @@ -9,6 +9,7 @@ import dev.gitlive.firebase.FirebaseApp import dev.gitlive.firebase.database.ChildEvent.Type.* import kotlinx.coroutines.flow.Flow import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationStrategy /** Returns the [FirebaseDatabase] instance of the default [FirebaseApp]. */ @@ -72,6 +73,8 @@ expect class DatabaseReference : Query { suspend fun setValue(strategy: SerializationStrategy, value: T, encodeDefaults: Boolean = true) suspend fun updateChildren(update: Map, encodeDefaults: Boolean = true) suspend fun removeValue() + + suspend fun runTransaction(strategy: KSerializer, transactionUpdate: (currentData: T) -> T): DataSnapshot } expect class DataSnapshot { diff --git a/firebase-database/src/commonTest/kotlin/dev.gitlive.firebase.database/database.kt b/firebase-database/src/commonTest/kotlin/dev/gitlive/firebase/database/database.kt similarity index 50% rename from firebase-database/src/commonTest/kotlin/dev.gitlive.firebase.database/database.kt rename to firebase-database/src/commonTest/kotlin/dev/gitlive/firebase/database/database.kt index 265f7b5f4..6e3d62bec 100644 --- a/firebase-database/src/commonTest/kotlin/dev.gitlive.firebase.database/database.kt +++ b/firebase-database/src/commonTest/kotlin/dev/gitlive/firebase/database/database.kt @@ -1,7 +1,3 @@ -/* - * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. - */ - package dev.gitlive.firebase.database import dev.gitlive.firebase.* @@ -18,6 +14,9 @@ class FirebaseDatabaseTest { @Serializable data class FirebaseDatabaseChildTest(val prop1: String? = null, val time: Double = 0.0) + @Serializable + data class DatabaseTest(val title: String, val likes: Int = 0) + @BeforeTest fun initializeFirebase() { Firebase @@ -28,7 +27,7 @@ class FirebaseDatabaseTest { FirebaseOptions( applicationId = "1:846484016111:ios:dd1f6688bad7af768c841a", apiKey = "AIzaSyCK87dcMFhzCz_kJVs2cT2AVlqOTLuyWV0", - databaseUrl = "https://fir-kotlin-sdk.firebaseio.com", + databaseUrl = "http://fir-kotlin-sdk.firebaseio.com", storageBucket = "fir-kotlin-sdk.appspot.com", projectId = "fir-kotlin-sdk", gcmSenderId = "846484016111" @@ -52,7 +51,7 @@ class FirebaseDatabaseTest { assertEquals(testValue, testReferenceValue) } - + @Test fun testChildCount() = runTest { setupRealtimeData() @@ -65,6 +64,52 @@ class FirebaseDatabaseTest { assertEquals(3, firebaseDatabaseChildCount) } + @Test + fun testBasicIncrementTransaction() = runTest { + val data = DatabaseTest("PostOne", 2) + val userRef = Firebase.database.reference("users/user_1/post_id_1") + setupDatabase(userRef, data, DatabaseTest.serializer()) + + // Check database before transaction + val userDocBefore = userRef.valueEvents.first().value(DatabaseTest.serializer()) + assertEquals(data.title, userDocBefore.title) + assertEquals(data.likes, userDocBefore.likes) + + // Run transaction + val transactionSnapshot = userRef.runTransaction(DatabaseTest.serializer()) { DatabaseTest(data.title, it.likes + 1) } + val userDocAfter = transactionSnapshot.value(DatabaseTest.serializer()) + + // Check the database after transaction + assertEquals(data.title, userDocAfter.title) + assertEquals(data.likes + 1, userDocAfter.likes) + + // cleanUp Firebase + cleanUp() + } + + @Test + fun testBasicDecrementTransaction() = runTest { + val data = DatabaseTest("PostTwo", 2) + val userRef = Firebase.database.reference("users/user_1/post_id_2") + setupDatabase(userRef, data, DatabaseTest.serializer()) + + // Check database before transaction + val userDocBefore = userRef.valueEvents.first().value(DatabaseTest.serializer()) + assertEquals(data.title, userDocBefore.title) + assertEquals(data.likes, userDocBefore.likes) + + // Run transaction + val transactionSnapshot = userRef.runTransaction(DatabaseTest.serializer()) { DatabaseTest(data.title, it.likes - 1) } + val userDocAfter = transactionSnapshot.value(DatabaseTest.serializer()) + + // Check the database after transaction + assertEquals(data.title, userDocAfter.title) + assertEquals(data.likes - 1, userDocAfter.likes) + + // cleanUp Firebase + cleanUp() + } + private suspend fun setupRealtimeData() { val firebaseDatabaseTestReference = Firebase.database .reference("FirebaseRealtimeDatabaseTest") @@ -77,4 +122,19 @@ class FirebaseDatabaseTest { firebaseDatabaseTestReference.child("2").setValue(firebaseDatabaseChildTest2) firebaseDatabaseTestReference.child("3").setValue(firebaseDatabaseChildTest3) } + + private fun cleanUp() { + Firebase + .takeIf { Firebase.apps(context).isNotEmpty() } + ?.apply { app.delete() } + } + + private suspend fun setupDatabase(ref: DatabaseReference, data: T, strategy: SerializationStrategy) { + try { + ref.setValue(strategy, data) + } catch (err: DatabaseException) { + println(err) + } + } + } \ No newline at end of file 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 eef0926ae..7427ff201 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 @@ -13,7 +13,6 @@ import dev.gitlive.firebase.database.ChildEvent.Type.* import dev.gitlive.firebase.decode import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.channels.ClosedSendChannelException import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.callbackFlow @@ -21,6 +20,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.produceIn import kotlinx.coroutines.selects.select import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationStrategy import platform.Foundation.* import kotlin.collections.component1 @@ -158,6 +158,27 @@ actual class DatabaseReference internal constructor( actual suspend fun removeValue() { ios.await(persistenceEnabled) { removeValueWithCompletionBlock(it) } } + + actual suspend fun runTransaction(strategy: KSerializer, transactionUpdate: (currentData: T) -> T): DataSnapshot { + val deferred = CompletableDeferred() + ios.runTransactionBlock( + block = { firMutableData -> + firMutableData?.value = firMutableData?.value?.let { + transactionUpdate(decode(strategy, firMutableData.value)) + } + FIRTransactionResult.successWithValue(firMutableData!!) + }, + andCompletionBlock = { error, _, snapshot -> + if (error != null) { + deferred.completeExceptionally(DatabaseException(error.toString(), null)) + } else { + deferred.complete(DataSnapshot(snapshot!!)) + } + }, + withLocalEvents = false + ) + return deferred.await() + } } @Suppress("UNCHECKED_CAST") 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 9a595e051..89fd672fc 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 @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.produceIn import kotlinx.coroutines.selects.select import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationStrategy import kotlin.js.Promise @@ -128,6 +129,23 @@ actual class DatabaseReference internal constructor(override val js: firebase.da actual suspend fun setValue(strategy: SerializationStrategy, value: T, encodeDefaults: Boolean) = rethrow { js.set(encode(strategy, value, encodeDefaults)).awaitWhileOnline() } + + actual suspend fun runTransaction(strategy: KSerializer, transactionUpdate: (currentData: T) -> T): DataSnapshot { + val deferred = CompletableDeferred() + js.transaction( + transactionUpdate, + { error, _, snapshot -> + if (error != null) { + deferred.completeExceptionally(error) + } else { + deferred.complete(DataSnapshot(snapshot!!)) + } + }, + applyLocally = false + ) + return deferred.await() + } + } actual class DataSnapshot internal constructor(val js: firebase.database.DataSnapshot) { @@ -186,7 +204,9 @@ suspend fun Promise.awaitWhileOnline(): T = coroutineScope { val notConnected = Firebase.database .reference(".info/connected") .valueEvents - .filter { !it.value() } + .filter { + !it.value() + } .produceIn(this) select { diff --git a/firebase-database/src/jsTest/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/jsTest/kotlin/dev/gitlive/firebase/database/database.kt new file mode 100644 index 000000000..e30aac335 --- /dev/null +++ b/firebase-database/src/jsTest/kotlin/dev/gitlive/firebase/database/database.kt @@ -0,0 +1,26 @@ +package dev.gitlive.firebase.database + +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 + .promise { + try { + test() + } catch (e: Throwable) { + e.log() + throw e + } + }.asDynamic() + +internal fun Throwable.log() { + console.error(this) + cause?.let { + console.error("Caused by:") + it.log() + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index dfd9c82b1..73f691ff8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,6 +12,8 @@ kotlin.native.cacheKind.iosX64=none #kotlin.native.enableDependencyPropagation=false kotlin.native.enableParallelExecutionCheck=false kotlin.setJvmTargetFromAndroidCompileOptions=true +kotlin.native.binary.memoryModel=experimental +kotlin.native.binary.freezing=disabled org.gradle.jvmargs=-Xmx2048m org.gradle.parallel=true systemProp.org.gradle.internal.publish.checksums.insecure=true