Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor new data model #43

Merged
merged 5 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ALTER TABLE valuesets
ADD COLUMN dibbsConceptType text GENERATED ALWAYS AS (
ADD COLUMN dibbs_concept_type text GENERATED ALWAYS AS (
CASE
WHEN type IN ('lotc', 'lrtc', 'ostc') THEN 'labs'
WHEN type = 'mrtc' THEN 'medications'
Expand Down
6 changes: 3 additions & 3 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,
mapQueryRowsToValueSetItems,
mapQueryRowsToConceptValueSets,
} from "@/app/database-service";

/**
Expand Down Expand Up @@ -89,7 +89,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 vsItems = await mapQueryRowsToValueSetItems(queryResults);
const valueSets = await mapQueryRowsToConceptValueSets(queryResults);

// Add params & patient identifiers to UseCaseRequest
const UseCaseRequest: UseCaseQueryRequest = {
Expand All @@ -108,7 +108,7 @@ export async function POST(request: NextRequest) {

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

// Bundle data
Expand Down
56 changes: 13 additions & 43 deletions query-connector/src/app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,38 +292,6 @@ export const metadata = {
description: "Try out TEFCA with queries for public health use cases.",
};

// TODO: Remove ValueSetItem, ValueSet, and valueSetTypeToClincalServiceTypeMap once
// ticket #2789 is resolved

/*Type to specify the expected components for each item in a value set that will be
displayed in the CustomizeQuery component*/
export interface ValueSetItem {
code: string;
display: string;
system: string;
include: boolean;
author: string;
clinicalServiceType: string;
valueSetName: string;
}

/*Type to specify the expected expected types of valueset items that will be displayed
as separate tabs in the CusomizeQuery component*/
export interface ValueSet {
labs: ValueSetItem[];
medications: ValueSetItem[];
conditions: ValueSetItem[];
}

export type ValueSetType = keyof ValueSet;

export const valueSetTypeToClincalServiceTypeMap = {
labs: ["ostc", "lotc", "lrtc"],
medications: ["mrtc"],
conditions: ["dxtc", "sdtc"],
};
/// TODO: Remove the above once ticket #2789 is resolved

/*
* The expected type of a ValueSet concept.
*/
Expand All @@ -336,17 +304,17 @@ export interface Concept {
/*
* The expected type of a ValueSet.
*/
// export interface ValueSet {
// valueset_id: string;
// valueset_version: string;
// valueset_name: string;
// author: string;
// system: string;
// ersdConceptType?: string;
// dibbsConceptType: string;
// includeValueSet: boolean;
// concepts: Concept[];
// }
export interface ValueSet {
valueSetId: string;
valueSetVersion: string;
valueSetName: string;
author: string;
system: string;
ersdConceptType?: string;
dibbsConceptType: string;
includeValueSet: boolean;
concepts: Concept[];
}

/*
* The expected type of ValueSets grouped by dibbsConceptType for the purpose of display.
Expand All @@ -356,3 +324,5 @@ export interface ValueSetDisplay {
medications: ValueSet[];
conditions: ValueSet[];
}

export type DibbsValueSetType = keyof ValueSetDisplay;
75 changes: 39 additions & 36 deletions query-connector/src/app/database-service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"use server";
import { Pool, PoolConfig, QueryResultRow } from "pg";
import { Bundle, OperationOutcome } from "fhir/r4";
import { ValueSetItem, valueSetTypeToClincalServiceTypeMap } from "./constants";
import { encode } from "base-64";
import { ValueSet } from "./constants";

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
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
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 @@ -48,45 +48,48 @@ export const getSavedQueryByName = async (name: string) => {
};

/**
* Helper function to filter the valueset-mapped rows of results returned from
* the DB for particular types of related clinical services.
* @param vsItems A list of value sets mapped from DB rows.
* @param type One of "labs", "medications", or "conditions".
* @returns A list of rows containing only the predicate service type.
* Maps the results returned from the DIBBs value set and coding system database
* into a collection of value sets, each containing one or more Concepts build out
* of the coding information in the DB.
* @param rows The Rows returned from the DB Query.
* @returns A list of ValueSets, which hold the Concepts pulled from the DB.
*/
export const filterValueSets = async (
vsItems: ValueSetItem[],
type: "labs" | "medications" | "conditions",
export const mapQueryRowsToConceptValueSets = async (
rows: QueryResultRow[],
) => {
// Assign clinical code type based on desired filter
// Mapping is established in TCR, so follow that convention
let valuesetFilters = valueSetTypeToClincalServiceTypeMap[type];
const results = vsItems.filter((vs) =>
valuesetFilters.includes(vs.clinicalServiceType),
);
return results;
};
// 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)) {
conceptsByVSId[r["valueset_id"]] = [];
}
conceptsByVSId[r["valueset_id"]].push(r);
return conceptsByVSId;
}, {});

/**
* Helper function that transforms and groups a set of database rows into a list of
* ValueSet items grouped by author and code_system for display on the CustomizeQuery page.
* @param rows The rows returned from the DB.
* @returns A list of ValueSetItems grouped by author and system.
*/
export const mapQueryRowsToValueSetItems = async (rows: QueryResultRow[]) => {
const vsItems = rows.map((r) => {
const vsTranslation: ValueSetItem = {
code: r["code"],
display: r["display"],
system: r["code_system"],
include: r["include"],
author: r["author"],
valueSetName: r["valueset_name"],
clinicalServiceType: r["type"],
// Each "prop" of the struct is now a ValueSet ID
// Iterate over them to create formal Concept Groups attached to a formal VS
const valueSets = Object.keys(vsIdGroupedRows).map((vsID) => {
const conceptGroup: QueryResultRow[] = vsIdGroupedRows[vsID];
const valueSet: ValueSet = {
valueSetId: conceptGroup[0]["valueset_id"],
valueSetVersion: conceptGroup[0]["version"],
valueSetName: conceptGroup[0]["valueset_name"],
author: conceptGroup[0]["author"],
system: conceptGroup[0]["code_system"],
ersdConceptType: conceptGroup[0]["type"],
dibbsConceptType: conceptGroup[0]["dibbs_concept_type"],
includeValueSet: conceptGroup.find((c) => c["include"]) ? true : false,
concepts: conceptGroup.map((c) => {
return {
code: c["code"],
display: c["display"],
include: c["include"],
};
}),
};
return vsTranslation;
return valueSet;
});
return vsItems;
return valueSets;
};

/*
Expand Down
31 changes: 20 additions & 11 deletions query-connector/src/app/format-service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
ContactPoint,
Identifier,
} from "fhir/r4";
import { ValueSetItem } from "./constants";
import { ValueSet } from "./constants";
import { QueryStruct } from "./query-service";

/**
Expand Down Expand Up @@ -261,29 +261,38 @@ export async function GetPhoneQueryFormats(phone: string) {
}

/**
* Formats a statefully updated list of value set items into a JSON structure
* Formats a statefully updated list of value sets 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.
* @param valueSets The list of value sets the user wants included.
* @returns A structured specification of a query that can be executed.
*/
export const formatValueSetItemsAsQuerySpec = async (
export const formatValueSetsAsQuerySpec = async (
useCase: string,
vsItems: ValueSetItem[],
valueSets: ValueSet[],
) => {
let secondEncounter: boolean = false;
if (["cancer", "chlamydia", "gonorrhea", "syphilis"].includes(useCase)) {
secondEncounter = true;
}
const labCodes: string[] = vsItems
const labCodes: string[] = valueSets
.filter((vs) => vs.system === "http://loinc.org")
.map((vs) => vs.code);
const snomedCodes: string[] = vsItems
.reduce((acc, vs) => {
vs.concepts.forEach((concept) => acc.push(concept.code));
return acc;
}, [] as string[]);
const snomedCodes: string[] = valueSets
.filter((vs) => vs.system === "http://snomed.info/sct")
.map((vs) => vs.code);
const rxnormCodes: string[] = vsItems
.reduce((acc, vs) => {
vs.concepts.forEach((concept) => acc.push(concept.code));
return acc;
}, [] as string[]);
const rxnormCodes: string[] = valueSets
.filter((vs) => vs.system === "http://www.nlm.nih.gov/research/umls/rxnorm")
.map((vs) => vs.code);
.reduce((acc, vs) => {
vs.concepts.forEach((concept) => acc.push(concept.code));
return acc;
}, [] as string[]);

const spec: QueryStruct = {
labCodes: labCodes,
Expand Down
13 changes: 5 additions & 8 deletions query-connector/src/app/query-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import {
} from "fhir/r4";

import FHIRClient from "./fhir-servers";
import { USE_CASES, FHIR_SERVERS, ValueSetItem } from "./constants";
import { USE_CASES, FHIR_SERVERS, ValueSet } from "./constants";
import { CustomQuery } from "./CustomQuery";
import { GetPhoneQueryFormats } from "./format-service";
import { formatValueSetItemsAsQuerySpec } from "./format-service";
import { formatValueSetsAsQuerySpec } from "./format-service";

/**
* The query response when the request source is from the Viewer UI.
Expand Down Expand Up @@ -151,7 +151,7 @@ async function patientQuery(
*/
export async function UseCaseQuery(
request: UseCaseQueryRequest,
queryValueSets: ValueSetItem[],
queryValueSets: ValueSet[],
queryResponse: QueryResponse = {},
): Promise<QueryResponse> {
const fhirClient = new FHIRClient(request.fhir_server);
Expand Down Expand Up @@ -192,15 +192,12 @@ export async function UseCaseQuery(
*/
async function generalizedQuery(
useCase: USE_CASES,
queryValueSets: ValueSetItem[],
queryValueSets: ValueSet[],
patientId: string,
fhirClient: FHIRClient,
queryResponse: QueryResponse,
): Promise<QueryResponse> {
const querySpec = await formatValueSetItemsAsQuerySpec(
useCase,
queryValueSets,
);
const querySpec = await formatValueSetsAsQuerySpec(useCase, queryValueSets);
const builtQuery = new CustomQuery(querySpec, patientId);
let response: fetch.Response | fetch.Response[];

Expand Down
7 changes: 4 additions & 3 deletions query-connector/src/app/query/SelectQuery.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import React, { useEffect, useState } from "react";
import { FHIR_SERVERS, USE_CASES, ValueSetItem } from "../constants";
import { FHIR_SERVERS, USE_CASES, ValueSet } from "../constants";
import CustomizeQuery from "./components/CustomizeQuery";
import SelectSavedQuery from "./components/selectQuery/SelectSavedQuery";

Expand Down Expand Up @@ -57,8 +57,8 @@ const SelectQuery: React.FC<SelectQueryProps> = ({
setFhirServer,
setShowCustomizeQuery,
}) => {
const [queryValueSets, setQueryValueSets] = useState<ValueSetItem[]>(
[] as ValueSetItem[],
const [queryValueSets, setQueryValueSets] = useState<ValueSet[]>(
[] as ValueSet[],
);
const [loadingQueryValueSets, setLoadingQueryValueSets] =
useState<boolean>(false);
Expand Down Expand Up @@ -97,6 +97,7 @@ const SelectQuery: React.FC<SelectQueryProps> = ({
}

const displayLoading = loadingResultResponse || loadingQueryValueSets;

return (
<div>
{displayLoading && <LoadingView loading={loadingResultResponse} />}
Expand Down
Loading