Skip to content

Commit

Permalink
Resource Creation using POST http verb (SingleResourcePost) (#2464)
Browse files Browse the repository at this point in the history
* draft singleresourcepost

* Remove dead code.

* resource consolidation after  post http verb request

* Remove local changes.

* fix unit tests.

* unit tests

* Update kotlin api docs.

* revert local changes.

* Resource consolidation as per http verb

* address review comments.

* order of arguments

* code to string conversion

* Address review comments.

* Fix version id and use existing api.

* Address review comments.

* Add unit tests.

* Address review comments.

---------

Co-authored-by: Santosh Pingle <spingle@google.com>
  • Loading branch information
santosh-pingle and Santosh Pingle committed May 28, 2024
1 parent 8e2aaf0 commit d83692e
Show file tree
Hide file tree
Showing 13 changed files with 456 additions and 77 deletions.
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) {
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
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ internal abstract class ResourceDao {
resourceId = updatedResource.logicalId,
serializedResource = iParser.encodeResourceToString(updatedResource),
lastUpdatedRemote = updatedResource.meta.lastUpdated?.toInstant() ?: it.lastUpdatedRemote,
versionId = updatedResource.meta.versionId,
)
updateChanges(entity, updatedResource)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ import com.google.android.fhir.search.count
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 com.google.android.fhir.sync.upload.ResourceConsolidatorFactory
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 kotlinx.coroutines.flow.firstOrNull
Expand Down Expand Up @@ -132,11 +132,13 @@ internal class FhirEngineImpl(private val database: Database, private val contex
.intersect(database.getAllLocalChanges().map { it.resourceId }.toSet())

override suspend fun syncUpload(
localChangesFetchMode: LocalChangesFetchMode,
uploadStrategy: UploadStrategy,
upload: (suspend (List<LocalChange>) -> Flow<UploadRequestResult>),
): Flow<SyncUploadProgress> = flow {
val resourceConsolidator = DefaultResourceConsolidator(database)
val localChangeFetcher = LocalChangeFetcherFactory.byMode(localChangesFetchMode, database)
val resourceConsolidator =
ResourceConsolidatorFactory.byHttpVerb(uploadStrategy.requestGeneratorMode, database)
val localChangeFetcher =
LocalChangeFetcherFactory.byMode(uploadStrategy.localChangesFetchMode, database)

emit(
SyncUploadProgress(
Expand Down
22 changes: 12 additions & 10 deletions engine/src/main/java/com/google/android/fhir/sync/FhirSyncWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,18 @@ abstract class FhirSyncWorker(appContext: Context, workerParams: WorkerParameter
FhirSynchronizer(
getFhirEngine(),
UploadConfiguration(
Uploader(
dataSource = dataSource,
patchGenerator =
PatchGeneratorFactory.byMode(
getUploadStrategy().patchGeneratorMode,
FhirEngineProvider.getFhirDatabase(applicationContext),
),
requestGenerator =
UploadRequestGeneratorFactory.byMode(getUploadStrategy().requestGeneratorMode),
),
uploader =
Uploader(
dataSource = dataSource,
patchGenerator =
PatchGeneratorFactory.byMode(
getUploadStrategy().patchGeneratorMode,
FhirEngineProvider.getFhirDatabase(applicationContext),
),
requestGenerator =
UploadRequestGeneratorFactory.byMode(getUploadStrategy().requestGeneratorMode),
),
uploadStrategy = getUploadStrategy(),
),
DownloadConfiguration(
DownloaderImpl(dataSource, getDownloadWorkManager()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package com.google.android.fhir.sync
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.UploadStrategy
import com.google.android.fhir.sync.upload.Uploader
import java.time.OffsetDateTime
import kotlinx.coroutines.flow.MutableSharedFlow
Expand All @@ -46,6 +46,7 @@ data class ResourceSyncException(val resourceType: ResourceType, val exception:

internal data class UploadConfiguration(
val uploader: Uploader,
val uploadStrategy: UploadStrategy,
)

internal class DownloadConfiguration(
Expand Down Expand Up @@ -131,18 +132,18 @@ internal class FhirSynchronizer(

private suspend fun upload(): SyncResult {
val exceptions = mutableListOf<ResourceSyncException>()
val localChangesFetchMode = LocalChangesFetchMode.AllChanges
fhirEngine.syncUpload(localChangesFetchMode, uploadConfiguration.uploader::upload).collect {
progress ->
progress.uploadError?.let { exceptions.add(it) }
?: setSyncState(
SyncJobStatus.InProgress(
SyncOperation.UPLOAD,
progress.initialTotal,
progress.initialTotal - progress.remaining,
),
)
}
fhirEngine
.syncUpload(uploadConfiguration.uploadStrategy, uploadConfiguration.uploader::upload)
.collect { progress ->
progress.uploadError?.let { exceptions.add(it) }
?: setSyncState(
SyncJobStatus.InProgress(
SyncOperation.UPLOAD,
progress.initialTotal,
progress.initialTotal - progress.remaining,
),
)
}

return if (exceptions.isEmpty()) {
SyncResult.Success()
Expand Down
Loading

0 comments on commit d83692e

Please sign in to comment.