Skip to content

Commit

Permalink
Merge branch 'main' into kcd/16-rework-name-regex-on-patient-search-page
Browse files Browse the repository at this point in the history
  • Loading branch information
katyasoup authored Oct 28, 2024
2 parents 6f32619 + 88ad9de commit a2f1de8
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 12 deletions.
8 changes: 4 additions & 4 deletions query-connector/e2e/alternate_queries.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,8 @@ test.describe("alternate queries with the Query Connector", () => {
await expect(page.getByText(TEST_PATIENT.MRN)).toBeVisible();
});

test("social determinants query with generalized function", async ({
page,
}) => {
// test("social determinants query with generalized function", async ({
test("cancer query with generalized function", async ({ page }) => {
await page.getByRole("button", { name: "Go to the demo" }).click();
await page.getByRole("button", { name: "Fill fields" }).click();
await page.getByRole("button", { name: "Search for patient" }).click();
Expand All @@ -54,7 +53,8 @@ test.describe("alternate queries with the Query Connector", () => {
await expect(
page.getByRole("heading", { name: "Select a query" }),
).toBeVisible();
await page.getByTestId("Select").selectOption("social-determinants");
// await page.getByTestId("Select").selectOption("social-determinants");
await page.getByTestId("Select").selectOption("cancer");
await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 });

Expand Down
24 changes: 20 additions & 4 deletions query-connector/src/app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ export const demoQueryOptions = [
{ value: "chlamydia", label: "Chlamydia case investigation" },
{ value: "gonorrhea", label: "Gonorrhea case investigation" },
{ value: "newborn-screening", label: "Newborn screening follow-up" },
{
value: "social-determinants",
label: "Gather social determinants of health",
},
// Temporarily remove social determinants
// {
// value: "social-determinants",
// label: "Gather social determinants of health",
// },
{ value: "syphilis", label: "Syphilis case investigation" },
];

Expand Down Expand Up @@ -386,3 +387,18 @@ export function isFhirResource(resource: unknown): resource is FhirResource {
"resourceType" in resource
);
}

// The value for "concept version" (sometimes) exists under "expansion" in the VSAC FHIR
// response. This is similar, but different in some subtle ways from the "compose.include"
// path that we're currently using to grab concept information. Although we could
// grab and parse this information from the FHIR response, it would involve
// changing our data model to store information that we think is a "nice to
// have", which is only available sometimes. As a result, we're purposefully
// leaving this blank until we can clean up the migration schema to drop these columns
export const INTENTIONAL_EMPTY_STRING_FOR_CONCEPT_VERSION = "";

// Originally, the column in the concept table was set up to maintain backwards e
//compatibility with the ICD-9 codes that the team is deciding not to support after
// we clean up the DB migration. Leaving these in until we can clean these up
// in the migration schema
export const INTENTIONAL_EMPTY_STRING_FOR_GEM_CODE = "";
143 changes: 141 additions & 2 deletions query-connector/src/app/database-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { Pool, PoolConfig, QueryResultRow } from "pg";
import { Bundle, OperationOutcome, ValueSet as FhirValueSet } from "fhir/r4";
import {
Concept,
DEFAULT_ERSD_VERSION,
ErsdConceptType,
INTENTIONAL_EMPTY_STRING_FOR_CONCEPT_VERSION,
INTENTIONAL_EMPTY_STRING_FOR_GEM_CODE,
ValueSet,
ersdToDibbsConceptMap,
} from "./constants";
Expand Down Expand Up @@ -184,7 +185,7 @@ export async function translateVSACToInternalValueSet(
ersdConceptType: ErsdConceptType,
) {
const id = fhirValueset.id;
const version = DEFAULT_ERSD_VERSION;
const version = fhirValueset.version;

const name = fhirValueset.title;
const author = fhirValueset.publisher;
Expand All @@ -207,3 +208,141 @@ export async function translateVSACToInternalValueSet(
concepts: concepts,
} as ValueSet;
}

/**
* Function call to insert a new ValueSet into the database.
* @param vs - a ValueSet in of the shape of our internal data model to insert
* @returns success / failure information, as well as errors as appropriate
*/
export async function insertValueSet(vs: ValueSet) {
let errorArray: string[] = [];

const insertValueSetPromise = generateValueSetSqlPromise(vs);
try {
await insertValueSetPromise;
} catch (e) {
console.error(
`ValueSet insertion for ${vs.valueSetId}_${vs.valueSetVersion} failed`,
);
console.error(e);
errorArray.push("Error occured in valuset insertion");
}

const insertConceptsPromiseArray = generateConceptSqlPromises(vs);
const conceptInsertResults = await Promise.allSettled(
insertConceptsPromiseArray,
);

const allConceptInsertsSucceed = conceptInsertResults.every(
(r) => r.status === "fulfilled",
);

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

const joinInsertsPromiseArray = generateValuesetConceptJoinSqlPromises(vs);
const joinInsertResults = await Promise.allSettled(joinInsertsPromiseArray);

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

if (!allJoinInsertsSucceed) {
logRejectedPromiseReasons(
joinInsertResults,
"ValueSet <> concept join insert failed",
);
errorArray.push("Error occured in ValueSet <> concept join seeding");
}

if (errorArray.length === 0) return { success: true };
return { success: false, error: errorArray.join(",") };
}

/**
* Helper function to generate the SQL needed for valueset insertion.
* @param vs - The ValueSet in of the shape of our internal data model to insert
* @returns The SQL statement for insertion
*/
function generateValueSetSqlPromise(vs: ValueSet) {
const valueSetOid = vs.valueSetId;

const valueSetUniqueId = `${valueSetOid}_${vs.valueSetVersion}`;
const insertValueSetSql =
"INSERT INTO valuesets VALUES($1,$2,$3,$4,$5,$6) RETURNING id;";
const valuesArray = [
valueSetUniqueId,
valueSetOid,
vs.valueSetVersion,
vs.valueSetName,
vs.author,
vs.ersdConceptType,
];

return dbClient.query(insertValueSetSql, valuesArray);
}

/**
* Helper function to generate the SQL needed for concept / valueset join insertion
* needed during valueset creation.
* @param vs - The ValueSet in of the shape of our internal data model to insert
* @returns The SQL statement array for all concepts for insertion
*/
function generateConceptSqlPromises(vs: ValueSet) {
const insertConceptsSqlArray = vs.concepts.map((concept) => {
const systemPrefix = stripProtocolAndTLDFromSystemUrl(vs.system);
const conceptUniqueId = `${systemPrefix}_${concept.code}`;
const insertConceptSql = `INSERT INTO concepts VALUES($1,$2,$3,$4,$5,$6) RETURNING id;`;
const conceptInsertPromise = dbClient.query(insertConceptSql, [
conceptUniqueId,
concept.code,
vs.system,
concept.display,
// see notes in constants file for the intentional empty strings
INTENTIONAL_EMPTY_STRING_FOR_GEM_CODE,
INTENTIONAL_EMPTY_STRING_FOR_CONCEPT_VERSION,
]);

return conceptInsertPromise;
});

return insertConceptsSqlArray;
}

function generateValuesetConceptJoinSqlPromises(vs: ValueSet) {
const valueSetUniqueId = `${vs.valueSetId}_${vs.valueSetVersion}`;
const insertConceptsSqlArray = vs.concepts.map((concept) => {
const systemPrefix = stripProtocolAndTLDFromSystemUrl(vs.system);
const conceptUniqueId = `${systemPrefix}_${concept.code}`;
const insertJoinSql = `INSERT INTO valueset_to_concept VALUES($1,$2, $3) RETURNING valueset_id, concept_id;`;
const conceptInsertPromise = dbClient.query(insertJoinSql, [
`${valueSetUniqueId}_${conceptUniqueId}`,
valueSetUniqueId,
conceptUniqueId,
]);

return conceptInsertPromise;
});

return insertConceptsSqlArray;
}

function stripProtocolAndTLDFromSystemUrl(systemURL: string) {
const match = systemURL.match(/https?:\/\/([^\.]+)/);
return match ? match[1] : systemURL;
}

function logRejectedPromiseReasons<T>(
resultsArray: PromiseSettledResult<T>[],
errorMessageString: string,
) {
return resultsArray
.filter((r): r is PromiseRejectedResult => r.status === "rejected")
.map((r) => {
console.error(errorMessageString);
console.error(r.reason);
return r.reason;
});
}
4 changes: 2 additions & 2 deletions query-connector/src/app/tests/integration/translation.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { DEFAULT_ERSD_VERSION, ValueSet } from "@/app/constants";
import { ValueSet } from "@/app/constants";
import ExampleVsacValueSet from "../assets/VSACValueSet.json";
import { translateVSACToInternalValueSet } from "../../database-service";
import { ValueSet as FhirValueSet } from "fhir/r4";

const EXPECTED_INTERNAL_VALUESET: ValueSet = {
valueSetId: ExampleVsacValueSet.id,
valueSetVersion: DEFAULT_ERSD_VERSION,
valueSetVersion: ExampleVsacValueSet.version,
valueSetName: ExampleVsacValueSet.title,
author: ExampleVsacValueSet.publisher,
system: ExampleVsacValueSet.compose.include[0].system,
Expand Down

0 comments on commit a2f1de8

Please sign in to comment.