Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Database transactions #199

Open
wants to merge 37 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
63d290a
Add database runTransaction expect and js externals
michaelprichardson Jul 6, 2021
b5a0b30
Add runTransaction methods for jvm, ios and js
michaelprichardson Jul 6, 2021
0b84470
Add serialization plugin to database build
michaelprichardson Jul 14, 2021
1536e42
Fix KSerialiser
michaelprichardson Jul 15, 2021
aaca522
Merge branch 'master' into database-transactions
michaelprichardson Jul 15, 2021
8b63096
Fix error thrown
michaelprichardson Jul 19, 2021
ab8d5d2
Merge branch 'database-transactions' of github.com:GitLiveApp/firebas…
michaelprichardson Jul 19, 2021
900426b
Fix ios error thrown
michaelprichardson Jul 19, 2021
c6fb31b
Remove Result type
michaelprichardson Jul 19, 2021
50e33d6
Add delete app method
michaelprichardson Aug 6, 2021
fcc97ce
Add database emulator and rules
michaelprichardson Aug 6, 2021
515b7da
Remove suspend
michaelprichardson Aug 6, 2021
872c1a1
Add database tests
michaelprichardson Aug 6, 2021
98fd19a
Fix js transaction name
michaelprichardson Aug 23, 2021
1d8b989
Add env vars for emulators to actions
michaelprichardson Aug 23, 2021
b4fbdb1
Merge branch 'master' into database-transactions
michaelprichardson Aug 23, 2021
59bbacf
Use GITHUB_ENV for env vars
michaelprichardson Aug 23, 2021
b9688c9
Move env to run level
michaelprichardson Aug 23, 2021
7350bb3
Move Firebase cleanup into function instead of AfterTest
michaelprichardson Aug 24, 2021
2f7a650
Remove await from js.transaction
michaelprichardson Aug 24, 2021
6ae78af
Merge branch 'database-transactions' of github.com:GitLiveApp/firebas…
michaelprichardson Aug 24, 2021
eccce95
Remove FIREBASE_DATABASE_EMULATOR_HOST env var
michaelprichardson Aug 24, 2021
06634d2
Reset mocha timeouts to 5s
michaelprichardson Aug 24, 2021
b97957d
Remove comment
michaelprichardson Aug 24, 2021
7425a6b
Merge branch 'master' into database-transactions
michaelprichardson Oct 27, 2021
a2332de
Merge branch 'master' into database-transactions
michaelprichardson Mar 16, 2022
6e41e70
updates to database-transactions
michaelprichardson Mar 17, 2022
87bb543
update serialization to 1.5.32
michaelprichardson Mar 18, 2022
9ae03ae
use .value
michaelprichardson Mar 18, 2022
6aef9dc
Merge branch 'master' into database-transactions
michaelprichardson Apr 1, 2022
1ed1028
Merge remote-tracking branch 'origin/database-transactions' into data…
michaelprichardson Apr 1, 2022
1546e58
Merge branch 'master' into database-transactions
michaelprichardson May 19, 2022
320846c
Merge branch 'master' into database-transactions
michaelprichardson Jul 27, 2022
b928409
enable the new memory model and disable freezing
michaelprichardson Jul 27, 2022
3c95915
move database tests into single file
michaelprichardson Jul 28, 2022
cbcdd48
import CompletableDeferred
michaelprichardson Aug 1, 2022
2dff075
return correct type to FIRTransactionResult
michaelprichardson Aug 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ object Firebase
expect class FirebaseApp {
val name: String
val options: FirebaseOptions
fun delete()
}

/** Returns the default firebase app instance. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -317,6 +318,11 @@ external object firebase {
fun update(value: Any?): Promise<Unit>
fun set(value: Any?): Promise<Unit>
fun push(): ThenableReference
fun <T> transaction(
transactionUpdate: (currentData: T) -> T,
onComplete: (error: Error?, committed: Boolean, snapshot: DataSnapshot?) -> Unit,
applyLocally: Boolean?
): Promise<T>
}

open class DataSnapshot {
Expand Down
1 change: 1 addition & 0 deletions firebase-database/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ version = project.property("firebase-database.version") as String
plugins {
id("com.android.library")
kotlin("multiplatform")
kotlin("plugin.serialization") version "1.5.10"
}

repositories {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license.
*/

@file:JvmName("tests")
package dev.gitlive.firebase.database

import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking

actual val emulatorHost: String = "10.0.2.2"

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

actual fun runTest(test: suspend () -> Unit) = runBlocking { test() }
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@
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
import dev.gitlive.firebase.decode
import dev.gitlive.firebase.safeOffer
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
Expand All @@ -25,6 +23,7 @@ import kotlinx.coroutines.selects.select
import kotlinx.coroutines.tasks.asDeferred
import kotlinx.coroutines.tasks.await
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationStrategy

@PublishedApi
Expand Down Expand Up @@ -188,8 +187,30 @@ actual class DatabaseReference internal constructor(
actual suspend fun removeValue() = android.removeValue()
.run { if(persistenceEnabled) await() else awaitWhileOnline() }
.run { Unit }
}

actual suspend fun <T> runTransaction(strategy: KSerializer<T>, transactionUpdate: (currentData: T) -> T): DataSnapshot {
val deferred = CompletableDeferred<DataSnapshot>()
android.runTransaction(object : Transaction.Handler {

override fun doTransaction(currentData: MutableData) =
Transaction.success(transactionUpdate(decode(strategy, currentData)) as MutableData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
override fun doTransaction(currentData: MutableData) =
Transaction.success(transactionUpdate(decode(strategy, currentData)) as MutableData)
override fun doTransaction(currentData: MutableData): Transaction.Result {
currentData.value = currentData.value?.let {
transactionUpdate(decode(strategy, it))
}
return Transaction.success(currentData)
}

I'm suggesting this change because I think transactions are not working.
We need to work with currentData.value, not with currentData itself.
Also we need to tolerate null value as described in the Firebase documentation.
I'm happy to help to update and merge this PR but I'm not sure I'll be able to do the same in js and iOS.


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) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]. */
Expand Down Expand Up @@ -71,6 +72,8 @@ expect class DatabaseReference : Query {
suspend fun <T> setValue(strategy: SerializationStrategy<T>, value: T, encodeDefaults: Boolean = true)
suspend fun updateChildren(update: Map<String, Any?>, encodeDefaults: Boolean = true)
suspend fun removeValue()

suspend fun <T> runTransaction(strategy: KSerializer<T>, transactionUpdate: (currentData: T) -> T): DataSnapshot
}

expect class DataSnapshot {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package dev.gitlive.firebase.database

import dev.gitlive.firebase.*
import kotlinx.coroutines.flow.first
import kotlinx.serialization.*
import kotlin.test.*

expect val emulatorHost: String
expect val context: Any
expect fun runTest(test: suspend () -> Unit)

class FirebaseDatabaseTest {

@Serializable
data class DatabaseTest(val title: String, val likes: Int = 0)

@BeforeTest
fun initializeFirebase() {
Firebase
.takeIf { Firebase.apps(context).isEmpty() }
?.apply {
initialize(
context,
FirebaseOptions(
applicationId = "1:846484016111:ios:dd1f6688bad7af768c841a",
apiKey = "AIzaSyCK87dcMFhzCz_kJVs2cT2AVlqOTLuyWV0",
databaseUrl = "http://fir-kotlin-sdk.firebaseio.com",
storageBucket = "fir-kotlin-sdk.appspot.com",
projectId = "fir-kotlin-sdk",
gcmSenderId = "846484016111"
)
)
Firebase.database.useEmulator(emulatorHost, 9000)
}
}

@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 fun cleanUp() {
Firebase
.takeIf { Firebase.apps(context).isNotEmpty() }
?.apply { app.delete() }
}

private suspend fun <T> setupDatabase(ref: DatabaseReference, data: T, strategy: SerializationStrategy<T>) {
try {
ref.setValue(strategy, data)
} catch (err: DatabaseException) {
println(err)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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
Expand Down Expand Up @@ -156,6 +157,24 @@ actual class DatabaseReference internal constructor(
actual suspend fun removeValue() {
ios.await(persistenceEnabled) { removeValueWithCompletionBlock(it) }
}

actual suspend fun <T> runTransaction(strategy: KSerializer<T>, transactionUpdate: (currentData: T) -> T): DataSnapshot {
val deferred = CompletableDeferred<DataSnapshot>()
ios.runTransactionBlock(
block = { firMutableData ->
FIRTransactionResult.successWithValue(transactionUpdate(decode(strategy, firMutableData)) as 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")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package dev.gitlive.firebase.database

import kotlinx.coroutines.*
import platform.Foundation.*

actual val emulatorHost: String = "localhost"

actual val context: Any = Unit

actual fun runTest(test: suspend () -> Unit) = runBlocking {
val testRun = MainScope().async { test() }
while (testRun.isActive) {
NSRunLoop.mainRunLoop.runMode(
NSDefaultRunLoopMode,
beforeDate = NSDate.create(timeInterval = 1.0, sinceDate = NSDate())
)
yield()
}
testRun.await()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -127,6 +128,23 @@ actual class DatabaseReference internal constructor(override val js: firebase.da

actual suspend fun <T> setValue(strategy: SerializationStrategy<T>, value: T, encodeDefaults: Boolean) =
rethrow { js.set(encode(strategy, value, encodeDefaults)).awaitWhileOnline() }

actual suspend fun <T> runTransaction(strategy: KSerializer<T>, transactionUpdate: (currentData: T) -> T): DataSnapshot {
val deferred = CompletableDeferred<DataSnapshot>()
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) {
Expand Down Expand Up @@ -185,7 +203,9 @@ suspend fun <T> Promise<T>.awaitWhileOnline(): T = coroutineScope {
val notConnected = Firebase.database
.reference(".info/connected")
.valueEvents
.filter { !it.value<Boolean>() }
.filter {
!it.value<Boolean>()
}
.produceIn(this)

select<T> {
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
6 changes: 6 additions & 0 deletions test/database.rules.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"rules": {
".read": true,
".write": true
}
}
Loading