diff --git a/query-connector/src/app/api/query/error-handling-service.ts b/query-connector/src/app/api/query/error-handling-service.ts index 7d6d30952..634b60c75 100644 --- a/query-connector/src/app/api/query/error-handling-service.ts +++ b/query-connector/src/app/api/query/error-handling-service.ts @@ -1,4 +1,5 @@ import { OperationOutcome } from "fhir/r4"; +import { NextResponse } from "next/server"; /** * Handles a request error by returning an OperationOutcome with a diagnostics message. @@ -21,3 +22,20 @@ export async function handleRequestError( }; return OperationOutcome; } + +export async function handleAndReturnError(error: unknown, status = 500) { + let diagnostics_message = "An error has occurred"; + + let OperationOutcome; + if (typeof error === "string") { + diagnostics_message = `${diagnostics_message}: ${error}`; + OperationOutcome = await handleRequestError(error as string); + } else { + if (error instanceof Error) { + diagnostics_message = `${diagnostics_message}: ${error}`; + } + OperationOutcome = await handleRequestError(diagnostics_message); + } + console.error(error); + return NextResponse.json(OperationOutcome, { status: status }); +} diff --git a/query-connector/src/app/api/query/hl7v2/route.ts b/query-connector/src/app/api/query/hl7v2/route.ts deleted file mode 100644 index 7f6734bf0..000000000 --- a/query-connector/src/app/api/query/hl7v2/route.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - MISSING_API_QUERY_PARAM, - INVALID_FHIR_SERVERS, - MISSING_PATIENT_IDENTIFIERS, - INVALID_QUERY, -} from "@/app/shared/constants"; -import { getFhirServerNames } from "@/app/shared/database-service"; -import { - QueryResponse, - APIQueryResponse, - createBundle, - makeFhirQuery, - QueryRequest, -} from "@/app/shared/query-service"; -import { NextRequest, NextResponse } from "next/server"; -import { handleRequestError } from "../error-handling-service"; -import { getSavedQueryById } from "@/app/backend/query-building"; -import { Message } from "node-hl7-client"; - -/** - * Runs a query for a given use case and FHIR server. Patient demographics are provided - * in the request body as a raw text HL7v2 message. The use_case and fhir_server are - * provided - * @param request - The incoming Next.js request object. - * @returns Response with UseCaseResponse. - */ -export async function POST(request: NextRequest) { - // Extract use_case and fhir_server from nextUrl - const params = request.nextUrl.searchParams; - const id = params.get("id"); - const fhir_server = params.get("fhir_server"); - const fhirServers = await getFhirServerNames(); - - if (!id || !fhir_server) { - const OperationOutcome = await handleRequestError(MISSING_API_QUERY_PARAM); - return NextResponse.json(OperationOutcome); - } else if (!Object.values(fhirServers).includes(fhir_server)) { - const OperationOutcome = await handleRequestError(INVALID_FHIR_SERVERS); - return NextResponse.json(OperationOutcome); - } - - // Lookup default parameters for particular use-case search - const queryResults = await getSavedQueryById(id); - - if (queryResults === undefined) { - const OperationOutcome = await handleRequestError(INVALID_QUERY); - return NextResponse.json(OperationOutcome, { - status: 500, - }); - } - - const rawMessage = await request.text(); - const parsedMessage = new Message({ text: rawMessage }); - - // Add params & patient identifiers to QueryRequest - const queryRequest: QueryRequest = { - query_name: queryResults?.query_name, - fhir_server: fhir_server, - first_name: parsedMessage.get("PID.5.2").toString() || "", - last_name: parsedMessage.get("PID.5.1").toString() || "", - dob: parsedMessage.get("PID.7.1").toString() || "", - }; - - if ( - !queryRequest.first_name && - !queryRequest.last_name && - !queryRequest.dob - ) { - const OperationOutcome = await handleRequestError( - MISSING_PATIENT_IDENTIFIERS, - ); - return NextResponse.json(OperationOutcome, { status: 400 }); - } - - const UseCaseQueryResponse: QueryResponse = await makeFhirQuery(queryRequest); - - // Bundle data - const bundle: APIQueryResponse = await createBundle(UseCaseQueryResponse); - - return NextResponse.json(bundle); -} diff --git a/query-connector/src/app/api/query/fhir/parsers.ts b/query-connector/src/app/api/query/parsers.ts similarity index 72% rename from query-connector/src/app/api/query/fhir/parsers.ts rename to query-connector/src/app/api/query/parsers.ts index 526d4d668..a7891a61c 100644 --- a/query-connector/src/app/api/query/fhir/parsers.ts +++ b/query-connector/src/app/api/query/parsers.ts @@ -2,6 +2,7 @@ import { Patient } from "fhir/r4"; import { FormatPhoneAsDigits } from "@/app/shared/format-service"; +import { USE_CASES, USE_CASE_DETAILS } from "@/app/shared/constants"; export type PatientIdentifiers = { first_name?: string; @@ -69,12 +70,13 @@ export async function parseMRNs( patient: Patient, ): Promise<(string | undefined)[] | undefined> { if (patient.identifier) { - const mrnIdentifiers = patient.identifier.filter((id) => - id.type?.coding?.some( - (coding) => - coding.system === "http://terminology.hl7.org/CodeSystem/v2-0203" && - coding.code === "MR", - ), + const mrnIdentifiers = patient.identifier.filter( + (id) => + id.type?.coding?.some( + (coding) => + coding.system === "http://terminology.hl7.org/CodeSystem/v2-0203" && + coding.code === "MR", + ), ); return mrnIdentifiers.map((id) => id.value); } @@ -99,3 +101,25 @@ export async function parsePhoneNumbers( return phoneNumbers.map((contactPoint) => contactPoint.value); } } + +export function parseHL7FromRequestBody(requestText: string) { + let result = requestText; + + // strip the leading { / closing } if they exist + if (requestText[0] === "{" || requestText[requestText.length - 1] === "}") { + const leadingClosingBraceRegex = /\{([\s\S]*)\}/; + const requestMatch = requestText.match(leadingClosingBraceRegex); + if (requestMatch) { + console.log(requestMatch[1]); + result = requestMatch[1].trim(); + } + } + return result; +} + +export function mapDeprecatedUseCaseToId(use_case: string | null) { + if (use_case === null) return null; + const potentialUseCaseMatch = USE_CASE_DETAILS[use_case as USE_CASES]; + const queryId = potentialUseCaseMatch?.id ?? null; + return queryId; +} diff --git a/query-connector/src/app/api/query/route.ts b/query-connector/src/app/api/query/route.ts index 8dbd63c8d..eda65ccd3 100644 --- a/query-connector/src/app/api/query/route.ts +++ b/query-connector/src/app/api/query/route.ts @@ -1,5 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; -import { handleRequestError } from "./error-handling-service"; +import { + handleAndReturnError, + handleRequestError, +} from "./error-handling-service"; import { makeFhirQuery, QueryRequest, @@ -16,9 +19,16 @@ import { INVALID_QUERY, USE_CASE_DETAILS, USE_CASES, + INVALID_MESSAGE_FORMAT, + HL7_BODY_MISFORMAT, } from "@/app/shared/constants"; import { getFhirServerNames } from "@/app/shared/database-service"; -import { parsePatientDemographics } from "./fhir/parsers"; +import { + mapDeprecatedUseCaseToId, + parseHL7FromRequestBody, + parsePatientDemographics, +} from "./parsers"; +import { Message } from "node-hl7-client"; /** * @swagger @@ -154,7 +164,7 @@ export async function GET(request: NextRequest) { * @swagger * /api/query: * post: - * description: A POST endpoint that accepts a FHIR patient resource in the request body to execute a query within the Query Connector + * description: A POST endpoint that accepts a FHIR patient resource or an HL7v2 message in the request body to execute a query within the Query Connector * parameters: * - name: fhir_server * in: query @@ -170,6 +180,13 @@ export async function GET(request: NextRequest) { * schema: * type: string * example: cf580d8d-cc7b-4eae-8a0d-96c36f9222e3 + * - name: message_format + * in: query + * description: Whether the request body contents are HL7 or FHIR formatted messages + * schema: + * type: string + * enum: [HL7, FHIR] + * example: FHIR * requestBody: * required: true * content: @@ -187,28 +204,6 @@ export async function GET(request: NextRequest) { * @returns Response with QueryResponse. */ export async function POST(request: NextRequest) { - let requestBody; - - try { - requestBody = await request.json(); - - // Check if requestBody is a patient resource - if (requestBody.resourceType !== "Patient") { - const OperationOutcome = await handleRequestError( - RESPONSE_BODY_IS_NOT_PATIENT_RESOURCE, - ); - return NextResponse.json(OperationOutcome); - } - } catch (error: unknown) { - let diagnostics_message = "An error occurred."; - console.error(error); - if (error instanceof Error) { - diagnostics_message = `${error.message}`; - } - const OperationOutcome = await handleRequestError(diagnostics_message); - return NextResponse.json(OperationOutcome, { status: 500 }); - } - // Extract id and fhir_server from nextUrl const params = request.nextUrl.searchParams; //deprecated, prefer id @@ -219,57 +214,84 @@ export async function POST(request: NextRequest) { const id = id_param ? id_param : mapDeprecatedUseCaseToId(use_case_param); - if (!id || !fhir_server || !requestBody) { - const OperationOutcome = await handleRequestError(MISSING_API_QUERY_PARAM); - return NextResponse.json(OperationOutcome, { - status: 500, - }); + if (!id || !fhir_server) { + return await handleAndReturnError(MISSING_API_QUERY_PARAM); } else if (!Object.values(fhirServers).includes(fhir_server)) { - const OperationOutcome = await handleRequestError(INVALID_FHIR_SERVERS); - return NextResponse.json(OperationOutcome, { - status: 500, - }); + return await handleAndReturnError(INVALID_FHIR_SERVERS); } const queryResults = await getSavedQueryById(id); - if (queryResults === undefined) { - const OperationOutcome = await handleRequestError(INVALID_QUERY); - return NextResponse.json(OperationOutcome, { - status: 500, - }); + return handleAndReturnError(INVALID_QUERY); } - // try getting params straight from requestBody - const given = requestBody["given"]; - const family = requestBody["family"]; - const dob = requestBody["dob"]; - const mrn = requestBody["mrn"]; - const phone = requestBody["phone"]; - const noParamsDefined = [given, family, dob, mrn, phone].every( - (e) => e === undefined, - ); + // check message format of body, default to FHIR + const messageFormat = params.get("message_format") ?? "FHIR"; + if (messageFormat !== "FHIR" && messageFormat !== "HL7") { + return await handleAndReturnError(INVALID_MESSAGE_FORMAT); + } - // Parse patient identifiers from a potential FHIR resource - const PatientIdentifiers = await parsePatientDemographics(requestBody); + let QueryRequest: QueryRequest; + if (messageFormat === "HL7") { + try { + let requestText = await request.text(); - if (Object.keys(PatientIdentifiers).length === 0 && noParamsDefined) { - const OperationOutcome = await handleRequestError( - MISSING_PATIENT_IDENTIFIERS, - ); - return NextResponse.json(OperationOutcome, { status: 400 }); - } + const parsedMessage = new Message({ + text: parseHL7FromRequestBody(requestText), + }); - // Add params & patient identifiers to QueryName - const QueryRequest: QueryRequest = { - query_name: queryResults.query_name, - fhir_server: fhir_server, - first_name: PatientIdentifiers?.first_name ?? given, - last_name: PatientIdentifiers?.last_name ?? family, - dob: PatientIdentifiers?.dob ?? dob, - mrn: PatientIdentifiers?.mrn ?? mrn, - phone: PatientIdentifiers?.phone ?? phone, - }; + console.log( + parsedMessage.get("PID.3.1").toString(), + parsedMessage.get("NK1.5.1").toString(), + ); + QueryRequest = { + query_name: queryResults?.query_name, + fhir_server: fhir_server, + first_name: parsedMessage.get("PID.5.2").toString() || "", + last_name: parsedMessage.get("PID.5.1").toString() || "", + dob: parsedMessage.get("PID.7.1").toString() || "", + mrn: parsedMessage.get("PID.3.1").toString() || "", + phone: parsedMessage.get("NK1.5.1").toString() || "", + }; + } catch (error: unknown) { + return await handleAndReturnError(error); + } + } else { + try { + const requestBodyToCheck = await request.json(); + // try extracting patient identifiers out of the request body from a potential + // FHIR message or as raw params + if ( + requestBodyToCheck.resourceType && + requestBodyToCheck.resourceType !== "Patient" + ) { + return await handleAndReturnError( + RESPONSE_BODY_IS_NOT_PATIENT_RESOURCE, + ); + } + + // Parse patient identifiers from a potential FHIR resource + const PatientIdentifiers = + await parsePatientDemographics(requestBodyToCheck); + + if (Object.keys(PatientIdentifiers).length === 0) { + return await handleAndReturnError(MISSING_PATIENT_IDENTIFIERS, 400); + } + + // Add params & patient identifiers to QueryName + QueryRequest = { + query_name: queryResults.query_name, + fhir_server: fhir_server, + first_name: PatientIdentifiers?.first_name, + last_name: PatientIdentifiers?.last_name, + dob: PatientIdentifiers?.dob, + mrn: PatientIdentifiers?.mrn, + phone: PatientIdentifiers?.phone, + }; + } catch (error: unknown) { + return await handleAndReturnError(error); + } + } const QueryResponse: QueryResponse = await makeFhirQuery(QueryRequest); @@ -280,10 +302,3 @@ export async function POST(request: NextRequest) { status: 200, }); } - -function mapDeprecatedUseCaseToId(use_case: string | null) { - if (use_case === null) return null; - const potentialUseCaseMatch = USE_CASE_DETAILS[use_case as USE_CASES]; - const queryId = potentialUseCaseMatch?.id ?? null; - return queryId; -} diff --git a/query-connector/src/app/shared/constants.ts b/query-connector/src/app/shared/constants.ts index e89f4b451..7f0fdb291 100644 --- a/query-connector/src/app/shared/constants.ts +++ b/query-connector/src/app/shared/constants.ts @@ -242,6 +242,10 @@ export const INVALID_FHIR_SERVERS = `Invalid fhir_server. Please provide a valid export const RESPONSE_BODY_IS_NOT_PATIENT_RESOURCE = "Request body is not a Patient resource."; export const MISSING_API_QUERY_PARAM = "Missing id or fhir_server."; +export const INVALID_MESSAGE_FORMAT = + "Invalid message format. Format parameter needs to be either 'HL7' or 'FHIR'"; +export const HL7_BODY_MISFORMAT = + "Invalid HL7 request. Please add your HL7 message to the request body in between curly braces like so - { YOUR MESSAGE HERE } "; export const MISSING_PATIENT_IDENTIFIERS = "No patient identifiers to parse from requestBody.";