Skip to content

Commit

Permalink
FHIR Server Configuration (#248)
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
nickclyde and pre-commit-ci[bot] authored Jan 13, 2025
1 parent 2158ac9 commit d063847
Show file tree
Hide file tree
Showing 16 changed files with 911 additions and 172 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
-- Add columns for connection tracking
ALTER TABLE fhir_servers
ADD COLUMN last_connection_attempt TIMESTAMP WITH TIME ZONE,
ADD COLUMN last_connection_successful BOOLEAN DEFAULT FALSE;

-- Update existing rows to have null values for new columns
UPDATE fhir_servers
SET last_connection_attempt = NULL,
last_connection_successful = NULL;

-- Convert name index to unique constraint
DROP INDEX fhir_servers_name_index;
CREATE UNIQUE INDEX fhir_servers_name_index ON fhir_servers (name);

-- Truncate existing table
TRUNCATE TABLE fhir_servers;

-- Direct FHIR servers
INSERT INTO fhir_servers (name, hostname, last_connection_attempt, last_connection_successful)
VALUES
('Public HAPI: Direct', 'https://hapi.fhir.org/baseR4', current_timestamp, true),
('HELIOS Meld: Direct', 'https://gw.interop.community/HeliosConnectathonSa/open', current_timestamp, true),
('JMC Meld: Direct', 'https://gw.interop.community/JMCHeliosSTISandbox/open', current_timestamp, true),
('Local e2e HAPI Server: Direct', 'http://hapi-fhir-server:8080/fhir', current_timestamp, true),
('OPHDST Meld: Direct', 'https://gw.interop.community/CDCSepHL7Connectatho/open', current_timestamp, true);

-- eHealthExchange FHIR servers
INSERT INTO fhir_servers (name, hostname, headers, last_connection_attempt, last_connection_successful)
VALUES
('HELIOS Meld: eHealthExchange',
'https://concept01.ehealthexchange.org:52780/fhirproxy/r4/',
'{
"Accept": "application/json, application/*+json, */*",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/fhir+json; charset=UTF-8",
"X-DESTINATION": "MeldOpen",
"X-POU": "PUBHLTH",
"prefer": "return=representation",
"Cache-Control": "no-cache"
}',
current_timestamp,
true),
('JMC Meld: eHealthExchange',
'https://concept01.ehealthexchange.org:52780/fhirproxy/r4/',
'{
"Accept": "application/json, application/*+json, */*",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/fhir+json; charset=UTF-8",
"X-DESTINATION": "JMCHelios",
"X-POU": "PUBHLTH",
"prefer": "return=representation",
"Cache-Control": "no-cache"
}',
current_timestamp,
true),
('OpenEpic: eHealthExchange',
'https://concept01.ehealthexchange.org:52780/fhirproxy/r4/',
'{
"Accept": "application/json, application/*+json, */*",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/fhir+json; charset=UTF-8",
"X-DESTINATION": "OpenEpic",
"X-POU": "PUBHLTH",
"prefer": "return=representation",
"Cache-Control": "no-cache"
}',
current_timestamp,
true),
('CernerHelios: eHealthExchange',
'https://concept01.ehealthexchange.org:52780/fhirproxy/r4/',
'{
"Accept": "application/json, application/*+json, */*",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/fhir+json; charset=UTF-8",
"X-DESTINATION": "CernerHelios",
"X-POU": "PUBHLTH",
"prefer": "return=representation",
"Cache-Control": "no-cache",
"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"
}',
current_timestamp,
true);
14 changes: 7 additions & 7 deletions query-connector/src/app/api/query/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import {
import { parsePatientDemographics } from "./parsing-service";
import {
USE_CASES,
FHIR_SERVERS,
FhirServers,
USE_CASE_DETAILS,
INVALID_FHIR_SERVERS,
INVALID_USE_CASE,
Expand All @@ -20,7 +18,10 @@ import {
} from "../../constants";

import { handleRequestError } from "./error-handling-service";
import { getSavedQueryByName } from "@/app/database-service";
import {
getFhirServerNames,
getSavedQueryByName,
} from "@/app/database-service";
import { unnestValueSetsFromQuery } from "@/app/utils";

/**
Expand Down Expand Up @@ -75,16 +76,15 @@ export async function POST(request: NextRequest) {
const params = request.nextUrl.searchParams;
const use_case = params.get("use_case");
const fhir_server = params.get("fhir_server");
const fhirServers = await getFhirServerNames();

if (!use_case || !fhir_server) {
const OperationOutcome = await handleRequestError(MISSING_API_QUERY_PARAM);
return NextResponse.json(OperationOutcome);
} else if (!Object.keys(USE_CASE_DETAILS).includes(use_case)) {
const OperationOutcome = await handleRequestError(INVALID_USE_CASE);
return NextResponse.json(OperationOutcome);
} else if (
!Object.values(FhirServers).includes(fhir_server as FHIR_SERVERS)
) {
} else if (!Object.values(fhirServers).includes(fhir_server)) {
const OperationOutcome = await handleRequestError(INVALID_FHIR_SERVERS);
return NextResponse.json(OperationOutcome);
}
Expand All @@ -97,7 +97,7 @@ export async function POST(request: NextRequest) {
// Add params & patient identifiers to UseCaseRequest
const UseCaseRequest: UseCaseQueryRequest = {
use_case: use_case as USE_CASES,
fhir_server: fhir_server as FHIR_SERVERS,
fhir_server: fhir_server,
...(PatientIdentifiers.first_name && {
first_name: PatientIdentifiers.first_name,
}),
Expand Down
24 changes: 5 additions & 19 deletions query-connector/src/app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,30 +34,14 @@ export const USE_CASE_DETAILS = {

export type USE_CASES = keyof typeof USE_CASE_DETAILS;

/**
* The FHIR servers that can be used in the app
*/
export const FhirServers = [
"HELIOS Meld: Direct",
"HELIOS Meld: eHealthExchange",
"JMC Meld: Direct",
"JMC Meld: eHealthExchange",
"Public HAPI: Direct",
"Local e2e HAPI Server: Direct",
"OpenEpic: eHealthExchange",
"CernerHelios: eHealthExchange",
"OPHDST Meld: Direct",
] as const;
export type FHIR_SERVERS = (typeof FhirServers)[number];

//Create type to specify the demographic data fields for a patient
export type DemoDataFields = {
FirstName: string;
LastName: string;
DOB: string;
MRN: string;
Phone: string;
FhirServer: FHIR_SERVERS;
FhirServer: string;
UseCase: USE_CASES;
};

Expand All @@ -71,7 +55,7 @@ export type PatientType =
| "newborn-screening-pass"
| "sti-syphilis-positive";

export const DEFAULT_DEMO_FHIR_SERVER = "HELIOS Meld: Direct";
export const DEFAULT_DEMO_FHIR_SERVER = "Public HAPI: Direct";
/*
* Common "Hyper Unlucky" patient data used for all non-newborn screening use cases
*/
Expand Down Expand Up @@ -340,13 +324,15 @@ export type FhirServerConfig = {
id: string;
name: string;
hostname: string;
last_connection_attempt: Date;
last_connection_successful: boolean;
headers: Record<string, string>;
};

export const INVALID_USE_CASE = `Invalid use_case. Please provide a valid use_case. Valid use_cases include ${Object.keys(
USE_CASE_DETAILS,
)}.`;
export const INVALID_FHIR_SERVERS = `Invalid fhir_server. Please provide a valid fhir_server. Valid fhir_servers include ${FhirServers}.`;
export const INVALID_FHIR_SERVERS = `Invalid fhir_server. Please provide a valid fhir_server.`;
export const RESPONSE_BODY_IS_NOT_PATIENT_RESOURCE =
"Request body is not a Patient resource.";
export const MISSING_API_QUERY_PARAM = "Missing use_case or fhir_server.";
Expand Down
200 changes: 200 additions & 0 deletions query-connector/src/app/database-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -760,3 +760,203 @@ export async function getFhirServerConfig(fhirServerName: string) {
const configs = await getFhirServerConfigs();
return configs.find((config) => config.name === fhirServerName);
}

/**
* Inserts a new FHIR server configuration into the database.
* @param name - The name of the FHIR server
* @param hostname - The URL/hostname of the FHIR server
* @param lastConnectionSuccessful - Optional boolean indicating if the last connection was successful
* @returns An object indicating success or failure with optional error message
*/
export async function insertFhirServer(
name: string,
hostname: string,
lastConnectionSuccessful?: boolean,
) {
const insertQuery = `
INSERT INTO fhir_servers (
name,
hostname,
last_connection_attempt,
last_connection_successful
)
VALUES ($1, $2, $3, $4);
`;

try {
await dbClient.query("BEGIN");

const result = await dbClient.query(insertQuery, [
name,
hostname,
new Date(),
lastConnectionSuccessful,
]);

// Clear the cache so the next getFhirServerConfigs call will fetch fresh data
cachedFhirServerConfigs = null;

await dbClient.query("COMMIT");

return {
success: true,
server: result.rows[0],
};
} catch (error) {
await dbClient.query("ROLLBACK");
console.error("Failed to insert FHIR server:", error);
return {
success: false,
error: "Failed to save the FHIR server configuration.",
};
}
}

/**
* Updates an existing FHIR server configuration in the database.
* @param id - The ID of the FHIR server to update
* @param name - The new name of the FHIR server
* @param hostname - The new URL/hostname of the FHIR server
* @param lastConnectionSuccessful - Optional boolean indicating if the last connection was successful
* @returns An object indicating success or failure with optional error message
*/
export async function updateFhirServer(
id: string,
name: string,
hostname: string,
lastConnectionSuccessful?: boolean,
) {
const updateQuery = `
UPDATE fhir_servers
SET
name = $2,
hostname = $3,
last_connection_attempt = CURRENT_TIMESTAMP,
last_connection_successful = $4
WHERE id = $1
RETURNING *;
`;

try {
await dbClient.query("BEGIN");

const result = await dbClient.query(updateQuery, [
id,
name,
hostname,
lastConnectionSuccessful,
]);

// Clear the cache so the next getFhirServerConfigs call will fetch fresh data
cachedFhirServerConfigs = null;

await dbClient.query("COMMIT");

if (result.rows.length === 0) {
return {
success: false,
error: "Server not found",
};
}

return {
success: true,
server: result.rows[0],
};
} catch (error) {
await dbClient.query("ROLLBACK");
console.error("Failed to update FHIR server:", error);
return {
success: false,
error: "Failed to update the server configuration.",
};
}
}

/**
* Updates the connection status for a FHIR server.
* @param name - The name of the FHIR server
* @param wasSuccessful - Whether the connection attempt was successful
* @returns An object indicating success or failure with optional error message
*/
export async function updateFhirServerConnectionStatus(
name: string,
wasSuccessful: boolean,
) {
const updateQuery = `
UPDATE fhir_servers
SET
last_connection_attempt = CURRENT_TIMESTAMP,
last_connection_successful = $2
WHERE name = $1
RETURNING *;
`;

try {
const result = await dbClient.query(updateQuery, [name, wasSuccessful]);

// Clear the cache so the next getFhirServerConfigs call will fetch fresh data
cachedFhirServerConfigs = null;

if (result.rows.length === 0) {
return {
success: false,
error: "Server not found",
};
}

return {
success: true,
server: result.rows[0],
};
} catch (error) {
console.error("Failed to update FHIR server connection status:", error);
return {
success: false,
error: "Failed to update the server connection status.",
};
}
}

/**
* Deletes a FHIR server configuration from the database.
* @param id - The ID of the FHIR server to delete
* @returns An object indicating success or failure with optional error message
*/
export async function deleteFhirServer(id: string) {
const deleteQuery = `
DELETE FROM fhir_servers
WHERE id = $1
RETURNING *;
`;

try {
await dbClient.query("BEGIN");

const result = await dbClient.query(deleteQuery, [id]);

// Clear the cache so the next getFhirServerConfigs call will fetch fresh data
cachedFhirServerConfigs = null;

await dbClient.query("COMMIT");

if (result.rows.length === 0) {
return {
success: false,
error: "Server not found",
};
}

return {
success: true,
server: result.rows[0],
};
} catch (error) {
await dbClient.query("ROLLBACK");
console.error("Failed to delete FHIR server:", error);
return {
success: false,
error: "Failed to delete the server configuration.",
};
}
}
Loading

0 comments on commit d063847

Please sign in to comment.