Skip to content

Commit

Permalink
Merge branch 'main' into nickclyde/fhir-server-config
Browse files Browse the repository at this point in the history
  • Loading branch information
nickclyde authored Jan 9, 2025
2 parents 8f93366 + 369c0e9 commit 675b312
Show file tree
Hide file tree
Showing 17 changed files with 720 additions and 379 deletions.
20 changes: 10 additions & 10 deletions query-connector/src/app/assets/dibbs_db_seed_query.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
"time_window_number": "30",
"time_window_unit": "days",
"query_name": "Cancer case investigation",
"conditions_list": "{Cancer (Leukemia)}",
"conditions_list": "{2}",
"query_data": {
"Cancer (Leukemia)": {
"2": {
"14_20240923": {
"valueSetId": "14_20240923",
"valueSetVersion": "20240923",
Expand Down Expand Up @@ -122,9 +122,9 @@
"time_window_number": "30",
"time_window_unit": "days",
"query_name": "Chlamydia case investigation",
"conditions_list": "{Chlamydia trachomatis infection (disorder)}",
"conditions_list": "{240589008}",
"query_data": {
"Chlamydia trachomatis infection (disorder)": {
"240589008": {
"2.16.840.1.113762.1.4.1146.999_20230602": {
"valueSetId": "2.16.840.1.113762.1.4.1146.999_20230602",
"valueSetVersion": "20230602",
Expand Down Expand Up @@ -2173,7 +2173,7 @@
"time_window_unit": "days",
"query_name": "Syphilis case investigation",
"query_data": {
"Congenital syphilis (disorder)": {
"35742006": {
"2.16.840.1.113762.1.4.1146.554_20191227": {
"valueSetId": "2.16.840.1.113762.1.4.1146.554_20191227",
"valueSetVersion": "20191227",
Expand Down Expand Up @@ -3347,7 +3347,7 @@
}
}
},
"conditions_list": "{Congenital syphilis (disorder)}"
"conditions_list": "{35742006}"
},
{
"author": "DIBBs",
Expand All @@ -3357,7 +3357,7 @@
"time_window_unit": "days",
"query_name": "Gonorrhea case investigation",
"query_data": {
"Gonorrhea (disorder)": {
"15628003": {
"2.16.840.1.113762.1.4.1146.1036_20190605": {
"valueSetId": "2.16.840.1.113762.1.4.1146.1036_20190605",
"valueSetVersion": "20190605",
Expand Down Expand Up @@ -4890,7 +4890,7 @@
}
}
},
"conditions_list": "{Gonorrhea (disorder)}"
"conditions_list": "{15628003}"
},
{
"author": "DIBBs",
Expand All @@ -4900,7 +4900,7 @@
"time_window_unit": "days",
"query_name": "Newborn screening follow-up",
"query_data": {
"Newborn Screening": {
"1": {
"1_20240909": {
"valueSetId": "1_20240909",
"valueSetVersion": "20240909",
Expand Down Expand Up @@ -5924,7 +5924,7 @@
}
}
},
"conditions_list": "{Newborn Screening}"
"conditions_list": "{1}"
}
]
}
89 changes: 88 additions & 1 deletion query-connector/src/app/backend/query-building.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"use server";

import { getDbClient } from "./dbClient";
import { QueryDetailsResult } from "../queryBuilding/utils";
import { NestedQuery, QueryDetailsResult } from "../queryBuilding/utils";
import { DibbsValueSet } from "../constants";
import { DEFAULT_TIME_WINDOW } from "../utils";
import { randomUUID } from "crypto";
const dbClient = getDbClient();

/**
Expand All @@ -28,3 +31,87 @@ export async function getSavedQueryDetails(queryId: string) {
console.error("Error retrieving query", error);
}
}

/**
* Backend handler function for upserting a query
* @param queryInput - frontend input for a query
* @param queryName - name of query
* @param author - author
* @param queryId - a queryId if previously defined
* @returns - all columns of the newly added row in the query table
*/
export async function saveCustomQuery(
queryInput: NestedQuery,
queryName: string,
author: string,
queryId?: string,
) {
const queryString = `
INSERT INTO query
values($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT(id)
DO UPDATE SET
query_name = EXCLUDED.query_name,
conditions_list = EXCLUDED.conditions_list,
query_data = EXCLUDED.query_data,
author = EXCLUDED.author,
date_last_modified = EXCLUDED.date_last_modified
RETURNING id, query_name;
`;
const { queryDataInsert, conditionInsert } =
formatQueryDataForDatabase(queryInput);

const NOW = new Date().toISOString();
try {
const dataToWrite = [
queryId ? queryId : randomUUID(),
queryName,
queryDataInsert,
conditionInsert,
author,
NOW,
NOW,
DEFAULT_TIME_WINDOW.timeWindowNumber,
DEFAULT_TIME_WINDOW.timeWindowUnit,
];
const result = await dbClient.query(queryString, dataToWrite);
if (result.rows.length > 0) {
return result.rows as unknown as QueryDetailsResult[];
}
console.error("Query save failed:", dataToWrite);
return [];
} catch (error) {
console.error("Error saving new query", error);
}
}

function formatQueryDataForDatabase(frontendInput: NestedQuery) {
const queryData: Record<string, { [valueSetId: string]: DibbsValueSet }> = {};
const conditionIds: string[] = [];

Object.entries(frontendInput).forEach(([conditionId, data]) => {
queryData[conditionId] = {};
conditionIds.push(conditionId);
Object.values(data).forEach((dibbsVsMap) => {
Object.entries(dibbsVsMap).forEach(([vsId, dibbsVs]) => {
queryData[conditionId][vsId] = dibbsVs;
});
});
});

return {
queryDataInsert: queryData,
conditionInsert: formatConditionsForPostgres(conditionIds),
};
}

function formatConditionsForPostgres(arr: string[]): string {
if (arr.length === 0) return "{}";

const escapedStrings = arr.map((str) => {
const escaped = str.replace(/"/g, '\\"');
return `"${escaped}"`;
});

return `{${escapedStrings.join(",")}}`;
}
8 changes: 2 additions & 6 deletions query-connector/src/app/query-building.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { randomUUID } from "crypto";
import { DibbsValueSet } from "./constants";
import { DEFAULT_TIME_WINDOW } from "./utils";

// TODO: Potentially merge this / infer this from the type created via the
// database creation workstream
Expand All @@ -11,11 +12,6 @@ export type QueryInput = {
timeWindowNumber?: Number;
};

const DEFAULT_TIME_WINDOW = {
timeWindowNumber: 1,
timeWindowUnit: "day",
};

/**
* Function that generates SQL needed for the query building flow
* @param input - Values of the shape QueryInput needed for query insertion
Expand Down Expand Up @@ -46,6 +42,6 @@ export function generateQueryInsertionSql(input: QueryInput) {
export interface CustomUserQuery {
query_id: string;
query_name: string;
conditions_list?: string;
conditions_list?: string[];
valuesets: DibbsValueSet[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
.checkbox label::before {
width: 1.25rem;
height: 1.25rem;
min-width: 1.25rem;
box-shadow: 0 0 0 1px $black !important;
position: static;
margin: 0;
Expand Down
25 changes: 15 additions & 10 deletions query-connector/src/app/query/designSystem/drawer/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type DrawerProps = {
isOpen: boolean;
onSave: () => void;
onClose: () => void;
onSearch?: () => void;
hasChanges: boolean;
};

Expand All @@ -20,6 +21,7 @@ type DrawerProps = {
* @param root0.title - The title displayed in the drawer.
* @param root0.placeholder - The placeholder text for the search field.
* @param root0.onClose - Function to handle closing the drawer.
* @param root0.onSearch - Function to handle search actions in the drawer.
* @param root0.isOpen - Boolean to control the visibility of the drawer.
* @param root0.toRender - The dynamic content to display.
* warning modal appears before saving
Expand All @@ -31,6 +33,7 @@ const Drawer: React.FC<DrawerProps> = ({
isOpen,
onClose,
toRender,
onSearch,
}: DrawerProps) => {
const handleClose = () => {
onClose();
Expand All @@ -52,16 +55,18 @@ const Drawer: React.FC<DrawerProps> = ({
</button>
<h2 className="margin-0 padding-bottom-2">{title}</h2>

<div className="padding-top-5">
<SearchField
id="searchFieldTemplate"
placeholder={placeholder}
className={styles.searchField}
onChange={(e) => {
e.preventDefault();
}}
/>
</div>
{onSearch && (
<div className="padding-top-5">
<SearchField
id="searchFieldTemplate"
placeholder={placeholder}
className={styles.searchField}
onChange={(e) => {
e.preventDefault();
}}
/>
</div>
)}
<div className="padding-top-2">{toRender}</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions query-connector/src/app/query/designSystem/modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const Modal: React.FC<ModalProps> = ({
aria-labelledby={`${id}-modal-heading`}
aria-describedby={`${id}-modal-description`}
isLarge={isLarge}
className="padding-x-2"
>
<ModalHeading id={`${id}-modal-heading`}>{heading}</ModalHeading>
<div id={`${id}-modal-description`} className="usa-prose">
Expand Down
4 changes: 2 additions & 2 deletions query-connector/src/app/queryBuilding/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
"use client";
import { SelectedQueryDetails } from "./querySelection/utils";
import BuildFromTemplates from "./buildFromTemplates/BuildFromTemplates";
import QuerySelection from "./querySelection/QuerySelection";
import { BuildStep } from "../constants";
import "react-toastify/dist/ReactToastify.css";
import { useState } from "react";
import { EMPTY_QUERY_SELECTION } from "./utils";
import BuildFromTemplates from "./buildFromTemplates/BuildFromTemplates";

/**
* Component for Query Building Flow
* @returns The Query Building component flow
*/
const QueryBuilding: React.FC = () => {
const [selectedQuery, setSelectedQuery] = useState<SelectedQueryDetails>(
EMPTY_QUERY_SELECTION,
structuredClone(EMPTY_QUERY_SELECTION),
);
const [buildStep, setBuildStep] = useState<BuildStep>("selection");

Expand Down
Loading

0 comments on commit 675b312

Please sign in to comment.