Skip to content

Commit

Permalink
Add FhirEngine interface method 'withTransaction' (#2535)
Browse files Browse the repository at this point in the history
* Add FhirEngine interface method 'withTransaction'

To support performing FhirEngine actions as a single atomic transaction

* Only allow FHIREngine CRUD api for withTransaction

* Rename BaseFhirEngine to CrudFhirEngine

* Move count method back to FhirEngine class

* Update 'withTransaction' tests

to better reflect need for atomic transactions

* Revert to using FhirEngine interface

and removed the CRUDEngine interface

* Simplify tests for fhirengine's withTransaction

* Refactor update FhirEngineImplTest

---------

Co-authored-by: Peter Lubell-Doughtie <peter@ona.io>
  • Loading branch information
LZRS and pld authored Oct 29, 2024
1 parent 8f19d0a commit 174f3e0
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 0 deletions.
6 changes: 6 additions & 0 deletions engine/src/main/java/com/google/android/fhir/FhirEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ interface FhirEngine {
* back and no record is purged.
*/
suspend fun purge(type: ResourceType, ids: Set<String>, forcePurge: Boolean = false)

/**
* Adds support for performing actions on `FhirEngine` as a single atomic transaction where the
* entire set of changes succeed or fail as a single entity
*/
suspend fun withTransaction(block: suspend FhirEngine.() -> Unit)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ internal class FhirEngineImpl(private val database: Database, private val contex
}
}

override suspend fun withTransaction(block: suspend FhirEngine.() -> Unit) {
database.withTransaction { this.block() }
}

private suspend fun saveResolvedResourcesToDatabase(resolved: List<Resource>?) {
resolved?.let {
database.deleteUpdates(it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ internal object TestFhirEngineImpl : FhirEngine {
download().collect()
}

override suspend fun withTransaction(block: suspend FhirEngine.() -> Unit) {}

override suspend fun count(search: Search): Long {
return 0
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,16 @@ import kotlinx.coroutines.test.runTest
import org.hl7.fhir.exceptions.FHIRException
import org.hl7.fhir.r4.model.Address
import org.hl7.fhir.r4.model.CanonicalType
import org.hl7.fhir.r4.model.CodeableConcept
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.DateTimeType
import org.hl7.fhir.r4.model.Encounter
import org.hl7.fhir.r4.model.Enumerations
import org.hl7.fhir.r4.model.HumanName
import org.hl7.fhir.r4.model.Meta
import org.hl7.fhir.r4.model.Observation
import org.hl7.fhir.r4.model.Patient
import org.hl7.fhir.r4.model.Reference
import org.hl7.fhir.r4.model.ResourceType
import org.junit.Assert.assertThrows
import org.junit.Before
Expand Down Expand Up @@ -805,6 +809,60 @@ class FhirEngineImplTest {
assertThat(services.database.getLocalChangesCount()).isEqualTo(0)
}

@Test
fun `withTransaction saves changes successfully`() = runTest {
fhirEngine.withTransaction {
val patient01 =
Patient().apply {
id = "patient-01"
gender = Enumerations.AdministrativeGender.FEMALE
}
this.create(patient01)

val patient01Observation =
Observation().apply {
id = "patient-01-observation"
status = Observation.ObservationStatus.FINAL
code = CodeableConcept()
subject = Reference(patient01)
}
this.create(patient01Observation)
}

assertThat(
fhirEngine.get<Patient>("patient-01"),
)
.isNotNull()
assertThat(fhirEngine.get<Observation>("patient-01-observation")).isNotNull()
assertThat(
fhirEngine.get<Observation>("patient-01-observation").subject.reference,
)
.isEqualTo("Patient/patient-01")
}

@Test
fun `withTransaction rolls back changes when an error occurs`() = runTest {
try {
fhirEngine.withTransaction {
val patientEncounter =
Encounter().apply {
id = "enc-01"
status = Encounter.EncounterStatus.FINISHED
class_ = Coding()
}

this.create(patientEncounter)

// An exception will rollback the entire block
this.get(ResourceType.Patient, "non_existent_id") as Patient
}
} catch (_: ResourceNotFoundException) {}

assertThrows(ResourceNotFoundException::class.java) {
runBlocking { fhirEngine.get<Encounter>("enc-01") }
}
}

companion object {
private const val TEST_PATIENT_1_ID = "test_patient_1"
private var TEST_PATIENT_1 =
Expand Down

0 comments on commit 174f3e0

Please sign in to comment.