Skip to content

Commit

Permalink
define backend query building insert function (#89)
Browse files Browse the repository at this point in the history
  • Loading branch information
fzhao99 authored Oct 31, 2024
1 parent 9950139 commit 90aa944
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 44 deletions.
4 changes: 2 additions & 2 deletions query-connector/src/app/api/query/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import { handleRequestError } from "./error-handling-service";
import {
getSavedQueryByName,
mapQueryRowsToConceptValueSets,
mapQueryRowsToValueSets,
} from "@/app/database-service";

/**
Expand Down Expand Up @@ -92,7 +92,7 @@ export async function POST(request: NextRequest) {
// Lookup default parameters for particular use-case search
const queryName = UseCaseToQueryName[use_case as USE_CASES];
const queryResults = await getSavedQueryByName(queryName);
const valueSets = await mapQueryRowsToConceptValueSets(queryResults);
const valueSets = await mapQueryRowsToValueSets(queryResults);

// Add params & patient identifiers to UseCaseRequest
const UseCaseRequest: UseCaseQueryRequest = {
Expand Down
1 change: 1 addition & 0 deletions query-connector/src/app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ export interface ValueSet {
valueSetId: string;
valueSetVersion: string;
valueSetName: string;
valueSetExternalId?: string;
author: string;
system: string;
ersdConceptType?: string;
Expand Down
80 changes: 71 additions & 9 deletions query-connector/src/app/database-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ import {
ersdToDibbsConceptMap,
} from "./constants";
import { encode } from "base-64";
import {
UserQueryInput,
generateQueryInsertionSql,
generateQueryToValueSetInsertionSql,
} from "./query-building";
import { UUID } from "crypto";

const getQuerybyNameSQL = `
select q.query_name, q.id, qtv.valueset_id, vs.name as valueset_name, vs.version, vs.author as author, vs.type, vs.dibbs_concept_type as dibbs_concept_type, qic.concept_id, qic.include, c.code, c.code_system, c.display
select q.query_name, q.id, qtv.valueset_id, vs.name as valueset_name, vs.oid as valueset_external_id, vs.version, vs.author as author, vs.type, vs.dibbs_concept_type as dibbs_concept_type, qic.concept_id, qic.include, c.code, c.code_system, c.display
from query q
left join query_to_valueset qtv on q.id = qtv.query_id
left join valuesets vs on qtv.valueset_id = vs.id
Expand Down Expand Up @@ -61,9 +67,7 @@ export const getSavedQueryByName = async (name: string) => {
* @param rows The Rows returned from the DB Query.
* @returns A list of ValueSets, which hold the Concepts pulled from the DB.
*/
export const mapQueryRowsToConceptValueSets = async (
rows: QueryResultRow[],
) => {
export const mapQueryRowsToValueSets = async (rows: QueryResultRow[]) => {
// Create groupings of rows (each of which is a single Concept) by their ValueSet ID
const vsIdGroupedRows = rows.reduce((conceptsByVSId, r) => {
if (!(r["valueset_id"] in conceptsByVSId)) {
Expand All @@ -81,9 +85,15 @@ export const mapQueryRowsToConceptValueSets = async (
valueSetId: conceptGroup[0]["valueset_id"],
valueSetVersion: conceptGroup[0]["version"],
valueSetName: conceptGroup[0]["valueset_name"],
// External ID might not be defined for user-defined valuesets
valueSetExternalId: conceptGroup[0]["valueset_external_id"]
? conceptGroup[0]["valueset_external_id"]
: undefined,
author: conceptGroup[0]["author"],
system: conceptGroup[0]["code_system"],
ersdConceptType: conceptGroup[0]["type"],
ersdConceptType: conceptGroup[0]["type"]
? conceptGroup[0]["type"]
: undefined,
dibbsConceptType: conceptGroup[0]["dibbs_concept_type"],
includeValueSet: conceptGroup.find((c) => c["include"]) ? true : false,
concepts: conceptGroup.map((c) => {
Expand Down Expand Up @@ -184,7 +194,7 @@ export async function translateVSACToInternalValueSet(
fhirValueset: FhirValueSet,
ersdConceptType: ErsdConceptType,
) {
const id = fhirValueset.id;
const oid = fhirValueset.id;
const version = fhirValueset.version;

const name = fhirValueset.title;
Expand All @@ -197,9 +207,10 @@ export async function translateVSACToInternalValueSet(
});

return {
valueSetId: id,
valueSetId: `${oid}_${version}`,
valueSetVersion: version,
valueSetName: name,
valueSetExternalId: oid,
author: author,
system: system,
ersdConceptType: ersdConceptType,
Expand Down Expand Up @@ -267,8 +278,11 @@ export async function insertValueSet(vs: ValueSet) {
* @returns The SQL statement for insertion
*/
function generateValueSetSqlPromise(vs: ValueSet) {
const valueSetOid = vs.valueSetId;
const valueSetOid = vs.valueSetExternalId;

// TODO: based on how non-VSAC valuests are shaped in the future, we may need
// to update the ID scheme to have something more generically defined that
// don't rely on potentially null external ID values.
const valueSetUniqueId = `${valueSetOid}_${vs.valueSetVersion}`;

// In the event a duplicate value set by OID + Version is entered, simply
Expand All @@ -295,7 +309,7 @@ function generateValueSetSqlPromise(vs: ValueSet) {
vs.valueSetVersion,
vs.valueSetName,
vs.author,
vs.ersdConceptType,
vs.dibbsConceptType,
];

return dbClient.query(insertValueSetSql, valuesArray);
Expand Down Expand Up @@ -391,3 +405,51 @@ function logRejectedPromiseReasons<T>(
return r.reason;
});
}

/**
* Function that orchestrates query insertion for the query building flow
* @param input - Values of the shape UserQueryInput needed for query insertion
* @returns - Success or failure status, with associated error message for frontend
*/
export async function insertQuery(input: UserQueryInput) {
const { sql, values } = generateQueryInsertionSql(input);
const insertUserQueryPromise = dbClient.query(sql, values);
const errorArray = [];

let queryId;
try {
const results = await insertUserQueryPromise;
queryId = results.rows[0].id as unknown as UUID;
} catch (e) {
console.error(
`Error occured in user query insertion: insertion for ${input.queryName} failed`,
);
console.error(e);
errorArray.push("Error occured in user query insertion");

return { success: false, error: errorArray.join(",") };
}

const insertJoinSqlArray = generateQueryToValueSetInsertionSql(
input,
queryId as UUID,
);

const joinPromises = insertJoinSqlArray.map((q) => {
dbClient.query(q.sql, q.values);
});

const joinInsertResults = await Promise.allSettled(joinPromises);

const joinInsertsSucceeded = joinInsertResults.every(
(r) => r.status === "fulfilled",
);

if (!joinInsertsSucceeded) {
logRejectedPromiseReasons(joinInsertResults, "Concept insertion failed");
errorArray.push("Error occured in concept insertion");
}

if (errorArray.length === 0) return { success: true };
return { success: false, error: errorArray.join(",") };
}
72 changes: 72 additions & 0 deletions query-connector/src/app/query-building.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { UUID, randomUUID } from "crypto";
import { Concept, ValueSet } from "./constants";

// TODO: Potentially merge this / infer this from the type created via the
// database creation workstream
export type UserQueryInput = {
queryName: string;
author: string;
valueSets: ValueSet[];
concepts?: Concept[];
timeWindowUnit?: string; // TODO: should probably type this more strongly
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 UserQueryInput needed for query insertion
* @returns [sql, values] needed for query building insertion
*/
export function generateQueryInsertionSql(input: UserQueryInput) {
const id = randomUUID();
const dateCreated = new Date().toISOString();
const dateLastModified = new Date().toISOString();

const sql =
"INSERT INTO query VALUES($1,$2,$3,$4,$5,$6,$7) RETURNING id, query_name;";

const values = [
id,
input.queryName,
input.author,
dateCreated,
dateLastModified,
input?.timeWindowNumber ?? DEFAULT_TIME_WINDOW.timeWindowNumber,
input?.timeWindowUnit ?? DEFAULT_TIME_WINDOW.timeWindowUnit,
];

return { sql: sql, values: values } as const;
}

/**
* Function that generates SQL for the query_to_valueset join table needed for
* query building.
* @param input - Values of the shape UserQueryInput needed for query insertion
* @param queryId - ID of the query that's already been created to associate with
* a given valueset
* @returns An array of {sql, values} to be inserted by the join insertion flow
*/
export function generateQueryToValueSetInsertionSql(
input: UserQueryInput,
queryId: UUID,
) {
const joinInsertionSqlArray = input.valueSets.map((v) => {
const sql =
"INSERT INTO query_to_valueset VALUES($1,$2,$3,$4) RETURNING query_id, valueset_id;";
const queryToValueSetId = `${queryId}_${v.valueSetId}`;
const values = [
queryToValueSetId,
queryId,
v.valueSetId,
v.valueSetExternalId,
];

return { sql: sql, values: values } as const;
});
return joinInsertionSqlArray;
}
28 changes: 21 additions & 7 deletions query-connector/src/app/query/SelectQuery.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
"use client";
import React, { useEffect, useState } from "react";
import { FHIR_SERVERS, USE_CASES, ValueSet } from "../constants";
import {
FHIR_SERVERS,
USE_CASES,
UseCaseToQueryName,
ValueSet,
} from "../constants";
import CustomizeQuery from "./components/CustomizeQuery";
import SelectSavedQuery from "./components/selectQuery/SelectSavedQuery";

Expand Down Expand Up @@ -71,12 +76,21 @@ const SelectQuery: React.FC<SelectQueryProps> = ({
// avoid name-change race conditions
let isSubscribed = true;

fetchUseCaseValueSets(
selectedQuery,
setQueryValueSets,
isSubscribed,
setLoadingQueryValueSets,
).catch(console.error);
const fetchDataAndUpdateState = async () => {
if (selectedQuery) {
const queryName = UseCaseToQueryName[selectedQuery as USE_CASES];
const valueSets = await fetchUseCaseValueSets(queryName);

// Only update if the fetch hasn't altered state yet
if (isSubscribed) {
setQueryValueSets(valueSets);
}
}
};

setLoadingQueryValueSets(true);
fetchDataAndUpdateState().catch(console.error);
setLoadingQueryValueSets(false);

// Destructor hook to prevent future state updates
return () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function groupValueSetsByNameAuthorSystem(valueSetsToGroup: ValueSet[]) {
valueSetId: row.valueSetId,
valueSetVersion: row.valueSetVersion,
valueSetName: row.valueSetName,
valueSetExternalId: row.valueSetExternalId,
author: row.author,
system: row.system,
ersdConceptType: row.ersdConceptType,
Expand Down
33 changes: 8 additions & 25 deletions query-connector/src/app/query/components/selectQuery/queryHooks.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,28 @@
import {
FHIR_SERVERS,
USE_CASES,
UseCaseToQueryName,
ValueSet,
hyperUnluckyPatient,
} from "@/app/constants";
import {
getSavedQueryByName,
mapQueryRowsToConceptValueSets,
mapQueryRowsToValueSets,
} from "@/app/database-service";
import { UseCaseQuery, UseCaseQueryResponse } from "@/app/query-service";
import { Patient } from "fhir/r4";

type SetStateCallback<T> = React.Dispatch<React.SetStateAction<T>>;

/**
* Query to grab valuesets based on use case
* @param selectedQuery - Query use case that's been selected
* @param valueSetStateCallback - state update function to set the valuesets
* @param isSubscribed - state destructor hook to prevent race conditions
* @param setIsLoading - update function to control loading UI
* Fetch to grab valuesets based the name of the query
* @param queryName - name of the query to grab associated ValueSets for
* @returns The valuesets from the specified query name
*/
export async function fetchUseCaseValueSets(
selectedQuery: USE_CASES,
valueSetStateCallback: SetStateCallback<ValueSet[]>,
isSubscribed: boolean,
setIsLoading: (isLoading: boolean) => void,
) {
if (selectedQuery) {
const queryName = UseCaseToQueryName[selectedQuery as USE_CASES];
export async function fetchUseCaseValueSets(queryName: string) {
const queryResults = await getSavedQueryByName(queryName);
const valueSets = await mapQueryRowsToValueSets(queryResults);

setIsLoading(true);
const queryResults = await getSavedQueryByName(queryName);
const valueSets = await mapQueryRowsToConceptValueSets(queryResults);

// Only update if the fetch hasn't altered state yet
if (isSubscribed) {
valueSetStateCallback(valueSets);
}
setIsLoading(false);
}
return valueSets;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { translateVSACToInternalValueSet } from "../../database-service";
import { ValueSet as FhirValueSet } from "fhir/r4";

const EXPECTED_INTERNAL_VALUESET: ValueSet = {
valueSetId: ExampleVsacValueSet.id,
valueSetId: `${ExampleVsacValueSet.id}_${ExampleVsacValueSet.version}`,
valueSetVersion: ExampleVsacValueSet.version,
valueSetName: ExampleVsacValueSet.title,
valueSetExternalId: ExampleVsacValueSet.id,
author: ExampleVsacValueSet.publisher,
system: ExampleVsacValueSet.compose.include[0].system,
ersdConceptType: "ostc",
Expand Down

0 comments on commit 90aa944

Please sign in to comment.