Skip to content

Commit

Permalink
WIP adding medical history timeline view. (#325)
Browse files Browse the repository at this point in the history
  • Loading branch information
AnalogJ authored Nov 23, 2023
1 parent a161f41 commit 2061684
Show file tree
Hide file tree
Showing 39 changed files with 1,444 additions and 209 deletions.
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

0 comments on commit 2061684

Please # to comment.