diff --git a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt index d03645c530..4b7ff2cb45 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt @@ -206,6 +206,12 @@ interface FhirEngine { * back and no record is purged. */ suspend fun purge(type: ResourceType, ids: Set, 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) } /** diff --git a/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt b/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt index faef0c90ac..595f433d9d 100644 --- a/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt +++ b/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt @@ -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?) { resolved?.let { database.deleteUpdates(it) diff --git a/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt b/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt index 0c5f80a1bf..1b85a71382 100644 --- a/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt +++ b/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt @@ -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 } diff --git a/engine/src/test/java/com/google/android/fhir/impl/FhirEngineImplTest.kt b/engine/src/test/java/com/google/android/fhir/impl/FhirEngineImplTest.kt index e0330514db..1d46c67aa8 100644 --- a/engine/src/test/java/com/google/android/fhir/impl/FhirEngineImplTest.kt +++ b/engine/src/test/java/com/google/android/fhir/impl/FhirEngineImplTest.kt @@ -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 @@ -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-01"), + ) + .isNotNull() + assertThat(fhirEngine.get("patient-01-observation")).isNotNull() + assertThat( + fhirEngine.get("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("enc-01") } + } + } + companion object { private const val TEST_PATIENT_1_ID = "test_patient_1" private var TEST_PATIENT_1 =