11package com.powersync.connector.supabase
22
3+ import co.touchlab.kermit.Logger
34import com.powersync.PowerSyncDatabase
45import com.powersync.connectors.PowerSyncBackendConnector
56import com.powersync.connectors.PowerSyncCredentials
67import com.powersync.db.crud.CrudEntry
78import com.powersync.db.crud.UpdateType
89import 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
916import 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
1517import io.github.jan.supabase.postgrest.Postgrest
1618import 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
1723import 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 )
2230public 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 }
0 commit comments