From fa51f691cf4c3fecf86f8b7250386112a8e34cc9 Mon Sep 17 00:00:00 2001 From: aditya-07 Date: Thu, 17 Feb 2022 14:57:19 +0530 Subject: [PATCH] Store remote version id for the resources and provide the version id for local change entity (#1127) * Store remote version id for the resources and provide the version id for local change entity * Review comment changes * Added additional test case * Updated the Database api to accept non-nullable version and lastupdate values * review comment: updated variable names --- .../android/fhir/db/impl/DatabaseImplTest.kt | 150 ++++++++++++++---- .../com/google/android/fhir/FhirEngine.kt | 7 +- .../com/google/android/fhir/db/Database.kt | 19 +++ .../android/fhir/db/impl/DatabaseImpl.kt | 43 ++++- .../android/fhir/db/impl/DbTypeConverters.kt | 7 + .../fhir/db/impl/dao/LocalChangeDao.kt | 22 ++- .../fhir/db/impl/dao/LocalChangeUtils.kt | 10 +- .../android/fhir/db/impl/dao/ResourceDao.kt | 42 ++++- .../db/impl/entities/LocalChangeEntity.kt | 3 +- .../fhir/db/impl/entities/ResourceEntity.kt | 5 +- .../android/fhir/impl/FhirEngineImpl.kt | 15 +- .../google/android/fhir/resource/Resources.kt | 6 + .../android/fhir/sync/FhirSynchronizer.kt | 4 +- .../android/fhir/resource/TestingUtils.kt | 2 +- .../android/fhir/impl/FhirEngineImplTest.kt | 2 +- 15 files changed, 277 insertions(+), 60 deletions(-) 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 2322248991..490508fe16 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 @@ -26,20 +26,19 @@ import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.db.impl.entities.LocalChangeEntity import com.google.android.fhir.logicalId import com.google.android.fhir.resource.TestingUtils +import com.google.android.fhir.resource.versionId import com.google.android.fhir.search.Operation import com.google.android.fhir.search.Order import com.google.android.fhir.search.Search import com.google.android.fhir.search.StringFilterModifier import com.google.android.fhir.search.getQuery import com.google.android.fhir.search.has -import com.google.android.fhir.sync.DataSource import com.google.common.truth.Truth.assertThat import java.math.BigDecimal import java.time.Instant -import kotlin.collections.ArrayList +import java.util.Date import kotlinx.coroutines.runBlocking import org.hl7.fhir.r4.model.Address -import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.CarePlan import org.hl7.fhir.r4.model.CodeableConcept import org.hl7.fhir.r4.model.Coding @@ -50,8 +49,8 @@ import org.hl7.fhir.r4.model.DecimalType import org.hl7.fhir.r4.model.Enumerations import org.hl7.fhir.r4.model.HumanName import org.hl7.fhir.r4.model.Immunization +import org.hl7.fhir.r4.model.Meta import org.hl7.fhir.r4.model.Observation -import org.hl7.fhir.r4.model.OperationOutcome import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Practitioner import org.hl7.fhir.r4.model.Quantity @@ -81,33 +80,6 @@ class DatabaseImplTest { /** Whether to run the test with encryption on or off. */ @JvmField @Parameterized.Parameter(0) var encrypted: Boolean = false - private val dataSource = - object : DataSource { - - override suspend fun loadData(path: String): Bundle { - return Bundle() - } - - override suspend fun insert( - resourceType: String, - resourceId: String, - payload: String - ): Resource { - return Patient() - } - - override suspend fun update( - resourceType: String, - resourceId: String, - payload: String - ): OperationOutcome { - return OperationOutcome() - } - - override suspend fun delete(resourceType: String, resourceId: String): OperationOutcome { - return OperationOutcome() - } - } private val context: Context = ApplicationProvider.getApplicationContext() private val services = FhirServices.builder(context) @@ -231,6 +203,49 @@ class DatabaseImplTest { assertThat(payload).isEqualTo(patientString) } + @Test + fun update_remoteResourceWithLocalChange_shouldSaveVersionIdAndLastUpdated() = runBlocking { + val patient = + Patient().apply { + id = "remote-patient-1" + addName( + HumanName().apply { + family = "FamilyName" + addGiven("FirstName") + } + ) + meta = + Meta().apply { + versionId = "remote-patient-1-version-001" + lastUpdated = Date() + } + } + + database.insertRemote(patient) + + val updatedPatient = + Patient().apply { + id = "remote-patient-1" + addName( + HumanName().apply { + family = "UpdatedFamilyName" + addGiven("UpdatedFirstName") + } + ) + } + database.update(updatedPatient) + + val selectedEntity = database.selectEntity(Patient::class.java, "remote-patient-1") + assertThat(selectedEntity.resourceId).isEqualTo("remote-patient-1") + assertThat(selectedEntity.versionId).isEqualTo(patient.meta.versionId) + assertThat(selectedEntity.lastUpdatedRemote).isEqualTo(patient.meta.lastUpdated.toInstant()) + + val squashedLocalChange = + database.getAllLocalChanges().first { it.localChange.resourceId == "remote-patient-1" } + assertThat(squashedLocalChange.localChange.resourceId).isEqualTo("remote-patient-1") + assertThat(squashedLocalChange.localChange.versionId).isEqualTo(patient.meta.versionId) + } + @Test fun delete_shouldAddDeleteLocalChange() = runBlocking { database.delete(Patient::class.java, TEST_PATIENT_1_ID) @@ -285,6 +300,64 @@ class DatabaseImplTest { .isTrue() } + @Test + fun insert_remoteResource_shouldSaveVersionIdAndLastUpdated() = runBlocking { + val patient = + Patient().apply { + id = "remote-patient-1" + meta = + Meta().apply { + versionId = "remote-patient-1-version-1" + lastUpdated = Date() + } + } + database.insertRemote(patient) + val selectedEntity = database.selectEntity(Patient::class.java, "remote-patient-1") + assertThat(selectedEntity.versionId).isEqualTo("remote-patient-1-version-1") + assertThat(selectedEntity.lastUpdatedRemote).isEqualTo(patient.meta.lastUpdated.toInstant()) + } + + @Test + fun insert_remoteResourceWithNoMeta_shouldSaveNullRemoteVersionAndLastUpdated() = runBlocking { + val patient = Patient().apply { id = "remote-patient-2" } + database.insertRemote(patient) + val selectedEntity = database.selectEntity(Patient::class.java, "remote-patient-2") + assertThat(selectedEntity.versionId).isNull() + assertThat(selectedEntity.lastUpdatedRemote).isNull() + } + + @Test + fun insert_localResourceWithNoMeta_shouldSaveNullRemoteVersionAndLastUpdated() = runBlocking { + val patient = Patient().apply { id = "local-patient-2" } + database.insert(patient) + val selectedEntity = database.selectEntity(Patient::class.java, "local-patient-2") + assertThat(selectedEntity.versionId).isNull() + assertThat(selectedEntity.lastUpdatedRemote).isNull() + } + + @Test + fun insert_localResourceWithNoMetaAndSync_shouldSaveRemoteVersionAndLastUpdated() = runBlocking { + val patient = Patient().apply { id = "remote-patient-3" } + val remoteMeta = + Meta().apply { + versionId = "remote-patient-3-version-001" + lastUpdated = Date() + } + database.insert(patient) + services.fhirEngine.syncUpload { + it.map { + it.token to + Patient().apply { + id = it.localChange.resourceId + meta = remoteMeta + } + } + } + val selectedEntity = database.selectEntity(Patient::class.java, "remote-patient-3") + assertThat(selectedEntity.versionId).isEqualTo(remoteMeta.versionId) + assertThat(selectedEntity.lastUpdatedRemote).isEqualTo(remoteMeta.lastUpdated.toInstant()) + } + @Test fun insertAll_remoteResources_shouldNotInsertAnyLocalChange() = runBlocking { val patient: Patient = testingUtils.readFromFile(Patient::class.java, "/date_test_patient.json") @@ -318,14 +391,20 @@ class DatabaseImplTest { @Test fun updateTwice_remoteResource_readSquashedChanges_shouldReturnMergedPatch() = runBlocking { + val remoteMeta = + Meta().apply { + versionId = "patient-version-1" + lastUpdated = Date() + } var patient: Patient = testingUtils.readFromFile(Patient::class.java, "/date_test_patient.json") + patient.meta = remoteMeta database.insertRemote(patient) patient = testingUtils.readFromFile(Patient::class.java, "/update_test_patient_1.json") database.update(patient) patient = testingUtils.readFromFile(Patient::class.java, "/update_test_patient_2.json") database.update(patient) val updatePatch = testingUtils.readJsonArrayFromFile("/update_patch_2.json") - val (_, resourceType, resourceId, _, type, payload) = + val (_, resourceType, resourceId, _, type, payload, versionId) = database .getAllLocalChanges() .single { it.localChange.resourceId.equals(patient.logicalId) } @@ -333,6 +412,8 @@ class DatabaseImplTest { assertThat(type).isEqualTo(LocalChangeEntity.Type.UPDATE) assertThat(resourceId).isEqualTo(patient.logicalId) assertThat(resourceType).isEqualTo(patient.resourceType.name) + assertThat(resourceType).isEqualTo(patient.resourceType.name) + assertThat(versionId).isEqualTo(remoteMeta.versionId) testingUtils.assertJsonArrayEqualsIgnoringOrder(JSONArray(payload), updatePatch) } @@ -340,13 +421,14 @@ class DatabaseImplTest { fun delete_remoteResource_shouldReturnDeleteLocalChange() = runBlocking { database.insertRemote(TEST_PATIENT_2) database.delete(Patient::class.java, TEST_PATIENT_2_ID) - val (_, resourceType, resourceId, _, type, payload) = + val (_, resourceType, resourceId, _, type, payload, versionId) = database.getAllLocalChanges().map { it.localChange }.single { it.resourceId.equals(TEST_PATIENT_2_ID) } assertThat(type).isEqualTo(LocalChangeEntity.Type.DELETE) assertThat(resourceId).isEqualTo(TEST_PATIENT_2_ID) assertThat(resourceType).isEqualTo(TEST_PATIENT_2.resourceType.name) + assertThat(versionId).isEqualTo(TEST_PATIENT_2.versionId) assertThat(payload).isEmpty() } @@ -2562,6 +2644,7 @@ class DatabaseImplTest { val TEST_PATIENT_1 = Patient() init { + TEST_PATIENT_1.meta.id = "v1-of-patient1" TEST_PATIENT_1.setId(TEST_PATIENT_1_ID) TEST_PATIENT_1.setGender(Enumerations.AdministrativeGender.MALE) } @@ -2570,6 +2653,7 @@ class DatabaseImplTest { val TEST_PATIENT_2 = Patient() init { + TEST_PATIENT_2.meta.id = "v1-of-patient2" TEST_PATIENT_2.setId(TEST_PATIENT_2_ID) TEST_PATIENT_2.setGender(Enumerations.AdministrativeGender.MALE) } 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 14733fca2d..3322282f7e 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt @@ -67,12 +67,13 @@ interface FhirEngine { * Synchronizes the [upload] result in the database. The database will be updated to reflect the * result of the [upload] operation. */ - suspend fun syncUpload(upload: (suspend (List) -> List)) + suspend fun syncUpload( + upload: (suspend (List) -> List>) + ) /** * Synchronizes the [download] result in the database. The database will be updated to reflect the - * result of the [download] operation. [onPageDownloaded] is called with the resources after each - * successful download of page. + * result of the [download] operation. */ suspend fun syncDownload(download: suspend (SyncDownloadContext) -> Flow>) 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 2077350698..42ac178370 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 @@ -19,8 +19,10 @@ package com.google.android.fhir.db import com.google.android.fhir.db.impl.dao.LocalChangeToken import com.google.android.fhir.db.impl.dao.SquashedLocalChange import com.google.android.fhir.db.impl.entities.LocalChangeEntity +import com.google.android.fhir.db.impl.entities.ResourceEntity import com.google.android.fhir.db.impl.entities.SyncedResourceEntity import com.google.android.fhir.search.SearchQuery +import java.time.Instant import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType @@ -50,6 +52,14 @@ internal interface Database { */ suspend fun update(resource: R) + /** Updates the `resource` meta in the FHIR resource database. */ + suspend fun updateVersionIdAndLastUpdated( + resourceId: String, + resourceType: ResourceType, + versionId: String, + lastUpdated: Instant + ) + /** * Selects the FHIR resource of type `clazz` with `id`. * @@ -59,6 +69,15 @@ internal interface Database { @Throws(ResourceNotFoundException::class) suspend fun select(clazz: Class, id: String): R + /** + * Selects the saved `ResourceEntity` of type `clazz` with `id`. + * + * @param The resource type + * @throws ResourceNotFoundException if the resource is not found in the database + */ + @Throws(ResourceNotFoundException::class) + suspend fun selectEntity(clazz: Class, id: String): ResourceEntity + /** * Return the last update data of a resource based on the resource type. If no resource of * [resourceType] is inserted, return `null`. 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 ba0ffe1bf5..54793e3d06 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 @@ -29,10 +29,12 @@ import com.google.android.fhir.db.impl.dao.LocalChangeToken import com.google.android.fhir.db.impl.dao.LocalChangeUtils import com.google.android.fhir.db.impl.dao.SquashedLocalChange import com.google.android.fhir.db.impl.entities.LocalChangeEntity +import com.google.android.fhir.db.impl.entities.ResourceEntity import com.google.android.fhir.db.impl.entities.SyncedResourceEntity import com.google.android.fhir.logicalId import com.google.android.fhir.resource.getResourceType import com.google.android.fhir.search.SearchQuery +import java.time.Instant import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType @@ -111,9 +113,25 @@ internal class DatabaseImpl( override suspend fun update(resource: R) { db.withTransaction { - val oldResource = select(resource.javaClass, resource.logicalId) + val oldResourceEntity = selectEntity(resource.javaClass, resource.logicalId) resourceDao.update(resource) - localChangeDao.addUpdate(oldResource, resource) + localChangeDao.addUpdate(oldResourceEntity, resource) + } + } + + override suspend fun updateVersionIdAndLastUpdated( + resourceId: String, + resourceType: ResourceType, + versionId: String, + lastUpdated: Instant + ) { + db.withTransaction { + resourceDao.updateRemoteVersionIdAndLastUpdate( + resourceId, + resourceType, + versionId, + lastUpdated + ) } } @@ -143,9 +161,20 @@ internal class DatabaseImpl( override suspend fun delete(clazz: Class, id: String) { db.withTransaction { + val remoteVersionId: String? = + try { + selectEntity(clazz, id).versionId + } catch (e: ResourceNotFoundException) { + null + } val type = getResourceType(clazz) val rowsDeleted = resourceDao.deleteResource(resourceId = id, resourceType = type) - if (rowsDeleted > 0) localChangeDao.addDelete(resourceId = id, resourceType = type) + if (rowsDeleted > 0) + localChangeDao.addDelete( + resourceId = id, + resourceType = type, + remoteVersionId = remoteVersionId + ) } } @@ -180,6 +209,14 @@ internal class DatabaseImpl( db.withTransaction { localChangeDao.discardLocalChanges(token) } } + override suspend fun selectEntity(clazz: Class, id: String): ResourceEntity { + return db.withTransaction { + val type = getResourceType(clazz) + resourceDao.getResourceEntity(resourceId = id, resourceType = type) + ?: throw ResourceNotFoundException(type.name, id) + } + } + companion object { /** * The name for unencrypted database. diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/DbTypeConverters.kt b/engine/src/main/java/com/google/android/fhir/db/impl/DbTypeConverters.kt index 56027879eb..ff522e9c82 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/DbTypeConverters.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/DbTypeConverters.kt @@ -20,6 +20,7 @@ import androidx.room.TypeConverter import ca.uhn.fhir.model.api.TemporalPrecisionEnum import com.google.android.fhir.db.impl.entities.LocalChangeEntity import java.math.BigDecimal +import java.time.Instant import java.util.Calendar import org.hl7.fhir.r4.model.ResourceType @@ -74,4 +75,10 @@ internal object DbTypeConverters { @JvmStatic @TypeConverter fun intToLocalChangeType(value: Int): LocalChangeEntity.Type = LocalChangeEntity.Type.from(value) + + @JvmStatic @TypeConverter fun instantToLong(value: Instant?): Long? = value?.toEpochMilli() + + @JvmStatic + @TypeConverter + fun longToInstant(value: Long?): Instant? = value?.let { Instant.ofEpochMilli(it) } } 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 db50f68e1b..478cc410fb 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 @@ -23,7 +23,9 @@ import androidx.room.Transaction import ca.uhn.fhir.parser.IParser import com.google.android.fhir.db.impl.entities.LocalChangeEntity import com.google.android.fhir.db.impl.entities.LocalChangeEntity.Type +import com.google.android.fhir.db.impl.entities.ResourceEntity import com.google.android.fhir.logicalId +import com.google.android.fhir.resource.versionId import com.google.android.fhir.toTimeZoneString import java.util.Date import org.hl7.fhir.r4.model.Resource @@ -61,12 +63,13 @@ internal abstract class LocalChangeDao { resourceId = resourceId, timestamp = timestamp, type = Type.INSERT, - payload = resourceString + payload = resourceString, + versionId = resource.versionId ) ) } - suspend fun addUpdate(oldResource: Resource, resource: Resource) { + suspend fun addUpdate(oldEntity: ResourceEntity, resource: Resource) { val resourceId = resource.logicalId val resourceType = resource.resourceType val timestamp = Date().toTimeZoneString() @@ -78,7 +81,12 @@ internal abstract class LocalChangeDao { "Unexpected DELETE when updating $resourceType/$resourceId. UPDATE failed." ) } - val jsonDiff = LocalChangeUtils.diff(iParser, oldResource, resource) + val jsonDiff = + LocalChangeUtils.diff( + iParser, + iParser.parseResource(oldEntity.serializedResource) as Resource, + resource + ) if (jsonDiff.length() == 0) { Timber.i( "New resource ${resource.resourceType}/${resource.id} is same as old resource. " + @@ -93,12 +101,13 @@ internal abstract class LocalChangeDao { resourceId = resourceId, timestamp = timestamp, type = Type.UPDATE, - payload = jsonDiff.toString() + payload = jsonDiff.toString(), + versionId = oldEntity.versionId ) ) } - suspend fun addDelete(resourceId: String, resourceType: ResourceType) { + suspend fun addDelete(resourceId: String, resourceType: ResourceType, remoteVersionId: String?) { val timestamp = Date().toTimeZoneString() addLocalChange( LocalChangeEntity( @@ -107,7 +116,8 @@ internal abstract class LocalChangeDao { resourceId = resourceId, timestamp = timestamp, type = Type.DELETE, - payload = "" + payload = "", + versionId = remoteVersionId ) ) } diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeUtils.kt b/engine/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeUtils.kt index ab065bac3a..0b71fb34a5 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeUtils.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeUtils.kt @@ -33,16 +33,17 @@ internal object LocalChangeUtils { localChangeEntities.reduce { first, second -> mergeLocalChanges(first, second) } fun mergeLocalChanges(first: LocalChangeEntity, second: LocalChangeEntity): LocalChangeEntity { + // TODO (maybe this should throw exception when two entities don't have the same versionID) val type: LocalChangeEntity.Type val payload: String when (second.type) { LocalChangeEntity.Type.UPDATE -> - when { - first.type.equals(LocalChangeEntity.Type.UPDATE) -> { + when (first.type) { + LocalChangeEntity.Type.UPDATE -> { type = LocalChangeEntity.Type.UPDATE payload = mergePatches(first.payload, second.payload) } - first.type.equals(LocalChangeEntity.Type.INSERT) -> { + LocalChangeEntity.Type.INSERT -> { type = LocalChangeEntity.Type.INSERT payload = applyPatch(first.payload, second.payload) } @@ -66,7 +67,8 @@ internal object LocalChangeUtils { resourceId = second.resourceId, resourceType = second.resourceType, type = type, - payload = payload + payload = payload, + versionId = second.versionId ) } diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt b/engine/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt index 7c561f04cc..220fba6cb7 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt @@ -37,6 +37,9 @@ import com.google.android.fhir.db.impl.entities.UriIndexEntity import com.google.android.fhir.index.ResourceIndexer import com.google.android.fhir.index.ResourceIndices import com.google.android.fhir.logicalId +import com.google.android.fhir.resource.lastUpdated +import com.google.android.fhir.resource.versionId +import java.time.Instant import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType @@ -51,14 +54,17 @@ internal abstract class ResourceDao { updateResource( resource.logicalId, resource.resourceType, - iParser.encodeResourceToString(resource) + iParser.encodeResourceToString(resource), ) + val resourceEntity = getResourceEntity(resource.logicalId, resource.resourceType) val entity = ResourceEntity( id = 0, resourceType = resource.resourceType, resourceId = resource.logicalId, - serializedResource = iParser.encodeResourceToString(resource) + serializedResource = iParser.encodeResourceToString(resource), + resourceEntity?.versionId, + resourceEntity?.lastUpdatedRemote ) val index = ResourceIndexer.index(resource) updateIndicesForResource(index, entity) @@ -118,6 +124,22 @@ internal abstract class ResourceDao { serializedResource: String ) + @Query( + """ + UPDATE ResourceEntity + SET versionId = :versionId, + lastUpdatedRemote = :lastUpdatedRemote + WHERE resourceId = :resourceId + AND resourceType = :resourceType + """ + ) + abstract suspend fun updateRemoteVersionIdAndLastUpdate( + resourceId: String, + resourceType: ResourceType, + versionId: String?, + lastUpdatedRemote: Instant? + ) + @Query( """ DELETE FROM ResourceEntity @@ -133,6 +155,18 @@ internal abstract class ResourceDao { ) abstract suspend fun getResource(resourceId: String, resourceType: ResourceType): String? + @Query( + """ + SELECT * + FROM ResourceEntity + WHERE resourceId = :resourceId AND resourceType = :resourceType + """ + ) + abstract suspend fun getResourceEntity( + resourceId: String, + resourceType: ResourceType + ): ResourceEntity? + @Query( """ SELECT ResourceEntity.serializedResource @@ -196,7 +230,9 @@ internal abstract class ResourceDao { id = 0, resourceType = resource.resourceType, resourceId = resource.logicalId, - serializedResource = iParser.encodeResourceToString(resource) + serializedResource = iParser.encodeResourceToString(resource), + resource.versionId, + resource.lastUpdated ) insertResource(entity) val index = ResourceIndexer.index(resource) diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/entities/LocalChangeEntity.kt b/engine/src/main/java/com/google/android/fhir/db/impl/entities/LocalChangeEntity.kt index c5578a2515..ee398b1698 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/entities/LocalChangeEntity.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/entities/LocalChangeEntity.kt @@ -57,7 +57,8 @@ data class LocalChangeEntity( val resourceId: String, val timestamp: String = "", val type: Type, - val payload: String + val payload: String, + val versionId: String? ) { enum class Type(val value: Int) { INSERT(1), // create a new resource. payload is the entire resource json. diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/entities/ResourceEntity.kt b/engine/src/main/java/com/google/android/fhir/db/impl/entities/ResourceEntity.kt index fdac840bd0..6de611c8f0 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/entities/ResourceEntity.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/entities/ResourceEntity.kt @@ -19,6 +19,7 @@ package com.google.android.fhir.db.impl.entities import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey +import java.time.Instant import org.hl7.fhir.r4.model.ResourceType @Entity(indices = [Index(value = ["resourceType", "resourceId"], unique = true)]) @@ -26,5 +27,7 @@ internal data class ResourceEntity( @PrimaryKey(autoGenerate = true) val id: Long, val resourceType: ResourceType, val resourceId: String, - val serializedResource: String + val serializedResource: String, + val versionId: String?, + val lastUpdatedRemote: Instant? ) 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 9c84cc4be6..e8d7871238 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 @@ -83,11 +83,22 @@ internal class FhirEngineImpl(private val database: Database, private val contex } override suspend fun syncUpload( - upload: (suspend (List) -> List) + upload: (suspend (List) -> List>) ) { val localChanges = database.getAllLocalChanges() if (localChanges.isNotEmpty()) { - upload(localChanges).forEach { database.deleteUpdates(it) } + upload(localChanges).forEach { + database.deleteUpdates(it.first) + if (it.second.hasMeta() && it.second.meta.hasVersionId() && it.second.meta.hasLastUpdated() + ) { + database.updateVersionIdAndLastUpdated( + it.second.id, + it.second.resourceType, + it.second.meta.versionId, + it.second.meta.lastUpdated.toInstant() + ) + } + } } } } diff --git a/engine/src/main/java/com/google/android/fhir/resource/Resources.kt b/engine/src/main/java/com/google/android/fhir/resource/Resources.kt index d53bc34103..48c0cf03c9 100644 --- a/engine/src/main/java/com/google/android/fhir/resource/Resources.kt +++ b/engine/src/main/java/com/google/android/fhir/resource/Resources.kt @@ -48,3 +48,9 @@ internal fun getResourceClass(resourceType: String): Class { val className = resourceType.replace(Regex("\\{[^}]*\\}"), "") return Class.forName(R4_RESOURCE_PACKAGE_PREFIX + className) as Class } + +internal val Resource.versionId + get() = meta.versionId + +internal val Resource.lastUpdated + get() = if (hasMeta()) meta.lastUpdated?.toInstant() else null 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 ee3735f7fb..42b6dd2867 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 @@ -156,12 +156,12 @@ internal class FhirSynchronizer( val exceptions = mutableListOf() fhirEngine.syncUpload { list -> - val tokens = mutableListOf() + val tokens = mutableListOf>() list.forEach { try { val response: Resource = doUpload(it.localChange) if (response.logicalId == it.localChange.resourceId || response.isUploadSuccess()) { - tokens.add(it.token) + tokens.add(it.token to response) } else { // TODO improve exception message exceptions.add( diff --git a/engine/src/test-common/java/com/google/android/fhir/resource/TestingUtils.kt b/engine/src/test-common/java/com/google/android/fhir/resource/TestingUtils.kt index 5455eed11d..9ce6bbb880 100644 --- a/engine/src/test-common/java/com/google/android/fhir/resource/TestingUtils.kt +++ b/engine/src/test-common/java/com/google/android/fhir/resource/TestingUtils.kt @@ -121,7 +121,7 @@ class TestingUtils constructor(private val iParser: IParser) { } override suspend fun syncUpload( - upload: suspend (List) -> List + upload: suspend (List) -> List> ) { upload(listOf()) } 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 2760ab7d48..024ebf086d 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 @@ -116,7 +116,7 @@ class FhirEngineImplTest { var localChanges = mutableListOf() fhirEngine.syncUpload { it -> localChanges.addAll(it) - return@syncUpload it.map { it.token } + return@syncUpload it.map { it.token to TEST_PATIENT_1 } } assertThat(localChanges).hasSize(1)