diff --git a/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt b/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt index a5338bef1d..2e6055fd82 100644 --- a/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt +++ b/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt @@ -39,6 +39,7 @@ import com.google.android.fhir.search.getQuery import com.google.android.fhir.search.has import com.google.android.fhir.search.include import com.google.android.fhir.search.revInclude +import com.google.android.fhir.sync.upload.LocalChangesFetchMode import com.google.android.fhir.testing.assertJsonArrayEqualsIgnoringOrder import com.google.android.fhir.testing.assertResourceEquals import com.google.android.fhir.testing.readFromFile @@ -90,9 +91,9 @@ import org.junit.runners.Parameterized.Parameters * Integration tests for [DatabaseImpl]. There are written as integration tests as officially * recommend because: * * Different versions of android are shipped with different versions of SQLite. Integration tests - * allow for better coverage on them. + * allow for better coverage on them. * * Robolectric's SQLite implementation does not match Android, e.g.: - * https://github.com/robolectric/robolectric/blob/master/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSQLiteConnection.java#L97 + * https://github.com/robolectric/robolectric/blob/master/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSQLiteConnection.java#L97 */ @MediumTest @RunWith(Parameterized::class) @@ -267,7 +268,7 @@ class DatabaseImplTest { } assertThat(resourceNotFoundException.message) .isEqualTo( - "Resource not found with type ${TEST_PATIENT_1.resourceType.name} and id $TEST_PATIENT_1_ID!" + "Resource not found with type ${TEST_PATIENT_1.resourceType.name} and id $TEST_PATIENT_1_ID!", ) assertThat(database.getLocalChanges(ResourceType.Patient, TEST_PATIENT_1_ID)).isEmpty() } @@ -280,7 +281,7 @@ class DatabaseImplTest { } assertThat(resourceIllegalStateException.message) .isEqualTo( - "Resource with type ${TEST_PATIENT_1.resourceType.name} and id $TEST_PATIENT_1_ID has local changes, either sync with server or FORCE_PURGE required" + "Resource with type ${TEST_PATIENT_1.resourceType.name} and id $TEST_PATIENT_1_ID has local changes, either sync with server or FORCE_PURGE required", ) } @@ -326,7 +327,7 @@ class DatabaseImplTest { } assertThat(resourceNotFoundException.message) .isEqualTo( - "Resource not found with type ${TEST_PATIENT_1.resourceType.name} and id $TEST_PATIENT_2_ID!" + "Resource not found with type ${TEST_PATIENT_1.resourceType.name} and id $TEST_PATIENT_2_ID!", ) } @@ -336,12 +337,10 @@ class DatabaseImplTest { assertThrows(ResourceNotFoundException::class.java) { runBlocking { database.update(TEST_PATIENT_2) } } - /* ktlint-disable max-line-length */ assertThat(resourceNotFoundException.message) .isEqualTo( - "Resource not found with type ${TEST_PATIENT_2.resourceType.name} and id $TEST_PATIENT_2_ID!" - /* ktlint-enable max-line-length */ - ) + "Resource not found with type ${TEST_PATIENT_2.resourceType.name} and id $TEST_PATIENT_2_ID!", + ) } @Test @@ -383,7 +382,7 @@ class DatabaseImplTest { HumanName().apply { family = "FamilyName" addGiven("FirstName") - } + }, ) meta = Meta().apply { @@ -401,7 +400,7 @@ class DatabaseImplTest { HumanName().apply { family = "UpdatedFamilyName" addGiven("UpdatedFirstName") - } + }, ) } database.update(updatedPatient) @@ -438,7 +437,7 @@ class DatabaseImplTest { database .getAllLocalChanges() .map { it } - .none { it.type == LocalChange.Type.DELETE && it.resourceId == "nonexistent_patient" } + .none { it.type == LocalChange.Type.DELETE && it.resourceId == "nonexistent_patient" }, ) .isTrue() } @@ -512,7 +511,9 @@ class DatabaseImplTest { lastUpdated = Date() } database.insert(patient) - services.fhirEngine.syncUpload { it -> + // Delete the patient created in setup as we only want to upload the patient in this test + database.deleteUpdates(listOf(TEST_PATIENT_1)) + services.fhirEngine.syncUpload(LocalChangesFetchMode.AllChanges) { it .first { it.resourceId == "remote-patient-3" } .let { @@ -521,7 +522,7 @@ class DatabaseImplTest { Patient().apply { id = it.resourceId meta = remoteMeta - } + }, ) } } @@ -538,7 +539,7 @@ class DatabaseImplTest { database .getAllLocalChanges() .map { it } - .none { it.resourceId in listOf(patient.logicalId, TEST_PATIENT_2_ID) } + .none { it.resourceId in listOf(patient.logicalId, TEST_PATIENT_2_ID) }, ) .isTrue() } @@ -552,14 +553,14 @@ class DatabaseImplTest { HumanName().apply { addGiven("Jane") family = "Doe" - } + }, ) } database.insert(patient) val result = database.search( - Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "Jane" }) }.getQuery() + Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "Jane" }) }.getQuery(), ) assertThat(result.size).isEqualTo(1) @@ -570,14 +571,14 @@ class DatabaseImplTest { HumanName().apply { addGiven("John") family = "Doe" - } + }, ) } database.insert(updatedPatient) val updatedResult = database.search( - Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "Jane" }) }.getQuery() + Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "Jane" }) }.getQuery(), ) assertThat(updatedResult.size).isEqualTo(0) } @@ -591,14 +592,14 @@ class DatabaseImplTest { HumanName().apply { addGiven("Jane") family = "Doe" - } + }, ) } database.insertRemote(patient) val result = database.search( - Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "Jane" }) }.getQuery() + Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "Jane" }) }.getQuery(), ) assertThat(result.size).isEqualTo(1) @@ -609,14 +610,14 @@ class DatabaseImplTest { HumanName().apply { addGiven("John") family = "Doe" - } + }, ) } database.insertRemote(updatedPatient) val updatedResult = database.search( - Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "Jane" }) }.getQuery() + Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "Jane" }) }.getQuery(), ) assertThat(updatedResult.size).isEqualTo(0) } @@ -648,14 +649,14 @@ class DatabaseImplTest { HumanName().apply { addGiven("Jane") family = "Doe" - } + }, ) } database.insertRemote(patient) val result = database.search( - Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "Jane" }) }.getQuery() + Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "Jane" }) }.getQuery(), ) assertThat(result.size).isEqualTo(1) @@ -666,14 +667,14 @@ class DatabaseImplTest { HumanName().apply { addGiven("John") family = "Doe" - } + }, ) } database.update(updatedPatient) val updatedResult = database.search( - Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "Jane" }) }.getQuery() + Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "Jane" }) }.getQuery(), ) assertThat(updatedResult.size).isEqualTo(0) } @@ -694,20 +695,37 @@ class DatabaseImplTest { } } + @Test + fun getLocalChangesCount_noLocalChange_returnsZero() = runBlocking { + database.deleteUpdates(listOf(TEST_PATIENT_1)) + assertThat(database.getLocalChangesCount()).isEqualTo(0) + } + + @Test + fun getLocalChangesCount_oneLocalChange_returnsOne() = runBlocking { + assertThat(database.getLocalChangesCount()).isEqualTo(1) + } + + @Test + fun getLocalChangesCount_twoLocalChange_returnsTwo() = runBlocking { + database.insert(TEST_PATIENT_2) + assertThat(database.getLocalChangesCount()).isEqualTo(2) + } + @Test fun search_sortDescending_twoVeryCloseFloatingPointNumbers_orderedCorrectly() = runBlocking { val smallerId = "risk_assessment_1" val largerId = "risk_assessment_2" database.insert( riskAssessment(id = smallerId, probability = BigDecimal("0.3")), - riskAssessment(id = largerId, probability = BigDecimal("0.30000000001")) + riskAssessment(id = largerId, probability = BigDecimal("0.30000000001")), ) val results = database.search( Search(ResourceType.RiskAssessment) .apply { sort(RiskAssessment.PROBABILITY, Order.DESCENDING) } - .getQuery() + .getQuery(), ) val ids = results.map { it.id } @@ -723,7 +741,7 @@ class DatabaseImplTest { listOf( RiskAssessment.RiskAssessmentPredictionComponent().apply { setProbability(DecimalType(probability)) - } + }, ) } @@ -737,7 +755,7 @@ class DatabaseImplTest { database.insert(patient) val result = database.search( - Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "eve" }) }.getQuery() + Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "eve" }) }.getQuery(), ) assertThat(result.single().id).isEqualTo("Patient/${patient.id}") @@ -753,7 +771,7 @@ class DatabaseImplTest { database.insert(patient) val result = database.search( - Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "eve" }) }.getQuery() + Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "eve" }) }.getQuery(), ) assertThat(result).isEmpty() @@ -776,10 +794,10 @@ class DatabaseImplTest { { value = "Eve" modifier = StringFilterModifier.MATCHES_EXACTLY - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Patient/${patient.id}") @@ -802,10 +820,10 @@ class DatabaseImplTest { { value = "Eve" modifier = StringFilterModifier.MATCHES_EXACTLY - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() @@ -829,10 +847,10 @@ class DatabaseImplTest { { value = "Eve" modifier = StringFilterModifier.CONTAINS - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Patient/${patient.id}") @@ -855,10 +873,10 @@ class DatabaseImplTest { { value = "eve" modifier = StringFilterModifier.CONTAINS - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() @@ -870,7 +888,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)), ) } @@ -884,10 +902,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.EQUAL value = BigDecimal("100") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") @@ -899,7 +917,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(100.5)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(100.5)), ) } @@ -913,10 +931,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.EQUAL value = BigDecimal("100") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() @@ -928,7 +946,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.0)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.0)), ) } @@ -942,10 +960,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.NOT_EQUAL value = BigDecimal("100") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") } @@ -956,7 +974,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)), ) } @@ -970,10 +988,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.NOT_EQUAL value = BigDecimal("100") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() @@ -985,7 +1003,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(100)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(100)), ) } @@ -999,10 +1017,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.GREATERTHAN value = BigDecimal("99.5") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") @@ -1014,7 +1032,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)), ) } @@ -1028,10 +1046,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.GREATERTHAN value = BigDecimal("99.5") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() @@ -1043,7 +1061,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)), ) } @@ -1057,10 +1075,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS value = BigDecimal("99.5") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") @@ -1072,7 +1090,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.0)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.0)), ) } @@ -1086,10 +1104,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS value = BigDecimal("99.5") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() @@ -1101,7 +1119,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.0)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.0)), ) } @@ -1115,10 +1133,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.LESSTHAN value = BigDecimal("99.5") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") @@ -1130,7 +1148,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)), ) } @@ -1144,10 +1162,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.LESSTHAN value = BigDecimal("99.5") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() @@ -1159,7 +1177,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)), ) } @@ -1173,10 +1191,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS value = BigDecimal("99.5") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") } @@ -1187,7 +1205,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(100)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(100)), ) } @@ -1201,10 +1219,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS value = BigDecimal("99.5") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() @@ -1216,7 +1234,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.0)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.0)), ) } @@ -1230,10 +1248,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.ENDS_BEFORE value = BigDecimal("99.5") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") @@ -1245,7 +1263,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)), ) } @@ -1259,10 +1277,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.ENDS_BEFORE value = BigDecimal("99.5") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() @@ -1274,7 +1292,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(100)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(100)), ) } @@ -1288,10 +1306,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.STARTS_AFTER value = BigDecimal("99.5") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") @@ -1303,7 +1321,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(99.5)), ) } @@ -1317,10 +1335,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.STARTS_AFTER value = BigDecimal("99.5") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() @@ -1332,7 +1350,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(93)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(93)), ) } @@ -1346,10 +1364,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.APPROXIMATE value = BigDecimal("100") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") @@ -1361,7 +1379,7 @@ class DatabaseImplTest { RiskAssessment().apply { id = "1" addPrediction( - RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(120)) + RiskAssessment.RiskAssessmentPredictionComponent().setProbability(DecimalType(120)), ) } @@ -1375,10 +1393,10 @@ class DatabaseImplTest { { prefix = ParamPrefixEnum.APPROXIMATE value = BigDecimal("100") - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() @@ -1402,10 +1420,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.APPROXIMATE - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Patient/1") } @@ -1428,10 +1446,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2020-03-14")) prefix = ParamPrefixEnum.APPROXIMATE - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -1454,10 +1472,10 @@ class DatabaseImplTest { { value = of(DateType("2013-03-14")) prefix = ParamPrefixEnum.APPROXIMATE - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Patient/1") } @@ -1480,10 +1498,10 @@ class DatabaseImplTest { { value = of(DateType("2020-03-14")) prefix = ParamPrefixEnum.APPROXIMATE - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -1505,10 +1523,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.STARTS_AFTER - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Patient/1") } @@ -1530,10 +1548,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.STARTS_AFTER - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -1555,10 +1573,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.ENDS_BEFORE - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Patient/1") } @@ -1580,10 +1598,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.ENDS_BEFORE - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -1605,10 +1623,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.NOT_EQUAL - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Patient/1") } @@ -1630,10 +1648,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.NOT_EQUAL - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -1655,10 +1673,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.EQUAL - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Patient/1") } @@ -1680,10 +1698,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.EQUAL - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -1705,10 +1723,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.GREATERTHAN - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Patient/1") } @@ -1730,10 +1748,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.GREATERTHAN - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -1755,10 +1773,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Patient/1") } @@ -1780,10 +1798,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -1805,10 +1823,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.LESSTHAN - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Patient/1") } @@ -1830,10 +1848,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.LESSTHAN - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -1855,10 +1873,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14")) prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Patient/1") } @@ -1880,10 +1898,10 @@ class DatabaseImplTest { { value = of(DateTimeType("2013-03-14T00:00:00-00:00")) prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -1912,10 +1930,10 @@ class DatabaseImplTest { value = BigDecimal("5.403") system = "http://unitsofmeasure.org" unit = "g" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Observation/1") } @@ -1944,10 +1962,10 @@ class DatabaseImplTest { value = BigDecimal("5.403") system = "http://unitsofmeasure.org" unit = "g" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -1976,10 +1994,10 @@ class DatabaseImplTest { value = BigDecimal("5.403") system = "http://unitsofmeasure.org" unit = "g" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Observation/1") } @@ -2008,10 +2026,10 @@ class DatabaseImplTest { value = BigDecimal("5.403") system = "http://unitsofmeasure.org" unit = "g" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -2040,10 +2058,10 @@ class DatabaseImplTest { value = BigDecimal("5.403") system = "http://unitsofmeasure.org" unit = "g" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Observation/1") } @@ -2072,10 +2090,10 @@ class DatabaseImplTest { value = BigDecimal("5.403") system = "http://unitsofmeasure.org" unit = "g" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -2104,10 +2122,10 @@ class DatabaseImplTest { value = BigDecimal("5.403") system = "http://unitsofmeasure.org" unit = "g" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Observation/1") } @@ -2136,10 +2154,10 @@ class DatabaseImplTest { value = BigDecimal("5.403") system = "http://unitsofmeasure.org" unit = "g" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -2168,10 +2186,10 @@ class DatabaseImplTest { value = BigDecimal("5.403") system = "http://unitsofmeasure.org" unit = "g" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Observation/1") } @@ -2200,10 +2218,10 @@ class DatabaseImplTest { value = BigDecimal("5.403") system = "http://unitsofmeasure.org" unit = "g" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -2232,10 +2250,10 @@ class DatabaseImplTest { value = BigDecimal("5.403") system = "http://unitsofmeasure.org" unit = "g" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Observation/1") } @@ -2264,10 +2282,10 @@ class DatabaseImplTest { value = BigDecimal("5.403") system = "http://unitsofmeasure.org" unit = "g" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -2296,10 +2314,10 @@ class DatabaseImplTest { value = BigDecimal("5.403") system = "http://unitsofmeasure.org" unit = "g" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Observation/1") } @@ -2328,10 +2346,10 @@ class DatabaseImplTest { value = BigDecimal("5.403") system = "http://unitsofmeasure.org" unit = "g" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result).isEmpty() } @@ -2360,17 +2378,17 @@ class DatabaseImplTest { value = BigDecimal("5403") system = "http://unitsofmeasure.org" unit = "mg" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.single().id).isEqualTo("Observation/1") } @Test fun search_nameGivenDuplicate_deduplicatePatient() = runBlocking { - var patient: Patient = readFromFile(Patient::class.java, "/patient_name_given_duplicate.json") + val patient: Patient = readFromFile(Patient::class.java, "/patient_name_given_duplicate.json") database.insertRemote(patient) val result = database.search( @@ -2380,7 +2398,7 @@ class DatabaseImplTest { count = 100 from = 0 } - .getQuery() + .getQuery(), ) assertThat(result.filter { it.id == patient.id }).hasSize(1) } @@ -2401,8 +2419,8 @@ class DatabaseImplTest { Coding( "http://hl7.org/fhir/sid/cvx", "140", - "Influenza, seasonal, injectable, preservative free" - ) + "Influenza, seasonal, injectable, preservative free", + ), ) status = Immunization.ImmunizationStatus.COMPLETED } @@ -2420,10 +2438,10 @@ class DatabaseImplTest { Coding( "http://hl7.org/fhir/sid/cvx", "140", - "Influenza, seasonal, injectable, preservative free" - ) + "Influenza, seasonal, injectable, preservative free", + ), ) - } + }, ) // Follow Immunization.ImmunizationStatus @@ -2431,7 +2449,7 @@ class DatabaseImplTest { Immunization.STATUS, { value = of(Coding("http://hl7.org/fhir/event-status", "completed", "Body Weight")) - } + }, ) } @@ -2440,10 +2458,10 @@ class DatabaseImplTest { { modifier = StringFilterModifier.MATCHES_EXACTLY value = "IN" - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.map { it.logicalId }).containsExactly("100").inOrder() } @@ -2464,8 +2482,8 @@ class DatabaseImplTest { category = listOf( CodeableConcept( - Coding("http://snomed.info/sct", "698360004", "Diabetes self management plan") - ) + Coding("http://snomed.info/sct", "698360004", "Diabetes self management plan"), + ), ) } database.insert(patient, TEST_PATIENT_1, carePlan) @@ -2479,13 +2497,17 @@ class DatabaseImplTest { { value = of( - Coding("http://snomed.info/sct", "698360004", "Diabetes self management plan") + Coding( + "http://snomed.info/sct", + "698360004", + "Diabetes self management plan", + ), ) - } + }, ) } } - .getQuery() + .getQuery(), ) assertThat(result.map { it.logicalId }).containsExactly("100").inOrder() } @@ -2496,14 +2518,14 @@ class DatabaseImplTest { Patient().apply { id = "older-patient" birthDateElement = DateType("2020-12-12") - } + }, ) database.insert( Patient().apply { id = "younger-patient" birthDateElement = DateType("2020-12-13") - } + }, ) assertThat( @@ -2511,9 +2533,9 @@ class DatabaseImplTest { .search( Search(ResourceType.Patient) .apply { sort(Patient.BIRTHDATE, Order.DESCENDING) } - .getQuery() + .getQuery(), ) - .map { it.id } + .map { it.id }, ) .containsExactly("Patient/younger-patient", "Patient/older-patient", "Patient/test_patient_1") } @@ -2524,14 +2546,14 @@ class DatabaseImplTest { Patient().apply { id = "older-patient" birthDateElement = DateType("2020-12-12") - } + }, ) database.insert( Patient().apply { id = "younger-patient" birthDateElement = DateType("2020-12-13") - } + }, ) assertThat( @@ -2539,9 +2561,9 @@ class DatabaseImplTest { .search( Search(ResourceType.Patient) .apply { sort(Patient.BIRTHDATE, Order.ASCENDING) } - .getQuery() + .getQuery(), ) - .map { it.id } + .map { it.id }, ) .containsExactly("Patient/test_patient_1", "Patient/older-patient", "Patient/younger-patient") } @@ -2554,7 +2576,11 @@ class DatabaseImplTest { id = "immunization-1" vaccineCode = CodeableConcept( - Coding("http://id.who.int/icd11/mms", "XM1NL1", "COVID-19 vaccine, inactivated virus") + Coding( + "http://id.who.int/icd11/mms", + "XM1NL1", + "COVID-19 vaccine, inactivated virus", + ), ) status = Immunization.ImmunizationStatus.COMPLETED }, @@ -2565,8 +2591,8 @@ class DatabaseImplTest { Coding( "http://id.who.int/icd11/mms", "XM5DF6", - "COVID-19 vaccine, live attenuated virus" - ) + "COVID-19 vaccine, live attenuated virus", + ), ) status = Immunization.ImmunizationStatus.COMPLETED }, @@ -2574,7 +2600,7 @@ class DatabaseImplTest { id = "immunization-3" vaccineCode = CodeableConcept( - Coding("http://id.who.int/icd11/mms", "XM6AT1", "COVID-19 vaccine, DNA based") + Coding("http://id.who.int/icd11/mms", "XM6AT1", "COVID-19 vaccine, DNA based"), ) status = Immunization.ImmunizationStatus.COMPLETED }, @@ -2585,11 +2611,11 @@ class DatabaseImplTest { Coding( "http://hl7.org/fhir/sid/cvx", "140", - "Influenza, seasonal, injectable, preservative free" - ) + "Influenza, seasonal, injectable, preservative free", + ), ) status = Immunization.ImmunizationStatus.COMPLETED - } + }, ) database.insert(*resources.toTypedArray()) @@ -2606,8 +2632,8 @@ class DatabaseImplTest { Coding( "http://id.who.int/icd11/mms", "XM1NL1", - "COVID-19 vaccine, inactivated virus" - ) + "COVID-19 vaccine, inactivated virus", + ), ) }, { @@ -2616,14 +2642,14 @@ class DatabaseImplTest { Coding( "http://id.who.int/icd11/mms", "XM5DF6", - "COVID-19 vaccine, inactivated virus" - ) + "COVID-19 vaccine, inactivated virus", + ), ) }, - operation = Operation.OR + operation = Operation.OR, ) } - .getQuery() + .getQuery(), ) assertThat(result.map { it.vaccineCode.codingFirstRep.code }) @@ -2639,7 +2665,11 @@ class DatabaseImplTest { id = "immunization-1" vaccineCode = CodeableConcept( - Coding("http://id.who.int/icd11/mms", "XM1NL1", "COVID-19 vaccine, inactivated virus") + Coding( + "http://id.who.int/icd11/mms", + "XM1NL1", + "COVID-19 vaccine, inactivated virus", + ), ) status = Immunization.ImmunizationStatus.COMPLETED }, @@ -2650,8 +2680,8 @@ class DatabaseImplTest { Coding( "http://id.who.int/icd11/mms", "XM5DF6", - "COVID-19 vaccine, live attenuated virus" - ) + "COVID-19 vaccine, live attenuated virus", + ), ) status = Immunization.ImmunizationStatus.COMPLETED }, @@ -2659,7 +2689,7 @@ class DatabaseImplTest { id = "immunization-3" vaccineCode = CodeableConcept( - Coding("http://id.who.int/icd11/mms", "XM6AT1", "COVID-19 vaccine, DNA based") + Coding("http://id.who.int/icd11/mms", "XM6AT1", "COVID-19 vaccine, DNA based"), ) status = Immunization.ImmunizationStatus.COMPLETED }, @@ -2670,11 +2700,11 @@ class DatabaseImplTest { Coding( "http://hl7.org/fhir/sid/cvx", "140", - "Influenza, seasonal, injectable, preservative free" - ) + "Influenza, seasonal, injectable, preservative free", + ), ) status = Immunization.ImmunizationStatus.COMPLETED - } + }, ) database.insert(*resources.toTypedArray()) @@ -2685,16 +2715,16 @@ class DatabaseImplTest { .apply { filter( Immunization.VACCINE_CODE, - { value = of(Coding("http://id.who.int/icd11/mms", "XM1NL1", "")) } + { value = of(Coding("http://id.who.int/icd11/mms", "XM1NL1", "")) }, ) filter( Immunization.VACCINE_CODE, - { value = of(Coding("http://id.who.int/icd11/mms", "XM5DF6", "")) } + { value = of(Coding("http://id.who.int/icd11/mms", "XM5DF6", "")) }, ) operation = Operation.OR } - .getQuery() + .getQuery(), ) assertThat(result.map { it.vaccineCode.codingFirstRep.code }) @@ -2712,7 +2742,7 @@ class DatabaseImplTest { HumanName().apply { addGiven("John") family = "Doe" - } + }, ) }, Patient().apply { @@ -2721,7 +2751,7 @@ class DatabaseImplTest { HumanName().apply { addGiven("Jane") family = "Doe" - } + }, ) }, Patient().apply { @@ -2730,7 +2760,7 @@ class DatabaseImplTest { HumanName().apply { addGiven("John") family = "Roe" - } + }, ) }, Patient().apply { @@ -2739,7 +2769,7 @@ class DatabaseImplTest { HumanName().apply { addGiven("Jane") family = "Roe" - } + }, ) }, Patient().apply { @@ -2748,9 +2778,9 @@ class DatabaseImplTest { HumanName().apply { addGiven("Rocky") family = "Balboa" - } + }, ) - } + }, ) database.insert(*resources.toTypedArray()) @@ -2768,7 +2798,7 @@ class DatabaseImplTest { value = "Jane" modifier = StringFilterModifier.MATCHES_EXACTLY }, - operation = Operation.OR + operation = Operation.OR, ) filter( @@ -2781,12 +2811,12 @@ class DatabaseImplTest { value = "Roe" modifier = StringFilterModifier.MATCHES_EXACTLY }, - operation = Operation.OR + operation = Operation.OR, ) operation = Operation.AND } - .getQuery() + .getQuery(), ) assertThat(result.map { it.nameFirstRep.nameAsSingleString }) @@ -2813,7 +2843,7 @@ class DatabaseImplTest { Identifier().apply { system = "https://custom-identifier-namespace" value = "OfficialIdentifier_DarcySmith_0001" - } + }, ) addName( @@ -2823,14 +2853,14 @@ class DatabaseImplTest { addGiven("Darcy") gender = Enumerations.AdministrativeGender.FEMALE birthDateElement = DateType("1970-01-01") - } + }, ) addExtension( Extension().apply { url = "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName" setValue(StringType("Marca")) - } + }, ) } // Get rid of the default service and create one with search params @@ -2847,10 +2877,10 @@ class DatabaseImplTest { { value = "Marca" modifier = StringFilterModifier.MATCHES_EXACTLY - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.map { it.nameFirstRep.nameAsSingleString }).contains("Darcy Smith") @@ -2864,7 +2894,7 @@ class DatabaseImplTest { Identifier().apply { system = "https://custom-identifier-namespace" value = "OfficialIdentifier_DarcySmith_0001" - } + }, ) addName( @@ -2874,14 +2904,14 @@ class DatabaseImplTest { addGiven("Darcy") gender = Enumerations.AdministrativeGender.FEMALE birthDateElement = DateType("1970-01-01") - } + }, ) addExtension( Extension().apply { url = "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName" setValue(StringType("Marca")) - } + }, ) } val identifierPartialSearchParameter = @@ -2908,10 +2938,10 @@ class DatabaseImplTest { { value = "OfficialIdentifier_" modifier = StringFilterModifier.STARTS_WITH - } + }, ) } - .getQuery() + .getQuery(), ) assertThat(result.map { it.nameFirstRep.nameAsSingleString }).contains("Darcy Smith") @@ -2922,21 +2952,21 @@ class DatabaseImplTest { database.insert( Patient().apply { id = "patient-test-001" }, Patient().apply { id = "patient-test-002" }, - Patient().apply { id = "patient-test-003" } + Patient().apply { id = "patient-test-003" }, ) database.update( Patient().apply { id = "patient-test-002" gender = Enumerations.AdministrativeGender.FEMALE - } + }, ) val result = database.search( Search(ResourceType.Patient) .apply { sort(LOCAL_LAST_UPDATED_PARAM, Order.DESCENDING) } - .getQuery() + .getQuery(), ) assertThat(result.map { it.logicalId }) @@ -2953,7 +2983,7 @@ class DatabaseImplTest { HumanName().apply { addGiven("James") family = "Gorden" - } + }, ) addGeneralPractitioner(Reference("Practitioner/gp-01")) addGeneralPractitioner(Reference("Practitioner/gp-02")) @@ -2966,7 +2996,7 @@ class DatabaseImplTest { HumanName().apply { addGiven("James") family = "Bond" - } + }, ) addGeneralPractitioner(Reference("Practitioner/gp-02")) addGeneralPractitioner(Reference("Practitioner/gp-03")) @@ -2980,7 +3010,7 @@ class DatabaseImplTest { HumanName().apply { family = "Practitioner-01" addGiven("General-01") - } + }, ) active = true } @@ -2991,7 +3021,7 @@ class DatabaseImplTest { HumanName().apply { family = "Practitioner-02" addGiven("General-02") - } + }, ) active = false } @@ -3002,7 +3032,7 @@ class DatabaseImplTest { HumanName().apply { family = "Practitioner-03" addGiven("General-03") - } + }, ) active = true } @@ -3019,7 +3049,7 @@ class DatabaseImplTest { { value = "James" modifier = StringFilterModifier.MATCHES_EXACTLY - } + }, ) include(Patient.GENERAL_PRACTITIONER) { @@ -3034,14 +3064,14 @@ class DatabaseImplTest { SearchResult( patient01, included = mapOf(Patient.GENERAL_PRACTITIONER.paramName to listOf(gp01)), - revIncluded = null + revIncluded = null, ), SearchResult( patient02, included = mapOf(Patient.GENERAL_PRACTITIONER.paramName to listOf(gp03)), - revIncluded = null - ) - ) + revIncluded = null, + ), + ), ) } @@ -3054,7 +3084,7 @@ class DatabaseImplTest { HumanName().apply { addGiven("James") family = "Gorden" - } + }, ) addGeneralPractitioner(Reference("Practitioner/gp-01")) } @@ -3066,7 +3096,7 @@ class DatabaseImplTest { HumanName().apply { addGiven("James") family = "Bond" - } + }, ) addGeneralPractitioner(Reference("Practitioner/gp-02")) } @@ -3108,7 +3138,7 @@ class DatabaseImplTest { { value = "James" modifier = StringFilterModifier.MATCHES_EXACTLY - } + }, ) revInclude(Condition.SUBJECT) { filter(Condition.CODE, { value = of(diabetesCodeableConcept) }) @@ -3125,15 +3155,15 @@ class DatabaseImplTest { patient01, included = null, revIncluded = - mapOf((ResourceType.Condition to Condition.SUBJECT.paramName) to listOf(con1)) + mapOf((ResourceType.Condition to Condition.SUBJECT.paramName) to listOf(con1)), ), SearchResult( patient02, included = null, revIncluded = - mapOf((ResourceType.Condition to Condition.SUBJECT.paramName) to listOf(con3)) - ) - ) + mapOf((ResourceType.Condition to Condition.SUBJECT.paramName) to listOf(con3)), + ), + ), ) } @@ -3154,7 +3184,7 @@ class DatabaseImplTest { HumanName().apply { addGiven("James") family = "Gorden" - } + }, ) addGeneralPractitioner(Reference("Practitioner/gp-01")) addGeneralPractitioner(Reference("Practitioner/gp-02")) @@ -3167,7 +3197,7 @@ class DatabaseImplTest { HumanName().apply { addGiven("James") family = "Bond" - } + }, ) addGeneralPractitioner(Reference("Practitioner/gp-01")) addGeneralPractitioner(Reference("Practitioner/gp-02")) @@ -3180,13 +3210,13 @@ class DatabaseImplTest { HumanName().apply { addGiven("James") family = "Doe" - } + }, ) addGeneralPractitioner(Reference("Practitioner/gp-01")) addGeneralPractitioner(Reference("Practitioner/gp-02")) addGeneralPractitioner(Reference("Practitioner/gp-03")) managingOrganization = Reference("Organization/org-03") - } + }, ) val practitioners = @@ -3197,7 +3227,7 @@ class DatabaseImplTest { HumanName().apply { family = "Practitioner-01" addGiven("General-01") - } + }, ) active = true }, @@ -3207,7 +3237,7 @@ class DatabaseImplTest { HumanName().apply { family = "Practitioner-02" addGiven("General-02") - } + }, ) active = true }, @@ -3217,10 +3247,10 @@ class DatabaseImplTest { HumanName().apply { family = "Practitioner-03" addGiven("General-03") - } + }, ) active = false - } + }, ) val organizations = @@ -3239,7 +3269,7 @@ class DatabaseImplTest { id = "org-03" name = "Organization-03" active = false - } + }, ) val conditions = @@ -3373,7 +3403,7 @@ class DatabaseImplTest { start = DateType(2022, 2, 1).value end = DateType(2022, 11, 1).value } - } + }, ) // 3 Patients. // Each has 3 conditions, only 2 should match @@ -3394,7 +3424,7 @@ class DatabaseImplTest { { value = "James" modifier = StringFilterModifier.MATCHES_EXACTLY - } + }, ) include(Patient.GENERAL_PRACTITIONER) { @@ -3404,7 +3434,7 @@ class DatabaseImplTest { { value = "Practitioner" modifier = StringFilterModifier.STARTS_WITH - } + }, ) operation = Operation.AND } @@ -3414,7 +3444,7 @@ class DatabaseImplTest { { value = "Organization" modifier = StringFilterModifier.STARTS_WITH - } + }, ) filter(Practitioner.ACTIVE, { value = of(true) }) operation = Operation.AND @@ -3431,7 +3461,7 @@ class DatabaseImplTest { { value = of(DateTimeType("2023-01-01")) prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS - } + }, ) } } @@ -3450,21 +3480,21 @@ class DatabaseImplTest { Pair(ResourceType.Condition, "subject") to listOf(resources["con-01-pa-01"]!!, resources["con-03-pa-01"]!!), Pair(ResourceType.Encounter, "subject") to - listOf(resources["en-01-pa-01"]!!, resources["en-02-pa-01"]!!) - ) + listOf(resources["en-01-pa-01"]!!, resources["en-02-pa-01"]!!), + ), ), SearchResult( resources["pa-02"]!!, mapOf( "general-practitioner" to listOf(resources["gp-01"]!!, resources["gp-02"]!!), - "organization" to listOf(resources["org-02"]!!) + "organization" to listOf(resources["org-02"]!!), ), mapOf( Pair(ResourceType.Condition, "subject") to listOf(resources["con-01-pa-02"]!!, resources["con-03-pa-02"]!!), Pair(ResourceType.Encounter, "subject") to - listOf(resources["en-01-pa-02"]!!, resources["en-02-pa-02"]!!) - ) + listOf(resources["en-01-pa-02"]!!, resources["en-02-pa-02"]!!), + ), ), SearchResult( resources["pa-03"]!!, @@ -3475,10 +3505,10 @@ class DatabaseImplTest { Pair(ResourceType.Condition, "subject") to listOf(resources["con-01-pa-03"]!!, resources["con-03-pa-03"]!!), Pair(ResourceType.Encounter, "subject") to - listOf(resources["en-01-pa-03"]!!, resources["en-02-pa-03"]!!) - ) - ) - ) + listOf(resources["en-01-pa-03"]!!, resources["en-02-pa-03"]!!), + ), + ), + ), ) } 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 59a2a1322e..0b9a31b4fb 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt @@ -19,6 +19,7 @@ package com.google.android.fhir import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.search.Search import com.google.android.fhir.sync.ConflictResolver +import com.google.android.fhir.sync.upload.LocalChangesFetchMode import java.time.OffsetDateTime import kotlinx.coroutines.flow.Flow import org.hl7.fhir.r4.model.Resource @@ -53,7 +54,8 @@ interface FhirEngine { * api caller should [Flow.collect] it. */ suspend fun syncUpload( - upload: (suspend (List) -> Flow>) + localChangesFetchMode: LocalChangesFetchMode, + upload: (suspend (List) -> Flow>), ) /** @@ -62,7 +64,7 @@ interface FhirEngine { */ suspend fun syncDownload( conflictResolver: ConflictResolver, - download: suspend () -> Flow> + download: suspend () -> Flow>, ) /** @@ -87,29 +89,32 @@ interface FhirEngine { * Retrieves a list of [LocalChange]s for [Resource] with given type and id, which can be used to * purge resource from database. If there is no local change for given [resourceType] and * [Resource.id], return an empty list. + * * @param type The [ResourceType] * @param id The resource id [Resource.id] * @return [List]<[LocalChange]> A list of local changes for given [resourceType] and - * [Resource.id] . If there is no local change for given [resourceType] and [Resource.id], return - * an empty list. + * [Resource.id] . If there is no local change for given [resourceType] and [Resource.id], + * return an empty list. */ suspend fun getLocalChanges(type: ResourceType, id: String): List /** * Purges a resource from the database based on resource type and id without any deletion of data * from the server. + * * @param type The [ResourceType] * @param id The resource id [Resource.id] * @param isLocalPurge default value is false here resource will not be deleted from - * LocalChangeEntity table but it will throw IllegalStateException("Resource has local changes - * either sync with server or FORCE_PURGE required") if local change exists. If true this API will - * delete resource entry from LocalChangeEntity table. + * LocalChangeEntity table but it will throw IllegalStateException("Resource has local changes + * either sync with server or FORCE_PURGE required") if local change exists. If true this API + * will delete resource entry from LocalChangeEntity table. */ suspend fun purge(type: ResourceType, id: String, forcePurge: Boolean = false) } /** * Returns a FHIR resource of type [R] with [id] from the local storage. + * * @param The resource type which should be a subtype of [Resource]. * @throws ResourceNotFoundException if the resource is not found */ @@ -120,6 +125,7 @@ suspend inline fun FhirEngine.get(id: String): R { /** * Deletes a FHIR resource of type [R] with [id] from the local storage. + * * @param The resource type which should be a subtype of [Resource]. */ suspend inline fun FhirEngine.delete(id: String) { @@ -138,7 +144,7 @@ data class SearchResult( /** Matching referenced resources as per the [Search.include] criteria in the query. */ val included: Map>?, /** Matching referenced resources as per the [Search.revInclude] criteria in the query. */ - val revIncluded: Map, List>? + val revIncluded: Map, List>?, ) { override fun equals(other: Any?) = other is SearchResult<*> && @@ -155,7 +161,7 @@ data class SearchResult( private fun equalsShallow( first: Map>?, - second: Map>? + second: Map>?, ) = if (first != null && second != null && first.size == second.size) { first.entries.asSequence().zip(second.entries.asSequence()).all { (x, y) -> @@ -168,7 +174,7 @@ data class SearchResult( @JvmName("equalsShallowRevInclude") private fun equalsShallow( first: Map, List>?, - second: Map, List>? + second: Map, List>?, ) = if (first != null && second != null && first.size == second.size) { first.entries.asSequence().zip(second.entries.asSequence()).all { (x, y) -> diff --git a/engine/src/main/java/com/google/android/fhir/db/Database.kt b/engine/src/main/java/com/google/android/fhir/db/Database.kt index b2cb416fcb..cbbc840b64 100644 --- a/engine/src/main/java/com/google/android/fhir/db/Database.kt +++ b/engine/src/main/java/com/google/android/fhir/db/Database.kt @@ -58,7 +58,7 @@ internal interface Database { resourceId: String, resourceType: ResourceType, versionId: String, - lastUpdated: Instant + lastUpdated: Instant, ) /** @@ -105,6 +105,9 @@ internal interface Database { */ suspend fun getAllLocalChanges(): List + /** Retrieves the count of [LocalChange]s stored in the database. */ + suspend fun getLocalChangesCount(): Int + /** Remove the [LocalChangeEntity] s with given ids. Call this after a successful sync. */ suspend fun deleteUpdates(token: LocalChangeToken) @@ -127,23 +130,25 @@ internal interface Database { * Retrieve a list of [LocalChange] for [Resource] with given type and id, which can be used to * purge resource from database. If there is no local change for given [resourceType] and * [Resource.id], return an empty list. + * * @param type The [ResourceType] * @param id The resource id [Resource.id] * @return [List]<[LocalChange]> A list of local changes for given [resourceType] and - * [Resource.id] . If there is no local change for given [resourceType] and [Resource.id], return - * empty list. + * [Resource.id] . If there is no local change for given [resourceType] and [Resource.id], + * return empty list. */ suspend fun getLocalChanges(type: ResourceType, id: String): List /** * Purge resource from database based on resource type and id without any deletion of data from * the server. + * * @param type The [ResourceType] * @param id The resource id [Resource.id] * @param isLocalPurge default value is false here resource will not be deleted from - * LocalChangeEntity table but it will throw IllegalStateException("Resource has local changes - * either sync with server or FORCE_PURGE required") if local change exists. If true this API will - * delete resource entry from LocalChangeEntity table. + * LocalChangeEntity table but it will throw IllegalStateException("Resource has local changes + * either sync with server or FORCE_PURGE required") if local change exists. If true this API + * will delete resource entry from LocalChangeEntity table. */ suspend fun purge(type: ResourceType, id: String, forcePurge: Boolean = false) } diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt b/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt index 8da36f8bce..8ca9fff23c 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt @@ -46,7 +46,7 @@ internal class DatabaseImpl( private val context: Context, private val iParser: IParser, databaseConfig: DatabaseConfig, - private val resourceIndexer: ResourceIndexer + private val resourceIndexer: ResourceIndexer, ) : com.google.android.fhir.db.Database { val db: ResourceDatabase @@ -88,8 +88,10 @@ internal class DatabaseImpl( openHelperFactory { SQLCipherSupportHelper( it, - databaseErrorStrategy = databaseConfig.databaseErrorStrategy - ) { DatabaseEncryptionKeyProvider.getOrCreatePassphrase(DATABASE_PASSPHRASE_NAME) } + databaseErrorStrategy = databaseConfig.databaseErrorStrategy, + ) { + DatabaseEncryptionKeyProvider.getOrCreatePassphrase(DATABASE_PASSPHRASE_NAME) + } } } @@ -115,7 +117,7 @@ internal class DatabaseImpl( val timeOfLocalChange = Instant.now() localChangeDao.addInsert(it, timeOfLocalChange) resourceDao.insertLocalResource(it, timeOfLocalChange) - } + }, ) } return logicalIds @@ -140,14 +142,14 @@ internal class DatabaseImpl( resourceId: String, resourceType: ResourceType, versionId: String, - lastUpdated: Instant + lastUpdated: Instant, ) { db.withTransaction { resourceDao.updateAndIndexRemoteVersionIdAndLastUpdate( resourceId, resourceType, versionId, - lastUpdated + lastUpdated, ) } } @@ -174,12 +176,13 @@ internal class DatabaseImpl( null } val rowsDeleted = resourceDao.deleteResource(resourceId = id, resourceType = type) - if (rowsDeleted > 0) + if (rowsDeleted > 0) { localChangeDao.addDelete( resourceId = id, resourceType = type, - remoteVersionId = remoteVersionId + remoteVersionId = remoteVersionId, ) + } } } @@ -200,7 +203,7 @@ internal class DatabaseImpl( IndexedIdAndResource( it.matchingIndex, it.idOfBaseResourceOnWhichThisMatchedInc ?: it.idOfBaseResourceOnWhichThisMatchedRev!!, - iParser.parseResource(it.serializedResource) as Resource + iParser.parseResource(it.serializedResource) as Resource, ) } } @@ -216,6 +219,10 @@ internal class DatabaseImpl( return db.withTransaction { localChangeDao.getAllLocalChanges().map { it.toLocalChange() } } } + override suspend fun getLocalChangesCount(): Int { + return db.withTransaction { localChangeDao.getLocalChangesCount() } + } + override suspend fun deleteUpdates(token: LocalChangeToken) { db.withTransaction { localChangeDao.discardLocalChanges(token) } } @@ -265,12 +272,12 @@ internal class DatabaseImpl( if (forcePurge) { resourceDao.deleteResource(resourceId = id, resourceType = type) localChangeDao.discardLocalChanges( - token = LocalChangeToken(localChangeEntityList.map { it.id }) + token = LocalChangeToken(localChangeEntityList.map { it.id }), ) } else { // local change is available but FORCE_PURGE = false then throw exception throw IllegalStateException( - "Resource with type $type and id $id has local changes, either sync with server or FORCE_PURGE required" + "Resource with type $type and id $id has local changes, either sync with server or FORCE_PURGE required", ) } } @@ -301,5 +308,5 @@ internal class DatabaseImpl( data class DatabaseConfig( val inMemory: Boolean, val enableEncryption: Boolean, - val databaseErrorStrategy: DatabaseErrorStrategy + val databaseErrorStrategy: DatabaseErrorStrategy, ) diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeDao.kt b/engine/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeDao.kt index f8c9524439..d69684a9bd 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeDao.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeDao.kt @@ -64,8 +64,8 @@ internal abstract class LocalChangeDao { timestamp = timeOfLocalChange, type = Type.INSERT, payload = resourceString, - versionId = resource.versionId - ) + versionId = resource.versionId, + ), ) } @@ -73,11 +73,12 @@ internal abstract class LocalChangeDao { val resourceId = resource.logicalId val resourceType = resource.resourceType - if (!localChangeIsEmpty(resourceId, resourceType) && + if ( + !localChangeIsEmpty(resourceId, resourceType) && lastChangeType(resourceId, resourceType)!! == Type.DELETE ) { throw InvalidLocalChangeException( - "Unexpected DELETE when updating $resourceType/$resourceId. UPDATE failed." + "Unexpected DELETE when updating $resourceType/$resourceId. UPDATE failed.", ) } val jsonDiff = @@ -85,7 +86,7 @@ internal abstract class LocalChangeDao { if (jsonDiff.length() == 0) { Timber.i( "New resource ${resource.resourceType}/${resource.id} is same as old resource. " + - "Not inserting UPDATE LocalChange." + "Not inserting UPDATE LocalChange.", ) return } @@ -97,8 +98,8 @@ internal abstract class LocalChangeDao { timestamp = timeOfLocalChange, type = Type.UPDATE, payload = jsonDiff.toString(), - versionId = oldEntity.versionId - ) + versionId = oldEntity.versionId, + ), ) } @@ -111,8 +112,8 @@ internal abstract class LocalChangeDao { timestamp = Date().toInstant(), type = Type.DELETE, payload = "", - versionId = remoteVersionId - ) + versionId = remoteVersionId, + ), ) } @@ -124,7 +125,7 @@ internal abstract class LocalChangeDao { AND resourceType = :resourceType ORDER BY id ASC LIMIT 1 - """ + """, ) abstract suspend fun lastChangeType(resourceId: String, resourceType: ResourceType): Type? @@ -135,7 +136,7 @@ internal abstract class LocalChangeDao { WHERE resourceId = :resourceId AND resourceType = :resourceType LIMIT 1 - """ + """, ) abstract suspend fun countLastChange(resourceId: String, resourceType: ResourceType): Int @@ -146,15 +147,23 @@ internal abstract class LocalChangeDao { """ SELECT * FROM LocalChangeEntity - ORDER BY LocalChangeEntity.id ASC""" + ORDER BY LocalChangeEntity.id ASC""", ) abstract suspend fun getAllLocalChanges(): List + @Query( + """ + SELECT COUNT(*) + FROM LocalChangeEntity + """, + ) + abstract suspend fun getLocalChangesCount(): Int + @Query( """ DELETE FROM LocalChangeEntity WHERE LocalChangeEntity.id = (:id) - """ + """, ) abstract suspend fun discardLocalChanges(id: Long) @@ -168,7 +177,7 @@ internal abstract class LocalChangeDao { DELETE FROM LocalChangeEntity WHERE resourceId = (:resourceId) AND resourceType = :resourceType - """ + """, ) abstract suspend fun discardLocalChanges(resourceId: String, resourceType: ResourceType) @@ -181,11 +190,11 @@ internal abstract class LocalChangeDao { SELECT * FROM LocalChangeEntity WHERE resourceId = :resourceId AND resourceType = :resourceType - """ + """, ) abstract suspend fun getLocalChanges( resourceType: ResourceType, - resourceId: String + resourceId: String, ): List class InvalidLocalChangeException(message: String?) : Exception(message) @@ -197,8 +206,8 @@ internal fun diff(parser: IParser, source: Resource, target: Resource): JSONArra return getFilteredJSONArray( JsonDiff.asJson( objectMapper.readValue(parser.encodeResourceToString(source), JsonNode::class.java), - objectMapper.readValue(parser.encodeResourceToString(target), JsonNode::class.java) - ) + objectMapper.readValue(parser.encodeResourceToString(target), JsonNode::class.java), + ), ) } @@ -210,12 +219,14 @@ internal fun diff(parser: IParser, source: Resource, target: Resource): JSONArra * and causes the issue with server update. * * An unfiltered JSON Array for family name update looks like + * * ``` * [{"op":"remove","path":"/meta"}, {"op":"remove","path":"/text"}, * {"op":"replace","path":"/name/0/family","value":"Nucleus"}] * ``` * * A filtered JSON Array for family name update looks like + * * ``` * [{"op":"replace","path":"/name/0/family","value":"Nucleus"}] * ``` @@ -226,6 +237,8 @@ private fun getFilteredJSONArray(jsonDiff: JsonNode) = return@with JSONArray( (0 until length()) .map { optJSONObject(it) } - .filterNot { jsonObject -> ignorePaths.any { jsonObject.optString("path").startsWith(it) } } + .filterNot { jsonObject -> + ignorePaths.any { jsonObject.optString("path").startsWith(it) } + }, ) } 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 4c93efec9f..adf2611487 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 @@ -30,6 +30,8 @@ import com.google.android.fhir.search.execute import com.google.android.fhir.sync.ConflictResolver import com.google.android.fhir.sync.Resolved import com.google.android.fhir.sync.upload.DefaultResourceConsolidator +import com.google.android.fhir.sync.upload.LocalChangeFetcherFactory +import com.google.android.fhir.sync.upload.LocalChangesFetchMode import java.time.OffsetDateTime import kotlinx.coroutines.flow.Flow import org.hl7.fhir.r4.model.Resource @@ -122,12 +124,15 @@ internal class FhirEngineImpl(private val database: Database, private val contex .intersect(database.getAllLocalChanges().map { it.resourceId }.toSet()) override suspend fun syncUpload( + localChangesFetchMode: LocalChangesFetchMode, upload: suspend (List) -> Flow>, ) { val resourceConsolidator = DefaultResourceConsolidator(database) - val localChanges = database.getAllLocalChanges() - if (localChanges.isNotEmpty()) { - upload(localChanges).collect { resourceConsolidator.consolidate(it.first, it.second) } + val localChangeFetcher = LocalChangeFetcherFactory.byMode(localChangesFetchMode, database) + while (localChangeFetcher.hasNext()) { + upload(localChangeFetcher.next()).collect { + resourceConsolidator.consolidate(it.first, it.second) + } } } } diff --git a/engine/src/main/java/com/google/android/fhir/sync/FhirSynchronizer.kt b/engine/src/main/java/com/google/android/fhir/sync/FhirSynchronizer.kt index c58857dfbb..f2f6de79b0 100644 --- a/engine/src/main/java/com/google/android/fhir/sync/FhirSynchronizer.kt +++ b/engine/src/main/java/com/google/android/fhir/sync/FhirSynchronizer.kt @@ -21,6 +21,7 @@ import com.google.android.fhir.DatastoreUtil import com.google.android.fhir.FhirEngine import com.google.android.fhir.sync.download.DownloadState import com.google.android.fhir.sync.download.Downloader +import com.google.android.fhir.sync.upload.LocalChangesFetchMode import com.google.android.fhir.sync.upload.UploadState import com.google.android.fhir.sync.upload.Uploader import java.time.OffsetDateTime @@ -129,7 +130,8 @@ internal class FhirSynchronizer( private suspend fun upload(): SyncResult { val exceptions = mutableListOf() - fhirEngine.syncUpload { list -> + val localChangesFetchMode = LocalChangesFetchMode.AllChanges + fhirEngine.syncUpload(localChangesFetchMode) { list -> flow { uploader.upload(list).collect { result -> when (result) { diff --git a/engine/src/main/java/com/google/android/fhir/sync/upload/LocalChangeFetcher.kt b/engine/src/main/java/com/google/android/fhir/sync/upload/LocalChangeFetcher.kt new file mode 100644 index 0000000000..34f8eee4d4 --- /dev/null +++ b/engine/src/main/java/com/google/android/fhir/sync/upload/LocalChangeFetcher.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2023 Google LLC + * + * 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 com.google.android.fhir.sync.upload + +import com.google.android.fhir.LocalChange +import com.google.android.fhir.db.Database +import kotlin.properties.Delegates + +/** + * Fetches local changes. + * + * This interface provides methods to check for the existence of further changes, retrieve the next + * batch of changes, and get the progress of fetched changes. + * + * It is marked as internal to keep [Database] unexposed to clients + */ +internal interface LocalChangeFetcher { + + /** Represents the initial total number of local changes to upload. */ + val total: Int + + /** Checks if there are more local changes to be fetched. */ + suspend fun hasNext(): Boolean + + /** Retrieves the next batch of local changes. */ + suspend fun next(): List + + /** + * Returns [FetchProgress], which contains the remaining changes left to upload and the initial + * total to upload. + */ + suspend fun getProgress(): FetchProgress +} + +data class FetchProgress( + val remaining: Int, + val initialTotal: Int, +) + +internal class AllChangesLocalChangeFetcher( + private val database: Database, +) : LocalChangeFetcher { + + override var total by Delegates.notNull() + + suspend fun initTotalCount() { + total = database.getLocalChangesCount() + } + + override suspend fun hasNext(): Boolean = database.getLocalChangesCount().isNotZero() + + override suspend fun next(): List = database.getAllLocalChanges() + + override suspend fun getProgress(): FetchProgress = + FetchProgress(database.getLocalChangesCount(), total) +} + +/** Represents the mode in which local changes should be fetched. */ +sealed class LocalChangesFetchMode { + object AllChanges : LocalChangesFetchMode() + + object PerResource : LocalChangesFetchMode() + + object EarliestChange : LocalChangesFetchMode() +} + +internal object LocalChangeFetcherFactory { + suspend fun byMode( + mode: LocalChangesFetchMode, + database: Database, + ): LocalChangeFetcher = + when (mode) { + is LocalChangesFetchMode.AllChanges -> + AllChangesLocalChangeFetcher(database).apply { initTotalCount() } + else -> throw NotImplementedError("$mode is not implemented yet.") + } +} + +private fun Int.isNotZero() = this != 0 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 6b7a27ae57..ebd90ab6dc 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 @@ -33,6 +33,7 @@ import com.google.android.fhir.sync.DownloadRequest import com.google.android.fhir.sync.DownloadWorkManager import com.google.android.fhir.sync.UploadRequest import com.google.android.fhir.sync.UrlDownloadRequest +import com.google.android.fhir.sync.upload.LocalChangesFetchMode import com.google.common.truth.Truth.assertThat import java.net.SocketTimeoutException import java.time.Instant @@ -111,7 +112,7 @@ object TestDataSourceImpl : DataSource { } open class TestDownloadManagerImpl( - private val queries: List = listOf("Patient?address-city=NAIROBI") + private val queries: List = listOf("Patient?address-city=NAIROBI"), ) : DownloadWorkManager { private val urls = LinkedList(queries) @@ -147,17 +148,17 @@ object TestFhirEngineImpl : FhirEngine { } override suspend fun syncUpload( - upload: suspend (List) -> Flow> - ) { - upload(getLocalChanges(ResourceType.Patient, "123")).collect() - } + localChangesFetchMode: LocalChangesFetchMode, + upload: suspend (List) -> Flow>, + ) = upload(getLocalChanges(ResourceType.Patient, "123")).collect() override suspend fun syncDownload( conflictResolver: ConflictResolver, - download: suspend () -> Flow> + download: suspend () -> Flow>, ) { download().collect() } + override suspend fun count(search: Search): Long { return 0 } @@ -176,8 +177,8 @@ object TestFhirEngineImpl : FhirEngine { payload = "{ 'resourceType' : 'Patient', 'id' : '123' }", token = LocalChangeToken(listOf()), type = LocalChange.Type.INSERT, - timestamp = Instant.now() - ) + timestamp = Instant.now(), + ), ) } 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 d19d1b8dfc..fbf400007e 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 @@ -29,6 +29,7 @@ import com.google.android.fhir.search.LOCAL_LAST_UPDATED_PARAM import com.google.android.fhir.search.search import com.google.android.fhir.sync.AcceptLocalConflictResolver import com.google.android.fhir.sync.AcceptRemoteConflictResolver +import com.google.android.fhir.sync.upload.LocalChangesFetchMode import com.google.android.fhir.testing.assertResourceEquals import com.google.android.fhir.testing.assertResourceNotEquals import com.google.android.fhir.testing.readFromFile @@ -85,7 +86,7 @@ class FhirEngineImplTest { HumanName().apply { family = "FamilyName" addGiven("GivenName") - } + }, ) } val ids = fhirEngine.create(patient.copy()) @@ -101,7 +102,7 @@ class FhirEngineImplTest { } assertThat(exception.message) .isEqualTo( - "Resource not found with type ${TEST_PATIENT_2.resourceType.name} and id $TEST_PATIENT_2_ID!" + "Resource not found with type ${TEST_PATIENT_2.resourceType.name} and id $TEST_PATIENT_2_ID!", ) } @@ -160,7 +161,7 @@ class FhirEngineImplTest { } assertThat(resourceNotFoundException.message) .isEqualTo( - "Resource not found with type ${ResourceType.Patient.name} and id nonexistent_patient!" + "Resource not found with type ${ResourceType.Patient.name} and id nonexistent_patient!", ) } @@ -175,7 +176,7 @@ class FhirEngineImplTest { listOf( buildPatient("3", "C", Enumerations.AdministrativeGender.FEMALE), buildPatient("2", "B", Enumerations.AdministrativeGender.FEMALE), - buildPatient("1", "A", Enumerations.AdministrativeGender.MALE) + buildPatient("1", "A", Enumerations.AdministrativeGender.MALE), ) fhirEngine.create(*patients.toTypedArray()) @@ -184,7 +185,7 @@ class FhirEngineImplTest { assertThat(result.size).isEqualTo(2) assertThat( - result.all { (it.resource as Patient).gender == Enumerations.AdministrativeGender.FEMALE } + result.all { (it.resource as Patient).gender == Enumerations.AdministrativeGender.FEMALE }, ) .isTrue() } @@ -195,7 +196,7 @@ class FhirEngineImplTest { listOf( buildPatient("3", "C", Enumerations.AdministrativeGender.FEMALE), buildPatient("2", "B", Enumerations.AdministrativeGender.FEMALE), - buildPatient("1", "A", Enumerations.AdministrativeGender.MALE) + buildPatient("1", "A", Enumerations.AdministrativeGender.MALE), ) fhirEngine.create(*patients.toTypedArray()) @@ -212,7 +213,7 @@ class FhirEngineImplTest { listOf( buildPatient("3", "C", Enumerations.AdministrativeGender.FEMALE), buildPatient("2", "B", Enumerations.AdministrativeGender.FEMALE), - buildPatient("1", "A", Enumerations.AdministrativeGender.MALE) + buildPatient("1", "A", Enumerations.AdministrativeGender.MALE), ) fhirEngine.create(*patients.toTypedArray()) @@ -258,7 +259,7 @@ class FhirEngineImplTest { }, buildPatient("2", "Patient2", Enumerations.AdministrativeGender.FEMALE).apply { meta = Meta().setTag(mutableListOf(Coding("http://d-tree.org/", "Tag2", "Tag 2"))) - } + }, ) fhirEngine.create(*patients.toTypedArray()) @@ -279,15 +280,15 @@ class FhirEngineImplTest { .setProfile( mutableListOf( CanonicalType( - "http://fhir.org/STU3/StructureDefinition/Example-Patient-Profile-1" - ) - ) + "http://fhir.org/STU3/StructureDefinition/Example-Patient-Profile-1", + ), + ), ) }, buildPatient("4", "C", Enumerations.AdministrativeGender.FEMALE).apply { meta = Meta().setProfile(mutableListOf(CanonicalType("http://d-tree.org/Diabetes-Patient"))) - } + }, ) fhirEngine.create(*patients.toTypedArray()) @@ -295,7 +296,7 @@ class FhirEngineImplTest { val result = fhirEngine .search( - "Patient?_profile=http://fhir.org/STU3/StructureDefinition/Example-Patient-Profile-1" + "Patient?_profile=http://fhir.org/STU3/StructureDefinition/Example-Patient-Profile-1", ) .map { it.resource as Patient } @@ -305,7 +306,7 @@ class FhirEngineImplTest { patient.meta.profile.all { it.value.equals("http://fhir.org/STU3/StructureDefinition/Example-Patient-Profile-1") } - } + }, ) .isTrue() } @@ -313,7 +314,7 @@ class FhirEngineImplTest { @Test fun syncUpload_uploadLocalChange() = runBlocking { val localChanges = mutableListOf() - fhirEngine.syncUpload { + fhirEngine.syncUpload(LocalChangesFetchMode.AllChanges) { flow { localChanges.addAll(it) emit(LocalChangeToken(it.flatMap { it.token.ids }) to TEST_PATIENT_1) @@ -321,7 +322,6 @@ class FhirEngineImplTest { } assertThat(localChanges).hasSize(1) - // val localChange = localChanges[0].localChange with(localChanges[0]) { assertThat(this.resourceType).isEqualTo(ResourceType.Patient.toString()) assertThat(this.resourceId).isEqualTo(TEST_PATIENT_1.id) @@ -340,7 +340,7 @@ class FhirEngineImplTest { private fun buildPatient( patientId: String, name: String, - patientGender: Enumerations.AdministrativeGender + patientGender: Enumerations.AdministrativeGender, ) = Patient().apply { id = patientId @@ -414,7 +414,7 @@ class FhirEngineImplTest { assertThat(get(0).payload).isEqualTo(patientString) } assertResourceEquals(patient, fhirEngine.get(ResourceType.Patient, patient.logicalId)) - // clear databse + // clear database runBlocking(Dispatchers.IO) { fhirEngine.clearDatabase() } // assert that previously present resource not available after clearing database assertThat(fhirEngine.getLocalChanges(patient.resourceType, patient.logicalId)).isEmpty() @@ -436,7 +436,7 @@ class FhirEngineImplTest { } assertThat(resourceNotFoundException.message) .isEqualTo( - "Resource not found with type ${TEST_PATIENT_1.resourceType.name} and id $TEST_PATIENT_1_ID!" + "Resource not found with type ${TEST_PATIENT_1.resourceType.name} and id $TEST_PATIENT_1_ID!", ) assertThat(fhirEngine.getLocalChanges(ResourceType.Patient, TEST_PATIENT_1_ID)).isEmpty() } @@ -450,7 +450,7 @@ class FhirEngineImplTest { } assertThat(resourceIllegalStateException.message) .isEqualTo( - "Resource with type ${TEST_PATIENT_1.resourceType.name} and id $TEST_PATIENT_1_ID has local changes, either sync with server or FORCE_PURGE required" + "Resource with type ${TEST_PATIENT_1.resourceType.name} and id $TEST_PATIENT_1_ID has local changes, either sync with server or FORCE_PURGE required", ) } @@ -462,9 +462,10 @@ class FhirEngineImplTest { } assertThat(resourceNotFoundException.message) .isEqualTo( - "Resource not found with type ${TEST_PATIENT_1.resourceType.name} and id nonexistent_patient!" + "Resource not found with type ${TEST_PATIENT_1.resourceType.name} and id nonexistent_patient!", ) } + fun syncDownload_conflictResolution_acceptRemote_shouldHaveNoLocalChangeAnymore() = runBlocking { val originalPatient = Patient().apply { @@ -478,7 +479,7 @@ class FhirEngineImplTest { HumanName().apply { family = "Stark" addGiven("Tony") - } + }, ) } fhirEngine.syncDownload(AcceptRemoteConflictResolver) { flowOf((listOf((originalPatient)))) } @@ -500,7 +501,7 @@ class FhirEngineImplTest { fhirEngine.syncDownload(AcceptRemoteConflictResolver) { flowOf((listOf(remoteChange))) } assertThat( - services.database.getAllLocalChanges().filter { it.resourceId == "Patient/original-001" } + services.database.getAllLocalChanges().filter { it.resourceId == "Patient/original-001" }, ) .isEmpty() assertResourceEquals(fhirEngine.get("original-001"), remoteChange) @@ -521,7 +522,7 @@ class FhirEngineImplTest { HumanName().apply { family = "Stark" addGiven("Tony") - } + }, ) } fhirEngine.syncDownload(AcceptLocalConflictResolver) { flowOf((listOf((originalPatient)))) } @@ -535,7 +536,7 @@ class FhirEngineImplTest { Address().apply { city = "Malibu" state = "California" - } + }, ) } fhirEngine.update(localChange) @@ -555,7 +556,7 @@ class FhirEngineImplTest { val localChangeDiff = """[{"op":"remove","path":"\/address\/0\/country"},{"op":"add","path":"\/address\/0\/city","value":"Malibu"},{"op":"add","path":"\/address\/-","value":{"city":"Malibu","state":"California"}}]""" assertThat( - services.database.getAllLocalChanges().first { it.resourceId == "original-002" }.payload + services.database.getAllLocalChanges().first { it.resourceId == "original-002" }.payload, ) .isEqualTo(localChangeDiff) assertResourceEquals(fhirEngine.get("original-002"), localChange) @@ -575,7 +576,7 @@ class FhirEngineImplTest { { value = of(DateTimeType(Date.from(localChangeTimestamp))) prefix = ParamPrefixEnum.EQUAL - } + }, ) } @@ -597,7 +598,7 @@ class FhirEngineImplTest { HumanName().apply { addGiven("John") family = "Doe" - } + }, ) } fhirEngine.update(patientUpdate) @@ -611,7 +612,7 @@ class FhirEngineImplTest { { value = of(DateTimeType(Date.from(localChangeTimestampWhenUpdated))) prefix = ParamPrefixEnum.EQUAL - } + }, ) } diff --git a/engine/src/test/java/com/google/android/fhir/sync/upload/AllChangesLocalChangeFetcherTest.kt b/engine/src/test/java/com/google/android/fhir/sync/upload/AllChangesLocalChangeFetcherTest.kt new file mode 100644 index 0000000000..b14637f446 --- /dev/null +++ b/engine/src/test/java/com/google/android/fhir/sync/upload/AllChangesLocalChangeFetcherTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2023 Google LLC + * + * 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 com.google.android.fhir.sync.upload + +import androidx.test.core.app.ApplicationProvider +import com.google.android.fhir.FhirServices +import com.google.common.truth.Truth.assertThat +import java.util.Date +import kotlinx.coroutines.test.runTest +import org.hl7.fhir.r4.model.Enumerations +import org.hl7.fhir.r4.model.Meta +import org.hl7.fhir.r4.model.Patient +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class AllChangesLocalChangeFetcherTest { + private val services = + FhirServices.builder(ApplicationProvider.getApplicationContext()).inMemory().build() + private val database = services.database + private lateinit var fetcher: AllChangesLocalChangeFetcher + + @Before + fun setup() = runTest { + database.insert(TEST_PATIENT_1, TEST_PATIENT_2) + fetcher = AllChangesLocalChangeFetcher(database).apply { initTotalCount() } + } + + @Test + fun `next returns all the localChanges`() = runTest { + val localChanges = fetcher.next() + assertThat(fetcher.next().size).isEqualTo(2) + assertThat(localChanges.map { it.resourceId }) + .containsExactly(TEST_PATIENT_1_ID, TEST_PATIENT_2_ID) + } + + @Test + fun `hasNext returns true when there are local changes`() = runTest { + assertThat(fetcher.hasNext()).isTrue() + } + + @Test + fun `hasNext returns false when there are no local changes`() = runTest { + database.deleteUpdates(listOf(TEST_PATIENT_1, TEST_PATIENT_2)) + assertThat(fetcher.hasNext()).isFalse() + } + + @Test + fun `getProgress when all local changes are removed`() = runTest { + database.deleteUpdates(listOf(TEST_PATIENT_1, TEST_PATIENT_2)) + assertThat(fetcher.getProgress()).isEqualTo(FetchProgress(0, 2)) + } + + @Test + fun `getProgress when half the local changes are removed`() = runTest { + database.deleteUpdates(listOf(TEST_PATIENT_1)) + assertThat(fetcher.getProgress()).isEqualTo(FetchProgress(1, 2)) + } + + @Test + fun `getProgress when none of the local changes are removed`() = runTest { + assertThat(fetcher.getProgress()).isEqualTo(FetchProgress(2, 2)) + } + + companion object { + private const val TEST_PATIENT_1_ID = "test_patient_1" + private var TEST_PATIENT_1 = + Patient().apply { + id = TEST_PATIENT_1_ID + gender = Enumerations.AdministrativeGender.MALE + } + + private const val TEST_PATIENT_2_ID = "test_patient_2" + private var TEST_PATIENT_2 = + Patient().apply { + id = TEST_PATIENT_2_ID + gender = Enumerations.AdministrativeGender.MALE + meta = Meta().apply { lastUpdated = Date() } + } + } +}