├── .github
└── workflows
│ ├── run-tests.yml
│ └── test-and-deploy.yml
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE.txt
├── README.md
├── package-lock.json
├── package.json
├── public
├── app.css
├── bootstrap.min.css
├── callback.html
├── config
│ ├── bluebutton_endpoints.json
│ ├── config-override-demo.json
│ ├── config.json
│ ├── epic_endpoints_r4.json
│ └── sandbox_endpoints.json
├── favicon.ico
├── index.html
└── manifest.json
├── scripts
└── get_epic_endpoints.sh
├── src
├── components
│ ├── App.js
│ ├── BlankSlate.js
│ ├── Fetcher.js
│ ├── FhirTree.js
│ ├── FileUploader.js
│ ├── GithubUploader.js
│ ├── Header.js
│ ├── Loader.js
│ ├── ProviderForm.js
│ ├── ProviderList.js
│ ├── SettingsImportDropZone.js
│ ├── TimeAgo.js
│ ├── Toolbar.js
│ └── Wizard
│ │ ├── DownloadButton.js
│ │ ├── Retrieve.js
│ │ ├── Review.js
│ │ ├── SelectProvider.js
│ │ ├── Start.js
│ │ ├── Upload.js
│ │ └── index.js
├── index.js
├── schemas
│ ├── config-schema.json
│ ├── endpoint-list-schema.json
│ ├── samples
│ │ └── user-settings.json
│ ├── upload-manifest-schema.json
│ └── user-settings-schema.json
├── serviceWorker.js
├── smart
│ ├── fhir.js
│ ├── smart-core.js
│ └── smart-popup.js
├── store
│ ├── config-loader.js
│ ├── csv-writer.js
│ ├── fhir-loader.js
│ ├── file-exporter.js
│ ├── github-api.js
│ ├── github-exporter.js
│ ├── initial-state-dev.js
│ ├── initial-state.js
│ ├── merge-objects.js
│ ├── spreadsheet-exporter.js
│ ├── store.js
│ ├── user-settings.js
│ └── zip-exporter.js
├── sw-build.js
├── sw.js
└── test
│ ├── App.test.js
│ ├── data
│ ├── document1.js
│ ├── document_reference.json
│ ├── duck.js
│ ├── josh
│ │ ├── unity-point
│ │ │ ├── allergy-intolerance.json
│ │ │ ├── condition.json
│ │ │ ├── document-reference-TksSRPcka4BoYduTTHg-k0jPC5rVuPExcfPtz4UQS-3uCXiMVAQgj2SsyEWKvUbPZ.jSROON4EThqtJZa5Az4PJuqIqOe6xg7cyGjo7YoRxEB.xml
│ │ │ ├── document-reference-Tr-wsqP3I4ItgWRr1UtQWZHlE7rlv7nbeuTFnUqoLY4.wE7nA4Ugt7PuvMvct9ydxMXubL7BSwkb00UgR1HewAiIlpSl3AHRtkFgkT7XRf.IB.xml
│ │ │ ├── document-reference.json
│ │ │ ├── immunization.json
│ │ │ ├── laboratory.json
│ │ │ ├── medication-order.json
│ │ │ ├── medication-statement.json
│ │ │ ├── patient.json
│ │ │ ├── procedure.json
│ │ │ ├── social-history.json
│ │ │ └── vital-signs.json
│ │ └── uw
│ │ │ ├── allergy-intolerance.json
│ │ │ ├── condition.json
│ │ │ ├── document-reference-TLFF7UrfbsbX.Of.6p86Rt6KAX0tYwTfligqU-QjdS78CAZHlHdbEzFy1YQvLdgFU2MvF6KOhqpGv.IGaD4leQBLPT55WRrZsm4pXroi.w2kB.xml
│ │ │ ├── document-reference-TOIpsPdnnD8RBIcWAoT9Vmynd.xVLCSnQF7yCE56mN5hWinmeCT0v-6C1EpEhHsy9injJ361KLSVBvx6A2AIIRIeOsUTWWPx9yZN8SaobGnMB.xml
│ │ │ ├── document-reference-TVmPYRkn9A.cSDZwweE61.Zpt7R6f6ekSxQqSl.qWZffg.pEYDV5OJRCBvvyp1iczssSv9tk9Uu518PR.kPn9ne.NNoNM.g4KBj1-BwCa6AQB.xml
│ │ │ ├── document-reference-TcoDtNRyERbHqp4AjTVQYeoAzJOflQgYLHQOzlLZ0k1WyM0GwXqRw.mQdpsitgmVgm3fAMcsjfFJlb0Cix6AQsdVH5IYv.9g4vhAm-Aqf5jEB.xml
│ │ │ ├── document-reference.json
│ │ │ ├── immunization.json
│ │ │ ├── laboratory.json
│ │ │ ├── medication-order.json
│ │ │ ├── medication-statement.json
│ │ │ ├── patient.json
│ │ │ ├── procedure.json
│ │ │ ├── social-history.json
│ │ │ └── vital-signs.json
│ ├── metadata.json
│ ├── observation-cerner.json
│ ├── observation-epic.json
│ ├── observation1.json
│ ├── observation2.json
│ ├── operation_outcome_404.json
│ ├── patient.json
│ ├── procedure.json
│ └── smart-configuration.json
│ ├── fhir.test.js
│ ├── merge-objects.test.js
│ ├── smart-core.test.js
│ ├── spreadsheet.test.js
│ └── util
│ └── base64toblob.js
└── upload-backends
├── node-server-aws
├── index.js
├── package-lock.json
└── package.json
└── node-server
├── index.js
├── package-lock.json
└── package.json
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: Node CI
2 |
3 | on:
4 | push:
5 | branches-ignore:
6 | - master
7 |
8 | jobs:
9 | build:
10 |
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | node-version: [18.x]
16 |
17 | steps:
18 | - uses: actions/checkout@v1
19 | - name: Use Node.js ${{ matrix.node-version }}
20 | uses: actions/setup-node@v1
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 | - name: npm install, build, and test
24 | run: |
25 | npm ci
26 | npm run build
27 | npm run test
28 | env:
29 | CI: true
30 |
--------------------------------------------------------------------------------
/.github/workflows/test-and-deploy.yml:
--------------------------------------------------------------------------------
1 | name: Test and Deploy Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 |
10 | test:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | node-version: [18.x]
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Use Node.js ${{ matrix.node-version }}
20 | uses: actions/setup-node@v1
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 | - name: npm install, build, and test
24 | run: |
25 | npm ci
26 | npm run build
27 | npm run test
28 | env:
29 | CI: true
30 |
31 | build-deploy:
32 | runs-on: ubuntu-latest
33 | needs: test
34 | steps:
35 | - uses: actions/checkout@v2
36 |
37 | - name: Build
38 | run: npm install && npm run-script build
39 |
40 | - run: mv $GITHUB_WORKSPACE/build/config/config-override-demo.json $GITHUB_WORKSPACE/build/config/config-override.json
41 | shell: bash
42 |
43 | - name: Deploy
44 | uses: peaceiris/actions-gh-pages@v4
45 | with:
46 | github_token: ${{ secrets.GITHUB_TOKEN }}
47 | publish_dir: ./build
48 | cname: procure.syncfor.science
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 | uploaded_files
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | notes.txt
28 | config-override.json
29 | config-override-dev.json
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "json.schemas": [
3 | {
4 | "fileMatch": [
5 | "config.json"
6 | ],
7 | "url": "./src/schemas/config-schema.json"
8 | },
9 | {
10 | "fileMatch": [
11 | "endpoints.json"
12 | ],
13 | "url": "./src/schemas/endpoint-list-schema.json"
14 | },
15 | {
16 | "fileMatch": [
17 | "procure-settings.json"
18 | ],
19 | "url": "./src/schemas/user-settings-schema.json"
20 | },
21 | {
22 | "fileMatch": [
23 | "upload-manifest.json"
24 | ],
25 | "url": "./src/schemas/upload-manifest-schema.json"
26 | },
27 | ],
28 | "git.ignoreLimitWarning": true
29 | }
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2020 President and Fellows of Harvard College
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "procure-wip",
3 | "version": "0.1.0",
4 | "private": false,
5 | "homepage": "https://procure.syncfor.science",
6 | "dependencies": {
7 | "@fortawesome/fontawesome-svg-core": "^1.2.19",
8 | "@fortawesome/free-brands-svg-icons": "^5.9.0",
9 | "@fortawesome/free-regular-svg-icons": "^5.9.0",
10 | "@fortawesome/free-solid-svg-icons": "^5.9.0",
11 | "@fortawesome/react-fontawesome": "^0.1.4",
12 | "cross-fetch": "^3.0.1",
13 | "file-saver": "^2.0.1",
14 | "jszip": "^3.2.1",
15 | "lodash": "^4.17.15",
16 | "match-url-wildcard": "0.0.4",
17 | "react": "^16.8.3",
18 | "react-dom": "^16.8.3",
19 | "react-json-tree": "^0.11.2",
20 | "react-select": "^3.0.8",
21 | "reactstrap": "^8.2.0",
22 | "sanitize-filename": "^1.6.1",
23 | "storeon": "^0.8.3",
24 | "strip-json-comments": "^2.0.1",
25 | "timeago.js": "^4.0.0-beta.2",
26 | "tv4": "^1.3.0",
27 | "xlsx-populate": "^1.20.1"
28 | },
29 | "scripts": {
30 | "start": "react-scripts --openssl-legacy-provider start",
31 | "build-sw": "node ./src/sw-build.js",
32 | "build": "react-scripts --openssl-legacy-provider build && rm -rf build/config/config-override-dev.json && npm run build-sw",
33 | "analyze": "source-map-explorer 'build/static/js/*.js'",
34 | "test": "react-scripts test",
35 | "eject": "react-scripts eject"
36 | },
37 | "eslintConfig": {
38 | "extends": "react-app"
39 | },
40 | "browserslist": [
41 | ">0.2%",
42 | "not dead",
43 | "not ie <= 11",
44 | "not op_mini all"
45 | ],
46 | "devDependencies": {
47 | "jest-fetch-mock": "^2.1.1",
48 | "react-scripts": "3.4.1",
49 | "react-test-renderer": "^16.8.6",
50 | "source-map-explorer": "^2.2.2",
51 | "workbox-build": "^4.3.1"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/public/app.css:
--------------------------------------------------------------------------------
1 | /* change the background color */
2 | .navbar {
3 | background-color: #3182CE !important;
4 | }
5 | /* change the brand and text color */
6 | .navbar .navbar-brand,
7 | .navbar .navbar-text {
8 | color: #ffffff !important;
9 | }
10 | /* change the link color */
11 | .navbar .navbar-nav .nav-link {
12 | color: #ffffff !important;
13 | }
14 | /* change the color of active or hovered links */
15 | .navbar .nav-item.active .nav-link,
16 | .navbar .nav-item:focus .nav-link,
17 | .navbar .nav-item:hover .nav-link {
18 | color: #ffffff !important;
19 | }
20 |
21 | /* Landscape phones and portrait tablets */
22 | @media (max-width: 767px) {
23 | .unbox-sm {
24 | -webkit-box-shadow: none !important;
25 | -moz-box-shadow: none !important;
26 | box-shadow: none !important;
27 | }
28 | }
29 | @media (min-width: 768px) {
30 | .bg-light-md {
31 | background: #f8f9fa !important;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/public/callback.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
--------------------------------------------------------------------------------
/public/config/bluebutton_endpoints.json:
--------------------------------------------------------------------------------
1 | {
2 | "entry": [{
3 | "name": "Sandbox - BB - Care Evolution",
4 | "clientId": "2df118d5-7058-4ddc-9fa9-a124fba2858f",
5 | "fhirEndpoint":"https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-r4",
6 | // CEPatient / CEPatient2018
7 | "id": "care-evolution-sandbox"
8 | },{
9 | "name": "Sandbox - BB - Medicare",
10 | "clientId": "FKCHfWRH8j1WdkObD9Beb6LSIyJf9gq1EGzYjsKw",
11 | "clientSecret": "MGJGlgkVckpyWjnnLovbm3cZEaMV6t2EkSIzrGiz0su925rJqB6934BYc0mPTsmItPrbjumA9jKnbh0anSNpNUX7WUckUuPVLORyyoJUnbXi13ifKet5lPYyKUAdzuj6",
12 | "fhirEndpoint": "https://sandbox.bluebutton.cms.gov/v1/fhir/",
13 | //BBUser00000, with password PW00000!
14 | "id": "cms-bb-sandbox",
15 | "scope": ["profile", "patient/Patient.read", "patient/ExplanationOfBenefit.read", "patient/Coverage.read"]
16 | }]
17 | }
--------------------------------------------------------------------------------
/public/config/config-override-demo.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": "Procure Demo",
3 | "noCustomEndpoints": true,
4 | "upload": {
5 | "simulate": true
6 | , "label": "Share with Test Project"
7 | , "name": "Test Project"
8 | , "uploadUrl":"http://localhost:8000/123-1570639420963.zip"
9 | , "infoUrl":"http://example.com"
10 | , "successMessage":"Thank you - your information has been transmitted!"
11 | , "continueUrl": "http://example.com"
12 | },
13 | "_endpointLists": {
14 | "sandbox_endpoints": {
15 | "path": "./config/sandbox_endpoints.json",
16 | "defaults": {
17 | "queryProfile": "uscdi",
18 | "scope": ["patient/*.read", "launch/patient"]
19 | }
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/public/config/sandbox_endpoints.json:
--------------------------------------------------------------------------------
1 | {
2 | "entry": [{
3 | //MyChart Login Username: fhirderrick
4 | //MyChart Login Password: epicepic1
5 | //see full list at: https://fhir.epic.com/Documentation?docId=testpatients
6 | "name": "Sandbox - Epic",
7 | "fhirEndpoint":"https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4/",
8 | "id": "epic-sandbox-secure-r4",
9 | "credentialId": "epic_sandbox_secure_r4",
10 | "scope": ["launch/patient", "patient/*.read"],
11 | "queryProfile": "uscdi_epic"
12 | },{
13 | //https://launch.smarthealthit.org/?auth_error=&fhir_version_2=r4&iss=&launch_pt=1&launch_url=&patient=&prov_skip_auth=1&provider=&pt_skip_auth=1&pt_skip_login=0&public_key=&sde=&token_lifetime=15&user_pt=75e95f92-3bfa-454f-aa99-99375173f201
14 | "name": "Sandbox - SMART",
15 | "fhirEndpoint": "https://launch.smarthealthit.org/v/r4/sim/eyJrIjoiMSIsImoiOiIxIiwiYiI6Ijc1ZTk1ZjkyLTNiZmEtNDU0Zi1hYTk5LTk5Mzc1MTczZjIwMSJ9/fhir",
16 | "credentialId": "smart_sandbox_secure",
17 | "queryProfile": "uscdi"
18 | },{
19 | //open issues: can't read practitioner references even with correct scopes
20 | //also, in general, sandbox server token times out on large requests, so can only test a few
21 | //queries at a time.
22 | //username: nancysmart, password: Cerner01
23 | //see full list at: https://docs.google.com/document/d/10RnVyF1etl_17pyCyK96tyhUWRbrTyEcqpwzW-Z-Ybs/edit
24 | "name": "Sandbox - Cerner",
25 | "fhirEndpoint": "https://fhir-myrecord.cerner.com/r4/ec2458f2-1e24-41c8-b71b-0e701af7583d/",
26 | "queryProfile": "uscdi_cerner",
27 | "credentialId": "cerner_sandbox_secure",
28 | "scope": [
29 | "launch/patient", "profile", "openid",
30 | "user/Binary.read", "user/Practitioner.read", "patient/AllergyIntolerance.read", "patient/Appointment.read",
31 | "patient/Binary.read", "patient/CarePlan.read", "patient/CareTeam.read", "patient/Condition.read",
32 | "patient/Consent.read", "patient/Contract.read", "patient/Coverage.read", "patient/Device.read",
33 | "patient/DiagnosticReport.read", "patient/DocumentReference.read", "patient/Encounter.read",
34 | "patient/FamilyMemberHistory.read", "patient/Goal.read", "patient/Immunization.read",
35 | "patient/InsurancePlan.read", "patient/MedicationAdministration.read", "patient/MedicationDispense.read",
36 | "patient/MedicationOrder.read", "patient/MedicationRequest.read", "patient/MedicationStatement.read",
37 | "patient/NutritionOrder.read", "patient/Observation.read", "patient/Patient.read", "patient/Person.read",
38 | "patient/Procedure.read", "patient/ProcedureRequest.read", "patient/Provenance.read",
39 | "patient/Questionnaire.read", "patient/QuestionnaireResponse.read", "patient/RelatedPerson.read",
40 | "patient/Schedule.read", "patient/ServiceRequest.read", "patient/Slot.read"
41 | ]
42 | // },{
43 | //login info is tied to user's google account and is restricted by contract, so requires sign up to get a client id
44 | // "name": "Sandbox - Secure - Meditech",
45 | // "fhirEndpoint": "https://greenfield-apis.meditech.com/v1/argonaut/v1/",
46 | // "queryProfile": "argonaut_meditech",
47 | // "scope": ["launch/patient", "openid", "profile", "patient/*.read"],
48 | // "credentialId": "meditech_sandbox_secure"
49 | // },{
50 | //Getting 504 error when retrieving resources.
51 | //see: https://groups.google.com/forum/#!category-topic/nextgenapiusers/application-questions--issues/wPmbUIaflPM
52 | //username: wwrist, password: wwrist!123
53 | // "name": "Sandbox - Secure - NextGen",
54 | // "fhirEndpoint": "https://apigw-west.nextgen.com/nge/staging/fhirapi/dstu2/v1.0",
55 | // "scope": ["launch/patient", "openid", "profile", "patient/*.read"],
56 | // "credentialId": "nextgen_sandbox_secure",
57 | // "queryProfile": "demographics"
58 | // },{
59 | //Allscripts does not seem to support CORS see issue:
60 | //https://developer.allscripts.com/Forums#/discussion/2665/cors-error-in-patient-facing-app/p1?new=1
61 | //username: bill.aaron@open.allscripts.com, password: Password#1
62 | // "name": "Sandbox - Secure - Allscripts Prof (FMH)",
63 | // "fhirEndpoint": "https://pro171fmh.open.allscripts.com/open",
64 | // "scope": ["launch/patient", "openid", "profile", "patient/*.read"],
65 | // "credentialId": "allscripts_sandbox_secure",
66 | // "queryProfile": "argonaut_spec"
67 | }]
68 | }
69 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync-for-science/procure-wip/83884e339c4f81e5cfb1b6c8e2a7beaf6b960d4b/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
17 |
18 |
27 | Loading...
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/scripts/get_epic_endpoints.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | curl https://open.epic.com/Endpoints/R4 -o ../public/config/epic_endpoints_r4.json
--------------------------------------------------------------------------------
/src/components/App.js:
--------------------------------------------------------------------------------
1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling
2 |
3 | import React, {useEffect} from "react";
4 | import useStoreon from "storeon/react";
5 | import {Alert, Container, Row, Col} from 'reactstrap';
6 | import _ from "lodash";
7 | import ProviderForm from "./ProviderForm";
8 | import ProviderList from "./ProviderList";
9 | import Fetcher from "./Fetcher";
10 | import Loader from "./Loader"
11 | import GithubUploader from "./GithubUploader";
12 | import Header from "./Header";
13 | import FhirTree from "./FhirTree";
14 | import Toolbar from "./Toolbar";
15 | import BlankSlate from "./BlankSlate";
16 | import FileUploader from "./FileUploader";
17 | import Wizard from "./Wizard/";
18 |
19 | const App = () => {
20 |
21 | //global state
22 | const { uiState, showWizard, upload, providers, appName } = useStoreon("uiState", "upload", "showWizard", "providers", "appName");
23 |
24 | useEffect(() => {
25 | document.title = appName || "Procure";
26 | }, [appName]);
27 |
28 | if (uiState.mode === "loading")
29 | return ;
30 |
31 | if (uiState.mode === "globalError")
32 | return
33 |
34 | Failed to load configuration information.
35 | {uiState.error.toString()}
36 |
37 | ;
38 |
39 | const inlineError =
40 | {uiState.error}
41 |
42 |
43 | const renderProviderList = () =>
44 |
45 |
46 |
47 | const renderData = (hasResources) =>
48 |
49 | { hasResources && }
50 |
51 |
52 | const renderBlankSlate = () =>
53 |
54 |
55 |
56 | const hasProviders = providers && providers.length;
57 | const hasData = hasProviders && _.find(providers, p => {
58 | return p.selected && p.data && p.data.entry && (p.data.entry.length || p.data.errorLog.length);
59 | });
60 | const hasResources = hasProviders && _.find(providers, p => {
61 | return p.selected && p.data && p.data.entry && p.data.entry.length
62 | });
63 |
64 | const fullUi =
65 |
66 |
67 |
68 | { uiState.error && uiState.mode === "ready" && inlineError }
69 | { hasProviders ? renderProviderList() : renderBlankSlate() }
70 | { hasData ? renderData(hasResources) : null }
71 |
72 | { uiState.mode === "editProvider" && }
73 | { uiState.mode === "loadData" && }
74 | { uiState.mode === "githubExport" && }
75 | { uiState.mode === "fileUpload" && }
76 |
77 |
78 |
79 | const wizardUi = showWizard &&
80 | upload && (upload.manifestUrl || upload.uploadUrl) &&
81 |
82 | { uiState.error && uiState.mode === "ready" &&
83 | {inlineError}
84 | }
85 |
86 |
87 |
88 | if (wizardUi) {
89 | document.body.classList.add("bg-light-md");
90 | return wizardUi;
91 | } else {
92 | document.body.classList = "";
93 | return fullUi;
94 | }
95 |
96 | }
97 | export default App;
--------------------------------------------------------------------------------
/src/components/BlankSlate.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import useStoreon from "storeon/react";
3 | import {Button} from 'reactstrap';
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
5 | import { faPlus } from "@fortawesome/free-solid-svg-icons"
6 |
7 | export default () => {
8 |
9 | const { dispatch } = useStoreon();
10 |
11 | const handleAddProvider = (e) => {
12 | e.preventDefault();
13 | dispatch("uiState/set", {mode: "editProvider"});
14 | }
15 |
16 | return
19 |
20 | Add Provider
21 |
22 |
23 | }
--------------------------------------------------------------------------------
/src/components/Fetcher.js:
--------------------------------------------------------------------------------
1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling
2 |
3 | import React, { useMemo, useEffect, useCallback } from "react";
4 | import useStoreon from "storeon/react";
5 | import {Button, Spinner, Modal, ModalBody, ModalFooter} from 'reactstrap';
6 | import { saveAs } from "file-saver";
7 |
8 | export default () => {
9 |
10 | const { uiState, providers, dispatch } = useStoreon("uiState", "providers");
11 |
12 | const provider = useMemo( () => {
13 | return providers.find(p => p.id === uiState.id)
14 | }, [uiState.id, providers]);
15 |
16 |
17 | const handleCancel = useCallback( e => {
18 | e.preventDefault();
19 | dispatch("fhir/cancelLoad");
20 | }, [dispatch]);
21 |
22 | const handleClose = useCallback( e => {
23 | if (e) e.preventDefault();
24 | dispatch("uiState/set", {mode: "ready"});
25 | }, [dispatch]);
26 |
27 | //escape key effect
28 | //creates a warning due to a reactjs bug: https://github.com/facebook/react/pull/15650
29 | useEffect(() => {
30 | const downHandler = e => {
31 | if (e.keyCode !== 27) return;
32 | if (uiState.submode === "authorizing" || uiState.submode === "loading") {
33 | handleCancel(e);
34 | } else {
35 | handleClose(e);
36 | }
37 | };
38 | window.addEventListener("keydown", downHandler);
39 | return () => {
40 | window.removeEventListener("keydown", downHandler);
41 | };
42 | }, [handleCancel, handleClose, uiState.submode]);
43 |
44 | const handleSave = e => {
45 | e.preventDefault();
46 | dispatch("providers/update", {
47 | id: provider.id,
48 | lastUpdated: new Date(),
49 | data: uiState.data
50 | });
51 | handleClose();
52 | }
53 |
54 | const handleLogDownload = e => {
55 | e.preventDefault();
56 | const blob = new Blob(
57 | [JSON.stringify(uiState.data.errorLog, null, 2)],
58 | {type: "application/json"}
59 | );
60 | saveAs(blob, "errors.json");
61 | }
62 |
63 | const renderAuthorizing = () =>
64 |
Waiting for authorization...
65 |
66 | Cancel
67 |
68 |
69 |
70 | const pluralizeEn = (text, len) => len === 1 ? text : text+"s";
71 |
72 | const renderLoading = () =>
73 |
74 | {uiState.requestCount} {pluralizeEn("request", uiState.requestCount)}
75 | /
76 | {uiState.errorCount} {pluralizeEn("error", uiState.errorCount)}
77 |
78 |
79 |
80 |
81 |
82 | Cancel
83 |
84 |
85 |
86 | const renderLoaded = () =>
87 |
88 | {uiState.data.entry.length} {pluralizeEn("resource", uiState.data.entry.length)} retrieved
89 | {uiState.data.files.length || "No"} {pluralizeEn("attachment", uiState.data.files.length)} downloaded
90 | {uiState.data.errorLog.length || "No"} {pluralizeEn("error", uiState.data.errorLog.length)} occurred
91 |
92 |
93 |
94 | const renderGlobalError = () =>
95 |
An error occurred: { uiState.status }
96 |
97 | Close
98 |
99 |
100 |
101 |
102 | const renderLoadedButtons = () => {
103 | const downloadLink = uiState.data.errorLog.length > 0 &&
104 | ;
105 | return
106 | {downloadLink}
107 | Cancel
108 | Save
109 |
110 | }
111 |
112 | return
113 |
114 | {provider.name}
115 | { uiState.submode === "authorizing" && renderAuthorizing() }
116 | { uiState.submode === "loading" && renderLoading() }
117 | { uiState.submode === "loaded" && renderLoaded() }
118 | { uiState.submode === "error" && renderGlobalError() }
119 |
120 | { uiState.submode === "loaded" && renderLoadedButtons() }
121 |
122 |
123 |
124 | }
--------------------------------------------------------------------------------
/src/components/FhirTree.js:
--------------------------------------------------------------------------------
1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling
2 |
3 | import React, { useState, useMemo, useRef } from "react";
4 | import useStoreon from "storeon/react";
5 | import Select from "react-select";
6 | import JSONTree from "react-json-tree";
7 | import {FormGroup, Button} from "reactstrap";
8 | import _ from "lodash";
9 | import { saveAs } from "file-saver";
10 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
11 | import { faChevronLeft, faChevronRight } from "@fortawesome/free-solid-svg-icons"
12 |
13 | export default () => {
14 |
15 | //app state
16 | const { providers, dateSortElements } = useStoreon("providers", "dateSortElements");
17 |
18 | //component state
19 | const [resourceType, setResourceType] = useState();
20 | const [position, setPosition] = useState(1);
21 | const [overlayResources, setOverlayResources] = useState([]);
22 |
23 | const componentTopRef = useRef();
24 |
25 | let resourceTypeOptions = useMemo( () => {
26 | return _.chain(providers)
27 | .filter( p => p.data && p.selected )
28 | .map( p => p.data.entry.map(e => e.resource.resourceType) )
29 | .flatten().uniq()
30 | .map(o => ({label: o, value: o}))
31 | .sortBy(["label"])
32 | .value();
33 | }, [providers]);
34 |
35 | let resources = useMemo( () => {
36 | return _.chain(providers)
37 | .filter( p => p.data && p.data.entry && p.selected )
38 | .map( p => p.data.entry ).flatten()
39 | .filter( e => (!resourceType || resourceType.value === e.resource.resourceType) )
40 | .sortBy( e => {
41 | const sortElements = dateSortElements[e.resource.resourceType] || [];
42 | const sortElement = _.find( sortElements, element => e.resource[element]);
43 | const dateValue =
44 | (sortElement && (e.resource[sortElement].start || e.resource[sortElement])) ||
45 | (e.resource.meta && e.resource.meta.lastUpdated) || "";
46 | return dateValue;
47 | })
48 | .reverse()
49 | .value();
50 | }, [providers, resourceType, dateSortElements]);
51 |
52 | //remove leading slashes, trailing slashes and protocol
53 | const simplifyUrl = (url) => url.replace(/^\/|https?:\/\/|\/*$/g, "")
54 |
55 | const referenceIndex = useMemo( () => {
56 | let index = {};
57 | _.each(providers, (provider, i) => {
58 | _.each(((provider.data && provider.data.entry) || []), (entry, j) => {
59 | index[simplifyUrl(entry.fullUrl)] = [i, j, "resource"]
60 | })
61 | _.each(((provider.data && provider.data.files) || []), (file, j) => {
62 | index[file.fileName] = [i, j, "file"]
63 | })
64 | });
65 | return index;
66 | }, [providers]);
67 |
68 | const handlePosChange = (dir, e) => {
69 | e.preventDefault();
70 | setPosition(position+dir);
71 | }
72 |
73 | const filters =
74 | {setResourceType(selection); setPosition(1);setOverlayResources([]);} }
77 | isSearchable={true}
78 | options={ resourceTypeOptions }
79 | placeholder="Filter by Resource Type"
80 | isClearable={true}
81 | />
82 |
83 |
84 | const renderPositionNav = () => {
85 | const noResources =
86 | No resources found
87 |
;
88 |
89 | const nextPrevNav =
90 | handlePosChange(-1, e)} className="mr-3"
93 | >
94 |
95 |
96 | Resource {position} of {resources.length}
97 | handlePosChange(1, e)} className="ml-3"
100 | >
101 |
102 |
103 |
;
104 |
105 | const overlayNav =
106 | {
107 | setOverlayResources(_.slice(overlayResources, 0, -1))
108 | }}>
109 |
110 | Back
111 |
112 |
;
113 |
114 | if (overlayResources.length) {
115 | return overlayNav;
116 | } else if (resources.length) {
117 | return nextPrevNav;
118 | } else {
119 | return noResources;
120 | }
121 |
122 | }
123 |
124 | const treeTheme = {
125 | base00: '#272822',base01: '#383830',base02: '#49483e',base03: '#75715e',
126 | base04: '#a59f85',base05: '#f8f8f2',base06: '#f5f4f1',base07: '#f9f8f5',
127 | base08: '#f92672',base09: '#fd971f',base0A: '#f4bf75',base0B: '#a6e22e',
128 | base0C: '#a1efe4',base0D: '#66d9ef',base0E: '#ae81ff',base0F: '#cc6633'
129 | };
130 |
131 | const handleReference = (url, e) => {
132 | e.preventDefault();
133 | const index = referenceIndex[url];
134 | if (index[2] === "file") {
135 | const file = providers[index[0]].data.files[index[1]];
136 | saveAs(file.blob, file.fileName);
137 | } else {
138 | setOverlayResources([...overlayResources, url]);
139 | window.scrollTo(0, componentTopRef.current.offsetTop);
140 | }
141 | }
142 |
143 | const valueRenderer = (raw, value, key) => {
144 | let link;
145 |
146 | if (key === "reference") {
147 | //remove leading and trailing slashes
148 | let refUrl = simplifyUrl(value);
149 |
150 | //add base url if relative path
151 | if ((refUrl.match(/\//g) || []).length === 1) {
152 | const fullUrl =
153 | (overlayResources.length && _.last(overlayResources).fullUrl) ||
154 | resources[position-1].fullUrl;
155 | const baseUrl = simplifyUrl(fullUrl).split("/");
156 | refUrl = baseUrl
157 | .slice(0, baseUrl.length-2).join("/") + "/" + refUrl;
158 | }
159 | if (referenceIndex[refUrl]) link = refUrl;
160 | //this could be optimized by only looking at attachment keys
161 | } else if (key === "url" && referenceIndex[value]) {
162 | link = value;
163 | //truncate very long values
164 | } else if (value.length > 500) {
165 | raw = value.substring(0,500) + "...";
166 | }
167 |
168 | return link ?
169 | " handleReference(link, e)}
171 | >{value} "
172 | : raw;
173 | }
174 |
175 | const getResourceByUrl = (url) => {
176 | const index = referenceIndex[url];
177 | if (index) return providers[index[0]].data.entry[index[1]];
178 | }
179 |
180 | const renderJsonTree = () => {
181 |
182 | const overlayData = overlayResources.length && getResourceByUrl(_.last(overlayResources))
183 | if (overlayResources.length && !overlayData)
184 | setOverlayResources(_.slice(overlayResources, 1,-1));
185 | const data = (
186 | overlayData && overlayData.resource
187 | ) || (
188 | resources[position-1] && resources[position-1].resource
189 | );
190 |
191 | if (data) return
196 | true}
201 | invertTheme={true}
202 | valueRenderer={valueRenderer}
203 | />
204 |
;
205 | }
206 |
207 | return
208 | { filters }
209 | { renderPositionNav() }
210 | { renderJsonTree() }
211 |
212 | }
--------------------------------------------------------------------------------
/src/components/FileUploader.js:
--------------------------------------------------------------------------------
1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling
2 |
3 | import React, { useEffect, useCallback } from "react";
4 | import useStoreon from "storeon/react";
5 | import {
6 | Button, Spinner, Modal, ModalHeader,
7 | ModalBody, ModalFooter, Container, Alert
8 | } from 'reactstrap';
9 |
10 | export default () => {
11 | //app state
12 | const {uiState, upload, dispatch} = useStoreon("uiState", "upload");
13 |
14 | const handleHideDialog = useCallback( e => {
15 | if (e) e.preventDefault()
16 | dispatch("uiState/set", {mode: "ready"})
17 | }, [dispatch]);
18 |
19 | const handleDone = useCallback( e => {
20 | if (e) e.preventDefault()
21 |
22 | if (upload.continueUrl)
23 | window.open(upload.continueUrl);
24 |
25 | handleHideDialog(e);
26 | }, [handleHideDialog, upload]);
27 |
28 | const handleCancelExport = useCallback( e => {
29 | e.preventDefault()
30 | const endState = uiState.submode === "getManifest"
31 | ? {mode: "ready"}
32 | : {mode: "fileUpload", submode: "preUpload"}
33 | dispatch("export/upload/cancel");
34 | dispatch("uiState/set", endState)
35 | }, [dispatch, uiState.submode]);
36 |
37 | const handleUploadFile = useCallback( e => {
38 | e.preventDefault()
39 | dispatch("export/upload/send");
40 | }, [dispatch]);
41 |
42 | //escape key effect
43 | //creates a warning due to a reactjs bug: https://github.com/facebook/react/pull/15650
44 | useEffect(() => {
45 | const downHandler = e => {
46 | if (e.keyCode !== 27) return;
47 | if (uiState.submode === "preUpload" || uiState.submode === "postUpload") {
48 | handleHideDialog(e);
49 | } else {
50 | handleCancelExport(e);
51 | }
52 | };
53 | window.addEventListener("keydown", downHandler);
54 | return () => {
55 | window.removeEventListener("keydown", downHandler);
56 | };
57 | }, [uiState.submode, handleCancelExport, handleHideDialog]);
58 |
59 | const renderLoading = () =>
60 |
61 | {uiState.status}
62 |
63 |
64 |
65 |
66 |
67 | Cancel
68 |
69 |
70 |
71 | const renderError = () =>
72 |
73 |
74 |
75 | {uiState.status}
76 |
77 |
78 |
79 |
80 | Close
81 |
82 |
83 |
84 | const renderUploadDetails = () =>
85 |
86 |
87 | Clicking the upload button below will share your medical record
88 | information with {upload.name} and indicates your agreement to the
89 | terms of use, privacy policy and other information outlined at
90 |
91 | {upload.infoUrl}
92 | .
93 |
94 |
95 |
96 | Upload
97 | Cancel
98 |
99 |
100 |
101 | const renderPostUpload = () =>
102 |
103 | {upload.successMessage || "Your information has been transmitted!"}
104 |
105 |
106 |
107 | {upload.continueUrl ? (upload.continueLabel || "Continue") : "Close"}
108 |
109 |
110 |
111 |
112 | return
113 | Share My Data
114 | {uiState.submode === "error" && renderError()}
115 | {uiState.submode === "getManifest" && renderLoading()}
116 | {uiState.submode === "preUpload" && renderUploadDetails()}
117 | {uiState.submode === "uploading" && renderLoading()}
118 | {uiState.submode === "postUpload" && renderPostUpload()}
119 |
120 |
121 | }
--------------------------------------------------------------------------------
/src/components/GithubUploader.js:
--------------------------------------------------------------------------------
1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling
2 |
3 | import React, { useState, useEffect, useCallback } from "react";
4 | import useStoreon from "storeon/react";
5 | import {
6 | Button, Spinner, Col, Row,
7 | Form, FormGroup, Modal, ModalHeader,
8 | ModalBody, ModalFooter, Label,
9 | Input, Container, Alert
10 | } from 'reactstrap';
11 |
12 | export default () => {
13 | //app state
14 | const {uiState, githubConfig, dispatch} = useStoreon("uiState", "githubConfig");
15 |
16 | //component state
17 | const [showInstructions, setShowInstructions] = useState();
18 |
19 | const handleHideDialog = useCallback( e => {
20 | if (e) e.preventDefault()
21 | dispatch("uiState/set", {mode: "ready"})
22 | }, [dispatch]);
23 |
24 | const handleCancelExport = useCallback( e => {
25 | e.preventDefault()
26 | dispatch("export/github/cancel");
27 | }, [dispatch]);
28 |
29 | //escape key effect
30 | //creates a warning due to a reactjs bug: https://github.com/facebook/react/pull/15650
31 | useEffect(() => {
32 | const downHandler = e => {
33 | if (e.keyCode !== 27) return;
34 | if (uiState.submode !== "push") {
35 | handleHideDialog(e);
36 | } else {
37 | handleCancelExport(e);
38 | }
39 | };
40 | window.addEventListener("keydown", downHandler);
41 | return () => {
42 | window.removeEventListener("keydown", downHandler);
43 | };
44 | }, [uiState.submode, handleCancelExport, handleHideDialog]);
45 |
46 | const handleSubmit = e => {
47 | e.preventDefault()
48 | if (!githubConfig.token || !githubConfig.owner || !githubConfig.project) {
49 | dispatch("uiState/merge", {
50 | error: "Please enter a personal access token, repository owner and repository project to continue."
51 | });
52 | } else {
53 | dispatch("export/github");
54 | }
55 | }
56 |
57 | const handleFieldChange = (fieldName, e) => {
58 | const value = fieldName === "pullRequest"
59 | ? e.target.value === "true"
60 | : e.target.value;
61 | dispatch("config/merge", {
62 | githubConfig: {
63 | ...githubConfig, [fieldName]: value
64 | }
65 | });
66 | dispatch("uiState/merge", {
67 | status: null,
68 | error: null
69 | });
70 | dispatch("refreshDirty");
71 | }
72 |
73 | const handleToggleInstructions = e => {
74 | e.preventDefault();
75 | setShowInstructions(!showInstructions);
76 | }
77 |
78 | const renderLoading = () =>
79 |
80 | {uiState.status}
81 |
82 |
83 |
84 |
85 |
86 | Cancel
87 |
88 |
89 |
90 | const renderError = () => (
91 |
92 |
93 |
94 | {uiState.error.toString()}
95 |
96 |
97 |
98 | );
99 |
100 | const renderMessage = () => {
101 | const url = uiState.statusUrl
102 | ? (view )
103 | : null;
104 | return
105 |
106 |
107 | {uiState.status}
108 | {url}.
109 |
110 |
111 |
112 | };
113 |
114 | const renderForm = () =>
194 |
195 | return
196 | Push to Github Repository
197 | { uiState.submode === "push" ? renderLoading() : renderForm() }
198 |
199 |
200 | }
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling
2 |
3 | import React, {useRef, useEffect} from "react";
4 | import useStoreon from "storeon/react";
5 | import {Navbar, NavbarBrand, Nav, NavItem, NavLink} from 'reactstrap';
6 | import SettingsImportDropZone from "./SettingsImportDropZone";
7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
8 | import { faMedkit } from "@fortawesome/free-solid-svg-icons"
9 |
10 | export default () => {
11 |
12 | const { providers, isDirty, warnOnPageNavigate, appName, dispatch } = useStoreon("providers", "isDirty", "warnOnPageNavigate", "appName");
13 | const fileInput = useRef();
14 |
15 | useEffect( () => {
16 | window.onbeforeunload = () => (isDirty && warnOnPageNavigate) ? true : null;
17 | });
18 |
19 | const handleImportSettings = e => {
20 | e.preventDefault();
21 | fileInput.current.value = "";
22 | fileInput.current.click();
23 | }
24 |
25 | const handleFileSelected = e => {
26 | e.preventDefault();
27 | e.stopPropagation();
28 | if (e.target.files.length === 1)
29 | readFile(e.target.files[0]);
30 | }
31 |
32 | const handleDownloadSettings = e => {
33 | e.preventDefault();
34 | dispatch("export/settings");
35 | }
36 |
37 | const readFile = (file) => {
38 | const replace = providers.length === 0 || window.confirm(
39 | "Current providers in your list will be replaced. Continue?"
40 | )
41 | if (replace) {
42 | dispatch("import/settings", file)
43 | dispatch("refreshDirty", true);
44 | };
45 | }
46 |
47 | return
48 |
54 |
55 |
56 |
57 | {appName || "Procure"}
58 |
59 |
60 |
61 | import settings
62 |
63 |
64 |
65 | export settings
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | }
--------------------------------------------------------------------------------
/src/components/Loader.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {Spinner} from 'reactstrap';
3 |
4 | export default () => {
5 |
6 | return
7 |
8 |
Loading
9 |
10 |
11 | }
--------------------------------------------------------------------------------
/src/components/ProviderList.js:
--------------------------------------------------------------------------------
1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling
2 |
3 | import React from "react";
4 | import useStoreon from "storeon/react";
5 | import TimeAgo from "./TimeAgo"
6 | import { Button, CardBody, Card, Row, Col } from "reactstrap";
7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
8 | import { faPlus, faTrash, faEdit, faCheckSquare } from "@fortawesome/free-solid-svg-icons"
9 | import { faSquare } from "@fortawesome/free-regular-svg-icons"
10 |
11 | export default () => {
12 |
13 | const { providers, dispatch } = useStoreon("providers");
14 |
15 | const handleDeleteProvider = (id, e) => {
16 | e.preventDefault();
17 | dispatch("providers/remove", id);
18 | dispatch("refreshDirty");
19 | }
20 |
21 | const handleEditProvider = (id, e) => {
22 | e.preventDefault();
23 | dispatch("uiState/set", {
24 | mode: "editProvider", id
25 | });
26 | }
27 |
28 | const handleLoadRecords = (id, e) => {
29 | e.preventDefault();
30 | dispatch("fhir/loadData", id);
31 | }
32 |
33 | const handleAddProvider = (e) => {
34 | e.preventDefault();
35 | dispatch("uiState/set", {mode: "editProvider"});
36 | }
37 |
38 | const toggleProviderSelection = (id, newValue, e) => {
39 | dispatch("providers/update", {
40 | id: id, selected: newValue
41 | });
42 | }
43 |
44 | const pluralizeEn = (text, len) => len === 1 ? text : text+"s";
45 |
46 | const renderCard = (provider) => {
47 |
48 |
49 | const renderDataDetails = () =>
50 | { provider.data.entry.length} { pluralizeEn("resource", provider.data.entry.length) }{" | "}
51 | { provider.data.files.length} { pluralizeEn("file", provider.data.files.length) }{" | "}
52 | { provider.data.errorLog.length} { pluralizeEn("error", provider.data.errorLog.length) }
53 | loaded
54 |
55 |
56 | const controls =
79 |
80 | return
81 |
82 |
83 |
84 | toggleProviderSelection(provider.id, !provider.selected)}
89 | />
90 |
91 |
92 |
93 |
94 |
95 | toggleProviderSelection(provider.id, !provider.selected, e)}
98 | >
99 | {provider.name}
100 |
101 | { provider.lastUpdated && renderDataDetails() }
102 | { controls }
103 |
104 |
105 |
106 |
107 | };
108 |
109 | return
110 |
111 |
115 |
116 | Add Provider
117 |
118 |
119 | { providers.map(renderCard) }
120 |
121 |
122 | }
--------------------------------------------------------------------------------
/src/components/SettingsImportDropZone.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | export default ({ children, dropHint, fileDropHandler }) => {
4 |
5 | const [dragCounter, setDragCounter] = useState(0);
6 |
7 | const isFileDrag = e => {
8 | return !e.dataTransfer.types ||
9 | e.dataTransfer.types[0] === "Files" ||
10 | e.dataTransfer.types[0] === "application/x-moz-file";
11 | }
12 |
13 | const handleDrop = e => {
14 | e.preventDefault();
15 | e.stopPropagation();
16 | if (e.dataTransfer.files && e.dataTransfer.files.length === 1)
17 | fileDropHandler(e.dataTransfer.files[0]);
18 | // e.dataTransfer.clearData();
19 | setDragCounter(0);
20 | }
21 |
22 | const handleDragOver = e => {
23 | e.preventDefault();
24 | e.stopPropagation();
25 | }
26 |
27 | const handleDragEnter = e => {
28 | e.preventDefault();
29 | e.stopPropagation();
30 | if (isFileDrag(e))
31 | setDragCounter(dragCounter+1);
32 | }
33 |
34 | const handleDragLeave = e => {
35 | e.preventDefault();
36 | e.stopPropagation();
37 | if (isFileDrag(e))
38 | setDragCounter(dragCounter-1);
39 | }
40 |
41 | const overlay =
51 |
52 | { dropHint || "Drop settings file here"}
53 |
54 |
55 |
56 | return
63 | { dragCounter > 0 && overlay }
64 | { children }
65 |
66 |
67 |
68 | }
--------------------------------------------------------------------------------
/src/components/TimeAgo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {format} from "timeago.js";
3 |
4 | export default class TimeAgo extends React.Component {
5 |
6 | constructor(props) {
7 | super(props);
8 | this.state= {timeAgo: format(this.props.time)}
9 | }
10 |
11 | componentDidMount() {
12 | this.interval = setInterval(() => {
13 | this.setState({timeAgo: format(this.props.time)});
14 | }, (this.props.refreshMinutes||1)*60000);
15 | }
16 |
17 | componentWillUnmount() {
18 | clearInterval(this.interval);
19 | }
20 |
21 | render() {
22 | return this.state.timeAgo
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/src/components/Toolbar.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from "react";
2 | import {Button, Dropdown, DropdownToggle, DropdownMenu, DropdownItem} from 'reactstrap';
3 | import useStoreon from "storeon/react";
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
5 | import { faTable, faFileDownload, faFileUpload } from "@fortawesome/free-solid-svg-icons"
6 | import { faGithub } from "@fortawesome/free-brands-svg-icons"
7 |
8 | export default (props) => {
9 |
10 | const {
11 | spreadsheetTemplates,
12 | upload,
13 | nonModalUi, dispatch
14 | } = useStoreon(
15 | "spreadsheetTemplates",
16 | "upload",
17 | "nonModalUi"
18 | );
19 |
20 | const [showSpreadsheetOptions, setShowSpreadsheetOptions] = useState(false);
21 |
22 | const handleExportData = (e) => {
23 | e.preventDefault();
24 | dispatch("export/download");
25 | }
26 |
27 | const handleFileUpload = (e) => {
28 | e.preventDefault()
29 | dispatch("export/upload");
30 | }
31 |
32 | const handleShowGithubExport = (e) => {
33 | e.preventDefault()
34 | dispatch("uiState/set", {mode: "githubExport"});
35 | }
36 |
37 | const handelExportSpreadsheet = (templateId, format, e) => {
38 | e.preventDefault()
39 | dispatch("export/spreadsheet", {templateId, format});
40 | }
41 |
42 | const renderSpreadsheetExport = () => {
43 | if (!Object.keys(spreadsheetTemplates||{}).length)
44 | return null;
45 | let menuItems = [];
46 | Object.keys(spreadsheetTemplates).forEach( templateId => {
47 | menuItems.push(
48 |
52 | {spreadsheetTemplates[templateId].name + " (Excel)"}
53 | ,
54 |
58 | {spreadsheetTemplates[templateId].name + " (CSV)"}
59 |
60 | )
61 | });
62 | return setShowSpreadsheetOptions(!showSpreadsheetOptions)}
67 | >
68 |
73 |
74 | Export as Spreadsheet
75 |
76 | {menuItems}
77 |
78 | }
79 |
80 | const uploadButton =
85 |
86 | {upload.label || "Share Data"}
87 | ;
88 |
89 | const downloadAllButton =
95 |
96 | Download Data
97 | ;
98 |
99 | const ghExportButton =
104 |
105 | Upload to Github
106 | ;
107 |
108 | return
109 | {upload && (upload.manifestUrl || upload.uploadUrl) && uploadButton}
110 | {downloadAllButton}
111 | {props.hasResources && renderSpreadsheetExport()}
112 | {ghExportButton}
113 |
114 |
115 | }
--------------------------------------------------------------------------------
/src/components/Wizard/DownloadButton.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from "react";
2 | import {Dropdown, DropdownToggle, DropdownMenu, DropdownItem} from 'reactstrap';
3 | import useStoreon from "storeon/react";
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
5 | import { faFileDownload } from "@fortawesome/free-solid-svg-icons"
6 |
7 | export default (props) => {
8 |
9 | const {
10 | spreadsheetTemplates,
11 | nonModalUi,
12 | dispatch
13 | } = useStoreon(
14 | "spreadsheetTemplates",
15 | "nonModalUi"
16 | );
17 |
18 | const [showDownloadOptions, setShowDownloadOptions] = useState(false);
19 |
20 | const handelExportSpreadsheet = (templateId, format, e) => {
21 | e.preventDefault()
22 | dispatch("export/spreadsheet", {templateId, format});
23 | }
24 |
25 | const handleExportData = (e) => {
26 | e.preventDefault();
27 | dispatch("export/download");
28 | }
29 |
30 | let menuItems = [
31 | FHIR Format
35 | ];
36 |
37 | (Object.keys(spreadsheetTemplates) || []).forEach( templateId => {
38 | menuItems.push(
39 |
43 | Spreadsheet of {spreadsheetTemplates[templateId].name}
44 |
45 | )
46 | });
47 |
48 | return setShowDownloadOptions(!showDownloadOptions)}
54 | >
55 |
60 |
61 | Download copy of data
62 |
63 | {menuItems}
64 |
65 |
66 | }
--------------------------------------------------------------------------------
/src/components/Wizard/Retrieve.js:
--------------------------------------------------------------------------------
1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling
2 |
3 | import React, { useMemo, useEffect, useCallback } from "react";
4 | import useStoreon from "storeon/react";
5 | import {Button, Spinner} from 'reactstrap';
6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
7 | import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"
8 |
9 | export default () => {
10 |
11 | const { uiState, providers, dispatch } = useStoreon("uiState", "providers");
12 |
13 | const provider = useMemo( () => {
14 | return providers.find(p => p.id === uiState.id)
15 | }, [uiState.id, providers]);
16 |
17 | const handleCancel = useCallback( e => {
18 | e.preventDefault();
19 | dispatch("fhir/cancelLoad", "editProvider");
20 | dispatch("providers/remove", provider.id)
21 | }, [dispatch, provider]);
22 |
23 | const handleBack = e => {
24 | dispatch("providers/remove", provider.id)
25 | dispatch("uiState/set", {mode: "editProvider"});
26 | }
27 |
28 | //escape key effect
29 | //creates a warning due to a reactjs bug: https://github.com/facebook/react/pull/15650
30 | useEffect(() => {
31 | const downHandler = e => {
32 | if (e.keyCode !== 27) return;
33 | if (uiState.submode === "authorizing" || uiState.submode === "loading") {
34 | handleCancel(e);
35 | }
36 | };
37 | window.addEventListener("keydown", downHandler);
38 | return () => {
39 | window.removeEventListener("keydown", downHandler);
40 | };
41 | }, [handleCancel, uiState.submode]);
42 |
43 | //TODO: this is kind of a hack to build on non-wizard modes (need to restructure modes to work with both)
44 | if (uiState.mode !== "loadData") return null;
45 | if (uiState.submode === "loaded") {
46 | dispatch("providers/update", {
47 | id: provider.id,
48 | lastUpdated: new Date(),
49 | data: uiState.data
50 | });
51 | dispatch("uiState/set", {mode: "review"});
52 | };
53 |
54 | const renderWorking = (status) =>
55 |
{status}
56 |
57 |
58 |
59 |
60 | Cancel
61 |
62 |
63 |
64 | const renderGlobalError = () =>
65 |
Error
66 |
An error occurred loading your record: {uiState.status}
67 |
68 |
69 |
70 | Back
71 |
72 |
73 |
74 |
75 | return
76 |
{provider.name}
77 | { uiState.submode === "authorizing" && renderWorking("Waiting for Portal Login...") }
78 | { uiState.submode === "loading" && renderWorking("Retrieving Healthcare Records...") }
79 | { uiState.submode === "error" && renderGlobalError() }
80 |
81 |
82 |
83 | }
--------------------------------------------------------------------------------
/src/components/Wizard/Review.js:
--------------------------------------------------------------------------------
1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling
2 |
3 | import React from "react";
4 | import { Button } from 'reactstrap';
5 | import useStoreon from "storeon/react";
6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
7 | import { faChevronRight } from "@fortawesome/free-solid-svg-icons"
8 | import DownloadButton from "./DownloadButton";
9 | import _ from "lodash";
10 |
11 | export default () => {
12 |
13 | const {providers, upload, dispatch} = useStoreon("providers", "upload");
14 |
15 | const handleAddProvider = e => {
16 | e.preventDefault();
17 | dispatch("uiState/set", {mode: "editProvider"});
18 | }
19 | const handleFileUpload = e => {
20 | e.preventDefault()
21 | dispatch("export/upload/send");
22 | }
23 |
24 | const handleDeleteProvider = (id, e) => {
25 | e.preventDefault();
26 | dispatch("providers/remove", id);
27 | dispatch("refreshDirty");
28 | }
29 |
30 | const renderProvider = (provider) => {
31 | return
32 | {provider.name}{" "}
33 | ( handleDeleteProvider(provider.id, e)}>remove )
34 |
35 | };
36 |
37 | const providerList = _.chain(providers)
38 | .sortBy( p => p.lastUpdated)
39 | .map(renderProvider)
40 | .value();
41 |
42 | return
43 |
Review Data to Share
44 |
You have collected records from the following healthcare providers:
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
56 |
57 |
Add another provider
58 |
59 |
60 |
61 |
62 |
63 |
64 |
69 |
70 |
71 | Share with {upload.name}
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | }
--------------------------------------------------------------------------------
/src/components/Wizard/SelectProvider.js:
--------------------------------------------------------------------------------
1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling
2 |
3 | import React, { useState, useEffect } from "react";
4 | import useStoreon from "storeon/react";
5 | import { Button } from 'reactstrap';
6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
7 | import { faChevronRight } from "@fortawesome/free-solid-svg-icons"
8 | import Select, { createFilter } from "react-select";
9 | import _ from "lodash";
10 |
11 | export default () => {
12 |
13 | //app state
14 | const {
15 | uiState, providers, credentials,
16 | organizations, orgDefaults, redirectUri, dispatch
17 | } = useStoreon(
18 | "uiState", "providers", "credentials",
19 | "organizations", "orgDefaults", "redirectUri"
20 | );
21 |
22 | //component state
23 | const [provider, setProvider] = useState({});
24 |
25 | const orgOptions = _.chain(organizations)
26 | .map( o => ({label: o.name, value: o.orgId || o.fhirEndpoint}) )
27 | .sortBy( o => o.label )
28 | .value();
29 |
30 | useEffect( () => {
31 | if (!provider.id && uiState.id && uiState.mode === "editProvider") {
32 | const currentProvider = providers.find(p => p.id === uiState.id);
33 | setProvider(currentProvider)
34 | }
35 | }, [uiState.id, uiState.mode, provider.id, providers])
36 |
37 | const handleSubmit = e => {
38 | e.preventDefault();
39 | dispatch("providers/upsertAndLoad", provider);
40 | dispatch("refreshDirty");
41 | }
42 |
43 | const handleSkip = e => {
44 | e.preventDefault();
45 | dispatch("uiState/set", {mode: "review"});
46 | }
47 |
48 | const handleOrgSelection = (selection) => {
49 | let orgConfig = organizations.find(
50 | o => selection.value === o.orgId || selection.value === o.fhirEndpoint
51 | );
52 |
53 | const defaults = orgDefaults[orgConfig.defaultId] || {};
54 | if (orgConfig.defaultId !== undefined)
55 | orgConfig = {...defaults, ...orgConfig};
56 |
57 | //get credential details if included by reference
58 | if (orgConfig.credentialId)
59 | orgConfig = {...credentials[orgConfig.credentialId], ...orgConfig};
60 |
61 | //update provider with new defaults
62 | setProvider({
63 | ...orgConfig,
64 | redirectUri,
65 | id: provider.id,
66 | orgId: selection.value,
67 | queryProfile: orgConfig.queryProfile || "argonaut_spec"
68 | });
69 | }
70 |
71 | const renderOrgSelector = () => {
72 |
73 | //workaround since component uses both the value and label to match and we just want to store the value
74 | const value = provider.orgId ? orgOptions.find(o => o.value === provider.orgId) : null;
75 |
76 | return
85 | }
86 |
87 | return
88 |
Select Your Provider
89 |
90 | Choose a healthcare institution where you've received care from the list below.
91 |
92 | If you've been to multiple healthcare providers, after retrieving your records you'll have the option to return to this screen to select another institution.
93 |
94 | { renderOrgSelector() }
95 |
96 |
97 |
98 | {providers.length > 0 &&
skip }
99 |
100 |
101 |
105 | Login to Patient Portal
106 |
107 |
108 |
109 |
110 |
111 |
112 | }
--------------------------------------------------------------------------------
/src/components/Wizard/Start.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import useStoreon from "storeon/react";
3 | import { Button } from 'reactstrap';
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 | import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
6 |
7 | export default() => {
8 |
9 | const {upload, dispatch} = useStoreon("upload");
10 |
11 | return
12 |
Welcome
13 |
This app will walk you through the process of collecting digital copies of your medical records from healthcare institutions where you've received care and sharing them with {upload.name}. You can find additional information at {upload.infoUrl} .
14 |
You'll also have the option to download a copy of the data to this device to keep for yourself.
15 |
dispatch("uiState/set", {mode: "editProvider" }) }
17 | >
18 | Get Started
19 |
20 |
21 |
22 |
23 |
24 |
25 | }
--------------------------------------------------------------------------------
/src/components/Wizard/Upload.js:
--------------------------------------------------------------------------------
1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling
2 |
3 | import React, { useEffect, useCallback } from "react";
4 | import useStoreon from "storeon/react";
5 | import DownloadButton from "./DownloadButton";
6 | import { Button, Spinner } from 'reactstrap';
7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
8 | import { faChevronRight, faChevronLeft } from "@fortawesome/free-solid-svg-icons"
9 |
10 | export default () => {
11 | //app state
12 | const {uiState, upload, dispatch} = useStoreon("uiState", "upload");
13 |
14 | const handleBack = useCallback( e => {
15 | if (e) e.preventDefault()
16 | dispatch("uiState/set", {mode: "review"})
17 | }, [dispatch]);
18 |
19 | const handleDone = useCallback( e => {
20 | if (e) e.preventDefault()
21 | if (upload.continueUrl)
22 | window.location.href = upload.continueUrl;
23 | }, [upload]);
24 |
25 | const handleCancel = useCallback( e => {
26 | e.preventDefault();
27 | dispatch("export/upload/cancel");
28 | dispatch("uiState/set", {mode: "review"});
29 | }, [dispatch]);
30 |
31 | //escape key effect
32 | //creates a warning due to a reactjs bug: https://github.com/facebook/react/pull/15650
33 | useEffect(() => {
34 | const downHandler = e => {
35 | if (e.keyCode !== 27 || uiState.submode === "uploading") return;
36 | handleCancel(e);
37 | };
38 | window.addEventListener("keydown", downHandler);
39 | return () => {
40 | window.removeEventListener("keydown", downHandler);
41 | };
42 | }, [uiState.submode, handleCancel, handleBack]);
43 |
44 | const renderWorking = () =>
45 |
{uiState.status || "Preparing to send data"}
46 |
47 |
48 |
49 |
50 | Cancel
51 |
52 |
53 |
54 | const renderError = () =>
55 |
Error
56 |
{uiState.status}
57 |
58 |
59 |
60 | Back
61 |
62 |
63 |
64 |
65 | const renderPostUpload = () =>
66 |
Success
67 |
{
68 | upload.successMessage ||
69 | ("Your healthcare records have been successfully shared with " + upload.name + ".")
70 | }
71 |
72 | {upload.continueUrl &&
73 |
74 |
75 | { upload.continueLabel || "Continue to " + upload.name }
76 |
77 |
78 |
79 | }
80 |
81 |
82 |
83 | return
84 | {uiState.submode === "error" && renderError()}
85 | {uiState.submode !== "postUpload" && uiState.submode !== "error" && renderWorking()}
86 | {uiState.submode === "postUpload" && renderPostUpload()}
87 |
88 |
89 | }
--------------------------------------------------------------------------------
/src/components/Wizard/index.js:
--------------------------------------------------------------------------------
1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ //disable to support bootstrap link styling
2 |
3 | import React from "react";
4 | import useStoreon from "storeon/react";
5 | import Start from "./Start";
6 | import SelectProvider from "./SelectProvider";
7 | import Retrieve from "./Retrieve";
8 | import Review from "./Review";
9 | import Upload from "./Upload";
10 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
11 | import { faMedkit, faChevronRight } from "@fortawesome/free-solid-svg-icons"
12 | import {Row, Col} from "reactstrap";
13 |
14 | export default() => {
15 |
16 | //app state
17 | const {uiState, dispatch, appName} = useStoreon("uiState", "appName");
18 |
19 | const handleSwitchMode = () => dispatch("wizard/hide");
20 |
21 | const numberStyle = {
22 | active: {background: "#3182CE", border: "1px solid white", color: "white"},
23 | inactive: {color: "#3182CE", border: "1px solid #3182CE", background: "white"}
24 | };
25 |
26 | const switchToFull = uiState.mode !== "loadData" &&
27 | (uiState.mode !== "upload" || uiState.submode === "loaded") &&
28 |
29 | switch to full version of Procure
30 |
;
31 |
32 | return
33 | {switchToFull}
34 |
35 |
36 |
37 | {appName || "Procure"}
38 |
39 | |
40 |
41 |
42 |
43 |
44 |
45 |
49 |
50 |
51 |
52 |
53 |
2
54 |
Retrieve Records
55 |
56 |
57 |
58 |
59 |
63 |
64 |
65 |
66 |
67 |
4
68 |
Share Records
69 |
70 |
71 |
72 |
73 | { uiState.mode === "ready" &&
}
74 | { uiState.mode === "editProvider" &&
}
75 | { uiState.mode === "loadData" &&
}
76 | { uiState.mode === "review" &&
}
77 | { uiState.mode === "fileUpload" &&
}
78 |
79 |
80 |
81 | |
82 |
83 |
84 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import StoreContext from 'storeon/react/context'
4 | //import 'bootstrap/dist/css/bootstrap.min.css';
5 | //moved to index.html to avoid flash of unstyled content when using dev server
6 | import store from "./store/store";
7 | import App from './components/App';
8 | import * as serviceWorker from './serviceWorker';
9 |
10 | store.dispatch("config/load", "ready");
11 |
12 | ReactDOM.render(
13 | ,
14 | document.getElementById('root')
15 | );
16 |
17 | serviceWorker.register();
--------------------------------------------------------------------------------
/src/schemas/config-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/schema#",
3 | "definitions": {
4 | "credentials": {
5 | "type": "object",
6 | "additionalProperties": {
7 | "type": "object",
8 | "required": ["clientId"],
9 | "properties": {
10 | "clientId": { "type": "string" },
11 | "clientSecret": { "type": "string" }
12 | }
13 | }
14 | },
15 | "mime_type_mappings": {
16 | "type": "object",
17 | "additionalProperties": {
18 | "type": "string",
19 | "maxLength": 4,
20 | "propertyNames": {
21 | "pattern": "^[-\\w]+/[-\\w+]+$"
22 | }
23 | }
24 | },
25 | "upload": {
26 | "type": "object",
27 | "anyOf": [
28 | { "required": ["whitelist"] },
29 | { "required": ["manifestUrl"] },
30 | { "required": ["name", "uploadUrl", "infoUrl"] }
31 | ],
32 | "properties": {
33 | "whitelist": {
34 | "type": "array",
35 | "items": {
36 | "type": "string",
37 | "pattern": "^https?:\/\/.+"
38 | }
39 | },
40 | "name": { "type": "string" },
41 | "label": { "type": "string" },
42 | "manifestUrl": {
43 | "type": "string",
44 | "format": "uri"
45 | },
46 | "uploadUrl": {
47 | "type": "string",
48 | "format": "uri"
49 | },
50 | "infoUrl": {
51 | "type": "string",
52 | "format": "uri"
53 | },
54 | "simulate": {"type": "boolean"},
55 | "successMessage": { "type": "string" },
56 | "continueLabel": { "type": "string" },
57 | "continueUrl": {
58 | "type": "string",
59 | "format": "uri"
60 | }
61 | }
62 | },
63 | "date_sort_elements": {
64 | "type": "object",
65 | "additionalProperties": {
66 | "type": "array",
67 | "items": { "type": "string" }
68 | },
69 | "propertyNames": {
70 | "pattern": "^[A-Z][A-Za-z]*$"
71 | }
72 | },
73 | "endpoint_lists": {
74 | "type": "object",
75 | "additionalProperties": { "$ref": "#/definitions/endpoint_list" }
76 | },
77 | "endpoint_list": {
78 | "type": "object",
79 | "required": [ "path" ],
80 | "properties": {
81 | "path": { "type": "string" },
82 | "defaults": { "$ref": "#/definitions/endpoint_list_default" }
83 | },
84 | "additionalProperties": false
85 | },
86 | "endpoint_list_default": {
87 | "type": "object",
88 | "properties": {
89 | "clientId": { "type": "string" },
90 | "clientSecret": { "type": "string" },
91 | "queryProfile": { "type": "string" },
92 | "isOpen": { "type": "boolean" },
93 | "scope": {
94 | "type": "array",
95 | "items": {
96 | "type": "string",
97 | "pattern": "^(([-\\w]+/(\\*\\.)?[*-\\w+]+(\\.\\w+)?)|profile|openid|fhirUser|launch|offline_access|online_access)$"
98 | }
99 | },
100 | "fhirEndpoint": {
101 | "type": "string",
102 | "format": "uri"
103 | },
104 | "patient": { "type": "string" }
105 | }
106 | },
107 | "query_profiles": {
108 | "type": "object",
109 | "additionalProperties": { "$ref": "#/definitions/query_profile" }
110 | },
111 | "query_profile": {
112 | "type": "object",
113 | "properties": {
114 | "title": { "type": "string" },
115 | "fhirVersion": {
116 | "type": "string",
117 | "enum": ["DSTU2", "STU3", "R2", "R3", "R4", "R5"]
118 | },
119 | "retryLimit": { "type": "integer" },
120 | "queries": {
121 | "type": "array",
122 | "minLength": 1,
123 | "items": { "$ref": "#/definitions/query" }
124 | }
125 | },
126 | "additionalProperties": false
127 | },
128 | "query": {
129 | "type": "object",
130 | "required": [ "path" ],
131 | "properties": {
132 | "title": { "type": "string" },
133 | "path": { "type": "string" },
134 | "skip": { "type": "boolean" },
135 | "notes" : {
136 | "type": "array",
137 | "items": { "type": "string" }
138 | },
139 | "retrieveReferences": {
140 | "type": ["array", "string"],
141 | "items": { "type": "string" }
142 | },
143 | "containReferences": {
144 | "type": ["array", "string"],
145 | "items": { "type": "string" }
146 | },
147 | "downloadAttachments": {
148 | "type": ["array", "string"],
149 | "items": { "type": "string" }
150 | },
151 | "pageLimit": { "type": "integer" }
152 | },
153 | "additionalProperties": false
154 | },
155 | "template_item": {
156 | "type": "object",
157 | "properties": {
158 | "path": {
159 | "type": ["array", "string"],
160 | "items": { "type": "string" }
161 | },
162 | "test": { "type": "string"},
163 | "id": { "type": "string"},
164 | "transform": { "type": "string"},
165 | "children": {
166 | "type": "array",
167 | "minLength": 1,
168 | "items": { "$ref": "#/definitions/template_item" }
169 | }
170 | }
171 | },
172 | "spreadsheet_template": {
173 | "additionalProperties": false,
174 | "type": "object",
175 | "required": ["name", "template"],
176 | "properties": {
177 | "name": { "type": "string" },
178 | "extends": {
179 | "type": "array",
180 | "items": { "type": "string"}
181 | },
182 | "sortBy": {
183 | "type": "array",
184 | "items": {
185 | "type": "object",
186 | "required": ["name", "dir"],
187 | "additionalProperties": false,
188 | "properties": {
189 | "name": { "type": "string" },
190 | "dir": {
191 | "type": "string",
192 | "enum": ["asc", "desc"]
193 | }
194 | }
195 | }
196 | },
197 | "template": {
198 | "type": "array",
199 | "minLength": 1,
200 | "items": { "$ref": "#/definitions/template_item" }
201 | }
202 | }
203 | },
204 | "spreadsheet_templates": {
205 | "type": "object",
206 | "additionalProperties": { "$ref": "#/definitions/spreadsheet_template" }
207 | }
208 | },
209 | "type": "object",
210 | "required": [ "queryProfiles" ],
211 | "additionalProperties": false,
212 | "properties": {
213 | "appName": {"type": "string"},
214 | "noCustomEndpoints": {"type": "boolean"},
215 | "credentials": { "$ref": "#/definitions/credentials" },
216 | "mimeTypeMappings": { "$ref": "#/definitions/mime_type_mappings" },
217 | "dateSortElements": { "$ref": "#/definitions/date_sort_elements" },
218 | "upload": { "$ref": "#/definitions/upload" },
219 | "redirectUri": {
220 | "type": ["string", "null"],
221 | "format": "uri"
222 | },
223 | "showWizard": { "type": "boolean" },
224 | "endpointLists": { "$ref": "#/definitions/endpoint_lists" },
225 | "queryProfiles": { "$ref": "#/definitions/query_profiles" },
226 | "spreadsheetTemplates": { "$ref": "#/definitions/spreadsheet_templates" },
227 | "warnOnPageNavigate": { "type": "boolean" },
228 | "ignoreState": { "type": "boolean" }
229 | }
230 | }
--------------------------------------------------------------------------------
/src/schemas/endpoint-list-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/schema#",
3 | "definitions": {
4 | "endpoint_item": {
5 | "type": "object",
6 | "anyOf": [
7 | { "required": ["fhirEndpoint", "name"] },
8 | { "required": ["FHIRPatientFacingURI", "OrganizationName"] }
9 | ],
10 | "properties": {
11 | "clientId": { "type": "string" },
12 | "clientSecret": { "type": "string" },
13 | "orgId": { "type": "string" },
14 | "queryProfile": { "type": "string" },
15 | "isOpen": { "type": "boolean" },
16 | "scope": {
17 | "type": "array",
18 | "items": {"type": "string"}
19 | },
20 | "fhirEndpoint": {
21 | "type": "string",
22 | "format": "uri"
23 | },
24 | "patient": { "type": "string" },
25 | "OrganizationName": { "type": "string" },
26 | "FHIRPatientFacingURI": {
27 | "type": "string",
28 | "format": "uri"
29 | }
30 | }
31 | }
32 | },
33 | "type": "object",
34 | "anyOf": [
35 | { "required": ["Entries"] },
36 | { "required": ["entry"] }
37 | ],
38 | "properties": {
39 | "Entries": {
40 | "type": "array",
41 | "items": { "$ref": "#/definitions/endpoint_item" }
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/src/schemas/samples/user-settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "providers": [
3 | {
4 | "name": "SMART Sandbox - Secure",
5 | "id": 2,
6 | "fhirEndpoint": "http://launch.smarthealthit.org/v/r2/sim/eyJrIjoiMSIsImoiOiIxIiwiYiI6ImM3ZWM5NTYwLTU4Y2QtNGEwOC04NzRiLTkxZTM0MjllZjFkNiJ9/fhir",
7 | "scope": [
8 | "patient/*.read",
9 | "launch/patient"
10 | ],
11 | "redirectUri": "http://localhost:3000/callback.html",
12 | "isOpen": true,
13 | "orgId": "http://launch.smarthealthit.org/v/r2/sim/eyJrIjoiMSIsImoiOiIxIiwiYiI6ImM3ZWM5NTYwLTU4Y2QtNGEwOC04NzRiLTkxZTM0MjllZjFkNiJ9/fhir",
14 | "queryProfile": "labs",
15 | "lastUpdated": null,
16 | "data": null,
17 | "patient": "123"
18 | }
19 | ],
20 | "githubConfig": {
21 | "token": "19bac35872878509e973cbf017249af977176434",
22 | "owner": "s4stest",
23 | "project": "healthrecord3"
24 | },
25 | "redirectUri": "http://localhost:3000/callback.html"
26 | }
--------------------------------------------------------------------------------
/src/schemas/upload-manifest-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/schema#",
3 | "type": "object",
4 | "required": [ "name", "uploadUrl", "infoUrl" ],
5 | "additionalProperties": false,
6 | "properties": {
7 | "name": { "type": "string" },
8 | "uploadUrl": {
9 | "type": "string",
10 | "format": "uri"
11 | },
12 | "infoUrl": {
13 | "type": "string",
14 | "format": "uri"
15 | },
16 | "successMessage": { "type": "string" },
17 | "continueLabel": { "type": "string" },
18 | "continueUrl": {
19 | "type": "string",
20 | "format": "uri"
21 | }
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/src/schemas/user-settings-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/schema#",
3 | "definitions": {
4 | "provider_secure": {
5 | "type": "object",
6 | "required": [
7 | "id", "fhirEndpoint", "orgId", "name",
8 | "queryProfile", "redirectUri", "scope"
9 | ],
10 | "properties": {
11 | "isOpen": {
12 | "type": "boolean",
13 | "enum": [ false ]
14 | },
15 | "id": { "type": ["string", "number"] },
16 | "name": { "type": "string" },
17 | "clientId": { "type": "string" },
18 | "clientSecret": { "type": "string" },
19 | "queryProfile": { "type": "string" },
20 | "orgId": { "type": "string" },
21 | "selected": {"type": "boolean" },
22 | "redirectUri": { "type": "string" },
23 | "scope": {
24 | "type": "array",
25 | "items": {
26 | "type": "string",
27 | "pattern": "^[-\\w]+/(\\*\\.)?[*-\\w+]+(\\.\\w+)?$"
28 | }
29 | },
30 | "fhirEndpoint": { "type": "string", "format": "uri" }
31 | }
32 | },
33 | "provider_open": {
34 | "type": "object",
35 | "required": [
36 | "id", "fhirEndpoint", "orgId", "name",
37 | "queryProfile", "redirectUri", "scope", "patient"
38 | ],
39 | "properties": {
40 | "isOpen": {
41 | "type": "boolean",
42 | "enum": [ true ]
43 | },
44 | "id": { "type": ["string", "number"] },
45 | "name": { "type": "string" },
46 | "clientId": { "type": "string" },
47 | "queryProfile": { "type": "string" },
48 | "orgId": { "type": "string" },
49 | "selected": {"type": "boolean" },
50 | "patient": { "type": "string" },
51 | "scope": {
52 | "type": "array",
53 | "items": {
54 | "type": "string",
55 | "pattern": "^[-\\w]+/(\\*\\.)?[*-\\w+]+(\\.\\w+)?$"
56 | }
57 | },
58 | "fhirEndpoint": { "type": "string", "format": "uri" }
59 | }
60 | },
61 | "providers": {
62 | "type": "array",
63 | "items": { "oneOf": [
64 | {"$ref": "#/definitions/provider_open"},
65 | {"$ref": "#/definitions/provider_secure"}
66 | ]}
67 | },
68 | "github_config": {
69 | "type": "object",
70 | "additionalProperties": false,
71 | "properties": {
72 | "token": { "type": "string" },
73 | "owner": { "type": "string" },
74 | "project": { "type": "string" }
75 | }
76 | }
77 | },
78 | "type": "object",
79 | "required": [ "redirectUri", "providers", "githubConfig"],
80 | "additionalProperties": false,
81 | "properties": {
82 | "providers": { "$ref": "#/definitions/providers" },
83 | "redirectUri": { "type": "string", "format": "uri" },
84 | "githubConfig": { "$ref": "#/definitions/github_config" }
85 | }
86 | }
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 |
36 | //NOTE: changed from CRA default per https://github.com/facebook/create-react-app/issues/5673
37 | const swUrl = `${process.env.PUBLIC_URL}/sw.js`;
38 |
39 | if (isLocalhost) {
40 | // This is running on localhost. Let's check if a service worker still exists or not.
41 | checkValidServiceWorker(swUrl, config);
42 |
43 | // Add some additional logging to localhost, pointing developers to the
44 | // service worker/PWA documentation.
45 | navigator.serviceWorker.ready.then(() => {
46 | console.log(
47 | 'This web app is being served cache-first by a service ' +
48 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
49 | );
50 | });
51 | } else {
52 | // Is not localhost. Just register service worker
53 | registerValidSW(swUrl, config);
54 | }
55 | });
56 | }
57 | }
58 |
59 | function registerValidSW(swUrl, config) {
60 | navigator.serviceWorker
61 | .register(swUrl)
62 | .then(registration => {
63 | registration.onupdatefound = () => {
64 | const installingWorker = registration.installing;
65 | if (installingWorker == null) {
66 | return;
67 | }
68 | installingWorker.onstatechange = () => {
69 | if (installingWorker.state === 'installed') {
70 | if (navigator.serviceWorker.controller) {
71 | // At this point, the updated precached content has been fetched,
72 | // but the previous service worker will still serve the older
73 | // content until all client tabs are closed.
74 | console.log(
75 | 'New content is available and will be used when all ' +
76 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
77 | );
78 |
79 | // Execute callback
80 | if (config && config.onUpdate) {
81 | config.onUpdate(registration);
82 | }
83 | } else {
84 | // At this point, everything has been precached.
85 | // It's the perfect time to display a
86 | // "Content is cached for offline use." message.
87 | console.log('Content is cached for offline use.');
88 |
89 | // Execute callback
90 | if (config && config.onSuccess) {
91 | config.onSuccess(registration);
92 | }
93 | }
94 | }
95 | };
96 | };
97 | })
98 | .catch(error => {
99 | console.error('Error during service worker registration:', error);
100 | });
101 | }
102 |
103 | function checkValidServiceWorker(swUrl, config) {
104 | // Check if the service worker can be found. If it can't reload the page.
105 | fetch(swUrl)
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready.then(registration => {
134 | registration.unregister();
135 | });
136 | }
137 | }
--------------------------------------------------------------------------------
/src/smart/smart-core.js:
--------------------------------------------------------------------------------
1 | import fhir from "./fhir";
2 |
3 | const authEndpointErr = "Authorization endpoint or token endpoint not found";
4 |
5 | function getPath(obj, path = "") {
6 | path = path.trim();
7 | if (!path) return obj;
8 | return path.split(".").reduce(
9 | (out, key) => out ? out[key] : undefined,
10 | obj
11 | );
12 | }
13 |
14 | function getAuthEndpoints(fhirEndpoint) {
15 | const url = fhirEndpoint.replace(/\/*$/, "/");
16 | return new Promise( (resolve, reject) => {
17 | getWellKnownEndpoints(url)
18 | .then( endpoints => resolve(endpoints) )
19 | .catch( err => {
20 | getCapabilityEndpoints(url)
21 | .then( endpoints => resolve(endpoints) )
22 | .catch( err => reject( authEndpointErr ) )
23 | });
24 | });
25 | }
26 |
27 | function getWellKnownEndpoints(baseUrl) {
28 | const url = baseUrl + ".well-known/smart-configuration";
29 | return fetch(url)
30 | .then( response => {
31 | if (!response.ok)
32 | throw new Error(`HTTP ${response.status} - ${response.statusText}`);
33 | return response;
34 | })
35 | .then( data => data.json() )
36 | .then( json => {
37 | let endpoints = {
38 | registrationEndpoint : json.registration_endpoint,
39 | authorizationEndpoint : json.authorization_endpoint,
40 | tokenEndpoint : json.token_endpoint
41 | };
42 | if (!endpoints.authorizationEndpoint || !endpoints.tokenEndpoint) {
43 | throw authEndpointErr;
44 | } else {
45 | return endpoints;
46 | }
47 | })
48 | .catch( e => { throw authEndpointErr });
49 | }
50 |
51 | function getCapabilityEndpoints(baseUrl) {
52 | const nsUri = "http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris";
53 | const url = baseUrl + "metadata";
54 |
55 | return fhir.fetchFHIR(url)
56 | .then( json => {
57 | const extensions = (getPath(json || {}, "rest.0.security.extension") || [])
58 | .filter(e => e.url === nsUri)
59 | .map(o => o.extension)[0];
60 |
61 | let endpoints = {};
62 | if (extensions) {
63 | extensions.forEach(ext => {
64 | if (ext.url === "register")
65 | endpoints.registrationEndpoint = ext.valueUri;
66 | if (ext.url === "authorize")
67 | endpoints.authorizationEndpoint = ext.valueUri;
68 | if (ext.url === "token")
69 | endpoints.tokenEndpoint = ext.valueUri;
70 | });
71 | }
72 | if (!endpoints.authorizationEndpoint || !endpoints.tokenEndpoint) {
73 | throw authEndpointErr;
74 | } else{
75 | return endpoints;
76 | }
77 | })
78 | .catch( e => { throw authEndpointErr });
79 | }
80 |
81 | function generateStateParam(targetLen=32) {
82 | const charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
83 | let result = [];
84 | while (targetLen--)
85 | result.push(charSet.charAt(Math.floor(Math.random() * charSet.length)));
86 | return result.join("");
87 | }
88 |
89 | function buildAuthorizeUrl(authEndpoint, fhirEndpoint, state, scope, redirectUri, clientId, clientSecret, launch) {
90 | if (Array.isArray(scope)) scope = scope.join(" ");
91 |
92 | let params = [
93 | "response_type=code",
94 | "client_id=" + encodeURIComponent(clientId),
95 | "scope=" + encodeURIComponent(scope),
96 | "redirect_uri=" + encodeURIComponent(redirectUri),
97 | "aud=" + encodeURIComponent(fhirEndpoint),
98 | "state=" + state
99 | ];
100 |
101 | if (launch)
102 | params.push("launch=" + encodeURIComponent(launch));
103 | if (clientSecret)
104 | params.push("client_secret=" + encodeURIComponent(clientSecret));
105 |
106 | return authEndpoint + "?" + params.join("&");
107 | }
108 |
109 | function exchangeCodeForToken(tokenEndpoint, code, redirectUri, clientId, clientSecret) {
110 | let config = {
111 | method: "post",
112 | headers: {"content-type": "application/x-www-form-urlencoded"},
113 | // mode: "cors",
114 | body: [
115 | "grant_type=authorization_code",
116 | "code=" + encodeURIComponent(code),
117 | "redirect_uri=" + encodeURIComponent(redirectUri)
118 | ].join("&")
119 | }
120 |
121 | if (clientSecret) {
122 | config.headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
123 | } else {
124 | config.body += "&client_id=" + encodeURIComponent(clientId);
125 | }
126 |
127 | return fetch(tokenEndpoint, config)
128 |
129 | }
130 |
131 | function refreshToken(tokenEndpoint, refreshToken) {
132 | return fetch(tokenEndpoint, {
133 | method: "post",
134 | headers: {"content-type": "application/x-www-form-urlencoded"},
135 | mode: "cors",
136 | body: [
137 | "grant_type=refresh_token",
138 | "refresh_token=" + encodeURIComponent(refreshToken)
139 | ].join("&")
140 | })
141 | }
142 |
143 | function buildQs(fhirParams) {
144 | return "?" + Object.keys(fhirParams).map( k => {
145 | const value = Array.isArray(fhirParams[k]) ? fhirParams[k].join(",") : fhirParams[k];
146 | return k + "=" + encodeURIComponent(value);
147 | }).join("&");
148 | }
149 |
150 | export default {
151 | getWellKnownEndpoints, getCapabilityEndpoints, getAuthEndpoints, buildQs,
152 | buildAuthorizeUrl, exchangeCodeForToken, refreshToken, generateStateParam
153 | }
--------------------------------------------------------------------------------
/src/smart/smart-popup.js:
--------------------------------------------------------------------------------
1 | import SMART from "./smart-core";
2 |
3 | export default class SmartPopup {
4 |
5 | constructor(provider) {
6 | this.provider = provider;
7 | this.authWindow = window.open('', '_blank');
8 | this.authWindow.document.write('Loading...');
9 | }
10 |
11 | parseQs(qs) {
12 | const pairs = qs.split('&');
13 | let result = {};
14 | pairs.forEach( pair => {
15 | pair = pair.split('=');
16 | result[pair[0]] = decodeURIComponent(pair[1]||"");
17 | });
18 | return result;
19 | }
20 |
21 | cancel() {
22 | if (this.reject) {
23 | clearInterval(this.popupPoll);
24 | window.removeEventListener("message", this.messageListener);
25 | this.authWindow.close();
26 | this.reject(new Error("cancelled"))
27 | }
28 | }
29 |
30 | authorize(authEndpoints, ignoreState) {
31 | return new Promise( (resolve, reject) => {
32 | this.reject = reject;
33 | const stateParam = SMART.generateStateParam();
34 |
35 | this.popupPoll = setInterval( () => {
36 | if (this.authWindow.closed) {
37 | clearInterval(this.popupPoll);
38 | window.removeEventListener("message", this.messageListener);
39 | reject(new Error("Login window closed by user"));
40 | }
41 | }, 500);
42 |
43 |
44 | const receiveMessage = (event) => {
45 | if (event.origin !== window.location.origin) return;
46 | //block react dev tools messages
47 | if (typeof event.data !== "string") return;
48 | clearInterval(this.popupPoll);
49 | window.removeEventListener("message", this.messageListener);
50 | this.authWindow.close();
51 |
52 | const data = this.parseQs(event.data);
53 |
54 | if (data.error) {
55 | reject(data.error);
56 | } else if (data.code && ignoreState !== true && data.state !== stateParam) {
57 | reject("Invalid state parameter returned by server");
58 | } else if (data.code) {
59 | SMART.exchangeCodeForToken(
60 | authEndpoints.tokenEndpoint,
61 | data.code,
62 | this.provider.redirectUri,
63 | this.provider.clientId,
64 | this.provider.clientSecret
65 | )
66 | .then( data => data.json() )
67 | .then( data => {
68 | if (data.access_token) {
69 | resolve(data);
70 | } else {
71 | console.log(data);
72 | reject("No access token in server token response");
73 | }
74 | })
75 | } else {
76 | reject("Failed to retrieve code parameter");
77 | }
78 | };
79 |
80 | const url = SMART.buildAuthorizeUrl(
81 | authEndpoints.authorizationEndpoint,
82 | this.provider.fhirEndpoint,
83 | stateParam,
84 | this.provider.scope,
85 | this.provider.redirectUri,
86 | this.provider.clientId
87 | );
88 |
89 | this.messageListener =
90 | window.addEventListener("message", receiveMessage, false);
91 |
92 | this.authWindow.location.href = url;
93 | });
94 | }
95 |
96 | }
--------------------------------------------------------------------------------
/src/store/csv-writer.js:
--------------------------------------------------------------------------------
1 | export default (rows) => {
2 | const processRow = (row) => {
3 | return row.map( rawValue => {
4 | let value = rawValue === null ? "" : rawValue.toString();
5 | if (rawValue instanceof Date)
6 | value = rawValue.toLocaleString();
7 | value = value.replace(/"/g, '""');
8 | if (value.search(/("|,|\n)/g) >= 0)
9 | value = '"' + value + '"';
10 | return value;
11 | }).join(",");
12 | };
13 |
14 | const csv = rows.map(processRow).join("\n");
15 | return new Blob([csv], { type: 'text/csv;charset=utf-8;' });
16 | }
--------------------------------------------------------------------------------
/src/store/fhir-loader.js:
--------------------------------------------------------------------------------
1 | import Smart from "../smart/smart-core";
2 | import SmartPopup from "../smart/smart-popup";
3 | import FHIR from "../smart/fhir";
4 |
5 | export default class FhirLoader {
6 |
7 | resetAbortController() {
8 | this.controller = new AbortController();
9 | this.signal = this.controller.signal;
10 | }
11 |
12 | cancel() {
13 | if (this.popup) {
14 | this.popup.cancel();
15 | } else {
16 | this.controller.abort();
17 | }
18 | }
19 |
20 | mergeAndDeDupeData(data) {
21 | let uniqueUrls = {};
22 | let files = [];
23 | let entry = [];
24 | let errorLog = [];
25 | let errorCount = 0;
26 | data.forEach( queryResult => {
27 | queryResult.entry.forEach( ent => {
28 | //prevent duplicates
29 | if (uniqueUrls[ent.fullUrl]) return;
30 | uniqueUrls[ent.fullUrl] = true;
31 | entry.push(ent)
32 | })
33 | files = files.concat(queryResult.files || []);
34 | errorLog = errorLog.concat(queryResult.errorLog || []);
35 | errorCount += queryResult.errorCount || 0;
36 | })
37 | return {entry, files, errorLog, errorCount}
38 | }
39 |
40 | getFHIR(provider, queryProfile, context, allowErrors=true, mimeTypeMappings={}, retryLimit, statusCallback) {
41 | const patientId = context ? context.patient : provider.patient;
42 |
43 | const fetchQuery = (query) => {
44 | let params = {...query.params}
45 | Object.keys(params).forEach(k => {
46 | if (Array.isArray(params[k])) params[k] = params[k].join(",");
47 | params[k] = params[k].toString().replace("{{patientId}}", patientId);
48 | });
49 | const path = query.path.replace(/\{patientId\}/g, patientId);
50 |
51 | return FHIR.getResourcesByQuery({
52 | fhirEndpoint: provider.fhirEndpoint,
53 | query: {...query, params, path, fhirVersion: queryProfile.fhirVersion},
54 | token: context ? context.access_token : null,
55 | retryLimit,
56 | allowErrors,
57 | statusCallback,
58 | signal: this.signal
59 | })
60 | .then( data => {
61 | if (!query.containReferences) return data;
62 |
63 | return FHIR.followAndEmbedReferences({
64 | entry: data.entry,
65 | paths: query.containReferences,
66 | fhirEndpoint: provider.fhirEndpoint,
67 | fhirVersion: queryProfile.fhirVersion,
68 | token: context ? context.access_token : null,
69 | retryLimit,
70 | allowErrors,
71 | statusCallback,
72 | signal: this.signal
73 | }).then( result => {
74 | data.errorLog = data.errorLog || [];
75 | data.errorLog = data.errorLog.concat(result.errorLog);
76 | data.errorCount = data.errorCount || 0;
77 | data.errorCount += result.errorCount;
78 | data.entry = result.entry;
79 | return data;
80 | });
81 |
82 | })
83 | .then( data => {
84 | if (!query.retrieveReferences) return data;
85 |
86 | return FHIR.followReferences({
87 | entry: data.entry,
88 | paths: query.retrieveReferences,
89 | fhirEndpoint: provider.fhirEndpoint,
90 | fhirVersion: queryProfile.fhirVersion,
91 | token: context ? context.access_token : null,
92 | retryLimit,
93 | allowErrors,
94 | statusCallback,
95 | signal: this.signal
96 | }).then( result => {
97 | data.errorLog = data.errorLog || [];
98 | data.errorLog = data.errorLog.concat(result.errorLog);
99 | data.errorCount = data.errorCount || 0;
100 | data.errorCount += result.errorCount;
101 | data.entry = data.entry.concat(result.entry);
102 | return data;
103 | });
104 | })
105 | .then( data => {
106 | if (!query.downloadAttachments) return data;
107 | return FHIR.findAndDownloadAttachments({
108 | entry: data.entry,
109 | paths: query.downloadAttachments,
110 | fhirEndpoint: provider.fhirEndpoint,
111 | fhirVersion: queryProfile.fhirVersion,
112 | token: context ? context.access_token : null,
113 | retryLimit,
114 | mimeTypeMappings,
115 | allowErrors: true,
116 | statusCallback,
117 | signal: this.signal
118 | }).then(result => {
119 | data.errorLog = data.errorLog || [];
120 | data.errorLog = data.errorLog.concat(result.errorLog || []);
121 | data.errorCount = data.errorCount || 0;
122 | data.errorCount += result.errorCount;
123 | data.files = result.files;
124 | data.entry = FHIR.attachmentsToFilenames({
125 | entry: data.entry, files: result.files, paths: query.downloadAttachments
126 | });
127 | return data;
128 | });
129 | })
130 | }
131 |
132 | this.resetAbortController();
133 |
134 | //sequential query
135 | // return queryProfile.queries.reduce((promiseChain, currentQuery) => {
136 | // return promiseChain.then( chainResults =>
137 | // fetchQuery(currentQuery) .then( currentResult =>
138 | // [ ...chainResults, currentResult ]
139 | // )
140 | // );
141 | // }, Promise.resolve([]))
142 | // .then( this.mergeAndDeDupeData )
143 |
144 | //parallel (moderated by browser)
145 | return Promise.all( queryProfile.queries.filter(q => q.skip !== true).map(fetchQuery) )
146 | .then( this.mergeAndDeDupeData )
147 | }
148 |
149 | authAndGetFHIR(provider, ignoreState) {
150 | //make available to cancel function
151 | this.popup = new SmartPopup(provider);
152 | return Smart.getAuthEndpoints(provider.fhirEndpoint)
153 | .then(authEndpoints => {
154 | return this.popup.authorize(authEndpoints, ignoreState);
155 | })
156 | .then(context => {
157 | this.popup = null;
158 | return context;
159 | }).catch(e => {
160 | this.popup = null;
161 | throw e;
162 | });
163 |
164 | }
165 |
166 | }
167 |
--------------------------------------------------------------------------------
/src/store/file-exporter.js:
--------------------------------------------------------------------------------
1 | import tv4 from "tv4";
2 | import matchUrl from 'match-url-wildcard';
3 | import configSchema from "../schemas/upload-manifest-schema.json";
4 | import _ from "lodash";
5 |
6 | export default class FileExporter {
7 |
8 | cancel() {
9 | this.controller.abort();
10 | }
11 |
12 | resetCancelled() {
13 | this.controller = new AbortController();
14 | this.signal = this.controller.signal;
15 | }
16 |
17 | isValidUrl(url, whitelist) {
18 | return _.find(whitelist, rule => matchUrl(url, rule)) !== undefined;
19 | }
20 |
21 | getManifest(url) {
22 | this.resetCancelled();
23 | const config = {
24 | signal: this.signal,
25 | accept: "application/json"
26 | }
27 | return fetch(url, config)
28 | .then( response => {
29 | if (!response.ok)
30 | throw new Error(`HTTP ${response.status} - ${response.statusText}`);
31 | return response;
32 | })
33 | .then( data => data.json() )
34 | .then( data => {
35 | tv4.validate(data, configSchema);
36 | if (tv4.error) {
37 | if (console) console.log(tv4.error);
38 | throw new Error(url + ": " + tv4.error.message);
39 | }
40 | return data;
41 | });
42 | }
43 |
44 | putFile(url, file) {
45 | this.resetCancelled()
46 | const config = {
47 | signal: this.signal,
48 | method: "PUT",
49 | headers: {
50 | "Content-Type": "application/octet-stream"
51 | },
52 | body: file
53 | }
54 | return fetch(url, config)
55 | .then( response => {
56 | if (!response.ok)
57 | throw new Error(`HTTP ${response.status} - ${response.statusText}`);
58 | return response;
59 | })
60 | .then( response => {
61 | const contentType = response.headers.get("content-type");
62 | if (contentType && contentType.indexOf("application/json") !== -1) {
63 | return response.json();
64 | } else {
65 | return {};
66 | }
67 | })
68 | .then( data => {
69 | if (data.error) throw new Error(data.error);
70 | })
71 | .catch( error => {
72 | console.log(error);
73 | if (error.name === "AbortError") {
74 | throw(error);
75 | } else {
76 | throw new Error(`An error occurred sending your data: ${error.message}`);
77 | }
78 | })
79 | }
80 |
81 |
82 | }
--------------------------------------------------------------------------------
/src/store/github-api.js:
--------------------------------------------------------------------------------
1 | const defaultBaseUrl = "https://api.github.com";
2 |
3 | function callApi(config, path, method="GET", jsonPayload) {
4 | const reqConfig = {
5 | headers: {"Authorization": `Bearer ${config.token}`},
6 | method
7 | }
8 | if (jsonPayload) reqConfig.body = JSON.stringify(jsonPayload);
9 |
10 | const url = `${config.baseUrl||defaultBaseUrl}/repos/${config.owner}/${config.project}/${path}`
11 | return fetch(url, reqConfig).then( response => {
12 | if (response.ok) {
13 | return response.json();
14 | } else if (response.status === 404) {
15 | throw new Error("Repository not found");
16 | } else if (response.stats === 500) {
17 | throw new Error(response.message || "Github API returned a 500 error");
18 | } else {
19 | return response.json().then( (json) => {
20 | throw new Error(json.message);
21 | });
22 | }
23 | })
24 | }
25 |
26 | function getLastCommit(config, branchName="master") {
27 | return callApi(config, `git/refs/heads/${branchName}`)
28 | //special handling for uninitialized repository (repo without any files)
29 | .catch( e => {
30 | if (e.message === "Git Repository is empty.")
31 | e.message = "Git repository is empty. Please initialize the repository and try again."
32 | throw e;
33 | });
34 | }
35 |
36 | function getTree(config, commit) {
37 | return callApi(config, `git/trees/${commit.object.sha}`)
38 | .then( tree => {
39 | if (tree.truncated)
40 | throw new Error("Unable to load entire head tree - too many files");
41 | return tree;
42 | })
43 | //handle 404 for when tree has been deleted and repo is empty
44 | .catch( e => {
45 | if (e.message === "Repository not found") {
46 | return {tree: []}
47 | }
48 | })
49 | }
50 |
51 | function createBlob(config, blob) {
52 | return callApi(config, "git/blobs", "POST", blob)
53 | }
54 |
55 | function createTree(config, tree) {
56 | return callApi(config, "git/trees", "POST", tree);
57 | }
58 |
59 | function commitTree(config, tree, message, lastCommit, branchName="master") {
60 | return callApi(config, "git/commits", "POST", {
61 | message,
62 | tree: tree.sha,
63 | parents: [lastCommit.object.sha]
64 | }).then( commit => {
65 | return callApi(config, `git/refs/heads/${branchName}`,"PATCH", {
66 | sha: commit.sha
67 | })
68 | })
69 | }
70 |
71 | function commitTreeToPullRequest(config, tree, message, lastCommit, title, newBranchName, baseBranch="master", bodyText) {
72 | return callApi(config, "git/commits", "POST", {
73 | message,
74 | tree: tree.sha,
75 | parents: [lastCommit.object.sha]
76 | }).then( commit => {
77 | return callApi(config, "git/refs", "POST", {
78 | ref: `refs/heads/${newBranchName}`,
79 | sha: commit.sha
80 | });
81 | }).then( () => {
82 | return callApi(config, "pulls", "POST", {
83 | title: title,
84 | body: bodyText,
85 | head: newBranchName,
86 | base: baseBranch,
87 | maintainer_can_modify: true
88 | })
89 | })
90 | }
91 |
92 | export default {
93 | getLastCommit, getTree, createBlob,
94 | createTree, commitTree, commitTreeToPullRequest
95 | }
--------------------------------------------------------------------------------
/src/store/github-exporter.js:
--------------------------------------------------------------------------------
1 | import gh from "./github-api";
2 | import sanitizeFilename from "sanitize-filename";
3 | import _ from "lodash";
4 |
5 | export default class GithubExporter {
6 |
7 | cancel() {
8 | this.canceled = true;
9 | }
10 |
11 | blobToBase64(blob) {
12 | return new Promise( (resolve, reject) => {
13 | let reader = new FileReader();
14 | reader.onload = () => {
15 | const dataUrl = reader.result;
16 | const base64 = dataUrl.split(',')[1];
17 | resolve(base64);
18 | };
19 | reader.onerror = reject;
20 | reader.readAsDataURL(blob);
21 | });
22 | }
23 |
24 | buildProviderTree(provider, folder) {
25 | const createBlob = (content, fileName, encoding="utf-8") => {
26 | if (this.canceled) throw new Error("Canceled");
27 |
28 | if (this.statusCb)
29 | this.statusCb("Adding " + folder + "/" + fileName);
30 |
31 | return gh.createBlob(this.config, {content, encoding})
32 | .then( blob => {
33 | return {
34 | sha: blob.sha,
35 | path: folder + "/" + fileName,
36 | mode: "100644", //file
37 | type: "blob"
38 | }
39 | })
40 | };
41 |
42 | const buildResourceTree = (bundles) => {
43 | return Promise.all(
44 | _.map( bundles, (bundle, resourceType) => {
45 | return createBlob(
46 | JSON.stringify(bundle, null, 2),
47 | resourceType+".json"
48 | )
49 | })
50 | )
51 | }
52 |
53 | const buildFileTree = (files) => {
54 | return Promise.all(
55 | _.map( files, file => {
56 | return this.blobToBase64(file.blob)
57 | .then( base64 => {
58 | return createBlob(base64, file.fileName, "base64")
59 | })
60 | })
61 | )
62 | }
63 |
64 | const buildErrorLogTree = (errorLog) => {
65 | return !errorLog.length
66 | ? Promise.resolve([])
67 | : createBlob(
68 | JSON.stringify(errorLog, null, 2),
69 | "error_log.json"
70 | ).then( blob => [blob] );
71 | }
72 |
73 | //TODO: share bundle generation code with ZipExporter
74 | const bundles = _.chain(provider.data.entry)
75 | .groupBy(e => e.resource.resourceType)
76 | .mapValues(entry => {
77 | return {
78 | entry: _.map(entry, e => ({fullUrl: e.fullUrl, resource: e.resource}) ),
79 | total: entry.length,
80 | type:"collection",
81 | resourceType: "Bundle"
82 | }
83 | }).value();
84 |
85 | return Promise.all([
86 | buildResourceTree(bundles),
87 | buildFileTree(provider.data.files),
88 | buildErrorLogTree(provider.data.errorLog)
89 | ]).then( trees => _.flatten(trees) );
90 |
91 | }
92 |
93 | buildTree(baseTree, allProviders) {
94 | const providers = _.filter( allProviders, p => {
95 | return p.selected && p.data && p.data.entry && (p.data.entry.length || p.data.errorLog.length)
96 | })
97 | const folders = _.map(providers, p => {
98 | return sanitizeFilename(p.name);
99 | })
100 | const tree = _.filter(baseTree.tree, item => {
101 | const clear = folders.find( f => item.type === "tree" && item.path === f);
102 | return clear ? false : true;
103 | });
104 | return Promise.all(
105 | _.map( providers, (provider, i) => {
106 | return this.buildProviderTree(provider, folders[i]);
107 | })
108 | ).then( newTrees => {
109 | return {
110 | ...baseTree,
111 | tree: tree.concat(_.flatten(newTrees))
112 | }
113 | })
114 | }
115 |
116 | prepareCommit(providers) {
117 | let lastCommit;
118 | if (this.statusCb)
119 | this.statusCb("Retrieving repository information");
120 |
121 | return gh.getLastCommit(this.config)
122 | .then( commit => {
123 | lastCommit = commit;
124 | if (this.canceled) throw new Error("canceled");
125 | return gh.getTree(this.config, commit);
126 | })
127 | .then( commit => {
128 | if (this.canceled) throw new Error("canceled");
129 | return this.buildTree(commit, providers)
130 | })
131 | .then( tree => {
132 | if (this.canceled) throw new Error("canceled");
133 | return gh.createTree(this.config, tree)
134 | })
135 | .then( tree => ({ tree, lastCommit }) );
136 | }
137 |
138 | export(providers, githubConfig, statusCb, branchName) {
139 | this.config = githubConfig;
140 | this.canceled = false;
141 | this.statusCb = statusCb;
142 | return githubConfig.pullRequest
143 | ? this.commitPullRequest(providers, branchName)
144 | : this.commitToMaster(providers);
145 | }
146 |
147 | commitToMaster(providers) {
148 | return this.prepareCommit(providers)
149 | .then( ({tree, lastCommit}) => {
150 |
151 | if (this.canceled) throw new Error("canceled");
152 |
153 | if (this.statusCb)
154 | this.statusCb("Committing files");
155 |
156 | return gh.commitTree(
157 | this.config,
158 | tree,
159 | "ProcureBot Commit",
160 | lastCommit
161 | ).then( () => {
162 | if (this.statusCb)
163 | this.statusCb("Data has been committed to master branch");
164 | });
165 | });
166 | }
167 |
168 | commitPullRequest(providers, branchName) {
169 | if (!branchName)
170 | branchName = "Procure/" +
171 | (new Date().toISOString()).replace(/:/g, "-");
172 |
173 | return this.prepareCommit(providers)
174 | .then( ({tree, lastCommit}) => {
175 |
176 | if (this.canceled) throw new Error("canceled");
177 |
178 | if (this.statusCb)
179 | this.statusCb("Creating pull request");
180 |
181 | return gh.commitTreeToPullRequest(
182 | this.config,
183 | tree,
184 | "ProcureBot commit",
185 | lastCommit,
186 | "ProcureBot pull request",
187 | branchName
188 | ).then( pull => {
189 | if (this.statusCb)
190 | this.statusCb("A new pull request has been created", pull.html_url);
191 | })
192 | });
193 | }
194 |
195 | }
--------------------------------------------------------------------------------
/src/store/initial-state.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | uiState: {mode: "loading"},
3 | spreadsheetTemplates: {},
4 | isDirty: false,
5 | nonModalUi: {},
6 | queryProfile: {},
7 | mimeTypeMappings: {},
8 | githubConfig: {},
9 | upload: {},
10 | providers: []
11 | }
--------------------------------------------------------------------------------
/src/store/merge-objects.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | //matching object keys merge properties
4 | //if key has an underscore, then object is replaced
5 | //arrays items merge by id, appending otherwise
6 | //array items with an underscore property replace all properties
7 | //objects item with an underscore property are deleted
8 | function merge(templates) {
9 |
10 | const mergeArray = (base, override) => {
11 | _.each(override, overrideItem => {
12 | const baseIndex = overrideItem.id
13 | ? _.findIndex(base, b => b.id === overrideItem.id)
14 | : -1;
15 | if (overrideItem._) {
16 | if (baseIndex > -1) base.splice(baseIndex, 1);
17 | } else if (baseIndex > -1) {
18 | base[baseIndex] = mergeObject(base[baseIndex], overrideItem);
19 | } else {
20 | base.push(overrideItem)
21 | }
22 | });
23 | return base;
24 | }
25 |
26 | const mergeObject = (base, override) => {
27 | _.each( _.keys(override), key => {
28 | if (key[0] === "_") {
29 | base[key.slice(1)] = override[key];
30 | } else if (base[key] === undefined) {
31 | base[key] = override[key];
32 | } else if (_.isPlainObject(override[key]) && override[key]._) {
33 | delete base[key];
34 | } else if (_.isPlainObject(override[key])) {
35 | base[key] = mergeObject(base[key], override[key]);
36 | } else if (_.isArray(base[key]) && _.isArray(override[key])) {
37 | base[key] = mergeArray(base[key], override[key]);
38 | } else {
39 | base[key] = override[key];
40 | }
41 | })
42 | return base;
43 | }
44 |
45 | let base = JSON.parse(JSON.stringify(templates[0]));
46 | _.each( templates, (template, i) => {
47 | if (i === 0) return;
48 | template = JSON.parse(JSON.stringify(template));
49 | if (_.isPlainObject(base) && _.isPlainObject(template)) {
50 | base = mergeObject(base, template)
51 | } else if (_.isArray(base) && _.isArray(template)) {
52 | base = mergeArray(base, template);
53 | } else {
54 | base = template;
55 | }
56 | })
57 | return base;
58 |
59 | }
60 |
61 | export default { merge };
--------------------------------------------------------------------------------
/src/store/spreadsheet-exporter.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 | import XlsxPopulate from "xlsx-populate";
3 | import saveAs from "file-saver";
4 | import sanitizeFilename from "sanitize-filename";
5 | import mergeObjects from "./merge-objects.js";
6 | import csvWriter from "./csv-writer";
7 |
8 | const defaultHelperFns = {
9 | validateExists: v => {
10 | return v !== undefined
11 | },
12 | validateCode: (v, template) => {
13 | if (!v) return false;
14 | if (!Array.isArray(v)) v = [v];
15 | return v.find( c => c.code === template.target ) ? true : false;
16 | },
17 | validateCodeableConcept: (v, template) => {
18 | if (!v) return false;
19 | if (!Array.isArray(v)) v = [v];
20 | return v.find( cc => {
21 | return cc.coding.find( c => c.code === template.target )
22 | }) ? true : false;
23 | },
24 | validateValue: (v, template) => {
25 | return v === template.target;
26 | },
27 | getHelperData: (v, template, helperData) => {
28 | return helperData[template.helperDataField];
29 | },
30 | stringifyCodings: v => {
31 | if (!v) return v;
32 | if (!Array.isArray(v)) v = [v];
33 | return v
34 | .map( c => c.system + "|" + c.code )
35 | .join(" ");
36 | },
37 | parseDateForExcel: (v, template, helperData) => {
38 | return (v && helperData.format !== "csv")
39 | ? new Date(v)
40 | : v;
41 | }
42 | }
43 |
44 | function flatten(element, template, helperData={}, helperFns) {
45 | helperFns = helperFns || defaultHelperFns;
46 | let rows = [];
47 | _.each(template, field => {
48 |
49 | const mergeRows = (rowsToMerge) => {
50 | if (rows.length === 0) rows = [{}];
51 | let newRows = [];
52 | _.each(rowsToMerge, mergeRow => {
53 | if (!_.isArray(mergeRow)) mergeRow = [mergeRow];
54 | _.each(mergeRow, mergeRowItem => {
55 | _.each(rows, row => {
56 | newRows.push({ ...row, ...mergeRowItem});
57 | });
58 | });
59 | });
60 | rows = newRows;
61 | }
62 |
63 | if (!_.isArray(field.path))
64 | field.path = [field.path];
65 |
66 | let data = _.chain(element)
67 | .at(element, field.path)
68 | .filter( v => v !== undefined )
69 | .first().value();
70 |
71 | //allow a fallback path. Note: this will always be evaluated last regardless of array position
72 | if (!data && field.path.indexOf("*") > -1)
73 | data = element;
74 |
75 | if (field.transform)
76 | data = helperFns[field.transform](data, field, helperData);
77 |
78 | const valid =
79 | field.test === undefined || helperFns[field.test](data, field, helperData);
80 |
81 | // console.log(valid, field, data)
82 |
83 | if (!valid) {
84 | rows = null;
85 | return false;
86 | } else if (field.children) {
87 | if (!_.isArray(data)) data = [data];
88 | const childRows = _.map( data, childElement => {
89 | return flatten(childElement, field.children, helperData, helperFns);
90 | });
91 | //check if all children were valid and bail if they weren't
92 | if (childRows.indexOf(null) > -1) {
93 | rows = null;
94 | return false;
95 | } else {
96 | mergeRows(childRows);
97 | }
98 | } else if (field.name) {
99 | if (!_.isArray(data)) data = [data];
100 | const rowsToMerge = data.map( item => ({[field.name]: item}) );
101 | mergeRows(rowsToMerge);
102 | }
103 | })
104 | return rows;
105 | }
106 |
107 | function flattenProviders(providers, template, helperData={}, helperFns) {
108 | return _.chain(providers)
109 | .filter( p => p.selected && p.data && p.data.entry && p.data.entry.length > 0)
110 | .map( p => {
111 | return _.chain(p.data.entry).map( e => {
112 | const providerHelperData = {...helperData, source: p.name};
113 | return flatten(e.resource, template, providerHelperData, helperFns);
114 | }).flatten().value()
115 | })
116 | .flatten()
117 | .filter( f => f !== null )
118 | .value();
119 | }
120 |
121 | function getTemplateColumns(template) {
122 | let cols = [];
123 | const arrayToCols = templateArray => {
124 | _.each(templateArray, field => {
125 | if (field.name) cols.push(field.name);
126 | if (field.children) arrayToCols(field.children);
127 | });
128 | }
129 | arrayToCols(template || template.template);
130 | return cols;
131 | }
132 |
133 | // JS Excel libraries with browser compatibility (others are node.js only)
134 | // https://github.com/SheetJS/js-xlsx (Apache2, very robust, support for .xls, formatting requires commercial license)
135 | // https://github.com/dtjohnson/xlsx-populate (MIT, good option)
136 | // https://github.com/exceljs/exceljs (MIT, good option, slightly more dependencies)
137 | // https://github.com/egeriis/zipcelx (MIT, very light weight, currently no date support)
138 | // https://en.wikipedia.org/wiki/Microsoft_Office_XML_formats#Excel_XML_Spreadsheet_example (Excel 2003, seems to not require zip)
139 |
140 | function exportFlatDataCSV(flatData, template, name) {
141 | const columns = getTemplateColumns(template);
142 | const data = flatData.map( row => {
143 | return columns.map( col => row[col] );
144 | });
145 | const blob = csvWriter([columns].concat(data));
146 | return new Promise( (resolve) => {
147 | saveAs(blob, sanitizeFilename(name) + ".csv")
148 | resolve();
149 | })
150 | }
151 |
152 | function exportFlatDataExcel(flatData, template, name) {
153 | const columns = getTemplateColumns(template);
154 | let dateCols = [];
155 | const data = flatData.map( row => {
156 | return columns.map( (col, i) => {
157 | if (row[col] instanceof Date && dateCols.indexOf(i+1) === -1)
158 | dateCols.push(i+1);
159 | return row[col]
160 | });
161 | });
162 |
163 | return XlsxPopulate.fromBlankAsync()
164 | .then( workbook => {
165 | workbook.sheet(0).name(name)
166 | .cell("A1").value(
167 | [columns].concat(data)
168 | )
169 | dateCols.forEach( dateCol => {
170 | workbook.sheet(0).column(dateCol)
171 | .style("numberFormat", "dddd, mmmm dd, yyyy");
172 | });
173 | return workbook.outputAsync()
174 | })
175 | .then( blob => {
176 | saveAs(blob, sanitizeFilename(name) + ".xlsx");
177 | });
178 | }
179 |
180 | function exportSpreadsheet(providers, spreadsheetTemplates, templateId, format) {
181 | const templateDefinition = spreadsheetTemplates[templateId];
182 |
183 | const templateDefinitions = _.map( (templateDefinition.extends||[]),
184 | id => spreadsheetTemplates[id]
185 | ).concat([templateDefinition]);
186 |
187 | const mergedTemplateDefinition = mergeObjects.merge(templateDefinitions);
188 | let flatData = flattenProviders(providers, mergedTemplateDefinition.template, { format });
189 |
190 | if (mergedTemplateDefinition.sortBy) {
191 | const sortBy = _.map(mergedTemplateDefinition.sortBy, s => s.name);
192 | const sortDir = _.map(mergedTemplateDefinition.sortBy, s => s.dir || "desc");
193 | flatData = _.orderBy(flatData, sortBy, sortDir);
194 | }
195 |
196 | //csv exporter has a bug with undefined values - change to null
197 | flatData = flatData.map( row => {
198 | return _.mapValues(row, v => v === undefined ? null : v )
199 | })
200 |
201 | if (format === "xlsx") {
202 | return exportFlatDataExcel(flatData, mergedTemplateDefinition.template, mergedTemplateDefinition.name)
203 | } else {
204 | return exportFlatDataCSV(flatData, mergedTemplateDefinition.template, mergedTemplateDefinition.name)
205 | }
206 | }
207 |
208 |
209 | export default {
210 | flatten, flattenProviders,
211 | defaultHelperFns, exportSpreadsheet
212 | }
--------------------------------------------------------------------------------
/src/store/user-settings.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 | import saveAs from "file-saver";
3 | import tv4 from "tv4";
4 | import userSettingsSchema from "../schemas/user-settings-schema.json";
5 |
6 | function buildJSON(providers, githubConfig, redirectUri) {
7 | const json = {
8 | providers: _.map(providers, p => ({
9 | ...p, lastUpdated: null, data: null
10 | }) ),
11 | githubConfig, redirectUri
12 | };
13 | return JSON.stringify(json, null, 2);
14 |
15 | }
16 |
17 | function download(providers, githubConfig, redirectUri, fileName="procure-settings.json") {
18 | const json = buildJSON(providers, githubConfig, redirectUri);
19 | const blob = new Blob(
20 | [json],
21 | {type: "application/json;charset=utf-8"}
22 | );
23 | saveAs(blob, fileName);
24 | }
25 |
26 | function readFromFile(file, redirectUri, queryProfiles) {
27 | const reader = new FileReader();
28 | return new Promise( (resolve, reject) => {
29 | reader.onload = e => resolve(reader.result);
30 | reader.onerror = e => reject(e);
31 | reader.readAsText(file);
32 | })
33 | .then( data => {
34 | try {
35 | return JSON.parse(data);
36 | } catch (e) {
37 | throw new Error("Unable to read file")
38 | }
39 | })
40 | .then( json => {
41 | if (json.redirectUri !== redirectUri)
42 | throw new Error("Only settings saved from this website can be imported")
43 | return json;
44 |
45 | })
46 | .then( json => {
47 | tv4.validate(json, userSettingsSchema);
48 | if (tv4.error) {
49 | if (console) console.log(tv4.error);
50 | throw new Error("Invalid file format: " + tv4.error.message);
51 | }
52 | return json;
53 |
54 | })
55 | .then( json => {
56 | _.each( json.providers, provider => {
57 | if (!queryProfiles[provider.queryProfile])
58 | throw new Error(
59 | "Query profile not found: " + provider.queryProfile
60 | );
61 | });
62 | return json;
63 | })
64 | }
65 |
66 | export default {
67 | buildJSON, download, readFromFile
68 | }
--------------------------------------------------------------------------------
/src/store/zip-exporter.js:
--------------------------------------------------------------------------------
1 | import sanitizeFilename from "sanitize-filename";
2 | import _ from "lodash";
3 | import jszip from "jszip";
4 | import { saveAs } from "file-saver";
5 |
6 | function addProviderToZip(zipFolder, provider) {
7 |
8 | // console.log(JSON.stringify(provider.data))
9 |
10 | const bundles = _.chain(provider.data.entry)
11 | .groupBy(e => e.resource.resourceType)
12 | .mapValues(entry => {
13 | return {
14 | entry: _.map(entry, e => ({fullUrl: e.fullUrl, resource: e.resource}) ),
15 | total: entry.length,
16 | type:"collection",
17 | resourceType: "Bundle"
18 | }
19 | }).value();
20 | _.each(bundles, (bundle, resourceType) => {
21 | zipFolder.file(resourceType+".json", JSON.stringify(bundle, null, 2))
22 | });
23 | _.each(provider.data.files, f => {
24 | zipFolder.file(f.fileName, f.blob, {type: "blob"});
25 | });
26 | if (provider.data.errorLog.length)
27 | zipFolder.file("error_log.json", JSON.stringify(provider.data.errorLog, null, 2));
28 | }
29 |
30 | function exportProvider(provider) {
31 | return new Promise( (resolve, reject) => {
32 | const zip = new jszip();
33 | addProviderToZip(zip, provider);
34 | const fileName = sanitizeFilename(provider.name);
35 | zip.generateAsync({type: "blob"})
36 | .then( blob => saveAs(blob, fileName) )
37 | .then( resolve )
38 | .catch( reject );
39 | });
40 | }
41 |
42 | function exportProviders(providers, fileName="procure-data.zip") {
43 | return exportProvidersAsBlob(providers)
44 | .then( blob => saveAs(blob, fileName) );
45 | }
46 |
47 | function exportProvidersAsBlob(providers) {
48 | return new Promise( (resolve, reject) => {
49 | const zip = new jszip();
50 | providers.filter( p => p.data && p.selected).forEach( provider => {
51 | const folder = zip.folder( sanitizeFilename(provider.name) );
52 | addProviderToZip(folder, provider);
53 | });
54 | zip.generateAsync({type: "blob"})
55 | .then( resolve )
56 | .catch( reject );
57 | });
58 | }
59 |
60 | function generateFile(providers, multiProviderFileName) {
61 | const exportableProviders = _.filter(providers, p => {
62 | return p.data && p.data.entry && p.selected && (p.data.entry.length || p.data.errorLog.length)
63 | });
64 |
65 | //work around jszip timezone bug
66 | const currDate = new Date();
67 | const dateWithOffset = new Date(currDate.getTime() - currDate.getTimezoneOffset() * 60000);
68 | jszip.defaults.date = dateWithOffset;
69 |
70 | if (exportableProviders.length > 1) {
71 | return exportProviders(exportableProviders, multiProviderFileName);
72 | } else {
73 | return exportProvider(exportableProviders[0]);
74 | }
75 | }
76 |
77 | export default { generateFile, exportProvidersAsBlob }
--------------------------------------------------------------------------------
/src/sw-build.js:
--------------------------------------------------------------------------------
1 | //see: https://github.com/facebook/create-react-app/issues/5673
2 |
3 | const workboxBuild = require('workbox-build');
4 | // NOTE: This should be run *AFTER* all your assets are built
5 | const buildSW = () => {
6 | // This will return a Promise
7 | return workboxBuild.injectManifest({
8 | swSrc: 'src/sw.js', // this is your sw template file
9 | swDest: 'build/sw.js', // this will be created in the build step
10 | globDirectory: 'build',
11 | globPatterns: [
12 | '**\/*.{js,css,html,png}',
13 | ]
14 | }).then(({count, size, warnings}) => {
15 | // Optionally, log any warnings and details.
16 | warnings.forEach(console.warn);
17 | console.log(`${count} files will be precached, totaling ${size} bytes.`);
18 | });
19 | }
20 | buildSW();
--------------------------------------------------------------------------------
/src/sw.js:
--------------------------------------------------------------------------------
1 | //see: https://github.com/facebook/create-react-app/issues/5673
2 |
3 | if ('function' === typeof importScripts) {
4 | importScripts(
5 | 'https://storage.googleapis.com/workbox-cdn/releases/3.5.0/workbox-sw.js'
6 | );
7 |
8 | /* global workbox */
9 | if (workbox) {
10 | console.log('Workbox is loaded');
11 |
12 | /* injection point for manifest files. */
13 | workbox.precaching.precacheAndRoute([]);
14 |
15 | /* custom cache rules*/
16 | workbox.routing.registerNavigationRoute('/index.html', {
17 | blacklist: [/^\/_/, /\/[^\/]+\.[^\/]+$/],
18 | });
19 |
20 | workbox.routing.registerNavigationRoute('/callback.html');
21 |
22 | workbox.routing.registerRoute(
23 | /\.(?:png|gif|jpg|jpeg)$/,
24 | workbox.strategies.cacheFirst({
25 | cacheName: 'images',
26 | plugins: [
27 | new workbox.expiration.Plugin({
28 | maxEntries: 60,
29 | maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
30 | }),
31 | ],
32 | })
33 | );
34 |
35 | } else {
36 | console.log('Workbox could not be loaded. No Offline support');
37 | }
38 | }
--------------------------------------------------------------------------------
/src/test/App.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import TestRender from "react-test-renderer";
3 | import StoreContext from "storeon/react/context"
4 | import App from "../components/App";
5 | import store from "../store/store";
6 |
7 | test('renders without crashing', () => {
8 | TestRender.create(
9 |
10 | )
11 | });
12 |
13 |
--------------------------------------------------------------------------------
/src/test/data/document_reference.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 2,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/DocumentReference?patient=TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "fullUrl": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/DocumentReference/Tr-wsqP3I4ItgWRr1UtQWZHlE7rlv7nbeuTFnUqoLY4.wE7nA4Ugt7PuvMvct9ydxMXubL7BSwkb00UgR1HewAiIlpSl3AHRtkFgkT7XRf.IB",
14 | "search": {
15 | "mode": "match"
16 | },
17 | "link": [
18 | {
19 | "relation": "self",
20 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/DocumentReference/Tr-wsqP3I4ItgWRr1UtQWZHlE7rlv7nbeuTFnUqoLY4.wE7nA4Ugt7PuvMvct9ydxMXubL7BSwkb00UgR1HewAiIlpSl3AHRtkFgkT7XRf.IB"
21 | }
22 | ],
23 | "resource": {
24 | "resourceType": "DocumentReference",
25 | "created": "2019-03-01T00:00:00Z",
26 | "indexed": "2019-03-01T00:00:00Z",
27 | "status": "current",
28 | "id": "Tr-wsqP3I4ItgWRr1UtQWZHlE7rlv7nbeuTFnUqoLY4.wE7nA4Ugt7PuvMvct9ydxMXubL7BSwkb00UgR1HewAiIlpSl3AHRtkFgkT7XRf.IB",
29 | "subject": {
30 | "display": "Joshua C Mandel",
31 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Patient/TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
32 | },
33 | "class": {
34 | "text": "Summarization of Episode Note",
35 | "coding": [
36 | {
37 | "system": "http://loinc.org",
38 | "code": "34133-9",
39 | "display": "Summarization of Episode Note"
40 | }
41 | ]
42 | },
43 | "type": {
44 | "text": "Summarization of Episode Note",
45 | "coding": [
46 | {
47 | "system": "http://loinc.org",
48 | "code": "34133-9",
49 | "display": "Summarization of Episode Note"
50 | }
51 | ]
52 | },
53 | "content": [
54 | {
55 | "attachment": {
56 | "contentType": "application/xml",
57 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Binary/Tr-wsqP3I4ItgWRr1UtQWZHlE7rlv7nbeuTFnUqoLY4.wE7nA4Ugt7PuvMvct9ydxMXubL7BSwkb00UgR1HewAiIlpSl3AHRtkFgkT7XRf.IB"
58 | }
59 | }
60 | ],
61 | "masterIdentifier": {
62 | "value": "1.2.840.114350.1.13.283.2.7.8.688883.188221825"
63 | }
64 | }
65 | },
66 | {
67 | "fullUrl": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/DocumentReference/TksSRPcka4BoYduTTHg-k0jPC5rVuPExcfPtz4UQS-3uCXiMVAQgj2SsyEWKvUbPZ.jSROON4EThqtJZa5Az4PJuqIqOe6xg7cyGjo7YoRxEB",
68 | "search": {
69 | "mode": "match"
70 | },
71 | "link": [
72 | {
73 | "relation": "self",
74 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/DocumentReference/TksSRPcka4BoYduTTHg-k0jPC5rVuPExcfPtz4UQS-3uCXiMVAQgj2SsyEWKvUbPZ.jSROON4EThqtJZa5Az4PJuqIqOe6xg7cyGjo7YoRxEB"
75 | }
76 | ],
77 | "resource": {
78 | "resourceType": "DocumentReference",
79 | "created": "2019-03-01T00:00:00Z",
80 | "indexed": "2019-03-01T00:00:00Z",
81 | "status": "current",
82 | "id": "TksSRPcka4BoYduTTHg-k0jPC5rVuPExcfPtz4UQS-3uCXiMVAQgj2SsyEWKvUbPZ.jSROON4EThqtJZa5Az4PJuqIqOe6xg7cyGjo7YoRxEB",
83 | "subject": {
84 | "display": "Joshua C Mandel",
85 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Patient/TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
86 | },
87 | "context": {
88 | "period": {
89 | "start": "2018-08-09T14:29:27Z",
90 | "end": "2018-08-09T15:14:57Z"
91 | }
92 | },
93 | "class": {
94 | "text": "Subsequent evaluation note",
95 | "coding": [
96 | {
97 | "system": "http://loinc.org",
98 | "code": "11506-3",
99 | "display": "Subsequent evaluation note"
100 | }
101 | ]
102 | },
103 | "type": {
104 | "text": "Subsequent evaluation note",
105 | "coding": [
106 | {
107 | "system": "http://loinc.org",
108 | "code": "11506-3",
109 | "display": "Subsequent evaluation note"
110 | }
111 | ]
112 | },
113 | "content": [
114 | {
115 | "attachment": {
116 | "contentType": "application/xml",
117 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Binary/TksSRPcka4BoYduTTHg-k0jPC5rVuPExcfPtz4UQS-3uCXiMVAQgj2SsyEWKvUbPZ.jSROON4EThqtJZa5Az4PJuqIqOe6xg7cyGjo7YoRxEB"
118 | }
119 | }
120 | ],
121 | "masterIdentifier": {
122 | "value": "1.2.840.114350.1.13.283.2.7.8.688883.188221830"
123 | }
124 | }
125 | },
126 | {
127 | "search": {
128 | "mode": "outcome"
129 | },
130 | "resource": {
131 | "resourceType": "OperationOutcome",
132 | "issue": [
133 | {
134 | "severity": "warning",
135 | "code": "informational",
136 | "details": {
137 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
138 | "coding": [
139 | {
140 | "system": "urn:oid:1.2.840.114350.1.13.283.2.7.2.657369",
141 | "code": "4119",
142 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
143 | }
144 | ]
145 | }
146 | }
147 | ]
148 | }
149 | }
150 | ]
151 | }
152 |
--------------------------------------------------------------------------------
/src/test/data/josh/unity-point/allergy-intolerance.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 4,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/AllergyIntolerance?patient=TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "fullUrl": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/AllergyIntolerance/Tt1FWzRK-u5MrXQ99WCD4LAB",
14 | "link": [
15 | {
16 | "relation": "self",
17 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/AllergyIntolerance/Tt1FWzRK-u5MrXQ99WCD4LAB"
18 | }
19 | ],
20 | "search": {
21 | "mode": "match"
22 | },
23 | "resource": {
24 | "resourceType": "AllergyIntolerance",
25 | "recordedDate": "2018-08-09T14:44:58Z",
26 | "status": "confirmed",
27 | "criticality": "CRITH",
28 | "onset": "2018-08-09",
29 | "id": "Tt1FWzRK-u5MrXQ99WCD4LAB",
30 | "recorder": {
31 | "display": "Tracy C W",
32 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Practitioner/TvNZ7FJcsjY1zDlNW4NPZ9wB"
33 | },
34 | "patient": {
35 | "display": "Joshua C Mandel",
36 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Patient/TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
37 | },
38 | "substance": {
39 | "text": "PEANUT OIL",
40 | "coding": [
41 | {
42 | "system": "http://www.nlm.nih.gov/research/umls/rxnorm",
43 | "code": "18235",
44 | "display": "PEANUT OIL"
45 | },
46 | {
47 | "system": "http://fdasis.nlm.nih.gov",
48 | "code": "2EUM6926ZI",
49 | "display": "PEANUT OIL"
50 | }
51 | ]
52 | },
53 | "reaction": [
54 | {
55 | "certainty": "confirmed",
56 | "onset": "2018-08-09",
57 | "manifestation": [
58 | {
59 | "text": "Hives"
60 | }
61 | ]
62 | }
63 | ]
64 | }
65 | },
66 | {
67 | "fullUrl": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/AllergyIntolerance/TgVbL6JR6IMJrsuByq.IvOAB",
68 | "link": [
69 | {
70 | "relation": "self",
71 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/AllergyIntolerance/TgVbL6JR6IMJrsuByq.IvOAB"
72 | }
73 | ],
74 | "search": {
75 | "mode": "match"
76 | },
77 | "resource": {
78 | "resourceType": "AllergyIntolerance",
79 | "recordedDate": "2018-08-09T14:45:23Z",
80 | "status": "confirmed",
81 | "criticality": "CRITH",
82 | "onset": "2018-08-09",
83 | "id": "TgVbL6JR6IMJrsuByq.IvOAB",
84 | "recorder": {
85 | "display": "Tracy C W",
86 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Practitioner/TvNZ7FJcsjY1zDlNW4NPZ9wB"
87 | },
88 | "patient": {
89 | "display": "Joshua C Mandel",
90 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Patient/TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
91 | },
92 | "substance": {
93 | "text": "TREE NUT"
94 | },
95 | "reaction": [
96 | {
97 | "certainty": "confirmed",
98 | "onset": "2018-08-09",
99 | "manifestation": [
100 | {
101 | "text": "Hives"
102 | }
103 | ]
104 | }
105 | ]
106 | }
107 | },
108 | {
109 | "fullUrl": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/AllergyIntolerance/TiduTM3NqBeVsB2-PLbw9xwB",
110 | "link": [
111 | {
112 | "relation": "self",
113 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/AllergyIntolerance/TiduTM3NqBeVsB2-PLbw9xwB"
114 | }
115 | ],
116 | "search": {
117 | "mode": "match"
118 | },
119 | "resource": {
120 | "resourceType": "AllergyIntolerance",
121 | "recordedDate": "2018-08-09T14:45:51Z",
122 | "status": "confirmed",
123 | "criticality": "CRITH",
124 | "onset": "2018-08-09",
125 | "id": "TiduTM3NqBeVsB2-PLbw9xwB",
126 | "recorder": {
127 | "display": "Tracy C W",
128 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Practitioner/TvNZ7FJcsjY1zDlNW4NPZ9wB"
129 | },
130 | "patient": {
131 | "display": "Joshua C Mandel",
132 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Patient/TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
133 | },
134 | "substance": {
135 | "text": "SULFA ANTIBIOTICS",
136 | "coding": [
137 | {
138 | "system": "http://hl7.org/fhir/ndfrt",
139 | "code": "N0000006054",
140 | "display": "SULFA ANTIBIOTICS"
141 | }
142 | ]
143 | },
144 | "reaction": [
145 | {
146 | "certainty": "confirmed",
147 | "onset": "2018-08-09",
148 | "manifestation": [
149 | {
150 | "text": "Hives"
151 | }
152 | ]
153 | }
154 | ]
155 | }
156 | },
157 | {
158 | "fullUrl": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/AllergyIntolerance/ToU1u5ERWHNH.SOFrW2aH.wB",
159 | "link": [
160 | {
161 | "relation": "self",
162 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/AllergyIntolerance/ToU1u5ERWHNH.SOFrW2aH.wB"
163 | }
164 | ],
165 | "search": {
166 | "mode": "match"
167 | },
168 | "resource": {
169 | "resourceType": "AllergyIntolerance",
170 | "recordedDate": "2018-08-09T14:46:07Z",
171 | "status": "confirmed",
172 | "criticality": "CRITH",
173 | "onset": "2018-08-09",
174 | "id": "ToU1u5ERWHNH.SOFrW2aH.wB",
175 | "recorder": {
176 | "display": "Tracy C W",
177 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Practitioner/TvNZ7FJcsjY1zDlNW4NPZ9wB"
178 | },
179 | "patient": {
180 | "display": "Joshua C Mandel",
181 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Patient/TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
182 | },
183 | "substance": {
184 | "text": "PENICILLINS",
185 | "coding": [
186 | {
187 | "system": "http://hl7.org/fhir/ndfrt",
188 | "code": "N0000005840",
189 | "display": "PENICILLINS"
190 | }
191 | ]
192 | },
193 | "reaction": [
194 | {
195 | "certainty": "confirmed",
196 | "onset": "2018-08-09",
197 | "manifestation": [
198 | {
199 | "text": "Hives"
200 | }
201 | ]
202 | }
203 | ]
204 | }
205 | },
206 | {
207 | "search": {
208 | "mode": "outcome"
209 | },
210 | "resource": {
211 | "resourceType": "OperationOutcome",
212 | "id": "1047952446",
213 | "issue": [
214 | {
215 | "severity": "warning",
216 | "code": "informational",
217 | "details": {
218 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
219 | "coding": [
220 | {
221 | "system": "urn:oid:1.2.840.114350.1.13.283.2.7.2.657369",
222 | "code": "4119",
223 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
224 | }
225 | ]
226 | }
227 | }
228 | ]
229 | }
230 | }
231 | ]
232 | }
233 |
--------------------------------------------------------------------------------
/src/test/data/josh/unity-point/condition.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 1,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Condition?patient=TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "fullUrl": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Condition/TPs59ch2gdUAvOkX8miUucwB",
14 | "link": [
15 | {
16 | "relation": "self",
17 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Condition/TPs59ch2gdUAvOkX8miUucwB"
18 | }
19 | ],
20 | "search": {
21 | "mode": "match"
22 | },
23 | "resource": {
24 | "resourceType": "Condition",
25 | "dateRecorded": "2018-08-09",
26 | "clinicalStatus": "active",
27 | "onsetDateTime": "2018-08-09",
28 | "verificationStatus": "confirmed",
29 | "id": "TPs59ch2gdUAvOkX8miUucwB",
30 | "patient": {
31 | "display": "Joshua C Mandel",
32 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Patient/TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
33 | },
34 | "asserter": {
35 | "display": "Dr. P Dhillon",
36 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Practitioner/TjYRBsFZy1bjsexwhN0oEmgB"
37 | },
38 | "code": {
39 | "text": "Gastroesophageal reflux disease",
40 | "coding": [
41 | {
42 | "system": "urn:oid:2.16.840.1.113883.6.90",
43 | "code": "K21.9",
44 | "display": "Gastroesophageal reflux disease"
45 | },
46 | {
47 | "system": "http://snomed.info/sct",
48 | "code": "235595009",
49 | "display": "Gastroesophageal reflux disease (disorder)"
50 | }
51 | ]
52 | },
53 | "category": {
54 | "text": "Diagnosis",
55 | "coding": [
56 | {
57 | "system": "http://loinc.org",
58 | "code": "29308-4",
59 | "display": "Diagnosis"
60 | },
61 | {
62 | "system": "http://snomed.info/sct",
63 | "code": "439401001",
64 | "display": "Diagnosis"
65 | },
66 | {
67 | "system": "http://hl7.org/fhir/condition-category",
68 | "code": "diagnosis",
69 | "display": "Diagnosis"
70 | },
71 | {
72 | "system": "http://argonautwiki.hl7.org/extension-codes",
73 | "code": "problem",
74 | "display": "Problem"
75 | }
76 | ]
77 | }
78 | }
79 | },
80 | {
81 | "search": {
82 | "mode": "outcome"
83 | },
84 | "resource": {
85 | "resourceType": "OperationOutcome",
86 | "issue": [
87 | {
88 | "severity": "warning",
89 | "code": "informational",
90 | "details": {
91 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
92 | "coding": [
93 | {
94 | "system": "urn:oid:1.2.840.114350.1.13.283.2.7.2.657369",
95 | "code": "4119",
96 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
97 | }
98 | ]
99 | }
100 | }
101 | ]
102 | }
103 | }
104 | ]
105 | }
106 |
--------------------------------------------------------------------------------
/src/test/data/josh/unity-point/document-reference.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 2,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/DocumentReference?patient=TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "fullUrl": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/DocumentReference/Tr-wsqP3I4ItgWRr1UtQWZHlE7rlv7nbeuTFnUqoLY4.wE7nA4Ugt7PuvMvct9ydxMXubL7BSwkb00UgR1HewAiIlpSl3AHRtkFgkT7XRf.IB",
14 | "search": {
15 | "mode": "match"
16 | },
17 | "link": [
18 | {
19 | "relation": "self",
20 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/DocumentReference/Tr-wsqP3I4ItgWRr1UtQWZHlE7rlv7nbeuTFnUqoLY4.wE7nA4Ugt7PuvMvct9ydxMXubL7BSwkb00UgR1HewAiIlpSl3AHRtkFgkT7XRf.IB"
21 | }
22 | ],
23 | "resource": {
24 | "resourceType": "DocumentReference",
25 | "created": "2019-03-01T00:00:00Z",
26 | "indexed": "2019-03-01T00:00:00Z",
27 | "status": "current",
28 | "id": "Tr-wsqP3I4ItgWRr1UtQWZHlE7rlv7nbeuTFnUqoLY4.wE7nA4Ugt7PuvMvct9ydxMXubL7BSwkb00UgR1HewAiIlpSl3AHRtkFgkT7XRf.IB",
29 | "subject": {
30 | "display": "Joshua C Mandel",
31 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Patient/TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
32 | },
33 | "class": {
34 | "text": "Summarization of Episode Note",
35 | "coding": [
36 | {
37 | "system": "http://loinc.org",
38 | "code": "34133-9",
39 | "display": "Summarization of Episode Note"
40 | }
41 | ]
42 | },
43 | "type": {
44 | "text": "Summarization of Episode Note",
45 | "coding": [
46 | {
47 | "system": "http://loinc.org",
48 | "code": "34133-9",
49 | "display": "Summarization of Episode Note"
50 | }
51 | ]
52 | },
53 | "content": [
54 | {
55 | "attachment": {
56 | "contentType": "application/xml",
57 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Binary/Tr-wsqP3I4ItgWRr1UtQWZHlE7rlv7nbeuTFnUqoLY4.wE7nA4Ugt7PuvMvct9ydxMXubL7BSwkb00UgR1HewAiIlpSl3AHRtkFgkT7XRf.IB"
58 | }
59 | }
60 | ],
61 | "masterIdentifier": {
62 | "value": "1.2.840.114350.1.13.283.2.7.8.688883.188221825"
63 | }
64 | }
65 | },
66 | {
67 | "fullUrl": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/DocumentReference/TksSRPcka4BoYduTTHg-k0jPC5rVuPExcfPtz4UQS-3uCXiMVAQgj2SsyEWKvUbPZ.jSROON4EThqtJZa5Az4PJuqIqOe6xg7cyGjo7YoRxEB",
68 | "search": {
69 | "mode": "match"
70 | },
71 | "link": [
72 | {
73 | "relation": "self",
74 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/DocumentReference/TksSRPcka4BoYduTTHg-k0jPC5rVuPExcfPtz4UQS-3uCXiMVAQgj2SsyEWKvUbPZ.jSROON4EThqtJZa5Az4PJuqIqOe6xg7cyGjo7YoRxEB"
75 | }
76 | ],
77 | "resource": {
78 | "resourceType": "DocumentReference",
79 | "created": "2019-03-01T00:00:00Z",
80 | "indexed": "2019-03-01T00:00:00Z",
81 | "status": "current",
82 | "id": "TksSRPcka4BoYduTTHg-k0jPC5rVuPExcfPtz4UQS-3uCXiMVAQgj2SsyEWKvUbPZ.jSROON4EThqtJZa5Az4PJuqIqOe6xg7cyGjo7YoRxEB",
83 | "subject": {
84 | "display": "Joshua C Mandel",
85 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Patient/TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
86 | },
87 | "context": {
88 | "period": {
89 | "start": "2018-08-09T14:29:27Z",
90 | "end": "2018-08-09T15:14:57Z"
91 | }
92 | },
93 | "class": {
94 | "text": "Subsequent evaluation note",
95 | "coding": [
96 | {
97 | "system": "http://loinc.org",
98 | "code": "11506-3",
99 | "display": "Subsequent evaluation note"
100 | }
101 | ]
102 | },
103 | "type": {
104 | "text": "Subsequent evaluation note",
105 | "coding": [
106 | {
107 | "system": "http://loinc.org",
108 | "code": "11506-3",
109 | "display": "Subsequent evaluation note"
110 | }
111 | ]
112 | },
113 | "content": [
114 | {
115 | "attachment": {
116 | "contentType": "application/xml",
117 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Binary/TksSRPcka4BoYduTTHg-k0jPC5rVuPExcfPtz4UQS-3uCXiMVAQgj2SsyEWKvUbPZ.jSROON4EThqtJZa5Az4PJuqIqOe6xg7cyGjo7YoRxEB"
118 | }
119 | }
120 | ],
121 | "masterIdentifier": {
122 | "value": "1.2.840.114350.1.13.283.2.7.8.688883.188221830"
123 | }
124 | }
125 | },
126 | {
127 | "search": {
128 | "mode": "outcome"
129 | },
130 | "resource": {
131 | "resourceType": "OperationOutcome",
132 | "issue": [
133 | {
134 | "severity": "warning",
135 | "code": "informational",
136 | "details": {
137 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
138 | "coding": [
139 | {
140 | "system": "urn:oid:1.2.840.114350.1.13.283.2.7.2.657369",
141 | "code": "4119",
142 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
143 | }
144 | ]
145 | }
146 | }
147 | ]
148 | }
149 | }
150 | ]
151 | }
152 |
--------------------------------------------------------------------------------
/src/test/data/josh/unity-point/immunization.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 1,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Immunization?patient=TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "fullUrl": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Immunization/TF52dFfxVm8obsBVv8jreKgB",
14 | "link": [
15 | {
16 | "relation": "self",
17 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Immunization/TF52dFfxVm8obsBVv8jreKgB"
18 | }
19 | ],
20 | "search": {
21 | "mode": "match"
22 | },
23 | "resource": {
24 | "resourceType": "Immunization",
25 | "status": "completed",
26 | "date": "2017-10-25",
27 | "wasNotGiven": false,
28 | "reported": true,
29 | "id": "TF52dFfxVm8obsBVv8jreKgB",
30 | "vaccineCode": {
31 | "text": "Influenza (FLUCELVAX) ccIIV4, prefilled syringe"
32 | },
33 | "patient": {
34 | "display": "Joshua C Mandel",
35 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Patient/TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
36 | },
37 | "site": {
38 | "text": "Left Arm",
39 | "coding": [
40 | {
41 | "system": "urn:oid:1.2.840.114350.1.13.283.2.7.10.768076.4040",
42 | "code": "14",
43 | "display": "Left Arm"
44 | }
45 | ]
46 | },
47 | "route": {
48 | "text": "Intramuscular",
49 | "coding": [
50 | {
51 | "system": "urn:oid:1.2.840.114350.1.13.283.2.7.10.768076.4030",
52 | "code": "2",
53 | "display": "Intramuscular"
54 | }
55 | ]
56 | }
57 | }
58 | },
59 | {
60 | "search": {
61 | "mode": "outcome"
62 | },
63 | "resource": {
64 | "resourceType": "OperationOutcome",
65 | "id": "1047952451",
66 | "issue": [
67 | {
68 | "severity": "warning",
69 | "code": "informational",
70 | "details": {
71 | "text": "CVX code for vaccine not found",
72 | "coding": [
73 | {
74 | "system": "urn:oid:1.2.840.114350.1.13.283.2.7.2.657369",
75 | "code": "4117",
76 | "display": "CVX code for vaccine not found"
77 | }
78 | ]
79 | }
80 | },
81 | {
82 | "severity": "warning",
83 | "code": "informational",
84 | "details": {
85 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
86 | "coding": [
87 | {
88 | "system": "urn:oid:1.2.840.114350.1.13.283.2.7.2.657369",
89 | "code": "4119",
90 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
91 | }
92 | ]
93 | }
94 | }
95 | ]
96 | }
97 | }
98 | ]
99 | }
100 |
--------------------------------------------------------------------------------
/src/test/data/josh/unity-point/medication-order.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 0,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/MedicationOrder?patient=TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "search": {
14 | "mode": "outcome"
15 | },
16 | "resource": {
17 | "resourceType": "OperationOutcome",
18 | "id": "1047952442",
19 | "issue": [
20 | {
21 | "severity": "warning",
22 | "code": "informational",
23 | "details": {
24 | "text": "Resource request returns no results.",
25 | "coding": [
26 | {
27 | "system": "urn:oid:1.2.840.114350.1.13.283.2.7.2.657369",
28 | "code": "4101",
29 | "display": "Resource request returns no results."
30 | }
31 | ]
32 | }
33 | },
34 | {
35 | "severity": "warning",
36 | "code": "informational",
37 | "details": {
38 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
39 | "coding": [
40 | {
41 | "system": "urn:oid:1.2.840.114350.1.13.283.2.7.2.657369",
42 | "code": "4119",
43 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
44 | }
45 | ]
46 | }
47 | }
48 | ]
49 | }
50 | }
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/src/test/data/josh/unity-point/medication-statement.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 0,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/MedicationStatement?patient=TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "search": {
14 | "mode": "outcome"
15 | },
16 | "resource": {
17 | "resourceType": "OperationOutcome",
18 | "id": "1047952444",
19 | "issue": [
20 | {
21 | "severity": "warning",
22 | "code": "informational",
23 | "details": {
24 | "text": "Resource request returns no results.",
25 | "coding": [
26 | {
27 | "system": "urn:oid:1.2.840.114350.1.13.283.2.7.2.657369",
28 | "code": "4101",
29 | "display": "Resource request returns no results."
30 | }
31 | ]
32 | }
33 | },
34 | {
35 | "severity": "warning",
36 | "code": "informational",
37 | "details": {
38 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
39 | "coding": [
40 | {
41 | "system": "urn:oid:1.2.840.114350.1.13.283.2.7.2.657369",
42 | "code": "4119",
43 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
44 | }
45 | ]
46 | }
47 | }
48 | ]
49 | }
50 | }
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/src/test/data/josh/unity-point/patient.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Patient",
3 | "birthDate": "1982-10-26",
4 | "active": true,
5 | "gender": "male",
6 | "deceasedBoolean": false,
7 | "id": "TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB",
8 | "name": [
9 | {
10 | "use": "usual",
11 | "text": "Joshua C Mandel",
12 | "family": [
13 | "Mandel"
14 | ],
15 | "given": [
16 | "Joshua",
17 | "C"
18 | ]
19 | }
20 | ],
21 | "identifier": [
22 | {
23 | "use": "usual",
24 | "system": "EPI",
25 | "value": "E7004467"
26 | },
27 | {
28 | "use": "usual",
29 | "system": "MAPL",
30 | "value": "APL324672"
31 | },
32 | {
33 | "use": "usual",
34 | "system": "urn:oid:1.2.840.114350.1.13.283",
35 | "value": "96853541"
36 | }
37 | ],
38 | "address": [
39 | {
40 | "use": "home",
41 | "line": [
42 | "443 Virginia Terrace"
43 | ],
44 | "city": "MADISON",
45 | "state": "WI",
46 | "postalCode": "53726",
47 | "country": "USA"
48 | }
49 | ],
50 | "telecom": [
51 | {
52 | "system": "phone",
53 | "value": "617-500-3253",
54 | "use": "home"
55 | },
56 | {
57 | "system": "phone",
58 | "value": "617-500-3253",
59 | "use": "mobile"
60 | },
61 | {
62 | "system": "email",
63 | "value": "jmandel@alum.mit.edu"
64 | },
65 | {
66 | "system": "email",
67 | "value": "jmandel@gmail.com"
68 | }
69 | ],
70 | "maritalStatus": {
71 | "text": "Single"
72 | },
73 | "communication": [
74 | {
75 | "preferred": true,
76 | "language": {
77 | "text": "English",
78 | "coding": [
79 | {
80 | "system": "urn:oid:2.16.840.1.113883.6.99",
81 | "code": "en",
82 | "display": "English"
83 | }
84 | ]
85 | }
86 | }
87 | ],
88 | "extension": [
89 | {
90 | "url": "http://hl7.org/fhir/StructureDefinition/us-core-race",
91 | "valueCodeableConcept": {
92 | "text": "White",
93 | "coding": [
94 | {
95 | "system": "2.16.840.1.113883.5.104",
96 | "code": "2106-3",
97 | "display": "White"
98 | }
99 | ]
100 | }
101 | },
102 | {
103 | "url": "http://hl7.org/fhir/StructureDefinition/us-core-ethnicity",
104 | "valueCodeableConcept": {
105 | "text": "Not Hispanic or Latino",
106 | "coding": [
107 | {
108 | "system": "2.16.840.1.113883.5.50",
109 | "code": "2186-5",
110 | "display": "Not Hispanic or Latino"
111 | }
112 | ]
113 | }
114 | }
115 | ]
116 | }
117 |
--------------------------------------------------------------------------------
/src/test/data/josh/unity-point/procedure.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 0,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Procedure?patient=TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "search": {
14 | "mode": "outcome"
15 | },
16 | "resource": {
17 | "resourceType": "OperationOutcome",
18 | "id": "1047952449",
19 | "issue": [
20 | {
21 | "severity": "warning",
22 | "code": "informational",
23 | "details": {
24 | "text": "Resource request returns no results.",
25 | "coding": [
26 | {
27 | "system": "urn:oid:1.2.840.114350.1.13.283.2.7.2.657369",
28 | "code": "4101",
29 | "display": "Resource request returns no results."
30 | }
31 | ]
32 | }
33 | },
34 | {
35 | "severity": "warning",
36 | "code": "informational",
37 | "details": {
38 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
39 | "coding": [
40 | {
41 | "system": "urn:oid:1.2.840.114350.1.13.283.2.7.2.657369",
42 | "code": "4119",
43 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
44 | }
45 | ]
46 | }
47 | }
48 | ]
49 | }
50 | }
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/src/test/data/josh/unity-point/social-history.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 1,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Observation?category=social-history&patient=TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "fullUrl": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Observation/TiyxTbXtRBBpqtDPzrXFF0JNq5wzvxoW5sNIpA8aqcg9uMXvd6jvHgdrzVl6hh3VzB",
14 | "link": [
15 | {
16 | "relation": "self",
17 | "url": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Observation/TiyxTbXtRBBpqtDPzrXFF0JNq5wzvxoW5sNIpA8aqcg9uMXvd6jvHgdrzVl6hh3VzB"
18 | }
19 | ],
20 | "search": {
21 | "mode": "match"
22 | },
23 | "resource": {
24 | "resourceType": "Observation",
25 | "issued": "2018-08-09T00:00:00Z",
26 | "status": "final",
27 | "id": "TiyxTbXtRBBpqtDPzrXFF0JNq5wzvxoW5sNIpA8aqcg9uMXvd6jvHgdrzVl6hh3VzB",
28 | "code": {
29 | "text": "Smoking History",
30 | "coding": [
31 | {
32 | "system": "http://loinc.org",
33 | "code": "72166-2",
34 | "display": "Tobacco smoking status NHIS"
35 | }
36 | ]
37 | },
38 | "subject": {
39 | "display": "Joshua C Mandel",
40 | "reference": "https://epicfhir.unitypoint.org/ProdFHIR/api/FHIR/DSTU2/Patient/TmI-PYc6jAfH6Yfyp62Z3ZO2h0JlDM31r3YW3j7xuNhIB"
41 | },
42 | "valueCodeableConcept": {
43 | "text": "Never Smoker",
44 | "coding": [
45 | {
46 | "system": "http://snomed.info/sct",
47 | "code": "266919005",
48 | "display": "Never smoker"
49 | }
50 | ]
51 | },
52 | "category": {
53 | "text": "Social History",
54 | "coding": [
55 | {
56 | "system": "http://hl7.org/fhir/observation-category",
57 | "code": "social-history",
58 | "display": "Social History"
59 | }
60 | ]
61 | }
62 | }
63 | },
64 | {
65 | "search": {
66 | "mode": "outcome"
67 | },
68 | "resource": {
69 | "resourceType": "OperationOutcome",
70 | "issue": [
71 | {
72 | "severity": "warning",
73 | "code": "informational",
74 | "details": {
75 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
76 | "coding": [
77 | {
78 | "system": "urn:oid:1.2.840.114350.1.13.283.2.7.2.657369",
79 | "code": "4119",
80 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
81 | }
82 | ]
83 | }
84 | }
85 | ]
86 | }
87 | }
88 | ]
89 | }
90 |
--------------------------------------------------------------------------------
/src/test/data/josh/uw/allergy-intolerance.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 3,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/AllergyIntolerance?patient=TITdhHEfYtlRbx9v37LFiEOc1-ntCEuzjFeL91S4oI7UB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "fullUrl": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/AllergyIntolerance/Tv4x9BZgrAwULZuFVC2mJ4AB",
14 | "link": [
15 | {
16 | "relation": "self",
17 | "url": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/AllergyIntolerance/Tv4x9BZgrAwULZuFVC2mJ4AB"
18 | }
19 | ],
20 | "search": {
21 | "mode": "match"
22 | },
23 | "resource": {
24 | "resourceType": "AllergyIntolerance",
25 | "recordedDate": "2019-02-09T01:48:09Z",
26 | "status": "confirmed",
27 | "criticality": "CRITH",
28 | "category": "medication",
29 | "onset": "2018-08-09",
30 | "id": "Tv4x9BZgrAwULZuFVC2mJ4AB",
31 | "recorder": {
32 | "display": "Nurse James D",
33 | "reference": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/Practitioner/TSme.Chk03VV6jPzXLPLjQgB"
34 | },
35 | "patient": {
36 | "display": "Joshua Craig Mandel",
37 | "reference": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/Patient/TITdhHEfYtlRbx9v37LFiEOc1-ntCEuzjFeL91S4oI7UB"
38 | },
39 | "substance": {
40 | "text": "PENICILLINS",
41 | "coding": [
42 | {
43 | "system": "http://hl7.org/fhir/ndfrt",
44 | "code": "N0000005840",
45 | "display": "PENICILLINS"
46 | }
47 | ]
48 | },
49 | "reaction": [
50 | {
51 | "certainty": "confirmed",
52 | "onset": "2018-08-09",
53 | "manifestation": [
54 | {
55 | "text": "HIVES"
56 | }
57 | ]
58 | }
59 | ]
60 | }
61 | },
62 | {
63 | "fullUrl": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/AllergyIntolerance/TuoxJuAZqAm4lZDQyr-AN7AB",
64 | "link": [
65 | {
66 | "relation": "self",
67 | "url": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/AllergyIntolerance/TuoxJuAZqAm4lZDQyr-AN7AB"
68 | }
69 | ],
70 | "search": {
71 | "mode": "match"
72 | },
73 | "resource": {
74 | "resourceType": "AllergyIntolerance",
75 | "recordedDate": "2019-02-09T01:48:09Z",
76 | "status": "confirmed",
77 | "criticality": "CRITH",
78 | "category": "medication",
79 | "onset": "2018-08-09",
80 | "id": "TuoxJuAZqAm4lZDQyr-AN7AB",
81 | "recorder": {
82 | "display": "Nurse James D",
83 | "reference": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/Practitioner/TSme.Chk03VV6jPzXLPLjQgB"
84 | },
85 | "patient": {
86 | "display": "Joshua Craig Mandel",
87 | "reference": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/Patient/TITdhHEfYtlRbx9v37LFiEOc1-ntCEuzjFeL91S4oI7UB"
88 | },
89 | "substance": {
90 | "text": "SULFA DRUGS",
91 | "coding": [
92 | {
93 | "system": "http://hl7.org/fhir/ndfrt",
94 | "code": "N0000006054",
95 | "display": "SULFA DRUGS"
96 | }
97 | ]
98 | },
99 | "reaction": [
100 | {
101 | "certainty": "confirmed",
102 | "onset": "2018-08-09",
103 | "manifestation": [
104 | {
105 | "text": "HIVES"
106 | }
107 | ]
108 | }
109 | ]
110 | }
111 | },
112 | {
113 | "fullUrl": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/AllergyIntolerance/T8pT-Ow1q06bhWmVQ1EfbzgB",
114 | "link": [
115 | {
116 | "relation": "self",
117 | "url": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/AllergyIntolerance/T8pT-Ow1q06bhWmVQ1EfbzgB"
118 | }
119 | ],
120 | "search": {
121 | "mode": "match"
122 | },
123 | "resource": {
124 | "resourceType": "AllergyIntolerance",
125 | "recordedDate": "2019-02-09T01:48:09Z",
126 | "status": "confirmed",
127 | "criticality": "CRITH",
128 | "category": "medication",
129 | "onset": "2018-08-09",
130 | "id": "T8pT-Ow1q06bhWmVQ1EfbzgB",
131 | "recorder": {
132 | "display": "Nurse James D",
133 | "reference": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/Practitioner/TSme.Chk03VV6jPzXLPLjQgB"
134 | },
135 | "patient": {
136 | "display": "Joshua Craig Mandel",
137 | "reference": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/Patient/TITdhHEfYtlRbx9v37LFiEOc1-ntCEuzjFeL91S4oI7UB"
138 | },
139 | "substance": {
140 | "text": "PEANUT OIL",
141 | "coding": [
142 | {
143 | "system": "http://www.nlm.nih.gov/research/umls/rxnorm",
144 | "code": "18235",
145 | "display": "PEANUT OIL"
146 | },
147 | {
148 | "system": "http://fdasis.nlm.nih.gov",
149 | "code": "2EUM6926ZI",
150 | "display": "PEANUT OIL"
151 | }
152 | ]
153 | },
154 | "reaction": [
155 | {
156 | "certainty": "confirmed",
157 | "onset": "2018-08-09",
158 | "manifestation": [
159 | {
160 | "text": "HIVES"
161 | }
162 | ]
163 | }
164 | ]
165 | }
166 | },
167 | {
168 | "search": {
169 | "mode": "outcome"
170 | },
171 | "resource": {
172 | "resourceType": "OperationOutcome",
173 | "issue": [
174 | {
175 | "severity": "warning",
176 | "code": "informational",
177 | "details": {
178 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
179 | "coding": [
180 | {
181 | "system": "urn:oid:1.2.840.114350.1.13.92.2.7.2.657369",
182 | "code": "4119",
183 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
184 | }
185 | ]
186 | }
187 | }
188 | ]
189 | }
190 | }
191 | ]
192 | }
193 |
--------------------------------------------------------------------------------
/src/test/data/josh/uw/condition.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 0,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/Condition?patient=TITdhHEfYtlRbx9v37LFiEOc1-ntCEuzjFeL91S4oI7UB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "search": {
14 | "mode": "outcome"
15 | },
16 | "resource": {
17 | "resourceType": "OperationOutcome",
18 | "issue": [
19 | {
20 | "severity": "warning",
21 | "code": "informational",
22 | "details": {
23 | "text": "Resource request returns no results.",
24 | "coding": [
25 | {
26 | "system": "urn:oid:1.2.840.114350.1.13.92.2.7.2.657369",
27 | "code": "4101",
28 | "display": "Resource request returns no results."
29 | }
30 | ]
31 | }
32 | },
33 | {
34 | "severity": "warning",
35 | "code": "informational",
36 | "details": {
37 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
38 | "coding": [
39 | {
40 | "system": "urn:oid:1.2.840.114350.1.13.92.2.7.2.657369",
41 | "code": "4119",
42 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
43 | }
44 | ]
45 | }
46 | }
47 | ]
48 | }
49 | }
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/src/test/data/josh/uw/immunization.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 0,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/Immunization?patient=TITdhHEfYtlRbx9v37LFiEOc1-ntCEuzjFeL91S4oI7UB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "search": {
14 | "mode": "outcome"
15 | },
16 | "resource": {
17 | "resourceType": "OperationOutcome",
18 | "issue": [
19 | {
20 | "severity": "warning",
21 | "code": "informational",
22 | "details": {
23 | "text": "Resource request returns no results.",
24 | "coding": [
25 | {
26 | "system": "urn:oid:1.2.840.114350.1.13.92.2.7.2.657369",
27 | "code": "4101",
28 | "display": "Resource request returns no results."
29 | }
30 | ]
31 | }
32 | },
33 | {
34 | "severity": "warning",
35 | "code": "informational",
36 | "details": {
37 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
38 | "coding": [
39 | {
40 | "system": "urn:oid:1.2.840.114350.1.13.92.2.7.2.657369",
41 | "code": "4119",
42 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
43 | }
44 | ]
45 | }
46 | }
47 | ]
48 | }
49 | }
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/src/test/data/josh/uw/laboratory.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 0,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/Observation?category=laboratory&patient=TITdhHEfYtlRbx9v37LFiEOc1-ntCEuzjFeL91S4oI7UB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "search": {
14 | "mode": "outcome"
15 | },
16 | "resource": {
17 | "resourceType": "OperationOutcome",
18 | "issue": [
19 | {
20 | "severity": "warning",
21 | "code": "informational",
22 | "details": {
23 | "text": "Resource request returns no results.",
24 | "coding": [
25 | {
26 | "system": "urn:oid:1.2.840.114350.1.13.92.2.7.2.657369",
27 | "code": "4101",
28 | "display": "Resource request returns no results."
29 | }
30 | ]
31 | }
32 | },
33 | {
34 | "severity": "warning",
35 | "code": "informational",
36 | "details": {
37 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
38 | "coding": [
39 | {
40 | "system": "urn:oid:1.2.840.114350.1.13.92.2.7.2.657369",
41 | "code": "4119",
42 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
43 | }
44 | ]
45 | }
46 | }
47 | ]
48 | }
49 | }
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/src/test/data/josh/uw/medication-order.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 0,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/MedicationOrder?patient=TITdhHEfYtlRbx9v37LFiEOc1-ntCEuzjFeL91S4oI7UB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "search": {
14 | "mode": "outcome"
15 | },
16 | "resource": {
17 | "resourceType": "OperationOutcome",
18 | "issue": [
19 | {
20 | "severity": "warning",
21 | "code": "informational",
22 | "details": {
23 | "text": "Resource request returns no results.",
24 | "coding": [
25 | {
26 | "system": "urn:oid:1.2.840.114350.1.13.92.2.7.2.657369",
27 | "code": "4101",
28 | "display": "Resource request returns no results."
29 | }
30 | ]
31 | }
32 | },
33 | {
34 | "severity": "warning",
35 | "code": "informational",
36 | "details": {
37 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
38 | "coding": [
39 | {
40 | "system": "urn:oid:1.2.840.114350.1.13.92.2.7.2.657369",
41 | "code": "4119",
42 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
43 | }
44 | ]
45 | }
46 | }
47 | ]
48 | }
49 | }
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/src/test/data/josh/uw/medication-statement.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 0,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/MedicationStatement?patient=TITdhHEfYtlRbx9v37LFiEOc1-ntCEuzjFeL91S4oI7UB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "search": {
14 | "mode": "outcome"
15 | },
16 | "resource": {
17 | "resourceType": "OperationOutcome",
18 | "issue": [
19 | {
20 | "severity": "warning",
21 | "code": "informational",
22 | "details": {
23 | "text": "Resource request returns no results.",
24 | "coding": [
25 | {
26 | "system": "urn:oid:1.2.840.114350.1.13.92.2.7.2.657369",
27 | "code": "4101",
28 | "display": "Resource request returns no results."
29 | }
30 | ]
31 | }
32 | },
33 | {
34 | "severity": "warning",
35 | "code": "informational",
36 | "details": {
37 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
38 | "coding": [
39 | {
40 | "system": "urn:oid:1.2.840.114350.1.13.92.2.7.2.657369",
41 | "code": "4119",
42 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
43 | }
44 | ]
45 | }
46 | }
47 | ]
48 | }
49 | }
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/src/test/data/josh/uw/patient.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Patient",
3 | "birthDate": "1982-10-26",
4 | "active": true,
5 | "gender": "male",
6 | "deceasedBoolean": false,
7 | "id": "TITdhHEfYtlRbx9v37LFiEOc1-ntCEuzjFeL91S4oI7UB",
8 | "careProvider": [
9 | {
10 | "display": "Puneet S Dhillon, MD",
11 | "reference": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/Practitioner/THnL-qrj8K5xFmTYKVnSAWwaUDfRF9oakUswhCdkFoosB"
12 | }
13 | ],
14 | "name": [
15 | {
16 | "use": "usual",
17 | "text": "Joshua Craig Mandel",
18 | "family": [
19 | "Mandel"
20 | ],
21 | "given": [
22 | "Joshua",
23 | "Craig"
24 | ]
25 | }
26 | ],
27 | "identifier": [
28 | {
29 | "use": "usual",
30 | "system": "urn:oid:1.2.840.114350.1.13.92.2.7.5.737384.0",
31 | "value": "54506458"
32 | },
33 | {
34 | "use": "usual",
35 | "system": "urn:oid:1.2.5.8.2.7",
36 | "value": "3296404"
37 | }
38 | ],
39 | "address": [
40 | {
41 | "use": "home",
42 | "line": [
43 | "443 VIRGINIA TER"
44 | ],
45 | "city": "MADISON",
46 | "state": "WI",
47 | "postalCode": "53726-5345",
48 | "country": "USA"
49 | }
50 | ],
51 | "telecom": [
52 | {
53 | "system": "phone",
54 | "value": "617-500-3253",
55 | "use": "home"
56 | },
57 | {
58 | "system": "phone",
59 | "value": "617-500-3253",
60 | "use": "mobile"
61 | },
62 | {
63 | "system": "email",
64 | "value": "jmandel@alum.mit.edu"
65 | }
66 | ],
67 | "maritalStatus": {
68 | "text": "Single"
69 | },
70 | "extension": [
71 | {
72 | "url": "http://hl7.org/fhir/StructureDefinition/us-core-race",
73 | "valueCodeableConcept": {
74 | "text": "White",
75 | "coding": [
76 | {
77 | "system": "2.16.840.1.113883.5.104",
78 | "code": "2106-3",
79 | "display": "White"
80 | }
81 | ]
82 | }
83 | },
84 | {
85 | "url": "http://hl7.org/fhir/StructureDefinition/us-core-ethnicity",
86 | "valueCodeableConcept": {
87 | "text": "Not Hispanic or Latino",
88 | "coding": [
89 | {
90 | "system": "2.16.840.1.113883.5.50",
91 | "code": "2186-5",
92 | "display": "Not Hispanic or Latino"
93 | }
94 | ]
95 | }
96 | },
97 | {
98 | "url": "http://hl7.org/fhir/StructureDefinition/us-core-birth-sex",
99 | "valueCodeableConcept": {
100 | "text": "Male",
101 | "coding": [
102 | {
103 | "system": "http://hl7.org/fhir/v3/AdministrativeGender",
104 | "code": "M",
105 | "display": "Male"
106 | }
107 | ]
108 | }
109 | }
110 | ]
111 | }
112 |
--------------------------------------------------------------------------------
/src/test/data/josh/uw/procedure.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 0,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/Procedure?patient=TITdhHEfYtlRbx9v37LFiEOc1-ntCEuzjFeL91S4oI7UB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "search": {
14 | "mode": "outcome"
15 | },
16 | "resource": {
17 | "resourceType": "OperationOutcome",
18 | "issue": [
19 | {
20 | "severity": "warning",
21 | "code": "informational",
22 | "details": {
23 | "text": "Resource request returns no results.",
24 | "coding": [
25 | {
26 | "system": "urn:oid:1.2.840.114350.1.13.92.2.7.2.657369",
27 | "code": "4101",
28 | "display": "Resource request returns no results."
29 | }
30 | ]
31 | }
32 | },
33 | {
34 | "severity": "warning",
35 | "code": "informational",
36 | "details": {
37 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
38 | "coding": [
39 | {
40 | "system": "urn:oid:1.2.840.114350.1.13.92.2.7.2.657369",
41 | "code": "4119",
42 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
43 | }
44 | ]
45 | }
46 | }
47 | ]
48 | }
49 | }
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/src/test/data/josh/uw/social-history.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "type": "searchset",
4 | "total": 1,
5 | "link": [
6 | {
7 | "relation": "self",
8 | "url": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/Observation?category=social-history&patient=TITdhHEfYtlRbx9v37LFiEOc1-ntCEuzjFeL91S4oI7UB"
9 | }
10 | ],
11 | "entry": [
12 | {
13 | "fullUrl": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/Observation/TZDmEElEnd.TLmKAgcX292BzZFPb-b40BUjUJeg0hYO9JK07ckCGI7fLPIri08u9RB",
14 | "link": [
15 | {
16 | "relation": "self",
17 | "url": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/Observation/TZDmEElEnd.TLmKAgcX292BzZFPb-b40BUjUJeg0hYO9JK07ckCGI7fLPIri08u9RB"
18 | }
19 | ],
20 | "search": {
21 | "mode": "match"
22 | },
23 | "resource": {
24 | "resourceType": "Observation",
25 | "issued": "2019-02-08T00:00:00Z",
26 | "status": "final",
27 | "id": "TZDmEElEnd.TLmKAgcX292BzZFPb-b40BUjUJeg0hYO9JK07ckCGI7fLPIri08u9RB",
28 | "code": {
29 | "text": "Smoking History",
30 | "coding": [
31 | {
32 | "system": "http://loinc.org",
33 | "code": "72166-2",
34 | "display": "Tobacco smoking status NHIS"
35 | }
36 | ]
37 | },
38 | "subject": {
39 | "display": "Joshua Craig Mandel",
40 | "reference": "https://epicproxy.hosp.wisc.edu/FhirProxy/api/FHIR/DSTU2/Patient/TITdhHEfYtlRbx9v37LFiEOc1-ntCEuzjFeL91S4oI7UB"
41 | },
42 | "valueCodeableConcept": {
43 | "text": "Never Smoker",
44 | "coding": [
45 | {
46 | "system": "http://snomed.info/sct",
47 | "code": "266919005",
48 | "display": "Never smoker"
49 | }
50 | ]
51 | },
52 | "category": {
53 | "text": "Social History",
54 | "coding": [
55 | {
56 | "system": "http://hl7.org/fhir/observation-category",
57 | "code": "social-history",
58 | "display": "Social History"
59 | }
60 | ]
61 | }
62 | }
63 | },
64 | {
65 | "search": {
66 | "mode": "outcome"
67 | },
68 | "resource": {
69 | "resourceType": "OperationOutcome",
70 | "issue": [
71 | {
72 | "severity": "warning",
73 | "code": "informational",
74 | "details": {
75 | "text": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system.",
76 | "coding": [
77 | {
78 | "system": "urn:oid:1.2.840.114350.1.13.92.2.7.2.657369",
79 | "code": "4119",
80 | "display": "This response includes information available to the authorized user at the time of the request. It may not contain the entire record available in the system."
81 | }
82 | ]
83 | }
84 | }
85 | ]
86 | }
87 | }
88 | ]
89 | }
90 |
--------------------------------------------------------------------------------
/src/test/data/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Conformance",
3 | "publisher": "Not provided",
4 | "date": "2019-03-05T13:34:36-05:00",
5 | "kind": "instance",
6 | "software": {
7 | "name": "Smile CDR",
8 | "version": "2018.05.R01"
9 | },
10 | "implementation": {
11 | "description": "FHIR REST Server",
12 | "url": "https://launch.smarthealthit.org/v/r2/sim/eyJrIjoiMSIsImoiOiIxIiwiYiI6IjdiNjk3MzIyLTM2MDctNDZjYi1hMjQwLWMwODFiY2NiYTJlNSJ9/fhir"
13 | },
14 | "fhirVersion": "1.0.2",
15 | "acceptUnknown": "extensions",
16 | "format": [
17 | "application/xml+fhir",
18 | "application/json+fhir"
19 | ],
20 | "rest": [
21 | {
22 | "mode": "server",
23 | "security": {
24 | "cors": true,
25 | "extension": [
26 | {
27 | "url": "http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris",
28 | "extension": [
29 | {
30 | "url": "authorize",
31 | "valueUri": "http://launch.smarthealthit.org/v/r2/sim/eyJrIjoiMSIsImoiOiIxIiwiYiI6IjdiNjk3MzIyLTM2MDctNDZjYi1hMjQwLWMwODFiY2NiYTJlNSJ9/auth/authorize"
32 | },
33 | {
34 | "url": "token",
35 | "valueUri": "http://launch.smarthealthit.org/v/r2/sim/eyJrIjoiMSIsImoiOiIxIiwiYiI6IjdiNjk3MzIyLTM2MDctNDZjYi1hMjQwLWMwODFiY2NiYTJlNSJ9/auth/token"
36 | }
37 | ]
38 | }
39 | ],
40 | "service": [
41 | {
42 | "coding": [
43 | {
44 | "system": "http://hl7.org/fhir/restful-security-service",
45 | "code": "SMART-on-FHIR",
46 | "display": "SMART-on-FHIR"
47 | }
48 | ],
49 | "text": "OAuth2 using SMART-on-FHIR profile (see http://docs.smarthealthit.org)"
50 | }
51 | ]
52 | }
53 | }
54 | ]
55 |
56 | }
--------------------------------------------------------------------------------
/src/test/data/operation_outcome_404.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "OperationOutcome",
3 | "issue": [
4 | {
5 | "severity": "error",
6 | "code": "processing",
7 | "diagnostics": "Resource Patient/c7ec9560-58cd-4a08-874b-91e3429ef1d6123 is not known"
8 | }
9 | ]
10 | }
--------------------------------------------------------------------------------
/src/test/data/patient.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Patient",
3 | "id": "c7ec9560-58cd-4a08-874b-91e3429ef1d6",
4 | "meta": {
5 | "versionId": "1",
6 | "lastUpdated": "2018-05-07T13:10:22.943-04:00",
7 | "tag": [
8 | {
9 | "system": "https://smarthealthit.org/tags",
10 | "code": "synthea-8-2017"
11 | }
12 | ]
13 | },
14 | "text": {
15 | "status": "generated",
16 | "div": "Generated by
Synthea . Version identifier: 1a8d765a5375bf72f3b7a92001940d05a6f21189
"
17 | },
18 | "extension": [
19 | {
20 | "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race",
21 | "valueCodeableConcept": {
22 | "coding": [
23 | {
24 | "system": "http://hl7.org/fhir/v3/Race",
25 | "code": "2106-3",
26 | "display": "White"
27 | }
28 | ],
29 | "text": "race"
30 | }
31 | },
32 | {
33 | "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity",
34 | "valueCodeableConcept": {
35 | "coding": [
36 | {
37 | "system": "http://hl7.org/fhir/v3/Ethnicity",
38 | "code": "2186-5",
39 | "display": "Nonhispanic"
40 | }
41 | ],
42 | "text": "ethnicity"
43 | }
44 | },
45 | {
46 | "url": "http://hl7.org/fhir/StructureDefinition/birthPlace",
47 | "valueAddress": {
48 | "city": "Springfield",
49 | "state": "MA",
50 | "country": "US"
51 | }
52 | },
53 | {
54 | "url": "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName",
55 | "valueString": "Gretchen Dach"
56 | },
57 | {
58 | "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex",
59 | "valueCode": "F"
60 | },
61 | {
62 | "url": "http://hl7.org/fhir/StructureDefinition/patient-interpreterRequired",
63 | "valueBoolean": false
64 | }
65 | ],
66 | "identifier": [
67 | {
68 | "system": "https://github.com/synthetichealth/synthea",
69 | "value": "7dd5adc3-9790-4f46-a428-9fe44803e71a"
70 | },
71 | {
72 | "type": {
73 | "coding": [
74 | {
75 | "system": "http://hl7.org/fhir/identifier-type",
76 | "code": "SB"
77 | }
78 | ]
79 | },
80 | "system": "http://hl7.org/fhir/sid/us-ssn",
81 | "value": "999483974"
82 | },
83 | {
84 | "type": {
85 | "coding": [
86 | {
87 | "system": "http://hl7.org/fhir/v2/0203",
88 | "code": "DL"
89 | }
90 | ]
91 | },
92 | "system": "urn:oid:2.16.840.1.113883.4.3.25",
93 | "value": "S99914780"
94 | },
95 | {
96 | "type": {
97 | "coding": [
98 | {
99 | "system": "http://hl7.org/fhir/v2/0203",
100 | "code": "MR"
101 | }
102 | ]
103 | },
104 | "system": "http://hospital.smarthealthit.org",
105 | "value": "7dd5adc3-9790-4f46-a428-9fe44803e71a"
106 | }
107 | ],
108 | "name": [
109 | {
110 | "use": "official",
111 | "family": [
112 | "Towne"
113 | ],
114 | "given": [
115 | "Caren"
116 | ],
117 | "prefix": [
118 | "Ms."
119 | ]
120 | }
121 | ],
122 | "telecom": [
123 | {
124 | "system": "phone",
125 | "value": "618-820-8880",
126 | "use": "home"
127 | }
128 | ],
129 | "gender": "female",
130 | "birthDate": "1966-12-11",
131 | "address": [
132 | {
133 | "extension": [
134 | {
135 | "url": "http://hl7.org/fhir/StructureDefinition/geolocation",
136 | "extension": [
137 | {
138 | "url": "latitude",
139 | "valueDecimal": 42.599686038693555
140 | },
141 | {
142 | "url": "longitude",
143 | "valueDecimal": -71.80711557602227
144 | }
145 | ]
146 | }
147 | ],
148 | "line": [
149 | "226 Lisandro Mills",
150 | "Suite 333"
151 | ],
152 | "city": "Fitchburg",
153 | "state": "MA",
154 | "postalCode": "01420",
155 | "country": "US"
156 | }
157 | ],
158 | "maritalStatus": {
159 | "coding": [
160 | {
161 | "system": "http://hl7.org/fhir/v3/MaritalStatus",
162 | "code": "S"
163 | }
164 | ],
165 | "text": "S"
166 | },
167 | "multipleBirthBoolean": false,
168 | "communication": [
169 | {
170 | "language": {
171 | "coding": [
172 | {
173 | "system": "http://hl7.org/fhir/ValueSet/languages",
174 | "code": "en-US",
175 | "display": "English (United States)"
176 | }
177 | ]
178 | }
179 | }
180 | ]
181 | }
--------------------------------------------------------------------------------
/src/test/data/procedure.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceType": "Bundle",
3 | "id": "3544e06e-2676-4445-a22c-62bcc52deeb0",
4 | "meta": {
5 | "lastUpdated": "2019-03-12T17:10:36.155-04:00"
6 | },
7 | "type": "searchset",
8 | "total": 2,
9 | "link": [
10 | {
11 | "relation": "self",
12 | "url": "https://launch.smarthealthit.org/v/r2/fhir/Procedure?patient=c7ec9560-58cd-4a08-874b-91e3429ef1d6"
13 | }
14 | ],
15 | "entry": [
16 | {
17 | "fullUrl": "https://launch.smarthealthit.org/v/r2/fhir/Procedure/RES442219",
18 | "resource": {
19 | "resourceType": "Procedure",
20 | "id": "RES442219",
21 | "meta": {
22 | "versionId": "1",
23 | "lastUpdated": "2018-05-07T13:40:01.272-04:00",
24 | "tag": [
25 | {
26 | "system": "https://smarthealthit.org/tags",
27 | "code": "synthea-8-2017"
28 | }
29 | ]
30 | },
31 | "subject": {
32 | "reference": "Patient/c7ec9560-58cd-4a08-874b-91e3429ef1d6"
33 | },
34 | "status": "completed",
35 | "code": {
36 | "coding": [
37 | {
38 | "system": "http://snomed.info/sct",
39 | "code": "73761001",
40 | "display": "Colonoscopy"
41 | }
42 | ],
43 | "text": "Colonoscopy"
44 | },
45 | "performedPeriod": {
46 | "start": "2016-12-11T12:29:29-05:00",
47 | "end": "2016-12-11T13:11:34-05:00"
48 | },
49 | "encounter": {
50 | "reference": "Encounter/06405d1d-1feb-4b20-8a2a-daa657fb71c4"
51 | }
52 | },
53 | "search": {
54 | "mode": "match"
55 | }
56 | },
57 | {
58 | "fullUrl": "https://launch.smarthealthit.org/v/r2/fhir/Procedure/RES442212",
59 | "resource": {
60 | "resourceType": "Procedure",
61 | "id": "RES442212",
62 | "meta": {
63 | "versionId": "1",
64 | "lastUpdated": "2018-05-07T13:23:28.863-04:00",
65 | "tag": [
66 | {
67 | "system": "https://smarthealthit.org/tags",
68 | "code": "synthea-8-2017"
69 | }
70 | ]
71 | },
72 | "subject": {
73 | "reference": "Patient/c7ec9560-58cd-4a08-874b-91e3429ef1d6"
74 | },
75 | "status": "completed",
76 | "code": {
77 | "coding": [
78 | {
79 | "system": "http://snomed.info/sct",
80 | "code": "428191000124101",
81 | "display": "Documentation of current medications"
82 | }
83 | ],
84 | "text": "Documentation of current medications"
85 | },
86 | "performedDateTime": "2011-06-21T01:49:44-04:00",
87 | "encounter": {
88 | "reference": "Encounter/c5c4d1a9-5f7a-40e0-8dd8-1bbd38f1e02b"
89 | }
90 | },
91 | "search": {
92 | "mode": "match"
93 | }
94 | }
95 | ]
96 | }
--------------------------------------------------------------------------------
/src/test/data/smart-configuration.json:
--------------------------------------------------------------------------------
1 | {
2 | "authorization_endpoint": "http://launch.smarthealthit.org/v/r2/sim/eyJrIjoiMSIsImoiOiIxIiwiYiI6IjdiNjk3MzIyLTM2MDctNDZjYi1hMjQwLWMwODFiY2NiYTJlNSJ9/auth/authorize",
3 | "token_endpoint": "http://launch.smarthealthit.org/v/r2/sim/eyJrIjoiMSIsImoiOiIxIiwiYiI6IjdiNjk3MzIyLTM2MDctNDZjYi1hMjQwLWMwODFiY2NiYTJlNSJ9/auth/token",
4 | "token_endpoint_auth_methods_supported": [
5 | "client_secret_basic",
6 | "client_secret_post"
7 | ],
8 | "scopes_supported": [
9 | "openid",
10 | "profile",
11 | "fhirUser",
12 | "launch",
13 | "launch/patient",
14 | "launch/encounter",
15 | "patient/*.*",
16 | "user/*.*",
17 | "offline_access"
18 | ],
19 | "response_types_supported": [
20 | "code"
21 | ],
22 | "capabilities": [
23 | "launch-ehr",
24 | "launch-standalone",
25 | "client-public",
26 | "client-confidential-symmetric",
27 | "context-passthrough-banner",
28 | "context-passthrough-style",
29 | "context-ehr-patient",
30 | "context-ehr-encounter",
31 | "context-standalone-patient",
32 | "context-standalone-encounter",
33 | "permission-offline",
34 | "permission-patient",
35 | "permission-user"
36 | ]
37 | }
--------------------------------------------------------------------------------
/src/test/merge-objects.test.js:
--------------------------------------------------------------------------------
1 | import mergeObjects from "../store/merge-objects.js";
2 |
3 | describe("Merge templates", () => {
4 |
5 | test("merge keys in objects, replacing values where fields overlap", () => {
6 | const baseTemplate = {a:"a", b:"b"};
7 | const newTemplate = {a:"a1"};
8 | const merged = mergeObjects.merge([baseTemplate, newTemplate]);
9 | expect(merged).toEqual( {a:"a1", b:"b"} );
10 | })
11 |
12 | test("merge nested keys", () => {
13 | const baseTemplate = {
14 | b: {
15 | c: {d:"d1", e:"e1"}
16 | }
17 | };
18 | const newTemplate = {b: { c: {e: "e2", f:"f1"} } };
19 | const merged = mergeObjects.merge([baseTemplate, newTemplate]);
20 | expect(merged).toEqual({
21 | b: {
22 | c: {d:"d1", e:"e2", f:"f1"}
23 | }
24 | });
25 | })
26 |
27 | test("override properties with leading underscores", () => {
28 | const baseTemplate = {
29 | b: {
30 | c: {d:"d1", e:"e1"}
31 | }
32 | };
33 | const newTemplate = {b: { _c: {e: "e2", f:"f1"} } };
34 | const merged = mergeObjects.merge([baseTemplate, newTemplate]);
35 | expect(merged).toEqual({
36 | b: {
37 | c: {e: "e2", f:"f1"}
38 | }
39 | });
40 | });
41 |
42 | test("merge objects in arrays with matching ids", () => {
43 | const baseTemplate = {
44 | a: [ {id: "b", c:"c1", d:"d1"}, {d:"d1"} ]
45 | };
46 | const newTemplate = { a: [{id:"b", c:"c2"}] };
47 | const merged = mergeObjects.merge([baseTemplate, newTemplate]);
48 | expect(merged).toEqual({
49 | a: [ {id: "b", c:"c2", d:"d1"}, {d:"d1"} ]
50 | });
51 | })
52 |
53 | test("append objects in arrays with non-matching ids", () => {
54 | const baseTemplate = {
55 | a: [ {id: "b", c:"c1", d:"d1"}, {d:"d1"} ]
56 | };
57 | const newTemplate = { a: [{id:"b1", c:"c2"}] };
58 | const merged = mergeObjects.merge([baseTemplate, newTemplate]);
59 | expect(merged).toEqual({
60 | a: [ {id: "b", c:"c1", d:"d1"}, {d:"d1"}, {id:"b1", c:"c2"} ]
61 | });
62 | })
63 |
64 | test("remove objects in array with underscore property", () => {
65 | const baseTemplate = {
66 | a: [ {id: "b", c:"c1", d:"d1"}, {d:"d1"} ]
67 | };
68 | const newTemplate = { a: [{id:"b", _:true}] };
69 | const merged = mergeObjects.merge([baseTemplate, newTemplate]);
70 | expect(merged).toEqual({
71 | a: [ {d:"d1"} ]
72 | });
73 | })
74 |
75 | test("merge array of strings", () => {
76 | const baseTemplate = {
77 | a: [ "one", "two" ]
78 | };
79 | const newTemplate = { a: ["three"] };
80 | const merged = mergeObjects.merge([baseTemplate, newTemplate]);
81 | expect(merged).toEqual({
82 | a: [ "one", "two", "three" ]
83 | });
84 | })
85 |
86 | test("merge template with just an array", () => {
87 | const baseTemplate = [
88 | {"path": "resourceType", "test": "validateValue", "target": "Observation"},
89 | {"path": "category", "test": "validateValue", "target": "vital-sign", "id": "category"},
90 | {"name": "Source", "transform": "getHelperData", "helperDataField": "source"}
91 | ]
92 | const newTemplate = [
93 | {"id":"category", "target":"laboratory"}
94 | ];
95 | const merged = mergeObjects.merge([baseTemplate, newTemplate]);
96 | expect(merged).toEqual([
97 | {"path": "resourceType", "test": "validateValue", "target": "Observation"},
98 | {"path": "category", "test": "validateValue", "target": "laboratory", "id": "category"},
99 | {"name": "Source", "transform": "getHelperData", "helperDataField": "source"}
100 | ]);
101 | })
102 |
103 | test("merge multiple templates", () => {
104 | const templates = [
105 | {a:"a", b:"b"},
106 | {a:"a1"},
107 | {c: "c"}
108 | ];
109 | const merged = mergeObjects.merge(templates);
110 | expect(merged).toEqual( {a:"a1", b:"b", c:"c"} );
111 | })
112 | });
113 |
--------------------------------------------------------------------------------
/src/test/smart-core.test.js:
--------------------------------------------------------------------------------
1 | global.fetch = require('jest-fetch-mock');
2 | import SMART from "../smart/smart-core";
3 |
4 | import metadata from "./data/metadata.json";
5 | import smartConfiguration from "./data/smart-configuration.json";
6 |
7 | describe("Finding auth endpoints ", () => {
8 |
9 | const fhirBase = "http://launch.smarthealthit.org/v/r2/sim/eyJrIjoiMSIsImoiOiIxIiwiYiI6IjdiNjk3MzIyLTM2MDctNDZjYi1hMjQwLWMwODFiY2NiYTJlNSJ9/fhir/";
10 | const endpointError = "Authorization endpoint or token endpoint not found";
11 |
12 | const checkEndpoints = (endpoints) => {
13 | const authorizeEndpoint = "http://launch.smarthealthit.org/v/r2/sim/eyJrIjoiMSIsImoiOiIxIiwiYiI6IjdiNjk3MzIyLTM2MDctNDZjYi1hMjQwLWMwODFiY2NiYTJlNSJ9/auth/authorize";
14 | const tokenEndpoint = "http://launch.smarthealthit.org/v/r2/sim/eyJrIjoiMSIsImoiOiIxIiwiYiI6IjdiNjk3MzIyLTM2MDctNDZjYi1hMjQwLWMwODFiY2NiYTJlNSJ9/auth/token";
15 | expect(endpoints.authorizationEndpoint).toBe(authorizeEndpoint);
16 | expect(endpoints.tokenEndpoint).toBe(tokenEndpoint);
17 | };
18 |
19 | beforeEach( fetch.resetMocks )
20 |
21 | test("extract auth endpoints from capability statement", () => {
22 | fetch.mockResponse(JSON.stringify(metadata))
23 | return SMART.getCapabilityEndpoints(fhirBase).then(checkEndpoints);
24 | })
25 |
26 | test("error if no endpoints in capability statement", () => {
27 | fetch.mockResponse("{}")
28 | return expect( SMART.getCapabilityEndpoints(fhirBase) )
29 | .rejects.toMatch(endpointError);
30 | })
31 |
32 | test("error if no capability statement", () => {
33 | fetch.mockResponse("", {status: 404})
34 | return expect( SMART.getCapabilityEndpoints(fhirBase) )
35 | .rejects.toMatch(endpointError);
36 | })
37 |
38 | test("extract auth endpoints from .well-known file", () => {
39 | fetch.mockResponse(JSON.stringify(smartConfiguration))
40 | return SMART.getWellKnownEndpoints(fhirBase).then(checkEndpoints);
41 | })
42 |
43 | test("error if no endpoints in .well-known file", () => {
44 | fetch.mockResponse("{}")
45 | return expect( SMART.getWellKnownEndpoints(fhirBase) )
46 | .rejects.toMatch(endpointError);
47 | })
48 |
49 | test("use .well-know file if available", () => {
50 | fetch.mockResponse(JSON.stringify(smartConfiguration))
51 | return SMART.getAuthEndpoints(fhirBase).then(checkEndpoints);
52 | })
53 |
54 | test("fall back to capability statement if no .well-known", () => {
55 | fetch.mockResponses([
56 | "", {status: 404}
57 | ],[
58 | JSON.stringify(metadata),
59 | ])
60 | return SMART.getAuthEndpoints(fhirBase).then(checkEndpoints);
61 | })
62 |
63 | test("error if no auth endpoints", () => {
64 | fetch.mockResponse("", {status: 404})
65 | return expect( SMART.getAuthEndpoints(fhirBase) )
66 | .rejects.toMatch(endpointError);
67 | })
68 | });
--------------------------------------------------------------------------------
/src/test/util/base64toblob.js:
--------------------------------------------------------------------------------
1 | //via https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript
2 | export default (b64Data, contentType, sliceSize) => {
3 | contentType = contentType || '';
4 | sliceSize = sliceSize || 512;
5 |
6 | var byteCharacters = atob(b64Data);
7 | var byteArrays = [];
8 |
9 | for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
10 | var slice = byteCharacters.slice(offset, offset + sliceSize);
11 |
12 | var byteNumbers = new Array(slice.length);
13 | for (var i = 0; i < slice.length; i++) {
14 | byteNumbers[i] = slice.charCodeAt(i);
15 | }
16 |
17 | var byteArray = new Uint8Array(byteNumbers);
18 |
19 | byteArrays.push(byteArray);
20 | }
21 |
22 | var blob = new Blob(byteArrays, {type: contentType});
23 | return blob;
24 | }
25 |
--------------------------------------------------------------------------------
/upload-backends/node-server-aws/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const app = express();
3 | const cors = require("cors");
4 | const aws = require("aws-sdk");
5 |
6 | // Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in .env file or env vars
7 | // IAM config for this user must support PUT to the S3 bucket specified in config.
8 | require('dotenv').config();
9 |
10 | const config = {
11 | port: 8000,
12 | name: "Test research project",
13 | destinationDir: "uploads",
14 | infoUrl: "http://google.com",
15 | successMessage: "Thank you - your information has been transmitted!",
16 |
17 | //requires an AWS bucket with CORS enabled
18 | bucket: "procure-test1",
19 |
20 | expires: 60*60 //one hour
21 |
22 | // continueLabel: "Link records with your account" ,
23 | // continueUrl: "http://google.com"
24 | };
25 |
26 | const s3 = new aws.S3({signatureVersion: 'v4'});
27 |
28 | app.use(cors());
29 |
30 | app.get("/manifest/:userid", (req, res) => {
31 | const fileName = req.params.userid + "-" + (new Date()).getTime() + ".zip";
32 | const params = {
33 | Bucket: config.bucket,
34 | Key: fileName,
35 | ContentType: "application/zip",
36 | Expires: config.expires
37 | };
38 | s3.getSignedUrl("putObject", params, (error, uploadUrl) => {
39 | if (error) {
40 | console.log(error)
41 | res.status(500).send("An error occurred generating a signed url.")
42 | } else {
43 | res.send({
44 | name: config.name,
45 | uploadUrl: uploadUrl,
46 | infoUrl: config.infoUrl,
47 | continueLabel: config.continueLabel,
48 | continueUrl: config.continueUrl,
49 | successMessage: config.successMessage
50 | })
51 | }
52 | });
53 | })
54 |
55 | app.listen(config.port, () => console.log(`Upload server listening on port ${config.port}!`))
--------------------------------------------------------------------------------
/upload-backends/node-server-aws/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "upload-server-aws",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "nodemon ./index.js"
9 | },
10 | "author": "",
11 | "license": "MIT",
12 | "dependencies": {
13 | "aws-sdk": "^2.545.0",
14 | "cors": "^2.8.5",
15 | "dotenv": "^8.1.0",
16 | "express": "^4.17.1"
17 | },
18 | "devDependencies": {
19 | "nodemon": "^2.0.4"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/upload-backends/node-server/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const app = express();
3 | const cors = require("cors");
4 | const bodyParser = require("body-parser");
5 | const fs = require("fs");
6 | const path = require("path");
7 |
8 | const config = {
9 | port: 8000,
10 | speed: 3000,
11 | destinationDir: "uploaded_files",
12 | name: "Test research project",
13 | infoUrl: "http://google.com",
14 | successMessage: "Thank you - your information has been transmitted!",
15 | continueLabel: "Return to research portal",
16 | continueUrl: "http://google.com"
17 | };
18 |
19 | app.use(cors());
20 |
21 | app.get("/manifest/:userid", (req, res) => {
22 | const fileName = req.params.userid + "-" + (new Date()).getTime() + ".zip";
23 | const uploadUrl = req.protocol + '://' + req.get('host') + "/" + fileName;
24 | res.send({
25 | name: config.name,
26 | uploadUrl: uploadUrl,
27 | infoUrl: config.infoUrl,
28 | continueLabel: config.continueLabel,
29 | continueUrl: config.continueUrl,
30 | successMessage: config.successMessage
31 | })
32 | })
33 |
34 | app.put("/:fileName", bodyParser.raw({inflate:false, limit: "5mb"}), (req, res) => {
35 | const filePath = path.join(__dirname, config.destinationDir, path.basename(req.params.fileName))
36 | fs.writeFile(filePath, req.body, "binary", (err) => {
37 | if (err) {
38 | console.log(err)
39 | res.status(500).send("An error occurred saving the file on the server.")
40 | } else {
41 | console.log(`Saved upload to ${filePath}`)
42 | setTimeout( () => {
43 | res.sendStatus(200);
44 |
45 | }, config.speed);
46 | }
47 | });
48 | });
49 |
50 | app.listen(config.port, () => console.log(`Upload server listening on port ${config.port}!`))
--------------------------------------------------------------------------------
/upload-backends/node-server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "upload-server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "nodemon ./index.js"
9 | },
10 | "author": "",
11 | "license": "MIT",
12 | "dependencies": {
13 | "body-parser": "^1.19.0",
14 | "cors": "^2.8.5",
15 | "express": "^4.17.1"
16 | },
17 | "devDependencies": {
18 | "nodemon": "^2.0.4"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------