Skip to content

Commit 369c0e9

Browse files
authored
query building write functionality pt 1 (#256)
1 parent 780509a commit 369c0e9

17 files changed

+720
-379
lines changed

query-connector/src/app/assets/dibbs_db_seed_query.json

+10-10
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
"time_window_number": "30",
88
"time_window_unit": "days",
99
"query_name": "Cancer case investigation",
10-
"conditions_list": "{Cancer (Leukemia)}",
10+
"conditions_list": "{2}",
1111
"query_data": {
12-
"Cancer (Leukemia)": {
12+
"2": {
1313
"14_20240923": {
1414
"valueSetId": "14_20240923",
1515
"valueSetVersion": "20240923",
@@ -122,9 +122,9 @@
122122
"time_window_number": "30",
123123
"time_window_unit": "days",
124124
"query_name": "Chlamydia case investigation",
125-
"conditions_list": "{Chlamydia trachomatis infection (disorder)}",
125+
"conditions_list": "{240589008}",
126126
"query_data": {
127-
"Chlamydia trachomatis infection (disorder)": {
127+
"240589008": {
128128
"2.16.840.1.113762.1.4.1146.999_20230602": {
129129
"valueSetId": "2.16.840.1.113762.1.4.1146.999_20230602",
130130
"valueSetVersion": "20230602",
@@ -2173,7 +2173,7 @@
21732173
"time_window_unit": "days",
21742174
"query_name": "Syphilis case investigation",
21752175
"query_data": {
2176-
"Congenital syphilis (disorder)": {
2176+
"35742006": {
21772177
"2.16.840.1.113762.1.4.1146.554_20191227": {
21782178
"valueSetId": "2.16.840.1.113762.1.4.1146.554_20191227",
21792179
"valueSetVersion": "20191227",
@@ -3347,7 +3347,7 @@
33473347
}
33483348
}
33493349
},
3350-
"conditions_list": "{Congenital syphilis (disorder)}"
3350+
"conditions_list": "{35742006}"
33513351
},
33523352
{
33533353
"author": "DIBBs",
@@ -3357,7 +3357,7 @@
33573357
"time_window_unit": "days",
33583358
"query_name": "Gonorrhea case investigation",
33593359
"query_data": {
3360-
"Gonorrhea (disorder)": {
3360+
"15628003": {
33613361
"2.16.840.1.113762.1.4.1146.1036_20190605": {
33623362
"valueSetId": "2.16.840.1.113762.1.4.1146.1036_20190605",
33633363
"valueSetVersion": "20190605",
@@ -4890,7 +4890,7 @@
48904890
}
48914891
}
48924892
},
4893-
"conditions_list": "{Gonorrhea (disorder)}"
4893+
"conditions_list": "{15628003}"
48944894
},
48954895
{
48964896
"author": "DIBBs",
@@ -4900,7 +4900,7 @@
49004900
"time_window_unit": "days",
49014901
"query_name": "Newborn screening follow-up",
49024902
"query_data": {
4903-
"Newborn Screening": {
4903+
"1": {
49044904
"1_20240909": {
49054905
"valueSetId": "1_20240909",
49064906
"valueSetVersion": "20240909",
@@ -5924,7 +5924,7 @@
59245924
}
59255925
}
59265926
},
5927-
"conditions_list": "{Newborn Screening}"
5927+
"conditions_list": "{1}"
59285928
}
59295929
]
59305930
}

query-connector/src/app/backend/query-building.ts

+88-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"use server";
22

33
import { getDbClient } from "./dbClient";
4-
import { QueryDetailsResult } from "../queryBuilding/utils";
4+
import { NestedQuery, QueryDetailsResult } from "../queryBuilding/utils";
5+
import { DibbsValueSet } from "../constants";
6+
import { DEFAULT_TIME_WINDOW } from "../utils";
7+
import { randomUUID } from "crypto";
58
const dbClient = getDbClient();
69

710
/**
@@ -28,3 +31,87 @@ export async function getSavedQueryDetails(queryId: string) {
2831
console.error("Error retrieving query", error);
2932
}
3033
}
34+
35+
/**
36+
* Backend handler function for upserting a query
37+
* @param queryInput - frontend input for a query
38+
* @param queryName - name of query
39+
* @param author - author
40+
* @param queryId - a queryId if previously defined
41+
* @returns - all columns of the newly added row in the query table
42+
*/
43+
export async function saveCustomQuery(
44+
queryInput: NestedQuery,
45+
queryName: string,
46+
author: string,
47+
queryId?: string,
48+
) {
49+
const queryString = `
50+
INSERT INTO query
51+
values($1, $2, $3, $4, $5, $6, $7, $8, $9)
52+
ON CONFLICT(id)
53+
DO UPDATE SET
54+
query_name = EXCLUDED.query_name,
55+
conditions_list = EXCLUDED.conditions_list,
56+
query_data = EXCLUDED.query_data,
57+
author = EXCLUDED.author,
58+
date_last_modified = EXCLUDED.date_last_modified
59+
RETURNING id, query_name;
60+
`;
61+
const { queryDataInsert, conditionInsert } =
62+
formatQueryDataForDatabase(queryInput);
63+
64+
const NOW = new Date().toISOString();
65+
try {
66+
const dataToWrite = [
67+
queryId ? queryId : randomUUID(),
68+
queryName,
69+
queryDataInsert,
70+
conditionInsert,
71+
author,
72+
NOW,
73+
NOW,
74+
DEFAULT_TIME_WINDOW.timeWindowNumber,
75+
DEFAULT_TIME_WINDOW.timeWindowUnit,
76+
];
77+
const result = await dbClient.query(queryString, dataToWrite);
78+
if (result.rows.length > 0) {
79+
return result.rows as unknown as QueryDetailsResult[];
80+
}
81+
console.error("Query save failed:", dataToWrite);
82+
return [];
83+
} catch (error) {
84+
console.error("Error saving new query", error);
85+
}
86+
}
87+
88+
function formatQueryDataForDatabase(frontendInput: NestedQuery) {
89+
const queryData: Record<string, { [valueSetId: string]: DibbsValueSet }> = {};
90+
const conditionIds: string[] = [];
91+
92+
Object.entries(frontendInput).forEach(([conditionId, data]) => {
93+
queryData[conditionId] = {};
94+
conditionIds.push(conditionId);
95+
Object.values(data).forEach((dibbsVsMap) => {
96+
Object.entries(dibbsVsMap).forEach(([vsId, dibbsVs]) => {
97+
queryData[conditionId][vsId] = dibbsVs;
98+
});
99+
});
100+
});
101+
102+
return {
103+
queryDataInsert: queryData,
104+
conditionInsert: formatConditionsForPostgres(conditionIds),
105+
};
106+
}
107+
108+
function formatConditionsForPostgres(arr: string[]): string {
109+
if (arr.length === 0) return "{}";
110+
111+
const escapedStrings = arr.map((str) => {
112+
const escaped = str.replace(/"/g, '\\"');
113+
return `"${escaped}"`;
114+
});
115+
116+
return `{${escapedStrings.join(",")}}`;
117+
}

query-connector/src/app/query-building.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { randomUUID } from "crypto";
22
import { DibbsValueSet } from "./constants";
3+
import { DEFAULT_TIME_WINDOW } from "./utils";
34

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

14-
const DEFAULT_TIME_WINDOW = {
15-
timeWindowNumber: 1,
16-
timeWindowUnit: "day",
17-
};
18-
1915
/**
2016
* Function that generates SQL needed for the query building flow
2117
* @param input - Values of the shape QueryInput needed for query insertion
@@ -46,6 +42,6 @@ export function generateQueryInsertionSql(input: QueryInput) {
4642
export interface CustomUserQuery {
4743
query_id: string;
4844
query_name: string;
49-
conditions_list?: string;
45+
conditions_list?: string[];
5046
valuesets: DibbsValueSet[];
5147
}

query-connector/src/app/query/designSystem/checkbox/checkbox.module.scss

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
.checkbox label::before {
1818
width: 1.25rem;
1919
height: 1.25rem;
20+
min-width: 1.25rem;
2021
box-shadow: 0 0 0 1px $black !important;
2122
position: static;
2223
margin: 0;

query-connector/src/app/query/designSystem/drawer/Drawer.tsx

+15-10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ type DrawerProps = {
1111
isOpen: boolean;
1212
onSave: () => void;
1313
onClose: () => void;
14+
onSearch?: () => void;
1415
hasChanges: boolean;
1516
};
1617

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

55-
<div className="padding-top-5">
56-
<SearchField
57-
id="searchFieldTemplate"
58-
placeholder={placeholder}
59-
className={styles.searchField}
60-
onChange={(e) => {
61-
e.preventDefault();
62-
}}
63-
/>
64-
</div>
58+
{onSearch && (
59+
<div className="padding-top-5">
60+
<SearchField
61+
id="searchFieldTemplate"
62+
placeholder={placeholder}
63+
className={styles.searchField}
64+
onChange={(e) => {
65+
e.preventDefault();
66+
}}
67+
/>
68+
</div>
69+
)}
6570
<div className="padding-top-2">{toRender}</div>
6671
</div>
6772
</div>

query-connector/src/app/query/designSystem/modal/Modal.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const Modal: React.FC<ModalProps> = ({
5050
id={`${id}-modal`}
5151
aria-labelledby={`${id}-modal-heading`}
5252
aria-describedby={`${id}-modal-description`}
53+
className="padding-x-2"
5354
>
5455
<ModalHeading id={`${id}-modal-heading`}>{heading}</ModalHeading>
5556
<div className="usa-prose">

query-connector/src/app/queryBuilding/page.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
"use client";
22
import { SelectedQueryDetails } from "./querySelection/utils";
3-
import BuildFromTemplates from "./buildFromTemplates/BuildFromTemplates";
43
import QuerySelection from "./querySelection/QuerySelection";
54
import { BuildStep } from "../constants";
65
import "react-toastify/dist/ReactToastify.css";
76
import { useState } from "react";
87
import { EMPTY_QUERY_SELECTION } from "./utils";
8+
import BuildFromTemplates from "./buildFromTemplates/BuildFromTemplates";
99

1010
/**
1111
* Component for Query Building Flow
1212
* @returns The Query Building component flow
1313
*/
1414
const QueryBuilding: React.FC = () => {
1515
const [selectedQuery, setSelectedQuery] = useState<SelectedQueryDetails>(
16-
EMPTY_QUERY_SELECTION,
16+
structuredClone(EMPTY_QUERY_SELECTION),
1717
);
1818
const [buildStep, setBuildStep] = useState<BuildStep>("selection");
1919

0 commit comments

Comments
 (0)