Skip to content

Commit

Permalink
[Quest/Malawi Core] Feature Request: Push Local changes from FHIR Eng…
Browse files Browse the repository at this point in the history
…ine database in case of sync failure (#86)

* [Quest/Malawi Core] Feature Request: Push Local changes from FHIR Engine database in case of sync failure

* Bug fix: Room dependency missing

* Feature request : Run in the background

* Trigger push changes manually

Revert "Trigger push changes manually"

This reverts commit 9bf5af1.

* Trigger push changes manually

* Bug fix

* Visible when FHIR sync worker fails at least 3 times
  • Loading branch information
calmwalija authored Aug 19, 2024
1 parent 815f31b commit a9d4786
Show file tree
Hide file tree
Showing 21 changed files with 998 additions and 0 deletions.
5 changes: 5 additions & 0 deletions android/engine/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,11 @@ dependencies {
debugImplementation("androidx.compose.ui:ui-test-manifest:")
testImplementation("androidx.compose.ui:ui-test-junit4:")

// Room
implementation("androidx.room:room-runtime:2.6.1")
kapt("androidx.room:room-compiler:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")

/**
* This is an SDK Dependency graph bug workaround file HAPI FHIR Dependencies missing at runtime
* after building FHIRCore application module
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2021 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.engine.data.local

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import org.smartregister.fhircore.engine.data.local.localChange.LocalChangeDao
import org.smartregister.fhircore.engine.data.local.localChange.LocalChangeEntity
import org.smartregister.fhircore.engine.data.local.syncAttempt.SyncAttemptTrackerDao
import org.smartregister.fhircore.engine.data.local.syncAttempt.SyncAttemptTrackerEntity

@Database(
version = 2,
entities =
[
LocalChangeEntity::class,
SyncAttemptTrackerEntity::class,
],
)
abstract class TingatheDatabase : RoomDatabase() {

abstract val localChangeDao: LocalChangeDao
abstract val syncAttemptTrackerDao: SyncAttemptTrackerDao

companion object {
fun databaseBuilder(context: Context): Builder<TingatheDatabase> {
return Room.databaseBuilder(context, TingatheDatabase::class.java, "tingathe_db")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2021 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.engine.data.local.localChange

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow

@Dao
abstract class LocalChangeDao {

@Query("SELECT * FROM LocalChangeEntity ORDER BY resourceType")
abstract fun query(): Flow<List<LocalChangeEntity>>

@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun upsert(data: List<LocalChangeEntity>)

@Query("SELECT * FROM LocalChangeEntity WHERE status != 2 ORDER BY type")
abstract suspend fun get(): List<LocalChangeEntity>

@Query("DELETE FROM LocalChangeEntity") abstract suspend fun deleteAll(): Int
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2021 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.engine.data.local.localChange

import androidx.room.Entity
import androidx.room.PrimaryKey
import com.google.android.fhir.LocalChange

@Entity
data class LocalChangeEntity(
val resourceId: String,
val resourceType: String,
val versionId: String? = null,
val type: String,
@PrimaryKey(autoGenerate = false) val payload: String,
val status: Int = 0,
) {
enum class Type(val value: Int) {
INSERT(1), // create a new resource. payload is the entire resource json.
UPDATE(2), // patch. payload is the json patch.
DELETE(3), // delete. payload is empty string.
;

companion object {
fun from(input: String): Type = entries.first { it.name == input }

fun from(input: Int): Type = entries.first { it.value == input }
}
}
}

fun LocalChange.toEntity(): LocalChangeEntity {
return LocalChangeEntity(resourceId, resourceType, versionId, type.name, payload)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2021 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.engine.data.local.localChange

sealed interface LocalChangeStateEvent {
data object Idle : LocalChangeStateEvent

data object Finished : LocalChangeStateEvent

data class Processing(val localChange: LocalChangeEntity) : LocalChangeStateEvent

data class Completed(val localChange: LocalChangeEntity) : LocalChangeStateEvent

data class Failed(val p0: LocalChangeEntity, val p1: Exception) : LocalChangeStateEvent
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2021 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.engine.data.local.localChange

import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import ca.uhn.fhir.parser.IParser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.hl7.fhir.r4.model.Binary
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Resource
import org.smartregister.fhircore.engine.data.local.localChange.LocalChangeEntity.Type.Companion.from

val jsonParser: IParser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser()
const val contentType = "application/json"

suspend fun bundleComponent(
localChangeEntities: List<LocalChangeEntity>,
): List<Bundle.BundleEntryComponent> {
return withContext(Dispatchers.IO) {
val updateLocalChange =
localChangeEntities
.filter { from(it.type) == LocalChangeEntity.Type.UPDATE }
.map { createRequest(it, createPathRequest(it)) }

val insertDeleteLocalChange =
localChangeEntities
.filterNot { from(it.type) == LocalChangeEntity.Type.UPDATE }
.sortedBy { it.versionId }
.map { change ->
createRequest(change, (jsonParser.parseResource(change.payload) as Resource))
}
insertDeleteLocalChange + updateLocalChange
}
}

fun createPathRequest(localChangeEntity: LocalChangeEntity): Binary {
return Binary().apply {
contentType = "application/json-patch+json"
data = localChangeEntity.payload.toByteArray()
}
}

fun createRequest(
localChange: LocalChangeEntity,
resourceToUpload: Resource,
): Bundle.BundleEntryComponent {
return Bundle.BundleEntryComponent().apply {
resource = resourceToUpload
request =
Bundle.BundleEntryRequestComponent().apply {
url = "${localChange.resourceType}/${localChange.resourceId}"
method =
when (from(localChange.type)) {
LocalChangeEntity.Type.INSERT -> Bundle.HTTPVerb.PUT
LocalChangeEntity.Type.UPDATE -> Bundle.HTTPVerb.PATCH
LocalChangeEntity.Type.DELETE -> Bundle.HTTPVerb.DELETE
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2021 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.engine.data.local.localChange.repository

import kotlinx.coroutines.flow.Flow
import org.smartregister.fhircore.engine.data.local.localChange.LocalChangeEntity

interface LocalChangeRepo {
suspend fun queryFHIRLocalChanges()

suspend fun deleteAll()

fun query(): Flow<List<LocalChangeEntity>>

suspend fun get(): List<LocalChangeEntity>

suspend fun upsert(data: List<LocalChangeEntity>)

operator fun invoke(index: Int, localChange: LocalChangeEntity): Flow<LocalChangeIndex>
}
Loading

0 comments on commit a9d4786

Please sign in to comment.