Skip to content

Commit

Permalink
Add fhir_servers table + getFhirServerConfigs function (#203)
Browse files Browse the repository at this point in the history
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
m-goggins and pre-commit-ci[bot] authored Dec 11, 2024
1 parent 541172a commit 884cd34
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 52 deletions.
11 changes: 11 additions & 0 deletions query-connector/flyway/sql/V02_01__add_fhir_servers.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS fhir_servers (
id UUID DEFAULT uuid_generate_v4 (),
name TEXT,
hostname TEXT,
headers JSON DEFAULT NULL
);

CREATE INDEX IF NOT EXISTS fhir_servers_id_index ON fhir_servers (id);
CREATE INDEX IF NOT EXISTS fhir_servers_name_index ON fhir_servers (name);

INSERT INTO fhir_servers (name, hostname) VALUES ('Public HAPI: Direct','https://hapi.fhir.org/baseR4');
8 changes: 8 additions & 0 deletions query-connector/src/app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,11 @@ export const INTENTIONAL_EMPTY_STRING_FOR_CONCEPT_VERSION = "";
// we clean up the DB migration. Leaving these in until we can clean these up
// in the migration schema
export const INTENTIONAL_EMPTY_STRING_FOR_GEM_CODE = "";

// Define the type for the FHIR server configurations
export type FhirServerConfig = {
id: string;
name: string;
hostname: string;
headers: Record<string, string>;
};
39 changes: 39 additions & 0 deletions query-connector/src/app/database-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
INTENTIONAL_EMPTY_STRING_FOR_GEM_CODE,
ValueSet,
ersdToDibbsConceptMap,
FhirServerConfig,
} from "./constants";
import { encode } from "base-64";
import {
Expand Down Expand Up @@ -885,3 +886,41 @@ export async function checkDBForData() {
result.rows.length > 0 && parseFloat(result.rows[0].estimated_count) > 0
);
}

//Cache for FHIR server configurations
let cachedFhirServerConfigs: Promise<FhirServerConfig[]> | null = null;

/**
* Fetches all FHIR server configurations from the database and caches the result.
* @param forceRefresh - Optional param to determine if the cache should be refreshed.
* @returns An array of FHIR server configurations.
*/
export async function getFhirServerConfigs(forceRefresh = false) {
if (forceRefresh || !cachedFhirServerConfigs) {
cachedFhirServerConfigs = (async () => {
const query = `SELECT * FROM fhir_servers;`;
const result = await dbClient.query(query);
return result.rows;
})();
}
return cachedFhirServerConfigs;
}

/**
* Fetches all FHIR server names from the database to make them available for selection on the front end/client side.
* @returns An array of FHIR server names.
*/
export async function getFhirServerNames(): Promise<string[]> {
const configs = await getFhirServerConfigs();
return configs.map((config) => config.name);
}

/**
* Fetches the configuration for a FHIR server from the database.
* @param fhirServerName - The name of the FHIR server to fetch the configuration for.
* @returns The configuration for the FHIR server.
*/
export async function getFhirServerConfig(fhirServerName: string) {
const configs = await getFhirServerConfigs();
return configs.find((config) => config.name === fhirServerName);
}
97 changes: 57 additions & 40 deletions query-connector/src/app/fhir-servers.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,43 @@
import fetch, { RequestInit, HeaderInit, Response } from "node-fetch";
import { v4 as uuidv4 } from "uuid";
import { FHIR_SERVERS } from "./constants";
import { FhirServerConfig } from "./constants";
import https from "https";
/**
* Defines the model for a FHIR server configuration
*/
type FHIR_SERVER_CONFIG = {
hostname: string;
init: RequestInit;
};

type DevFhirServerConfig = FhirServerConfig & { trustSelfSigned?: boolean };

/**
* The configurations for the FHIR servers currently supported.
*/
const localE2EFhirServer =
process.env.E2E_LOCAL_FHIR_SERVER ?? "http://hapi-fhir-server:8080/fhir";
export const fhirServers: Record<FHIR_SERVERS, FHIR_SERVER_CONFIG> = {
export const fhirServers: Record<string, DevFhirServerConfig> = {
"HELIOS Meld: Direct": {
id: "HELIOS Meld: Direct",
name: "HELIOS Meld: Direct",
hostname: "https://gw.interop.community/HeliosConnectathonSa/open",
init: {} as RequestInit,
headers: {},
},
"HELIOS Meld: eHealthExchange": configureEHX("MeldOpen"),
"JMC Meld: Direct": {
id: "JMC Meld: Direct",
name: "JMC Meld: Direct",
hostname: "https://gw.interop.community/JMCHeliosSTISandbox/open",
init: {} as RequestInit,
headers: {},
},
"JMC Meld: eHealthExchange": configureEHX("JMCHelios"),
"Public HAPI: Direct": {
hostname: "https://hapi.fhir.org/baseR4",
init: {} as RequestInit,
},
"Local e2e HAPI Server: Direct": {
id: "Local e2e HAPI Server: Direct",
name: "Local e2e HAPI Server: Direct",
hostname: localE2EFhirServer,
init: {} as RequestInit,
headers: {},
},
"OpenEpic: eHealthExchange": configureEHX("OpenEpic"),
"CernerHelios: eHealthExchange": configureEHX("CernerHelios"),
"OPHDST Meld: Direct": {
id: "OPHDST Meld: Direct",
name: "OPHDST Meld: Direct",
hostname: "https://gw.interop.community/CDCSepHL7Connectatho/open",
init: {} as RequestInit,
headers: {},
},
};

Expand All @@ -47,31 +46,27 @@ export const fhirServers: Record<FHIR_SERVERS, FHIR_SERVER_CONFIG> = {
* @param xdestination The x-destination header value
* @returns The configuration for the server
*/
function configureEHX(xdestination: string): FHIR_SERVER_CONFIG {
let init: RequestInit = {
method: "GET",
headers: {
Accept: "application/json, application/*+json, */*",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/fhir+json; charset=UTF-8",
"X-DESTINATION": xdestination,
"X-POU": "PUBHLTH",
"X-Request-Id": uuidv4(),
prefer: "return=representation",
"Cache-Control": "no-cache",
} as HeaderInit,
// Trust eHealth Exchange's self-signed certificate
agent: new https.Agent({
rejectUnauthorized: false,
}),
function configureEHX(xdestination: string): DevFhirServerConfig {
const headers = {
Accept: "application/json, application/*+json, */*",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/fhir+json; charset=UTF-8",
"X-DESTINATION": xdestination,
"X-POU": "PUBHLTH",
"X-Request-Id": uuidv4(),
prefer: "return=representation",
"Cache-Control": "no-cache",
};
if (xdestination === "CernerHelios" && init.headers) {
(init.headers as Record<string, string>)["OAUTHSCOPES"] =
if (xdestination === "CernerHelios") {
(headers as Record<string, string>)["OAUTHSCOPES"] =
"system/Condition.read system/Encounter.read system/Immunization.read system/MedicationRequest.read system/Observation.read system/Patient.read system/Procedure.read system/MedicationAdministration.read system/DiagnosticReport.read system/RelatedPerson.read";
}
return {
id: xdestination,
name: xdestination,
hostname: "https://concept01.ehealthexchange.org:52780/fhirproxy/r4/",
init: init,
headers,
trustSelfSigned: true,
};
}

Expand All @@ -84,10 +79,32 @@ class FHIRClient {
private hostname: string;
private init;

constructor(server: FHIR_SERVERS) {
const config = fhirServers[server];
constructor(server: string, configurations: FhirServerConfig[]) {
// Get the configuration for the server if it exists
let config: DevFhirServerConfig | undefined = configurations.find(
(config) => config.name === server,
);
if (!config) {
config = fhirServers[server];
}

if (!config) {
throw new Error(`No configuration found for server: ${server}`);
}
// Set server hostname
this.hostname = config.hostname;
this.init = config.init;
// Set request init, including headers
let init: RequestInit = {
method: "GET",
headers: config.headers as HeaderInit,
};
// Trust eHealth Exchange's self-signed certificate
if (config.trustSelfSigned) {
init.agent = new https.Agent({
rejectUnauthorized: false,
});
}
this.init = init;
}

async get(path: string): Promise<Response> {
Expand Down
4 changes: 3 additions & 1 deletion query-connector/src/app/query-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { CustomQuery } from "./CustomQuery";
import { GetPhoneQueryFormats } from "./format-service";
import { formatValueSetsAsQuerySpec } from "./format-service";
import { getFhirServerConfigs } from "./database-service";

/**
* The query response when the request source is from the Viewer UI.
Expand Down Expand Up @@ -150,7 +151,8 @@ export async function UseCaseQuery(
queryValueSets: ValueSet[],
queryResponse: QueryResponse = {},
): Promise<QueryResponse> {
const fhirClient = new FHIRClient(request.fhir_server);
const fhirServerConfigs = await getFhirServerConfigs();
const fhirClient = new FHIRClient(request.fhir_server, fhirServerConfigs);

if (!queryResponse.Patient || queryResponse.Patient.length === 0) {
await patientQuery(request, fhirClient, queryResponse);
Expand Down
22 changes: 15 additions & 7 deletions query-connector/src/app/query/components/SearchForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
Mode,
} from "@/app/constants";
import { UseCaseQueryResponse, UseCaseQuery } from "@/app/query-service";
import { fhirServers } from "@/app/fhir-servers";
import { fhirServers as hardcodedFhirServers } from "@/app/fhir-servers";
import styles from "./searchForm/searchForm.module.scss";
import { FormatPhoneAsDigits } from "@/app/format-service";
import { PAGE_TITLES } from "@/app/query/components/stepIndicator/StepIndicator";
Expand All @@ -27,7 +27,8 @@ interface SearchFormProps {
) => void;
setMode: (mode: Mode) => void;
setLoading: (loading: boolean) => void;
fhirServer: FHIR_SERVERS;
fhirServers: string[];
selectedFhirServer: FHIR_SERVERS;
setFhirServer: React.Dispatch<React.SetStateAction<FHIR_SERVERS>>;
}

Expand All @@ -39,19 +40,21 @@ interface SearchFormProps {
* @param root0.setLoading - The function to set the loading state.
* @param root0.setPatientDiscoveryQueryResponse - callback function to set the
* patient for use in future steps
* @param root0.fhirServer - server to do the query against
* @param root0.selectedFhirServer - server to do the query against
* @param root0.setFhirServer - callback function to update specified query
* @param root0.fhirServers - list of available FHIR servers to query against, from the DB & hardcoded (for now)
* @returns - The SearchForm component.
*/
const SearchForm: React.FC<SearchFormProps> = ({
const SearchForm: React.FC<SearchFormProps> = function SearchForm({
useCase,
setUseCase,
setPatientDiscoveryQueryResponse,
setMode,
setLoading,
fhirServer,
fhirServers,
selectedFhirServer: fhirServer,
setFhirServer,
}) => {
}) {
//Set the patient options based on the demoOption
const [firstName, setFirstName] = useState<string>("");
const [lastName, setLastName] = useState<string>("");
Expand Down Expand Up @@ -110,6 +113,11 @@ const SearchForm: React.FC<SearchFormProps> = ({
window.scrollTo(0, 0);
}, []);

const combinedFhirServers = [
...fhirServers,
...Object.keys(hardcodedFhirServers),
];

return (
<>
<form onSubmit={HandleSubmit}>
Expand Down Expand Up @@ -174,7 +182,7 @@ const SearchForm: React.FC<SearchFormProps> = ({
}}
required
>
{Object.keys(fhirServers).map((fhirServer: string) => (
{combinedFhirServers.map((fhirServer: string) => (
<option key={fhirServer} value={fhirServer}>
{fhirServer}
</option>
Expand Down
15 changes: 12 additions & 3 deletions query-connector/src/app/query/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { UseCaseQueryResponse } from "../query-service";
import ResultsView from "./components/ResultsView";
import PatientSearchResults from "./components/PatientSearchResults";
Expand All @@ -20,9 +20,10 @@ import StepIndicator, {
} from "./components/stepIndicator/StepIndicator";
import SiteAlert from "./designSystem/SiteAlert";
import { Patient } from "fhir/r4";
import { getFhirServerNames } from "@/app/database-service";

/**
* Parent component for the query page. Based on the mode, it will display the search
* Client side parent component for the query page. Based on the mode, it will display the search
* form, the results of the query, or the multiple patients view.
* @returns - The Query component.
*/
Expand All @@ -33,6 +34,13 @@ const Query: React.FC = () => {
const [fhirServer, setFhirServer] = useState<FHIR_SERVERS>(
DEFAULT_DEMO_FHIR_SERVER,
);
const [fhirServers, setFhirServers] = useState<string[]>([]);

useEffect(() => {
getFhirServerNames().then((servers) => {
setFhirServers(servers);
});
}, []);

const [patientDiscoveryQueryResponse, setPatientDiscoveryQueryResponse] =
useState<UseCaseQueryResponse>({});
Expand Down Expand Up @@ -66,7 +74,8 @@ const Query: React.FC = () => {
setMode={setMode}
setLoading={setLoading}
setPatientDiscoveryQueryResponse={setPatientDiscoveryQueryResponse}
fhirServer={fhirServer}
fhirServers={fhirServers}
selectedFhirServer={fhirServer}
setFhirServer={setFhirServer}
/>
)}
Expand Down
2 changes: 1 addition & 1 deletion query-connector/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es2017",
"lib": [
"dom",
"dom.iterable",
Expand Down

0 comments on commit 884cd34

Please sign in to comment.