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 all 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
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
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)
santosh-pingle marked this conversation as resolved.
Show resolved Hide resolved
.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
Loading