Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resource Creation using POST http verb (SingleResourcePost) #2464

Merged
merged 23 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ 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.sync.upload.ResourceUploadResponseMapping
import com.google.android.fhir.sync.upload.UploadRequestResult
import com.google.android.fhir.sync.upload.UploadStrategy.AllChangesSquashedBundlePut
import com.google.android.fhir.testing.assertJsonArrayEqualsIgnoringOrder
import com.google.android.fhir.testing.assertResourceEquals
import com.google.android.fhir.testing.readFromFile
Expand Down Expand Up @@ -552,7 +552,7 @@ class DatabaseImplTest {
// 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) {
.syncUpload(AllChangesSquashedBundlePut) {
MJ1998 marked this conversation as resolved.
Show resolved Hide resolved
it
.first { it.resourceId == "remote-patient-3" }
.let {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/*
* Copyright 2024 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 android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import ca.uhn.fhir.context.FhirContext
import com.google.android.fhir.FhirServices
import com.google.android.fhir.db.Database
import com.google.android.fhir.db.ResourceNotFoundException
import com.google.android.fhir.logicalId
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.hl7.fhir.r4.model.DomainResource
import org.hl7.fhir.r4.model.InstantType
import org.hl7.fhir.r4.model.Observation
import org.hl7.fhir.r4.model.ResourceType
import org.junit.After
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized

@RunWith(AndroidJUnit4::class)
class HttpPostResourceConsolidatorTest {
@JvmField @Parameterized.Parameter(0) var encrypted: Boolean = false

private val context: Context = ApplicationProvider.getApplicationContext()
private lateinit var database: Database
private lateinit var resourceConsolidator: ResourceConsolidator

@Before
fun setupDatabase() = runBlocking {
database =
FhirServices.builder(context)
.inMemory()
.apply {
if (encrypted) enableEncryptionIfSupported()
setSearchParameters(null)
}
.build()
.database
resourceConsolidator = HttpPostResourceConsolidator(database)
}

@After
fun closeDatabase() {
database.close()
}

@Test
fun consolidate_shouldUpdateResourceId() = runBlocking {
val patientJsonString =
"""
{
"resourceType": "Patient",
"id": "patient1"
}
"""
.trimIndent()
val patient =
FhirContext.forR4Cached().newJsonParser().parseResource(patientJsonString) as DomainResource
database.insert(patient)
val localChanges = database.getLocalChanges(patient.resourceType, patient.logicalId)

val postSyncPatientJsonString =
"""
{
"resourceType": "Patient",
"id": "patient2",
"meta": {
"versionId": "1"
}
}
"""
.trimIndent()
val postSyncPatient =
FhirContext.forR4Cached().newJsonParser().parseResource(postSyncPatientJsonString)
as DomainResource
postSyncPatient.meta.lastUpdatedElement = InstantType.now()
val uploadRequestResult =
UploadRequestResult.Success(
listOf(ResourceUploadResponseMapping(localChanges, postSyncPatient)),
)
resourceConsolidator.consolidate(uploadRequestResult)

assertThat(database.select(ResourceType.Patient, "patient2").logicalId)
.isEqualTo(postSyncPatient.logicalId)

val exception =
assertThrows(ResourceNotFoundException::class.java) {
runBlocking { database.select(ResourceType.Patient, "patient1") }
}

assertThat(exception.message).isEqualTo("Resource not found with type Patient and id patient1!")
}

@Test
fun consolidate_dependentResources_shouldUpdateReferenceValue() = runBlocking {
val patientJsonString =
"""
{
"resourceType": "Patient",
"id": "patient1"
}
"""
.trimIndent()
val patient =
FhirContext.forR4Cached().newJsonParser().parseResource(patientJsonString) as DomainResource
val observationJsonString =
"""
{
"resourceType": "Observation",
"id": "observation1",
"subject": {
"reference": "Patient/patient1"
}
}
"""
.trimIndent()
val observation =
FhirContext.forR4Cached().newJsonParser().parseResource(observationJsonString)
as DomainResource
database.insert(patient, observation)
val postSyncPatientJsonString =
"""
{
"resourceType": "Patient",
"id": "patient2",
"meta": {
"versionId": "1"
}
}
"""
.trimIndent()
val postSyncPatient =
FhirContext.forR4Cached().newJsonParser().parseResource(postSyncPatientJsonString)
as DomainResource
postSyncPatient.meta.lastUpdatedElement = InstantType.now()
val localChanges = database.getLocalChanges(patient.resourceType, patient.logicalId)
val uploadRequestResult =
UploadRequestResult.Success(
listOf(ResourceUploadResponseMapping(localChanges, postSyncPatient)),
)

resourceConsolidator.consolidate(uploadRequestResult)

assertThat(
(database.select(ResourceType.Observation, "observation1") as Observation)
.subject
.reference,
)
.isEqualTo("Patient/patient2")
}

@Test
fun consolidate_localChanges_shouldUpdateReferenceValue() = runBlocking {
val patientJsonString =
"""
{
"resourceType": "Patient",
"id": "patient1"
}
"""
.trimIndent()
val patient =
FhirContext.forR4Cached().newJsonParser().parseResource(patientJsonString) as DomainResource
val observationJsonString =
"""
{
"resourceType": "Observation",
"id": "observation1",
"subject": {
"reference": "Patient/patient1"
}
}
"""
.trimIndent()
val observation =
FhirContext.forR4Cached().newJsonParser().parseResource(observationJsonString)
as DomainResource
database.insert(patient, observation)
val postSyncPatientJsonString =
"""
{
"resourceType": "Patient",
"id": "patient2",
"meta": {
"versionId": "1"
}
}
"""
.trimIndent()
val postSyncPatient =
FhirContext.forR4Cached().newJsonParser().parseResource(postSyncPatientJsonString)
as DomainResource
postSyncPatient.meta.lastUpdatedElement = InstantType.now()
val localChanges = database.getLocalChanges(patient.resourceType, patient.logicalId)
val uploadRequestResult =
UploadRequestResult.Success(
listOf(ResourceUploadResponseMapping(localChanges, postSyncPatient)),
)

resourceConsolidator.consolidate(uploadRequestResult)

val localChange = database.getLocalChanges(ResourceType.Observation, "observation1").last()
assertThat(
(FhirContext.forR4Cached().newJsonParser().parseResource(localChange.payload)
as Observation)
.subject
.reference,
)
.isEqualTo("Patient/patient2")
}
}
4 changes: 2 additions & 2 deletions engine/src/main/java/com/google/android/fhir/FhirEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ 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 com.google.android.fhir.sync.upload.SyncUploadProgress
import com.google.android.fhir.sync.upload.UploadRequestResult
import com.google.android.fhir.sync.upload.UploadStrategy
import java.time.OffsetDateTime
import kotlinx.coroutines.flow.Flow
import org.hl7.fhir.r4.model.Resource
Expand Down Expand Up @@ -130,7 +130,7 @@ interface FhirEngine {
*/
@Deprecated("To be deprecated.")
suspend fun syncUpload(
localChangesFetchMode: LocalChangesFetchMode,
uploadStrategy: UploadStrategy,
upload: (suspend (List<LocalChange>) -> Flow<UploadRequestResult>),
): Flow<SyncUploadProgress>

Expand Down
16 changes: 16 additions & 0 deletions engine/src/main/java/com/google/android/fhir/db/Database.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ internal interface Database {
lastUpdated: Instant,
)

/**
* Updates existing [Resource] present in the [ResourceEntity] for metadata such as versionId,
* resourceId, lastModifiedTime, and reference value for other referring resources. In the
* [LocalChangeEntity] table, it only updates the reference value for referring resources. This
* method is more suitable if [preSyncResourceId] and post-sync resourceId [postSyncResource] are
* different. However, even if [preSyncResourceId] and post-sync resourceId are the same, it still
* updates the reference value of referring resources, which is just redundant.
*
* @param preSyncResourceId The [Resource.id] of the resource before synchronization.
* @param postSyncResource The [Resource] after synchronization.
*/
suspend fun updateResourcesAndLocalChangesPostSync(
MJ1998 marked this conversation as resolved.
Show resolved Hide resolved
preSyncResourceId: String,
postSyncResource: Resource,
)

/**
* Selects the FHIR resource of type `clazz` with `id`.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,36 @@ internal class DatabaseImpl(
}
}

override suspend fun updateResourcesAndLocalChangesPostSync(
preSyncResourceId: String,
postSyncResource: Resource,
) {
val preSyncResource =
iParser.parseResource(
selectEntity(postSyncResource.resourceType, preSyncResourceId).serializedResource,
) as Resource

preSyncResource.let {
db.withTransaction {
resourceDao.updateResourcePostSync(
preSyncResourceId,
postSyncResource,
)

updateReferringResources(
referringResourcesUuids = getResourceUuidsThatRefereceTheGivenResource(it),
oldResource = it,
updatedResource = postSyncResource,
)

localChangeDao.updateReferencesInLocalChange(
oldResource = it,
updatedResource = postSyncResource,
)
}
santosh-pingle marked this conversation as resolved.
Show resolved Hide resolved
}
}

override suspend fun select(type: ResourceType, id: String): Resource {
return db.withTransaction {
resourceDao.getResource(resourceId = id, resourceType = type)?.let {
Expand Down Expand Up @@ -423,6 +453,31 @@ internal class DatabaseImpl(
}
}

/**
* Retrieves a list of UUIDs for resources that reference [preSyncResource]. [preSyncResource] can
* be referenced as the reference value in other resources, returning those resource UUIDs.
* Essentially, [LocalChangeResourceReference] contains
* [LocalChangeResourceReference.resourceReferenceValue] and
* [LocalChangeResourceReference.localChangeId]. [LocalChange] contains UUIDs for every resource.
*
* @param preSyncResource The resource that is being referenced.
* @return A list of UUIDs of resources that reference [preSyncResource].
*/
private suspend fun getResourceUuidsThatRefereceTheGivenResource(
preSyncResource: Resource,
): List<UUID> {
return db.withTransaction {
val preSyncReference = "${preSyncResource.resourceType.name}/${preSyncResource.logicalId}"
val localChangeIds =
localChangeDao
.getLocalChangeReferencesWithValue(preSyncReference)
.map { it.localChangeId }
.distinct()
val localChanges = localChangeDao.getLocalChanges(localChangeIds)
localChanges.map { it.resourceUuid }.distinct()
}
}

companion object {
/**
* The name for unencrypted database.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ internal abstract class LocalChangeDao {
* place. This method returns a list of the [ResourceEntity.resourceUuid] for all the resources
* whose [LocalChange] contained references to the oldResource
*/
private suspend fun updateReferencesInLocalChange(
internal suspend fun updateReferencesInLocalChange(
santosh-pingle marked this conversation as resolved.
Show resolved Hide resolved
oldResource: Resource,
updatedResource: Resource,
): List<UUID> {
Expand Down
Loading
Loading