Merge pull request #223 from fastenhealth/paginate_graph

This commit is contained in:
Jason Kulatunga 2023-10-04 11:36:22 -07:00 committed by GitHub
commit 877a1326f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 303 additions and 66 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
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)
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, *models.ResourceGraphMetadata, error)
AddResourceComposition(ctx context.Context, compositionTitle string, resources []*models.ResourceBase) error
//UpsertProfile(context.Context, *models.Profile) error
//UpsertOrganziation(context.Context, *models.Organization) error

View File

@ -141,7 +141,8 @@ func (sr *SqliteRepository) GetUserByUsername(ctx context.Context, username stri
return &foundUser, result.Error
}
// TODO: check for error, right now we return a nil which may cause a panic.
//TODO: check for error, right now we return a nil which may cause a panic.
//TODO: can we cache the current user? //SECURITY:
func (sr *SqliteRepository) GetCurrentUser(ctx context.Context) (*models.User, error) {
username := ctx.Value(pkg.ContextKeyTypeAuthUsername)
if username == nil {
@ -446,6 +447,7 @@ func (sr *SqliteRepository) ListResources(ctx context.Context, queryOptions mode
}
}
//TODO: should this be deprecated? (replaced by ListResources)
func (sr *SqliteRepository) GetResourceByResourceTypeAndId(ctx context.Context, sourceResourceType string, sourceResourceId string) (*models.ResourceBase, error) {
currentUser, currentUserErr := sr.GetCurrentUser(ctx)
if currentUserErr != nil {
@ -635,14 +637,13 @@ func (sr *SqliteRepository) FindResourceAssociationsByTypeAndId(ctx context.Cont
// - find source for each resource
// - (SECURITY) ensure the current user and the source for each resource matches
// - check if there is a Composition resource Type already.
// - if Composition type already exists:
// - update "relatesTo" field with additional data.
// - else:
// - Create a Composition resource type (populated with "relatesTo" references to all provided Resources)
//
// - if Composition type already exists:
// - update "relatesTo" field with additional data.
// - else:
// - Create a Composition resource type (populated with "relatesTo" references to all provided Resources)
// - add AddResourceAssociation for all resources linked to the Composition resource
// - store the Composition resource
// TODO: determine if we should be using a List Resource instead of a Composition resource
//TODO: determine if we should be using a List Resource instead of a Composition resource
func (sr *SqliteRepository) AddResourceComposition(ctx context.Context, compositionTitle string, resources []*models.ResourceBase) error {
currentUser, currentUserErr := sr.GetCurrentUser(ctx)
if currentUserErr != nil {

View File

@ -7,24 +7,38 @@ import (
"github.com/fastenhealth/fasten-onprem/backend/pkg"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/fastenhealth/fasten-onprem/backend/pkg/utils"
databaseModel "github.com/fastenhealth/fasten-onprem/backend/pkg/models/database"
"golang.org/x/exp/slices"
"log"
"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)
// Generate a graph
// 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, *models.ResourceGraphMetadata, error) {
currentUser, currentUserErr := sr.GetCurrentUser(ctx)
if currentUserErr != nil {
return nil, currentUserErr
return nil, nil, currentUserErr
}
// Get list of all resources
wrappedResourceModels, err := sr.ListResources(ctx, models.ListResourceQueryOptions{})
if err != nil {
return nil, err
//initialize the graph results metadata
resourceGraphMetadata := models.ResourceGraphMetadata{
TotalElements: 0,
PageSize: 20, //TODO: replace this with pkg.DefaultPageSize
Page: options.Page,
}
// Get list of all (non-reciprocal) relationships
@ -37,20 +51,60 @@ func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context, graph
}).
Find(&relatedResourceRelationships)
if result.Error != nil {
return nil, result.Error
return nil, nil, result.Error
}
//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 := sr.ListResources(ctx, models.ListResourceQueryOptions{})
//if err != nil {
// return nil, err
//}
//add vertices to the graph (must be done first)
for ndx, _ := range wrappedResourceModels {
err = g.AddVertex(
&wrappedResourceModels[ndx],
//we don't want to request all resources from the database, so we will create a placeholder vertex for each resource.
//we will then use the vertex id to lookup the resource from the database.
//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 ndx, _ := range resourcePlaceholders {
resourcePlaceholder := resourcePlaceholders[ndx]
log.Printf("Adding vertex: %v", resourcePlaceholder.ID())
err := g.AddVertex(
&resourcePlaceholder,
)
if err != nil {
return nil, fmt.Errorf("an error occurred while adding vertex: %v", err)
return nil, nil, fmt.Errorf("an error occurred while adding vertex: %v", err)
}
}
@ -60,7 +114,7 @@ func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context, graph
//add edges to graph
for _, relationship := range relatedResourceRelationships {
err = g.AddEdge(
err := g.AddEdge(
resourceKeysVertexId(relationship.ResourceBaseSourceID.String(), relationship.ResourceBaseSourceResourceType, relationship.ResourceBaseSourceResourceID),
resourceKeysVertexId(relationship.RelatedResourceSourceID.String(), relationship.RelatedResourceSourceResourceType, relationship.RelatedResourceSourceResourceID),
)
@ -87,7 +141,7 @@ func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context, graph
// }
adjacencyMap, err := g.AdjacencyMap()
if err != nil {
return nil, fmt.Errorf("error while generating AdjacencyMap: %v", err)
return nil, 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
@ -96,13 +150,13 @@ func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context, graph
// ie. "empty" verticies in this map are "root" nodes.
predecessorMap, err := g.PredecessorMap()
if err != nil {
return nil, fmt.Errorf("error while generating PredecessorMap: %v", err)
return nil, 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.
resourceListDictionary := map[string][]*models.ResourceBase{}
resourcePlaceholderListDictionary := map[string][]*VertexResourcePlaceholder{}
sources, _, sourceFlattenLevel := getSourcesAndSinksForGraphType(graphType)
for vertexId, val := range predecessorMap {
@ -112,7 +166,7 @@ func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context, graph
continue
}
resource, err := g.Vertex(vertexId)
resourcePlaceholder, err := g.Vertex(vertexId)
if err != nil {
//could not find this vertex in graph, ignoring
log.Printf("could not find vertex in graph: %v", err)
@ -123,47 +177,70 @@ func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context, graph
foundSourceType := ""
foundSourceLevel := -1
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)) {
foundSourceType = resource.SourceResourceType
if slices.Contains(sourceResourceTypes, strings.ToLower(resourcePlaceholder.ResourceType)) {
foundSourceType = resourcePlaceholder.ResourceType
foundSourceLevel = ndx
break
}
}
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 {
resourceListDictionary[foundSourceType] = []*models.ResourceBase{}
if _, ok := resourcePlaceholderListDictionary[foundSourceType]; !ok {
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: 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 := sr.InflateResourceGraphAtPage(resourcePlaceholderListDictionary, options.Page)
if err != nil {
return nil, 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) {
// this is a "root" encounter, which is not related to any condition, we should add it to the Unknown encounters list
vertexId := resourceVertexId(resource)
sr.Logger.Debugf("populating resource: %s", vertexId)
vertexId := resourceVertexId(&VertexResourcePlaceholder{
ResourceType: resource.SourceResourceType,
ResourceID: resource.SourceResourceID,
SourceID: resource.SourceID.String(),
UserID: resource.UserID.String(),
})
sr.Logger.Debugf("populating resourcePlaceholder: %s", vertexId)
resource.RelatedResource = []*models.ResourceBase{}
//get all the resources associated with this node
//get all the resource placeholders associated with this node
//TODO: handle error?
graph.DFS(g, vertexId, func(relatedVertexId string) bool {
relatedResourceFhir, _ := g.Vertex(relatedVertexId)
//skip the current resource if it's referenced in this list.
//also skip the current resource if its a Binary resource (which is a special case)
if vertexId != resourceVertexId(relatedResourceFhir) && relatedResourceFhir.SourceResourceType != "Binary" {
resource.RelatedResource = append(resource.RelatedResource, relatedResourceFhir)
relatedResourcePlaceholder, _ := g.Vertex(relatedVertexId)
//skip the current resourcePlaceholder if it's referenced in this list.
//also skip the current resourcePlaceholder if its a Binary resourcePlaceholder (which is a special case)
if vertexId != resourceVertexId(relatedResourcePlaceholder) && relatedResourcePlaceholder.ResourceType != "Binary" {
relatedResource, err := sr.GetResourceByResourceTypeAndId(ctx, relatedResourcePlaceholder.ResourceType, relatedResourcePlaceholder.ResourceID)
if err != nil {
sr.Logger.Warnf("ignoring, cannot safely handle error which occurred while getting related resource: %v", err)
return true
}
resource.RelatedResource = append(
resource.RelatedResource,
relatedResource,
)
}
return false
})
}
// Step 4: flatten resources (if needed) and sort them
for resourceType, _ := range resourceListDictionary {
sourceFlatten, sourceFlattenOk := sourceFlattenLevel[strings.ToLower(resourceType)]
@ -187,9 +264,15 @@ func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context, graph
// this is a "root" condition,
resourceListDictionary[resourceType][ndx].RelatedResource = []*models.ResourceBase{}
vertexId := resourceVertexId(resourceListDictionary[resourceType][ndx])
currentResource := resourceListDictionary[resourceType][ndx]
vertexId := resourceKeysVertexId(currentResource.SourceID.String(), currentResource.SourceResourceType, currentResource.SourceResourceID)
for relatedVertexId, _ := range adjacencyMap[vertexId] {
relatedResourceFhir, _ := g.Vertex(relatedVertexId)
relatedResourcePlaceholder, _ := g.Vertex(relatedVertexId)
relatedResourceFhir, err := sr.GetResourceByResourceTypeAndId(ctx, relatedResourcePlaceholder.ResourceType, relatedResourcePlaceholder.ResourceID)
if err != nil {
sr.Logger.Warnf("ignoring, cannot safely handle error which occurred while getting related resource (flatten=false): %v", err)
continue
}
flattenRelatedResourcesFn(relatedResourceFhir)
resourceListDictionary[resourceType][ndx].RelatedResource = append(resourceListDictionary[resourceType][ndx].RelatedResource, relatedResourceFhir)
}
@ -204,7 +287,68 @@ func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context, graph
// Step 5: return the populated resource list dictionary
return resourceListDictionary, nil
return resourceListDictionary, &resourceGraphMetadata, nil
}
// LoadResourceGraphAtPage - this function will take a dictionary of placeholder "sources" graph and load the actual resources from the database, for a specific page
// - 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 (sr *SqliteRepository) 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.
//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)
selectList := [][]interface{}{}
for ndx, _ := range resourcePlaceholderListDictionary[resourceType] {
selectList = append(selectList, []interface{}{
resourcePlaceholderListDictionary[resourceType][ndx].UserID,
resourcePlaceholderListDictionary[resourceType][ndx].SourceID,
resourcePlaceholderListDictionary[resourceType][ndx].ResourceType,
resourcePlaceholderListDictionary[resourceType][ndx].ResourceID,
})
}
tableName, err := databaseModel.GetTableNameByResourceType(resourceType)
if err != nil {
return nil, totalElements, err
}
var tableWrappedResourceModels []models.ResourceBase
sr.GormClient.
Where("(user_id, source_id, source_resource_type, source_resource_id) IN ?", selectList).
Table(tableName).
Find(&tableWrappedResourceModels)
//append these resources to the rootWrappedResourceModels list
rootWrappedResourceModels = append(rootWrappedResourceModels, tableWrappedResourceModels...)
}
//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
resourceListDictionary := map[string][]*models.ResourceBase{}
for ndx, _ := range rootWrappedResourceModels {
resourceType := rootWrappedResourceModels[ndx].SourceResourceType
if _, ok := resourceListDictionary[resourceType]; !ok {
resourceListDictionary[resourceType] = []*models.ResourceBase{}
}
resourceListDictionary[resourceType] = append(resourceListDictionary[resourceType], &rootWrappedResourceModels[ndx])
}
// Step 4: return the populated resource list dictionary
return resourceListDictionary, totalElements, nil
}
//We need to support the following types of graphs:
@ -351,8 +495,8 @@ func foundResourceGraphSink(checkResourceType string, sinkResourceTypes [][]stri
}
// helper function for GetResourceGraph, creating a "hash" for the resource
func resourceVertexId(resource *models.ResourceBase) string {
return resourceKeysVertexId(resource.SourceID.String(), resource.SourceResourceType, resource.SourceResourceID)
func resourceVertexId(resourcePlaceholder *VertexResourcePlaceholder) string {
return resourceKeysVertexId(resourcePlaceholder.SourceID, resourcePlaceholder.ResourceType, resourcePlaceholder.ResourceID)
}
func resourceKeysVertexId(sourceId string, resourceType string, resourceId string) string {
return strings.ToLower(fmt.Sprintf("%s/%s/%s", sourceId, resourceType, resourceId))

View File

@ -0,0 +1,7 @@
package models
type ResourceGraphMetadata struct {
TotalElements int `json:"total_elements"`
PageSize int `json:"page_size"`
Page int `json:"page"`
}

View File

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

View File

@ -0,0 +1,16 @@
package utils
import "github.com/fastenhealth/fasten-onprem/backend/pkg/models"
func PaginateResourceList(resourceList []models.ResourceBase, skip int, size int) []models.ResourceBase {
if skip > len(resourceList) {
skip = len(resourceList)
}
end := skip + size
if end > len(resourceList) {
end = len(resourceList)
}
return resourceList[skip:end]
}

View File

@ -129,12 +129,26 @@ func GetResourceFhirGraph(c *gin.Context) {
graphType := strings.Trim(c.Param("graphType"), "/")
resourceListDictionary, err := databaseRepo.GetFlattenedResourceGraph(c, pkg.ResourceGraphType(graphType))
graphOptions := models.ResourceGraphOptions{}
if len(c.Query("page")) > 0 {
pageNumb, err := strconv.Atoi(c.Query("page"))
if err != nil {
logger.Errorln("An error occurred while calculating page number", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
graphOptions.Page = pageNumb
}
resourceListDictionary, resourceListMetadata, err := databaseRepo.GetFlattenedResourceGraph(c, pkg.ResourceGraphType(graphType), graphOptions)
if err != nil {
logger.Errorln("An error occurred while retrieving list of resources", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": resourceListDictionary})
c.JSON(http.StatusOK, gin.H{"success": true, "data": map[string]interface{}{
"results": resourceListDictionary,
"metadata": resourceListMetadata,
}})
}

View File

@ -0,0 +1,12 @@
import {ResourceFhir} from './resource_fhir';
export class ResourceGraphResponse {
results: {[resource_type: string]: ResourceFhir[]}
metadata: ResourceGraphMetadata
}
export class ResourceGraphMetadata {
total_elements: number
page_size: number
page: number
}

View File

@ -12,7 +12,7 @@
<div class="row mt-5 mb-3">
<div class="col-12">
<div class="alert alert-warning" role="alert">
<div [hidden]="true" class="alert alert-warning" role="alert">
<strong>Warning!</strong> Fasten has detected medical Encounters that are not associated with a Condition.
They are grouped under the "Unassigned" section below.
<br/>
@ -34,6 +34,18 @@
<!-- Condition List -->
<app-report-medical-history-condition *ngFor="let condition of conditions; let i = index" [conditionGroup]="condition"></app-report-medical-history-condition>
<app-report-medical-history-explanation-of-benefit *ngFor="let eob of explanationOfBenefits; let i = index" [explanationOfBenefitGroup]="eob"></app-report-medical-history-explanation-of-benefit>
<div class="row">
<div class="col-12 d-flex justify-content-center flex-nowrap">
<ngb-pagination
*ngIf="resourceGraphMetadata.total_elements > resourceGraphMetadata.page_size"
[(page)]="resourceGraphMetadata.page"
(pageChange)="pageChange($event)"
[pageSize]="resourceGraphMetadata.page_size"
[collectionSize]="resourceGraphMetadata.total_elements"></ngb-pagination>
</div>
</div>
</ng-template>
<ng-template #emptyReport>

View File

@ -4,6 +4,7 @@ import {ResourceFhir} from '../../models/fasten/resource_fhir';
import { ModalDismissReasons, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {ReportMedicalHistoryEditorComponent} from '../../components/report-medical-history-editor/report-medical-history-editor.component';
import {forkJoin} from 'rxjs';
import {ResourceGraphMetadata, ResourceGraphResponse} from '../../models/fasten/resource-graph-response';
// import {ReportEditorRelatedComponent} from '../../components/report-editor-related/report-editor-related.component';
@Component({
@ -21,6 +22,12 @@ export class MedicalHistoryComponent implements OnInit {
unassigned_encounters: ResourceFhir[] = []
resourceLookup: {[name: string]: ResourceFhir} = {}
resourceGraphMetadata: ResourceGraphMetadata = {
total_elements: 0,
page_size: 0,
page: 1
}
constructor(
private fastenApi: FastenApiService,
private modalService: NgbModal
@ -28,12 +35,21 @@ export class MedicalHistoryComponent implements OnInit {
ngOnInit(): void {
//load the first page
this.pageChange(1)
}
pageChange(page: number){
this.loading = true
this.fastenApi.getResourceGraph().subscribe(results => {
this.fastenApi.getResourceGraph(null, page).subscribe((response: ResourceGraphResponse) => {
this.loading = false
this.conditions = [].concat(results["Condition"] || [], results["Composition"] || [])
this.unassigned_encounters = results["Encounter"] || []
this.explanationOfBenefits = results["ExplanationOfBenefit"] || []
this.resourceGraphMetadata = response.metadata
//page counter is 1 indexed but the backend is 0 indexed
this.resourceGraphMetadata.page = this.resourceGraphMetadata.page + 1
this.conditions = [].concat(response.results["Condition"] || [], response.results["Composition"] || [])
this.unassigned_encounters = response.results["Encounter"] || []
this.explanationOfBenefits = response.results["ExplanationOfBenefit"] || []
//populate a lookup table with all resources
for(let condition of this.conditions){
@ -66,6 +82,7 @@ export class MedicalHistoryComponent implements OnInit {
}
openEditorRelated(): void {
const modalRef = this.modalService.open(ReportMedicalHistoryEditorComponent, {
size: 'xl',

View File

@ -67,14 +67,17 @@
<!-- Pagination -->
<ngb-pagination
class="mr-auto"
[collectionSize]="allObservationGroups.length"
[(page)]="currentPage"
[pageSize]="pageSize"
(pageChange)="populateObservationsForCurrentPage()"
>
</ngb-pagination>
<div class="row">
<div class="col-12 d-flex justify-content-center flex-nowrap">
<ngb-pagination
[collectionSize]="allObservationGroups.length"
[(page)]="currentPage"
[pageSize]="pageSize"
(pageChange)="populateObservationsForCurrentPage()"
>
</ngb-pagination>
</div>
</div>
</ng-template>

View File

@ -22,6 +22,7 @@ import * as fhirpath from 'fhirpath';
import _ from 'lodash';
import {DashboardConfig} from '../models/widget/dashboard-config';
import {DashboardWidgetQuery} from '../models/widget/dashboard-widget-query';
import {ResourceGraphResponse} from '../models/fasten/resource-graph-response';
import { fetchEventSource } from '@microsoft/fetch-event-source';
@Injectable({
@ -178,16 +179,21 @@ export class FastenApiService {
return this._httpClient.post<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/query`, query)
}
getResourceGraph(graphType?: string): Observable<{[resourceType: string]: ResourceFhir[]}> {
getResourceGraph(graphType?: string, page?:number): Observable<ResourceGraphResponse> {
if(!graphType){
graphType = "MedicalHistory"
}
let queryParams = {}
if(page){
//the backend is 0 indexed, but the frontend is 1 indexed
queryParams["page"] = page - 1
}
return this._httpClient.get<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/resource/graph/${graphType}`)
return this._httpClient.get<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/resource/graph/${graphType}`, {params: queryParams})
.pipe(
map((response: ResponseWrapper) => {
console.log("RESPONSE", response)
return response.data as {[name: string]: ResourceFhir[]}
return response.data as ResourceGraphResponse
})
);
}

2
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/dave/jennifer v1.6.1
github.com/dominikbraun/graph v0.15.0
github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3
github.com/fastenhealth/fasten-sources v0.3.1
github.com/fastenhealth/fasten-sources v0.3.2
github.com/fastenhealth/gofhir-models v0.0.5
github.com/gin-gonic/gin v1.9.0
github.com/glebarez/sqlite v1.5.0

4
go.sum
View File

@ -197,8 +197,8 @@ github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M=
github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
github.com/fastenhealth/fasten-sources v0.3.1 h1:JtAtPWB5WErtQgPAepnISqm/fhIt7elOsFWk1a4g0yc=
github.com/fastenhealth/fasten-sources v0.3.1/go.mod h1:KUtpp65GaKlpRvl5i8zWTVZlI1yJaUPkvGVqQVEC21w=
github.com/fastenhealth/fasten-sources v0.3.2 h1:AdFayvwu88zO5JmMNhxlZDARZW1q6iBNHl9Ew8BSLHs=
github.com/fastenhealth/fasten-sources v0.3.2/go.mod h1:KUtpp65GaKlpRvl5i8zWTVZlI1yJaUPkvGVqQVEC21w=
github.com/fastenhealth/gofhir-models v0.0.5 h1:wU2Dz+/h9MzZCTRgkQzeq5l0EFuMI6C5xgCbKislFpg=
github.com/fastenhealth/gofhir-models v0.0.5/go.mod h1:xB8ikGxu3bUq2b1JYV+CZpHqBaLXpOizFR0eFBCunis=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=