Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Carin Blue Button support #35

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions default.env
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,16 @@ VITE_VERSION_STRING=

# SMART on FHIR client id configurations:
# Ensure that your development client is registered with the proper redirect uris
#EHRs
VITE_EPIC_CLIENT_ID=
VITE_CERNER_CLIENT_ID=

#Insurance
VITE_AETNA_CLIENT_ID=
VITE_CAREFIRST_CLIENT_ID=
VITE_HUMANA_CLIENT_ID=
VITE_ACENTRA_CLIENT_ID=

# SOF for HIMSS 2024 only:
#VITE_EPIC_HIMSS_CLIENT_ID=
#VITE_ECW_HIMSS_CLIENT_ID=
Expand Down
4 changes: 4 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ interface ImportMetaEnv {
readonly VITE_ECW_HIMSS_CLIENT_ID: string
readonly VITE_EPIC_CLIENT_ID: string
readonly VITE_CERNER_CLIENT_ID: string
readonly VITE_AETNA_CLIENT_ID: string
readonly VITE_CAREFIRST_CLIENT_ID: string
readonly VITE_HUMANA_CLIENT_ID: string
readonly VITE_ACENTRA_CLIENT_ID: string
readonly VITE_MEDITECH_BEARER_TOKEN: string
readonly VITE_API_BASE: string
readonly VITE_VIEWER_BASE: string
Expand Down
222 changes: 222 additions & 0 deletions src/lib/components/app/FetchCARINBB.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
<script lang="ts">
import {
Button,
Col,
FormGroup,
Input,
Label,
Row,
Spinner } from 'sveltestrap';

import { SOF_HOSTS } from '$lib/config';
import type { ResourceRetrieveEvent, SOFAuthEvent, SOFHost } from '$lib/utils/types';
import { authorize, getResourcesWithReferences } from '$lib/utils/sofClient.js';
import { createEventDispatcher, onMount } from 'svelte';

// For "quick sample" in demo
import { EXAMPLE_IPS, IPS_DEFAULT } from '$lib/config';
import type { IPSRetrieveEvent } from '$lib/utils/types';

const authDispatch = createEventDispatcher<{'sof-auth-init': SOFAuthEvent; 'sof-auth-fail': SOFAuthEvent}>();
const resourceDispatch = createEventDispatcher<{'update-resources': ResourceRetrieveEvent}>();
let processing = false;
let loadingSample = false;
let fetchError = "";
let result: ResourceRetrieveEvent = {
resources: undefined
};

let sofHostSelection = SOF_HOSTS[0].id;
let sofHost:SOFHost | undefined = SOF_HOSTS.find(e => e.id == sofHostSelection);

$: {
if (sofHostSelection) {
sofHost = SOF_HOSTS.find(e => e.id == sofHostSelection);
}
}

async function prepareIps() {
fetchError = "";
try {
if (sofHost) {
try {
authorize(sofHost.url, sofHost.clientId);// , sofHost.clientSecret);
authDispatch('sof-auth-init', { data: true });
} catch (e) {
authDispatch('sof-auth-fail', { data: false });
}
}
} catch (e) {
console.log('Failed', e);
fetchError = "Error preparing IPS";
}
}

onMount(async function() {
let key = sessionStorage.getItem('SMART_KEY');
if (key) {
let token = sessionStorage.getItem(JSON.parse(key));
if (token) {
let url = JSON.parse(token).serverUrl;
let sofHostAuthd = SOF_HOSTS.find(e => e.url == url);
if (sofHostAuthd) {
sofHost = sofHostAuthd;
sofHostSelection = sofHost.id;
await fetchData();
sessionStorage.removeItem(key);
sessionStorage.removeItem('SMART_KEY');
}
}
}
});

function endSession() {
let key = sessionStorage.getItem('SMART_KEY');
if (key) {
sessionStorage.removeItem(JSON.parse(key));
sessionStorage.removeItem('SMART_KEY');
}
}

async function fetchData() {
processing = true;
try {
let resources = await getResourcesWithReferences(1);
result.resources = resources;
console.log(resources)
processing = false;
return resourceDispatch('update-resources', result);
} catch (e: any) {
console.log(e.message);
fetchError = e.message;
processing = false;
endSession();
}
}

// Demo quick sample loader
let defaultUrl = EXAMPLE_IPS[IPS_DEFAULT];
const ipsDispatch = createEventDispatcher<{'ips-retrieved': IPSRetrieveEvent}>();
let ipsResult: IPSRetrieveEvent = {
ips: undefined
};
async function quickLoad() {
fetchError = "";
loadingSample = true;
try {
let content;
let hostname;
const contentResponse = await fetch(defaultUrl!, {
headers: { accept: 'application/fhir+json' }
}).then(function(response) {
if (!response.ok) {
// make the promise be rejected if we didn't get a 2xx response
throw new Error("Unable to fetch IPS", {cause: response});
} else {
return response;
}
});
content = await contentResponse.json();
hostname = defaultUrl?.hostname;
loadingSample = false
ipsResult = {
ips: content,
source: hostname
};
ipsDispatch('ips-retrieved', ipsResult);
} catch (e) {
loadingSample = false;
console.log('Failed', e);
fetchError = "Error preparing IPS";
}
}

async function testAPI() {
fetchError = "";
loadingSample = true;
try {
let content;
let hostname;
const contentResponse = await fetch("/api/proxy", {
method: 'POST',
headers: { accept: 'application/fhir+json' },
body: JSON.stringify({ url: defaultUrl })
}).then(function(response) {
if (!response.ok) {
// make the promise be rejected if we didn't get a 2xx response
throw new Error("Unable to fetch IPS", {cause: response});
} else {
return response;
}
});
content = await contentResponse.json();
loadingSample = false
console.log(content);
} catch (e) {
loadingSample = false;
console.log('Failed', e);
fetchError = "Error preparing IPS";
}
}
</script>
<form on:submit|preventDefault={() => prepareIps()}>
<FormGroup>
<Label>Fetch US Core data via SMART authorization</Label>
{#each SOF_HOSTS as host}
<Row class="mx-2">
<Input type="radio" bind:group={sofHostSelection} value={host.id} label={host.name} />
{#if host.note}
<p class="text-secondary" style="margin-left:25px">{@html host.note}</p>
{/if}
</Row>
{/each}
</FormGroup>

<Row>
<Col xs="auto">
<Button color="primary" style="width:fit-content" disabled={processing || loadingSample} type="submit">
{#if !processing}
Fetch Data
{:else}
Fetching...
{/if}
</Button>
</Col>
<Col xs="auto">
<Button
color="secondary"
outline
style="width:fit-content"
disabled={processing || loadingSample}
type="button"
on:click={() => quickLoad()}>
{#if !loadingSample}
Quick Sample
{:else}
Loading...
{/if}
</Button>
</Col>
<Col xs="auto">
<Button
color="secondary"
outline
style="width:fit-content"
disabled={processing || loadingSample}
type="button"
on:click={() => testAPI()}>
{#if !loadingSample}
Test API
{:else}
Testing...
{/if}
</Button>
</Col>
{#if processing || loadingSample}
<Col xs="auto" class="d-flex align-items-center px-0">
<Spinner color="primary" type="border" size="md"/>
</Col>
{/if}
</Row>
</form>
<span class="text-danger">{fetchError}</span>
10 changes: 6 additions & 4 deletions src/lib/components/app/FetchSoF.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@
try {
if (sofHost) {
try {
authorize(sofHost.url, sofHost.clientId);
authDispatch('sof-auth-init');
authorize(sofHost.url, sofHost.clientId);// , sofHost.clientSecret);
authDispatch('sof-auth-init', { data: true });
} catch (e) {
authDispatch('sof-auth-fail')
authDispatch('sof-auth-fail', { data: false });
}
}
} catch (e) {
Expand Down Expand Up @@ -86,7 +86,9 @@
console.log(resources)
processing = false;
return resourceDispatch('update-resources', result);
} catch (e) {
} catch (e: any) {
console.log(e.message);
fetchError = e.message;
processing = false;
endSession();
}
Expand Down
31 changes: 25 additions & 6 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,26 @@ export const SOF_HOSTS = [
url: "https://launch.smarthealthit.org/v/r4/sim/WzMsIiIsIiIsIkFVVE8iLDAsMCwwLCIiLCIiLCIiLCIiLCIiLCIiLCIiLDAsMF0/fhir",
clientId: "<no client id>",
note: "Credentials provided"
}
},
{
id: "aetna",
name: "AETNA Insurance Sandbox",
url: "https://vteapif1.aetna.com/fhirdemo/v1/patientaccess",
clientId: import.meta.env.VITE_AETNA_CLIENT_ID,
note: "VTETestUser01 / FHIRdemo2020"
},
{
id: "carefirst",
name: "CareFirst",
url: "https://chpdc-api-sita.carefirst.com/v1/fhir/patientaccess",
// url: "https://chpmd-api-sita.carefirst.com/v1/fhir/patientaccess",
// url: "https://dsnp-api-sita.carefirst.com/v1/fhir/patientaccess",
// url: "https://mapd-api-sita.carefirst.com/v1/fhir/patientaccess",
// url: "https://mhbe-api-sita.carefirst.com/v1/fhir/patientaccess",
// url: "https://egwp-api-sita.carefirst.com/v1/fhir/patientaccess",
clientId: import.meta.env.VITE_CAREFIRST_CLIENT_ID,
note: "Credentials provided"
},
];

export const BEARER_AUTHORIZATION = {
Expand Down Expand Up @@ -75,19 +94,19 @@ export const SOF_PATIENT_RESOURCES = [
'AllergyIntolerance',
// 'MedicationStatement', // Not in EPIC USCDI R4
'MedicationRequest',
// 'Medication', // can't search by patient; "Only an _ID search is allowed."
// 'Medication', // Pulled in via references - can't search by patient; "Only an _ID search is allowed."
'Condition',
'Encounter',
// 'Observation', // "Must have either code or category."
// 'Organization', // can't search by patient; "Only an _ID search is allowed."
// 'Observation', // Handle specially for IPS codes - "Must have either code or category."
// 'Organization', // Pulled in via references - can't search by patient; "Only an _ID search is allowed."
'Immunization',
// 'Device',
// 'DeviceUseStatement', // Not in EPIC USCDI R4
'DiagnosticReport', // TODO change to subject
// 'ImagingStudy', // Not in EPIC USCDI R4
// 'Media', // Not in EPIC USCDI R4
// 'Practitioner', // can't search by patient; "Either name, family, or identifier is a required parameter."
// 'PractitionerRole', // can't search by patient; "An identifier, practitioner, organization, location, or specialty parameter is required."
// 'Practitioner', // Pulled in via references - can't search by patient; "Either name, family, or identifier is a required parameter."
// 'PractitionerRole', // Pulled in via references - can't search by patient; "An identifier, practitioner, organization, location, or specialty parameter is required."
'Procedure', // TODO change to subject
// 'Specimen', // Not in EPIC USCDI R4
];
Expand Down
Loading
Loading