fix: crash SQLiteConstraintException lors de la sync Bonsai
- BonsaiApiClient : optLong/optString avec fallback pour éviter JSONException si l'API omet projectId, type, status ou priority dans la réponse - BonsaiSyncManager : enveloppe sync() dans runCatching pour ne jamais propager d'exception non gérée ; chaque insert de tâche est aussi isolé pour que les erreurs individuelles ne bloquent pas toute la sync Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ android {
|
|||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "0.0.11"
|
versionName = "0.0.12"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class BonsaiApiClient @Inject constructor(
|
|||||||
val o = arr.getJSONObject(i)
|
val o = arr.getJSONObject(i)
|
||||||
BonsaiIssueDto(
|
BonsaiIssueDto(
|
||||||
id = o.getLong("id"),
|
id = o.getLong("id"),
|
||||||
projectId = o.getLong("projectId"),
|
projectId = o.optLong("projectId", projectId), // fallback = the project we queried
|
||||||
type = o.getString("type"),
|
type = o.getString("type"),
|
||||||
name = o.getString("name"),
|
name = o.getString("name"),
|
||||||
status = o.getString("status"),
|
status = o.getString("status"),
|
||||||
@@ -71,12 +71,12 @@ class BonsaiApiClient @Inject constructor(
|
|||||||
|
|
||||||
suspend fun createIssue(projectId: Long, req: BonsaIssueRequest): ApiResult<BonsaiIssueDto> =
|
suspend fun createIssue(projectId: Long, req: BonsaIssueRequest): ApiResult<BonsaiIssueDto> =
|
||||||
post("projects/$projectId/issues", req.toJson()) { o ->
|
post("projects/$projectId/issues", req.toJson()) { o ->
|
||||||
o.toIssueDto()
|
o.toIssueDto(fallbackProjectId = projectId)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateIssue(projectId: Long, issueId: Long, req: BonsaIssueRequest): ApiResult<BonsaiIssueDto> =
|
suspend fun updateIssue(projectId: Long, issueId: Long, req: BonsaIssueRequest): ApiResult<BonsaiIssueDto> =
|
||||||
put("projects/$projectId/issues/$issueId", req.toJson()) { o ->
|
put("projects/$projectId/issues/$issueId", req.toJson()) { o ->
|
||||||
o.toIssueDto()
|
o.toIssueDto(fallbackProjectId = projectId)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteIssue(projectId: Long, issueId: Long): ApiResult<Unit> = withContext(Dispatchers.IO) {
|
suspend fun deleteIssue(projectId: Long, issueId: Long): ApiResult<Unit> = withContext(Dispatchers.IO) {
|
||||||
@@ -146,13 +146,13 @@ class BonsaiApiClient @Inject constructor(
|
|||||||
description?.let { put("description", it) }
|
description?.let { put("description", it) }
|
||||||
}.toString()
|
}.toString()
|
||||||
|
|
||||||
private fun JSONObject.toIssueDto() = BonsaiIssueDto(
|
private fun JSONObject.toIssueDto(fallbackProjectId: Long = 0L) = BonsaiIssueDto(
|
||||||
id = getLong("id"),
|
id = getLong("id"),
|
||||||
projectId = getLong("projectId"),
|
projectId = optLong("projectId", fallbackProjectId),
|
||||||
type = getString("type"),
|
type = optString("type", "Task"),
|
||||||
name = getString("name"),
|
name = getString("name"),
|
||||||
status = getString("status"),
|
status = optString("status", "todo"),
|
||||||
priority = getString("priority"),
|
priority = optString("priority", "MOYENNE"),
|
||||||
assignee = optString("assignee").takeIf { it.isNotEmpty() },
|
assignee = optString("assignee").takeIf { it.isNotEmpty() },
|
||||||
dueDate = optString("dueDate").takeIf { it.isNotEmpty() },
|
dueDate = optString("dueDate").takeIf { it.isNotEmpty() },
|
||||||
description = optString("description").takeIf { it.isNotEmpty() },
|
description = optString("description").takeIf { it.isNotEmpty() },
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ class BonsaiSyncManager @Inject constructor(
|
|||||||
private val taskRepository: TaskRepository,
|
private val taskRepository: TaskRepository,
|
||||||
private val labelRepository: LabelRepository,
|
private val labelRepository: LabelRepository,
|
||||||
) {
|
) {
|
||||||
suspend fun sync(): SyncResult {
|
suspend fun sync(): SyncResult = runCatching { doSync() }
|
||||||
|
.getOrElse { SyncResult.Failure(it.message ?: "Erreur interne") }
|
||||||
|
|
||||||
|
private suspend fun doSync(): SyncResult {
|
||||||
if (!authManager.isLoggedIn) return SyncResult.NotLoggedIn
|
if (!authManager.isLoggedIn) return SyncResult.NotLoggedIn
|
||||||
|
|
||||||
val projectsResult = apiClient.getProjects()
|
val projectsResult = apiClient.getProjects()
|
||||||
@@ -89,12 +92,15 @@ class BonsaiSyncManager @Inject constructor(
|
|||||||
|
|
||||||
// Sync tasks (issues of type Task)
|
// Sync tasks (issues of type Task)
|
||||||
allTaskIssues.forEach { issue ->
|
allTaskIssues.forEach { issue ->
|
||||||
|
runCatching {
|
||||||
val labels = milestonesByIssueId[issue.id] ?: emptyList()
|
val labels = milestonesByIssueId[issue.id] ?: emptyList()
|
||||||
val task = issue.toTask(labels, now)
|
val task = issue.toTask(labels, now)
|
||||||
val existing = taskRepository.getTaskById(task.id)
|
val existing = taskRepository.getTaskById(task.id)
|
||||||
if (existing == null) taskRepository.insertTask(task)
|
if (existing == null) taskRepository.insertTask(task)
|
||||||
else taskRepository.updateTask(task)
|
else taskRepository.updateTask(task)
|
||||||
}
|
}
|
||||||
|
// Individual task failures are skipped; the next sync will retry.
|
||||||
|
}
|
||||||
|
|
||||||
return SyncResult.Success
|
return SyncResult.Success
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user