diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 5a64a9e5..686fd446 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -65,5 +65,7 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+ build-args: |
+ FASTEN_ENV=${{ github.ref_name == 'main' && 'prod' || 'sandbox' }}
# cache-from: type=gha
# cache-to: type=gha,mode=max
diff --git a/Dockerfile b/Dockerfile
index 499ef709..72470362 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,6 +2,7 @@
# Frontend Build
#########################################################################################################
FROM node:18.9.0 as frontend-build
+ARG FASTEN_ENV=sandbox
WORKDIR /usr/src/fastenhealth/frontend
#COPY frontend/package.json frontend/yarn.lock ./
COPY frontend/package.json ./
@@ -9,7 +10,7 @@ COPY frontend/package.json ./
RUN yarn config set registry "http://registry.npmjs.org" \
&& yarn install --frozen-lockfile --network-timeout 100000
COPY frontend/ ./
-RUN yarn run build -- --configuration sandbox --output-path=../dist
+RUN yarn run build -- --configuration ${FASTEN_ENV} --output-path=../dist
#########################################################################################################
# Backend Build
@@ -29,26 +30,13 @@ RUN CGO_ENABLED=0 go build -o /go/bin/fasten ./backend/cmd/fasten/
# create folder structure
RUN mkdir -p /opt/fasten/db \
&& mkdir -p /opt/fasten/web \
- && mkdir -p /opt/fasten/config \
- && curl -o /opt/fasten/db/fasten.db -L https://github.com/fastenhealth/testdata/raw/main/fasten.db
-
+ && mkdir -p /opt/fasten/config
#########################################################################################################
# Distribution Build
#########################################################################################################
-FROM couchdb:3.2
+FROM gcr.io/distroless/static-debian11
-ENV FASTEN_COUCHDB_ADMIN_USERNAME=admin
-ENV FASTEN_COUCHDB_ADMIN_PASSWORD=mysecretpassword
-ENV FASTEN_JWT_ISSUER_KEY=thisismysupersecuressessionsecretlength
-
-ARG S6_ARCH=amd64
-RUN curl https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${S6_ARCH}.tar.gz -L -s --output /tmp/s6-overlay-${S6_ARCH}.tar.gz \
- && tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / \
- && rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.gz
-
-COPY /docker/couchdb/fasten.ini /opt/couchdb/etc/local.ini
-COPY /docker/rootfs /
WORKDIR /opt/fasten/
COPY --from=backend-build /opt/fasten/ /opt/fasten/
@@ -57,7 +45,7 @@ COPY --from=backend-build /go/bin/fasten /opt/fasten/fasten
COPY LICENSE.md /opt/fasten/LICENSE.md
COPY config.yaml /opt/fasten/config/config.yaml
-ENTRYPOINT ["/init"]
+CMD ["/opt/fasten/fasten", "start", "--config", "/opt/fasten/config/config.yaml"]
diff --git a/backend/pkg/database/interface.go b/backend/pkg/database/interface.go
index 35835520..33c14fac 100644
--- a/backend/pkg/database/interface.go
+++ b/backend/pkg/database/interface.go
@@ -22,6 +22,8 @@ type DatabaseRepository interface {
GetResourceBySourceId(context.Context, string, string) (*models.ResourceFhir, error)
ListResources(context.Context, models.ListResourceQueryOptions) ([]models.ResourceFhir, error)
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
//UpsertProfile(context.Context, *models.Profile) error
//UpsertOrganziation(context.Context, *models.Organization) error
diff --git a/backend/pkg/database/sqlite_repository.go b/backend/pkg/database/sqlite_repository.go
index cce9db18..28bf06e1 100644
--- a/backend/pkg/database/sqlite_repository.go
+++ b/backend/pkg/database/sqlite_repository.go
@@ -194,7 +194,47 @@ func (sr *SqliteRepository) UpsertRawResource(ctx context.Context, sourceCredent
SourceResourceID: rawResource.SourceResourceID,
SourceResourceType: rawResource.SourceResourceType,
},
- ResourceRaw: datatypes.JSON(rawResource.ResourceRaw),
+ ResourceRaw: datatypes.JSON(rawResource.ResourceRaw),
+ RelatedResourceFhir: nil,
+ }
+
+ //create associations
+ //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)
+ }
+ }
+
+ }
+ }
+
+ //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)
@@ -203,7 +243,7 @@ func (sr *SqliteRepository) UpsertRawResource(ctx context.Context, sourceCredent
SourceID: wrappedResourceModel.GetSourceID(),
SourceResourceID: wrappedResourceModel.GetSourceResourceID(),
SourceResourceType: wrappedResourceModel.GetSourceResourceType(), //TODO: and UpdatedAt > old UpdatedAt
- }).FirstOrCreate(wrappedResourceModel)
+ }).Omit("RelatedResourceFhir.*").FirstOrCreate(wrappedResourceModel)
if createResult.Error != nil {
return false, createResult.Error
@@ -211,7 +251,7 @@ func (sr *SqliteRepository) UpsertRawResource(ctx context.Context, sourceCredent
//at this point, wrappedResourceModel contains the data found in the database.
// check if the database resource matches the new resource.
if wrappedResourceModel.ResourceRaw.String() != string(rawResource.ResourceRaw) {
- updateResult := createResult.Updates(wrappedResourceModel)
+ updateResult := createResult.Omit("RelatedResourceFhir.*").Updates(wrappedResourceModel)
return updateResult.RowsAffected > 0, updateResult.Error
} else {
return false, nil
@@ -271,13 +311,20 @@ func (sr *SqliteRepository) ListResources(ctx context.Context, queryOptions mode
queryParam.OriginBase.SourceID = sourceUUID
}
+ if len(queryOptions.SourceResourceID) > 0 {
+ queryParam.OriginBase.SourceResourceID = queryOptions.SourceResourceID
+ }
manifestJson, _ := json.MarshalIndent(queryParam, "", " ")
sr.Logger.Infof("THE QUERY OBJECT===========> %v", string(manifestJson))
var wrappedResourceModels []models.ResourceFhir
- results := sr.GormClient.WithContext(ctx).
- Where(queryParam).
+ queryBuilder := sr.GormClient.WithContext(ctx)
+ if queryOptions.PreloadRelated {
+ //enable preload functionality in query
+ queryBuilder = queryBuilder.Preload("RelatedResourceFhir").Preload("RelatedResourceFhir.RelatedResourceFhir")
+ }
+ results := queryBuilder.Where(queryParam).
Find(&wrappedResourceModels)
return wrappedResourceModels, results.Error
@@ -345,6 +392,48 @@ func (sr *SqliteRepository) GetPatientForSources(ctx context.Context) ([]models.
return wrappedResourceModels, results.Error
}
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Resource Associations
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+func (sr *SqliteRepository) AddResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error {
+ //ensure that the sources are "owned" by the same user
+
+ if source.UserID != relatedSource.UserID {
+ return fmt.Errorf("user id's must match when adding associations")
+ } else if source.UserID != sr.GetCurrentUser(ctx).ID {
+ 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.
+ return sr.GormClient.WithContext(ctx).Table("related_resources").Create(map[string]interface{}{
+ "resource_fhir_source_id": source.ID,
+ "resource_fhir_source_resource_type": resourceType,
+ "resource_fhir_source_resource_id": resourceId,
+ "related_resource_fhir_source_id": relatedSource.ID,
+ "related_resource_fhir_source_resource_type": relatedResourceType,
+ "related_resource_fhir_source_resource_id": relatedResourceId,
+ }).Error
+}
+
+func (sr *SqliteRepository) RemoveResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error {
+ if source.UserID != relatedSource.UserID {
+ return fmt.Errorf("user id's must match when adding associations")
+ } else if source.UserID != sr.GetCurrentUser(ctx).ID {
+ 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.
+ return sr.GormClient.WithContext(ctx).Table("related_resources").Delete(map[string]interface{}{
+ "resource_fhir_source_id": source.ID,
+ "resource_fhir_source_resource_type": resourceType,
+ "resource_fhir_source_resource_id": resourceId,
+ "related_resource_fhir_source_id": relatedSource.ID,
+ "related_resource_fhir_source_resource_type": relatedResourceType,
+ "related_resource_fhir_source_resource_id": relatedResourceId,
+ }).Error
+}
+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SourceCredential
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/backend/pkg/models/resource_association.go b/backend/pkg/models/resource_association.go
new file mode 100644
index 00000000..f453df9b
--- /dev/null
+++ b/backend/pkg/models/resource_association.go
@@ -0,0 +1,15 @@
+package models
+
+type ResourceAssociation struct {
+ SourceID string `json:"source_id"`
+ SourceResourceType string `json:"source_resource_type"`
+ SourceResourceID string `json:"source_resource_id"`
+
+ OldRelatedSourceID string `json:"old_related_source_id"`
+ OldRelatedSourceResourceType string `json:"old_related_source_resource_type"`
+ OldRelatedSourceResourceID string `json:"old_related_source_resource_id"`
+
+ NewRelatedSourceID string `json:"new_related_source_id"`
+ NewRelatedSourceResourceType string `json:"new_related_source_resource_type"`
+ NewRelatedSourceResourceID string `json:"new_related_source_resource_id"`
+}
diff --git a/backend/pkg/models/resource_fhir.go b/backend/pkg/models/resource_fhir.go
index 02ba3936..cc037a30 100644
--- a/backend/pkg/models/resource_fhir.go
+++ b/backend/pkg/models/resource_fhir.go
@@ -9,9 +9,15 @@ type ResourceFhir struct {
//embedded data
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;"`
}
type ListResourceQueryOptions struct {
SourceID string
SourceResourceType string
+ SourceResourceID string
+
+ PreloadRelated bool
}
diff --git a/backend/pkg/web/handler/resource_fhir.go b/backend/pkg/web/handler/resource_fhir.go
index 73d50b9e..b5d26ac4 100644
--- a/backend/pkg/web/handler/resource_fhir.go
+++ b/backend/pkg/web/handler/resource_fhir.go
@@ -20,6 +20,12 @@ func ListResourceFhir(c *gin.Context) {
if len(c.Query("sourceID")) > 0 {
listResourceQueryOptions.SourceID = c.Query("sourceID")
}
+ if len(c.Query("sourceResourceID")) > 0 {
+ listResourceQueryOptions.SourceResourceID = c.Query("sourceResourceID")
+ }
+ if len(c.Query("preloadRelated")) > 0 {
+ listResourceQueryOptions.PreloadRelated = true
+ }
wrappedResourceModels, err := databaseRepo.ListResources(c, listResourceQueryOptions)
@@ -49,3 +55,55 @@ func GetResourceFhir(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": wrappedResourceModel})
}
+
+func ReplaceResourceAssociation(c *gin.Context) {
+
+ logger := c.MustGet("LOGGER").(*logrus.Entry)
+ databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository)
+
+ resourceAssociation := models.ResourceAssociation{}
+ if err := c.ShouldBindJSON(&resourceAssociation); err != nil {
+ logger.Errorln("An error occurred while parsing posted resource association data", err)
+ c.JSON(http.StatusBadRequest, gin.H{"success": false})
+ return
+ }
+
+ sourceCred, err := databaseRepo.GetSource(c, resourceAssociation.SourceID)
+ if err != nil {
+ logger.Errorln("An error occurred while retrieving source", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"success": false})
+ return
+ }
+
+ if len(resourceAssociation.OldRelatedSourceID) > 0 {
+ oldRelatedSourceCred, err := databaseRepo.GetSource(c, resourceAssociation.OldRelatedSourceID)
+ if err != nil {
+ logger.Errorln("An error occurred while retrieving old related source", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"success": false})
+ return
+ }
+
+ err = databaseRepo.RemoveResourceAssociation(c, sourceCred, resourceAssociation.SourceResourceType, resourceAssociation.SourceResourceID, oldRelatedSourceCred, resourceAssociation.OldRelatedSourceResourceType, resourceAssociation.OldRelatedSourceResourceID)
+ if err != nil {
+ logger.Errorln("An error occurred while deleting resource association", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"success": false})
+ return
+ }
+ }
+
+ newRelatedSourceCred, err := databaseRepo.GetSource(c, resourceAssociation.NewRelatedSourceID)
+ if err != nil {
+ logger.Errorln("An error occurred while retrieving new related source", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"success": false})
+ return
+ }
+
+ err = databaseRepo.AddResourceAssociation(c, sourceCred, resourceAssociation.SourceResourceType, resourceAssociation.SourceResourceID, newRelatedSourceCred, resourceAssociation.NewRelatedSourceResourceType, resourceAssociation.NewRelatedSourceResourceID)
+ if err != nil {
+ logger.Errorln("An error occurred while associating resource", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"success": false})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"success": true})
+}
diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go
index c0c5df6c..bf8d5dd7 100644
--- a/backend/pkg/web/server.go
+++ b/backend/pkg/web/server.go
@@ -59,6 +59,7 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
secure.GET("/source/:sourceId/summary", handler.GetSourceSummary)
secure.GET("/resource/fhir", handler.ListResourceFhir) //
secure.GET("/resource/fhir/:sourceId/:resourceId", handler.GetResourceFhir)
+ secure.POST("/resource/association", handler.ReplaceResourceAssociation)
}
if ae.Config.GetString("log.level") == "DEBUG" {
diff --git a/config.yaml b/config.yaml
index 46533d65..383516e5 100644
--- a/config.yaml
+++ b/config.yaml
@@ -22,4 +22,4 @@ web:
log:
file: '' #absolute or relative paths allowed, eg. web.log
- level: DEBUG
+ level: INFO
diff --git a/docker/couchdb/Dockerfile b/docker/couchdb/Dockerfile
deleted file mode 100644
index 0e2aadab..00000000
--- a/docker/couchdb/Dockerfile
+++ /dev/null
@@ -1,20 +0,0 @@
-#########################################################################################################
-# CouchDB Build
-# NOTE: the context for this build should be the root of the repository.
-#########################################################################################################
-FROM couchdb:3.2
-
-ENV FASTEN_COUCHDB_ADMIN_USERNAME=admin
-ENV FASTEN_COUCHDB_ADMIN_PASSWORD=mysecretpassword
-ENV FASTEN_JWT_ISSUER_KEY=thisismysupersecuressessionsecretlength
-
-ARG S6_ARCH=amd64
-RUN curl https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${S6_ARCH}.tar.gz -L -s --output /tmp/s6-overlay-${S6_ARCH}.tar.gz \
- && tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / \
- && rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.gz
-
-COPY /docker/couchdb/fasten.ini /opt/couchdb/etc/local.ini
-COPY /docker/rootfs /
-RUN rm -rf /etc/services.d/fasten #delete the fasten app from the couchdb-only container.
-
-ENTRYPOINT ["/init"]
diff --git a/docker/couchdb/fasten.ini b/docker/couchdb/fasten.ini
deleted file mode 100644
index 7b82eeb7..00000000
--- a/docker/couchdb/fasten.ini
+++ /dev/null
@@ -1,54 +0,0 @@
-; CouchDB Configuration Settings
-; Custom settings should be made in this file. They will override settings
-; in default.ini, but unlike changes made to default.ini, this file won't be
-; overwritten on server upgrade.
-
-[couch_peruser]
-
-; fasten requires that each user have a private database. These databases are writable only by the corresponding user.
-; Databases are in the following form: userdb-{hex encoded username}
-enable = true
-
-[chttpd_auth]
-
-; require_valid_user must be set to false because Fasten will check session endpoint to determine if user is authenticated.
-; if this option is not disabled, user is prompted with basic auth.
-require_valid_user = false
-
-[httpd]
-
-; enable CORS support, required because the database is hosted on a different node.
-enable_cors = true
-
-; ------------------------------------------ DOCKER MODIFICATIONS
-; ------------------------------------------ DOCKER MODIFICATIONS
-; ------------------------------------------ DOCKER MODIFICATIONS
-; ------------------------------------------ DOCKER MODIFICATIONS
-
-; always use single node in docker
-[couchdb]
-;max_document_size = 4294967296 ; bytes
-;os_process_timeout = 5000
-single_node = true
-
-; when running in docker, allow cors for all domains
-; TODO, we should find a more secure way to do this
-[cors]
-origins = *
-headers = accept, authorization, content-type, origin, referer
-credentials = true
-methods = GET, PUT, POST, HEAD, DELETE
-max_age = 3600
-
-# make sure the databse is listening to all traffic, not just from localhost within the container.
-[chttpd]
-;port = 5984
-;bind_address = 127.0.0.1
-bind_address = 0.0.0.0
-enable_cors = true
-x_forwarded_host = X-Forwarded-Host
-; require_valid_user must be set to false because Fasten will check session endpoint to determine if user is authenticated.
-; if this option is not disabled, user is prompted with basic auth.
-require_valid_user = false
-; fasten uses JWT tokens to authenticate against the database. we override the authentication_handlers to add jwt_authentication_handler
-authentication_handlers = {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler}
diff --git a/docker/rootfs/etc/cont-init.d/01-timezone b/docker/rootfs/etc/cont-init.d/01-timezone
deleted file mode 100644
index 83fb30c2..00000000
--- a/docker/rootfs/etc/cont-init.d/01-timezone
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/with-contenv bash
-
-if [ -n "${TZ}" ]
-then
- ln -snf "/usr/share/zoneinfo/${TZ}" /etc/localtime
- echo "${TZ}" > /etc/timezone
-fi
diff --git a/docker/rootfs/etc/cont-init.d/05-couchdb-config b/docker/rootfs/etc/cont-init.d/05-couchdb-config
deleted file mode 100644
index a9af87c7..00000000
--- a/docker/rootfs/etc/cont-init.d/05-couchdb-config
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/usr/bin/with-contenv bash
-
-if [ -f "/opt/couchdb/data/.config_complete" ]; then
- echo "Couchdb config has already completed, skipping"
-else
- #echo -n means we dont pass newline to base64 (ugh). eg. hello = aGVsbG8K vs aGVsbG8=
- FASTEN_JWT_ISSUER_KEY_BASE64=$(echo -n "${FASTEN_JWT_ISSUER_KEY}" | base64)
-
-
-cat << EOF >> /opt/couchdb/etc/local.d/generated.ini
-
-; ------------------------------------------ GENERATED MODIFICATIONS
-; ------------------------------------------ GENERATED MODIFICATIONS
-; ------------------------------------------ GENERATED MODIFICATIONS
-;
-[jwt_auth]
-required_claims = exp, {iss, "docker-fastenhealth"}
-
-[jwt_keys]
-hmac:_default = ${FASTEN_JWT_ISSUER_KEY_BASE64}
-
-
-; users should change this default password
-[admins]
-${FASTEN_COUCHDB_ADMIN_USERNAME} = ${FASTEN_COUCHDB_ADMIN_PASSWORD}
-
-EOF
-
- # create the config complete flag
- echo "Couchdb config: complete"
- touch /opt/couchdb/data/.config_complete
-
-fi
diff --git a/docker/rootfs/etc/cont-init.d/50-couchdb-init b/docker/rootfs/etc/cont-init.d/50-couchdb-init
deleted file mode 100755
index 6338b0ad..00000000
--- a/docker/rootfs/etc/cont-init.d/50-couchdb-init
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/with-contenv bash
-
-if [ -f "/opt/couchdb/data/.init_complete" ]; then
- echo "Couchdb initialization has already completed, skipping"
-else
- # start couchdb as a background process (store PID)
- echo "Couchdb initialization: start couchdb in background mode (non-standard port)"
- # https://linux.die.net/man/1/couchdb
- sed -i -e 's/;port = 5984/port = 5432/g' /opt/couchdb/etc/local.ini
- sed -i -e 's/bind_address = 0.0.0.0/bind_address = 127.0.0.1/g' /opt/couchdb/etc/local.ini
- /opt/couchdb/bin/couchdb -b &
- COUCHDB_PID=$!
-
- # wait for couchdb to be ready
- until $(curl --output /dev/null --silent --head --fail http://127.0.0.1:5432/_up); do echo "couchdb not ready" && sleep 5; done
-
- # create couch_peruser required system databases manually on startup
- echo "couchdb ready, start creating system databases"
- curl --fail -X PUT -u ${FASTEN_COUCHDB_ADMIN_USERNAME}:${FASTEN_COUCHDB_ADMIN_PASSWORD} http://127.0.0.1:5432/_users
- curl --fail -X PUT -u ${FASTEN_COUCHDB_ADMIN_USERNAME}:${FASTEN_COUCHDB_ADMIN_PASSWORD} http://127.0.0.1:5432/_replicator
- curl --fail -X PUT -u ${FASTEN_COUCHDB_ADMIN_USERNAME}:${FASTEN_COUCHDB_ADMIN_PASSWORD} http://127.0.0.1:5432/_global_changes
- echo "system databases created successfully"
-
- # gracefully stop couchdb process
- echo "killing couchdb process"
- /opt/couchdb/bin/couchdb -k
-
- sed -i -e 's/port = 5432/;port = 5984/g' /opt/couchdb/etc/local.ini
- sed -i -e 's/bind_address = 127.0.0.1/bind_address = 0.0.0.0/g' /opt/couchdb/etc/local.ini
-
- # create the init complete flag
- echo "Couchdb initialization: complete"
- touch /opt/couchdb/data/.init_complete
-
-fi
diff --git a/docker/rootfs/etc/services.d/couchdb/run b/docker/rootfs/etc/services.d/couchdb/run
deleted file mode 100644
index 677da6c7..00000000
--- a/docker/rootfs/etc/services.d/couchdb/run
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/with-contenv bash
-
-echo "starting couchdb"
-/docker-entrypoint.sh /opt/couchdb/bin/couchdb
diff --git a/docker/rootfs/etc/services.d/fasten/run b/docker/rootfs/etc/services.d/fasten/run
deleted file mode 100644
index e7874cb9..00000000
--- a/docker/rootfs/etc/services.d/fasten/run
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/with-contenv bash
-
-# wait for couchdb to be ready
-until $(curl --output /dev/null --silent --head --fail http://127.0.0.1:5984/_up); do echo "couchdb not ready" && sleep 5; done
-
-echo "starting fasten"
-/opt/fasten/fasten start --config /opt/fasten/config/config.yaml
diff --git a/frontend/angular.json b/frontend/angular.json
index 6f741371..b51f5332 100644
--- a/frontend/angular.json
+++ b/frontend/angular.json
@@ -73,7 +73,7 @@
],
"optimization": true,
"outputHashing": "all",
- "sourceMap": false,
+ "sourceMap": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
@@ -82,7 +82,7 @@
{
"type": "initial",
"maximumWarning": "2mb",
- "maximumError": "5mb"
+ "maximumError": "15mb"
},
{
"type": "anyComponentStyle",
diff --git a/frontend/package.json b/frontend/package.json
index c297d9f5..eabef9ae 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -23,6 +23,7 @@
"@angular/platform-browser": "^14.1.3",
"@angular/platform-browser-dynamic": "^14.1.3",
"@angular/router": "^14.1.3",
+ "@circlon/angular-tree-component": "^11.0.4",
"@fortawesome/angular-fontawesome": "^0.11.1",
"@fortawesome/fontawesome-svg-core": "^6.2.0",
"@fortawesome/free-regular-svg-icons": "^6.2.0",
@@ -34,6 +35,8 @@
"chart.js": "2.9.4",
"crypto-pouch": "^4.0.1",
"fhirclient": "^2.5.1",
+ "fhirpath": "^3.3.0",
+ "fuse.js": "^6.6.2",
"garbados-crypt": "^3.0.0-beta",
"humanize-duration": "^3.27.3",
"idb": "^7.1.0",
diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts
index 6f9431f1..52fb9c66 100644
--- a/frontend/src/app/app.module.ts
+++ b/frontend/src/app/app.module.ts
@@ -29,6 +29,7 @@ import {AuthService} from './services/auth.service';
import { PatientProfileComponent } from './pages/patient-profile/patient-profile.component';
import { MedicalHistoryComponent } from './pages/medical-history/medical-history.component';
+
@NgModule({
declarations: [
AppComponent,
diff --git a/frontend/src/app/components/header/header.component.html b/frontend/src/app/components/header/header.component.html
index a0a588dd..fb57e02e 100644
--- a/frontend/src/app/components/header/header.component.html
+++ b/frontend/src/app/components/header/header.component.html
@@ -10,9 +10,13 @@
×
+
-
Dashboard
+ -
+ Medical History
+
-
Sources
diff --git a/frontend/src/app/components/report-header/report-header.component.html b/frontend/src/app/components/report-header/report-header.component.html
new file mode 100644
index 00000000..2a621e94
--- /dev/null
+++ b/frontend/src/app/components/report-header/report-header.component.html
@@ -0,0 +1,39 @@
+
+
+
{{reportHeaderTitle}}
+
{{reportHeaderSubTitle}}
+
+
+
+
+
diff --git a/frontend/src/app/components/report-header/report-header.component.scss b/frontend/src/app/components/report-header/report-header.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/frontend/src/app/components/report-header/report-header.component.spec.ts b/frontend/src/app/components/report-header/report-header.component.spec.ts
new file mode 100644
index 00000000..aff05512
--- /dev/null
+++ b/frontend/src/app/components/report-header/report-header.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ReportHeaderComponent } from './report-header.component';
+
+describe('ReportHeaderComponent', () => {
+ let component: ReportHeaderComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ ReportHeaderComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ReportHeaderComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/components/report-header/report-header.component.ts b/frontend/src/app/components/report-header/report-header.component.ts
new file mode 100644
index 00000000..2ea0398b
--- /dev/null
+++ b/frontend/src/app/components/report-header/report-header.component.ts
@@ -0,0 +1,41 @@
+import {Component, Input, OnInit} from '@angular/core';
+import {ResourceFhir} from '../../models/fasten/resource_fhir';
+import {FastenApiService} from '../../services/fasten-api.service';
+import * as fhirpath from 'fhirpath';
+
+@Component({
+ selector: 'report-header',
+ templateUrl: './report-header.component.html',
+ styleUrls: ['./report-header.component.scss']
+})
+export class ReportHeaderComponent implements OnInit {
+ patient: ResourceFhir = null
+ primaryCare: ResourceFhir = null
+ @Input() reportHeaderTitle: string = ""
+ @Input() reportHeaderSubTitle: string = "Organized by condition and encounters"
+
+ constructor(
+ private fastenApi: FastenApiService,
+ ) { }
+
+ ngOnInit(): void {
+ this.fastenApi.getResources("Patient").subscribe(results => {
+ console.log(results)
+ this.patient = results[0]
+
+ let primaryCareId = fhirpath.evaluate(this.patient.resource_raw, "Patient.generalPractitioner.reference.first()")
+ console.log("GP:", primaryCareId)
+ if(primaryCareId){
+ let primaryCareIdStr = primaryCareId.join("")
+ let primaryCareIdParts = primaryCareIdStr.split("/")
+ if(primaryCareIdParts.length == 2) {
+ console.log(primaryCareIdParts)
+ this.fastenApi.getResources(primaryCareIdParts[0], this.patient.source_id, primaryCareIdParts[1]).subscribe(primaryResults => {
+ this.primaryCare = primaryResults[0]
+ })
+ }
+ }
+ })
+ }
+
+}
diff --git a/frontend/src/app/components/report-medical-history-condition/report-medical-history-condition.component.html b/frontend/src/app/components/report-medical-history-condition/report-medical-history-condition.component.html
new file mode 100644
index 00000000..02845e81
--- /dev/null
+++ b/frontend/src/app/components/report-medical-history-condition/report-medical-history-condition.component.html
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{careTeamEntry.value | fhirPath: "CareTeam.participant.member.display"}}
+
+
+ {{careTeamEntry.value | fhirPath: "CareTeam.participant.role.text"}}
+
+
+
+
+
+ {{practitionerEntry.value | fhirPath: "Practitioner.name.family"}}, {{practitionerEntry.value | fhirPath: "Practitioner.name.given"}}
+
+
+ {{practitionerEntry.value | fhirPath: "Practitioner.name.prefix"}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{encounter | fhirPath: "Encounter.period.start" | date}}
+
+
+ {{encounter | fhirPath: "Encounter.location.first().location.display"}}
+
+
+
+
Medications:
+
+ -
+ {{medication | fhirPath: "MedicationRequest.medicationReference.display":"MedicationRequest.medicationCodeableConcept.text"}}
+
+
+
+
+
+
+
Procedures:
+
+ -
+ {{procedure | fhirPath: "Procedure.code.text"}}
+
+
+
+
+
+
+
Tests and Examinations:
+
+ -
+ {{diagnosticReport | fhirPath: "DiagnosticReport.code.text":"DiagnosticReport.code.coding.display"}}
+
+
+
+
+
+
+
Device:
+
+ -
+ {{device | fhirPath: "Device.code.text"}}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/components/report-medical-history-condition/report-medical-history-condition.component.scss b/frontend/src/app/components/report-medical-history-condition/report-medical-history-condition.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/frontend/src/app/components/report-medical-history-condition/report-medical-history-condition.component.spec.ts b/frontend/src/app/components/report-medical-history-condition/report-medical-history-condition.component.spec.ts
new file mode 100644
index 00000000..72de9112
--- /dev/null
+++ b/frontend/src/app/components/report-medical-history-condition/report-medical-history-condition.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ReportMedicalHistoryConditionComponent } from './report-medical-history-condition.component';
+
+describe('ReportMedicalHistoryConditionComponent', () => {
+ let component: ReportMedicalHistoryConditionComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ ReportMedicalHistoryConditionComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ReportMedicalHistoryConditionComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/components/report-medical-history-condition/report-medical-history-condition.component.ts b/frontend/src/app/components/report-medical-history-condition/report-medical-history-condition.component.ts
new file mode 100644
index 00000000..e36d4e3f
--- /dev/null
+++ b/frontend/src/app/components/report-medical-history-condition/report-medical-history-condition.component.ts
@@ -0,0 +1,52 @@
+import {Component, Input, OnInit} from '@angular/core';
+import {ResourceFhir} from '../../models/fasten/resource_fhir';
+
+@Component({
+ selector: 'app-report-medical-history-condition',
+ templateUrl: './report-medical-history-condition.component.html',
+ styleUrls: ['./report-medical-history-condition.component.scss']
+})
+export class ReportMedicalHistoryConditionComponent implements OnInit {
+
+
+ @Input() condition: ResourceFhir
+
+ careTeams: {[careTeamId: string]: ResourceFhir} = {}
+ practitioners: {[practitionerId: string]: ResourceFhir} = {}
+ encounters: {[encounterId: string]: ResourceFhir} = {}
+
+
+ constructor() { }
+
+ ngOnInit(): void {
+
+ for(let resource of this.condition.related_resources){
+ this.recExtractResources(resource)
+ }
+
+ // console.log("EXTRACTED CARETEAM", this.careTeams)
+ // console.log("EXTRACTED practitioners", this.practitioners)
+ // console.log("EXTRACTED encounters", this.encounters)
+ }
+
+ recExtractResources(resource: ResourceFhir){
+ if(resource.source_resource_type == "CareTeam"){
+ this.careTeams[this.genResourceId(resource)] = resource
+ } else if (resource.source_resource_type == "Practitioner"){
+ this.practitioners[this.genResourceId(resource)] = resource
+ } else if (resource.source_resource_type == "Encounter"){
+ this.encounters[this.genResourceId(resource)] = resource
+ }
+ if(!resource.related_resources){
+ return
+ }
+ for(let relatedResource of resource.related_resources){
+ this.recExtractResources(relatedResource)
+ }
+ }
+
+ genResourceId(relatedResource: ResourceFhir): string {
+ return `${relatedResource.source_id}/${relatedResource.source_resource_type}/${relatedResource.source_resource_id}`
+ }
+
+}
diff --git a/frontend/src/app/components/report-medical-history-editor/report-medical-history-editor.component.html b/frontend/src/app/components/report-medical-history-editor/report-medical-history-editor.component.html
new file mode 100644
index 00000000..7bf3082a
--- /dev/null
+++ b/frontend/src/app/components/report-medical-history-editor/report-medical-history-editor.component.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/app/components/report-medical-history-editor/report-medical-history-editor.component.scss b/frontend/src/app/components/report-medical-history-editor/report-medical-history-editor.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/frontend/src/app/components/report-medical-history-editor/report-medical-history-editor.component.spec.ts b/frontend/src/app/components/report-medical-history-editor/report-medical-history-editor.component.spec.ts
new file mode 100644
index 00000000..92ea50d9
--- /dev/null
+++ b/frontend/src/app/components/report-medical-history-editor/report-medical-history-editor.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ReportMedicalHistoryEditorComponent } from './report-medical-history-editor.component';
+
+describe('ReportEditorRelatedComponent', () => {
+ let component: ReportMedicalHistoryEditorComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ ReportMedicalHistoryEditorComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ReportMedicalHistoryEditorComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/components/report-medical-history-editor/report-medical-history-editor.component.ts b/frontend/src/app/components/report-medical-history-editor/report-medical-history-editor.component.ts
new file mode 100644
index 00000000..c5207beb
--- /dev/null
+++ b/frontend/src/app/components/report-medical-history-editor/report-medical-history-editor.component.ts
@@ -0,0 +1,169 @@
+import {Component, Input, OnInit} from '@angular/core';
+import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
+import {ResourceFhir} from '../../models/fasten/resource_fhir';
+import {FastenApiService} from '../../services/fasten-api.service';
+import * as fhirpath from 'fhirpath';
+
+class RelatedNode {
+ name: string
+ resourceType: string
+ resourceId: string
+ sourceId: string
+ draggable: boolean
+ children: RelatedNode[]
+}
+
+@Component({
+ selector: 'app-report-medical-history-editor',
+ templateUrl: './report-medical-history-editor.component.html',
+ styleUrls: ['./report-medical-history-editor.component.scss']
+})
+export class ReportMedicalHistoryEditorComponent implements OnInit {
+
+ @Input() conditions: ResourceFhir[] = []
+ @Input() encounters: ResourceFhir[] = []
+
+ assignedEncounters: {[name: string]: ResourceFhir} = {}
+
+ nodes = [
+ // {
+ // id: 1,
+ // name: 'root1',
+ // children: [
+ // { id: 2, name: 'child1' },
+ // { id: 3, name: 'child2' }
+ // ]
+ // },
+ // {
+ // id: 4,
+ // name: 'root2',
+ // children: [
+ // { id: 5, name: 'child2.1' },
+ // {
+ // id: 6,
+ // name: 'child2.2',
+ // children: [
+ // { id: 7, name: 'subsub' }
+ // ]
+ // }
+ // ]
+ // }
+ ];
+ options = {
+ allowDrag: (node) => {return node.data.draggable},
+ allowDrop: (element, { parent, index }) => {
+ // return true / false based on element, to.parent, to.index. e.g.
+ return parent.data.resourceType == "Condition";
+ },
+
+ }
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ private fastenApi: FastenApiService,
+ ) { }
+
+ ngOnInit(): void {
+ this.nodes = this.generateNodes(this.conditions)
+ }
+
+
+ onResourceMoved($event) {
+
+ this.fastenApi.replaceResourceAssociation({
+
+ source_id: $event.to.parent.sourceId,
+ source_resource_type: $event.to.parent.resourceType,
+ source_resource_id: $event.to.parent.resourceId,
+
+ new_related_source_id: $event.node.sourceId,
+ new_related_source_resource_type: $event.node.resourceType,
+ new_related_source_resource_id: $event.node.resourceId,
+
+
+ }).subscribe(results => {
+ console.log(results)
+ })
+ }
+
+
+
+ generateNodes(resouceFhirList: ResourceFhir[]): RelatedNode[] {
+ let relatedNodes = resouceFhirList.map((resourceFhir) => { return this.recGenerateNode(resourceFhir) })
+
+ //create an unassigned encounters "condition"
+ if(this.encounters.length > 0){
+ let unassignedCondition = {
+ name: "[Unassigned Encounters]",
+ resourceType: "Condition",
+ resourceId: "UNASSIGNED",
+ sourceId: "UNASSIGNED",
+ draggable: false,
+ children: []
+ }
+
+ for(let encounter of this.encounters){
+ let encounterId = `${encounter.source_id}/${encounter.source_resource_type}/${encounter.source_resource_id}`
+ if(!this.assignedEncounters[encounterId]){
+ this.assignedEncounters[encounterId] = encounter
+ unassignedCondition.children.push(this.recGenerateNode(encounter))
+ }
+ }
+
+ if(unassignedCondition.children.length > 0){
+ //only add the unassigned condition block if the subchildren list is populated.
+ relatedNodes.push(unassignedCondition)
+ }
+ }
+
+ console.log("NODES:", relatedNodes)
+ return relatedNodes
+ }
+
+ recGenerateNode(resourceFhir: ResourceFhir): RelatedNode {
+ let relatedNode = {
+ sourceId: resourceFhir.source_id,
+ name: `[${resourceFhir.source_resource_type}/${resourceFhir.source_resource_id}]`,
+ resourceId: resourceFhir.source_resource_id,
+ resourceType: resourceFhir.source_resource_type,
+ draggable: resourceFhir.source_resource_type == "Encounter" || resourceFhir.source_resource_type == "Condition",
+ children: [],
+ }
+
+ switch (resourceFhir.source_resource_type) {
+ case "Condition":
+ relatedNode.name += ` ${fhirpath.evaluate(resourceFhir.resource_raw, "Condition.onsetPeriod.start")} ${fhirpath.evaluate(resourceFhir.resource_raw, "Condition.code.text.first()")}`
+ break
+ case "Encounter":
+ relatedNode.name += ` ${fhirpath.evaluate(resourceFhir.resource_raw, "Encounter.period.start")} ${fhirpath.evaluate(resourceFhir.resource_raw, "Encounter.location.first().location.display")}`
+ break
+ case "CareTeam":
+ relatedNode.name += ` ${fhirpath.evaluate(resourceFhir.resource_raw, "CareTeam.participant.member.display")}`
+ break
+ case "Location":
+ relatedNode.name += ` ${fhirpath.evaluate(resourceFhir.resource_raw, "Location.name")}`
+ break
+ case "Organization":
+ relatedNode.name += ` ${fhirpath.evaluate(resourceFhir.resource_raw, "Organization.name")}`
+ break
+ case "Practitioner":
+ relatedNode.name += ` ${fhirpath.evaluate(resourceFhir.resource_raw, "Practitioner.name.family")}`
+ break
+ case "MedicationRequest":
+ relatedNode.name += ` ${fhirpath.evaluate(resourceFhir.resource_raw, "MedicationRequest.medicationReference.display")}`
+ break
+ }
+
+ this.assignedEncounters[`${resourceFhir.source_id}/${resourceFhir.source_resource_type}/${resourceFhir.source_resource_id}`] = resourceFhir
+
+ if(!resourceFhir.related_resources){
+ return relatedNode
+ } else {
+ relatedNode.children = resourceFhir.related_resources.map((relatedResourceFhir)=>{
+ return this.recGenerateNode(relatedResourceFhir)
+ })
+ return relatedNode
+ }
+ }
+
+}
diff --git a/frontend/src/app/components/shared.module.ts b/frontend/src/app/components/shared.module.ts
index 5ad5d505..86eb7d21 100644
--- a/frontend/src/app/components/shared.module.ts
+++ b/frontend/src/app/components/shared.module.ts
@@ -34,6 +34,12 @@ import { ListFallbackResourceComponent } from './list-fallback-resource/list-fal
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
import { ToastComponent } from './toast/toast.component';
import { MomentModule } from 'ngx-moment';
+import { ReportHeaderComponent } from './report-header/report-header.component';
+import { FhirPathPipe } from '../pipes/fhir-path.pipe';
+import { ReportMedicalHistoryEditorComponent } from './report-medical-history-editor/report-medical-history-editor.component';
+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';
@NgModule({
imports: [
@@ -42,6 +48,7 @@ import { MomentModule } from 'ngx-moment';
NgxDatatableModule,
NgbModule,
MomentModule,
+ TreeModule,
],
declarations: [
ComponentsSidebarComponent,
@@ -74,38 +81,49 @@ import { MomentModule } from 'ngx-moment';
ResourceListOutletDirective,
ListFallbackResourceComponent,
ToastComponent,
+ ReportHeaderComponent,
+ FhirPathPipe,
+ ReportMedicalHistoryEditorComponent,
+ FilterPipe,
+ ReportMedicalHistoryConditionComponent,
],
- exports: [
- ComponentsSidebarComponent,
- ListAllergyIntoleranceComponent,
- ListAdverseEventComponent,
- ListCarePlanComponent,
- ListCommunicationComponent,
- ListConditionComponent,
- ListEncounterComponent,
- ListAppointmentComponent,
- ListGenericResourceComponent,
- ListImmunizationComponent,
- ListMedicationAdministrationComponent,
- ListMedicationComponent,
- ListMedicationDispenseComponent,
- ListMedicationRequestComponent,
- ListNutritionOrderComponent,
- ListObservationComponent,
- ListPatientComponent,
- ListProcedureComponent,
- ListDeviceRequestComponent,
- UtilitiesSidebarComponent,
- ListCoverageComponent,
- ListServiceRequestComponent,
- ListDocumentReferenceComponent,
- ListDeviceComponent,
- ListDiagnosticReportComponent,
- ListGoalComponent,
- ResourceListComponent,
- ResourceListOutletDirective,
- ToastComponent,
- ]
+ exports: [
+ ComponentsSidebarComponent,
+ ListAllergyIntoleranceComponent,
+ ListAdverseEventComponent,
+ ListCarePlanComponent,
+ ListCommunicationComponent,
+ ListConditionComponent,
+ ListEncounterComponent,
+ ListAppointmentComponent,
+ ListGenericResourceComponent,
+ ListImmunizationComponent,
+ ListMedicationAdministrationComponent,
+ ListMedicationComponent,
+ ListMedicationDispenseComponent,
+ ListMedicationRequestComponent,
+ ListNutritionOrderComponent,
+ ListObservationComponent,
+ ListPatientComponent,
+ ListProcedureComponent,
+ ListDeviceRequestComponent,
+ UtilitiesSidebarComponent,
+ ListCoverageComponent,
+ ListServiceRequestComponent,
+ ListDocumentReferenceComponent,
+ ListDeviceComponent,
+ ListDiagnosticReportComponent,
+ ListGoalComponent,
+ ResourceListComponent,
+ ResourceListOutletDirective,
+ ToastComponent,
+ ReportHeaderComponent,
+ ReportMedicalHistoryEditorComponent,
+ FhirPathPipe,
+ FilterPipe,
+ ReportMedicalHistoryConditionComponent
+
+ ]
})
export class SharedModule { }
diff --git a/frontend/src/app/models/fasten/resource_association.ts b/frontend/src/app/models/fasten/resource_association.ts
new file mode 100644
index 00000000..ec7953e5
--- /dev/null
+++ b/frontend/src/app/models/fasten/resource_association.ts
@@ -0,0 +1,15 @@
+
+export class ResourceAssociation {
+ source_id: string
+ source_resource_type: string
+ source_resource_id: string
+
+ old_related_source_id?: string
+ old_related_source_resource_type?: string
+ old_related_source_resource_id?: string
+
+
+ new_related_source_id: string
+ new_related_source_resource_type: string
+ new_related_source_resource_id: string
+}
diff --git a/frontend/src/app/models/fasten/resource_fhir.ts b/frontend/src/app/models/fasten/resource_fhir.ts
index 619b4586..afeb92a2 100644
--- a/frontend/src/app/models/fasten/resource_fhir.ts
+++ b/frontend/src/app/models/fasten/resource_fhir.ts
@@ -6,6 +6,7 @@ export class ResourceFhir {
fhir_version: string = ""
resource_raw: IResourceRaw
+ related_resources?: ResourceFhir[] = []
constructor(object?: any) {
return Object.assign(this, object)
diff --git a/frontend/src/app/models/queue/source-sync-message.ts b/frontend/src/app/models/queue/source-sync-message.ts
deleted file mode 100644
index 9edcad3c..00000000
--- a/frontend/src/app/models/queue/source-sync-message.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import {Source} from '../../models/fasten/source';
-
-export class SourceSyncMessage {
- source: Source
- current_user: string
- auth_token: string
- couchdb_endpoint_base: string
- fasten_api_endpoint_base: string
- response?: any
-}
diff --git a/frontend/src/app/pages/medical-history/medical-history.component.html b/frontend/src/app/pages/medical-history/medical-history.component.html
index 339f267d..97a6f8bb 100644
--- a/frontend/src/app/pages/medical-history/medical-history.component.html
+++ b/frontend/src/app/pages/medical-history/medical-history.component.html
@@ -3,122 +3,31 @@
-
-
-
Medical History
-
-
-
- Patient: Caldwell, Ruben
- Address: 123 B Street
Gainsville, FL, 94153
- Date of Birth: June 20, 1929
- Phone: 415-343-2342
- Email: myemail@gmail.com
-
-
-
-
- Primary Care: Bishop, J. ANRP
- Address: Malcom Randall VA
Medical Center Gainsville FL
- Phone: 123-321-5532
- Email: myemail@va.com
-
+
+
+
+
+
+
+
+
+
Warning! Fasten has detected medical Encounters that are not associated with a Condition.
+ They are grouped under the "Unassigned" section below.
+
+ You can re-organize your conditions & encounters by using the
report editor
+
-
Condition
-
-
-
History
+ Condition
-
-
-
-
-
Gout
-
-
-
Nov 16, 2002 - Present
-
-
-
-
-
-
-
-
-
-
-
-
Involved in Care
-
-
-
- James Bishop, ANRP
-
-
- Primary Care
-
-
-
- Matthew Leonard, MD
-
-
- Diagnosing Physician
-
-
-
- Stephanie Wrenn, MD
-
-
- Dietitian
-
-
-
-
-
Initial Presentation
-
-
- Acute right knee pain and tenderness around the joint line - this was likely caused by acute renal failure.
-
-
-
-
-
-
-
-
-
-
Nov 19, 2012
-
-
- Medications: Colchicine, as needed for gout attacks
-
-
-
-
-
Nov 16, 2012
-
-
- Procedures: The fluid in your right knee was drained.
-
-
- Tests and Examinations: The fluid tested prositive for gout crystals
-
-
- Medications: You were given a steroid injection to reduce inflammation and as short course prednistone to reduce pain and inflammation
-
-
-
-
-
-
+
diff --git a/frontend/src/app/pages/medical-history/medical-history.component.ts b/frontend/src/app/pages/medical-history/medical-history.component.ts
index cc54add2..92976957 100644
--- a/frontend/src/app/pages/medical-history/medical-history.component.ts
+++ b/frontend/src/app/pages/medical-history/medical-history.component.ts
@@ -1,4 +1,10 @@
-import { Component, OnInit } from '@angular/core';
+import {Component, Input, OnInit} from '@angular/core';
+import {FastenApiService} from '../../services/fasten-api.service';
+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 {ReportEditorRelatedComponent} from '../../components/report-editor-related/report-editor-related.component';
@Component({
selector: 'app-medical-history',
@@ -6,10 +12,96 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./medical-history.component.scss']
})
export class MedicalHistoryComponent implements OnInit {
+ closeResult = '';
+ conditions: ResourceFhir[] = []
+
+ unassigned_encounters: ResourceFhir[] = []
+ resourceLookup: {[name: string]: ResourceFhir} = {}
+
+ constructor(
+ private fastenApi: FastenApiService,
+ private modalService: NgbModal
+ ) { }
- constructor() { }
ngOnInit(): void {
+ forkJoin([
+ this.fastenApi.getResources("Condition", null, null, true),
+ this.fastenApi.getResources("Encounter", null, null, true)
+ ]).subscribe(results => {
+ this.conditions = results[0]
+ //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)
+ console.log("Found unassigned encounters:", this.unassigned_encounters.length, this.unassigned_encounters)
+ this.conditions.push({
+ fhir_version: '',
+ resource_raw: {
+ resourceType: "Condition",
+ code:{
+ text: "UNASSIGNED",
+ }
+ },
+ source_id: 'UNASSIGNED',
+ source_resource_id: 'UNASSIGNED',
+ source_resource_type: 'UNASSIGNED',
+ related_resources: this.unassigned_encounters
+ } as any)
+ }
+
+
+ })
+
}
+ openEditorRelated(): void {
+ const modalRef = this.modalService.open(ReportMedicalHistoryEditorComponent);
+ modalRef.componentInstance.conditions = this.conditions;
+ modalRef.componentInstance.encounters = this.unassigned_encounters;
+ }
+
+
+ recPopulateResourceLookup(resourceFhir: ResourceFhir) {
+ if(!resourceFhir){
+ return
+ }
+ this.resourceLookup[`${resourceFhir.source_id}/${resourceFhir.source_resource_type}/${resourceFhir.source_resource_id}`] = resourceFhir
+
+ if(!resourceFhir.related_resources){
+ return
+ } else {
+
+ for(let relatedResourceFhir of resourceFhir.related_resources){
+ this.recPopulateResourceLookup(relatedResourceFhir)
+ }
+
+ return
+ }
+ }
+
+
+
+ // private getDismissReason(reason: any): string {
+ // if (reason === ModalDismissReasons.ESC) {
+ // return 'by pressing ESC';
+ // } else if (reason === ModalDismissReasons.BACKDROP_CLICK) {
+ // return 'by clicking on a backdrop';
+ // } else {
+ // return `with: ${reason}`;
+ // }
+ // }
}
diff --git a/frontend/src/app/pages/medical-sources/medical-sources.component.ts b/frontend/src/app/pages/medical-sources/medical-sources.component.ts
index 55957a1d..ad221903 100644
--- a/frontend/src/app/pages/medical-sources/medical-sources.component.ts
+++ b/frontend/src/app/pages/medical-sources/medical-sources.component.ts
@@ -11,7 +11,6 @@ import {ActivatedRoute, Router} from '@angular/router';
import {Location} from '@angular/common';
import {ToastService} from '../../services/toast.service';
import {ToastNotification, ToastType} from '../../models/fasten/toast';
-import {SourceSyncMessage} from '../../models/queue/source-sync-message';
import {environment} from '../../../environments/environment';
// If you dont import this angular will import the wrong "Location"
diff --git a/frontend/src/app/pages/patient-profile/patient-profile.component.html b/frontend/src/app/pages/patient-profile/patient-profile.component.html
index d06092fc..12921994 100644
--- a/frontend/src/app/pages/patient-profile/patient-profile.component.html
+++ b/frontend/src/app/pages/patient-profile/patient-profile.component.html
@@ -3,34 +3,13 @@
-
-
-
Patient Profile
-
-
-
- Patient: Caldwell, Ruben
- Address: 123 B Street
Gainsville, FL, 94153
- Date of Birth: June 20, 1929
- Phone: 415-343-2342
- Email: myemail@gmail.com
-
-
-
-
- Primary Care: Bishop, J. ANRP
- Address: Malcom Randall VA
Medical Center Gainsville FL
- Phone: 123-321-5532
- Email: myemail@va.com
-
-
-
+
-
Caldwell, Ruben
+ {{patient | fhirPath: "Patient.name.family.first()"}}, {{patient | fhirPath: "Patient.name.given.first()"}}
@@ -44,28 +23,28 @@
First Name:
- Ruben
+ {{patient | fhirPath: "Patient.name.given.first()"}}
Last Name:
- Caldwell
+ {{patient | fhirPath: "Patient.name.family.first()"}}
Gender:
- M
+ {{patient | fhirPath: "Patient.gender" | titlecase}}
Martial Status:
- Married
+ {{patient | fhirPath: "Patient.maritalStatus.text" | titlecase}}
@@ -86,35 +65,35 @@
Language:
- English
+ {{patient | fhirPath: "Patient.communication.language.text" | titlecase}}
Address:
- 123 B Street
Gainsville, FL, 94153
+ {{patient | fhirPath: "Patient.address.line.first()"}}
{{patient | fhirPath: "Patient.address.city.first()"}}, {{patient | fhirPath: "Patient.address.state.first()"}}, {{patient | fhirPath: "Patient.address.postalCode.first()"}}
Date of Birth:
- June 20, 1929
+ {{patient | fhirPath: "Patient.birthDate" | date }}
Phone:
- 415-343-2342
+ {{patient | fhirPath: "Patient.telecom.where(system='phone').value.first()"}}
Email:
- myemail@gmail.com
+ {{patient | fhirPath: "Patient.telecom.where(system='email').value.first()"}}
@@ -163,14 +142,12 @@
Immunizations
- Phenumovax
- 07/09/2022
-
- Hepatitis B Series [complete]
- 01/12/2005
-
- Tetanus Toxoid [complete]
- 02/02/2010
+
+ {{immunization | fhirPath: "Immunization.vaccineCode.text" }}
+ {{immunization | fhirPath: "Immunization.occurrenceDateTime" | date }}
+ {{immunization | fhirPath: "Immunization.location.display" }}
+
+
@@ -178,6 +155,12 @@
Allergies
+
+ {{allergy | fhirPath: "AllergyIntolerance.code.text" }}
+ {{allergy | fhirPath: "AllergyIntolerance.occurrenceDateTime" | date }}
+
+
+
Latex
(AV/Historical) with rash and agitation
diff --git a/frontend/src/app/pages/patient-profile/patient-profile.component.ts b/frontend/src/app/pages/patient-profile/patient-profile.component.ts
index 16e7c269..203ad9f5 100644
--- a/frontend/src/app/pages/patient-profile/patient-profile.component.ts
+++ b/frontend/src/app/pages/patient-profile/patient-profile.component.ts
@@ -1,4 +1,6 @@
import { Component, OnInit } from '@angular/core';
+import {ResourceFhir} from '../../models/fasten/resource_fhir';
+import {FastenApiService} from '../../services/fasten-api.service';
@Component({
selector: 'app-patient-profile',
@@ -7,9 +9,28 @@ import { Component, OnInit } from '@angular/core';
})
export class PatientProfileComponent implements OnInit {
- constructor() { }
+ patient: ResourceFhir = null
+ immunizations: ResourceFhir[] = []
+ allergyIntolerances: ResourceFhir[] = []
+ constructor(
+ private fastenApi: FastenApiService,
+ ) { }
ngOnInit(): void {
+ this.fastenApi.getResources("Patient").subscribe(results => {
+ console.log(results)
+ this.patient = results[0]
+ })
+
+ this.fastenApi.getResources("Immunization").subscribe(results => {
+ console.log(results)
+ this.immunizations = results
+ })
+
+ this.fastenApi.getResources("AllergyIntolerance").subscribe(results => {
+ console.log(results)
+ this.allergyIntolerances = results
+ })
}
}
diff --git a/frontend/src/app/pipes/fhir-path.pipe.spec.ts b/frontend/src/app/pipes/fhir-path.pipe.spec.ts
new file mode 100644
index 00000000..6c50d3ec
--- /dev/null
+++ b/frontend/src/app/pipes/fhir-path.pipe.spec.ts
@@ -0,0 +1,8 @@
+import { FhirPathPipe } from './fhir-path.pipe';
+
+describe('FhirPathPipe', () => {
+ it('create an instance', () => {
+ const pipe = new FhirPathPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/pipes/fhir-path.pipe.ts b/frontend/src/app/pipes/fhir-path.pipe.ts
new file mode 100644
index 00000000..54e914cd
--- /dev/null
+++ b/frontend/src/app/pipes/fhir-path.pipe.ts
@@ -0,0 +1,22 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import {ResourceFhir} from '../models/fasten/resource_fhir';
+import { evaluate } from 'fhirpath'
+
+@Pipe({
+ name: 'fhirPath'
+})
+export class FhirPathPipe implements PipeTransform {
+
+ transform(resourceFhir: ResourceFhir, ...pathQueryWithFallback: string[]): string {
+
+ for(let pathQuery of pathQueryWithFallback){
+ let result = evaluate(resourceFhir.resource_raw, pathQuery).join(", ")
+ if(result){
+ return result
+ }
+ }
+
+ return null
+ }
+
+}
diff --git a/frontend/src/app/pipes/filter.pipe.spec.ts b/frontend/src/app/pipes/filter.pipe.spec.ts
new file mode 100644
index 00000000..1427de36
--- /dev/null
+++ b/frontend/src/app/pipes/filter.pipe.spec.ts
@@ -0,0 +1,8 @@
+import { FilterPipe } from './filter.pipe';
+
+describe('FilterPipe', () => {
+ it('create an instance', () => {
+ const pipe = new FilterPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/pipes/filter.pipe.ts b/frontend/src/app/pipes/filter.pipe.ts
new file mode 100644
index 00000000..dc3da4ce
--- /dev/null
+++ b/frontend/src/app/pipes/filter.pipe.ts
@@ -0,0 +1,34 @@
+// import { Pipe, PipeTransform } from '@angular/core';
+//
+// @Pipe({
+// name: 'filter',
+// })
+// export class FilterPipe implements PipeTransform {
+// transform(items: any[], filter: Record): any {
+// if (!items || !filter) {
+// return items;
+// }
+//
+// const key = Object.keys(filter)[0];
+// const value = filter[key];
+//
+// return items.filter((e) => e[key].indexOf(value) !== -1);
+// }
+// }
+
+
+import {Pipe, PipeTransform} from '@angular/core';
+
+@Pipe({
+ name: 'filter'
+})
+export class FilterPipe implements PipeTransform {
+ transform(items: any[], field : string, value : string): any[] {
+ if (!items) return null;
+ if (!value || value.length == 0) return items;
+ let filtered = items.filter(it =>
+ it[field].toLowerCase().indexOf(value.toLowerCase()) !=-1);
+
+ return filtered.length > 0 ? filtered : null;
+ }
+}
diff --git a/frontend/src/app/services/fasten-api.service.ts b/frontend/src/app/services/fasten-api.service.ts
index b6f6186b..d604fbe6 100644
--- a/frontend/src/app/services/fasten-api.service.ts
+++ b/frontend/src/app/services/fasten-api.service.ts
@@ -13,6 +13,7 @@ import {MetadataSource} from '../models/fasten/metadata-source';
import {AuthService} from './auth.service';
import {GetEndpointAbsolutePath} from '../../lib/utils/endpoint_absolute_path';
import {environment} from '../../environments/environment';
+import {ResourceAssociation} from '../models/fasten/resource_association';
@Injectable({
providedIn: 'root'
@@ -100,7 +101,7 @@ export class FastenApiService {
);
}
- getResources(sourceResourceType?: string, sourceID?: string): Observable {
+ getResources(sourceResourceType?: string, sourceID?: string, sourceResourceID?: string, preloadRelated?: boolean): Observable {
let queryParams = {}
if(sourceResourceType){
queryParams["sourceResourceType"] = sourceResourceType
@@ -109,6 +110,13 @@ export class FastenApiService {
queryParams["sourceID"] = sourceID
}
+ if(sourceResourceID){
+ queryParams["sourceResourceID"] = sourceResourceID
+ }
+ if(preloadRelated){
+ queryParams["preloadRelated"] = "true"
+ }
+
return this._httpClient.get(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/resource/fhir`, {params: queryParams})
.pipe(
map((response: ResponseWrapper) => {
@@ -128,4 +136,14 @@ export class FastenApiService {
})
);
}
+
+ replaceResourceAssociation(resourceAssociation: ResourceAssociation): Observable {
+ return this._httpClient.post(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/resource/association`, resourceAssociation)
+ .pipe(
+ map((response: ResponseWrapper) => {
+ console.log("RESPONSE", response)
+ return response.data
+ })
+ );
+ }
}
diff --git a/frontend/src/assets/scss/template/_calendar.scss b/frontend/src/assets/scss/template/_calendar.scss
index d66f93f4..6b63f715 100755
--- a/frontend/src/assets/scss/template/_calendar.scss
+++ b/frontend/src/assets/scss/template/_calendar.scss
@@ -129,7 +129,7 @@ ng-fullcalendar {
@include media-breakpoint-up(sm) { font-size: 22px; }
}
- .fc-left {
+ .fc-left {
display: flex;
}
.fc-right { order: 3; }
@@ -449,7 +449,7 @@ ng-fullcalendar {
.fc-list-item {
flex: 0 0 calc(80% - 5px);
max-width: calc(80% - 5px);
- dispLay: flex;
+ display: flex;
flex-direction: column;
border-left: 4px solid transparent;
background-color: #fff;
diff --git a/frontend/src/assets/scss/util/_typography.scss b/frontend/src/assets/scss/util/_typography.scss
index 97c3994b..8e806caa 100755
--- a/frontend/src/assets/scss/util/_typography.scss
+++ b/frontend/src/assets/scss/util/_typography.scss
@@ -56,7 +56,7 @@
.tx-spacing-4 { letter-spacing: 2px; }
.tx-spacing-5 { letter-spacing: 2.5px; }
.tx-spacing-6 { letter-spacing: 3px; }
-.tx-spacing-7 { letter-spacign: 3.5px; }
+.tx-spacing-7 { letter-spacing: 3.5px; }
.tx-spacing-8 { letter-spacing: 4px; }
.tx-spacing--1 { letter-spacing: -0.5px; }
diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss
index e828131e..7e8b9cad 100644
--- a/frontend/src/styles.scss
+++ b/frontend/src/styles.scss
@@ -246,5 +246,5 @@
@import '~@swimlane/ngx-datatable/index.css';
@import '~@swimlane/ngx-datatable/themes/bootstrap.css';
@import '~@swimlane/ngx-datatable/assets/icons.css';
-
+@import '~@circlon/angular-tree-component/css/angular-tree-component.css';
@import '~highlight.js/styles/github.css';
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 63028a83..ac608505 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1222,6 +1222,14 @@
"@babel/helper-validator-identifier" "^7.18.6"
to-fast-properties "^2.0.0"
+"@circlon/angular-tree-component@^11.0.4":
+ version "11.0.4"
+ resolved "https://registry.yarnpkg.com/@circlon/angular-tree-component/-/angular-tree-component-11.0.4.tgz#7fb5ca294331d45d4d2a01cb7afc7197a45aa905"
+ integrity sha512-Ck86mG6Z9eWG03RiOACDzrCjuzEDXU8rcEDi0aw0+Ku62x6ZY2mx8G0VX3CLEkS1BAXM2ef6luOIcoSKAKtDaA==
+ dependencies:
+ mobx "~4.14.1"
+ tslib "^2.0.0"
+
"@colors/colors@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
@@ -1450,6 +1458,22 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
+"@lhncbc/ucum-lhc@^4.1.3":
+ version "4.1.6"
+ resolved "https://registry.yarnpkg.com/@lhncbc/ucum-lhc/-/ucum-lhc-4.1.6.tgz#7ca11266c3b79b8ef8c41f8c7cd9989a6e020e1f"
+ integrity sha512-5VWDRwPZAcytx19Mh2aVbqrMSoclWPRIn65niGas5OgyIWD8nB5UbpgLQPvmV6+Z7l+a2L4E+7QIqfiimTYvfg==
+ dependencies:
+ coffeescript "^2.7.0"
+ csv-parse "^4.4.6"
+ csv-stringify "^1.0.4"
+ escape-html "^1.0.3"
+ is-integer "^1.0.6"
+ jsonfile "^2.2.3"
+ stream "0.0.2"
+ stream-transform "^0.1.1"
+ string-to-stream "^1.1.0"
+ xmldoc "^0.4.0"
+
"@ng-bootstrap/ng-bootstrap@10.0.0":
version "10.0.0"
resolved "https://registry.yarnpkg.com/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-10.0.0.tgz#6022927bac7029bdd12d7f1e10b5b20074db06dc"
@@ -2317,6 +2341,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
dependencies:
color-convert "^2.0.1"
+antlr4@~4.9.3:
+ version "4.9.3"
+ resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.9.3.tgz#268b844ff8ce97d022399a05d4b37aa6ab4047b2"
+ integrity sha512-qNy2odgsa0skmNMCuxzXhM4M8J1YDaPv3TI+vCdnOAanu0N982wBrSqziDKRDctEZLZy9VffqIZXc0UGjjSP/g==
+
anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
@@ -2968,6 +2997,11 @@ codelyzer@^5.1.2:
source-map "^0.5.7"
sprintf-js "^1.1.2"
+coffeescript@^2.7.0:
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/coffeescript/-/coffeescript-2.7.0.tgz#a43ec03be6885d6d1454850ea70b9409c391279c"
+ integrity sha512-hzWp6TUE2d/jCcN67LrW1eh5b/rSDKQK6oD6VMLlggYVUUFexgTH9z3dNYihzX4RMhze5FTUsUmOXViJKFQR/A==
+
color-convert@^1.9.0, color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -3014,7 +3048,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.5, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
-commander@^2.11.0, commander@^2.12.1, commander@^2.20.0:
+commander@^2.11.0, commander@^2.12.1, commander@^2.18.0, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -3290,6 +3324,18 @@ cssesc@^3.0.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+csv-parse@^4.4.6:
+ version "4.16.3"
+ resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-4.16.3.tgz#7ca624d517212ebc520a36873c3478fa66efbaf7"
+ integrity sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==
+
+csv-stringify@^1.0.4:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-1.1.2.tgz#77a41526581bce3380f12b00d7c5bbac70c82b58"
+ integrity sha512-3NmNhhd+AkYs5YtM1GEh01VR6PKj6qch2ayfQaltx5xpcAdThjnbbI5eT8CzRVpXfGKAxnmrSYLsNl/4f3eWiw==
+ dependencies:
+ lodash.get "~4.4.2"
+
custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
@@ -3307,6 +3353,11 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"
+date-fns@^1.30.1:
+ version "1.30.1"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
+ integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
+
date-format@^4.0.13:
version "4.0.13"
resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.13.tgz#87c3aab3a4f6f37582c5f5f63692d2956fa67890"
@@ -3513,6 +3564,11 @@ electron-to-chromium@^1.4.251:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.256.tgz#c735032f412505e8e0482f147a8ff10cfca45bf4"
integrity sha512-x+JnqyluoJv8I0U9gVe+Sk2st8vF0CzMt78SXxuoWCooLLY2k5VerIBdpvG7ql6GKI4dzNnPjmqgDJ76EdaAKw==
+emitter-component@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/emitter-component/-/emitter-component-1.1.1.tgz#065e2dbed6959bf470679edabeaf7981d1003ab6"
+ integrity sha512-G+mpdiAySMuB7kesVRLuyvYRqDmshB7ReKEVuyBPkzQlmiDiLrt7hHHIy4Aff552bgknVN7B2/d3lzhGO5dvpQ==
+
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@@ -3769,7 +3825,7 @@ escalade@^3.1.1:
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
-escape-html@~1.0.3:
+escape-html@^1.0.3, escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
@@ -4003,6 +4059,17 @@ fhirclient@^2.5.1:
jose "^4.6.0"
js-base64 "^3.7.2"
+fhirpath@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/fhirpath/-/fhirpath-3.3.0.tgz#3fc68592b297985185665e5321b7faff5ffb662a"
+ integrity sha512-Fl+DUSoQIRg/gh4EKsD4Nd2Y7NrY9AoIbAtDWwe+pOjlWNmwl5lX6sx8DB8j0/9UaGrg72qxBU+c5eAbmC8u1w==
+ dependencies:
+ "@lhncbc/ucum-lhc" "^4.1.3"
+ antlr4 "~4.9.3"
+ commander "^2.18.0"
+ date-fns "^1.30.1"
+ js-yaml "^3.13.1"
+
figures@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
@@ -4153,6 +4220,11 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+fuse.js@^6.6.2:
+ version "6.6.2"
+ resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.6.2.tgz#fe463fed4b98c0226ac3da2856a415576dc9a111"
+ integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==
+
garbados-crypt@^3.0.0-beta:
version "3.0.0-beta"
resolved "https://registry.yarnpkg.com/garbados-crypt/-/garbados-crypt-3.0.0-beta.tgz#9b70b62c31b9340be5c5a55c19dc3c14300af12c"
@@ -4782,6 +4854,11 @@ is-finite-x@^3.0.2:
infinity-x "^1.0.1"
is-nan-x "^1.0.2"
+is-finite@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3"
+ integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==
+
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
@@ -4819,6 +4896,13 @@ is-index-x@^1.0.0:
to-number-x "^2.0.0"
to-string-symbols-supported-x "^1.0.0"
+is-integer@^1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/is-integer/-/is-integer-1.0.7.tgz#6bde81aacddf78b659b6629d629cadc51a886d5c"
+ integrity sha512-RPQc/s9yBHSvpi+hs9dYiJ2cuFeU6x3TyyIp8O2H6SKEltIvJOzRj9ToyvcStDvPR/pS4rxgr1oBFajQjZ2Szg==
+ dependencies:
+ is-finite "^1.0.0"
+
is-interactive@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e"
@@ -5182,6 +5266,13 @@ jsonc-parser@3.1.0:
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.1.0.tgz#73b8f0e5c940b83d03476bc2e51a20ef0932615d"
integrity sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==
+jsonfile@^2.2.3:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
+ integrity sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
@@ -5494,6 +5585,11 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
+lodash.get@~4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+ integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
+
lodash.isnull@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash.isnull/-/lodash.isnull-3.0.0.tgz#fafbe59ea1dca27eed786534039dd84c2e07c56e"
@@ -5795,6 +5891,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+mobx@~4.14.1:
+ version "4.14.1"
+ resolved "https://registry.yarnpkg.com/mobx/-/mobx-4.14.1.tgz#0dc5622523363f6f5b467a5a0c4c99846b789748"
+ integrity sha512-Oyg7Sr7r78b+QPYLufJyUmxTWcqeQ96S1nmtyur3QL8SeI6e0TqcKKcxbG+sVJLWANhHQkBW/mDmgG5DDC4fdw==
+
moment@^2.10.2, moment@^2.29.4:
version "2.29.4"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
@@ -7184,7 +7285,7 @@ readable-stream@1.1.14:
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
-readable-stream@^2.0.1, readable-stream@~2.3.6:
+readable-stream@^2.0.1, readable-stream@^2.1.0, readable-stream@~2.3.6:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@@ -7513,6 +7614,11 @@ sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+sax@~1.1.1:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.6.tgz#5d616be8a5e607d54e114afae55b7eaf2fcc3240"
+ integrity sha512-8zci48uUQyfqynGDSkUMD7FCJB96hwLnlZOXlgs1l3TX+LW27t3psSWKUxC0fxVgA86i8tL4NwGcY1h/6t3ESg==
+
schema-utils@^2.6.5:
version "2.7.1"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
@@ -7907,6 +8013,18 @@ str2buf@^1.3.0:
resolved "https://registry.yarnpkg.com/str2buf/-/str2buf-1.3.0.tgz#a4172afff4310e67235178e738a2dbb573abead0"
integrity sha512-xIBmHIUHYZDP4HyoXGHYNVmxlXLXDrtFHYT0eV6IOdEj3VO9ccaF1Ejl9Oq8iFjITllpT8FhaXb4KsNmw+3EuA==
+stream-transform@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/stream-transform/-/stream-transform-0.1.2.tgz#7d8e6b4e03ac4781778f8c79517501bfb0762a9f"
+ integrity sha512-3HXId/0W8sktQnQM6rOZf2LuDDMbakMgAjpViLk758/h0br+iGqZFFfUxxJSqEvGvT742PyFr4v/TBXUtowdCg==
+
+stream@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/stream/-/stream-0.0.2.tgz#7f5363f057f6592c5595f00bc80a27f5cec1f0ef"
+ integrity sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g==
+ dependencies:
+ emitter-component "^1.1.1"
+
streamroller@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.2.tgz#abd444560768b340f696307cf84d3f46e86c0e63"
@@ -7916,6 +8034,14 @@ streamroller@^3.1.2:
debug "^4.3.4"
fs-extra "^8.1.0"
+string-to-stream@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/string-to-stream/-/string-to-stream-1.1.1.tgz#aba78f73e70661b130ee3e1c0192be4fef6cb599"
+ integrity sha512-QySF2+3Rwq0SdO3s7BAp4x+c3qsClpPQ6abAmb0DGViiSBAkT5kL6JT2iyzEVP+T1SmzHrQD1TwlP9QAHCc+Sw==
+ dependencies:
+ inherits "^2.0.1"
+ readable-stream "^2.1.0"
+
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
@@ -8845,6 +8971,13 @@ xmlbuilder@~11.0.0:
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
+xmldoc@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-0.4.0.tgz#d257224be8393eaacbf837ef227fd8ec25b36888"
+ integrity sha512-rJ/+/UzYCSlFNuAzGuRyYgkH2G5agdX1UQn4+5siYw9pkNC3Hu/grYNDx/dqYLreeSjnY5oKg74CMBKxJHSg6Q==
+ dependencies:
+ sax "~1.1.1"
+
xtend@^4.0.2, xtend@~4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
diff --git a/go.mod b/go.mod
index 5b2dbc6e..ac33519b 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.18
require (
github.com/analogj/go-util v0.0.0-20210417161720-39b497cca03b
- github.com/fastenhealth/fasten-sources v0.0.3
+ github.com/fastenhealth/fasten-sources v0.0.4
github.com/gin-gonic/gin v1.8.1
github.com/glebarez/sqlite v1.5.0
github.com/golang-jwt/jwt/v4 v4.4.2
diff --git a/go.sum b/go.sum
index cda78cce..6a867822 100644
--- a/go.sum
+++ b/go.sum
@@ -72,8 +72,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fastenhealth/fasten-sources v0.0.3 h1:g07Lk59hiOWO2PhJoaP7eC0OIeIsxE50e6wH6sW96J4=
-github.com/fastenhealth/fasten-sources v0.0.3/go.mod h1:f6N8wLHEPXbz1QvI+JDQzIz9u1xu0Lp/j0LxYFHkpBM=
+github.com/fastenhealth/fasten-sources v0.0.4 h1:yIauhnrczFheH/DIRabmlUhmahiRGUFsNJ0z/0XsLNQ=
+github.com/fastenhealth/fasten-sources v0.0.4/go.mod h1:f6N8wLHEPXbz1QvI+JDQzIz9u1xu0Lp/j0LxYFHkpBM=
github.com/fastenhealth/gofhir-models v0.0.4 h1:Q//StwNXGfK+WAS2DckGBHAP1R4cHMRZEF/sLGgmR04=
github.com/fastenhealth/gofhir-models v0.0.4/go.mod h1:xB8ikGxu3bUq2b1JYV+CZpHqBaLXpOizFR0eFBCunis=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=