adding allow_unsafe_endpoints config option.
adding flattend resource graph. adding Reciprocal resources adding a report labs page.
This commit is contained in:
parent
f8aac49524
commit
918c856338
|
@ -24,6 +24,8 @@ func (c *configuration) Init() error {
|
|||
c.SetDefault("web.listen.port", "8080")
|
||||
c.SetDefault("web.listen.host", "0.0.0.0")
|
||||
c.SetDefault("web.listen.basepath", "")
|
||||
c.SetDefault("web.allow_unsafe_endpoints", false)
|
||||
|
||||
c.SetDefault("web.src.frontend.path", "/opt/fasten/web")
|
||||
|
||||
c.SetDefault("database.location", "/opt/fasten/db/fasten.db") //TODO: should be /opt/fasten/fasten.db
|
||||
|
|
|
@ -24,6 +24,7 @@ type DatabaseRepository interface {
|
|||
GetPatientForSources(ctx context.Context) ([]models.ResourceFhir, 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
|
||||
GetFlattenedResourceGraph(ctx context.Context) ([]*models.ResourceFhir, []*models.ResourceFhir, error)
|
||||
//UpsertProfile(context.Context, *models.Profile) error
|
||||
//UpsertOrganziation(context.Context, *models.Organization) error
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/dominikbraun/graph"
|
||||
sourceModel "github.com/fastenhealth/fasten-sources/clients/models"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
|
||||
|
@ -11,6 +12,7 @@ import (
|
|||
"github.com/glebarez/sqlite"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
"net/url"
|
||||
|
@ -202,39 +204,25 @@ func (sr *SqliteRepository) UpsertRawResource(ctx context.Context, sourceCredent
|
|||
//note: we create the association in the related_resources table **before** the model actually exists.
|
||||
|
||||
if rawResource.ReferencedResources != nil {
|
||||
//these are resources that are referenced by the current resource
|
||||
relatedResources := []*models.ResourceFhir{}
|
||||
|
||||
//reciprocalRelatedResources := []*models.ResourceFhir{}
|
||||
for _, referencedResource := range rawResource.ReferencedResources {
|
||||
parts := strings.Split(referencedResource, "/")
|
||||
if len(parts) == 2 {
|
||||
|
||||
relatedResource := &models.ResourceFhir{
|
||||
OriginBase: models.OriginBase{
|
||||
SourceID: source.ID,
|
||||
SourceResourceType: parts[0],
|
||||
SourceResourceID: parts[1],
|
||||
},
|
||||
RelatedResourceFhir: nil,
|
||||
}
|
||||
relatedResources = append(relatedResources, relatedResource)
|
||||
|
||||
//if the related resource is an Encounter or Condition, make sure we create a reciprocal association as well, just incase
|
||||
if parts[0] == "Condition" || parts[0] == "Encounter" {
|
||||
//manually create association (we've tried to create using Association.Append, and it doesnt work for some reason.
|
||||
err := sr.AddResourceAssociation(ctx, &source, parts[0], parts[1], &source, wrappedResourceModel.SourceResourceType, wrappedResourceModel.SourceResourceID)
|
||||
if err != nil {
|
||||
sr.Logger.Errorf("Error when creating a reciprocal association for %s: %v", referencedResource, err)
|
||||
}
|
||||
}
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
relatedResource := &models.ResourceFhir{
|
||||
OriginBase: models.OriginBase{
|
||||
SourceID: source.ID,
|
||||
SourceResourceType: parts[0],
|
||||
SourceResourceID: parts[1],
|
||||
},
|
||||
RelatedResourceFhir: nil,
|
||||
}
|
||||
err := sr.AddReciprocalResourceAssociations(ctx, &source, wrappedResourceModel, &source, relatedResource)
|
||||
if err != nil {
|
||||
sr.Logger.Errorf("Error when creating a reciprocal association for %s: %v", referencedResource, err)
|
||||
}
|
||||
}
|
||||
|
||||
//ignore errors when creating associations (we always get a 'WHERE conditions required' error, )
|
||||
sr.GormClient.WithContext(ctx).Model(wrappedResourceModel).Association("RelatedResourceFhir").Append(relatedResources)
|
||||
|
||||
}
|
||||
|
||||
sr.Logger.Infof("insert/update (%v) %v", rawResource.SourceResourceType, rawResource.SourceResourceID)
|
||||
|
@ -322,7 +310,7 @@ func (sr *SqliteRepository) ListResources(ctx context.Context, queryOptions mode
|
|||
queryBuilder := sr.GormClient.WithContext(ctx)
|
||||
if queryOptions.PreloadRelated {
|
||||
//enable preload functionality in query
|
||||
queryBuilder = queryBuilder.Preload("RelatedResourceFhir").Preload("RelatedResourceFhir.RelatedResourceFhir")
|
||||
queryBuilder = queryBuilder.Preload("RelatedResourceFhir")
|
||||
}
|
||||
results := queryBuilder.Where(queryParam).
|
||||
Find(&wrappedResourceModels)
|
||||
|
@ -392,11 +380,159 @@ func (sr *SqliteRepository) GetPatientForSources(ctx context.Context) ([]models.
|
|||
return wrappedResourceModels, results.Error
|
||||
}
|
||||
|
||||
// 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) ([]*models.ResourceFhir, []*models.ResourceFhir, error) {
|
||||
// Get list of all resources
|
||||
wrappedResourceModels, err := sr.ListResources(ctx, models.ListResourceQueryOptions{})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Get list of all relationships
|
||||
var relatedResourceRelationships []models.RelatedResource
|
||||
|
||||
// SELECT * FROM related_resources WHERE user_id = "53c1e930-63af-46c9-b760-8e83cbc1abd9";
|
||||
result := sr.GormClient.WithContext(ctx).
|
||||
Table("related_resources").
|
||||
Where(models.RelatedResource{
|
||||
ResourceFhirUserID: sr.GetCurrentUser(ctx).ID,
|
||||
}).
|
||||
Scan(&relatedResourceRelationships)
|
||||
if result.Error != nil {
|
||||
return nil, nil, result.Error
|
||||
}
|
||||
|
||||
//Generate Graph
|
||||
g := graph.New(resourceVertexId, graph.Directed(), graph.Acyclic(), graph.Rooted())
|
||||
|
||||
//add vertices to the graph (must be done first)
|
||||
for ndx, _ := range wrappedResourceModels {
|
||||
err = g.AddVertex(
|
||||
&wrappedResourceModels[ndx],
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("an error occurred while adding vertex: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
//add edges to graph
|
||||
for _, relationship := range relatedResourceRelationships {
|
||||
err = g.AddEdge(
|
||||
resourceKeysVertexId(relationship.ResourceFhirSourceID.String(), relationship.ResourceFhirSourceResourceType, relationship.ResourceFhirSourceResourceID),
|
||||
resourceKeysVertexId(relationship.RelatedResourceFhirSourceID.String(), relationship.RelatedResourceFhirSourceResourceType, relationship.RelatedResourceFhirSourceResourceID),
|
||||
)
|
||||
if err != nil {
|
||||
//this may occur because vertices may not exist
|
||||
sr.Logger.Warnf("ignoring, an error occurred while adding edge: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
//// simplify graph if possible.
|
||||
//graph.TransitiveReduction(g)
|
||||
|
||||
// AdjacencyMap computes and returns an adjacency map containing all vertices in the graph.
|
||||
//
|
||||
// There is an entry for each vertex, and each of those entries is another map whose keys are
|
||||
// the hash values of the adjacent vertices. The value is an Edge instance that stores the
|
||||
// source and target hash values (these are the same as the map keys) as well as edge metadata.
|
||||
// map[string]map[string]Edge[string]{
|
||||
// "A": map[string]Edge[string]{
|
||||
// "B": {Source: "A", Target: "B"}
|
||||
// "C": {Source: "A", Target: "C"}
|
||||
// }
|
||||
// }
|
||||
adjacencyMap, err := g.AdjacencyMap()
|
||||
if err != nil {
|
||||
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
|
||||
// vertices joined by an outgoing edge are considered adjacent to the current vertex, whereas
|
||||
// predecessors are the vertices joined by an ingoing edge.
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Doing this in one massive function, because passing graph by reference is difficult due to generics.
|
||||
|
||||
// Step 1: use predecessorMap to find all "root" encounters and conditions. store those nodes in their respective lists.
|
||||
encounterList := []*models.ResourceFhir{}
|
||||
conditionList := []*models.ResourceFhir{}
|
||||
for vertexId, val := range predecessorMap {
|
||||
|
||||
if len(val) != 0 {
|
||||
//skip any nodes/verticies/resources that are not "root"
|
||||
continue
|
||||
}
|
||||
|
||||
resource, err := g.Vertex(vertexId)
|
||||
if err != nil {
|
||||
//could not find this vertex in graph, ignoring
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.ToLower(resource.SourceResourceType) == "condition" {
|
||||
conditionList = append(conditionList, resource)
|
||||
} else if strings.ToLower(resource.SourceResourceType) == "encounter" {
|
||||
encounterList = append(encounterList, resource)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.ResourceFhir) {
|
||||
// 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)
|
||||
|
||||
resource.RelatedResourceFhir = []*models.ResourceFhir{}
|
||||
|
||||
//get all the resources associated with this node
|
||||
graph.DFS(g, vertexId, func(relatedVertexId string) bool {
|
||||
relatedResourceFhir, _ := g.Vertex(relatedVertexId)
|
||||
//skip the current resource if it's referenced in this list.
|
||||
if vertexId != resourceVertexId(relatedResourceFhir) {
|
||||
resource.RelatedResourceFhir = append(resource.RelatedResourceFhir, relatedResourceFhir)
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
// Step 3: populate related resources for each encounter, flattened
|
||||
for ndx, _ := range encounterList {
|
||||
// this is a "root" encounter, which is not related to any condition, we should add it to the Unknown encounters list
|
||||
flattenRelatedResourcesFn(encounterList[ndx])
|
||||
}
|
||||
|
||||
// Step 4: find all encounters referenced by the root conditions, populate them, then add them to the condition as RelatedResourceFhir
|
||||
for ndx, _ := range conditionList {
|
||||
// this is a "root" condition,
|
||||
|
||||
conditionList[ndx].RelatedResourceFhir = []*models.ResourceFhir{}
|
||||
vertexId := resourceVertexId(conditionList[ndx])
|
||||
for relatedVertexId, _ := range adjacencyMap[vertexId] {
|
||||
relatedResourceFhir, _ := g.Vertex(relatedVertexId)
|
||||
flattenRelatedResourcesFn(relatedResourceFhir)
|
||||
conditionList[ndx].RelatedResourceFhir = append(conditionList[ndx].RelatedResourceFhir, relatedResourceFhir)
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: sort conditionList by date
|
||||
|
||||
return conditionList, encounterList, nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Resource Associations
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (sr *SqliteRepository) AddResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error {
|
||||
//edges are always "strongly connected", however "source" nodes (roots, like Condition or Encounter) are only one way.
|
||||
//add an edge from every resource to its related resource. Keep in mind that FHIR resources may not contain reciprocal edges, so we ensure the graph is rooted by flipping any
|
||||
//related resources that are "Condition" or "Encounter"
|
||||
func (sr *SqliteRepository) AddReciprocalResourceAssociations(ctx context.Context, source *models.SourceCredential, resource *models.ResourceFhir, relatedSource *models.SourceCredential, relatedResource *models.ResourceFhir) error {
|
||||
//ensure that the sources are "owned" by the same user
|
||||
|
||||
if source.UserID != relatedSource.UserID {
|
||||
|
@ -405,11 +541,111 @@ func (sr *SqliteRepository) AddResourceAssociation(ctx context.Context, source *
|
|||
return fmt.Errorf("user id's must match current user")
|
||||
}
|
||||
|
||||
//manually create association (we've tried to create using Association.Append, and it doesnt work for some reason.
|
||||
//manually create association(s) (we've tried to create using Association.Append, and it doesnt work for some reason.
|
||||
|
||||
if isResourceGraphSource(resource) && isResourceGraphSource(relatedResource) {
|
||||
//handle the case where both resources are "sources" (Condition or Encounter)
|
||||
if strings.ToLower(resource.SourceResourceType) == "condition" {
|
||||
//condition is always the source
|
||||
if err := sr.AddResourceAssociation(
|
||||
ctx,
|
||||
source,
|
||||
resource.SourceResourceType,
|
||||
resource.SourceResourceID,
|
||||
source,
|
||||
relatedResource.SourceResourceType,
|
||||
relatedResource.SourceResourceID,
|
||||
); err != nil {
|
||||
//ignoring errors, could be due to duplicate edges
|
||||
sr.Logger.Warnf("an error occurred while creating resource association: %v", err)
|
||||
}
|
||||
} else {
|
||||
|
||||
if err := sr.AddResourceAssociation(
|
||||
ctx,
|
||||
source,
|
||||
relatedResource.SourceResourceType,
|
||||
relatedResource.SourceResourceID,
|
||||
source,
|
||||
resource.SourceResourceType,
|
||||
resource.SourceResourceID,
|
||||
); err != nil {
|
||||
//ignoring errors, could be due to duplicate edges
|
||||
sr.Logger.Warnf("an error occurred while creating resource association: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
} else if isResourceGraphSource(resource) || isResourceGraphSink(relatedResource) {
|
||||
//resource is a Source, or the relatedResource is a sink, create a "resource" => "relatedResource" edge
|
||||
|
||||
if err := sr.AddResourceAssociation(
|
||||
ctx,
|
||||
source,
|
||||
resource.SourceResourceType,
|
||||
resource.SourceResourceID,
|
||||
source,
|
||||
relatedResource.SourceResourceType,
|
||||
relatedResource.SourceResourceID,
|
||||
); err != nil {
|
||||
//ignoring errors, could be due to duplicate edges
|
||||
sr.Logger.Warnf("an error occurred while creating resource association: %v", err)
|
||||
}
|
||||
|
||||
} else if isResourceGraphSource(relatedResource) || isResourceGraphSink(resource) {
|
||||
//relatedResource is a Source, or the resource is a sink, create a "relatedResource" => "resource" edge
|
||||
|
||||
if err := sr.AddResourceAssociation(
|
||||
ctx,
|
||||
source,
|
||||
relatedResource.SourceResourceType,
|
||||
relatedResource.SourceResourceID,
|
||||
source,
|
||||
resource.SourceResourceType,
|
||||
resource.SourceResourceID,
|
||||
); err != nil {
|
||||
//ignoring errors, could be due to duplicate edges
|
||||
sr.Logger.Warnf("an error occurred while creating resource association: %v", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
//this is a regular pair of resources, create reciprocal edges
|
||||
|
||||
if err := sr.AddResourceAssociation(
|
||||
ctx,
|
||||
source,
|
||||
resource.SourceResourceType,
|
||||
resource.SourceResourceID,
|
||||
source,
|
||||
relatedResource.SourceResourceType,
|
||||
relatedResource.SourceResourceID,
|
||||
); err != nil {
|
||||
//ignoring errors, could be due to duplicate edges
|
||||
sr.Logger.Warnf("an error occurred while creating resource association: %v", err)
|
||||
}
|
||||
|
||||
if err := sr.AddResourceAssociation(
|
||||
ctx,
|
||||
source,
|
||||
relatedResource.SourceResourceType,
|
||||
relatedResource.SourceResourceID,
|
||||
source,
|
||||
resource.SourceResourceType,
|
||||
resource.SourceResourceID,
|
||||
); err != nil {
|
||||
//ignoring errors, could be due to duplicate edges
|
||||
sr.Logger.Warnf("an error occurred while creating resource association: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sr *SqliteRepository) AddResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error {
|
||||
return sr.GormClient.WithContext(ctx).Table("related_resources").Create(map[string]interface{}{
|
||||
"resource_fhir_user_id": source.UserID,
|
||||
"resource_fhir_source_id": source.ID,
|
||||
"resource_fhir_source_resource_type": resourceType,
|
||||
"resource_fhir_source_resource_id": resourceId,
|
||||
"related_resource_fhir_user_id": relatedSource.UserID,
|
||||
"related_resource_fhir_source_id": relatedSource.ID,
|
||||
"related_resource_fhir_source_resource_type": relatedResourceType,
|
||||
"related_resource_fhir_source_resource_id": relatedResourceId,
|
||||
|
@ -425,9 +661,11 @@ func (sr *SqliteRepository) RemoveResourceAssociation(ctx context.Context, sourc
|
|||
|
||||
//manually create association (we've tried to create using Association.Append, and it doesnt work for some reason.
|
||||
return sr.GormClient.WithContext(ctx).Table("related_resources").Delete(map[string]interface{}{
|
||||
"resource_fhir_user_id": source.UserID,
|
||||
"resource_fhir_source_id": source.ID,
|
||||
"resource_fhir_source_resource_type": resourceType,
|
||||
"resource_fhir_source_resource_id": resourceId,
|
||||
"related_resource_fhir_user_id": relatedSource.UserID,
|
||||
"related_resource_fhir_source_id": relatedSource.ID,
|
||||
"related_resource_fhir_source_resource_type": relatedResourceType,
|
||||
"related_resource_fhir_source_resource_id": relatedResourceId,
|
||||
|
@ -543,3 +781,23 @@ func sqlitePragmaString(pragmas map[string]string) string {
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
//source resource types are resources that are at the root of the graph, nothing may reference them directly
|
||||
func isResourceGraphSource(resource *models.ResourceFhir) bool {
|
||||
sourceResourceTypes := []string{"condition", "encounter"}
|
||||
return slices.Contains(sourceResourceTypes, strings.ToLower(resource.SourceResourceType)) // true
|
||||
}
|
||||
|
||||
//sink resource types are the leaves of the graph, they must not reference anything else. (only be referenced)
|
||||
func isResourceGraphSink(resource *models.ResourceFhir) bool {
|
||||
sinkResourceTypes := []string{"location", "binary", "device", "organization", "practitioner", "medication", "patient"}
|
||||
return slices.Contains(sinkResourceTypes, strings.ToLower(resource.SourceResourceType)) // true
|
||||
}
|
||||
|
||||
// helper function for GetResourceGraph, creating a "hash" for the resource
|
||||
func resourceVertexId(resource *models.ResourceFhir) string {
|
||||
return resourceKeysVertexId(resource.SourceID.String(), resource.SourceResourceType, resource.SourceResourceID)
|
||||
}
|
||||
func resourceKeysVertexId(sourceId string, resourceType string, resourceId string) string {
|
||||
return strings.ToLower(fmt.Sprintf("%s/%s/%s", sourceId, resourceType, resourceId))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package models
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
//this model is used by the DB (see ResourceAssociation for web model)
|
||||
type RelatedResource struct {
|
||||
ResourceFhirUserID uuid.UUID `gorm:"resource_fhir_user_id"`
|
||||
ResourceFhirSourceID uuid.UUID `gorm:"resource_fhir_source_id"`
|
||||
ResourceFhirSourceResourceType string `gorm:"resource_fhir_source_resource_type"`
|
||||
ResourceFhirSourceResourceID string `gorm:"resource_fhir_source_resource_id"`
|
||||
|
||||
RelatedResourceFhirUserID uuid.UUID `gorm:"related_resource_fhir_user_id"`
|
||||
RelatedResourceFhirSourceID uuid.UUID `gorm:"related_resource_fhir_source_id"`
|
||||
RelatedResourceFhirSourceResourceType string `gorm:"related_resource_fhir_source_resource_type"`
|
||||
RelatedResourceFhirSourceResourceID string `gorm:"related_resource_fhir_source_resource_id"`
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package models
|
||||
|
||||
//this model is used by the Webserver (see RelatedResource for db model)
|
||||
type ResourceAssociation struct {
|
||||
SourceID string `json:"source_id"`
|
||||
SourceResourceType string `json:"source_resource_type"`
|
||||
|
|
|
@ -11,7 +11,7 @@ type ResourceFhir struct {
|
|||
ResourceRaw datatypes.JSON `json:"resource_raw" gorm:"resource_raw"`
|
||||
|
||||
//relationships
|
||||
RelatedResourceFhir []*ResourceFhir `json:"related_resources" gorm:"many2many:related_resources;ForeignKey:source_id,source_resource_type,source_resource_id;references:source_id,source_resource_type,source_resource_id;"`
|
||||
RelatedResourceFhir []*ResourceFhir `json:"related_resources" gorm:"many2many:related_resources;ForeignKey:user_id,source_id,source_resource_type,source_resource_id;references:user_id,source_id,source_resource_type,source_resource_id;"`
|
||||
}
|
||||
|
||||
type ListResourceQueryOptions struct {
|
||||
|
|
|
@ -107,3 +107,25 @@ func ReplaceResourceAssociation(c *gin.Context) {
|
|||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// GetResourceFhirGraph
|
||||
// Retrieve a list of all fhir resources (vertex), and a list of all associations (edge)
|
||||
// Generate a graph
|
||||
// find the PredecessorMap
|
||||
// - filter to only vertices that are "Condition" or "Encounter" and are "root" nodes (have no edges directed to this node)
|
||||
func GetResourceFhirGraph(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository)
|
||||
|
||||
conditionResourceList, encounterResourceList, err := databaseRepo.GetFlattenedResourceGraph(c)
|
||||
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": map[string][]*models.ResourceFhir{
|
||||
"Condition": conditionResourceList,
|
||||
"Encounter": encounterResourceList,
|
||||
}})
|
||||
}
|
||||
|
|
|
@ -12,8 +12,6 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CreateSource(c *gin.Context) {
|
||||
|
@ -179,64 +177,6 @@ func ListSource(c *gin.Context) {
|
|||
c.JSON(http.StatusOK, gin.H{"success": true, "data": sourceCreds})
|
||||
}
|
||||
|
||||
func RawRequestSource(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository)
|
||||
|
||||
//!!!!!!INSECURE!!!!!!S
|
||||
//We're setting the username to a user provided value, this is insecure, but required for calling databaseRepo fns
|
||||
c.Set("AUTH_USERNAME", c.Param("username"))
|
||||
|
||||
foundSource, err := databaseRepo.GetSource(c, c.Param("sourceId"))
|
||||
if err != nil {
|
||||
logger.Errorf("An error occurred while finding source credential: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if foundSource == nil {
|
||||
logger.Errorf("Did not source credentials for %s", c.Param("sourceType"))
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
client, updatedSource, err := factory.GetSourceClient(sourcePkg.GetFastenEnv(), foundSource.SourceType, c, logger, foundSource)
|
||||
if err != nil {
|
||||
logger.Errorf("Could not initialize source client %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
//TODO: if source has been updated, we should save the access/refresh token.
|
||||
if updatedSource != nil {
|
||||
logger.Warnf("TODO: source credential has been updated, we should store it in the database: %v", updatedSource)
|
||||
// err := databaseRepo.CreateSource(c, updatedSource)
|
||||
// if err != nil {
|
||||
// logger.Errorf("An error occurred while updating source credential %v", err)
|
||||
// c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||||
// return
|
||||
// }
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
|
||||
parsedUrl, err := url.Parse(strings.TrimSuffix(c.Param("path"), "/"))
|
||||
if err != nil {
|
||||
logger.Errorf("Error parsing request, %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
//make sure we include all query string parameters with the raw request.
|
||||
parsedUrl.RawQuery = c.Request.URL.Query().Encode()
|
||||
|
||||
err = client.GetRequest(parsedUrl.String(), &resp)
|
||||
if err != nil {
|
||||
logger.Errorf("Error making raw request, %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error(), "data": resp})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": resp})
|
||||
}
|
||||
|
||||
func SyncSourceResources(c context.Context, logger *logrus.Entry, databaseRepo database.DatabaseRepository, sourceCred models.SourceCredential) (sourceModels.UpsertSummary, error) {
|
||||
// after creating the source, we should do a bulk import
|
||||
sourceClient, updatedSource, err := factory.GetSourceClient(sourcePkg.GetFastenEnv(), sourceCred.SourceType, c, logger, sourceCred)
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"github.com/fastenhealth/fasten-sources/clients/factory"
|
||||
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
These Endpoints are only available when Fasten is deployed with allow_unsafe_endpoints enabled.
|
||||
*/
|
||||
|
||||
func UnsafeRequestSource(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
appConfig := c.MustGet("CONFIG").(config.Interface)
|
||||
databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository)
|
||||
//safety check incase this function is called in another way
|
||||
if !appConfig.GetBool("web.allow_unsafe_endpoints") {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
//!!!!!!INSECURE!!!!!!S
|
||||
//We're setting the username to a user provided value, this is insecure, but required for calling databaseRepo fns
|
||||
c.Set("AUTH_USERNAME", c.Param("username"))
|
||||
|
||||
foundSource, err := databaseRepo.GetSource(c, c.Param("sourceId"))
|
||||
if err != nil {
|
||||
logger.Errorf("An error occurred while finding source credential: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if foundSource == nil {
|
||||
logger.Errorf("Did not source credentials for %s", c.Param("sourceType"))
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
client, updatedSource, err := factory.GetSourceClient(sourcePkg.GetFastenEnv(), foundSource.SourceType, c, logger, foundSource)
|
||||
if err != nil {
|
||||
logger.Errorf("Could not initialize source client %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
//TODO: if source has been updated, we should save the access/refresh token.
|
||||
if updatedSource != nil {
|
||||
logger.Warnf("TODO: source credential has been updated, we should store it in the database: %v", updatedSource)
|
||||
// err := databaseRepo.CreateSource(c, updatedSource)
|
||||
// if err != nil {
|
||||
// logger.Errorf("An error occurred while updating source credential %v", err)
|
||||
// c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||||
// return
|
||||
// }
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
|
||||
parsedUrl, err := url.Parse(strings.TrimSuffix(c.Param("path"), "/"))
|
||||
if err != nil {
|
||||
logger.Errorf("Error parsing request, %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
//make sure we include all query string parameters with the raw request.
|
||||
parsedUrl.RawQuery = c.Request.URL.Query().Encode()
|
||||
|
||||
err = client.GetRequest(parsedUrl.String(), &resp)
|
||||
if err != nil {
|
||||
logger.Errorf("Error making raw request, %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error(), "data": resp})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": resp})
|
||||
}
|
|
@ -57,17 +57,31 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
|
|||
secure.GET("/source/:sourceId", handler.GetSource)
|
||||
secure.POST("/source/:sourceId/sync", handler.SourceSync)
|
||||
secure.GET("/source/:sourceId/summary", handler.GetSourceSummary)
|
||||
secure.GET("/resource/fhir", handler.ListResourceFhir) //
|
||||
secure.GET("/resource/fhir", handler.ListResourceFhir)
|
||||
secure.GET("/resource/graph", handler.GetResourceFhirGraph)
|
||||
secure.GET("/resource/fhir/:sourceId/:resourceId", handler.GetResourceFhir)
|
||||
secure.POST("/resource/association", handler.ReplaceResourceAssociation)
|
||||
}
|
||||
|
||||
if ae.Config.GetString("log.level") == "DEBUG" {
|
||||
//in debug mode, this endpoint lets us request data directly from the source api
|
||||
ae.Logger.Warningf("***INSECURE*** ***INSECURE*** DEBUG mode enables developer functionality, including unauthenticated raw api requests")
|
||||
if ae.Config.GetBool("web.allow_unsafe_endpoints") {
|
||||
//this endpoint lets us request data directly from the source api
|
||||
ae.Logger.Warningln("***UNSAFE***")
|
||||
ae.Logger.Warningln("***UNSAFE***")
|
||||
ae.Logger.Warningln("***UNSAFE***")
|
||||
ae.Logger.Warningln("***UNSAFE***")
|
||||
ae.Logger.Warningln("***UNSAFE***")
|
||||
ae.Logger.Warningf("\"web.allow_unsafe_endpoints\" mode enabled!! This enables developer functionality, including unauthenticated raw api requests")
|
||||
ae.Logger.Warningln("***UNSAFE***")
|
||||
ae.Logger.Warningln("***UNSAFE***")
|
||||
ae.Logger.Warningln("***UNSAFE***")
|
||||
ae.Logger.Warningln("***UNSAFE***")
|
||||
ae.Logger.Warningln("***UNSAFE***")
|
||||
unsafe := api.Group("/unsafe")
|
||||
{
|
||||
//http://localhost:9090/api/raw/test@test.com/436d7277-ad56-41ce-9823-44e353d1b3f6/Patient/smart-1288992
|
||||
unsafe.GET("/:username/:sourceId/*path", handler.UnsafeRequestSource)
|
||||
|
||||
//http://localhost:9090/api/raw/test@test.com/436d7277-ad56-41ce-9823-44e353d1b3f6/Patient/smart-1288992
|
||||
api.GET("/raw/:username/:sourceId/*path", handler.RawRequestSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {IsAuthenticatedAuthGuard} from './auth-guards/is-authenticated-auth-guar
|
|||
import {SourceDetailComponent} from './pages/source-detail/source-detail.component';
|
||||
import {PatientProfileComponent} from './pages/patient-profile/patient-profile.component';
|
||||
import {MedicalHistoryComponent} from './pages/medical-history/medical-history.component';
|
||||
import {ReportLabsComponent} from './pages/report-labs/report-labs.component';
|
||||
|
||||
const routes: Routes = [
|
||||
|
||||
|
@ -29,6 +30,7 @@ const routes: Routes = [
|
|||
|
||||
{ path: 'patient-profile', component: PatientProfileComponent, canActivate: [ IsAuthenticatedAuthGuard] },
|
||||
{ path: 'medical-history', component: MedicalHistoryComponent, canActivate: [ IsAuthenticatedAuthGuard] },
|
||||
{ path: 'labs', component: ReportLabsComponent, canActivate: [ IsAuthenticatedAuthGuard] },
|
||||
|
||||
// { path: 'general-pages', loadChildren: () => import('./general-pages/general-pages.module').then(m => m.GeneralPagesModule) },
|
||||
// { path: 'ui-elements', loadChildren: () => import('./ui-elements/ui-elements.module').then(m => m.UiElementsModule) },
|
||||
|
|
|
@ -28,6 +28,7 @@ import { MomentModule } from 'ngx-moment';
|
|||
import {AuthService} from './services/auth.service';
|
||||
import { PatientProfileComponent } from './pages/patient-profile/patient-profile.component';
|
||||
import { MedicalHistoryComponent } from './pages/medical-history/medical-history.component';
|
||||
import { ReportLabsComponent } from './pages/report-labs/report-labs.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
|
@ -43,6 +44,7 @@ import { MedicalHistoryComponent } from './pages/medical-history/medical-history
|
|||
SourceDetailComponent,
|
||||
PatientProfileComponent,
|
||||
MedicalHistoryComponent,
|
||||
ReportLabsComponent,
|
||||
],
|
||||
imports: [
|
||||
FormsModule,
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<div class="card card-dashboard-seven mb-3">
|
||||
<div class="card-header tx-medium">
|
||||
<div class="row" >
|
||||
<!-- Condition Header -->
|
||||
<div class="col-6">
|
||||
{{observationTitle}}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
{{observations[0] | fhirPath: "Observation.effectiveDateTime": "Observation.issued" | date}}
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- card-header -->
|
||||
<div class="card-body">
|
||||
|
||||
<div class="row">
|
||||
<!-- Condition Details -->
|
||||
|
||||
<div class="col-6 mb-2">
|
||||
|
||||
<div class="row pl-3">
|
||||
<div class="col-12 mt-3 mb-2">
|
||||
<p>
|
||||
<strong>Test Code (loinc):</strong> {{observationCode}} <br/>
|
||||
<strong>Latest Test Date:</strong> {{observations[0] | fhirPath: "Observation.effectiveDateTime": "Observation.issued" | date}} <br/>
|
||||
<strong>Ordered By:</strong> {{observations[0] | fhirPath: "Observation.encounter.display"}} <br/>
|
||||
<strong>Notes:</strong>
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-6 bg-gray-100">
|
||||
<div class="row">
|
||||
<p>
|
||||
<small>
|
||||
Troponin I is a part of the troponin complex. It binds to actin in thin myofilaments to hold the actin-tropomyosin complex in place. Because of it myosin cannot bind actin in relaxed muscle. When calcium binds to the Troponin C it causes conformational changes which lead to dislocation of troponin I and finally tropomyosin leaves the binding site for myosin on actin leading to contraction of muscle. The letter I is given due to its inhibitory character.
|
||||
</small>
|
||||
</p>
|
||||
|
||||
|
||||
<ng-container *ngFor="let observation of observations">
|
||||
|
||||
<div routerLink="/source/{{observation?.source_id}}/resource/{{observation?.source_resource_id}}" class="col-6 mt-3 mb-2 tx-indigo">
|
||||
<strong>{{observation | fhirPath: "Observation.valueQuantity.value" }}</strong>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div><!-- card-body -->
|
||||
</div>
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ReportLabsObservationComponent } from './report-labs-observation.component';
|
||||
|
||||
describe('ReportLabsObservationComponent', () => {
|
||||
let component: ReportLabsObservationComponent;
|
||||
let fixture: ComponentFixture<ReportLabsObservationComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ReportLabsObservationComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ReportLabsObservationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import {ResourceFhir} from '../../models/fasten/resource_fhir';
|
||||
|
||||
@Component({
|
||||
selector: 'app-report-labs-observation',
|
||||
templateUrl: './report-labs-observation.component.html',
|
||||
styleUrls: ['./report-labs-observation.component.scss']
|
||||
})
|
||||
export class ReportLabsObservationComponent implements OnInit {
|
||||
|
||||
@Input() observations: ResourceFhir[]
|
||||
@Input() observationCode: string
|
||||
@Input() observationTitle: string
|
||||
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
|
@ -40,6 +40,7 @@ import { ReportMedicalHistoryEditorComponent } from './report-medical-history-ed
|
|||
import { TreeModule } from '@circlon/angular-tree-component';
|
||||
import {FilterPipe} from '../pipes/filter.pipe';
|
||||
import { ReportMedicalHistoryConditionComponent } from './report-medical-history-condition/report-medical-history-condition.component';
|
||||
import { ReportLabsObservationComponent } from './report-labs-observation/report-labs-observation.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -86,6 +87,7 @@ import { ReportMedicalHistoryConditionComponent } from './report-medical-history
|
|||
ReportMedicalHistoryEditorComponent,
|
||||
FilterPipe,
|
||||
ReportMedicalHistoryConditionComponent,
|
||||
ReportLabsObservationComponent,
|
||||
],
|
||||
exports: [
|
||||
ComponentsSidebarComponent,
|
||||
|
@ -118,10 +120,11 @@ import { ReportMedicalHistoryConditionComponent } from './report-medical-history
|
|||
ResourceListOutletDirective,
|
||||
ToastComponent,
|
||||
ReportHeaderComponent,
|
||||
ReportMedicalHistoryEditorComponent,
|
||||
FhirPathPipe,
|
||||
FilterPipe,
|
||||
ReportMedicalHistoryConditionComponent
|
||||
ReportMedicalHistoryEditorComponent,
|
||||
FhirPathPipe,
|
||||
FilterPipe,
|
||||
ReportMedicalHistoryConditionComponent,
|
||||
ReportLabsObservationComponent
|
||||
|
||||
]
|
||||
})
|
||||
|
|
|
@ -25,25 +25,15 @@ export class MedicalHistoryComponent implements OnInit {
|
|||
|
||||
|
||||
ngOnInit(): void {
|
||||
forkJoin([
|
||||
this.fastenApi.getResources("Condition", null, null, true),
|
||||
this.fastenApi.getResources("Encounter", null, null, true)
|
||||
]).subscribe(results => {
|
||||
this.conditions = results[0]
|
||||
this.fastenApi.getResourceGraph().subscribe(results => {
|
||||
this.conditions = results["Condition"]
|
||||
this.unassigned_encounters = results["Encounter"]
|
||||
|
||||
//populate a lookup table with all resources
|
||||
for(let condition of this.conditions){
|
||||
this.recPopulateResourceLookup(condition)
|
||||
}
|
||||
|
||||
console.log("Populated resource lookup:", this.resourceLookup);
|
||||
|
||||
//find unassigned encounters
|
||||
console.log("all encounters:", results[1].length, results[1]);
|
||||
(results[1] || []).map((encounter) => {
|
||||
if(!this.resourceLookup[`${encounter.source_id}/${encounter.source_resource_type}/${encounter.source_resource_id}`]){
|
||||
this.unassigned_encounters.push(encounter)
|
||||
}
|
||||
})
|
||||
|
||||
if(this.unassigned_encounters.length > 0){
|
||||
console.log("Found mapping:", this.resourceLookup)
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<div class="az-content">
|
||||
<div class="container">
|
||||
<div class="az-content-body">
|
||||
|
||||
<!-- Header Row -->
|
||||
<report-header [reportHeaderTitle]="'Labs'"></report-header>
|
||||
|
||||
|
||||
|
||||
<!-- Observations Title -->
|
||||
<div class="row mt-5 mb-3">
|
||||
<div class="col-6">
|
||||
<h1 class="az-dashboard-title">Observations</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Observations List -->
|
||||
<app-report-labs-observation *ngFor="let observationGroup of observationGroups | keyvalue"
|
||||
[observations]="observationGroup.value"
|
||||
[observationCode]="observationGroup.key"
|
||||
[observationTitle]="observationGroupTitles[observationGroup.key]"
|
||||
></app-report-labs-observation>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ReportLabsComponent } from './report-labs.component';
|
||||
|
||||
describe('ReportLabsComponent', () => {
|
||||
let component: ReportLabsComponent;
|
||||
let fixture: ComponentFixture<ReportLabsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ReportLabsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ReportLabsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import {FastenApiService} from '../../services/fasten-api.service';
|
||||
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {ResourceFhir} from '../../models/fasten/resource_fhir';
|
||||
import * as fhirpath from 'fhirpath';
|
||||
|
||||
@Component({
|
||||
selector: 'app-report-labs',
|
||||
templateUrl: './report-labs.component.html',
|
||||
styleUrls: ['./report-labs.component.scss']
|
||||
})
|
||||
export class ReportLabsComponent implements OnInit {
|
||||
|
||||
observationGroups: {[key: string]: ResourceFhir[]} = {}
|
||||
observationGroupTitles: {[key: string]: string} = {}
|
||||
|
||||
constructor(
|
||||
private fastenApi: FastenApiService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.fastenApi.getResources("Observation").subscribe(results => {
|
||||
console.log("ALL OBSERVATIONS", results)
|
||||
|
||||
//loop though all observations, group by "code.system": "http://loinc.org"
|
||||
for(let observation of results){
|
||||
let observationGroup = fhirpath.evaluate(observation.resource_raw, "Observation.code.coding.where(system='http://loinc.org').first().code")[0]
|
||||
this.observationGroups[observationGroup] = this.observationGroups[observationGroup] ? this.observationGroups[observationGroup] : []
|
||||
this.observationGroups[observationGroup].push(observation)
|
||||
|
||||
if(!this.observationGroupTitles[observationGroup]){
|
||||
this.observationGroupTitles[observationGroup] = fhirpath.evaluate(observation.resource_raw, "Observation.code.coding.where(system='http://loinc.org').first().display")[0]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//TODO: sort observation groups
|
||||
|
||||
// this.observationGroups = results
|
||||
//
|
||||
// //populate a lookup table with all resources
|
||||
// for (let condition of this.conditions) {
|
||||
// this.recPopulateResourceLookup(condition)
|
||||
// }
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -126,6 +126,16 @@ export class FastenApiService {
|
|||
);
|
||||
}
|
||||
|
||||
getResourceGraph(): Observable<{[resourceType: string]: ResourceFhir[]}> {
|
||||
return this._httpClient.get<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/resource/graph`)
|
||||
.pipe(
|
||||
map((response: ResponseWrapper) => {
|
||||
console.log("RESPONSE", response)
|
||||
return response.data as {[name: string]: ResourceFhir[]}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getResourceBySourceId(sourceId: string, resourceId: string): Observable<ResourceFhir> {
|
||||
|
||||
return this._httpClient.get<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/resource/fhir/${sourceId}/${resourceId}`)
|
||||
|
|
3
go.mod
3
go.mod
|
@ -4,6 +4,7 @@ go 1.18
|
|||
|
||||
require (
|
||||
github.com/analogj/go-util v0.0.0-20210417161720-39b497cca03b
|
||||
github.com/dominikbraun/graph v0.15.0
|
||||
github.com/fastenhealth/fasten-sources v0.0.7
|
||||
github.com/gin-gonic/gin v1.8.1
|
||||
github.com/glebarez/sqlite v1.5.0
|
||||
|
@ -14,6 +15,7 @@ require (
|
|||
github.com/spf13/viper v1.12.0
|
||||
github.com/urfave/cli/v2 v2.11.2
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17
|
||||
gorm.io/datatypes v1.0.7
|
||||
gorm.io/gorm v1.24.1
|
||||
)
|
||||
|
@ -57,7 +59,6 @@ require (
|
|||
github.com/subosito/gotenv v1.3.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
|
||||
golang.org/x/net v0.2.0 // indirect
|
||||
golang.org/x/oauth2 v0.2.0 // indirect
|
||||
golang.org/x/sys v0.2.0 // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -65,6 +65,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||
github.com/denisenkom/go-mssqldb v0.12.0 h1:VtrkII767ttSPNRfFekePK3sctr+joXgO58stqQbtUA=
|
||||
github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/dominikbraun/graph v0.15.0 h1:U6N0fWJTDsUO9R5NhFE1Xi0YGGQ1+nKV9KO5apClf3E=
|
||||
github.com/dominikbraun/graph v0.15.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
|
|
Loading…
Reference in New Issue