Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 1.0.0-BETA7

* Update supabase connector to use supabase-kt version 3
* Handle postgres error codes in supabase connector

## 1.0.0-BETA6

* Fix Custom Write Checkpoint application logic
Expand Down
2 changes: 1 addition & 1 deletion connectors/supabase/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ kotlin {
api(project(":core"))
implementation(libs.kotlinx.coroutines.core)
implementation(libs.supabase.client)
api(libs.supabase.gotrue)
api(libs.supabase.auth)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,56 @@
package com.powersync.connector.supabase

import co.touchlab.kermit.Logger
import com.powersync.PowerSyncDatabase
import com.powersync.connectors.PowerSyncBackendConnector
import com.powersync.connectors.PowerSyncCredentials
import com.powersync.db.crud.CrudEntry
import com.powersync.db.crud.UpdateType
import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.annotations.SupabaseInternal
import io.github.jan.supabase.auth.Auth
import io.github.jan.supabase.auth.auth
import io.github.jan.supabase.auth.providers.builtin.Email
import io.github.jan.supabase.auth.status.SessionStatus
import io.github.jan.supabase.auth.user.UserSession
import io.github.jan.supabase.createSupabaseClient
import io.github.jan.supabase.gotrue.Auth
import io.github.jan.supabase.gotrue.SessionStatus
import io.github.jan.supabase.gotrue.auth
import io.github.jan.supabase.gotrue.providers.builtin.Email
import io.github.jan.supabase.gotrue.user.UserSession
import io.github.jan.supabase.postgrest.Postgrest
import io.github.jan.supabase.postgrest.from
import io.ktor.client.plugins.HttpSend
import io.ktor.client.plugins.plugin
import io.ktor.client.statement.bodyAsText
import io.ktor.utils.io.InternalAPI
import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.json.Json

/**
* Get a Supabase token to authenticate against the PowerSync instance.
*/
@OptIn(SupabaseInternal::class, InternalAPI::class)
public class SupabaseConnector(
public val supabaseClient: SupabaseClient,
public val powerSyncEndpoint: String,
) : PowerSyncBackendConnector() {
private var errorCode: String? = null

private object PostgresFatalCodes {
// Using Regex patterns for Postgres error codes
private val FATAL_RESPONSE_CODES =
listOf(
// Class 22 — Data Exception
"^22...".toRegex(),
// Class 23 — Integrity Constraint Violation
"^23...".toRegex(),
// INSUFFICIENT PRIVILEGE
"^42501$".toRegex(),
)

fun isFatalError(code: String): Boolean =
FATAL_RESPONSE_CODES.any { pattern ->
pattern.matches(code)
}
}

public constructor(
supabaseUrl: String,
supabaseKey: String,
Expand All @@ -41,6 +69,25 @@ public class SupabaseConnector(
require(
supabaseClient.pluginManager.getPluginOrNull(Postgrest) != null,
) { "The Postgrest plugin must be installed on the Supabase client" }

// This retrieves the error code from the response
// as this is not accessible in the Supabase client RestException
// to handle fatal Postgres errors
supabaseClient.httpClient.httpClient.plugin(HttpSend).intercept { request ->
val resp = execute(request)
val response = resp.response
if (response.status.value == 400) {
val responseText = response.bodyAsText()

try {
val error = Json { coerceInputValues = true }.decodeFromString<Map<String, String?>>(responseText)
errorCode = error["code"]
} catch (e: Exception) {
Logger.e("Failed to parse error response: $e")
}
}
resp
}
}

public suspend fun login(
Expand Down Expand Up @@ -109,6 +156,7 @@ public class SupabaseConnector(
lastEntry = entry

val table = supabaseClient.from(entry.table)

when (entry.op) {
UpdateType.PUT -> {
val data = entry.opData?.toMutableMap() ?: mutableMapOf()
Expand Down Expand Up @@ -136,7 +184,14 @@ public class SupabaseConnector(

transaction.complete(null)
} catch (e: Exception) {
println("Data upload error - retrying last entry: ${lastEntry!!}, $e")
if (errorCode != null && PostgresFatalCodes.isFatalError(errorCode.toString())) {
Logger.e("Data upload error: ${e.message}")
Logger.e("Discarding entry: $lastEntry")
transaction.complete(null)
return
}

Logger.e("Data upload error - retrying last entry: $lastEntry, $e")
throw e
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,30 @@ import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import com.powersync.PowerSyncDatabase
import com.powersync.connector.supabase.SupabaseConnector
import io.github.jan.supabase.gotrue.SessionStatus
import io.github.jan.supabase.auth.status.SessionStatus
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

sealed class AuthState {
data object SignedOut: AuthState()
data object SignedIn: AuthState()
data object SignedOut : AuthState()

data object SignedIn : AuthState()
}

internal class AuthViewModel(
private val supabase: SupabaseConnector,
private val db: PowerSyncDatabase,
private val navController: NavController
): ViewModel() {
private val navController: NavController,
) : ViewModel() {
private val _authState = MutableStateFlow<AuthState>(AuthState.SignedOut)
val authState: StateFlow<AuthState> = _authState
private val _userId = MutableStateFlow<String?>(null)
val userId: StateFlow<String?> = _userId

init {
viewModelScope.launch {
supabase.sessionStatus.collect() {
supabase.sessionStatus.collect {
when (it) {
is SessionStatus.Authenticated -> {
_authState.value = AuthState.SignedIn
Expand All @@ -48,12 +49,18 @@ internal class AuthViewModel(
}
}

suspend fun signIn(email: String, password: String) {
suspend fun signIn(
email: String,
password: String,
) {
supabase.login(email, password)
_authState.value = AuthState.SignedIn
}

suspend fun signUp(email: String, password: String) {
suspend fun signUp(
email: String,
password: String,
) {
supabase.signUp(email, password)
_authState.value = AuthState.SignedIn
}
Expand Down
2 changes: 1 addition & 1 deletion demos/hello-powersync/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ plugins {
alias(projectLibs.plugins.kotlinMultiplatform) apply false
alias(projectLibs.plugins.cocoapods) apply false
alias(libs.plugins.buildKonfig) apply false
}
}
32 changes: 21 additions & 11 deletions demos/hello-powersync/composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
import org.jetbrains.compose.ExperimentalComposeLibrary
import java.util.Properties
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING

plugins {
alias(projectLibs.plugins.kotlinMultiplatform)
Expand Down Expand Up @@ -60,16 +60,25 @@ kotlin {

android {
namespace = "com.powersync.demos"
compileSdk = projectLibs.versions.android.compileSdk.get().toInt()
compileSdk =
projectLibs.versions.android.compileSdk
.get()
.toInt()

sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
sourceSets["main"].res.srcDirs("src/androidMain/res")
sourceSets["main"].resources.srcDirs("src/commonMain/resources")

defaultConfig {
applicationId = "com.powersync.demos"
minSdk = projectLibs.versions.android.minSdk.get().toInt()
targetSdk = projectLibs.versions.android.targetSdk.get().toInt()
minSdk =
projectLibs.versions.android.minSdk
.get()
.toInt()
targetSdk =
projectLibs.versions.android.targetSdk
.get()
.toInt()
versionCode = 1
versionName = "1.0"
}
Expand All @@ -96,13 +105,14 @@ android {
}
}

val localProperties = Properties().apply {
try {
load(rootProject.file("local.properties").reader())
} catch (ignored: java.io.IOException) {
throw Error("local.properties file not found")
val localProperties =
Properties().apply {
try {
load(rootProject.file("local.properties").reader())
} catch (ignored: java.io.IOException) {
throw Error("local.properties file not found")
}
}
}

buildkonfig {
packageName = "com.powersync.demos"
Expand All @@ -124,4 +134,4 @@ buildkonfig {
stringConfigField("SUPABASE_USER_EMAIL")
stringConfigField("SUPABASE_USER_PASSWORD")
}
}
}
6 changes: 1 addition & 5 deletions demos/supabase-todolist/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ kotlin = "2.0.20"
coroutines = "1.8.1"
kotlinx-datetime = "0.5.0"
kotlinx-io = "0.5.4"
ktor = "2.3.12"
ktor = "3.0.1"
uuid = "0.8.2"
buildKonfig = "0.15.1"

supabase = "2.6.1"
junit = "4.13.2"

compose = "1.6.11"
Expand Down Expand Up @@ -51,9 +50,6 @@ ktor-client-contentnegotiation = { module = "io.ktor:ktor-client-content-negotia
ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }

supabase-client = { module = "io.github.jan-tennert.supabase:postgrest-kt", version.ref = "supabase" }
supabase-gotrue = { module = "io.github.jan-tennert.supabase:gotrue-kt", version.ref = "supabase" }

androidx-core = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
Expand Down
31 changes: 21 additions & 10 deletions demos/supabase-todolist/shared/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import java.util.Properties
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
import java.util.Properties

plugins {
alias(libs.plugins.kotlinMultiplatform)
Expand Down Expand Up @@ -63,30 +63,41 @@ kotlin {

android {
namespace = "com.powersync.demos"
compileSdk = libs.versions.android.compileSdk.get().toInt()
compileSdk =
libs.versions.android.compileSdk
.get()
.toInt()
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
sourceSets["main"].res.srcDirs("src/androidMain/res")
sourceSets["main"].resources.srcDirs("src/commonMain/resources")

defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
minSdk =
libs.versions.android.minSdk
.get()
.toInt()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
jvmToolchain(libs.versions.java.get().toInt())
jvmToolchain(
libs.versions.java
.get()
.toInt(),
)
}
}

val localProperties = Properties().apply {
try {
load(rootProject.file("local.properties").reader())
} catch (ignored: java.io.IOException) {
throw Error("local.properties file not found")
val localProperties =
Properties().apply {
try {
load(rootProject.file("local.properties").reader())
} catch (ignored: java.io.IOException) {
throw Error("local.properties file not found")
}
}
}

buildkonfig {
packageName = "com.powersync.demos"
Expand Down
Loading
Loading