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 bb4dfb0241..16553409bb 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt @@ -202,6 +202,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 fe5331bd73..d0a274313a 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 @@ -97,6 +97,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 c99e17be73..5faf188a28 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 @@ -176,6 +176,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 48be1a491b..2864fc721e 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 @@ -48,12 +48,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 @@ -783,6 +787,71 @@ 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 { + val patient01 = + Patient().apply { + id = "patient-01" + gender = Enumerations.AdministrativeGender.FEMALE + } + + fhirEngine.create(patient01) + + try { + fhirEngine.withTransaction { + val patientEncounter = + Encounter().apply { + id = "enc-01" + status = Encounter.EncounterStatus.FINISHED + class_ = Coding() + subject = Reference(patient01) + } + + this.create(patientEncounter) + + // Update encounter to reference non-existent subject to force ResourceNotFoundException + val nonExistentSubject = this.get(ResourceType.Patient, "non_existent_id") as Patient + patientEncounter.subject = Reference(nonExistentSubject) + this.update(patientEncounter) + } + } 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 =