@@ -7,24 +7,50 @@ import com.powersync.connectors.PowerSyncCredentials
77import com.powersync.db.crud.CrudEntry
88import com.powersync.db.crud.UpdateType
99import io.github.jan.supabase.SupabaseClient
10+ import io.github.jan.supabase.annotations.SupabaseInternal
1011import io.github.jan.supabase.auth.Auth
1112import io.github.jan.supabase.auth.auth
1213import io.github.jan.supabase.auth.providers.builtin.Email
1314import io.github.jan.supabase.auth.status.SessionStatus
1415import io.github.jan.supabase.auth.user.UserSession
1516import io.github.jan.supabase.createSupabaseClient
16- import io.github.jan.supabase.exceptions.BadRequestRestException
1717import io.github.jan.supabase.postgrest.Postgrest
1818import 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
1923import kotlinx.coroutines.flow.StateFlow
24+ import kotlinx.serialization.json.Json
2025
2126/* *
2227 * Get a Supabase token to authenticate against the PowerSync instance.
2328 */
29+ @OptIn(SupabaseInternal ::class , InternalAPI ::class )
2430public class SupabaseConnector (
2531 public val supabaseClient : SupabaseClient ,
2632 public val powerSyncEndpoint : String ,
2733) : 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+
2854 public constructor (
2955 supabaseUrl: String ,
3056 supabaseKey: String ,
@@ -43,6 +69,25 @@ public class SupabaseConnector(
4369 require(
4470 supabaseClient.pluginManager.getPluginOrNull(Postgrest ) != null ,
4571 ) { " 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+ }
4691 }
4792
4893 public suspend fun login (
@@ -111,6 +156,7 @@ public class SupabaseConnector(
111156 lastEntry = entry
112157
113158 val table = supabaseClient.from(entry.table)
159+
114160 when (entry.op) {
115161 UpdateType .PUT -> {
116162 val data = entry.opData?.toMutableMap() ? : mutableMapOf ()
@@ -138,19 +184,15 @@ public class SupabaseConnector(
138184
139185 transaction.complete(null )
140186 } catch (e: Exception ) {
141- when (e) {
142- is BadRequestRestException -> {
143- if (e.message?.contains(" violates not-null constraint" ) == true ) {
144- Logger .e(" Not-null constraint violation: ${e.message} " )
145- transaction.complete(null )
146- return
147- }
148- }
149- else -> {
150- Logger .e(" Data upload error - retrying last entry: $lastEntry , $e " )
151- throw e
152- }
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
153192 }
193+
194+ Logger .e(" Data upload error - retrying last entry: $lastEntry , $e " )
195+ throw e
154196 }
155197 }
156198}
0 commit comments