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

WIP adding medical history timeline view. #325

Merged
merged 20 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
102 changes: 52 additions & 50 deletions backend/pkg/database/gorm_repository_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,10 @@ func (rp *VertexResourcePlaceholder) ID() string {
// Retrieve a list of all fhir resources (vertex), and a list of all associations (edge)
// Generate a graph
// return list of root nodes, and their flattened related resources.
func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphType pkg.ResourceGraphType, options models.ResourceGraphOptions) (map[string][]*models.ResourceBase, *models.ResourceGraphMetadata, error) {
func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphType pkg.ResourceGraphType, options models.ResourceGraphOptions) (map[string][]*models.ResourceBase, error) {
currentUser, currentUserErr := gr.GetCurrentUser(ctx)
if currentUserErr != nil {
return nil, nil, currentUserErr
}

//initialize the graph results metadata
resourceGraphMetadata := models.ResourceGraphMetadata{
TotalElements: 0,
PageSize: 20, //TODO: replace this with pkg.DefaultPageSize
Page: options.Page,
return nil, currentUserErr
}

// Get list of all (non-reciprocal) relationships
Expand All @@ -52,18 +45,13 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy
}).
Find(&relatedResourceRelationships)
if result.Error != nil {
return nil, nil, result.Error
return nil, result.Error
}
log.Printf("found %d related resources", len(relatedResourceRelationships))

//Generate Graph
// TODO optimization: eventually cache the graph in a database/storage, and update when new resources are added.
g := graph.New(resourceVertexId, graph.Directed(), graph.Acyclic(), graph.Rooted())

//// Get list of all resources TODO - REPLACED THIS
//wrappedResourceModels, err := gr.ListResources(ctx, models.ListResourceQueryOptions{})
//if err != nil {
// return nil, err
//}
g := graph.New(resourceVertexId, graph.Directed(), graph.Rooted())

//add vertices to the graph (must be done first)
//we don't want to request all resources from the database, so we will create a placeholder vertex for each resource.
Expand Down Expand Up @@ -105,7 +93,7 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy
&resourcePlaceholder,
)
if err != nil {
return nil, nil, fmt.Errorf("an error occurred while adding vertex: %v", err)
return nil, fmt.Errorf("an error occurred while adding vertex: %v", err)
}
}

Expand Down Expand Up @@ -142,7 +130,7 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy
// }
adjacencyMap, err := g.AdjacencyMap()
if err != nil {
return nil, nil, fmt.Errorf("error while generating AdjacencyMap: %v", err)
return nil, fmt.Errorf("error while generating AdjacencyMap: %v", err)
}

// For a directed graph, PredecessorMap is the complement of AdjacencyMap. This is because in a directed graph, only
Expand All @@ -151,12 +139,12 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy
// ie. "empty" verticies in this map are "root" nodes.
predecessorMap, err := g.PredecessorMap()
if err != nil {
return nil, nil, fmt.Errorf("error while generating PredecessorMap: %v", err)
return nil, fmt.Errorf("error while generating PredecessorMap: %v", err)
}

// Doing this in one massive function, because passing graph by reference is difficult due to generics.

// Step 1: use predecessorMap to find all "root" resources (eg. MedicalHistory - encounters and conditions). store those nodes in their respective lists.
// Step 1: use predecessorMap to find all "root" resources (eg. MedicalHistory - encounters and EOB). store those nodes in their respective lists.
resourcePlaceholderListDictionary := map[string][]*VertexResourcePlaceholder{}
sources, _, sourceFlattenLevel := getSourcesAndSinksForGraphType(graphType)

Expand Down Expand Up @@ -201,11 +189,10 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy
// Step 2: now that we've created a relationship graph using placeholders, we need to determine which page of resources to return
// and look up the actual resources from the database.

resourceListDictionary, totalElements, err := gr.InflateResourceGraphAtPage(resourcePlaceholderListDictionary, options.Page)
resourceListDictionary, err := gr.InflateSelectedResourcesInResourceGraph(currentUser, resourcePlaceholderListDictionary, options)
if err != nil {
return nil, nil, fmt.Errorf("error while paginating & inflating resource graph: %v", err)
return nil, fmt.Errorf("error while paginating & inflating resource graph: %v", err)
}
resourceGraphMetadata.TotalElements = totalElements

// Step 3: define a function. When given a resource, should find all related resources, flatten the heirarchy and set the RelatedResourceFhir list
flattenRelatedResourcesFn := func(resource *models.ResourceBase) {
Expand All @@ -220,22 +207,30 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy

resource.RelatedResource = []*models.ResourceBase{}

//make sure we don't keep traversing the same node over and over again
visited := map[string]bool{
vertexId: true,
}

//get all the resource placeholders associated with this node
//TODO: handle error?
graph.DFS(g, vertexId, func(relatedVertexId string) bool {

relatedResourcePlaceholder, _ := g.Vertex(relatedVertexId)
//skip the current resourcePlaceholder if it's referenced in this list.
//skip any "visted" nodes
//also skip the current resourcePlaceholder if its a Binary resourcePlaceholder (which is a special case)
if vertexId != resourceVertexId(relatedResourcePlaceholder) && relatedResourcePlaceholder.ResourceType != "Binary" {
if _, hasVisited := visited[resourceVertexId(relatedResourcePlaceholder)]; !hasVisited && relatedResourcePlaceholder.ResourceType != "Binary" {
relatedResource, err := gr.GetResourceByResourceTypeAndId(ctx, relatedResourcePlaceholder.ResourceType, relatedResourcePlaceholder.ResourceID)
if err != nil {
gr.Logger.Warnf("ignoring, cannot safely handle error which occurred while getting related resource: %v", err)
return true
gr.Logger.Warnf("ignoring, cannot safely handle error which occurred while getting related resource (%s/%s): %v", relatedResourcePlaceholder.ResourceType, relatedResourcePlaceholder.ResourceID, err)
return false
}
resource.RelatedResource = append(
resource.RelatedResource,
relatedResource,
)
visited[resourceVertexId(relatedResourcePlaceholder)] = true
}
return false
})
Expand Down Expand Up @@ -288,36 +283,50 @@ func (gr *GormRepository) GetFlattenedResourceGraph(ctx context.Context, graphTy

// Step 5: return the populated resource list dictionary

return resourceListDictionary, &resourceGraphMetadata, nil
return resourceListDictionary, nil
}

// LoadResourceGraphAtPage - this function will take a dictionary of placeholder "sources" graph and load the actual resources from the database, for a specific page
// InflateSelectedResourcesInResourceGraph - this function will take a dictionary of placeholder "sources" graph and load the selected resources (and their descendants) from the database.
// - first, it will load all the "source" resources (eg. Encounter, Condition, etc)
// - sort the root resources by date, desc
// - use the page number + page size to determine which root resources to return
// - return a dictionary of "source" resource lists
func (gr *GormRepository) InflateResourceGraphAtPage(resourcePlaceholderListDictionary map[string][]*VertexResourcePlaceholder, page int) (map[string][]*models.ResourceBase, int, error) {
totalElements := 0
// Step 3a: since we cant calulate the sort order until the resources are loaded, we need to load all the root resources first.
func (gr *GormRepository) InflateSelectedResourcesInResourceGraph(currentUser *models.User, resourcePlaceholderListDictionary map[string][]*VertexResourcePlaceholder, options models.ResourceGraphOptions) (map[string][]*models.ResourceBase, error) {

// Step 3a: group the selected resources by type, so we only need to do 1 query per type
selectedResourceIdsByResourceType := map[string][]models.OriginBase{}

for _, resourceId := range options.ResourcesIds {
if _, ok := selectedResourceIdsByResourceType[resourceId.SourceResourceType]; !ok {
selectedResourceIdsByResourceType[resourceId.SourceResourceType] = []models.OriginBase{}
}
selectedResourceIdsByResourceType[resourceId.SourceResourceType] = append(selectedResourceIdsByResourceType[resourceId.SourceResourceType], resourceId)
}

// Step 3b: query the database for all the selected resources

//TODO: maybe its more performant to query each resource by type/id/source, since they are indexed already?
rootWrappedResourceModels := []models.ResourceBase{}
for resourceType, _ := range resourcePlaceholderListDictionary {
// resourcePlaceholderListDictionary contains top level resource types (eg. Encounter, Condition, etc)
for resourceType, _ := range selectedResourceIdsByResourceType {
// selectedResourceIdsByResourceType contains selected resources grouped by ty[e types (eg. Encounter, Condition, etc)

//convert these to a list of interface{} for the query
selectList := [][]interface{}{}
for ndx, _ := range resourcePlaceholderListDictionary[resourceType] {
for ndx, _ := range selectedResourceIdsByResourceType[resourceType] {

selectedResource := selectedResourceIdsByResourceType[resourceType][ndx]

selectList = append(selectList, []interface{}{
resourcePlaceholderListDictionary[resourceType][ndx].UserID,
resourcePlaceholderListDictionary[resourceType][ndx].SourceID,
resourcePlaceholderListDictionary[resourceType][ndx].ResourceType,
resourcePlaceholderListDictionary[resourceType][ndx].ResourceID,
currentUser.ID,
selectedResource.SourceID,
selectedResource.SourceResourceType,
selectedResource.SourceResourceID,
})
}

tableName, err := databaseModel.GetTableNameByResourceType(resourceType)
if err != nil {
return nil, totalElements, err
return nil, err
}
var tableWrappedResourceModels []models.ResourceBase
gr.GormClient.
Expand All @@ -332,13 +341,7 @@ func (gr *GormRepository) InflateResourceGraphAtPage(resourcePlaceholderListDict
//sort
rootWrappedResourceModels = utils.SortResourceListByDate(rootWrappedResourceModels)

//calculate total elements
totalElements = len(rootWrappedResourceModels)

//paginate (by calculating window for the slice)
rootWrappedResourceModels = utils.PaginateResourceList(rootWrappedResourceModels, page, 20) //todo: replace size with pkg.ResourceListPageSize

// Step 3b: now that we have the root resources, lets generate a dictionary of resource lists, keyed by resource type
// Step 3c: now that we have the selected root resources, lets generate a dictionary of resource lists, keyed by resource type
resourceListDictionary := map[string][]*models.ResourceBase{}
for ndx, _ := range rootWrappedResourceModels {
resourceType := rootWrappedResourceModels[ndx].SourceResourceType
Expand All @@ -349,7 +352,7 @@ func (gr *GormRepository) InflateResourceGraphAtPage(resourcePlaceholderListDict
}

// Step 4: return the populated resource list dictionary
return resourceListDictionary, totalElements, nil
return resourceListDictionary, nil
}

// We need to support the following types of graphs:
Expand Down Expand Up @@ -445,11 +448,10 @@ func getSourcesAndSinksForGraphType(graphType pkg.ResourceGraphType) ([][]string
switch graphType {
case pkg.ResourceGraphTypeMedicalHistory:
sources = [][]string{
{"condition", "composition"},
{"encounter", "explanationofbenefit"},
}
sinks = [][]string{
{"location", "device", "organization", "practitioner", "medication", "patient", "coverage"}, //resources that are shared across multiple conditions
{"condition", "composition", "location", "device", "organization", "practitioner", "medication", "patient", "coverage"}, //resources that are shared across multiple conditions
{"binary"},
}
sourceFlattenRelated = map[string]bool{
Expand Down
Loading