Better Reporting (#12)
This commit is contained in:
parent
f67c369a22
commit
6fd69575d1
|
@ -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
|
||||
|
|
22
Dockerfile
22
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"]
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -195,6 +195,46 @@ func (sr *SqliteRepository) UpsertRawResource(ctx context.Context, sourceCredent
|
|||
SourceResourceType: rawResource.SourceResourceType,
|
||||
},
|
||||
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
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -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"`
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
|
|
|
@ -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" {
|
||||
|
|
|
@ -22,4 +22,4 @@ web:
|
|||
|
||||
log:
|
||||
file: '' #absolute or relative paths allowed, eg. web.log
|
||||
level: DEBUG
|
||||
level: INFO
|
||||
|
|
|
@ -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"]
|
|
@ -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}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
#!/usr/bin/with-contenv bash
|
||||
|
||||
echo "starting couchdb"
|
||||
/docker-entrypoint.sh /opt/couchdb/bin/couchdb
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -10,9 +10,13 @@
|
|||
<a href="#" (click)="toggleHeaderMenu($event)" class="close">×</a>
|
||||
</div><!-- az-header-menu-header -->
|
||||
<ul class="nav">
|
||||
|
||||
<li class="nav-item" ngbDropdown [ngClass]="{ 'active': dashboard.isActive }">
|
||||
<a routerLink="/dashboard" routerLinkActive="active" #dashboard="routerLinkActive" class="nav-link"><fa-icon [icon]="['fas', 'table-columns']"></fa-icon> Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item" ngbDropdown [ngClass]="{ 'active': medicalHistory.isActive }">
|
||||
<a routerLink="/medical-history" routerLinkActive="active" #medicalHistory="routerLinkActive" class="nav-link"><fa-icon [icon]="['fas', 'book-medical']"></fa-icon> Medical History</a>
|
||||
</li>
|
||||
<li class="nav-item" ngbDropdown [ngClass]="{ 'active': sources.isActive }">
|
||||
<a routerLink="/sources" routerLinkActive="active" #sources="routerLinkActive" class="nav-link"><fa-icon [icon]="['fas', 'hospital']"></fa-icon> Sources</a>
|
||||
</li>
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<div class="az-dashboard-one-title">
|
||||
<div>
|
||||
<h2 class="az-dashboard-title">{{reportHeaderTitle}}</h2>
|
||||
<p class="az-dashboard-text">{{reportHeaderSubTitle}}</p>
|
||||
</div>
|
||||
<div class="az-content-header-right">
|
||||
<div class="media">
|
||||
<div class="media-body">
|
||||
<label>Last Updated</label>
|
||||
<h6>Oct 10, 2018</h6>
|
||||
</div><!-- media-body -->
|
||||
</div><!-- media -->
|
||||
<div *ngIf="primaryCare" class="media">
|
||||
<div class="media-body">
|
||||
<label>Primary Care</label>
|
||||
<h6>{{primaryCare | fhirPath: "Practitioner.name.family.first()"}}, {{primaryCare | fhirPath: "Practitioner.name.given.first()"}}</h6>
|
||||
</div><!-- media-body -->
|
||||
</div><!-- media -->
|
||||
<div class="media">
|
||||
<div class="media-body">
|
||||
<label>Selected Sources</label>
|
||||
<h6>All Sources</h6>
|
||||
</div><!-- media-body -->
|
||||
</div><!-- media -->
|
||||
<a [routerLink]="'/sources'" class="btn btn-purple">Add Source</a>
|
||||
</div>
|
||||
</div><!-- az-dashboard-one-title -->
|
||||
|
||||
<div class="az-dashboard-nav">
|
||||
<nav class="nav">
|
||||
</nav>
|
||||
|
||||
<nav class="nav">
|
||||
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="far fa-save"></i> Save Report</a>
|
||||
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="far fa-file-pdf"></i> Export to PDF</a>
|
||||
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="far fa-envelope"></i>Send to Email</a>
|
||||
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="fas fa-ellipsis-h"></i></a>
|
||||
</nav>
|
||||
</div>
|
|
@ -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<ReportHeaderComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ReportHeaderComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ReportHeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -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]
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
<div class="card card-dashboard-seven mb-3">
|
||||
<div class="card-header tx-medium">
|
||||
<div class="row" routerLink="/source/{{condition?.source_id}}/resource/{{condition?.source_resource_id}}">
|
||||
<!-- Condition Header -->
|
||||
<div class="col-6">
|
||||
{{condition | fhirPath: "Condition.code.text.first()":"Condition.code.coding.display.first()"}}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
{{condition | fhirPath: "Condition.onsetPeriod.start":"Condition.onsetDateTime" | date }} - {{condition | fhirPath: "Condition.onsetPeriod.end" | 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 tx-indigo">
|
||||
<p>Involved in Care</p>
|
||||
</div>
|
||||
<ng-container *ngFor="let careTeamEntry of careTeams | keyvalue">
|
||||
<div class="col-6">
|
||||
<strong>{{careTeamEntry.value | fhirPath: "CareTeam.participant.member.display"}}</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
{{careTeamEntry.value | fhirPath: "CareTeam.participant.role.text"}}
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let practitionerEntry of practitioners | keyvalue">
|
||||
<div class="col-6">
|
||||
<strong>{{practitionerEntry.value | fhirPath: "Practitioner.name.family"}}, {{practitionerEntry.value | fhirPath: "Practitioner.name.given"}}</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
{{practitionerEntry.value | fhirPath: "Practitioner.name.prefix"}}
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<!-- <div class="col-12 mt-3 mb-2 tx-indigo">-->
|
||||
<!-- <h5>Initial Presentation</h5>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="col-12">-->
|
||||
<!-- Acute right knee pain and tenderness around the joint line - this was likely caused by acute renal failure.-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-6 bg-gray-100">
|
||||
<div class="row">
|
||||
<ng-container *ngFor="let encounter of condition.related_resources | filter:'source_resource_type':'Encounter'">
|
||||
|
||||
<div routerLink="/source/{{encounter?.source_id}}/resource/{{encounter?.source_resource_id}}" class="col-6 mt-3 mb-2 tx-indigo">
|
||||
<strong>{{encounter | fhirPath: "Encounter.period.start" | date}}</strong>
|
||||
</div>
|
||||
<div routerLink="/source/{{encounter?.source_id}}/resource/{{encounter?.source_resource_id}}" class="col-6 mt-3 mb-2 tx-indigo">
|
||||
<small>{{encounter | fhirPath: "Encounter.location.first().location.display"}}</small>
|
||||
</div>
|
||||
|
||||
<div *ngIf="encounter.related_resources | filter:'source_resource_type':'MedicationRequest' as medications" class="col-12 mt-2 mb-2">
|
||||
<strong>Medications:</strong>
|
||||
<ul>
|
||||
<li routerLink="/source/{{medication?.source_id}}/resource/{{medication?.source_resource_id}}" *ngFor="let medication of medications">
|
||||
{{medication | fhirPath: "MedicationRequest.medicationReference.display":"MedicationRequest.medicationCodeableConcept.text"}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div *ngIf="encounter.related_resources | filter:'source_resource_type':'Procedure' as procedures" class="col-12 mt-2 mb-2">
|
||||
<strong>Procedures:</strong>
|
||||
<ul>
|
||||
<li routerLink="/source/{{procedure?.source_id}}/resource/{{procedure?.source_resource_id}}" *ngFor="let procedure of procedures">
|
||||
{{procedure | fhirPath: "Procedure.code.text"}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div *ngIf="encounter.related_resources | filter:'source_resource_type':'DiagnosticReport' as diagnosticReports" class="col-12 mt-2 mb-2">
|
||||
<strong>Tests and Examinations:</strong>
|
||||
<ul>
|
||||
<li routerLink="/source/{{diagnosticReport?.source_id}}/resource/{{diagnosticReport?.source_resource_id}}" *ngFor="let diagnosticReport of diagnosticReports">
|
||||
{{diagnosticReport | fhirPath: "DiagnosticReport.code.text":"DiagnosticReport.code.coding.display"}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div *ngIf="encounter.related_resources | filter:'source_resource_type':'Device' as devices" class="col-12 mt-2 mb-2">
|
||||
<strong>Device:</strong>
|
||||
<ul>
|
||||
<li routerLink="/source/{{device?.source_id}}/resource/{{device?.source_resource_id}}" *ngFor="let device of devices">
|
||||
{{device | fhirPath: "Device.code.text"}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div><!-- card-body -->
|
||||
</div>
|
|
@ -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<ReportMedicalHistoryConditionComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ReportMedicalHistoryConditionComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ReportMedicalHistoryConditionComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -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}`
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<div class="modal-header">
|
||||
<h4 class="modal-title"> Condition Editor </h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')"><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<tree-root [nodes]="nodes" [options]="options" (moveNode)="onResourceMoved($event)"></tree-root>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-dark" (click)="activeModal.close('Close click')">Close</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -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<ReportMedicalHistoryEditorComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ReportMedicalHistoryEditorComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ReportMedicalHistoryEditorComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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,6 +81,11 @@ import { MomentModule } from 'ngx-moment';
|
|||
ResourceListOutletDirective,
|
||||
ListFallbackResourceComponent,
|
||||
ToastComponent,
|
||||
ReportHeaderComponent,
|
||||
FhirPathPipe,
|
||||
ReportMedicalHistoryEditorComponent,
|
||||
FilterPipe,
|
||||
ReportMedicalHistoryConditionComponent,
|
||||
],
|
||||
exports: [
|
||||
ComponentsSidebarComponent,
|
||||
|
@ -105,6 +117,12 @@ import { MomentModule } from 'ngx-moment';
|
|||
ResourceListComponent,
|
||||
ResourceListOutletDirective,
|
||||
ToastComponent,
|
||||
ReportHeaderComponent,
|
||||
ReportMedicalHistoryEditorComponent,
|
||||
FhirPathPipe,
|
||||
FilterPipe,
|
||||
ReportMedicalHistoryConditionComponent
|
||||
|
||||
]
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -6,6 +6,7 @@ export class ResourceFhir {
|
|||
|
||||
fhir_version: string = ""
|
||||
resource_raw: IResourceRaw
|
||||
related_resources?: ResourceFhir[] = []
|
||||
|
||||
constructor(object?: any) {
|
||||
return Object.assign(this, object)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -3,122 +3,31 @@
|
|||
<div class="az-content-body">
|
||||
|
||||
<!-- Header Row -->
|
||||
<div class="row">
|
||||
<div class="col-6 bg-indigo tx-white d-flex align-items-center">
|
||||
<h1>Medical History</h1>
|
||||
<report-header [reportHeaderTitle]="'Medical History'"></report-header>
|
||||
|
||||
|
||||
<!-- Editor Button -->
|
||||
<div class="row mt-5 mb-3">
|
||||
<div class="col-12">
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<strong>Warning!</strong> Fasten has detected medical Encounters that are not associated with a Condition.
|
||||
They are grouped under the "Unassigned" section below.
|
||||
<br/>
|
||||
You can re-organize your conditions & encounters by using the <a class="alert-link cursor-pointer" (click)="openEditorRelated()">report editor</a>
|
||||
</div>
|
||||
<div class="col-3 mt-2 mb-2 tx-12">
|
||||
<p class="mb-0">
|
||||
<strong>Patient:</strong> Caldwell, Ruben<br/>
|
||||
<strong>Address:</strong> 123 B Street<br/>Gainsville, FL, 94153<br/>
|
||||
<strong>Date of Birth:</strong> June 20, 1929<br/>
|
||||
<strong>Phone:</strong> 415-343-2342<br/>
|
||||
<strong>Email:</strong> myemail@gmail.com
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-3 tx-indigo mt-2 mb-2 tx-12">
|
||||
<p class="mb-0">
|
||||
<strong>Primary Care:</strong> Bishop, J. ANRP<br/>
|
||||
<strong>Address:</strong> Malcom Randall VA <br/>Medical Center Gainsville FL<br/>
|
||||
<strong>Phone:</strong> 123-321-5532<br/>
|
||||
<strong>Email:</strong> myemail@va.com<br/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conditions Title -->
|
||||
<div class="row mt-5 mb-3">
|
||||
<div class="col-6">
|
||||
<h1>Condition</h1>
|
||||
</div>
|
||||
<div class="col-6 tx-indigo">
|
||||
<h1>History</h1>
|
||||
<h1 class="az-dashboard-title">Condition</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Condition List -->
|
||||
|
||||
<div class="row bg-indigo tx-white pt-1 pb-1">
|
||||
<!-- Condition Header -->
|
||||
<div class="col-6 d-flex align-items-center">
|
||||
<h3 class="mb-0">Gout</h3>
|
||||
</div>
|
||||
<div class="col-6 d-flex align-items-center">
|
||||
<h3 class="mb-0">Nov 16, 2002 - Present</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<!-- Condition Details -->
|
||||
|
||||
<div class="col-6 mb-2">
|
||||
|
||||
<div class="row pl-3">
|
||||
<div class="col-12 mt-3 mb-2 tx-indigo">
|
||||
<h5>Involved in Care</h5>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong>James Bishop</strong>, ANRP
|
||||
</div>
|
||||
<div class="col-6">
|
||||
Primary Care
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong>Matthew Leonard</strong>, MD
|
||||
</div>
|
||||
<div class="col-6">
|
||||
Diagnosing Physician
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong>Stephanie Wrenn</strong>, MD
|
||||
</div>
|
||||
<div class="col-6">
|
||||
Dietitian
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12 mt-3 mb-2 tx-indigo">
|
||||
<h5>Initial Presentation</h5>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
Acute right knee pain and tenderness around the joint line - this was likely caused by acute renal failure.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-6 mb-2 bg-gray-100">
|
||||
<div class="row">
|
||||
<div class="col-12 mt-3 mb-2 tx-indigo">
|
||||
<h5>Nov 19, 2012</h5>
|
||||
</div>
|
||||
<div class="col-12 mt-2 mb-2">
|
||||
<strong>Medications:</strong> Colchicine, as needed for gout attacks
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12 mt-3 mb-2 tx-indigo">
|
||||
<h5>Nov 16, 2012</h5>
|
||||
</div>
|
||||
<div class="col-12 mt-2 mb-2">
|
||||
<strong>Procedures:</strong> The fluid in your right knee was drained.
|
||||
</div>
|
||||
<div class="col-12 mt-2 mb-2">
|
||||
<strong>Tests and Examinations:</strong> The fluid tested prositive for gout crystals
|
||||
</div>
|
||||
<div class="col-12 mt-2 mb-2">
|
||||
<strong>Medications:</strong> You were given a steroid injection to reduce inflammation and as short course prednistone to reduce pain and inflammation
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<app-report-medical-history-condition *ngFor="let condition of conditions; let i = index" [condition]="condition"></app-report-medical-history-condition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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}`;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -3,34 +3,13 @@
|
|||
<div class="az-content-body">
|
||||
|
||||
<!-- Header Row -->
|
||||
<div class="row">
|
||||
<div class="col-6 bg-indigo tx-white d-flex align-items-center">
|
||||
<h1>Patient Profile</h1>
|
||||
</div>
|
||||
<div class="col-3 mt-2 mb-2 tx-12">
|
||||
<p class="mb-0">
|
||||
<strong>Patient:</strong> Caldwell, Ruben<br/>
|
||||
<strong>Address:</strong> 123 B Street<br/>Gainsville, FL, 94153<br/>
|
||||
<strong>Date of Birth:</strong> June 20, 1929<br/>
|
||||
<strong>Phone:</strong> 415-343-2342<br/>
|
||||
<strong>Email:</strong> myemail@gmail.com
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-3 tx-indigo mt-2 mb-2 tx-12">
|
||||
<p class="mb-0">
|
||||
<strong>Primary Care:</strong> Bishop, J. ANRP<br/>
|
||||
<strong>Address:</strong> Malcom Randall VA <br/>Medical Center Gainsville FL<br/>
|
||||
<strong>Phone:</strong> 123-321-5532<br/>
|
||||
<strong>Email:</strong> myemail@va.com<br/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<report-header [reportHeaderTitle]="'Patient Profile'"></report-header>
|
||||
|
||||
<div class="pl-3 pr-3">
|
||||
<!-- Patient Name Row -->
|
||||
<div class="row mt-5 mb-3">
|
||||
<div class="col-6">
|
||||
<h1>Caldwell, Ruben</h1>
|
||||
<h1>{{patient | fhirPath: "Patient.name.family.first()"}}, {{patient | fhirPath: "Patient.name.given.first()"}}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -44,28 +23,28 @@
|
|||
<strong class="tx-indigo">First Name:</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
Ruben
|
||||
{{patient | fhirPath: "Patient.name.given.first()"}}
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong class="tx-indigo">Last Name:</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
Caldwell
|
||||
{{patient | fhirPath: "Patient.name.family.first()"}}
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong class="tx-indigo">Gender:</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
M
|
||||
{{patient | fhirPath: "Patient.gender" | titlecase}}
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong class="tx-indigo">Martial Status:</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
Married
|
||||
{{patient | fhirPath: "Patient.maritalStatus.text" | titlecase}}
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
|
@ -86,35 +65,35 @@
|
|||
<strong class="tx-indigo">Language:</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
English
|
||||
{{patient | fhirPath: "Patient.communication.language.text" | titlecase}}
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong class="tx-indigo">Address:</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
123 B Street<br/>Gainsville, FL, 94153<br/>
|
||||
{{patient | fhirPath: "Patient.address.line.first()"}}<br/>{{patient | fhirPath: "Patient.address.city.first()"}}, {{patient | fhirPath: "Patient.address.state.first()"}}, {{patient | fhirPath: "Patient.address.postalCode.first()"}}
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong class="tx-indigo">Date of Birth:</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
June 20, 1929
|
||||
{{patient | fhirPath: "Patient.birthDate" | date }}
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong class="tx-indigo">Phone:</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
415-343-2342
|
||||
{{patient | fhirPath: "Patient.telecom.where(system='phone').value.first()"}}
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong class="tx-indigo">Email:</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
myemail@gmail.com
|
||||
{{patient | fhirPath: "Patient.telecom.where(system='email').value.first()"}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -163,14 +142,12 @@
|
|||
<h2 class="tx-indigo">Immunizations</h2>
|
||||
|
||||
<p>
|
||||
<strong class="tx-indigo">Phenumovax</strong><br/>
|
||||
07/09/2022<br/>
|
||||
<ng-container *ngFor="let immunization of immunizations; let i = index">
|
||||
<strong class="tx-indigo">{{immunization | fhirPath: "Immunization.vaccineCode.text" }}</strong><br/>
|
||||
{{immunization | fhirPath: "Immunization.occurrenceDateTime" | date }}<br/>
|
||||
{{immunization | fhirPath: "Immunization.location.display" }}<br/>
|
||||
<br/>
|
||||
<strong class="tx-indigo">Hepatitis B Series [complete]</strong><br/>
|
||||
01/12/2005<br/>
|
||||
<br/>
|
||||
<strong class="tx-indigo">Tetanus Toxoid [complete]</strong><br/>
|
||||
02/02/2010<br/>
|
||||
</ng-container>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
@ -178,6 +155,12 @@
|
|||
<h2 class="tx-indigo">Allergies</h2>
|
||||
|
||||
<p>
|
||||
<ng-container *ngFor="let allergy of allergyIntolerances; let i = index">
|
||||
<strong class="tx-indigo">{{allergy | fhirPath: "AllergyIntolerance.code.text" }}</strong><br/>
|
||||
{{allergy | fhirPath: "AllergyIntolerance.occurrenceDateTime" | date }}<br/>
|
||||
<br/>
|
||||
</ng-container>
|
||||
|
||||
<strong class="tx-indigo">Latex</strong><br/>
|
||||
(AV/Historical) with rash and agitation<br/>
|
||||
<br/>
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import { FhirPathPipe } from './fhir-path.pipe';
|
||||
|
||||
describe('FhirPathPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new FhirPathPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { FilterPipe } from './filter.pipe';
|
||||
|
||||
describe('FilterPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new FilterPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
// import { Pipe, PipeTransform } from '@angular/core';
|
||||
//
|
||||
// @Pipe({
|
||||
// name: 'filter',
|
||||
// })
|
||||
// export class FilterPipe implements PipeTransform {
|
||||
// transform(items: any[], filter: Record<string, any>): 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;
|
||||
}
|
||||
}
|
|
@ -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<ResourceFhir[]> {
|
||||
getResources(sourceResourceType?: string, sourceID?: string, sourceResourceID?: string, preloadRelated?: boolean): Observable<ResourceFhir[]> {
|
||||
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<any>(`${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<any> {
|
||||
return this._httpClient.post<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/resource/association`, resourceAssociation)
|
||||
.pipe(
|
||||
map((response: ResponseWrapper) => {
|
||||
console.log("RESPONSE", response)
|
||||
return response.data
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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"
|
||||
|
|
2
go.mod
2
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
Loading…
Reference in New Issue