Skip to content

Commit 371e6a0

Browse files
DominicGBauerDominicGBauer
andauthored
chore: update supabase and handle null entry in connector (#77)
* chore: update supabase * chore: update supabase * chore: udpate beta * feat: add check for all postgres errors * docs: change changelog --------- Co-authored-by: DominicGBauer <[email protected]>
1 parent 6d321a9 commit 371e6a0

File tree

11 files changed

+153
-56
lines changed

11 files changed

+153
-56
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 1.0.0-BETA7
4+
5+
* Update supabase connector to use supabase-kt version 3
6+
* Handle postgres error codes in supabase connector
7+
38
## 1.0.0-BETA6
49

510
* Fix Custom Write Checkpoint application logic

connectors/supabase/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ kotlin {
3131
api(project(":core"))
3232
implementation(libs.kotlinx.coroutines.core)
3333
implementation(libs.supabase.client)
34-
api(libs.supabase.gotrue)
34+
api(libs.supabase.auth)
3535
}
3636
}
3737
}

connectors/supabase/src/commonMain/kotlin/com/powersync/connector/supabase/SupabaseConnector.kt

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,56 @@
11
package com.powersync.connector.supabase
22

3+
import co.touchlab.kermit.Logger
34
import com.powersync.PowerSyncDatabase
45
import com.powersync.connectors.PowerSyncBackendConnector
56
import com.powersync.connectors.PowerSyncCredentials
67
import com.powersync.db.crud.CrudEntry
78
import com.powersync.db.crud.UpdateType
89
import io.github.jan.supabase.SupabaseClient
10+
import io.github.jan.supabase.annotations.SupabaseInternal
11+
import io.github.jan.supabase.auth.Auth
12+
import io.github.jan.supabase.auth.auth
13+
import io.github.jan.supabase.auth.providers.builtin.Email
14+
import io.github.jan.supabase.auth.status.SessionStatus
15+
import io.github.jan.supabase.auth.user.UserSession
916
import io.github.jan.supabase.createSupabaseClient
10-
import io.github.jan.supabase.gotrue.Auth
11-
import io.github.jan.supabase.gotrue.SessionStatus
12-
import io.github.jan.supabase.gotrue.auth
13-
import io.github.jan.supabase.gotrue.providers.builtin.Email
14-
import io.github.jan.supabase.gotrue.user.UserSession
1517
import io.github.jan.supabase.postgrest.Postgrest
1618
import io.github.jan.supabase.postgrest.from
19+
import io.ktor.client.plugins.HttpSend
20+
import io.ktor.client.plugins.plugin
21+
import io.ktor.client.statement.bodyAsText
22+
import io.ktor.utils.io.InternalAPI
1723
import kotlinx.coroutines.flow.StateFlow
24+
import kotlinx.serialization.json.Json
1825

1926
/**
2027
* Get a Supabase token to authenticate against the PowerSync instance.
2128
*/
29+
@OptIn(SupabaseInternal::class, InternalAPI::class)
2230
public class SupabaseConnector(
2331
public val supabaseClient: SupabaseClient,
2432
public val powerSyncEndpoint: String,
2533
) : PowerSyncBackendConnector() {
34+
private var errorCode: String? = null
35+
36+
private object PostgresFatalCodes {
37+
// Using Regex patterns for Postgres error codes
38+
private val FATAL_RESPONSE_CODES =
39+
listOf(
40+
// Class 22 — Data Exception
41+
"^22...".toRegex(),
42+
// Class 23 — Integrity Constraint Violation
43+
"^23...".toRegex(),
44+
// INSUFFICIENT PRIVILEGE
45+
"^42501$".toRegex(),
46+
)
47+
48+
fun isFatalError(code: String): Boolean =
49+
FATAL_RESPONSE_CODES.any { pattern ->
50+
pattern.matches(code)
51+
}
52+
}
53+
2654
public constructor(
2755
supabaseUrl: String,
2856
supabaseKey: String,
@@ -41,6 +69,25 @@ public class SupabaseConnector(
4169
require(
4270
supabaseClient.pluginManager.getPluginOrNull(Postgrest) != null,
4371
) { "The Postgrest plugin must be installed on the Supabase client" }
72+
73+
// This retrieves the error code from the response
74+
// as this is not accessible in the Supabase client RestException
75+
// to handle fatal Postgres errors
76+
supabaseClient.httpClient.httpClient.plugin(HttpSend).intercept { request ->
77+
val resp = execute(request)
78+
val response = resp.response
79+
if (response.status.value == 400) {
80+
val responseText = response.bodyAsText()
81+
82+
try {
83+
val error = Json { coerceInputValues = true }.decodeFromString<Map<String, String?>>(responseText)
84+
errorCode = error["code"]
85+
} catch (e: Exception) {
86+
Logger.e("Failed to parse error response: $e")
87+
}
88+
}
89+
resp
90+
}
4491
}
4592

4693
public suspend fun login(
@@ -109,6 +156,7 @@ public class SupabaseConnector(
109156
lastEntry = entry
110157

111158
val table = supabaseClient.from(entry.table)
159+
112160
when (entry.op) {
113161
UpdateType.PUT -> {
114162
val data = entry.opData?.toMutableMap() ?: mutableMapOf()
@@ -136,7 +184,14 @@ public class SupabaseConnector(
136184

137185
transaction.complete(null)
138186
} catch (e: Exception) {
139-
println("Data upload error - retrying last entry: ${lastEntry!!}, $e")
187+
if (errorCode != null && PostgresFatalCodes.isFatalError(errorCode.toString())) {
188+
Logger.e("Data upload error: ${e.message}")
189+
Logger.e("Discarding entry: $lastEntry")
190+
transaction.complete(null)
191+
return
192+
}
193+
194+
Logger.e("Data upload error - retrying last entry: $lastEntry, $e")
140195
throw e
141196
}
142197
}

demos/android-supabase-todolist/app/src/main/java/com/powersync/androidexample/Auth.kt

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,30 @@ import androidx.lifecycle.viewModelScope
55
import co.touchlab.kermit.Logger
66
import com.powersync.PowerSyncDatabase
77
import com.powersync.connector.supabase.SupabaseConnector
8-
import io.github.jan.supabase.gotrue.SessionStatus
8+
import io.github.jan.supabase.auth.status.SessionStatus
99
import kotlinx.coroutines.flow.MutableStateFlow
1010
import kotlinx.coroutines.flow.StateFlow
1111
import kotlinx.coroutines.launch
1212

1313
sealed class AuthState {
14-
data object SignedOut: AuthState()
15-
data object SignedIn: AuthState()
14+
data object SignedOut : AuthState()
15+
16+
data object SignedIn : AuthState()
1617
}
1718

1819
internal class AuthViewModel(
1920
private val supabase: SupabaseConnector,
2021
private val db: PowerSyncDatabase,
21-
private val navController: NavController
22-
): ViewModel() {
22+
private val navController: NavController,
23+
) : ViewModel() {
2324
private val _authState = MutableStateFlow<AuthState>(AuthState.SignedOut)
2425
val authState: StateFlow<AuthState> = _authState
2526
private val _userId = MutableStateFlow<String?>(null)
2627
val userId: StateFlow<String?> = _userId
2728

2829
init {
2930
viewModelScope.launch {
30-
supabase.sessionStatus.collect() {
31+
supabase.sessionStatus.collect {
3132
when (it) {
3233
is SessionStatus.Authenticated -> {
3334
_authState.value = AuthState.SignedIn
@@ -48,12 +49,18 @@ internal class AuthViewModel(
4849
}
4950
}
5051

51-
suspend fun signIn(email: String, password: String) {
52+
suspend fun signIn(
53+
email: String,
54+
password: String,
55+
) {
5256
supabase.login(email, password)
5357
_authState.value = AuthState.SignedIn
5458
}
5559

56-
suspend fun signUp(email: String, password: String) {
60+
suspend fun signUp(
61+
email: String,
62+
password: String,
63+
) {
5764
supabase.signUp(email, password)
5865
_authState.value = AuthState.SignedIn
5966
}

demos/hello-powersync/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ plugins {
66
alias(projectLibs.plugins.kotlinMultiplatform) apply false
77
alias(projectLibs.plugins.cocoapods) apply false
88
alias(libs.plugins.buildKonfig) apply false
9-
}
9+
}

demos/hello-powersync/composeApp/build.gradle.kts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
12
import org.jetbrains.compose.ExperimentalComposeLibrary
23
import java.util.Properties
3-
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
44

55
plugins {
66
alias(projectLibs.plugins.kotlinMultiplatform)
@@ -60,16 +60,25 @@ kotlin {
6060

6161
android {
6262
namespace = "com.powersync.demos"
63-
compileSdk = projectLibs.versions.android.compileSdk.get().toInt()
63+
compileSdk =
64+
projectLibs.versions.android.compileSdk
65+
.get()
66+
.toInt()
6467

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

6972
defaultConfig {
7073
applicationId = "com.powersync.demos"
71-
minSdk = projectLibs.versions.android.minSdk.get().toInt()
72-
targetSdk = projectLibs.versions.android.targetSdk.get().toInt()
74+
minSdk =
75+
projectLibs.versions.android.minSdk
76+
.get()
77+
.toInt()
78+
targetSdk =
79+
projectLibs.versions.android.targetSdk
80+
.get()
81+
.toInt()
7382
versionCode = 1
7483
versionName = "1.0"
7584
}
@@ -96,13 +105,14 @@ android {
96105
}
97106
}
98107

99-
val localProperties = Properties().apply {
100-
try {
101-
load(rootProject.file("local.properties").reader())
102-
} catch (ignored: java.io.IOException) {
103-
throw Error("local.properties file not found")
108+
val localProperties =
109+
Properties().apply {
110+
try {
111+
load(rootProject.file("local.properties").reader())
112+
} catch (ignored: java.io.IOException) {
113+
throw Error("local.properties file not found")
114+
}
104115
}
105-
}
106116

107117
buildkonfig {
108118
packageName = "com.powersync.demos"
@@ -124,4 +134,4 @@ buildkonfig {
124134
stringConfigField("SUPABASE_USER_EMAIL")
125135
stringConfigField("SUPABASE_USER_PASSWORD")
126136
}
127-
}
137+
}

demos/supabase-todolist/gradle/libs.versions.toml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ kotlin = "2.0.20"
1010
coroutines = "1.8.1"
1111
kotlinx-datetime = "0.5.0"
1212
kotlinx-io = "0.5.4"
13-
ktor = "2.3.12"
13+
ktor = "3.0.1"
1414
uuid = "0.8.2"
1515
buildKonfig = "0.15.1"
1616

17-
supabase = "2.6.1"
1817
junit = "4.13.2"
1918

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

54-
supabase-client = { module = "io.github.jan-tennert.supabase:postgrest-kt", version.ref = "supabase" }
55-
supabase-gotrue = { module = "io.github.jan-tennert.supabase:gotrue-kt", version.ref = "supabase" }
56-
5753
androidx-core = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" }
5854
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
5955
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }

demos/supabase-todolist/shared/build.gradle.kts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import java.util.Properties
21
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
2+
import java.util.Properties
33

44
plugins {
55
alias(libs.plugins.kotlinMultiplatform)
@@ -63,30 +63,41 @@ kotlin {
6363

6464
android {
6565
namespace = "com.powersync.demos"
66-
compileSdk = libs.versions.android.compileSdk.get().toInt()
66+
compileSdk =
67+
libs.versions.android.compileSdk
68+
.get()
69+
.toInt()
6770
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
6871
sourceSets["main"].res.srcDirs("src/androidMain/res")
6972
sourceSets["main"].resources.srcDirs("src/commonMain/resources")
7073

7174
defaultConfig {
72-
minSdk = libs.versions.android.minSdk.get().toInt()
75+
minSdk =
76+
libs.versions.android.minSdk
77+
.get()
78+
.toInt()
7379
}
7480
compileOptions {
7581
sourceCompatibility = JavaVersion.VERSION_17
7682
targetCompatibility = JavaVersion.VERSION_17
7783
}
7884
kotlin {
79-
jvmToolchain(libs.versions.java.get().toInt())
85+
jvmToolchain(
86+
libs.versions.java
87+
.get()
88+
.toInt(),
89+
)
8090
}
8191
}
8292

83-
val localProperties = Properties().apply {
84-
try {
85-
load(rootProject.file("local.properties").reader())
86-
} catch (ignored: java.io.IOException) {
87-
throw Error("local.properties file not found")
93+
val localProperties =
94+
Properties().apply {
95+
try {
96+
load(rootProject.file("local.properties").reader())
97+
} catch (ignored: java.io.IOException) {
98+
throw Error("local.properties file not found")
99+
}
88100
}
89-
}
90101

91102
buildkonfig {
92103
packageName = "com.powersync.demos"

0 commit comments

Comments
 (0)