diff --git a/firebase-app/src/wasmJsMain/kotlin/dev/gitlive/firebase/externals/app.kt b/firebase-app/src/wasmJsMain/kotlin/dev/gitlive/firebase/externals/app.kt new file mode 100644 index 000000000..ab2066cf9 --- /dev/null +++ b/firebase-app/src/wasmJsMain/kotlin/dev/gitlive/firebase/externals/app.kt @@ -0,0 +1,32 @@ +@file:JsModule("firebase/app") +@file:JsNonModule + +package dev.gitlive.firebase.externals + +import kotlin.js.Promise + +external fun initializeApp(options: Any, name: String = definedExternally): FirebaseApp + +external fun getApp(name: String = definedExternally): FirebaseApp + +external fun getApps(): Array + +external fun deleteApp(app: FirebaseApp): Promise + +external interface FirebaseApp { + val automaticDataCollectionEnabled: Boolean + val name: String + val options: FirebaseOptions +} + +external interface FirebaseOptions { + val apiKey: String + val appId : String + val authDomain: String? + val databaseURL: String? + val measurementId: String? + val messagingSenderId: String? + val gaTrackingId: String? + val projectId: String? + val storageBucket: String? +} diff --git a/firebase-app/src/wasmJsMain/kotlin/dev/gitlive/firebase/firebase.kt b/firebase-app/src/wasmJsMain/kotlin/dev/gitlive/firebase/firebase.kt new file mode 100644 index 000000000..ed7619ab9 --- /dev/null +++ b/firebase-app/src/wasmJsMain/kotlin/dev/gitlive/firebase/firebase.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.gitlive.firebase + +import dev.gitlive.firebase.externals.deleteApp +import dev.gitlive.firebase.externals.getApp +import dev.gitlive.firebase.externals.getApps +import dev.gitlive.firebase.externals.initializeApp +import kotlin.js.json +import dev.gitlive.firebase.externals.FirebaseApp as JsFirebaseApp + +actual val Firebase.app: FirebaseApp + get() = FirebaseApp(getApp()) + +actual fun Firebase.app(name: String): FirebaseApp = + FirebaseApp(getApp(name)) + +actual fun Firebase.initialize(context: Any?): FirebaseApp? = + throw UnsupportedOperationException("Cannot initialize firebase without options in JS") + +actual fun Firebase.initialize(context: Any?, options: FirebaseOptions, name: String): FirebaseApp = + FirebaseApp(initializeApp(options.toJson(), name)) + +actual fun Firebase.initialize(context: Any?, options: FirebaseOptions) = + FirebaseApp(initializeApp(options.toJson())) + +actual class FirebaseApp internal constructor(val js: JsFirebaseApp) { + actual val name: String + get() = js.name + actual val options: FirebaseOptions + get() = js.options.run { + FirebaseOptions(appId, apiKey, databaseURL, gaTrackingId, storageBucket, projectId, messagingSenderId, authDomain) + } + + actual suspend fun delete() { + deleteApp(js) + } +} + +actual fun Firebase.apps(context: Any?) = getApps().map { FirebaseApp(it) } + +private fun FirebaseOptions.toJson() = json( + "apiKey" to apiKey, + "appId" to applicationId, + "databaseURL" to (databaseUrl ?: undefined), + "storageBucket" to (storageBucket ?: undefined), + "projectId" to (projectId ?: undefined), + "gaTrackingId" to (gaTrackingId ?: undefined), + "messagingSenderId" to (gcmSenderId ?: undefined), + "authDomain" to (authDomain ?: undefined) +) + +actual open class FirebaseException(code: String?, cause: Throwable) : Exception("$code: ${cause.message}", cause) +actual open class FirebaseNetworkException(code: String?, cause: Throwable) : FirebaseException(code, cause) +actual open class FirebaseTooManyRequestsException(code: String?, cause: Throwable) : FirebaseException(code, cause) +actual open class FirebaseApiNotAvailableException(code: String?, cause: Throwable) : FirebaseException(code, cause) diff --git a/firebase-auth/src/wasmJsMain/kotlin/auth/auth.kt b/firebase-auth/src/wasmJsMain/kotlin/auth/auth.kt new file mode 100644 index 000000000..f2db371b4 --- /dev/null +++ b/firebase-auth/src/wasmJsMain/kotlin/auth/auth.kt @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.gitlive.firebase.auth + +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseApp +import dev.gitlive.firebase.FirebaseException +import dev.gitlive.firebase.FirebaseNetworkException +import dev.gitlive.firebase.auth.externals.* +import kotlinx.coroutines.await +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlin.js.json +import dev.gitlive.firebase.auth.externals.AuthResult as JsAuthResult + +actual val Firebase.auth + get() = rethrow { FirebaseAuth(getAuth()) } + +actual fun Firebase.auth(app: FirebaseApp) = + rethrow { FirebaseAuth(getAuth(app.js)) } + +actual class FirebaseAuth internal constructor(val js: Auth) { + + actual val currentUser: FirebaseUser? + get() = rethrow { js.currentUser?.let { FirebaseUser(it) } } + + actual val authStateChanged get() = callbackFlow { + val unsubscribe = js.onAuthStateChanged { + trySend(it?.let { FirebaseUser(it) }) + } + awaitClose { unsubscribe() } + } + + actual val idTokenChanged get() = callbackFlow { + val unsubscribe = js.onIdTokenChanged { + trySend(it?.let { FirebaseUser(it) }) + } + awaitClose { unsubscribe() } + } + + actual var languageCode: String + get() = js.languageCode ?: "" + set(value) { js.languageCode = value } + + actual suspend fun applyActionCode(code: String) = rethrow { applyActionCode(js, code).await() } + actual suspend fun confirmPasswordReset(code: String, newPassword: String) = rethrow { confirmPasswordReset(js, code, newPassword).await() } + + actual suspend fun createUserWithEmailAndPassword(email: String, password: String) = + rethrow { AuthResult(createUserWithEmailAndPassword(js, email, password).await()) } + + actual suspend fun fetchSignInMethodsForEmail(email: String): List = rethrow { fetchSignInMethodsForEmail(js, email).await().asList() } + + actual suspend fun sendPasswordResetEmail(email: String, actionCodeSettings: ActionCodeSettings?) = + rethrow { sendPasswordResetEmail(js, email, actionCodeSettings?.toJson()).await() } + + actual suspend fun sendSignInLinkToEmail(email: String, actionCodeSettings: ActionCodeSettings) = + rethrow { sendSignInLinkToEmail(js, email, actionCodeSettings.toJson()).await() } + + actual fun isSignInWithEmailLink(link: String) = rethrow { isSignInWithEmailLink(js, link) } + + actual suspend fun signInWithEmailAndPassword(email: String, password: String) = + rethrow { AuthResult(signInWithEmailAndPassword(js, email, password).await()) } + + actual suspend fun signInWithCustomToken(token: String) = + rethrow { AuthResult(signInWithCustomToken(js, token).await()) } + + actual suspend fun signInAnonymously() = + rethrow { AuthResult(signInAnonymously(js).await()) } + + actual suspend fun signInWithCredential(authCredential: AuthCredential) = + rethrow { AuthResult(signInWithCredential(js, authCredential.js).await()) } + + actual suspend fun signInWithEmailLink(email: String, link: String) = + rethrow { AuthResult(signInWithEmailLink(js, email, link).await()) } + + actual suspend fun signOut() = rethrow { signOut(js).await() } + + actual suspend fun updateCurrentUser(user: FirebaseUser) = + rethrow { updateCurrentUser(js, user.js).await() } + + actual suspend fun verifyPasswordResetCode(code: String): String = + rethrow { verifyPasswordResetCode(js, code).await() } + + actual suspend fun checkActionCode(code: String): T = rethrow { + val result = checkActionCode(js, code).await() + @Suppress("UNCHECKED_CAST") + return when(result.operation) { + "EMAIL_SIGNIN" -> ActionCodeResult.SignInWithEmailLink + "VERIFY_EMAIL" -> ActionCodeResult.VerifyEmail(result.data.email!!) + "PASSWORD_RESET" -> ActionCodeResult.PasswordReset(result.data.email!!) + "RECOVER_EMAIL" -> ActionCodeResult.RecoverEmail(result.data.email!!, result.data.previousEmail!!) + "VERIFY_AND_CHANGE_EMAIL" -> ActionCodeResult.VerifyBeforeChangeEmail( + result.data.email!!, + result.data.previousEmail!! + ) + "REVERT_SECOND_FACTOR_ADDITION" -> ActionCodeResult.RevertSecondFactorAddition( + result.data.email!!, + result.data.multiFactorInfo?.let { MultiFactorInfo(it) } + ) + else -> throw UnsupportedOperationException(result.operation) + } as T + } + + actual fun useEmulator(host: String, port: Int) = rethrow { connectAuthEmulator(js, "http://$host:$port") } +} + +actual class AuthResult internal constructor(val js: JsAuthResult) { + actual val user: FirebaseUser? + get() = rethrow { js.user?.let { FirebaseUser(it) } } +} + +actual class AuthTokenResult(val js: IdTokenResult) { +// actual val authTimestamp: Long +// get() = js.authTime + actual val claims: Map + get() = (js("Object").keys(js.claims) as Array).mapNotNull { + key -> js.claims[key]?.let { key to it } + }.toMap() +// actual val expirationTimestamp: Long +// get() = android.expirationTime +// actual val issuedAtTimestamp: Long +// get() = js.issuedAtTime + actual val signInProvider: String? + get() = js.signInProvider + actual val token: String? + get() = js.token +} + +internal fun ActionCodeSettings.toJson() = json( + "url" to url, + "android" to (androidPackageName?.run { json("installApp" to installIfNotAvailable, "minimumVersion" to minimumVersion, "packageName" to packageName) } ?: undefined), + "dynamicLinkDomain" to (dynamicLinkDomain ?: undefined), + "handleCodeInApp" to canHandleCodeInApp, + "ios" to (iOSBundleId?.run { json("bundleId" to iOSBundleId) } ?: undefined) +) + +actual open class FirebaseAuthException(code: String?, cause: Throwable): FirebaseException(code, cause) +actual open class FirebaseAuthActionCodeException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) +actual open class FirebaseAuthEmailException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) +actual open class FirebaseAuthInvalidCredentialsException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) +actual open class FirebaseAuthWeakPasswordException(code: String?, cause: Throwable): FirebaseAuthInvalidCredentialsException(code, cause) +actual open class FirebaseAuthInvalidUserException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) +actual open class FirebaseAuthMultiFactorException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) +actual open class FirebaseAuthRecentLoginRequiredException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) +actual open class FirebaseAuthUserCollisionException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) +actual open class FirebaseAuthWebException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) + + +internal inline fun T.rethrow(function: T.() -> R): R = dev.gitlive.firebase.auth.rethrow { function() } + +private inline fun rethrow(function: () -> R): R { + try { + return function() + } catch (e: Exception) { + throw e + } catch(e: dynamic) { + throw errorToException(e) + } +} + +private fun errorToException(cause: dynamic) = when(val code = cause.code?.toString()?.lowercase()) { + "auth/invalid-user-token" -> FirebaseAuthInvalidUserException(code, cause) + "auth/requires-recent-login" -> FirebaseAuthRecentLoginRequiredException(code, cause) + "auth/user-disabled" -> FirebaseAuthInvalidUserException(code, cause) + "auth/user-token-expired" -> FirebaseAuthInvalidUserException(code, cause) + "auth/web-storage-unsupported" -> FirebaseAuthWebException(code, cause) + "auth/network-request-failed" -> FirebaseNetworkException(code, cause) + "auth/timeout" -> FirebaseNetworkException(code, cause) + "auth/weak-password" -> FirebaseAuthWeakPasswordException(code, cause) + "auth/invalid-credential", + "auth/invalid-verification-code", + "auth/missing-verification-code", + "auth/invalid-verification-id", + "auth/missing-verification-id" -> FirebaseAuthInvalidCredentialsException(code, cause) + "auth/maximum-second-factor-count-exceeded", + "auth/second-factor-already-in-use" -> FirebaseAuthMultiFactorException(code, cause) + "auth/credential-already-in-use" -> FirebaseAuthUserCollisionException(code, cause) + "auth/email-already-in-use" -> FirebaseAuthUserCollisionException(code, cause) + "auth/invalid-email" -> FirebaseAuthEmailException(code, cause) +// "auth/app-deleted" -> +// "auth/app-not-authorized" -> +// "auth/argument-error" -> +// "auth/invalid-api-key" -> +// "auth/operation-not-allowed" -> +// "auth/too-many-arguments" -> +// "auth/unauthorized-domain" -> + else -> { + println("Unknown error code in ${JSON.stringify(cause)}") + FirebaseAuthException(code, cause) + } +} diff --git a/firebase-auth/src/wasmJsMain/kotlin/auth/credentials.kt b/firebase-auth/src/wasmJsMain/kotlin/auth/credentials.kt new file mode 100644 index 000000000..eb6fb9efe --- /dev/null +++ b/firebase-auth/src/wasmJsMain/kotlin/auth/credentials.kt @@ -0,0 +1,100 @@ +package dev.gitlive.firebase.auth + +import dev.gitlive.firebase.auth.externals.ApplicationVerifier +import dev.gitlive.firebase.auth.externals.EmailAuthProvider +import dev.gitlive.firebase.auth.externals.FacebookAuthProvider +import dev.gitlive.firebase.auth.externals.GithubAuthProvider +import dev.gitlive.firebase.auth.externals.GoogleAuthProvider +import dev.gitlive.firebase.auth.externals.PhoneAuthProvider +import dev.gitlive.firebase.auth.externals.TwitterAuthProvider +import kotlinx.coroutines.await +import kotlin.js.json +import dev.gitlive.firebase.auth.externals.AuthCredential as JsAuthCredential +import dev.gitlive.firebase.auth.externals.OAuthProvider as JsOAuthProvider + +actual open class AuthCredential(val js: JsAuthCredential) { + actual val providerId: String + get() = js.providerId +} + +actual class PhoneAuthCredential(js: JsAuthCredential) : AuthCredential(js) +actual class OAuthCredential(js: JsAuthCredential) : AuthCredential(js) + +actual object EmailAuthProvider { + actual fun credential(email: String, password: String): AuthCredential = + AuthCredential(EmailAuthProvider.credential(email, password)) + + actual fun credentialWithLink( + email: String, + emailLink: String + ): AuthCredential = AuthCredential(EmailAuthProvider.credentialWithLink(email, emailLink)) +} + +actual object FacebookAuthProvider { + actual fun credential(accessToken: String): AuthCredential = + AuthCredential(FacebookAuthProvider.credential(accessToken)) +} + +actual object GithubAuthProvider { + actual fun credential(token: String): AuthCredential = + AuthCredential(GithubAuthProvider.credential(token)) +} + +actual object GoogleAuthProvider { + actual fun credential(idToken: String?, accessToken: String?): AuthCredential { + require(idToken != null || accessToken != null) { + "Both parameters are optional but at least one must be present." + } + return AuthCredential(GoogleAuthProvider.credential(idToken, accessToken)) + } +} + +actual class OAuthProvider(val js: JsOAuthProvider) { + + actual constructor( + provider: String, + scopes: List, + customParameters: Map, + auth: FirebaseAuth + ) : this(JsOAuthProvider(provider)) { + rethrow { + scopes.forEach { js.addScope(it) } + js.setCustomParameters(customParameters) + } + } + actual companion object { + actual fun credential(providerId: String, accessToken: String?, idToken: String?, rawNonce: String?): OAuthCredential = rethrow { + JsOAuthProvider(providerId) + .credential( + json( + "accessToken" to (accessToken ?: undefined), + "idToken" to (idToken ?: undefined), + "rawNonce" to (rawNonce ?: undefined) + ), + accessToken ?: undefined + ) + .let { OAuthCredential(it) } + } + } +} + +actual class PhoneAuthProvider(val js: PhoneAuthProvider) { + + actual constructor(auth: FirebaseAuth) : this(PhoneAuthProvider(auth.js)) + + actual fun credential(verificationId: String, smsCode: String): PhoneAuthCredential = PhoneAuthCredential(PhoneAuthProvider.credential(verificationId, smsCode)) + actual suspend fun verifyPhoneNumber(phoneNumber: String, verificationProvider: PhoneVerificationProvider): AuthCredential = rethrow { + val verificationId = js.verifyPhoneNumber(phoneNumber, verificationProvider.verifier).await() + val verificationCode = verificationProvider.getVerificationCode(verificationId) + credential(verificationId, verificationCode) + } +} + +actual interface PhoneVerificationProvider { + val verifier: ApplicationVerifier + suspend fun getVerificationCode(verificationId: String): String +} + +actual object TwitterAuthProvider { + actual fun credential(token: String, secret: String): AuthCredential = AuthCredential(TwitterAuthProvider.credential(token, secret)) +} diff --git a/firebase-auth/src/wasmJsMain/kotlin/auth/externals/auth.kt b/firebase-auth/src/wasmJsMain/kotlin/auth/externals/auth.kt new file mode 100644 index 000000000..e3bf651d9 --- /dev/null +++ b/firebase-auth/src/wasmJsMain/kotlin/auth/externals/auth.kt @@ -0,0 +1,299 @@ +@file:JsModule("firebase/auth") +@file:JsNonModule + +package dev.gitlive.firebase.auth.externals + +import dev.gitlive.firebase.Unsubscribe +import dev.gitlive.firebase.externals.FirebaseApp +import kotlin.js.Json +import kotlin.js.Promise + +external fun applyActionCode(auth: Auth, code: String): Promise + +external fun checkActionCode(auth: Auth, code: String): Promise + +external fun confirmPasswordReset(auth: Auth, code: String, newPassword: String): Promise + +external fun connectAuthEmulator(auth: Auth, url: String, options: Any? = definedExternally) + +external fun createUserWithEmailAndPassword( + auth: Auth, + email: String, + password: String +): Promise + +external fun deleteUser(user: User): Promise + +external fun fetchSignInMethodsForEmail(auth: Auth, email: String): Promise> + +external fun getAuth(app: FirebaseApp? = definedExternally): Auth + +external fun initializeAuth(app: FirebaseApp? = definedExternally, deps: dynamic = definedExternally): Auth + +external fun getIdToken(user: User, forceRefresh: Boolean?): Promise + +external fun getIdTokenResult(user: User, forceRefresh: Boolean?): Promise + +external fun isSignInWithEmailLink(auth: Auth, link: String): Boolean + +external fun linkWithCredential(user: User, credential: AuthCredential): Promise + +external fun multiFactor(user: User): MultiFactorUser + +external fun onAuthStateChanged(auth: Auth, nextOrObserver: (User?) -> Unit): Unsubscribe + +external fun onIdTokenChanged(auth: Auth, nextOrObserver: (User?) -> Unit): Unsubscribe + +external fun sendEmailVerification(user: User, actionCodeSettings: Any?): Promise + +external fun reauthenticateWithCredential( + user: User, + credential: AuthCredential +): Promise + +external fun reload(user: User): Promise + +external fun sendPasswordResetEmail( + auth: Auth, + email: String, + actionCodeSettings: Any? +): Promise + +external fun sendSignInLinkToEmail( + auth: Auth, + email: String, + actionCodeSettings: Any? +): Promise + +external fun signInAnonymously(auth: Auth): Promise + +external fun signInWithCredential(auth: Auth, authCredential: AuthCredential): Promise + +external fun signInWithCustomToken(auth: Auth, token: String): Promise + +external fun signInWithEmailAndPassword( + auth: Auth, + email: String, + password: String +): Promise + +external fun signInWithEmailLink(auth: Auth, email: String, link: String): Promise + +external fun signInWithPopup(auth: Auth, provider: AuthProvider): Promise + +external fun signInWithRedirect(auth: Auth, provider: AuthProvider): Promise + +external fun getRedirectResult(auth: Auth): Promise + +external fun signOut(auth: Auth): Promise + +external fun unlink(user: User, providerId: String): Promise + +external fun updateCurrentUser(auth: Auth, user: User?): Promise + +external fun updateEmail(user: User, newEmail: String): Promise + +external fun updatePassword(user: User, newPassword: String): Promise + +external fun updatePhoneNumber(user: User, phoneCredential: AuthCredential): Promise + +external fun updateProfile(user: User, profile: Json): Promise + +external fun verifyBeforeUpdateEmail( + user: User, + newEmail: String, + actionCodeSettings: Any? +): Promise + +external fun verifyPasswordResetCode(auth: Auth, code: String): Promise + +external interface Auth { + val currentUser: User? + var languageCode: String? + + fun onAuthStateChanged(nextOrObserver: (User?) -> Unit): Unsubscribe + fun onIdTokenChanged(nextOrObserver: (User?) -> Unit): Unsubscribe + fun signOut(): Promise + fun updateCurrentUser(user: User?): Promise +} + +external interface UserInfo { + val displayName: String? + val email: String? + val phoneNumber: String? + val photoURL: String? + val providerId: String + val uid: String +} + +external interface User : UserInfo { + val emailVerified: Boolean + val isAnonymous: Boolean + val metadata: UserMetadata + val providerData: Array + val refreshToken: String + val tenantId: String? + + fun delete(): Promise + fun getIdToken(forceRefresh: Boolean?): Promise + fun getIdTokenResult(forceRefresh: Boolean?): Promise + fun reload(): Promise +} + +external interface UserMetadata { + val creationTime: String? + val lastSignInTime: String? +} + +external interface IdTokenResult { + val authTime: String + val claims: Json + val expirationTime: String + val issuedAtTime: String + val signInProvider: String? + val signInSecondFactor: String? + val token: String +} + +external interface ActionCodeInfo { + val operation: String + val data: ActionCodeData +} + +external interface ActionCodeData { + val email: String? + val multiFactorInfo: MultiFactorInfo? + val previousEmail: String? +} + +external interface AuthResult { + val credential: AuthCredential? + val operationType: String? + val user: User? +} + +external interface AuthCredential { + val providerId: String + val signInMethod: String +} + +external interface OAuthCredential : AuthCredential { + val accessToken: String? + val idToken: String? + val secret: String? +} + +external interface UserCredential { + val operationType: String + val providerId: String? + val user: User +} + +external interface ProfileUpdateRequest { + val displayName: String? + val photoURL: String? +} + +external interface MultiFactorUser { + val enrolledFactors: Array + + fun enroll(assertion: MultiFactorAssertion, displayName: String?): Promise + fun getSession(): Promise + fun unenroll(option: MultiFactorInfo): Promise + fun unenroll(option: String): Promise +} + +external interface MultiFactorInfo { + val displayName: String? + val enrollmentTime: String + val factorId: String + val uid: String +} + +external interface MultiFactorAssertion { + val factorId: String +} + +external interface MultiFactorSession + +external interface MultiFactorResolver { + val auth: Auth + val hints: Array + val session: MultiFactorSession + + fun resolveSignIn(assertion: MultiFactorAssertion): Promise +} + +external interface AuthProvider + +external interface AuthError + +external object EmailAuthProvider : AuthProvider { + fun credential(email: String, password: String): AuthCredential + fun credentialWithLink(email: String, emailLink: String): AuthCredential +} + +external object FacebookAuthProvider : AuthProvider { + fun credential(token: String): AuthCredential +} + +external object GithubAuthProvider : AuthProvider { + fun credential(token: String): AuthCredential +} + +external class GoogleAuthProvider : AuthProvider { + fun addScope(scope: String) + companion object { + fun credential(idToken: String?, accessToken: String?): AuthCredential + fun credentialFromResult(userCredential: UserCredential): OAuthCredential? + fun credentialFromError(error: AuthError): OAuthCredential? + } +} + +external class OAuthProvider(providerId: String) : AuthProvider { + val providerId: String + fun credential(optionsOrIdToken: Any?, accessToken: String?): AuthCredential + + fun addScope(scope: String) + fun setCustomParameters(customOAuthParameters: Map) +} + +external interface OAuthCredentialOptions { + val accessToken: String? + val idToken: String? + val rawNonce: String? +} + +external class PhoneAuthProvider(auth: Auth?) : AuthProvider { + companion object { + fun credential( + verificationId: String, + verificationCode: String + ): AuthCredential + } + + fun verifyPhoneNumber( + phoneInfoOptions: String, + applicationVerifier: ApplicationVerifier + ): Promise +} + +external interface ApplicationVerifier { + val type: String + fun verify(): Promise +} + +external object TwitterAuthProvider : AuthProvider { + fun credential(token: String, secret: String): AuthCredential +} + +external interface Persistence { + val type: String +} + +external val browserLocalPersistence: Persistence +external val browserSessionPersistence: Persistence +external val indexedDBLocalPersistence: Persistence +external val inMemoryPersistence: Persistence + +external fun setPersistence(auth: Auth, persistence: Persistence): Promise; diff --git a/firebase-auth/src/wasmJsMain/kotlin/auth/multifactor.kt b/firebase-auth/src/wasmJsMain/kotlin/auth/multifactor.kt new file mode 100644 index 000000000..d4ad636a7 --- /dev/null +++ b/firebase-auth/src/wasmJsMain/kotlin/auth/multifactor.kt @@ -0,0 +1,48 @@ +package dev.gitlive.firebase.auth + +import dev.gitlive.firebase.auth.externals.MultiFactorUser +import kotlinx.coroutines.await +import kotlin.js.Date +import dev.gitlive.firebase.auth.externals.MultiFactorAssertion as JsMultiFactorAssertion +import dev.gitlive.firebase.auth.externals.MultiFactorInfo as JsMultiFactorInfo +import dev.gitlive.firebase.auth.externals.MultiFactorResolver as JsMultiFactorResolver +import dev.gitlive.firebase.auth.externals.MultiFactorSession as JsMultiFactorSession + +actual class MultiFactor(val js: MultiFactorUser) { + actual val enrolledFactors: List + get() = rethrow { js.enrolledFactors.map { MultiFactorInfo(it) } } + actual suspend fun enroll(multiFactorAssertion: MultiFactorAssertion, displayName: String?) = + rethrow { js.enroll(multiFactorAssertion.js, displayName).await() } + actual suspend fun getSession(): MultiFactorSession = + rethrow { MultiFactorSession(js.getSession().await()) } + actual suspend fun unenroll(multiFactorInfo: MultiFactorInfo) = + rethrow { js.unenroll(multiFactorInfo.js).await() } + actual suspend fun unenroll(factorUid: String) = + rethrow { js.unenroll(factorUid).await() } +} + +actual class MultiFactorInfo(val js: JsMultiFactorInfo) { + actual val displayName: String? + get() = rethrow { js.displayName } + actual val enrollmentTime: Double + get() = rethrow { (Date(js.enrollmentTime).getTime() / 1000.0) } + actual val factorId: String + get() = rethrow { js.factorId } + actual val uid: String + get() = rethrow { js.uid } +} + +actual class MultiFactorAssertion(val js: JsMultiFactorAssertion) { + actual val factorId: String + get() = rethrow { js.factorId } +} + +actual class MultiFactorSession(val js: JsMultiFactorSession) + +actual class MultiFactorResolver(val js: JsMultiFactorResolver) { + actual val auth: FirebaseAuth = rethrow { FirebaseAuth(js.auth) } + actual val hints: List = rethrow { js.hints.map { MultiFactorInfo(it) } } + actual val session: MultiFactorSession = rethrow { MultiFactorSession(js.session) } + + actual suspend fun resolveSignIn(assertion: MultiFactorAssertion): AuthResult = rethrow { AuthResult(js.resolveSignIn(assertion.js).await()) } +} diff --git a/firebase-auth/src/wasmJsMain/kotlin/auth/user.kt b/firebase-auth/src/wasmJsMain/kotlin/auth/user.kt new file mode 100644 index 000000000..d8b817a7e --- /dev/null +++ b/firebase-auth/src/wasmJsMain/kotlin/auth/user.kt @@ -0,0 +1,78 @@ +package dev.gitlive.firebase.auth + +import dev.gitlive.firebase.auth.externals.* +import kotlinx.coroutines.await +import kotlin.js.Date +import dev.gitlive.firebase.auth.externals.UserInfo as JsUserInfo +import kotlin.js.json + +actual class FirebaseUser internal constructor(val js: User) { + actual val uid: String + get() = rethrow { js.uid } + actual val displayName: String? + get() = rethrow { js.displayName } + actual val email: String? + get() = rethrow { js.email } + actual val phoneNumber: String? + get() = rethrow { js.phoneNumber } + actual val photoURL: String? + get() = rethrow { js.photoURL } + actual val isAnonymous: Boolean + get() = rethrow { js.isAnonymous } + actual val isEmailVerified: Boolean + get() = rethrow { js.emailVerified } + actual val metaData: UserMetaData? + get() = rethrow { UserMetaData(js.metadata) } + actual val multiFactor: MultiFactor + get() = rethrow { MultiFactor(multiFactor(js)) } + actual val providerData: List + get() = rethrow { js.providerData.map { UserInfo(it) } } + actual val providerId: String + get() = rethrow { js.providerId } + actual suspend fun delete() = rethrow { js.delete().await() } + actual suspend fun reload() = rethrow { js.reload().await() } + actual suspend fun getIdToken(forceRefresh: Boolean): String? = rethrow { js.getIdToken(forceRefresh).await() } + actual suspend fun getIdTokenResult(forceRefresh: Boolean): AuthTokenResult = rethrow { AuthTokenResult(getIdTokenResult(js, forceRefresh).await()) } + actual suspend fun linkWithCredential(credential: AuthCredential): AuthResult = rethrow { AuthResult( linkWithCredential(js, credential.js).await()) } + actual suspend fun reauthenticate(credential: AuthCredential) = rethrow { + reauthenticateWithCredential(js, credential.js).await() + Unit + } + actual suspend fun reauthenticateAndRetrieveData(credential: AuthCredential): AuthResult = rethrow { AuthResult(reauthenticateWithCredential(js, credential.js).await()) } + + actual suspend fun sendEmailVerification(actionCodeSettings: ActionCodeSettings?) = rethrow { sendEmailVerification(js, actionCodeSettings?.toJson()).await() } + actual suspend fun unlink(provider: String): FirebaseUser? = rethrow { FirebaseUser(unlink(js, provider).await()) } + actual suspend fun updateEmail(email: String) = rethrow { updateEmail(js, email).await() } + actual suspend fun updatePassword(password: String) = rethrow { updatePassword(js, password).await() } + actual suspend fun updatePhoneNumber(credential: PhoneAuthCredential) = rethrow { updatePhoneNumber(js, credential.js).await() } + actual suspend fun updateProfile(displayName: String?, photoUrl: String?): Unit = rethrow { + val request = listOfNotNull( + displayName.takeUnless { it === UNCHANGED }?.let { "displayName" to it }, + photoUrl.takeUnless { it === UNCHANGED }?.let { "photoURL" to it } + ) + updateProfile(js, json(*request.toTypedArray())).await() + } + actual suspend fun verifyBeforeUpdateEmail(newEmail: String, actionCodeSettings: ActionCodeSettings?) = rethrow { verifyBeforeUpdateEmail(js, newEmail, actionCodeSettings?.toJson()).await() } +} + +actual class UserInfo(val js: JsUserInfo) { + actual val displayName: String? + get() = rethrow { js.displayName } + actual val email: String? + get() = rethrow { js.email } + actual val phoneNumber: String? + get() = rethrow { js.phoneNumber } + actual val photoURL: String? + get() = rethrow { js.photoURL } + actual val providerId: String + get() = rethrow { js.providerId } + actual val uid: String + get() = rethrow { js.uid } +} + +actual class UserMetaData(val js: UserMetadata) { + actual val creationTime: Double? + get() = rethrow {js.creationTime?.let { (Date(it).getTime() / 1000.0) } } + actual val lastSignInTime: Double? + get() = rethrow {js.lastSignInTime?.let { (Date(it).getTime() / 1000.0) } } +} diff --git a/firebase-common-internal/src/wasmJsMain/kotlin/internal/EncodedObject.kt b/firebase-common-internal/src/wasmJsMain/kotlin/internal/EncodedObject.kt new file mode 100644 index 000000000..47f3aad5e --- /dev/null +++ b/firebase-common-internal/src/wasmJsMain/kotlin/internal/EncodedObject.kt @@ -0,0 +1,32 @@ +package dev.gitlive.firebase.internal + +import kotlin.js.Json +import kotlin.js.json + +val EncodedObject.js: Json get() = json(*getRaw().entries.map { (key, value) -> key to value }.toTypedArray()) + +@PublishedApi +internal actual fun Any.asNativeMap(): Map<*, *>? { + val json = when (this) { + is Number -> null + is Boolean -> null + is String -> null + is Map<*, *> -> { + if (keys.all { it is String }) { + this as Json + } else { + null + } + } + is Collection<*> -> null + is Array<*> -> null + else -> { + this as Json + } + } ?: return null + val mutableMap = mutableMapOf() + for (key in js("Object").keys(json)) { + mutableMap[key] = json[key] + } + return mutableMap.toMap() +} diff --git a/firebase-common-internal/src/wasmJsMain/kotlin/internal/_decoders.kt b/firebase-common-internal/src/wasmJsMain/kotlin/internal/_decoders.kt new file mode 100644 index 000000000..167fc7f8e --- /dev/null +++ b/firebase-common-internal/src/wasmJsMain/kotlin/internal/_decoders.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.gitlive.firebase.internal + +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.encoding.CompositeDecoder +import kotlin.js.Json + +actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, polymorphicIsNested: Boolean): CompositeDecoder = when (descriptor.kind) { + StructureKind.CLASS, StructureKind.OBJECT -> decodeAsMap(false) + StructureKind.LIST -> decodeAsList() + StructureKind.MAP -> (js("Object").entries(value) as Array>).let { + FirebaseCompositeDecoder( + it.size, + settings + ) { desc, index -> it[index / 2].run { if (index % 2 == 0) { + val key = get(0) as String + if (desc.getElementDescriptor(index).kind == PrimitiveKind.STRING) { + key + } else { + JSON.parse(key) + } + } else get(1) } } + } + + is PolymorphicKind -> decodeAsMap(polymorphicIsNested) + else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet") +} + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +actual fun getPolymorphicType(value: Any?, discriminator: String): String = + (value as Json)[discriminator] as String + +private fun FirebaseDecoder.decodeAsList(): CompositeDecoder = (value as Array<*>).let { + FirebaseCompositeDecoder(it.size, settings) { _, index -> it[index] } +} +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +private fun FirebaseDecoder.decodeAsMap(isNestedPolymorphic: Boolean): CompositeDecoder = (value as Json).let { json -> + FirebaseClassDecoder(js("Object").keys(value).length as Int, settings, { json[it] != undefined }) { desc, index -> + if (isNestedPolymorphic) { + if (desc.getElementName(index) == "value") { + json + } else { + json[desc.getElementName(index)] + } + } else { + json[desc.getElementName(index)] + } + } +} \ No newline at end of file diff --git a/firebase-common-internal/src/wasmJsMain/kotlin/internal/_encoders.kt b/firebase-common-internal/src/wasmJsMain/kotlin/internal/_encoders.kt new file mode 100644 index 000000000..5cf22b6bd --- /dev/null +++ b/firebase-common-internal/src/wasmJsMain/kotlin/internal/_encoders.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.gitlive.firebase.internal + +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlin.js.json + +actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) { + StructureKind.LIST -> encodeAsList(descriptor) + StructureKind.MAP -> { + val map = json() + var lastKey = "" + value = map + FirebaseCompositeEncoder(settings) { _, index, value -> + if (index % 2 == 0) { + lastKey = (value as? String) ?: JSON.stringify(value) + } else { + map[lastKey] = value + } + } + } + StructureKind.CLASS, StructureKind.OBJECT -> encodeAsMap(descriptor) + is PolymorphicKind -> encodeAsMap(descriptor) + else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet") +} + +private fun FirebaseEncoder.encodeAsList(descriptor: SerialDescriptor): FirebaseCompositeEncoder = Array(descriptor.elementsCount) { null } + .also { value = it } + .let { FirebaseCompositeEncoder(settings) { _, index, value -> it[index] = value } } +private fun FirebaseEncoder.encodeAsMap(descriptor: SerialDescriptor): FirebaseCompositeEncoder = json() + .also { value = it } + .let { + FirebaseCompositeEncoder( + settings, + setPolymorphicType = { discriminator, type -> + it[discriminator] = type + }, + set = { _, index, value -> it[descriptor.getElementName(index)] = value } + ) + } diff --git a/firebase-common/src/wasmJsMain/kotlin/firebase/Unsubscribe.kt b/firebase-common/src/wasmJsMain/kotlin/firebase/Unsubscribe.kt new file mode 100644 index 000000000..087d4f86b --- /dev/null +++ b/firebase-common/src/wasmJsMain/kotlin/firebase/Unsubscribe.kt @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.gitlive.firebase + +typealias Unsubscribe = () -> Unit diff --git a/firebase-config/src/wasmJsMain/kotlin/remoteconfig/FirebaseRemoteConfig.kt b/firebase-config/src/wasmJsMain/kotlin/remoteconfig/FirebaseRemoteConfig.kt new file mode 100644 index 000000000..bba268c12 --- /dev/null +++ b/firebase-config/src/wasmJsMain/kotlin/remoteconfig/FirebaseRemoteConfig.kt @@ -0,0 +1,118 @@ +package dev.gitlive.firebase.remoteconfig + +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseApp +import dev.gitlive.firebase.FirebaseException +import dev.gitlive.firebase.remoteconfig.externals.* +import kotlinx.coroutines.await +import kotlin.js.json + +actual val Firebase.remoteConfig: FirebaseRemoteConfig + get() = rethrow { FirebaseRemoteConfig(getRemoteConfig()) } + +actual fun Firebase.remoteConfig(app: FirebaseApp): FirebaseRemoteConfig = rethrow { + FirebaseRemoteConfig(getRemoteConfig(app.js)) +} + +actual class FirebaseRemoteConfig internal constructor(val js: RemoteConfig) { + actual val all: Map + get() = rethrow { getAllKeys().map { Pair(it, getValue(it)) }.toMap() } + + actual val info: FirebaseRemoteConfigInfo + get() = rethrow { + FirebaseRemoteConfigInfo( + configSettings = js.settings.toFirebaseRemoteConfigSettings(), + fetchTimeMillis = js.fetchTimeMillis, + lastFetchStatus = js.lastFetchStatus.toFetchStatus() + ) + } + + actual suspend fun activate(): Boolean = rethrow { activate(js).await() } + actual suspend fun ensureInitialized(): Unit = rethrow { ensureInitialized(js).await() } + + actual suspend fun fetch(minimumFetchIntervalInSeconds: Long?): Unit = + rethrow { fetchConfig(js).await() } + + actual suspend fun fetchAndActivate(): Boolean = rethrow { fetchAndActivate(js).await() } + + actual fun getValue(key: String): FirebaseRemoteConfigValue = rethrow { + FirebaseRemoteConfigValue(getValue(js, key)) + } + + actual fun getKeysByPrefix(prefix: String): Set { + return getAllKeys().filter { it.startsWith(prefix) }.toSet() + } + + private fun getAllKeys(): Set { + val objectKeys = js("Object.keys") + return objectKeys(getAll(js)).unsafeCast>().toSet() + } + + actual suspend fun reset() { + // not implemented for JS target + } + + actual suspend fun settings(init: FirebaseRemoteConfigSettings.() -> Unit) { + val settings = FirebaseRemoteConfigSettings().apply(init) + js.settings.apply { + fetchTimeoutMillis = settings.fetchTimeoutInSeconds * 1000 + minimumFetchIntervalMillis = settings.minimumFetchIntervalInSeconds * 1000 + } + } + + actual suspend fun setDefaults(vararg defaults: Pair) = rethrow { + js.defaultConfig = json(*defaults) + } + + private fun Settings.toFirebaseRemoteConfigSettings(): FirebaseRemoteConfigSettings { + return FirebaseRemoteConfigSettings( + fetchTimeoutInSeconds = fetchTimeoutMillis.toLong() / 1000, + minimumFetchIntervalInSeconds = minimumFetchIntervalMillis.toLong() / 1000 + ) + } + + private fun String.toFetchStatus(): FetchStatus { + return when (this) { + "no-fetch-yet" -> FetchStatus.NoFetchYet + "success" -> FetchStatus.Success + "failure" -> FetchStatus.Failure + "throttle" -> FetchStatus.Throttled + else -> error("Unknown FetchStatus: $this") + } + } +} + +actual open class FirebaseRemoteConfigException(code: String, cause: Throwable) : + FirebaseException(code, cause) + +actual class FirebaseRemoteConfigClientException(code: String, cause: Throwable) : + FirebaseRemoteConfigException(code, cause) + +actual class FirebaseRemoteConfigFetchThrottledException(code: String, cause: Throwable) : + FirebaseRemoteConfigException(code, cause) + +actual class FirebaseRemoteConfigServerException(code: String, cause: Throwable) : + FirebaseRemoteConfigException(code, cause) + + +internal inline fun rethrow(function: () -> R): R { + try { + return function() + } catch (e: Exception) { + throw e + } catch (e: dynamic) { + throw errorToException(e) + } +} + +internal fun errorToException(error: dynamic) = (error?.code ?: error?.message ?: "") + .toString() + .lowercase() + .let { code -> + when { + else -> { + println("Unknown error code in ${JSON.stringify(error)}") + FirebaseRemoteConfigException(code, error) + } + } + } diff --git a/firebase-config/src/wasmJsMain/kotlin/remoteconfig/FirebaseRemoteConfigValue.kt b/firebase-config/src/wasmJsMain/kotlin/remoteconfig/FirebaseRemoteConfigValue.kt new file mode 100644 index 000000000..2891f926a --- /dev/null +++ b/firebase-config/src/wasmJsMain/kotlin/remoteconfig/FirebaseRemoteConfigValue.kt @@ -0,0 +1,19 @@ +package dev.gitlive.firebase.remoteconfig + +import dev.gitlive.firebase.remoteconfig.externals.Value + +actual class FirebaseRemoteConfigValue(val js: Value) { + actual fun asBoolean(): Boolean = rethrow { js.asBoolean() } + actual fun asByteArray(): ByteArray = rethrow { js.asString()?.encodeToByteArray() ?: byteArrayOf() } + actual fun asDouble(): Double = rethrow { js.asNumber().toDouble() } + actual fun asLong(): Long = rethrow { js.asNumber().toLong() } + actual fun asString(): String = rethrow { js.asString() ?: "" } + actual fun getSource(): ValueSource = rethrow { js.getSource().toSource() } + + private fun String.toSource() = when (this) { + "default" -> ValueSource.Default + "remote" -> ValueSource.Remote + "static" -> ValueSource.Static + else -> error("Unknown ValueSource: $this") + } +} diff --git a/firebase-config/src/wasmJsMain/kotlin/remoteconfig/externals/remoteconfig.kt b/firebase-config/src/wasmJsMain/kotlin/remoteconfig/externals/remoteconfig.kt new file mode 100644 index 000000000..b2a42dc84 --- /dev/null +++ b/firebase-config/src/wasmJsMain/kotlin/remoteconfig/externals/remoteconfig.kt @@ -0,0 +1,47 @@ +@file:JsModule("firebase/remote-config") +@file:JsNonModule + +package dev.gitlive.firebase.remoteconfig.externals + +import dev.gitlive.firebase.externals.FirebaseApp +import kotlin.js.Json +import kotlin.js.Promise + +external fun activate(remoteConfig: RemoteConfig): Promise + +external fun ensureInitialized(remoteConfig: RemoteConfig): Promise + +external fun fetchAndActivate(remoteConfig: RemoteConfig): Promise + +external fun fetchConfig(remoteConfig: RemoteConfig): Promise + +external fun getAll(remoteConfig: RemoteConfig): Json + +external fun getBoolean(remoteConfig: RemoteConfig, key: String): Boolean + +external fun getNumber(remoteConfig: RemoteConfig, key: String): Number + +external fun getRemoteConfig(app: FirebaseApp? = definedExternally): RemoteConfig + +external fun getString(remoteConfig: RemoteConfig, key: String): String? + +external fun getValue(remoteConfig: RemoteConfig, key: String): Value + +external interface RemoteConfig { + var defaultConfig: Any + var fetchTimeMillis: Long + var lastFetchStatus: String + val settings: Settings +} + +external interface Settings { + var fetchTimeoutMillis: Number + var minimumFetchIntervalMillis: Number +} + +external interface Value { + fun asBoolean(): Boolean + fun asNumber(): Number + fun asString(): String? + fun getSource(): String +} diff --git a/firebase-database/src/wasmJsMain/kotlin/database/ServerValue.kt b/firebase-database/src/wasmJsMain/kotlin/database/ServerValue.kt new file mode 100644 index 000000000..cfb14b69b --- /dev/null +++ b/firebase-database/src/wasmJsMain/kotlin/database/ServerValue.kt @@ -0,0 +1,21 @@ +package dev.gitlive.firebase.database + +import dev.gitlive.firebase.database.externals.serverTimestamp +import kotlinx.serialization.Serializable +import dev.gitlive.firebase.database.externals.increment as jsIncrement + +/** Represents a Firebase ServerValue. */ +@Serializable(with = ServerValueSerializer::class) +actual class ServerValue internal actual constructor( + internal actual val nativeValue: Any +){ + actual companion object { + actual val TIMESTAMP: ServerValue get() = ServerValue(serverTimestamp()) + actual fun increment(delta: Double): ServerValue = ServerValue(jsIncrement(delta)) + } + + override fun equals(other: Any?): Boolean = + this === other || other is ServerValue && nativeValue == other.nativeValue + override fun hashCode(): Int = nativeValue.hashCode() + override fun toString(): String = "ServerValue($nativeValue)" +} diff --git a/firebase-database/src/wasmJsMain/kotlin/database/database.kt b/firebase-database/src/wasmJsMain/kotlin/database/database.kt new file mode 100644 index 000000000..7dfc71a81 --- /dev/null +++ b/firebase-database/src/wasmJsMain/kotlin/database/database.kt @@ -0,0 +1,277 @@ +/* + * 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.DecodeSettings +import dev.gitlive.firebase.EncodeDecodeSettingsBuilder +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseApp +import dev.gitlive.firebase.database.externals.CancelCallback +import dev.gitlive.firebase.database.externals.ChangeSnapshotCallback +import dev.gitlive.firebase.database.externals.Database +import dev.gitlive.firebase.database.externals.child +import dev.gitlive.firebase.database.externals.connectDatabaseEmulator +import dev.gitlive.firebase.database.externals.enableLogging +import dev.gitlive.firebase.database.externals.getDatabase +import dev.gitlive.firebase.database.externals.onChildAdded +import dev.gitlive.firebase.database.externals.onChildChanged +import dev.gitlive.firebase.database.externals.onChildMoved +import dev.gitlive.firebase.database.externals.onChildRemoved +import dev.gitlive.firebase.database.externals.onDisconnect +import dev.gitlive.firebase.database.externals.onValue +import dev.gitlive.firebase.database.externals.push +import dev.gitlive.firebase.database.externals.query +import dev.gitlive.firebase.database.externals.ref +import dev.gitlive.firebase.database.externals.remove +import dev.gitlive.firebase.database.externals.set +import dev.gitlive.firebase.database.externals.update +import dev.gitlive.firebase.internal.EncodedObject +import dev.gitlive.firebase.internal.decode +import dev.gitlive.firebase.internal.js +import dev.gitlive.firebase.internal.reencodeTransformation +import kotlinx.coroutines.asDeferred +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.produceIn +import kotlinx.coroutines.selects.select +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.KSerializer +import kotlin.js.Promise +import kotlin.js.json +import dev.gitlive.firebase.database.externals.DataSnapshot as JsDataSnapshot +import dev.gitlive.firebase.database.externals.DatabaseReference as JsDatabaseReference +import dev.gitlive.firebase.database.externals.OnDisconnect as JsOnDisconnect +import dev.gitlive.firebase.database.externals.Query as JsQuery +import dev.gitlive.firebase.database.externals.endAt as jsEndAt +import dev.gitlive.firebase.database.externals.equalTo as jsEqualTo +import dev.gitlive.firebase.database.externals.goOffline as jsGoOffline +import dev.gitlive.firebase.database.externals.goOnline as jsGoOnline +import dev.gitlive.firebase.database.externals.limitToFirst as jsLimitToFirst +import dev.gitlive.firebase.database.externals.limitToLast as jsLimitToLast +import dev.gitlive.firebase.database.externals.orderByChild as jsOrderByChild +import dev.gitlive.firebase.database.externals.orderByKey as jsOrderByKey +import dev.gitlive.firebase.database.externals.orderByValue as jsOrderByValue +import dev.gitlive.firebase.database.externals.runTransaction as jsRunTransaction +import dev.gitlive.firebase.database.externals.startAt as jsStartAt + +actual val Firebase.database + get() = rethrow { FirebaseDatabase(getDatabase()) } + +actual fun Firebase.database(app: FirebaseApp) = + rethrow { FirebaseDatabase(getDatabase(app = app.js)) } + +actual fun Firebase.database(url: String) = + rethrow { FirebaseDatabase(getDatabase(url = url)) } + +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 fun reference(path: String) = rethrow { DatabaseReference(NativeDatabaseReference(ref(js, path), js)) } + actual fun reference() = rethrow { DatabaseReference(NativeDatabaseReference(ref(js), js)) } + actual fun setPersistenceEnabled(enabled: Boolean) {} + actual fun setPersistenceCacheSizeBytes(cacheSizeInBytes: Long) {} + actual fun setLoggingEnabled(enabled: Boolean) = rethrow { enableLogging(enabled) } + actual fun useEmulator(host: String, port: Int) = rethrow { connectDatabaseEmulator(js, host, port) } + + actual fun goOffline() = rethrow { jsGoOffline(js) } + + actual fun goOnline() = rethrow { jsGoOnline(js) } +} + +internal actual open class NativeQuery( + open val js: JsQuery, + val database: Database +) + +actual open class Query internal actual constructor( + nativeQuery: NativeQuery +) { + + internal constructor(js: JsQuery, database: Database) : this(NativeQuery(js, database)) + + open val js: JsQuery = nativeQuery.js + val database: Database = nativeQuery.database + + actual fun orderByKey() = Query(query(js, jsOrderByKey()), database) + actual fun orderByValue() = Query(query(js, jsOrderByValue()), database) + actual fun orderByChild(path: String) = Query(query(js, jsOrderByChild(path)), database) + + actual val valueEvents + get() = callbackFlow { + val unsubscribe = rethrow { + onValue( + query = js, + callback = { trySend(DataSnapshot(it, database)) }, + cancelCallback = { close(DatabaseException(it)).run { } } + ) + } + awaitClose { rethrow { unsubscribe() } } + } + + actual fun childEvents(vararg types: ChildEvent.Type) = callbackFlow { + val unsubscribes = rethrow { + types.map { type -> + val callback: ChangeSnapshotCallback = { snapshot, previousChildName -> + trySend( + ChildEvent( + DataSnapshot(snapshot, database), + type, + previousChildName + ) + ) + } + + val cancelCallback: CancelCallback = { + close(DatabaseException(it)).run { } + } + + when (type) { + ChildEvent.Type.ADDED -> onChildAdded(js, callback, cancelCallback) + ChildEvent.Type.CHANGED -> onChildChanged(js, callback, cancelCallback) + ChildEvent.Type.MOVED -> onChildMoved(js, callback, cancelCallback) + ChildEvent.Type.REMOVED -> onChildRemoved(js, callback, cancelCallback) + } + } + } + awaitClose { rethrow { unsubscribes.forEach { it.invoke() } } } + } + + actual fun startAt(value: String, key: String?) = Query(query(js, jsStartAt(value, key ?: undefined)), database) + + actual fun startAt(value: Double, key: String?) = Query(query(js, jsStartAt(value, key ?: undefined)), database) + + actual fun startAt(value: Boolean, key: String?) = Query(query(js, jsStartAt(value, key ?: undefined)), database) + + actual fun endAt(value: String, key: String?) = Query(query(js, jsEndAt(value, key ?: undefined)), database) + + actual fun endAt(value: Double, key: String?) = Query(query(js, jsEndAt(value, key ?: undefined)), database) + + actual fun endAt(value: Boolean, key: String?) = Query(query(js, jsEndAt(value, key ?: undefined)), database) + + actual fun limitToFirst(limit: Int) = Query(query(js, jsLimitToFirst(limit)), database) + + actual fun limitToLast(limit: Int) = Query(query(js, jsLimitToLast(limit)), database) + + actual fun equalTo(value: String, key: String?) = Query(query(js, jsEqualTo(value, key ?: undefined)), database) + + actual fun equalTo(value: Double, key: String?) = Query(query(js, jsEqualTo(value, key ?: undefined)), database) + + actual fun equalTo(value: Boolean, key: String?) = Query(query(js, jsEqualTo(value, key ?: undefined)), database) + + override fun toString() = js.toString() +} + +@PublishedApi +internal actual class NativeDatabaseReference internal constructor( + override val js: JsDatabaseReference, + database: Database +) : NativeQuery(js, database) { + + actual val key get() = rethrow { js.key } + actual fun push() = rethrow { NativeDatabaseReference(push(js), database) } + actual fun child(path: String) = rethrow { NativeDatabaseReference(child(js, path), database) } + + actual fun onDisconnect() = rethrow { NativeOnDisconnect(onDisconnect(js), database) } + + actual suspend fun removeValue() = rethrow { remove(js).awaitWhileOnline(database) } + + actual suspend fun setValueEncoded(encodedValue: Any?) = rethrow { + set(js, encodedValue).awaitWhileOnline(database) + } + + actual suspend fun updateEncodedChildren(encodedUpdate: EncodedObject) = + rethrow { update(js, encodedUpdate.js).awaitWhileOnline(database) } + + + actual suspend fun runTransaction(strategy: KSerializer, buildSettings: EncodeDecodeSettingsBuilder.() -> Unit, transactionUpdate: (currentData: T) -> T): DataSnapshot { + return DataSnapshot(jsRunTransaction(js, transactionUpdate = { currentData -> + reencodeTransformation(strategy, currentData ?: json(), buildSettings, transactionUpdate) + }).awaitWhileOnline(database).snapshot, database) + } +} + +actual class DataSnapshot internal constructor( + val js: JsDataSnapshot, + val database: Database +) { + actual val value get(): Any? { + check(!hasChildren) { "DataSnapshot.value can only be used for primitive values (snapshots without children)" } + return js.`val`() + } + + actual inline fun value() = + rethrow { decode(value = js.`val`()) } + + actual inline fun value(strategy: DeserializationStrategy, buildSettings: DecodeSettings.Builder.() -> Unit) = + rethrow { decode(strategy, js.`val`(), buildSettings) } + + actual val exists get() = rethrow { js.exists() } + actual val key get() = rethrow { js.key } + actual fun child(path: String) = DataSnapshot(js.child(path), database) + actual val hasChildren get() = js.hasChildren() + actual val children: Iterable = rethrow { + ArrayList(js.size).also { + js.forEach { snapshot -> it.add(DataSnapshot(snapshot, database)); false /* don't cancel enumeration */ } + } + } + actual val ref: DatabaseReference + get() = DatabaseReference(NativeDatabaseReference(js.ref, database)) + +} + +@PublishedApi +internal actual class NativeOnDisconnect internal constructor( + val js: JsOnDisconnect, + val database: Database +) { + + actual suspend fun removeValue() = rethrow { js.remove().awaitWhileOnline(database) } + actual suspend fun cancel() = rethrow { js.cancel().awaitWhileOnline(database) } + + actual suspend fun setValue(encodedValue: Any?) = + rethrow { js.set(encodedValue).awaitWhileOnline(database) } + + actual suspend fun updateEncodedChildren(encodedUpdate: EncodedObject) = + rethrow { js.update(encodedUpdate.js).awaitWhileOnline(database) } + +} + +val OnDisconnect.js get() = native.js +val OnDisconnect.database get() = native.database + +actual class DatabaseException actual constructor(message: String?, cause: Throwable?) : RuntimeException(message, cause) { + constructor(error: dynamic) : this("${error.code ?: "UNKNOWN"}: ${error.message}", error.unsafeCast()) +} + +inline fun T.rethrow(function: T.() -> R): R = dev.gitlive.firebase.database.rethrow { function() } + +inline fun rethrow(function: () -> R): R { + try { + return function() + } catch (e: Exception) { + throw e + } catch (e: dynamic) { + throw DatabaseException(e) + } +} + +suspend fun Promise.awaitWhileOnline(database: Database): T = coroutineScope { + + val notConnected = FirebaseDatabase(database) + .reference(".info/connected") + .valueEvents + .filter { !it.value() } + .produceIn(this) + + select { + this@awaitWhileOnline.asDeferred().onAwait { it.also { notConnected.cancel() } } + notConnected.onReceive { throw DatabaseException("Database not connected", null) } + } + +} diff --git a/firebase-database/src/wasmJsMain/kotlin/database/externals/callbacks.kt b/firebase-database/src/wasmJsMain/kotlin/database/externals/callbacks.kt new file mode 100644 index 000000000..86a592432 --- /dev/null +++ b/firebase-database/src/wasmJsMain/kotlin/database/externals/callbacks.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.gitlive.firebase.database.externals + +typealias ChangeSnapshotCallback = (data: DataSnapshot, previousChildName: String?) -> Unit +typealias ValueSnapshotCallback = (data: DataSnapshot) -> Unit +typealias CancelCallback = (error: Throwable) -> Unit diff --git a/firebase-database/src/wasmJsMain/kotlin/database/externals/database.kt b/firebase-database/src/wasmJsMain/kotlin/database/externals/database.kt new file mode 100644 index 000000000..b9390b21b --- /dev/null +++ b/firebase-database/src/wasmJsMain/kotlin/database/externals/database.kt @@ -0,0 +1,147 @@ +@file:JsModule("firebase/database") +@file:JsNonModule + +package dev.gitlive.firebase.database.externals + +import dev.gitlive.firebase.Unsubscribe +import dev.gitlive.firebase.externals.FirebaseApp +import kotlin.js.Promise + +external fun child(parent: DatabaseReference, path: String): DatabaseReference + +external fun connectDatabaseEmulator( + db: Database, + host: String, + port: Int, + options: Any? = definedExternally +) + +external fun enableLogging(enabled: Boolean?, persistent: Boolean? = definedExternally) + +external fun endAt(value: Any?, key: String? = definedExternally): QueryConstraint + +external fun endBefore(value: Any?, key: String? = definedExternally): QueryConstraint + +external fun equalTo(value: Any?, key: String? = definedExternally): QueryConstraint + +external fun get(query: Query): Promise + +external fun getDatabase( + app: FirebaseApp? = definedExternally, + url: String? = definedExternally +): Database + +external fun increment(delta: Double): Any + +external fun limitToFirst(limit: Int): QueryConstraint + +external fun limitToLast(limit: Int): QueryConstraint + +external fun off(query: Query, eventType: String?, callback: Any?) + +external fun goOffline(db: Database) + +external fun goOnline(db: Database) + +external fun onChildAdded( + query: Query, + callback: ChangeSnapshotCallback, + cancelCallback: CancelCallback? = definedExternally, +): Unsubscribe + +external fun onChildChanged( + query: Query, + callback: ChangeSnapshotCallback, + cancelCallback: CancelCallback? = definedExternally, +): Unsubscribe + +external fun onChildMoved( + query: Query, + callback: ChangeSnapshotCallback, + cancelCallback: CancelCallback? = definedExternally, +): Unsubscribe + +external fun onChildRemoved( + query: Query, + callback: ChangeSnapshotCallback, + cancelCallback: CancelCallback? = definedExternally, +): Unsubscribe + +external fun onValue( + query: Query, + callback: ValueSnapshotCallback, + cancelCallback: CancelCallback? = definedExternally, +): Unsubscribe + +external fun onDisconnect(ref: DatabaseReference): OnDisconnect + +external fun orderByChild(path: String): QueryConstraint + +external fun orderByKey(): QueryConstraint + +external fun orderByValue(): QueryConstraint + +external fun push(parent: DatabaseReference, value: Any? = definedExternally): ThenableReference + +external fun query(query: Query, vararg queryConstraints: QueryConstraint): Query + +external fun ref(db: Database, path: String? = definedExternally): DatabaseReference + +external fun remove(ref: DatabaseReference): Promise + +external fun serverTimestamp(): Any + +external fun set(ref: DatabaseReference, value: Any?): Promise + +external fun startAfter(value: Any?, key: String? = definedExternally): QueryConstraint + +external fun startAt(value: Any?, key: String? = definedExternally): QueryConstraint + +external fun update(ref: DatabaseReference, values: Any): Promise + +external fun runTransaction( + ref: DatabaseReference, + transactionUpdate: (currentData: T) -> T, + options: Any? = definedExternally +): Promise + +external interface Database { + val app: FirebaseApp +} + +external interface Query { + val ref: DatabaseReference +} + +external interface QueryConstraint + +external interface DatabaseReference : Query { + val key: String? + val parent: DatabaseReference? + val root: DatabaseReference +} + +external interface ThenableReference : DatabaseReference + +external interface DataSnapshot { + val key: String? + val size: Int + val ref: DatabaseReference + fun `val`(): Any + fun exists(): Boolean + fun forEach(action: (a: DataSnapshot) -> Boolean): Boolean + fun child(path: String): DataSnapshot + fun hasChildren(): Boolean; +} + +external interface OnDisconnect { + fun cancel(): Promise + fun remove(): Promise + fun set(value: Any?): Promise + fun update(value: Any): Promise +} + +external interface TransactionResult { + val committed: Boolean + val snapshot: DataSnapshot +} diff --git a/firebase-firestore/src/wasmJsMain/kotlin/firestore/FieldValue.kt b/firebase-firestore/src/wasmJsMain/kotlin/firestore/FieldValue.kt new file mode 100644 index 000000000..39aba6c99 --- /dev/null +++ b/firebase-firestore/src/wasmJsMain/kotlin/firestore/FieldValue.kt @@ -0,0 +1,32 @@ +package dev.gitlive.firebase.firestore + +import dev.gitlive.firebase.firestore.externals.deleteField +import kotlinx.serialization.Serializable +import dev.gitlive.firebase.firestore.externals.serverTimestamp as jsServerTimestamp +import dev.gitlive.firebase.firestore.externals.arrayRemove as jsArrayRemove +import dev.gitlive.firebase.firestore.externals.arrayUnion as jsArrayUnion +import dev.gitlive.firebase.firestore.externals.increment as jsIncrement + +/** Represents a platform specific Firebase FieldValue. */ +typealias NativeFieldValue = dev.gitlive.firebase.firestore.externals.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 as NativeFieldValue).isEqual(other.nativeValue as NativeFieldValue) + override fun hashCode(): Int = nativeValue.hashCode() + override fun toString(): String = nativeValue.toString() + + actual companion object { + actual val serverTimestamp: FieldValue get() = rethrow { FieldValue(jsServerTimestamp()) } + actual val delete: FieldValue get() = rethrow { FieldValue(deleteField()) } + actual fun increment(value: Int): FieldValue = rethrow { FieldValue(jsIncrement(value)) } + actual fun arrayUnion(vararg elements: Any): FieldValue = rethrow { FieldValue(jsArrayUnion(*elements)) } + actual fun arrayRemove(vararg elements: Any): FieldValue = rethrow { FieldValue(jsArrayRemove(*elements)) } + } +} diff --git a/firebase-firestore/src/wasmJsMain/kotlin/firestore/GeoPoint.kt b/firebase-firestore/src/wasmJsMain/kotlin/firestore/GeoPoint.kt new file mode 100644 index 000000000..2271e446e --- /dev/null +++ b/firebase-firestore/src/wasmJsMain/kotlin/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 = dev.gitlive.firebase.firestore.externals.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 by nativeValue::latitude + actual val longitude: Double by nativeValue::longitude + + override fun equals(other: Any?): Boolean = + this === other || other is GeoPoint && nativeValue.isEqual(other.nativeValue) + override fun hashCode(): Int = nativeValue.hashCode() + override fun toString(): String = "GeoPoint[lat=$latitude,long=$longitude]" +} diff --git a/firebase-firestore/src/wasmJsMain/kotlin/firestore/Timestamp.kt b/firebase-firestore/src/wasmJsMain/kotlin/firestore/Timestamp.kt new file mode 100644 index 000000000..dabec0055 --- /dev/null +++ b/firebase-firestore/src/wasmJsMain/kotlin/firestore/Timestamp.kt @@ -0,0 +1,34 @@ +package dev.gitlive.firebase.firestore + +import kotlinx.serialization.Serializable + +/** 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 platform specific Firebase Timestamp. */ +actual typealias NativeTimestamp = dev.gitlive.firebase.firestore.externals.Timestamp + +/** 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.toDouble(), nanoseconds.toDouble())) + + actual val seconds: Long = nativeValue.seconds.toLong() + actual val nanoseconds: Int = nativeValue.nanoseconds.toInt() + + override fun equals(other: Any?): Boolean = + this === other || other is Timestamp && nativeValue.isEqual(other.nativeValue) + override fun hashCode(): Int = nativeValue.toMillis().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/wasmJsMain/kotlin/firestore/_encoders.kt b/firebase-firestore/src/wasmJsMain/kotlin/firestore/_encoders.kt new file mode 100644 index 000000000..84445fb4d --- /dev/null +++ b/firebase-firestore/src/wasmJsMain/kotlin/firestore/_encoders.kt @@ -0,0 +1,10 @@ +package dev.gitlive.firebase.firestore + +@PublishedApi +internal actual fun isSpecialValue(value: Any) = when(value) { + is NativeFieldValue, + is NativeGeoPoint, + is NativeTimestamp, + is NativeDocumentReferenceType -> true + else -> false +} diff --git a/firebase-firestore/src/wasmJsMain/kotlin/firestore/externals/firestore.kt b/firebase-firestore/src/wasmJsMain/kotlin/firestore/externals/firestore.kt new file mode 100644 index 000000000..532118bfc --- /dev/null +++ b/firebase-firestore/src/wasmJsMain/kotlin/firestore/externals/firestore.kt @@ -0,0 +1,334 @@ +@file:JsModule("firebase/firestore") +@file:JsNonModule + +package dev.gitlive.firebase.firestore.externals + +import dev.gitlive.firebase.Unsubscribe +import dev.gitlive.firebase.externals.FirebaseApp +import kotlin.js.Json +import kotlin.js.Promise + +external fun documentId(): FieldPath + +external class FieldPath(vararg fieldNames: String) { + fun isEqual(other: FieldPath): Boolean + +} + +external fun refEqual(left: DocumentReference, right: DocumentReference): Boolean + +external fun addDoc(reference: CollectionReference, data: Any): Promise + +external fun arrayRemove(vararg elements: Any): FieldValue + +external fun arrayUnion(vararg elements: Any): FieldValue + +external fun clearIndexedDbPersistence(firestore: Firestore): Promise + +external fun collection(firestore: Firestore, collectionPath: String): CollectionReference + +external fun collection(reference: DocumentReference, collectionPath: String): CollectionReference + +external fun collectionGroup(firestore: Firestore, collectionId: String): Query + +external fun connectFirestoreEmulator( + firestore: Firestore, + host: String, + port: Int, + options: Any? = definedExternally +) + +external fun deleteDoc(reference: DocumentReference): Promise + +external fun deleteField(): FieldValue + +external fun disableNetwork(firestore: Firestore): Promise + +external fun doc(firestore: Firestore, documentPath: String): DocumentReference + +external fun doc(firestore: CollectionReference, documentPath: String? = definedExternally): DocumentReference + +external fun enableIndexedDbPersistence( + firestore: Firestore, + persistenceSettings: Any? = definedExternally +): Promise + +external fun enableNetwork(firestore: Firestore): Promise + +external fun endAt(document: DocumentSnapshot): QueryConstraint + +external fun endAt(vararg fieldValues: Any): QueryConstraint + +external fun endBefore(document: DocumentSnapshot): QueryConstraint + +external fun endBefore(vararg fieldValues: Any): QueryConstraint + +external fun getDoc( + reference: DocumentReference, + options: Any? = definedExternally +): Promise + +external fun getDocFromCache( + reference: DocumentReference, +): Promise + +external fun getDocFromServer( + reference: DocumentReference, +): Promise + +external fun getDocs(query: Query): Promise + +external fun getDocsFromCache(query: Query): Promise + +external fun getDocsFromServer(query: Query): Promise + +external fun getFirestore(app: FirebaseApp? = definedExternally): Firestore + +external fun increment(n: Int): FieldValue + +external fun initializeFirestore(app: FirebaseApp, settings: dynamic = definedExternally, databaseId: String? = definedExternally): Firestore + +external fun limit(limit: Number): QueryConstraint + +external fun onSnapshot( + reference: DocumentReference, + next: (snapshot: DocumentSnapshot) -> Unit, + error: (error: Throwable) -> Unit +): Unsubscribe + +external fun onSnapshot( + reference: DocumentReference, + options: Json, + next: (snapshot: DocumentSnapshot) -> Unit, + error: (error: Throwable) -> Unit +): Unsubscribe + +external fun onSnapshot( + reference: Query, + next: (snapshot: QuerySnapshot) -> Unit, + error: (error: Throwable) -> Unit +): Unsubscribe + +external fun onSnapshot( + reference: Query, + options: Json, + next: (snapshot: QuerySnapshot) -> Unit, + error: (error: Throwable) -> Unit +): Unsubscribe + +external fun orderBy(field: String, direction: Any): QueryConstraint + +external fun orderBy(field: FieldPath, direction: Any): QueryConstraint + +external fun query(query: Query, vararg queryConstraints: QueryConstraint): Query + +external fun runTransaction( + firestore: Firestore, + updateFunction: (transaction: Transaction) -> Promise, + options: Any? = definedExternally +): Promise + +external fun serverTimestamp(): FieldValue + +external fun setDoc( + documentReference: DocumentReference, + data: Any, + options: Any? = definedExternally +): Promise + +external fun setLogLevel(logLevel: String) + +external fun startAfter(document: DocumentSnapshot): QueryConstraint + +external fun startAfter(vararg fieldValues: Any): QueryConstraint + +external fun startAt(document: DocumentSnapshot): QueryConstraint + +external fun startAt(vararg fieldValues: Any): QueryConstraint + +external fun updateDoc(reference: DocumentReference, data: Any): Promise + +external fun updateDoc( + reference: DocumentReference, + field: String, + value: Any?, + vararg moreFieldsAndValues: Any? +): Promise + +external fun updateDoc( + reference: DocumentReference, + field: FieldPath, + value: Any?, + vararg moreFieldsAndValues: Any? +): Promise + +external fun where(field: String, opStr: String, value: Any?): QueryConstraint + +external fun where(field: FieldPath, opStr: String, value: Any?): QueryConstraint + +external fun and(vararg queryConstraints: QueryConstraint): QueryConstraint + +external fun or(vararg queryConstraints: QueryConstraint): QueryConstraint + +external fun writeBatch(firestore: Firestore): WriteBatch + +external interface Firestore { + val app: FirebaseApp +} + +external class GeoPoint constructor(latitude: Double, longitude: Double) { + val latitude: Double + val longitude: Double + fun isEqual(other: GeoPoint): Boolean +} + +external interface CollectionReference : Query { + val id: String + val path: String + val parent: DocumentReference? +} + +external interface DocumentChange { + val doc: DocumentSnapshot + val newIndex: Int + val oldIndex: Int + val type: String +} + +external class DocumentReference { + val id: String + val path: String + val parent: CollectionReference +} + +external interface DocumentSnapshot { + val id: String + val ref: DocumentReference + val metadata: SnapshotMetadata + fun data(options: Any? = definedExternally): Any? + fun exists(): Boolean + fun get(fieldPath: String, options: Any? = definedExternally): Any? + fun get(fieldPath: FieldPath, options: Any? = definedExternally): Any? +} + +external class FieldValue { + fun isEqual(other: FieldValue): Boolean +} + +external interface Query + +external interface QueryConstraint + +external interface QuerySnapshot { + val docs: Array + val empty: Boolean + val metadata: SnapshotMetadata + fun docChanges(): Array +} + +external interface SnapshotMetadata { + val hasPendingWrites: Boolean + val fromCache: Boolean +} + +external interface Transaction { + fun get(documentReference: DocumentReference): Promise + + fun set( + documentReference: DocumentReference, + data: Any, + options: Any? = definedExternally + ): Transaction + + fun update(documentReference: DocumentReference, data: Any): Transaction + + fun update( + documentReference: DocumentReference, + field: String, + value: Any?, + vararg moreFieldsAndValues: Any? + ): Transaction + + fun update( + documentReference: DocumentReference, + field: FieldPath, + value: Any?, + vararg moreFieldsAndValues: Any? + ): Transaction + + fun delete(documentReference: DocumentReference): Transaction +} + +external interface WriteBatch { + fun commit(): Promise + + fun delete(documentReference: DocumentReference): WriteBatch + + fun set( + documentReference: DocumentReference, + data: Any, + options: Any? = definedExternally + ): WriteBatch + + fun update(documentReference: DocumentReference, data: Any): WriteBatch + + fun update( + documentReference: DocumentReference, + field: String, + value: Any?, + vararg moreFieldsAndValues: Any? + ): WriteBatch + + fun update( + documentReference: DocumentReference, + field: FieldPath, + value: Any?, + vararg moreFieldsAndValues: Any? + ): WriteBatch +} + +external class Timestamp(seconds: Double, nanoseconds: Double) { + companion object { + fun now(): Timestamp + } + + val seconds: Double + val nanoseconds: Double + fun toMillis(): Double + + fun isEqual(other: Timestamp): Boolean +} + +external interface FirestoreLocalCache { + val kind: String +} + +external interface MemoryLocalCache : FirestoreLocalCache +external interface PersistentLocalCache : FirestoreLocalCache + +external interface MemoryCacheSettings { + val garbageCollector: MemoryGarbageCollector +} + +external interface MemoryGarbageCollector { + val kind: String +} + +external interface MemoryLruGarbageCollector : MemoryGarbageCollector +external interface MemoryEagerGarbageCollector : MemoryGarbageCollector + +external interface PersistentCacheSettings { + val cacheSizeBytes: Int + val tabManager: PersistentTabManager +} + +external interface PersistentTabManager { + val kind: String +} + +external fun memoryLocalCache(settings: MemoryCacheSettings): MemoryLocalCache +external fun memoryEagerGarbageCollector(): MemoryEagerGarbageCollector +external fun memoryLruGarbageCollector(settings: dynamic = definedExternally): MemoryLruGarbageCollector +external fun persistentLocalCache(settings: PersistentCacheSettings): PersistentLocalCache +external fun persistentSingleTabManager(settings: dynamic = definedExternally): PersistentTabManager +external fun persistentMultipleTabManager(): PersistentTabManager diff --git a/firebase-firestore/src/wasmJsMain/kotlin/firestore/firestore.kt b/firebase-firestore/src/wasmJsMain/kotlin/firestore/firestore.kt new file mode 100644 index 000000000..f51f9888b --- /dev/null +++ b/firebase-firestore/src/wasmJsMain/kotlin/firestore/firestore.kt @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.gitlive.firebase.firestore + +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseApp +import dev.gitlive.firebase.FirebaseException +import dev.gitlive.firebase.externals.getApp +import dev.gitlive.firebase.firestore.externals.MemoryCacheSettings +import dev.gitlive.firebase.firestore.externals.PersistentCacheSettings +import dev.gitlive.firebase.firestore.externals.getDoc +import dev.gitlive.firebase.firestore.externals.getDocFromCache +import dev.gitlive.firebase.firestore.externals.getDocFromServer +import dev.gitlive.firebase.firestore.externals.getDocs +import dev.gitlive.firebase.firestore.externals.getDocsFromCache +import dev.gitlive.firebase.firestore.externals.getDocsFromServer +import dev.gitlive.firebase.firestore.externals.memoryEagerGarbageCollector +import dev.gitlive.firebase.firestore.externals.memoryLocalCache +import dev.gitlive.firebase.firestore.externals.memoryLruGarbageCollector +import dev.gitlive.firebase.firestore.externals.persistentLocalCache +import dev.gitlive.firebase.firestore.internal.NativeDocumentSnapshotWrapper +import dev.gitlive.firebase.firestore.internal.NativeFirebaseFirestoreWrapper +import dev.gitlive.firebase.firestore.internal.SetOptions +import kotlin.js.Json +import kotlin.js.json +import dev.gitlive.firebase.firestore.externals.Firestore as JsFirestore +import dev.gitlive.firebase.firestore.externals.CollectionReference as JsCollectionReference +import dev.gitlive.firebase.firestore.externals.DocumentChange as JsDocumentChange +import dev.gitlive.firebase.firestore.externals.DocumentReference as JsDocumentReference +import dev.gitlive.firebase.firestore.externals.DocumentSnapshot as JsDocumentSnapshot +import dev.gitlive.firebase.firestore.externals.FieldPath as JsFieldPath +import dev.gitlive.firebase.firestore.externals.Query as JsQuery +import dev.gitlive.firebase.firestore.externals.QuerySnapshot as JsQuerySnapshot +import dev.gitlive.firebase.firestore.externals.SnapshotMetadata as JsSnapshotMetadata +import dev.gitlive.firebase.firestore.externals.Transaction as JsTransaction +import dev.gitlive.firebase.firestore.externals.WriteBatch as JsWriteBatch +import dev.gitlive.firebase.firestore.externals.documentId as jsDocumentId + +actual val Firebase.firestore get() = + rethrow { FirebaseFirestore(NativeFirebaseFirestoreWrapper(getApp())) } + +actual fun Firebase.firestore(app: FirebaseApp) = + rethrow { FirebaseFirestore(NativeFirebaseFirestoreWrapper(app.js)) } + +actual data class NativeFirebaseFirestore(val js: JsFirestore) + +val FirebaseFirestore.js: JsFirestore get() = native.js + +actual data class FirebaseFirestoreSettings( + actual val sslEnabled: Boolean, + actual val host: String, + actual val cacheSettings: LocalCacheSettings, +) { + + actual companion object { + actual val CACHE_SIZE_UNLIMITED: Long = -1L + internal actual val DEFAULT_HOST: String = "firestore.googleapis.com" + internal actual val MINIMUM_CACHE_BYTES: Long = 1 * 1024 * 1024 + // According to documentation, default JS Firestore cache size is 40MB, not 100MB + internal actual val DEFAULT_CACHE_SIZE_BYTES: Long = 40 * 1024 * 1024 + } + + actual class Builder internal constructor( + actual var sslEnabled: Boolean, + actual var host: String, + actual var cacheSettings: LocalCacheSettings, + ) { + + actual constructor() : this( + true, + DEFAULT_HOST, + persistentCacheSettings { }, + ) + actual constructor(settings: FirebaseFirestoreSettings) : this(settings.sslEnabled, settings.host, settings.cacheSettings) + + actual fun build(): FirebaseFirestoreSettings = FirebaseFirestoreSettings(sslEnabled, host, cacheSettings) + } + + @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") + val js: Json get() = json().apply { + set("ssl", sslEnabled) + set("host", host) + set("localCache", + when (cacheSettings) { + is LocalCacheSettings.Persistent -> persistentLocalCache( + json( + "cacheSizeBytes" to cacheSettings.sizeBytes + ).asDynamic() as PersistentCacheSettings + ) + is LocalCacheSettings.Memory -> { + val garbageCollecorSettings = when (val garbageCollectorSettings = cacheSettings.garbaseCollectorSettings) { + is MemoryGarbageCollectorSettings.Eager -> memoryEagerGarbageCollector() + is MemoryGarbageCollectorSettings.LRUGC -> memoryLruGarbageCollector(json("cacheSizeBytes" to garbageCollectorSettings.sizeBytes)) + } + memoryLocalCache(json("garbageCollector" to garbageCollecorSettings).asDynamic() as MemoryCacheSettings) + } + }) + } +} + +actual fun firestoreSettings( + settings: FirebaseFirestoreSettings?, + builder: FirebaseFirestoreSettings.Builder.() -> Unit +): FirebaseFirestoreSettings = FirebaseFirestoreSettings.Builder().apply { + settings?.let { + sslEnabled = it.sslEnabled + host = it.host + cacheSettings = it.cacheSettings + } +}.apply(builder).build() + +actual data class NativeWriteBatch(val js: JsWriteBatch) + +val WriteBatch.js get() = native.js + +actual data class NativeTransaction(val js: JsTransaction) + +val Transaction.js get() = native.js + +/** A class representing a platform specific Firebase DocumentReference. */ +actual typealias NativeDocumentReferenceType = JsDocumentReference + +val DocumentReference.js get() = native.js + +actual open class NativeQuery(open val js: JsQuery) +internal val JsQuery.wrapped get() = NativeQuery(this) + +val Query.js get() = native.js + +actual data class NativeCollectionReference(override val js: JsCollectionReference) : NativeQuery(js) + +val CollectionReference.js get() = native.js + +actual class FirebaseFirestoreException(cause: Throwable, val code: FirestoreExceptionCode) : FirebaseException(code.toString(), cause) + +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") +actual val FirebaseFirestoreException.code: FirestoreExceptionCode get() = code + +actual class QuerySnapshot(val js: JsQuerySnapshot) { + actual val documents + get() = js.docs.map { DocumentSnapshot(NativeDocumentSnapshotWrapper(it)) } + actual val documentChanges + get() = js.docChanges().map { DocumentChange(it) } + actual val metadata: SnapshotMetadata get() = SnapshotMetadata(js.metadata) +} + +actual class DocumentChange(val js: JsDocumentChange) { + actual val document: DocumentSnapshot + get() = DocumentSnapshot(NativeDocumentSnapshotWrapper(js.doc)) + actual val newIndex: Int + get() = js.newIndex + actual val oldIndex: Int + get() = js.oldIndex + actual val type: ChangeType + get() = ChangeType.values().first { it.jsString == js.type } +} + +actual data class NativeDocumentSnapshot(val js: JsDocumentSnapshot) + +val DocumentSnapshot.js get() = native.js + +actual class SnapshotMetadata(val js: JsSnapshotMetadata) { + actual val hasPendingWrites: Boolean get() = js.hasPendingWrites + actual val isFromCache: Boolean get() = js.fromCache +} + +actual class FieldPath private constructor(val js: JsFieldPath) { + + actual companion object { + actual val documentId = FieldPath(jsDocumentId()) + } + actual constructor(vararg fieldNames: String) : this(dev.gitlive.firebase.firestore.rethrow { + JsFieldPath(*fieldNames) + }) + actual val documentId: FieldPath get() = FieldPath.documentId + actual val encoded: EncodedFieldPath = js + override fun equals(other: Any?): Boolean = other is FieldPath && js.isEqual(other.js) + override fun hashCode(): Int = js.hashCode() + override fun toString(): String = js.toString() +} + +actual typealias EncodedFieldPath = JsFieldPath + +actual enum class FirestoreExceptionCode { + OK, + CANCELLED, + UNKNOWN, + INVALID_ARGUMENT, + DEADLINE_EXCEEDED, + NOT_FOUND, + ALREADY_EXISTS, + PERMISSION_DENIED, + RESOURCE_EXHAUSTED, + FAILED_PRECONDITION, + ABORTED, + OUT_OF_RANGE, + UNIMPLEMENTED, + INTERNAL, + UNAVAILABLE, + DATA_LOSS, + UNAUTHENTICATED +} + +actual enum class Direction(internal val jsString : String) { + ASCENDING("asc"), + DESCENDING("desc"); +} + +actual enum class ChangeType(internal val jsString : String) { + ADDED("added"), + MODIFIED("modified"), + REMOVED("removed"); +} + +inline fun T.rethrow(function: T.() -> R): R = dev.gitlive.firebase.firestore.rethrow { function() } + +inline fun rethrow(function: () -> R): R { + try { + return function() + } catch (e: Exception) { + throw e + } catch(e: dynamic) { + throw errorToException(e) + } +} + +fun errorToException(e: dynamic) = (e?.code ?: e?.message ?: "") + .toString() + .lowercase() + .let { + when { + "cancelled" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.CANCELLED) + "invalid-argument" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.INVALID_ARGUMENT) + "deadline-exceeded" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.DEADLINE_EXCEEDED) + "not-found" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.NOT_FOUND) + "already-exists" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.ALREADY_EXISTS) + "permission-denied" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.PERMISSION_DENIED) + "resource-exhausted" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.RESOURCE_EXHAUSTED) + "failed-precondition" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.FAILED_PRECONDITION) + "aborted" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.ABORTED) + "out-of-range" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.OUT_OF_RANGE) + "unimplemented" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.UNIMPLEMENTED) + "internal" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.INTERNAL) + "unavailable" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.UNAVAILABLE) + "data-loss" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.DATA_LOSS) + "unauthenticated" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.UNAUTHENTICATED) + "unknown" in it -> FirebaseFirestoreException(e, FirestoreExceptionCode.UNKNOWN) + else -> { + println("Unknown error code in ${JSON.stringify(e)}") + FirebaseFirestoreException(e, FirestoreExceptionCode.UNKNOWN) + } + } + } + +// from: https://discuss.kotlinlang.org/t/how-to-access-native-js-object-as-a-map-string-any/509/8 +fun entriesOf(jsObject: dynamic): List> = + (js("Object.entries") as (dynamic) -> Array>) + .invoke(jsObject) + .map { entry -> entry[0] as String to entry[1] } + +// from: https://discuss.kotlinlang.org/t/how-to-access-native-js-object-as-a-map-string-any/509/8 +fun mapOf(jsObject: dynamic): Map = + entriesOf(jsObject).toMap() diff --git a/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/JSQueryGet.kt b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/JSQueryGet.kt new file mode 100644 index 000000000..51524558a --- /dev/null +++ b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/JSQueryGet.kt @@ -0,0 +1,7 @@ +package dev.gitlive.firebase.firestore.internal + +import dev.gitlive.firebase.firestore.Source +import dev.gitlive.firebase.firestore.externals.Query +import dev.gitlive.firebase.firestore.externals.getDocs +import dev.gitlive.firebase.firestore.externals.getDocsFromCache +import dev.gitlive.firebase.firestore.externals.getDocsFromServer diff --git a/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeCollectionReferenceWrapper.kt b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeCollectionReferenceWrapper.kt new file mode 100644 index 000000000..f3f475006 --- /dev/null +++ b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeCollectionReferenceWrapper.kt @@ -0,0 +1,38 @@ +package dev.gitlive.firebase.firestore.internal + +import dev.gitlive.firebase.firestore.NativeCollectionReference +import dev.gitlive.firebase.firestore.externals.CollectionReference +import dev.gitlive.firebase.firestore.externals.addDoc +import dev.gitlive.firebase.firestore.externals.doc +import dev.gitlive.firebase.firestore.rethrow +import dev.gitlive.firebase.internal.EncodedObject +import dev.gitlive.firebase.internal.js +import kotlinx.coroutines.await + +@PublishedApi +internal actual class NativeCollectionReferenceWrapper internal actual constructor(actual override val native: NativeCollectionReference) : NativeQueryWrapper(native) { + + constructor(js: CollectionReference) : this(NativeCollectionReference(js)) + + override val js: CollectionReference = native.js + + actual val path: String + get() = rethrow { js.path } + + actual val document get() = rethrow { NativeDocumentReference(doc(js)) } + + actual val parent get() = rethrow { js.parent?.let{ NativeDocumentReference(it) } } + + actual fun document(documentPath: String) = rethrow { + NativeDocumentReference( + doc( + js, + documentPath + ) + ) + } + + actual suspend fun addEncoded(data: EncodedObject) = rethrow { + NativeDocumentReference(addDoc(js, data.js).await()) + } +} \ No newline at end of file diff --git a/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeDocumentReference.kt b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeDocumentReference.kt new file mode 100644 index 000000000..f38a8faf0 --- /dev/null +++ b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeDocumentReference.kt @@ -0,0 +1,110 @@ +package dev.gitlive.firebase.firestore.internal + +import dev.gitlive.firebase.firestore.EncodedFieldPath +import dev.gitlive.firebase.firestore.NativeCollectionReference +import dev.gitlive.firebase.firestore.NativeDocumentReferenceType +import dev.gitlive.firebase.firestore.NativeDocumentSnapshot +import dev.gitlive.firebase.firestore.Source +import dev.gitlive.firebase.firestore.errorToException +import dev.gitlive.firebase.firestore.externals.deleteDoc +import dev.gitlive.firebase.firestore.externals.getDoc +import dev.gitlive.firebase.firestore.externals.getDocFromCache +import dev.gitlive.firebase.firestore.externals.getDocFromServer +import dev.gitlive.firebase.firestore.externals.onSnapshot +import dev.gitlive.firebase.firestore.externals.refEqual +import dev.gitlive.firebase.firestore.externals.setDoc +import dev.gitlive.firebase.firestore.externals.updateDoc +import dev.gitlive.firebase.firestore.performUpdate +import dev.gitlive.firebase.firestore.rethrow +import dev.gitlive.firebase.internal.EncodedObject +import dev.gitlive.firebase.internal.js +import kotlinx.coroutines.await +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlin.js.json + +@PublishedApi +internal actual class NativeDocumentReference actual constructor(actual val nativeValue: NativeDocumentReferenceType) { + val js: NativeDocumentReferenceType = nativeValue + + actual val id: String + get() = rethrow { js.id } + + actual val path: String + get() = rethrow { js.path } + + actual val parent: NativeCollectionReferenceWrapper + get() = rethrow { NativeCollectionReferenceWrapper(js.parent) } + + actual fun collection(collectionPath: String) = rethrow { + NativeCollectionReference( + dev.gitlive.firebase.firestore.externals.collection( + js, + collectionPath + ) + ) + } + + actual suspend fun get(source: Source) = rethrow { + NativeDocumentSnapshot( + js.get(source).await() + ) + } + + actual val snapshots: Flow get() = snapshots() + + actual fun snapshots(includeMetadataChanges: Boolean) = callbackFlow { + val unsubscribe = onSnapshot( + js, + json("includeMetadataChanges" to includeMetadataChanges), + { trySend(NativeDocumentSnapshot(it)) }, + { close(errorToException(it)) } + ) + awaitClose { unsubscribe() } + } + + actual suspend fun setEncoded(encodedData: EncodedObject, setOptions: SetOptions) = rethrow { + setDoc(js, encodedData.js, setOptions.js).await() + } + + actual suspend fun updateEncoded(encodedData: EncodedObject) = rethrow { updateDoc( + js, + encodedData.js + ).await() } + + actual suspend fun updateEncodedFieldsAndValues(encodedFieldsAndValues: List>) { + rethrow { + encodedFieldsAndValues.takeUnless { encodedFieldsAndValues.isEmpty() } + ?.performUpdate { field, value, moreFieldsAndValues -> + updateDoc(js, field, value, *moreFieldsAndValues) + } + ?.await() + } + } + + actual suspend fun updateEncodedFieldPathsAndValues(encodedFieldsAndValues: List>) { + rethrow { + encodedFieldsAndValues.takeUnless { encodedFieldsAndValues.isEmpty() } + ?.performUpdate { field, value, moreFieldsAndValues -> + updateDoc(js, field, value, *moreFieldsAndValues) + }?.await() + } + } + + actual suspend fun delete() = rethrow { deleteDoc(js).await() } + + override fun equals(other: Any?): Boolean = + this === other || other is NativeDocumentReference && refEqual( + nativeValue, + other.nativeValue + ) + override fun hashCode(): Int = nativeValue.hashCode() + override fun toString(): String = "DocumentReference(path=$path)" +} + +private fun NativeDocumentReferenceType.get(source: Source) = when (source) { + Source.DEFAULT -> getDoc(this) + Source.CACHE -> getDocFromCache(this) + Source.SERVER -> getDocFromServer(this) +} \ No newline at end of file diff --git a/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeDocumentSnapshotWrapper.kt b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeDocumentSnapshotWrapper.kt new file mode 100644 index 000000000..dfc83769a --- /dev/null +++ b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeDocumentSnapshotWrapper.kt @@ -0,0 +1,40 @@ +package dev.gitlive.firebase.firestore.internal + +import dev.gitlive.firebase.firestore.EncodedFieldPath +import dev.gitlive.firebase.firestore.NativeDocumentSnapshot +import dev.gitlive.firebase.firestore.ServerTimestampBehavior +import dev.gitlive.firebase.firestore.SnapshotMetadata +import dev.gitlive.firebase.firestore.externals.DocumentSnapshot +import dev.gitlive.firebase.firestore.rethrow +import kotlin.js.json + +@PublishedApi +internal actual class NativeDocumentSnapshotWrapper actual internal constructor(actual val native: NativeDocumentSnapshot) { + + constructor(js: DocumentSnapshot) : this(NativeDocumentSnapshot(js)) + + val js: DocumentSnapshot = native.js + + actual val id get() = rethrow { js.id } + actual val reference get() = rethrow { NativeDocumentReference(js.ref) } + + actual fun getEncoded(field: String, serverTimestampBehavior: ServerTimestampBehavior): Any? = rethrow { + js.get(field, getTimestampsOptions(serverTimestampBehavior)) + } + + actual fun getEncoded(fieldPath: EncodedFieldPath, serverTimestampBehavior: ServerTimestampBehavior): Any? = rethrow { + js.get(fieldPath, getTimestampsOptions(serverTimestampBehavior)) + } + + actual fun encodedData(serverTimestampBehavior: ServerTimestampBehavior): Any? = rethrow { + js.data(getTimestampsOptions(serverTimestampBehavior)) + } + + actual fun contains(field: String) = rethrow { js.get(field) != undefined } + actual fun contains(fieldPath: EncodedFieldPath) = rethrow { js.get(fieldPath) != undefined } + actual val exists get() = rethrow { js.exists() } + actual val metadata: SnapshotMetadata get() = SnapshotMetadata(js.metadata) + + fun getTimestampsOptions(serverTimestampBehavior: ServerTimestampBehavior) = + json("serverTimestamps" to serverTimestampBehavior.name.lowercase()) +} \ No newline at end of file diff --git a/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeFirebaseFirestoreWrapper.kt b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeFirebaseFirestoreWrapper.kt new file mode 100644 index 000000000..4c186869d --- /dev/null +++ b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeFirebaseFirestoreWrapper.kt @@ -0,0 +1,113 @@ +package dev.gitlive.firebase.firestore.internal + +import dev.gitlive.firebase.externals.FirebaseApp +import dev.gitlive.firebase.firestore.FirebaseFirestoreSettings +import dev.gitlive.firebase.firestore.NativeCollectionReference +import dev.gitlive.firebase.firestore.NativeFirebaseFirestore +import dev.gitlive.firebase.firestore.NativeQuery +import dev.gitlive.firebase.firestore.NativeTransaction +import dev.gitlive.firebase.firestore.NativeWriteBatch +import dev.gitlive.firebase.firestore.externals.clearIndexedDbPersistence +import dev.gitlive.firebase.firestore.externals.connectFirestoreEmulator +import dev.gitlive.firebase.firestore.externals.doc +import dev.gitlive.firebase.firestore.externals.initializeFirestore +import dev.gitlive.firebase.firestore.externals.setLogLevel +import dev.gitlive.firebase.firestore.externals.writeBatch +import dev.gitlive.firebase.firestore.firestoreSettings +import dev.gitlive.firebase.firestore.rethrow +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.await +import kotlinx.coroutines.promise + +internal actual class NativeFirebaseFirestoreWrapper internal constructor( + private val createNative: NativeFirebaseFirestoreWrapper.() -> NativeFirebaseFirestore +){ + + internal actual constructor(native: NativeFirebaseFirestore) : this({ native }) + internal constructor(app: FirebaseApp) : this( + { + NativeFirebaseFirestore( + initializeFirestore(app, settings.js).also { + emulatorSettings?.run { + connectFirestoreEmulator(it, host, port) + } + } + ) + } + ) + + private data class EmulatorSettings(val host: String, val port: Int) + + actual var settings: FirebaseFirestoreSettings = FirebaseFirestoreSettings.Builder().build() + set(value) { + if (lazyNative.isInitialized()) { + throw IllegalStateException("FirebaseFirestore has already been started and its settings can no longer be changed. You can only call setFirestoreSettings() before calling any other methods on a FirebaseFirestore object.") + } else { + field = value + } + } + private var emulatorSettings: EmulatorSettings? = null + + // initializeFirestore must be called before any call, including before `getFirestore()` + // To allow settings to be updated, we defer creating the wrapper until the first call to `native` + private val lazyNative = lazy { + createNative() + } + actual val native: NativeFirebaseFirestore by lazyNative + private val js get() = native.js + + actual fun collection(collectionPath: String) = rethrow { + NativeCollectionReference( + dev.gitlive.firebase.firestore.externals.collection( + js, + collectionPath + ) + ) + } + + actual fun collectionGroup(collectionId: String) = rethrow { + NativeQuery( + dev.gitlive.firebase.firestore.externals.collectionGroup( + js, + collectionId + ) + ) + } + + actual fun document(documentPath: String) = rethrow { + NativeDocumentReference( + doc( + js, + documentPath + ) + ) + } + + actual fun batch() = rethrow { NativeWriteBatch(writeBatch(js)) } + + actual fun setLoggingEnabled(loggingEnabled: Boolean) = + rethrow { setLogLevel(if (loggingEnabled) "error" else "silent") } + + actual suspend fun runTransaction(func: suspend NativeTransaction.() -> T) = + rethrow { dev.gitlive.firebase.firestore.externals.runTransaction( + js, + { GlobalScope.promise { NativeTransaction(it).func() } }).await() } + + actual suspend fun clearPersistence() = + rethrow { clearIndexedDbPersistence(js).await() } + + actual fun useEmulator(host: String, port: Int) = rethrow { + settings = firestoreSettings(settings) { + this.host = "$host:$port" + } + emulatorSettings = EmulatorSettings(host, port) + } + + actual suspend fun disableNetwork() { + rethrow { dev.gitlive.firebase.firestore.externals.disableNetwork(js).await() } + } + + actual suspend fun enableNetwork() { + rethrow { dev.gitlive.firebase.firestore.externals.enableNetwork(js).await() } + } +} \ No newline at end of file diff --git a/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeQueryWrapper.kt b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeQueryWrapper.kt new file mode 100644 index 000000000..61c98df06 --- /dev/null +++ b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeQueryWrapper.kt @@ -0,0 +1,154 @@ +package dev.gitlive.firebase.firestore.internal + +import dev.gitlive.firebase.firestore.Direction +import dev.gitlive.firebase.firestore.EncodedFieldPath +import dev.gitlive.firebase.firestore.Filter +import dev.gitlive.firebase.firestore.NativeDocumentSnapshot +import dev.gitlive.firebase.firestore.NativeQuery +import dev.gitlive.firebase.firestore.QuerySnapshot +import dev.gitlive.firebase.firestore.Source +import dev.gitlive.firebase.firestore.WhereConstraint +import dev.gitlive.firebase.firestore.errorToException +import dev.gitlive.firebase.firestore.externals.Query +import dev.gitlive.firebase.firestore.externals.QueryConstraint +import dev.gitlive.firebase.firestore.externals.and +import dev.gitlive.firebase.firestore.externals.getDocs +import dev.gitlive.firebase.firestore.externals.getDocsFromCache +import dev.gitlive.firebase.firestore.externals.getDocsFromServer +import dev.gitlive.firebase.firestore.externals.onSnapshot +import dev.gitlive.firebase.firestore.externals.or +import dev.gitlive.firebase.firestore.externals.query +import dev.gitlive.firebase.firestore.rethrow +import dev.gitlive.firebase.firestore.wrapped +import kotlinx.coroutines.await +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlin.js.json + +@PublishedApi +internal actual open class NativeQueryWrapper actual internal constructor(actual open val native: NativeQuery) { + + constructor(js: Query) : this(NativeQuery(js)) + + open val js: Query get() = native.js + + actual suspend fun get(source: Source) = rethrow { QuerySnapshot(js.get(source).await()) } + + actual fun limit(limit: Number) = query( + js, + dev.gitlive.firebase.firestore.externals.limit(limit) + ).wrapped + + actual fun where(filter: Filter) = query(js, filter.toQueryConstraint()).wrapped + + private fun Filter.toQueryConstraint(): QueryConstraint = when (this) { + is Filter.And -> and(*filters.map { it.toQueryConstraint() }.toTypedArray()) + is Filter.Or -> or(*filters.map { it.toQueryConstraint() }.toTypedArray()) + is Filter.Field -> { + val value = when (constraint) { + is WhereConstraint.ForNullableObject -> constraint.safeValue + is WhereConstraint.ForObject -> constraint.safeValue + is WhereConstraint.ForArray -> constraint.safeValues.toTypedArray() + } + dev.gitlive.firebase.firestore.externals.where(field, constraint.filterOp, value) + } + is Filter.Path -> { + val value = when (constraint) { + is WhereConstraint.ForNullableObject -> constraint.safeValue + is WhereConstraint.ForObject -> constraint.safeValue + is WhereConstraint.ForArray -> constraint.safeValues.toTypedArray() + } + dev.gitlive.firebase.firestore.externals.where(path.js, constraint.filterOp, value) + } + } + + private val WhereConstraint.filterOp: String get() = when (this) { + is WhereConstraint.EqualTo -> "==" + is WhereConstraint.NotEqualTo -> "!=" + is WhereConstraint.LessThan -> "<" + is WhereConstraint.LessThanOrEqualTo -> "<=" + is WhereConstraint.GreaterThan -> ">" + is WhereConstraint.GreaterThanOrEqualTo -> ">=" + is WhereConstraint.ArrayContains -> "array-contains" + is WhereConstraint.ArrayContainsAny -> "array-contains-any" + is WhereConstraint.InArray -> "in" + is WhereConstraint.NotInArray -> "not-in" + } + + actual fun orderBy(field: String, direction: Direction) = rethrow { + query(js, dev.gitlive.firebase.firestore.externals.orderBy(field, direction.jsString)).wrapped + } + + actual fun orderBy(field: EncodedFieldPath, direction: Direction) = rethrow { + query(js, dev.gitlive.firebase.firestore.externals.orderBy(field, direction.jsString)).wrapped + } + + actual fun startAfter(document: NativeDocumentSnapshot) = rethrow { query( + js, + dev.gitlive.firebase.firestore.externals.startAfter(document.js) + ).wrapped } + + actual fun startAfter(vararg fieldValues: Any) = rethrow { query( + js, + dev.gitlive.firebase.firestore.externals.startAfter(*fieldValues) + ).wrapped } + + actual fun startAt(document: NativeDocumentSnapshot) = rethrow { query( + js, + dev.gitlive.firebase.firestore.externals.startAt(document.js) + ).wrapped } + + actual fun startAt(vararg fieldValues: Any) = rethrow { query( + js, + dev.gitlive.firebase.firestore.externals.startAt(*fieldValues) + ).wrapped } + + actual fun endBefore(document: NativeDocumentSnapshot) = rethrow { query( + js, + dev.gitlive.firebase.firestore.externals.endBefore(document.js) + ).wrapped } + + actual fun endBefore(vararg fieldValues: Any) = rethrow { query( + js, + dev.gitlive.firebase.firestore.externals.endBefore(*fieldValues) + ).wrapped } + + actual fun endAt(document: NativeDocumentSnapshot) = rethrow { query( + js, + dev.gitlive.firebase.firestore.externals.endAt(document.js) + ).wrapped } + + actual fun endAt(vararg fieldValues: Any) = rethrow { query( + js, + dev.gitlive.firebase.firestore.externals.endAt(*fieldValues) + ).wrapped } + + actual val snapshots get() = callbackFlow { + val unsubscribe = rethrow { + onSnapshot( + js, + { trySend(QuerySnapshot(it)) }, + { close(errorToException(it)) } + ) + } + awaitClose { rethrow { unsubscribe() } } + } + + actual fun snapshots(includeMetadataChanges: Boolean) = callbackFlow { + val unsubscribe = rethrow { + onSnapshot( + js, + json("includeMetadataChanges" to includeMetadataChanges), + { trySend(QuerySnapshot(it)) }, + { close(errorToException(it)) } + ) + } + awaitClose { rethrow { unsubscribe() } } + } +} + +private fun Query.get(source: Source) = when (source) { + Source.DEFAULT -> getDocs(this) + Source.CACHE -> getDocsFromCache(this) + Source.SERVER -> getDocsFromServer(this) +} \ No newline at end of file diff --git a/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeTransactionWrapper.kt b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeTransactionWrapper.kt new file mode 100644 index 000000000..9bcabf42d --- /dev/null +++ b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeTransactionWrapper.kt @@ -0,0 +1,57 @@ +package dev.gitlive.firebase.firestore.internal + +import dev.gitlive.firebase.firestore.DocumentReference +import dev.gitlive.firebase.firestore.EncodedFieldPath +import dev.gitlive.firebase.firestore.NativeTransaction +import dev.gitlive.firebase.firestore.externals.Transaction +import dev.gitlive.firebase.firestore.js +import dev.gitlive.firebase.firestore.performUpdate +import dev.gitlive.firebase.firestore.rethrow +import dev.gitlive.firebase.internal.EncodedObject +import dev.gitlive.firebase.internal.js +import kotlinx.coroutines.await + +@PublishedApi +internal actual class NativeTransactionWrapper actual internal constructor(actual val native: NativeTransaction) { + + constructor(js: Transaction) : this(NativeTransaction(js)) + + val js = native.js + + actual fun setEncoded( + documentRef: DocumentReference, + encodedData: EncodedObject, + setOptions: SetOptions + ): NativeTransactionWrapper = rethrow { + js.set(documentRef.js, encodedData.js, setOptions.js) + } + .let { this } + + actual fun updateEncoded(documentRef: DocumentReference, encodedData: EncodedObject): NativeTransactionWrapper = rethrow { js.update(documentRef.js, encodedData.js) } + .let { this } + + actual fun updateEncodedFieldsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ): NativeTransactionWrapper = rethrow { + encodedFieldsAndValues.performUpdate { field, value, moreFieldsAndValues -> + js.update(documentRef.js, field, value, *moreFieldsAndValues) + } + }.let { this } + + actual fun updateEncodedFieldPathsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ): NativeTransactionWrapper = rethrow { + encodedFieldsAndValues.performUpdate { field, value, moreFieldsAndValues -> + js.update(documentRef.js, field, value, *moreFieldsAndValues) + } + }.let { this } + + actual fun delete(documentRef: DocumentReference) = + rethrow { js.delete(documentRef.js) } + .let { this } + + actual suspend fun get(documentRef: DocumentReference) = + rethrow { NativeDocumentSnapshotWrapper(js.get(documentRef.js).await()) } +} \ No newline at end of file diff --git a/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeWriteBatchWrapper.kt b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeWriteBatchWrapper.kt new file mode 100644 index 000000000..819e563d1 --- /dev/null +++ b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/NativeWriteBatchWrapper.kt @@ -0,0 +1,53 @@ +package dev.gitlive.firebase.firestore.internal + +import dev.gitlive.firebase.firestore.DocumentReference +import dev.gitlive.firebase.firestore.EncodedFieldPath +import dev.gitlive.firebase.firestore.NativeWriteBatch +import dev.gitlive.firebase.firestore.externals.WriteBatch +import dev.gitlive.firebase.firestore.js +import dev.gitlive.firebase.firestore.performUpdate +import dev.gitlive.firebase.firestore.rethrow +import dev.gitlive.firebase.internal.EncodedObject +import dev.gitlive.firebase.internal.js +import kotlinx.coroutines.await + +@PublishedApi +internal actual class NativeWriteBatchWrapper actual internal constructor(actual val native: NativeWriteBatch) { + + constructor(js: WriteBatch) : this(NativeWriteBatch(js)) + + val js = native.js + + actual fun setEncoded( + documentRef: DocumentReference, + encodedData: EncodedObject, + setOptions: SetOptions + ): NativeWriteBatchWrapper = rethrow { js.set(documentRef.js, encodedData.js, setOptions.js) }.let { this } + + actual fun updateEncoded(documentRef: DocumentReference, encodedData: EncodedObject): NativeWriteBatchWrapper = rethrow { js.update(documentRef.js, encodedData.js) } + .let { this } + + actual fun updateEncodedFieldsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ): NativeWriteBatchWrapper = rethrow { + encodedFieldsAndValues.performUpdate { field, value, moreFieldsAndValues -> + js.update(documentRef.js, field, value, *moreFieldsAndValues) + } + }.let { this } + + actual fun updateEncodedFieldPathsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ): NativeWriteBatchWrapper = rethrow { + encodedFieldsAndValues.performUpdate { field, value, moreFieldsAndValues -> + js.update(documentRef.js, field, value, *moreFieldsAndValues) + } + }.let { this } + + actual fun delete(documentRef: DocumentReference) = + rethrow { js.delete(documentRef.js) } + .let { this } + + actual suspend fun commit() = rethrow { js.commit().await() } +} \ No newline at end of file diff --git a/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/SetOptions.kt b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/SetOptions.kt new file mode 100644 index 000000000..3857ad03a --- /dev/null +++ b/firebase-firestore/src/wasmJsMain/kotlin/firestore/internal/SetOptions.kt @@ -0,0 +1,12 @@ +package dev.gitlive.firebase.firestore.internal + +import kotlin.js.Json +import kotlin.js.json + +internal val SetOptions.js: Json + get() = when (this) { + is SetOptions.Merge -> json("merge" to true) + is SetOptions.Overwrite -> json("merge" to false) + is SetOptions.MergeFields -> json("mergeFields" to fields.toTypedArray()) + is SetOptions.MergeFieldPaths -> json("mergeFields" to encodedFieldPaths.toTypedArray()) +} diff --git a/firebase-functions/src/wasmJsMain/kotlin/functions/externals/HttpsCallableExt.kt b/firebase-functions/src/wasmJsMain/kotlin/functions/externals/HttpsCallableExt.kt new file mode 100644 index 000000000..ef4d3f6f0 --- /dev/null +++ b/firebase-functions/src/wasmJsMain/kotlin/functions/externals/HttpsCallableExt.kt @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.gitlive.firebase.functions.externals + +import kotlin.js.Promise + +operator fun HttpsCallable.invoke() = asDynamic()() as Promise +operator fun HttpsCallable.invoke(data: Any?) = asDynamic()(data) as Promise diff --git a/firebase-functions/src/wasmJsMain/kotlin/functions/externals/functions.kt b/firebase-functions/src/wasmJsMain/kotlin/functions/externals/functions.kt new file mode 100644 index 000000000..7cf5388c6 --- /dev/null +++ b/firebase-functions/src/wasmJsMain/kotlin/functions/externals/functions.kt @@ -0,0 +1,24 @@ +@file:JsModule("firebase/functions") +@file:JsNonModule + +package dev.gitlive.firebase.functions.externals + +import dev.gitlive.firebase.externals.FirebaseApp +import kotlin.js.Json + +external fun connectFunctionsEmulator(functions: Functions, host: String, port: Int) + +external fun getFunctions( + app: FirebaseApp? = definedExternally, + regionOrCustomDomain: String? = definedExternally +): Functions + +external fun httpsCallable(functions: Functions, name: String, options: Json?): HttpsCallable + +external interface Functions + +external interface HttpsCallableResult { + val data: Any? +} + +external interface HttpsCallable diff --git a/firebase-functions/src/wasmJsMain/kotlin/functions/functions.kt b/firebase-functions/src/wasmJsMain/kotlin/functions/functions.kt new file mode 100644 index 000000000..cc6f8f100 --- /dev/null +++ b/firebase-functions/src/wasmJsMain/kotlin/functions/functions.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.gitlive.firebase.functions + +import dev.gitlive.firebase.DecodeSettings +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseApp +import dev.gitlive.firebase.FirebaseException +import dev.gitlive.firebase.functions.externals.Functions +import dev.gitlive.firebase.functions.externals.HttpsCallable +import dev.gitlive.firebase.functions.externals.connectFunctionsEmulator +import dev.gitlive.firebase.functions.externals.getFunctions +import dev.gitlive.firebase.functions.externals.httpsCallable +import dev.gitlive.firebase.functions.externals.invoke +import dev.gitlive.firebase.internal.decode +import kotlinx.coroutines.await +import kotlinx.serialization.DeserializationStrategy +import kotlin.js.json +import dev.gitlive.firebase.functions.externals.HttpsCallableResult as JsHttpsCallableResult + +actual val Firebase.functions: FirebaseFunctions + get() = rethrow { FirebaseFunctions(getFunctions()) } + +actual fun Firebase.functions(region: String) = + rethrow { FirebaseFunctions(getFunctions(regionOrCustomDomain = region)) } + +actual fun Firebase.functions(app: FirebaseApp) = + rethrow { FirebaseFunctions(getFunctions(app.js)) } + +actual fun Firebase.functions(app: FirebaseApp, region: String) = + rethrow { FirebaseFunctions(getFunctions(app.js, region)) } + +actual class FirebaseFunctions internal constructor(val js: Functions) { + actual fun httpsCallable(name: String, timeout: Long?) = + rethrow { HttpsCallableReference( httpsCallable(js, name, timeout?.let { json("timeout" to timeout.toDouble()) }).native) } + + actual fun useEmulator(host: String, port: Int) = connectFunctionsEmulator(js, host, port) +} + +@PublishedApi +internal actual data class NativeHttpsCallableReference(val js: HttpsCallable) { + actual suspend fun invoke(encodedData: Any): HttpsCallableResult = rethrow { + HttpsCallableResult(js(encodedData).await()) + } + actual suspend fun invoke(): HttpsCallableResult = rethrow { HttpsCallableResult(js().await()) } +} + +internal val HttpsCallable.native get() = NativeHttpsCallableReference(this) + +val HttpsCallableReference.js: HttpsCallable get() = native.js + +actual class HttpsCallableResult(val js: JsHttpsCallableResult) { + + actual inline fun data() = + rethrow { decode(value = js.data) } + + actual inline fun data(strategy: DeserializationStrategy, buildSettings: DecodeSettings.Builder.() -> Unit) = + rethrow { decode(strategy, js.data, buildSettings) } + +} + +actual class FirebaseFunctionsException(cause: Throwable, val code: FunctionsExceptionCode, val details: Any?) : FirebaseException(cause.message, cause) + +actual val FirebaseFunctionsException.code: FunctionsExceptionCode get() = code + +actual val FirebaseFunctionsException.details: Any? get() = details + +actual enum class FunctionsExceptionCode { + OK, + CANCELLED, + UNKNOWN, + INVALID_ARGUMENT, + DEADLINE_EXCEEDED, + NOT_FOUND, + ALREADY_EXISTS, + PERMISSION_DENIED, + RESOURCE_EXHAUSTED, + FAILED_PRECONDITION, + ABORTED, + OUT_OF_RANGE, + UNIMPLEMENTED, + INTERNAL, + UNAVAILABLE, + DATA_LOSS, + UNAUTHENTICATED +} + +inline fun T.rethrow(function: T.() -> R): R = dev.gitlive.firebase.functions.rethrow { function() } + +inline fun rethrow(function: () -> R): R { + try { + return function() + } catch (e: Exception) { + throw e + } catch(e: dynamic) { + throw errorToException(e) + } +} + +fun errorToException(e: dynamic) = (e?.code ?: e?.message ?: "") + .toString() + .lowercase() + .let { + when { + "cancelled" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.CANCELLED, e.details) + "invalid-argument" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.INVALID_ARGUMENT, e.details) + "deadline-exceeded" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.DEADLINE_EXCEEDED, e.details) + "not-found" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.NOT_FOUND, e.details) + "already-exists" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.ALREADY_EXISTS, e.details) + "permission-denied" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.PERMISSION_DENIED, e.details) + "resource-exhausted" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.RESOURCE_EXHAUSTED, e.details) + "failed-precondition" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.FAILED_PRECONDITION, e.details) + "aborted" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.ABORTED, e.details) + "out-of-range" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.OUT_OF_RANGE, e.details) + "unimplemented" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.UNIMPLEMENTED, e.details) + "internal" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.INTERNAL, e.details) + "unavailable" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.UNAVAILABLE, e.details) + "data-loss" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.DATA_LOSS, e.details) + "unauthenticated" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.UNAUTHENTICATED, e.details) + "unknown" in it -> FirebaseFunctionsException(e, FunctionsExceptionCode.UNKNOWN, e.details) + else -> { + println("Unknown error code in ${JSON.stringify(e)}") + FirebaseFunctionsException(e, FunctionsExceptionCode.UNKNOWN, e.details) + } + } + } diff --git a/firebase-installations/src/wasmJsMain/kotlin/installations/externals/installations.kt b/firebase-installations/src/wasmJsMain/kotlin/installations/externals/installations.kt new file mode 100644 index 000000000..c2b236f76 --- /dev/null +++ b/firebase-installations/src/wasmJsMain/kotlin/installations/externals/installations.kt @@ -0,0 +1,17 @@ +@file:JsModule("firebase/installations") +@file:JsNonModule + +package dev.gitlive.firebase.installations.externals + +import dev.gitlive.firebase.externals.FirebaseApp +import kotlin.js.Promise + +external fun delete(installations: Installations): Promise + +external fun getId(installations: Installations): Promise + +external fun getInstallations(app: FirebaseApp? = definedExternally): Installations + +external fun getToken(installations: Installations, forceRefresh: Boolean): Promise + +external interface Installations diff --git a/firebase-installations/src/wasmJsMain/kotlin/installations/installations.kt b/firebase-installations/src/wasmJsMain/kotlin/installations/installations.kt new file mode 100644 index 000000000..b1ddce4e0 --- /dev/null +++ b/firebase-installations/src/wasmJsMain/kotlin/installations/installations.kt @@ -0,0 +1,37 @@ +package dev.gitlive.firebase.installations + +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseApp +import dev.gitlive.firebase.FirebaseException +import dev.gitlive.firebase.installations.externals.* +import kotlinx.coroutines.await + +actual val Firebase.installations + get() = rethrow { FirebaseInstallations(getInstallations()) } + +actual fun Firebase.installations(app: FirebaseApp) = + rethrow { FirebaseInstallations(getInstallations(app.js)) } + +actual class FirebaseInstallations internal constructor(val js: Installations) { + + actual suspend fun delete() = rethrow { delete(js).await() } + + actual suspend fun getId(): String = rethrow { getId(js).await() } + + actual suspend fun getToken(forceRefresh: Boolean): String = + rethrow { getToken(js, forceRefresh).await() } +} + +actual open class FirebaseInstallationsException(code: String?, cause: Throwable): FirebaseException(code, cause) + +inline fun T.rethrow(function: T.() -> R): R = dev.gitlive.firebase.installations.rethrow { function() } + +inline fun rethrow(function: () -> R): R { + try { + return function() + } catch (e: Exception) { + throw e + } catch(e: dynamic) { + throw FirebaseInstallationsException(e.code as String?, e) + } +} diff --git a/firebase-perf/src/wasmJsMain/kotlin/perf/externals/performance.kt b/firebase-perf/src/wasmJsMain/kotlin/perf/externals/performance.kt new file mode 100644 index 000000000..dc6a04d0a --- /dev/null +++ b/firebase-perf/src/wasmJsMain/kotlin/perf/externals/performance.kt @@ -0,0 +1,27 @@ +@file:JsModule("firebase/performance") +@file:JsNonModule + +package dev.gitlive.firebase.perf.externals + +import dev.gitlive.firebase.externals.FirebaseApp + +external fun getPerformance(app: FirebaseApp? = definedExternally): FirebasePerformance + +external fun trace(performance: FirebasePerformance, name: String): PerformanceTrace + +external interface FirebasePerformance { + var dataCollectionEnabled: Boolean + var instrumentationEnabled: Boolean +} + +external interface PerformanceTrace { + fun getAttribute(attr: String): String? + fun getAttributes(): Map + fun getMetric(metricName: String): Int + fun incrementMetric(metricName: String, num: Int? = definedExternally) + fun putAttribute(attr: String, value: String) + fun putMetric(metricName: String, num: Int) + fun removeAttribute(attr: String) + fun start() + fun stop() +} diff --git a/firebase-perf/src/wasmJsMain/kotlin/perf/metrics/Trace.kt b/firebase-perf/src/wasmJsMain/kotlin/perf/metrics/Trace.kt new file mode 100644 index 000000000..2f89a26c7 --- /dev/null +++ b/firebase-perf/src/wasmJsMain/kotlin/perf/metrics/Trace.kt @@ -0,0 +1,26 @@ +package dev.gitlive.firebase.perf.metrics + +import dev.gitlive.firebase.perf.externals.PerformanceTrace +import dev.gitlive.firebase.perf.rethrow + + +actual class Trace internal constructor(private val js: PerformanceTrace) { + + actual fun start() = rethrow { js.start() } + actual fun stop() = rethrow { js.stop() } + actual fun getLongMetric(metricName: String) = rethrow { js.getMetric(metricName).toLong() } + actual fun incrementMetric(metricName: String, incrementBy: Long) = rethrow { js.incrementMetric(metricName, incrementBy.toInt()) } + actual fun putMetric(metricName: String, value: Long) = rethrow { js.putMetric(metricName, value.toInt()) } + fun getAttribute(attribute: String): String? = rethrow { js.getAttribute(attribute) } + fun putAttribute(attribute: String, value: String) = rethrow { js.putAttribute(attribute, value) } + fun removeAttribute(attribute: String) = rethrow { js.removeAttribute(attribute) } + + fun primitiveHashMap(container: dynamic): HashMap { + val m = HashMap().asDynamic() + m.map = container + val keys = js("Object.keys") + m.`$size` = keys(container).length + return m + } + +} diff --git a/firebase-perf/src/wasmJsMain/kotlin/perf/performance.kt b/firebase-perf/src/wasmJsMain/kotlin/perf/performance.kt new file mode 100644 index 000000000..ed27b5001 --- /dev/null +++ b/firebase-perf/src/wasmJsMain/kotlin/perf/performance.kt @@ -0,0 +1,62 @@ +package dev.gitlive.firebase.perf + +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseApp +import dev.gitlive.firebase.FirebaseException +import dev.gitlive.firebase.perf.externals.getPerformance +import dev.gitlive.firebase.perf.externals.trace +import dev.gitlive.firebase.perf.metrics.Trace +import dev.gitlive.firebase.perf.externals.FirebasePerformance as JsFirebasePerformance + +actual val Firebase.performance: FirebasePerformance + get() = rethrow { + FirebasePerformance(getPerformance()) + } + +actual fun Firebase.performance(app: FirebaseApp): FirebasePerformance = rethrow { + FirebasePerformance(getPerformance(app.js)) +} + +actual class FirebasePerformance internal constructor(val js: JsFirebasePerformance) { + + actual fun newTrace(traceName: String): Trace = rethrow { + Trace(trace(js, traceName)) + } + + actual fun isPerformanceCollectionEnabled(): Boolean = js.dataCollectionEnabled + + actual fun setPerformanceCollectionEnabled(enable: Boolean) { + js.dataCollectionEnabled = enable + } + + fun isInstrumentationEnabled(): Boolean = js.instrumentationEnabled + + fun setInstrumentationEnabled(enable: Boolean) { + js.instrumentationEnabled = enable + } +} + +actual open class FirebasePerformanceException(code: String, cause: Throwable) : + FirebaseException(code, cause) + +internal inline fun rethrow(function: () -> R): R { + try { + return function() + } catch (e: Exception) { + throw e + } catch (e: dynamic) { + throw errorToException(e) + } +} + +internal fun errorToException(error: dynamic) = (error?.code ?: error?.message ?: "") + .toString() + .lowercase() + .let { code -> + when { + else -> { + println("Unknown error code in ${JSON.stringify(error)}") + FirebasePerformanceException(code, error) + } + } + } diff --git a/firebase-storage/src/wasmJsMain/kotlin/storage/externals/storage.kt b/firebase-storage/src/wasmJsMain/kotlin/storage/externals/storage.kt new file mode 100644 index 000000000..dbd7768d4 --- /dev/null +++ b/firebase-storage/src/wasmJsMain/kotlin/storage/externals/storage.kt @@ -0,0 +1,67 @@ +@file:JsModule("firebase/storage") +@file:JsNonModule + +package dev.gitlive.firebase.storage.externals + +import dev.gitlive.firebase.externals.FirebaseApp +import kotlin.js.Promise + +external fun getStorage(app: FirebaseApp? = definedExternally): FirebaseStorage + +external fun ref(storage: FirebaseStorage, url: String? = definedExternally): StorageReference +external fun ref(ref: StorageReference, url: String? = definedExternally): StorageReference + +external fun getDownloadURL(ref: StorageReference): Promise + +external fun uploadBytes(ref: StorageReference, file: dynamic): Promise + +external fun uploadBytesResumable(ref: StorageReference, data: dynamic): UploadTask + +external fun deleteObject(ref: StorageReference): Promise; + +external fun listAll(ref: StorageReference): Promise; + +external fun connectFirestoreEmulator( + storage: FirebaseStorage, + host: String, + port: Double, + options: Any? = definedExternally +) + +external interface FirebaseStorage { + var maxOperationRetryTime: Double + var maxUploadRetryTime: Double +} + +external interface StorageReference { + val bucket: String + val fullPath: String + val name: String + val parent: StorageReference? + val root: StorageReference + val storage: FirebaseStorage +} + +external open class ListResult { + val items: Array + val nextPageToken: String + val prefixes: Array +} + +external interface StorageError + +external interface UploadTaskSnapshot { + val bytesTransferred: Number + val ref: StorageReference + val state: String + val task: UploadTask + val totalBytes: Number +} + +external class UploadTask : Promise { + fun cancel(): Boolean; + fun on(event: String, next: (snapshot: UploadTaskSnapshot) -> Unit, error: (a: StorageError) -> Unit, complete: () -> Unit): () -> Unit + fun pause(): Boolean; + fun resume(): Boolean; + val snapshot: UploadTaskSnapshot +} diff --git a/firebase-storage/src/wasmJsMain/kotlin/storage/storage.kt b/firebase-storage/src/wasmJsMain/kotlin/storage/storage.kt new file mode 100644 index 000000000..9ba0380d5 --- /dev/null +++ b/firebase-storage/src/wasmJsMain/kotlin/storage/storage.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2023 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.gitlive.firebase.storage + +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseApp +import dev.gitlive.firebase.FirebaseException +import dev.gitlive.firebase.storage.externals.* +import kotlinx.coroutines.await +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.emitAll + +actual val Firebase.storage + get() = FirebaseStorage(getStorage()) + +actual fun Firebase.storage(app: FirebaseApp) = + FirebaseStorage(getStorage(app.js)) + +actual class FirebaseStorage(val js: dev.gitlive.firebase.storage.externals.FirebaseStorage) { + actual val maxOperationRetryTimeMillis = js.maxOperationRetryTime.toLong() + actual val maxUploadRetryTimeMillis = js.maxUploadRetryTime.toLong() + + actual fun setMaxOperationRetryTimeMillis(maxOperationRetryTimeMillis: Long) { + js.maxOperationRetryTime = maxOperationRetryTimeMillis.toDouble() + } + + actual fun setMaxUploadRetryTimeMillis(maxUploadRetryTimeMillis: Long) { + js.maxUploadRetryTime = maxUploadRetryTimeMillis.toDouble() + } + + actual fun useEmulator(host: String, port: Int) { + connectFirestoreEmulator(js, host, port.toDouble()) + } + + actual val reference: StorageReference get() = StorageReference(ref(js)) + + actual fun reference(location: String) = rethrow { StorageReference(ref(js, location)) } + +} + +actual class StorageReference(val js: dev.gitlive.firebase.storage.externals.StorageReference) { + actual val path: String get() = js.fullPath + actual val name: String get() = js.name + actual val bucket: String get() = js.bucket + actual val parent: StorageReference? get() = js.parent?.let { StorageReference(it) } + actual val root: StorageReference get() = StorageReference(js.root) + actual val storage: FirebaseStorage get() = FirebaseStorage(js.storage) + + actual fun child(path: String): StorageReference = StorageReference(ref(js, path)) + + actual suspend fun delete() = rethrow { deleteObject(js).await() } + + actual suspend fun getDownloadUrl(): String = rethrow { getDownloadURL(js).await().toString() } + + actual suspend fun listAll(): ListResult = rethrow { ListResult(listAll(js).await()) } + + actual suspend fun putFile(file: File): Unit = rethrow { uploadBytes(js, file).await() } + + actual fun putFileResumable(file: File): ProgressFlow = rethrow { + val uploadTask = uploadBytesResumable(js, file) + + val flow = callbackFlow { + val unsubscribe = uploadTask.on( + "state_changed", + { + when(it.state) { + "paused" -> trySend(Progress.Paused(it.bytesTransferred, it.totalBytes)) + "running" -> trySend(Progress.Running(it.bytesTransferred, it.totalBytes)) + "canceled" -> cancel() + "success", "error" -> Unit + else -> TODO("Unknown state ${it.state}") + } + }, + { close(errorToException(it)) }, + { close() } + ) + awaitClose { unsubscribe() } + } + + return object : ProgressFlow { + override suspend fun collect(collector: FlowCollector) = collector.emitAll(flow) + override fun pause() = uploadTask.pause().run {} + override fun resume() = uploadTask.resume().run {} + override fun cancel() = uploadTask.cancel().run {} + } + } + +} + +actual class ListResult(js: dev.gitlive.firebase.storage.externals.ListResult) { + actual val prefixes: List = js.prefixes.map { StorageReference(it) } + actual val items: List = js.items.map { StorageReference(it) } + actual val pageToken: String? = js.nextPageToken +} + +actual typealias File = org.w3c.files.File + +actual open class FirebaseStorageException(code: String, cause: Throwable) : + FirebaseException(code, cause) + +internal inline fun rethrow(function: () -> R): R { + try { + return function() + } catch (e: Exception) { + throw e + } catch (e: dynamic) { + throw errorToException(e) + } +} + +internal fun errorToException(error: dynamic) = (error?.code ?: error?.message ?: "") + .toString() + .lowercase() + .let { code -> + when { + else -> { + println("Unknown error code in ${JSON.stringify(error)}") + FirebaseStorageException(code, error) + } + } + } \ No newline at end of file diff --git a/test-utils/src/wasmJsMain/kotlin/dev/gitlive/firebase/TestUtils.wasmJs.kt b/test-utils/src/wasmJsMain/kotlin/dev/gitlive/firebase/TestUtils.wasmJs.kt new file mode 100644 index 000000000..2cca3d659 --- /dev/null +++ b/test-utils/src/wasmJsMain/kotlin/dev/gitlive/firebase/TestUtils.wasmJs.kt @@ -0,0 +1,19 @@ +package dev.gitlive.firebase + +import kotlinx.coroutines.CoroutineScope + +actual fun nativeAssertEquals(expected: Any?, actual: Any?) { + TODO("Not implemented") +} + +actual fun nativeListOf(vararg elements: Any?): Any { + TODO("Not yet implemented") +} + +actual fun nativeMapOf(vararg pairs: Pair): Any { + TODO("Not yet implemented") +} + +actual fun runBlockingTest(action: suspend CoroutineScope.() -> Unit) { + TODO("Not implemented") +} \ No newline at end of file