WIP paginate graph (medical history view)

This commit is contained in:
Jason Kulatunga 2023-08-12 10:26:40 -06:00
parent 9c5bf15016
commit 21dc5ebe92
No known key found for this signature in database
3 changed files with 98 additions and 43 deletions

View File

@ -28,7 +28,7 @@ type DatabaseRepository interface {
AddResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error AddResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error
RemoveResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error RemoveResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error
FindResourceAssociationsByTypeAndId(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string) ([]models.RelatedResource, error) FindResourceAssociationsByTypeAndId(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string) ([]models.RelatedResource, error)
GetFlattenedResourceGraph(ctx context.Context, graphType pkg.ResourceGraphType) (map[string][]*models.ResourceBase, error) GetFlattenedResourceGraph(ctx context.Context, graphType pkg.ResourceGraphType, options models.ResourceGraphOptions) (map[string][]*models.ResourceBase, error)
AddResourceComposition(ctx context.Context, compositionTitle string, resources []*models.ResourceBase) error AddResourceComposition(ctx context.Context, compositionTitle string, resources []*models.ResourceBase) error
//UpsertProfile(context.Context, *models.Profile) error //UpsertProfile(context.Context, *models.Profile) error
//UpsertOrganziation(context.Context, *models.Organization) error //UpsertOrganziation(context.Context, *models.Organization) error

View File

@ -12,21 +12,27 @@ import (
"strings" "strings"
) )
type VertexResourcePlaceholder struct {
UserID string
SourceID string
ResourceID string
ResourceType string
RelatedResourcePlaceholder []*VertexResourcePlaceholder
}
func (rp *VertexResourcePlaceholder) ID() string {
return resourceKeysVertexId(rp.SourceID, rp.ResourceType, rp.ResourceID)
}
// Retrieve a list of all fhir resources (vertex), and a list of all associations (edge) // Retrieve a list of all fhir resources (vertex), and a list of all associations (edge)
// Generate a graph // Generate a graph
// return list of root nodes, and their flattened related resources. // return list of root nodes, and their flattened related resources.
func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context, graphType pkg.ResourceGraphType) (map[string][]*models.ResourceBase, error) { func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context, graphType pkg.ResourceGraphType, options models.ResourceGraphOptions) (map[string][]*models.ResourceBase, error) {
currentUser, currentUserErr := sr.GetCurrentUser(ctx) currentUser, currentUserErr := sr.GetCurrentUser(ctx)
if currentUserErr != nil { if currentUserErr != nil {
return nil, currentUserErr return nil, currentUserErr
} }
// Get list of all resources
wrappedResourceModels, err := sr.ListResources(ctx, models.ListResourceQueryOptions{})
if err != nil {
return nil, err
}
// Get list of all (non-reciprocal) relationships // Get list of all (non-reciprocal) relationships
var relatedResourceRelationships []models.RelatedResource var relatedResourceRelationships []models.RelatedResource
@ -44,10 +50,48 @@ func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context, graph
// TODO optimization: eventually cache the graph in a database/storage, and update when new resources are added. // 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()) g := graph.New(resourceVertexId, graph.Directed(), graph.Acyclic(), graph.Rooted())
//// Get list of all resources TODO - REPLACED THIS
//wrappedResourceModels, err := sr.ListResources(ctx, models.ListResourceQueryOptions{})
//if err != nil {
// return nil, err
//}
//add vertices to the graph (must be done first) //add vertices to the graph (must be done first)
for ndx, _ := range wrappedResourceModels { //we don't want to request all resources from the database, so we will create a placeholder vertex for each resource.
err = g.AddVertex( //we will then use the vertex id to lookup the resource from the database.
&wrappedResourceModels[ndx], //this is a bit of a hack, but it allows us to use the graph library without having to load all resources into memory.
//create a placeholder vertex for each resource (ensuring uniqueness)
resourcePlaceholders := map[string]VertexResourcePlaceholder{}
for _, relationship := range relatedResourceRelationships {
//create placeholders
fromResourcePlaceholder := VertexResourcePlaceholder{
UserID: relationship.ResourceBaseUserID.String(),
SourceID: relationship.ResourceBaseSourceID.String(),
ResourceID: relationship.ResourceBaseSourceResourceID,
ResourceType: relationship.ResourceBaseSourceResourceType,
}
toResourcePlaceholder := VertexResourcePlaceholder{
UserID: relationship.RelatedResourceUserID.String(),
SourceID: relationship.RelatedResourceSourceID.String(),
ResourceID: relationship.RelatedResourceSourceResourceID,
ResourceType: relationship.RelatedResourceSourceResourceType,
}
//add placeholders to map, if they don't already exist
if _, ok := resourcePlaceholders[fromResourcePlaceholder.ID()]; !ok {
resourcePlaceholders[fromResourcePlaceholder.ID()] = fromResourcePlaceholder
}
if _, ok := resourcePlaceholders[toResourcePlaceholder.ID()]; !ok {
resourcePlaceholders[toResourcePlaceholder.ID()] = toResourcePlaceholder
}
}
for _, resourcePlaceholder := range resourcePlaceholders {
err := g.AddVertex(
&resourcePlaceholder,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("an error occurred while adding vertex: %v", err) return nil, fmt.Errorf("an error occurred while adding vertex: %v", err)
@ -60,7 +104,7 @@ func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context, graph
//add edges to graph //add edges to graph
for _, relationship := range relatedResourceRelationships { for _, relationship := range relatedResourceRelationships {
err = g.AddEdge( err := g.AddEdge(
resourceKeysVertexId(relationship.ResourceBaseSourceID.String(), relationship.ResourceBaseSourceResourceType, relationship.ResourceBaseSourceResourceID), resourceKeysVertexId(relationship.ResourceBaseSourceID.String(), relationship.ResourceBaseSourceResourceType, relationship.ResourceBaseSourceResourceID),
resourceKeysVertexId(relationship.RelatedResourceSourceID.String(), relationship.RelatedResourceSourceResourceType, relationship.RelatedResourceSourceResourceID), resourceKeysVertexId(relationship.RelatedResourceSourceID.String(), relationship.RelatedResourceSourceResourceType, relationship.RelatedResourceSourceResourceID),
) )
@ -102,7 +146,7 @@ func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context, graph
// Doing this in one massive function, because passing graph by reference is difficult due to generics. // 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 conditions). store those nodes in their respective lists.
resourceListDictionary := map[string][]*models.ResourceBase{} resourcePlaceholderListDictionary := map[string][]*VertexResourcePlaceholder{}
sources, _, sourceFlattenLevel := getSourcesAndSinksForGraphType(graphType) sources, _, sourceFlattenLevel := getSourcesAndSinksForGraphType(graphType)
for vertexId, val := range predecessorMap { for vertexId, val := range predecessorMap {
@ -112,7 +156,7 @@ func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context, graph
continue continue
} }
resource, err := g.Vertex(vertexId) resourcePlaceholder, err := g.Vertex(vertexId)
if err != nil { if err != nil {
//could not find this vertex in graph, ignoring //could not find this vertex in graph, ignoring
log.Printf("could not find vertex in graph: %v", err) log.Printf("could not find vertex in graph: %v", err)
@ -123,88 +167,94 @@ func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context, graph
foundSourceType := "" foundSourceType := ""
foundSourceLevel := -1 foundSourceLevel := -1
for ndx, sourceResourceTypes := range sources { for ndx, sourceResourceTypes := range sources {
log.Printf("testing resourceType: %s", resource.SourceResourceType) log.Printf("testing resourceType: %s", resourcePlaceholder.ResourceType)
if slices.Contains(sourceResourceTypes, strings.ToLower(resource.SourceResourceType)) { if slices.Contains(sourceResourceTypes, strings.ToLower(resourcePlaceholder.ResourceType)) {
foundSourceType = resource.SourceResourceType foundSourceType = resourcePlaceholder.ResourceType
foundSourceLevel = ndx foundSourceLevel = ndx
break break
} }
} }
if foundSourceLevel == -1 { if foundSourceLevel == -1 {
continue //skip this resource, it is not a valid source type continue //skip this resourcePlaceholder, it is not a valid source type
} }
if _, ok := resourceListDictionary[foundSourceType]; !ok { if _, ok := resourcePlaceholderListDictionary[foundSourceType]; !ok {
resourceListDictionary[foundSourceType] = []*models.ResourceBase{} resourcePlaceholderListDictionary[foundSourceType] = []*VertexResourcePlaceholder{}
} }
resourceListDictionary[foundSourceType] = append(resourceListDictionary[foundSourceType], resource) resourcePlaceholderListDictionary[foundSourceType] = append(resourcePlaceholderListDictionary[foundSourceType], resourcePlaceholder)
} }
// Step 2: define a function. When given a resource, should find all related resources, flatten the heirarchy and set the RelatedResourceFhir list // Step 2: 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) { flattenRelatedResourcesFn := func(resourcePlaceholder *VertexResourcePlaceholder) {
// this is a "root" encounter, which is not related to any condition, we should add it to the Unknown encounters list // this is a "root" encounter, which is not related to any condition, we should add it to the Unknown encounters list
vertexId := resourceVertexId(resource) vertexId := resourceVertexId(resourcePlaceholder)
sr.Logger.Debugf("populating resource: %s", vertexId) sr.Logger.Debugf("populating resourcePlaceholder: %s", vertexId)
resource.RelatedResource = []*models.ResourceBase{} resourcePlaceholder.RelatedResourcePlaceholder = []*VertexResourcePlaceholder{}
//get all the resources associated with this node //get all the resources associated with this node
//TODO: handle error? //TODO: handle error?
graph.DFS(g, vertexId, func(relatedVertexId string) bool { graph.DFS(g, vertexId, func(relatedVertexId string) bool {
relatedResourceFhir, _ := g.Vertex(relatedVertexId) relatedResourcePlaceholder, _ := g.Vertex(relatedVertexId)
//skip the current resource if it's referenced in this list. //skip the current resourcePlaceholder if it's referenced in this list.
//also skip the current resource if its a Binary resource (which is a special case) //also skip the current resourcePlaceholder if its a Binary resourcePlaceholder (which is a special case)
if vertexId != resourceVertexId(relatedResourceFhir) && relatedResourceFhir.SourceResourceType != "Binary" { if vertexId != resourceVertexId(relatedResourcePlaceholder) && relatedResourcePlaceholder.ResourceType != "Binary" {
resource.RelatedResource = append(resource.RelatedResource, relatedResourceFhir) resourcePlaceholder.RelatedResourcePlaceholder = append(resourcePlaceholder.RelatedResourcePlaceholder, relatedResourcePlaceholder)
} }
return false return false
}) })
} }
for resourceType, _ := range resourceListDictionary { // Step 3: 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.
//TODO: Step 3a: since we cant calulate the sort order until the resources are loaded, we need to load all the root resources first.
// Step 4: flatten resources (if needed) and sort them
for resourceType, _ := range resourcePlaceholderListDictionary {
sourceFlatten, sourceFlattenOk := sourceFlattenLevel[strings.ToLower(resourceType)] sourceFlatten, sourceFlattenOk := sourceFlattenLevel[strings.ToLower(resourceType)]
if sourceFlattenOk && sourceFlatten == true { if sourceFlattenOk && sourceFlatten == true {
//if flatten is set to true, we want to flatten the graph. This is usually for non primary source types (eg. Encounter is a source type, but Condition is the primary source type) //if flatten is set to true, we want to flatten the graph. This is usually for non primary source types (eg. Encounter is a source type, but Condition is the primary source type)
// Step 3: populate related resources for each encounter, flattened // Step 3: populate related resources for each encounter, flattened
for ndx, _ := range resourceListDictionary[resourceType] { for ndx, _ := range resourcePlaceholderListDictionary[resourceType] {
// this is a "root" encounter, which is not related to any condition, we should add it to the Unknown encounters list // this is a "root" encounter, which is not related to any condition, we should add it to the Unknown encounters list
flattenRelatedResourcesFn(resourceListDictionary[resourceType][ndx]) flattenRelatedResourcesFn(resourcePlaceholderListDictionary[resourceType][ndx])
//sort all related resources (by date, desc) //sort all related resources (by date, desc)
resourceListDictionary[resourceType][ndx].RelatedResource = utils.SortResourcePtrListByDate(resourceListDictionary[resourceType][ndx].RelatedResource) resourcePlaceholderListDictionary[resourceType][ndx].RelatedResource = utils.SortResourcePtrListByDate(resourcePlaceholderListDictionary[resourceType][ndx].RelatedResource)
} }
} else { } else {
// if flatten is set to false, we want to preserve the top relationships in the graph heirarchy. This is usually for primary source types (eg. Condition is the primary source type) // if flatten is set to false, we want to preserve the top relationships in the graph heirarchy. This is usually for primary source types (eg. Condition is the primary source type)
// we want to ensure context is preserved, so we will flatten the graph futher down in the heirarchy // we want to ensure context is preserved, so we will flatten the graph futher down in the heirarchy
// Step 4: find all encounters referenced by the root conditions, populate them, then add them to the condition as RelatedResourceFhir // Step 4: find all encounters referenced by the root conditions, populate them, then add them to the condition as RelatedResourceFhir
for ndx, _ := range resourceListDictionary[resourceType] { for ndx, _ := range resourcePlaceholderListDictionary[resourceType] {
// this is a "root" condition, // this is a "root" condition,
resourceListDictionary[resourceType][ndx].RelatedResource = []*models.ResourceBase{} resourcePlaceholderListDictionary[resourceType][ndx].RelatedResource = []*models.ResourceBase{}
vertexId := resourceVertexId(resourceListDictionary[resourceType][ndx]) vertexId := resourceVertexId(resourcePlaceholderListDictionary[resourceType][ndx])
for relatedVertexId, _ := range adjacencyMap[vertexId] { for relatedVertexId, _ := range adjacencyMap[vertexId] {
relatedResourceFhir, _ := g.Vertex(relatedVertexId) relatedResourceFhir, _ := g.Vertex(relatedVertexId)
flattenRelatedResourcesFn(relatedResourceFhir) flattenRelatedResourcesFn(relatedResourceFhir)
resourceListDictionary[resourceType][ndx].RelatedResource = append(resourceListDictionary[resourceType][ndx].RelatedResource, relatedResourceFhir) resourcePlaceholderListDictionary[resourceType][ndx].RelatedResource = append(resourcePlaceholderListDictionary[resourceType][ndx].RelatedResource, relatedResourceFhir)
} }
//sort all related resources (by date, desc) //sort all related resources (by date, desc)
resourceListDictionary[resourceType][ndx].RelatedResource = utils.SortResourcePtrListByDate(resourceListDictionary[resourceType][ndx].RelatedResource) resourcePlaceholderListDictionary[resourceType][ndx].RelatedResource = utils.SortResourcePtrListByDate(resourcePlaceholderListDictionary[resourceType][ndx].RelatedResource)
} }
} }
resourceListDictionary[resourceType] = utils.SortResourcePtrListByDate(resourceListDictionary[resourceType]) resourcePlaceholderListDictionary[resourceType] = utils.SortResourcePtrListByDate(resourcePlaceholderListDictionary[resourceType])
} }
// Step 5: return the populated resource list dictionary // Step 5: return the populated resource list dictionary
return resourceListDictionary, nil return resourcePlaceholderListDictionary, nil
} }
//We need to support the following types of graphs: //We need to support the following types of graphs:
@ -351,8 +401,8 @@ func foundResourceGraphSink(checkResourceType string, sinkResourceTypes [][]stri
} }
// helper function for GetResourceGraph, creating a "hash" for the resource // helper function for GetResourceGraph, creating a "hash" for the resource
func resourceVertexId(resource *models.ResourceBase) string { func resourceVertexId(resourcePlaceholder *VertexResourcePlaceholder) string {
return resourceKeysVertexId(resource.SourceID.String(), resource.SourceResourceType, resource.SourceResourceID) return resourceKeysVertexId(resourcePlaceholder.SourceID, resourcePlaceholder.ResourceType, resourcePlaceholder.ResourceID)
} }
func resourceKeysVertexId(sourceId string, resourceType string, resourceId string) string { func resourceKeysVertexId(sourceId string, resourceType string, resourceId string) string {
return strings.ToLower(fmt.Sprintf("%s/%s/%s", sourceId, resourceType, resourceId)) return strings.ToLower(fmt.Sprintf("%s/%s/%s", sourceId, resourceType, resourceId))

View File

@ -0,0 +1,5 @@
package models
type ResourceGraphOptions struct {
Page int
}