Skip to content

Commit

Permalink
Add immunization query backend (#292)
Browse files Browse the repository at this point in the history
Co-authored-by: m-goggins <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Nick Clyde <[email protected]>
Co-authored-by: Marcelle <[email protected]>
  • Loading branch information
5 people authored Jan 15, 2025
1 parent 7766fcf commit 706daa1
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 9 deletions.
2 changes: 1 addition & 1 deletion query-connector/flyway/conf/flyway.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ flyway.url=jdbc:postgresql://db:5432/tefca_db
flyway.locations=filesystem:/flyway/sql
flyway.user=postgres
flyway.password=pw
flyway.baselineOnMigrate=false
flyway.baselineOnMigrate=false
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
ALTER TABLE fhir_servers
ADD COLUMN disable_cert_validation BOOLEAN DEFAULT FALSE;

-- Update rows with eHealthExchange in the server name to have disable_cert_validation set to true
UPDATE fhir_servers
SET disable_cert_validation = TRUE
WHERE name LIKE '%eHealthExchange%';

-- Update rows with eHealthExchange in the server name to strip the trailing slash from the hostname
UPDATE fhir_servers
SET hostname = regexp_replace(hostname, '/$', '')
WHERE name LIKE '%eHealthExchange%';
6 changes: 6 additions & 0 deletions query-connector/src/app/CustomQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export class CustomQuery {
socialHistoryQuery: string = "";
encounterQuery: string = "";
encounterClassTypeQuery: string = "";
immunizationQuery: string = "";

// Some queries need to be batched in waves because their encounter references
// might depend on demographic information
Expand Down Expand Up @@ -94,6 +95,8 @@ export class CustomQuery {
classTypeFilter !== ""
? `/Encounter?subject=${patientId}&class=${classTypeFilter}`
: "";

this.immunizationQuery = `/Immunization?patient=${patientId}`;
}

/**
Expand All @@ -109,6 +112,7 @@ export class CustomQuery {
this.socialHistoryQuery,
this.encounterQuery,
this.encounterClassTypeQuery,
this.immunizationQuery,
];
const filteredRequests = queryRequests.filter((q) => q !== "");
return filteredRequests;
Expand Down Expand Up @@ -136,6 +140,8 @@ export class CustomQuery {
return this.encounterQuery;
case "encounterClass":
return this.encounterClassTypeQuery;
case "immunization":
return this.immunizationQuery;
default:
return "";
}
Expand Down
9 changes: 8 additions & 1 deletion query-connector/src/app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Medication,
MedicationAdministration,
MedicationRequest,
Immunization,
} from "fhir/r4";

export const USE_CASE_DETAILS = {
Expand All @@ -30,6 +31,10 @@ export const USE_CASE_DETAILS = {
queryName: "Cancer case investigation",
condition: "Cancer (Leukemia)",
},
immunization: {
queryName: "Immunization",
condition: "Immunization",
},
} as const;

export type USE_CASES = keyof typeof USE_CASE_DETAILS;
Expand Down Expand Up @@ -285,7 +290,8 @@ export type FhirResource =
| Encounter
| Medication
| MedicationAdministration
| MedicationRequest;
| MedicationRequest
| Immunization;

/**
* A type guard function that checks if the given resource is a valid FHIR resource.
Expand Down Expand Up @@ -327,6 +333,7 @@ export type FhirServerConfig = {
last_connection_attempt: Date;
last_connection_successful: boolean;
headers: Record<string, string>;
disable_cert_validation: boolean;
};

export const INVALID_USE_CASE = `Invalid use_case. Please provide a valid use_case. Valid use_cases include ${Object.keys(
Expand Down
14 changes: 11 additions & 3 deletions query-connector/src/app/database-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -765,13 +765,15 @@ export async function getFhirServerConfig(fhirServerName: string) {
* 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 disableCertValidation - Whether to disable certificate validation
* @param lastConnectionSuccessful - Optional boolean indicating if the last connection was successful
* @param bearerToken - Optional bearer token for authentication
* @returns An object indicating success or failure with optional error message
*/
export async function insertFhirServer(
name: string,
hostname: string,
disableCertValidation: boolean,
lastConnectionSuccessful?: boolean,
bearerToken?: string,
) {
Expand All @@ -781,9 +783,10 @@ export async function insertFhirServer(
hostname,
last_connection_attempt,
last_connection_successful,
headers
headers,
disable_cert_validation
)
VALUES ($1, $2, $3, $4, $5);
VALUES ($1, $2, $3, $4, $5, $6);
`;

try {
Expand All @@ -800,6 +803,7 @@ export async function insertFhirServer(
new Date(),
lastConnectionSuccessful,
headers,
disableCertValidation,
]);

// Clear the cache so the next getFhirServerConfigs call will fetch fresh data
Expand All @@ -826,6 +830,7 @@ export async function insertFhirServer(
* @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 disableCertValidation - Whether to disable certificate validation
* @param lastConnectionSuccessful - Optional boolean indicating if the last connection was successful
* @param bearerToken - Optional bearer token for authentication
* @returns An object indicating success or failure with optional error message
Expand All @@ -834,6 +839,7 @@ export async function updateFhirServer(
id: string,
name: string,
hostname: string,
disableCertValidation: boolean,
lastConnectionSuccessful?: boolean,
bearerToken?: string,
) {
Expand All @@ -844,7 +850,8 @@ export async function updateFhirServer(
hostname = $3,
last_connection_attempt = CURRENT_TIMESTAMP,
last_connection_successful = $4,
headers = $5
headers = $5,
disable_cert_validation = $6
WHERE id = $1
RETURNING *;
`;
Expand Down Expand Up @@ -891,6 +898,7 @@ export async function updateFhirServer(
hostname,
lastConnectionSuccessful,
headers,
disableCertValidation,
]);

// Clear the cache so the next getFhirServerConfigs call will fetch fresh data
Expand Down
6 changes: 2 additions & 4 deletions query-connector/src/app/fhir-servers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import fetch, { RequestInit, HeaderInit, Response } from "node-fetch";
import { FhirServerConfig } from "./constants";
import https from "https";

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

/**
* A client for querying a FHIR server
* @param server The FHIR server to query
Expand All @@ -15,7 +13,7 @@ class FHIRClient {

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

Expand All @@ -30,7 +28,7 @@ class FHIRClient {
headers: config.headers as HeaderInit,
};
// Trust eHealth Exchange's self-signed certificate
if (config.trustSelfSigned) {
if (config.disable_cert_validation) {
init.agent = new https.Agent({
rejectUnauthorized: false,
});
Expand Down
11 changes: 11 additions & 0 deletions query-connector/src/app/fhir-servers/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import styles from "./fhirServers.module.scss";
import classNames from "classnames";
import SiteAlert from "../query/designSystem/SiteAlert";
import Table from "../query/designSystem/table/Table";
import Checkbox from "../query/designSystem/checkbox/Checkbox";

// Dynamic import with proper typing for Modal
import type { ModalProps } from "../query/designSystem/modal/Modal";
Expand All @@ -37,6 +38,7 @@ const FhirServers: React.FC = () => {
const [serverUrl, setServerUrl] = useState("");
const [authMethod, setAuthMethod] = useState<"none" | "basic">("none");
const [bearerToken, setBearerToken] = useState("");
const [disableCertValidation, setDisableCertValidation] = useState(false);
const [connectionStatus, setConnectionStatus] = useState<
"idle" | "success" | "error"
>("idle");
Expand Down Expand Up @@ -78,6 +80,7 @@ const FhirServers: React.FC = () => {
setServerName(server.name);
setServerUrl(server.hostname);
setConnectionStatus("idle");
setDisableCertValidation(server.disable_cert_validation);

// Set auth method and bearer token if they exist
if (server.headers?.Authorization?.startsWith("Bearer ")) {
Expand Down Expand Up @@ -146,6 +149,7 @@ const FhirServers: React.FC = () => {
const result = await insertFhirServer(
serverName,
serverUrl,
disableCertValidation,
connectionResult.success,
authMethod === "basic" ? bearerToken : undefined,
);
Expand All @@ -164,6 +168,7 @@ const FhirServers: React.FC = () => {
selectedServer.id,
serverName,
serverUrl,
disableCertValidation,
connectionResult.success,
authMethod === "basic" ? bearerToken : undefined,
);
Expand Down Expand Up @@ -404,6 +409,12 @@ const FhirServers: React.FC = () => {
/>
</>
)}
<Checkbox
id="disable-cert-validation"
label="Disable certificate validation"
checked={disableCertValidation}
onChange={(e) => setDisableCertValidation(e.target.checked)}
/>
</Modal>
</div>
</>
Expand Down
16 changes: 16 additions & 0 deletions query-connector/src/app/format-service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
Address,
ContactPoint,
Identifier,
Immunization,
Coding,
} from "fhir/r4";
import { DibbsValueSet } from "./constants";
import { QueryStruct } from "./query-service";
Expand Down Expand Up @@ -304,3 +306,17 @@ export const formatValueSetsAsQuerySpec = async (

return spec;
};

/**
* Formats the route of a FHIR Immunization object.
* @param immunization - The Immunization object to format.
* @returns The formatted route .
*/
export const formatImmunizationRoute = (immunization: Immunization): string => {
const initial = immunization.route?.coding?.[0].display ?? "";
const readable = immunization.route?.coding?.filter(
(code: Coding) =>
code.system === "http://terminology.hl7.org/CodeSystem/v2-0162",
);
return readable?.[0].display ?? initial;
};
2 changes: 2 additions & 0 deletions query-connector/src/app/query-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ async function generalizedQuery(
// Special cases for newborn screening, which just use one query
if (useCase === "newborn-screening") {
response = await fhirClient.get(builtQuery.getQuery("observation"));
} else if (useCase === "immunization") {
response = await fhirClient.get(builtQuery.getQuery("immunization"));
} else {
const queryRequests: string[] = builtQuery.getAllQueries();
response = await fhirClient.getBatch(queryRequests);
Expand Down
10 changes: 10 additions & 0 deletions query-connector/src/app/query/components/ResultsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Backlink from "./backLink/Backlink";
import { USE_CASES, USE_CASE_DETAILS } from "@/app/constants";
import { RETURN_LABEL } from "@/app/query/components/stepIndicator/StepIndicator";
import TitleBox from "./stepIndicator/TitleBox";
import ImmunizationTable from "./resultsView/tableComponents/ImmunizationTable";

type ResultsViewProps = {
useCaseQueryResponse: UseCaseQueryResponse;
Expand Down Expand Up @@ -113,6 +114,9 @@ function mapQueryResponseToAccordionDataStructure(
const medicationRequests = useCaseQueryResponse.MedicationRequest
? useCaseQueryResponse.MedicationRequest
: null;
const immunizations = useCaseQueryResponse.Immunization
? useCaseQueryResponse.Immunization
: null;

const accordionItems: ResultsViewAccordionItem[] = [
{
Expand Down Expand Up @@ -146,6 +150,12 @@ function mapQueryResponseToAccordionDataStructure(
<MedicationRequestTable medicationRequests={medicationRequests} />
) : null,
},
{
title: "Immunizations",
content: immunizations ? (
<ImmunizationTable immunizations={immunizations} />
) : null,
},
];
return accordionItems;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from "react";
import Table from "@/app/query/designSystem/table/Table";
import { Immunization } from "fhir/r4";
import {
formatDate,
formatImmunizationRoute,
} from "../../../../format-service";
import styles from "./resultsTables.module.scss";

/**
* The props for the ImmunizationTable component.
*/
export interface ImmunizationTableProps {
immunizations: Immunization[];
}

/**
* Displays a table of data from array of Immunization resources.
* @param props - Immunization table props.
* @param props.immunizations - The array of Immunization resources.
* @returns - The ImmunizationTable component.
*/
const ImmunizationTable: React.FC<ImmunizationTableProps> = ({
immunizations,
}) => {
return (
<Table bordered={false} className="margin-top-0-important">
<thead>
<tr className={styles.immunizationRow}>
<th>Date</th>
<th>Vaccine name</th>
<th>Dose</th>
<th>Route</th>
</tr>
</thead>
<tbody>
{immunizations.map((immunization) => (
<tr className={styles.immunizationRow} key={immunization.id}>
<td>{formatDate(immunization.occurrenceDateTime)}</td>
<td>{immunization.vaccineCode.coding?.[0].display}</td>
<td>
{immunization.doseQuantity?.value}{" "}
{immunization.doseQuantity?.code}
</td>
<td>{formatImmunizationRoute(immunization)}</td>
</tr>
))}
</tbody>
</Table>
);
};

export default ImmunizationTable;
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ $medicationColumnSpacing: 1fr 3fr 1fr;
$encountersColumnSpacing: 2fr 1fr 1fr 1fr;
$diagnosticsColumnSpacing: 1fr 3fr;
$demographicsColumnSpacing: 1fr 3fr;
$immunizationColumnSpacing: 1fr 3fr 1fr 1fr;

.demographicsRow {
grid-template-columns: $demographicsColumnSpacing;
Expand Down Expand Up @@ -36,3 +37,8 @@ $demographicsColumnSpacing: 1fr 3fr;
grid-template-columns: $diagnosticsColumnSpacing;
gap: $columnGap;
}

.immunizationRow {
grid-template-columns: $immunizationColumnSpacing;
gap: $columnGap;
}
4 changes: 4 additions & 0 deletions query-connector/src/styles/custom-styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -574,4 +574,8 @@ ul.usa-sidenav
flex: 1 1 0%;
}
}

.usa-checkbox {
background-color: transparent;
}
}

0 comments on commit 706daa1

Please sign in to comment.