Skip to content

Commit

Permalink
Execute custom queries (#2566)
Browse files Browse the repository at this point in the history
* Execute custom queries

* Update encoder interceptor

* Ummm what

* Does this fix text encoder

* Revert jest setup

* Move to format service

* Intermediate testing work

* Cleanup to merge form

* Add test page back

* Passing integration test
  • Loading branch information
bamader authored Sep 18, 2024
1 parent 9f96cf4 commit b1c2bf3
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 166 deletions.
21 changes: 11 additions & 10 deletions containers/tefca-viewer/docker-compose-dev.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,4 @@
services:
# Flyway migrations and DB version control
flyway:
image: flyway/flyway:10.16-alpine
command: -configFiles=/flyway/conf/flyway.conf -schemas=public -connectRetries=60 migrate
volumes:
- ./flyway/sql:/flyway/sql
- ./flyway/conf/flyway.conf:/flyway/conf/flyway.conf
depends_on:
- db

# Postgresql DB
db:
image: "postgres:alpine"
Expand All @@ -21,3 +11,14 @@ services:
interval: 2s
timeout: 5s
retries: 20

# Flyway migrations and DB version control
flyway:
image: flyway/flyway:10.16-alpine
command: -configFiles=/flyway/conf/flyway.conf -schemas=public -connectRetries=60 migrate
volumes:
- ./flyway/sql:/flyway/sql
- ./flyway/conf/flyway.conf:/flyway/conf/flyway.conf
depends_on:
db:
condition: service_started
16 changes: 14 additions & 2 deletions containers/tefca-viewer/src/app/api/query/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ import {
FHIR_SERVERS,
FhirServers,
UseCases,
UseCaseToQueryName,
} from "../../constants";

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

/**
* Health check for TEFCA Viewer
Expand Down Expand Up @@ -81,6 +86,11 @@ export async function POST(request: NextRequest) {
return NextResponse.json(OperationOutcome);
}

// Lookup default parameters for particular use-case search
const queryName = UseCaseToQueryName[use_case as USE_CASES];
const queryResults = await getSavedQueryByName(queryName);
const vsItems = await mapQueryRowsToValueSetItems(queryResults);

// Add params & patient identifiers to UseCaseRequest
const UseCaseRequest: UseCaseQueryRequest = {
use_case: use_case as USE_CASES,
Expand All @@ -96,8 +106,10 @@ export async function POST(request: NextRequest) {
...(PatientIdentifiers.phone && { phone: PatientIdentifiers.phone }),
};

const UseCaseQueryResponse: QueryResponse =
await UseCaseQuery(UseCaseRequest);
const UseCaseQueryResponse: QueryResponse = await UseCaseQuery(
UseCaseRequest,
vsItems,
);

// Bundle data
const bundle: APIQueryResponse = await createBundle(UseCaseQueryResponse);
Expand Down
43 changes: 1 addition & 42 deletions containers/tefca-viewer/src/app/database-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { Pool, PoolConfig, QueryResultRow } from "pg";
import dotenv from "dotenv";
import { ValueSetItem } from "./constants";
import { QueryStruct } from "./demoQueries";

const getQuerybyNameSQL = `
select q.query_name, q.id, qtv.valueset_id, vs.name as valueset_name, vs.author as author, vs.type, qic.concept_id, qic.include, c.code, c.code_system, c.display
Expand All @@ -17,11 +16,7 @@ select q.query_name, q.id, qtv.valueset_id, vs.name as valueset_name, vs.author
// Load environment variables from tefca.env and establish a Pool configuration
dotenv.config({ path: "tefca.env" });
const dbConfig: PoolConfig = {
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
host: process.env.POSTGRES_HOST,
port: Number(process.env.POSTGRES_PORT),
database: process.env.POSTGRES_DB,
connectionString: process.env.DATABASE_URL,
max: 10, // Maximum # of connections in the pool
idleTimeoutMillis: 30000, // A client must sit idle this long before being released
connectionTimeoutMillis: 2000, // Wait this long before timing out when connecting new client
Expand Down Expand Up @@ -100,39 +95,3 @@ export const mapQueryRowsToValueSetItems = async (rows: QueryResultRow[]) => {
});
return vsItems;
};

/**
* Formats a statefully updated list of value set items into a JSON structure
* used for executing custom queries.
* @param useCase The base use case being queried for.
* @param vsItems The list of value set items the user wants included.
* @returns A structured specification of a query that can be executed.
*/
export const formatValueSetItemsAsQuerySpec = async (
useCase: string,
vsItems: ValueSetItem[],
) => {
let secondEncounter: boolean = false;
if (["cancer", "chlamydia", "gonorrhea", "syphilis"].includes(useCase)) {
secondEncounter = true;
}
const labCodes: string[] = vsItems
.filter((vs) => vs.system === "http://loinc.org")
.map((vs) => vs.code);
const snomedCodes: string[] = vsItems
.filter((vs) => vs.system === "http://snomed.info/sct")
.map((vs) => vs.code);
const rxnormCodes: string[] = vsItems
.filter((vs) => vs.system === "http://www.nlm.nih.gov/research/umls/rxnorm")
.map((vs) => vs.code);

const spec: QueryStruct = {
labCodes: labCodes,
snomedCodes: snomedCodes,
rxnormCodes: rxnormCodes,
classTypeCodes: [] as string[],
hasSecondEncounterQuery: secondEncounter,
};

return spec;
};
38 changes: 38 additions & 0 deletions containers/tefca-viewer/src/app/format-service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
ContactPoint,
Identifier,
} from "fhir/r4";
import { ValueSetItem } from "./constants";
import { QueryStruct } from "./demoQueries";

/**
* Formats a string.
Expand Down Expand Up @@ -257,3 +259,39 @@ export async function GetPhoneQueryFormats(phone: string) {
});
return possibleFormats;
}

/**
* Formats a statefully updated list of value set items into a JSON structure
* used for executing custom queries.
* @param useCase The base use case being queried for.
* @param vsItems The list of value set items the user wants included.
* @returns A structured specification of a query that can be executed.
*/
export const formatValueSetItemsAsQuerySpec = async (
useCase: string,
vsItems: ValueSetItem[],
) => {
let secondEncounter: boolean = false;
if (["cancer", "chlamydia", "gonorrhea", "syphilis"].includes(useCase)) {
secondEncounter = true;
}
const labCodes: string[] = vsItems
.filter((vs) => vs.system === "http://loinc.org")
.map((vs) => vs.code);
const snomedCodes: string[] = vsItems
.filter((vs) => vs.system === "http://snomed.info/sct")
.map((vs) => vs.code);
const rxnormCodes: string[] = vsItems
.filter((vs) => vs.system === "http://www.nlm.nih.gov/research/umls/rxnorm")
.map((vs) => vs.code);

const spec: QueryStruct = {
labCodes: labCodes,
snomedCodes: snomedCodes,
rxnormCodes: rxnormCodes,
classTypeCodes: [] as string[],
hasSecondEncounterQuery: secondEncounter,
};

return spec;
};
25 changes: 23 additions & 2 deletions containers/tefca-viewer/src/app/query-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import {
} from "fhir/r4";

import FHIRClient from "./fhir-servers";
import { USE_CASES, FHIR_SERVERS } from "./constants";
import { USE_CASES, FHIR_SERVERS, ValueSetItem } from "./constants";
import * as dq from "./demoQueries";
import { CustomQuery } from "./CustomQuery";
import { GetPhoneQueryFormats } from "./format-service";
import { formatValueSetItemsAsQuerySpec } from "./format-service";

/**
* The query response when the request source is from the Viewer UI.
Expand Down Expand Up @@ -145,11 +146,13 @@ async function patientQuery(
* Query a FHIR API for a public health use case based on patient demographics provided
* in the request. If data is found, return in a queryResponse object.
* @param request - UseCaseQueryRequest object containing the patient demographics and use case.
* @param queryValueSets - The value sets to be included in query filtering.
* @param queryResponse - The response object to store the query results.
* @returns - The response object containing the query results.
*/
export async function UseCaseQuery(
request: UseCaseQueryRequest,
queryValueSets: ValueSetItem[],
queryResponse: QueryResponse = {},
): Promise<QueryResponse> {
const fhirClient = new FHIRClient(request.fhir_server);
Expand All @@ -166,6 +169,7 @@ export async function UseCaseQuery(

await generalizedQuery(
request.use_case,
queryValueSets,
patientId,
fhirClient,
queryResponse,
Expand All @@ -174,13 +178,30 @@ export async function UseCaseQuery(
return queryResponse;
}

/**
* Performs a generalized query for collections of patients matching
* particular criteria. The query is determined by a collection of passed-in
* valuesets to include in the query results, and any patients found must
* have eCR data interseecting with these valuesets.
* @param useCase The particular use case the query is associated with.
* @param queryValueSets The valuesets to include as reference points for patient
* data.
* @param patientId The ID of the patient for whom to search.
* @param fhirClient The client used to communicate with the FHIR server.
* @param queryResponse The response object for the query results.
* @returns A promise for an updated query response.
*/
async function generalizedQuery(
useCase: USE_CASES,
queryValueSets: ValueSetItem[],
patientId: string,
fhirClient: FHIRClient,
queryResponse: QueryResponse,
): Promise<QueryResponse> {
const querySpec = UseCaseToStructMap[useCase];
const querySpec = await formatValueSetItemsAsQuerySpec(
useCase,
queryValueSets,
);
const builtQuery = new CustomQuery(querySpec, patientId);
let response: fetch.Response | fetch.Response[];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import {
UseCaseQueryRequest,
} from "../../query-service";
import ResultsView from "./ResultsView";
import { ValueSetItem } from "@/app/constants";

/**
* The props for the MultiplePatientSearchResults component.
*/
export interface MultiplePatientSearchResultsProps {
patients: Patient[];
originalRequest: UseCaseQueryRequest;
queryValueSets: ValueSetItem[];
setLoading: (loading: boolean) => void;
goBack: () => void;
}
Expand All @@ -29,13 +31,15 @@ export interface MultiplePatientSearchResultsProps {
* @param root0 - MultiplePatientSearchResults props.
* @param root0.patients - The array of Patient resources.
* @param root0.originalRequest - The original request object.
* @param root0.queryValueSets - The stateful collection of value sets to include
* in the query.
* @param root0.setLoading - The function to set the loading state.
* @param root0.goBack - The function to go back to the previous page.
* @returns - The MultiplePatientSearchResults component.
*/
const MultiplePatientSearchResults: React.FC<
MultiplePatientSearchResultsProps
> = ({ patients, originalRequest, setLoading, goBack }) => {
> = ({ patients, originalRequest, queryValueSets, setLoading, goBack }) => {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
Expand Down Expand Up @@ -90,6 +94,7 @@ const MultiplePatientSearchResults: React.FC<
patients,
index,
originalRequest,
queryValueSets,
setSingleUseCaseQueryResponse,
setLoading,
)
Expand Down Expand Up @@ -177,18 +182,20 @@ function searchResultsNote(request: UseCaseQueryRequest): JSX.Element {
* @param patients - The array of patients.
* @param index - The index of the patient to view.
* @param originalRequest - The original request object.
* @param queryValueSets - The value sets to include as part of the query.
* @param setUseCaseQueryResponse - The function to set the use case query response.
* @param setLoading - The function to set the loading state.
*/
async function viewRecord(
patients: Patient[],
index: number,
originalRequest: UseCaseQueryRequest,
queryValueSets: ValueSetItem[],
setUseCaseQueryResponse: (UseCaseQueryResponse: UseCaseQueryResponse) => void,
setLoading: (loading: boolean) => void,
): Promise<void> {
setLoading(true);
const queryResponse = await UseCaseQuery(originalRequest, {
const queryResponse = await UseCaseQuery(originalRequest, queryValueSets, {
Patient: [patients[index]],
});
setUseCaseQueryResponse(queryResponse);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
patientOptions,
stateOptions,
Mode,
ValueSetItem,
} from "../../constants";
import {
UseCaseQueryResponse,
Expand All @@ -28,6 +29,7 @@ import { FormatPhoneAsDigits } from "@/app/format-service";

interface SearchFormProps {
useCase: USE_CASES;
queryValueSets: ValueSetItem[];
setUseCase: (useCase: USE_CASES) => void;
setOriginalRequest: (originalRequest: UseCaseQueryRequest) => void;
setUseCaseQueryResponse: (UseCaseQueryResponse: UseCaseQueryResponse) => void;
Expand All @@ -39,6 +41,7 @@ interface SearchFormProps {
/**
* @param root0 - SearchFormProps
* @param root0.useCase - The use case this query will cover.
* @param root0.queryValueSets - Stateful collection of valuesets to use in the query.
* @param root0.setUseCase - Update stateful use case.
* @param root0.setOriginalRequest - The function to set the original request.
* @param root0.setUseCaseQueryResponse - The function to set the use case query response.
Expand All @@ -49,6 +52,7 @@ interface SearchFormProps {
*/
const SearchForm: React.FC<SearchFormProps> = ({
useCase,
queryValueSets,
setUseCase,
setOriginalRequest,
setUseCaseQueryResponse,
Expand Down Expand Up @@ -120,7 +124,7 @@ const SearchForm: React.FC<SearchFormProps> = ({
phone: FormatPhoneAsDigits(phone),
};
setOriginalRequest(originalRequest);
const queryResponse = await UseCaseQuery(originalRequest);
const queryResponse = await UseCaseQuery(originalRequest, queryValueSets);
setUseCaseQueryResponse(queryResponse);
if (!queryResponse.Patient || queryResponse.Patient.length === 0) {
setMode("no-patients");
Expand Down
2 changes: 2 additions & 0 deletions containers/tefca-viewer/src/app/query/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const Query: React.FC = () => {
<Suspense fallback="...Loading">
<SearchForm
useCase={useCase}
queryValueSets={queryValuesets}
setUseCase={setUseCase}
setMode={setMode}
setLoading={setLoading}
Expand Down Expand Up @@ -98,6 +99,7 @@ const Query: React.FC = () => {
<MultiplePatientSearchResults
patients={useCaseQueryResponse?.Patient ?? []}
originalRequest={originalRequest}
queryValueSets={queryValuesets}
setLoading={setLoading}
goBack={() => setMode("search")}
/>
Expand Down
Loading

0 comments on commit b1c2bf3

Please sign in to comment.