Skip to content

Commit

Permalink
Refactor data migration to use ResouceConfig (#3000)
Browse files Browse the repository at this point in the history
Use resource configuration to allow fetching for base and related
resources.

Signed-off-by: Elly Kitoto <[email protected]>
Co-authored-by: Peter Lubell-Doughtie <[email protected]>
  • Loading branch information
ellykits and pld authored Jan 23, 2024
1 parent 6f7ac86 commit 6d593b8
Show file tree
Hide file tree
Showing 15 changed files with 194 additions and 171 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@
package org.smartregister.fhircore.engine.configuration.migration

import kotlinx.serialization.Serializable
import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.domain.model.DataQuery
import org.smartregister.fhircore.engine.domain.model.FhirResourceConfig
import org.smartregister.fhircore.engine.domain.model.RuleConfig

@Serializable
data class MigrationConfig(
val resourceType: ResourceType,
val updateValues: List<UpdateValueConfig>,
val dataQueries: List<DataQuery>?,
val resourceConfig: FhirResourceConfig,
val rules: List<RuleConfig>,
val version: Int,
val purgeAffectedResources: Boolean = false,
val resourceFilterExpression: ResourceFilterExpression? = null,
Expand All @@ -40,5 +39,5 @@ data class ResourceFilterExpression(
@Serializable
data class UpdateValueConfig(
val jsonPathExpression: String,
val valueRule: RuleConfig,
val computedValueKey: String,
) : java.io.Serializable
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,11 @@ import org.smartregister.fhircore.engine.configuration.register.ActiveResourceFi
import org.smartregister.fhircore.engine.data.local.register.RegisterRepository
import org.smartregister.fhircore.engine.domain.model.Code
import org.smartregister.fhircore.engine.domain.model.DataQuery
import org.smartregister.fhircore.engine.domain.model.FhirResourceConfig
import org.smartregister.fhircore.engine.domain.model.RelatedResourceCount
import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData
import org.smartregister.fhircore.engine.domain.model.ResourceConfig
import org.smartregister.fhircore.engine.domain.model.RuleConfig
import org.smartregister.fhircore.engine.domain.model.SortConfig
import org.smartregister.fhircore.engine.rulesengine.ConfigRulesExecutor
import org.smartregister.fhircore.engine.util.DispatcherProvider
Expand Down Expand Up @@ -826,6 +829,88 @@ constructor(
}
}

suspend fun searchResourcesRecursively(
filterActiveResources: List<ActiveResourceFilterConfig>?,
fhirResourceConfig: FhirResourceConfig,
secondaryResourceConfigs: List<FhirResourceConfig>?,
currentPage: Int? = null,
pageSize: Int? = null,
configRules: List<RuleConfig>?,
): List<RepositoryResourceData> {
val baseResourceConfig = fhirResourceConfig.baseResource
val relatedResourcesConfig = fhirResourceConfig.relatedResources
val configComputedRuleValues = configRules.configRulesComputedValues()
val search =
Search(type = baseResourceConfig.resource).apply {
applyConfiguredSortAndFilters(
resourceConfig = baseResourceConfig,
filterActiveResources = filterActiveResources,
sortData = true,
configComputedRuleValues = configComputedRuleValues,
)
if (currentPage != null && pageSize != null) {
count = pageSize
from = currentPage * pageSize
}
}

val baseFhirResources =
kotlin
.runCatching {
withContext(dispatcherProvider.io()) { fhirEngine.search<Resource>(search) }
}
.onFailure { Timber.e(it, "Error retrieving resources. Empty list returned by default") }
.getOrDefault(emptyList())

return baseFhirResources.map { searchResult ->
val retrievedRelatedResources =
withContext(dispatcherProvider.io()) {
retrieveRelatedResources(
resources = listOf(searchResult.resource),
relatedResourcesConfigs = relatedResourcesConfig,
relatedResourceWrapper = RelatedResourceWrapper(),
configComputedRuleValues = configComputedRuleValues,
)
}
RepositoryResourceData(
resourceRulesEngineFactId = baseResourceConfig.id ?: baseResourceConfig.resource.name,
resource = searchResult.resource,
relatedResourcesMap = retrievedRelatedResources.relatedResourceMap,
relatedResourcesCountMap = retrievedRelatedResources.relatedResourceCountMap,
secondaryRepositoryResourceData =
withContext(dispatcherProvider.io()) {
secondaryResourceConfigs.retrieveSecondaryRepositoryResourceData(
filterActiveResources,
)
},
)
}
}

protected fun List<RuleConfig>?.configRulesComputedValues(): Map<String, Any> {
if (this == null) return emptyMap()
val configRules = configRulesExecutor.generateRules(this)
return configRulesExecutor.fireRules(configRules)
}

/** This function fetches other resources that are not linked to the base/primary resource. */
protected suspend fun List<FhirResourceConfig>?.retrieveSecondaryRepositoryResourceData(
filterActiveResources: List<ActiveResourceFilterConfig>?,
): LinkedList<RepositoryResourceData> {
val secondaryRepositoryResourceDataLinkedList = LinkedList<RepositoryResourceData>()
this?.forEach {
secondaryRepositoryResourceDataLinkedList.addAll(
searchResourcesRecursively(
fhirResourceConfig = it,
filterActiveResources = filterActiveResources,
secondaryResourceConfigs = null,
configRules = null,
),
)
}
return secondaryRepositoryResourceDataLinkedList
}

/**
* A wrapper data class to hold search results. All related resources are flattened into one Map
* including the nested related resources as required by the Rules Engine facts.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,19 @@ package org.smartregister.fhircore.engine.data.local.register

import com.google.android.fhir.FhirEngine
import com.google.android.fhir.search.Search
import java.util.LinkedList
import javax.inject.Inject
import kotlinx.coroutines.withContext
import org.hl7.fhir.r4.model.Resource
import org.smartregister.fhircore.engine.configuration.ConfigType
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.configuration.app.ConfigService
import org.smartregister.fhircore.engine.configuration.profile.ProfileConfiguration
import org.smartregister.fhircore.engine.configuration.register.ActiveResourceFilterConfig
import org.smartregister.fhircore.engine.configuration.register.RegisterConfiguration
import org.smartregister.fhircore.engine.data.local.DefaultRepository
import org.smartregister.fhircore.engine.domain.model.ActionParameter
import org.smartregister.fhircore.engine.domain.model.ActionParameterType
import org.smartregister.fhircore.engine.domain.model.FhirResourceConfig
import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData
import org.smartregister.fhircore.engine.domain.model.RuleConfig
import org.smartregister.fhircore.engine.domain.repository.Repository
import org.smartregister.fhircore.engine.rulesengine.ConfigRulesExecutor
import org.smartregister.fhircore.engine.util.DispatcherProvider
Expand Down Expand Up @@ -81,82 +78,6 @@ constructor(
)
}

private suspend fun searchResourcesRecursively(
filterActiveResources: List<ActiveResourceFilterConfig>?,
fhirResourceConfig: FhirResourceConfig,
secondaryResourceConfigs: List<FhirResourceConfig>?,
currentPage: Int? = null,
pageSize: Int? = null,
configRules: List<RuleConfig>?,
): List<RepositoryResourceData> {
val baseResourceConfig = fhirResourceConfig.baseResource
val relatedResourcesConfig = fhirResourceConfig.relatedResources
val configComputedRuleValues = configRules.configRulesComputedValues()
val search =
Search(type = baseResourceConfig.resource).apply {
applyConfiguredSortAndFilters(
resourceConfig = baseResourceConfig,
filterActiveResources = filterActiveResources,
sortData = true,
configComputedRuleValues = configComputedRuleValues,
)
if (currentPage != null && pageSize != null) {
count = pageSize
from = currentPage * pageSize
}
}

val baseFhirResources =
kotlin
.runCatching {
withContext(dispatcherProvider.io()) { fhirEngine.search<Resource>(search) }
}
.onFailure { Timber.e(it, "Error retrieving resources. Empty list returned by default") }
.getOrDefault(emptyList())

return baseFhirResources.map { searchResult ->
val retrievedRelatedResources =
withContext(dispatcherProvider.io()) {
retrieveRelatedResources(
resources = listOf(searchResult.resource),
relatedResourcesConfigs = relatedResourcesConfig,
relatedResourceWrapper = RelatedResourceWrapper(),
configComputedRuleValues = configComputedRuleValues,
)
}
RepositoryResourceData(
resourceRulesEngineFactId = baseResourceConfig.id ?: baseResourceConfig.resource.name,
resource = searchResult.resource,
relatedResourcesMap = retrievedRelatedResources.relatedResourceMap,
relatedResourcesCountMap = retrievedRelatedResources.relatedResourceCountMap,
secondaryRepositoryResourceData =
withContext(dispatcherProvider.io()) {
secondaryResourceConfigs.retrieveSecondaryRepositoryResourceData(
filterActiveResources,
)
},
)
}
}

/** This function fetches other resources that are not linked to the base/primary resource. */
private suspend fun List<FhirResourceConfig>?.retrieveSecondaryRepositoryResourceData(
filterActiveResources: List<ActiveResourceFilterConfig>?,
): LinkedList<RepositoryResourceData> {
val secondaryRepositoryResourceDataLinkedList = LinkedList<RepositoryResourceData>()
this?.forEach {
secondaryRepositoryResourceDataLinkedList.addAll(
searchResourcesRecursively(
fhirResourceConfig = it,
filterActiveResources = filterActiveResources,
secondaryResourceConfigs = null,
configRules = null,
),
)
}
return secondaryRepositoryResourceDataLinkedList
}

/** Count register data for the provided [registerId]. Use the configured base resource filters */
override suspend fun countRegisterData(
registerId: String,
Expand Down Expand Up @@ -246,12 +167,6 @@ constructor(
): RegisterConfiguration =
configurationRegistry.retrieveConfiguration(ConfigType.Register, registerId, paramsMap)

private fun List<RuleConfig>?.configRulesComputedValues(): Map<String, Any> {
if (this == null) return emptyMap()
val configRules = configRulesExecutor.generateRules(this)
return configRulesExecutor.fireRules(configRules)
}

companion object {
const val ACTIVE = "active"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import org.joda.time.Instant
import org.json.JSONException
import org.json.JSONObject
import org.smartregister.fhircore.engine.data.local.DefaultRepository
import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData
import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor
import timber.log.Timber

Expand Down Expand Up @@ -472,20 +473,20 @@ suspend fun Task.updateDependentTaskDueDate(
* to be a boolean otherwise the [toBoolean] function will evaluate to false and hence return an
* empty list.
*/
fun List<Resource>.filterByFhirPathExpression(
fun List<RepositoryResourceData>.filterByFhirPathExpression(
fhirPathDataExtractor: FhirPathDataExtractor,
conditionalFhirPathExpressions: List<String>?,
matchAll: Boolean,
): List<Resource> {
): List<RepositoryResourceData> {
if (conditionalFhirPathExpressions.isNullOrEmpty()) return this
return this.filter { resource ->
return this.filter { repositoryResourceData ->
if (matchAll) {
conditionalFhirPathExpressions.all {
fhirPathDataExtractor.extractValue(resource, it).toBoolean()
fhirPathDataExtractor.extractValue(repositoryResourceData.resource, it).toBoolean()
}
} else {
conditionalFhirPathExpressions.any {
fhirPathDataExtractor.extractValue(resource, it).toBoolean()
fhirPathDataExtractor.extractValue(repositoryResourceData.resource, it).toBoolean()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.smartregister.fhircore.engine.data.local.DefaultRepository
import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData
import org.smartregister.fhircore.engine.robolectric.RobolectricTest
import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor

Expand Down Expand Up @@ -753,28 +754,34 @@ class ResourceExtensionTest : RobolectricTest() {
fun testFilterByExpression() {
val tasks =
listOf(
Task().apply {
id = "Task/task1"
description = "New Task"
status = Task.TaskStatus.READY
executionPeriod =
Period().apply {
start = Date().plusMonths(-1)
end = Date().plusDays(-1)
}
addBasedOn(Reference("care1"))
},
Task().apply {
id = "Task/task2"
description = "Another task"
status = Task.TaskStatus.READY
executionPeriod =
Period().apply {
start = Date().plusMonths(-1)
end = Date().plusDays(-1)
}
addBasedOn(Reference("CarePlan/care2"))
},
RepositoryResourceData(
resource =
Task().apply {
id = "Task/task1"
description = "New Task"
status = Task.TaskStatus.READY
executionPeriod =
Period().apply {
start = Date().plusMonths(-1)
end = Date().plusDays(-1)
}
addBasedOn(Reference("care1"))
},
),
RepositoryResourceData(
resource =
Task().apply {
id = "Task/task2"
description = "Another task"
status = Task.TaskStatus.READY
executionPeriod =
Period().apply {
start = Date().plusMonths(-1)
end = Date().plusDays(-1)
}
addBasedOn(Reference("CarePlan/care2"))
},
),
)

// Task with malformed basedOn references
Expand All @@ -787,7 +794,7 @@ class ResourceExtensionTest : RobolectricTest() {
)

Assert.assertTrue(filteredTasks.isNotEmpty())
Assert.assertEquals(filteredTasks.first().logicalId, tasks.first().logicalId)
Assert.assertEquals(filteredTasks.first().resource.logicalId, tasks.first().resource.logicalId)

// Task with correct basedOn references
val filteredTasks2 =
Expand All @@ -798,6 +805,6 @@ class ResourceExtensionTest : RobolectricTest() {
)

Assert.assertTrue(filteredTasks2.isNotEmpty())
Assert.assertEquals(filteredTasks2.first().logicalId, tasks.last().logicalId)
Assert.assertEquals(filteredTasks2.first().resource.logicalId, tasks.last().resource.logicalId)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021-2023 Ona Systems, Inc
* Copyright 2021-2024 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021-2023 Ona Systems, Inc
* Copyright 2021-2024 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021-2023 Ona Systems, Inc
* Copyright 2021-2024 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021-2023 Ona Systems, Inc
* Copyright 2021-2024 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021-2023 Ona Systems, Inc
* Copyright 2021-2024 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
Loading

0 comments on commit 6d593b8

Please sign in to comment.