diff --git a/containers/ecr-viewer/src/app/api/save-fhir-data/route.ts b/containers/ecr-viewer/src/app/api/save-fhir-data/route.ts index 1f51ff90aa..41212704af 100644 --- a/containers/ecr-viewer/src/app/api/save-fhir-data/route.ts +++ b/containers/ecr-viewer/src/app/api/save-fhir-data/route.ts @@ -43,13 +43,19 @@ export async function POST(request: NextRequest) { } if (requestBody.metadata) { - return saveWithMetadata( + const { message, status } = await saveWithMetadata( fhirBundle, ecrId, saveSource, requestBody.metadata, ); + return NextResponse.json({ message }, { status }); } else { - return saveFhirData(fhirBundle, ecrId, saveSource); + const { message, status } = await saveFhirData( + fhirBundle, + ecrId, + saveSource, + ); + return NextResponse.json({ message }, { status }); } } diff --git a/containers/ecr-viewer/src/app/api/save-fhir-data/save-fhir-data-service.ts b/containers/ecr-viewer/src/app/api/save-fhir-data/save-fhir-data-service.ts index ec79f91b6f..0694958d57 100644 --- a/containers/ecr-viewer/src/app/api/save-fhir-data/save-fhir-data-service.ts +++ b/containers/ecr-viewer/src/app/api/save-fhir-data/save-fhir-data-service.ts @@ -1,5 +1,4 @@ import { BlobServiceClient } from "@azure/storage-blob"; -import { NextResponse } from "next/server"; import { getDB } from "../services/postgres_db"; import { PutObjectCommand, PutObjectCommandOutput } from "@aws-sdk/client-s3"; import { Bundle } from "fhir/r4"; @@ -10,38 +9,47 @@ import { BundleExtendedMetadata, BundleMetadata } from "./types"; import { s3Client } from "../services/s3Client"; import { get_pool } from "../services/sqlserver_db"; +interface SaveResponse { + message: string; + status: number; +} + /** * Saves a FHIR bundle to a postgres database. * @async * @function saveToPostgres * @param fhirBundle - The FHIR bundle to be saved. * @param ecrId - The unique identifier for the Electronic Case Reporting (ECR) associated with the FHIR bundle. - * @returns A promise that resolves when the FHIR bundle is successfully saved to postgres. - * @throws {Error} Throws an error if the FHIR bundle cannot be saved to postgress. + * @returns An object containing the status and message. */ -export const saveToPostgres = async (fhirBundle: Bundle, ecrId: string) => { +export const saveToPostgres = async ( + fhirBundle: Bundle, + ecrId: string, +): Promise => { const { database, pgPromise } = getDB(); const { ParameterizedQuery: PQ } = pgPromise; const addFhir = new PQ({ - text: "INSERT INTO fhir VALUES ($1, $2) RETURNING ecr_id", + text: "INSERT INTO fhir VALUES ($1, $2)", values: [ecrId, fhirBundle], }); try { - const saveECR = await database.one(addFhir); + await database.none(addFhir); - return NextResponse.json( - { message: "Success. Saved FHIR Bundle to database: " + saveECR.ecr_id }, - { status: 200 }, - ); + return { + message: "Success. Saved FHIR bundle.", + status: 200, + }; } catch (error: any) { - console.error("Error inserting data to database:", error); - return NextResponse.json( - { - message: `Failed to insert data to database. ${error.message}`, - }, - { status: 500 }, - ); + console.error({ + message: `Failed to save FHIR bundle to postgres.`, + error, + ecrId, + }); + return { + message: "Failed to save FHIR bundle.", + status: 500, + }; } }; @@ -51,8 +59,7 @@ export const saveToPostgres = async (fhirBundle: Bundle, ecrId: string) => { * @function saveToS3 * @param fhirBundle - The FHIR bundle to be saved. * @param ecrId - The unique identifier for the Electronic Case Reporting (ECR) associated with the FHIR bundle. - * @returns A promise that resolves when the FHIR bundle is successfully saved to the S3 bucket. - * @throws {Error} Throws an error if the FHIR bundle cannot be saved to the S3 bucket. + * @returns An object containing the status and message. */ export const saveToS3 = async (fhirBundle: Bundle, ecrId: string) => { const bucketName = process.env.ECR_BUCKET_NAME; @@ -73,15 +80,21 @@ export const saveToS3 = async (fhirBundle: Bundle, ecrId: string) => { if (httpStatusCode !== 200) { throw new Error(`HTTP Status Code: ${httpStatusCode}`); } - return NextResponse.json( - { message: "Success. Saved FHIR Bundle to S3: " + ecrId }, - { status: 200 }, - ); + + return { + message: "Success. Saved FHIR bundle.", + status: 200, + }; } catch (error: any) { - return NextResponse.json( - { message: "Failed to insert data to S3. " + error.message }, - { status: 500 }, - ); + console.error({ + message: "Failed to save FHIR bundle to S3.", + error, + ecrId, + }); + return { + message: "Failed to save FHIR bundle.", + status: 500, + }; } }; @@ -91,10 +104,12 @@ export const saveToS3 = async (fhirBundle: Bundle, ecrId: string) => { * @function saveToAzure * @param fhirBundle - The FHIR bundle to be saved. * @param ecrId - The unique ID for the eCR associated with the FHIR bundle. - * @returns A promise that resolves when the FHIR bundle is successfully saved to Azure Blob Storage. - * @throws {Error} Throws an error if the FHIR bundle cannot be saved to Azure Blob Storage. + * @returns An object containing the status and message. */ -export const saveToAzure = async (fhirBundle: Bundle, ecrId: string) => { +export const saveToAzure = async ( + fhirBundle: Bundle, + ecrId: string, +): Promise => { // TODO: Make this global after we get Azure access const blobClient = BlobServiceClient.fromConnectionString( process.env.AZURE_STORAGE_CONNECTION_STRING!, @@ -119,19 +134,20 @@ export const saveToAzure = async (fhirBundle: Bundle, ecrId: string) => { throw new Error(`HTTP Status Code: ${response._response.status}`); } - return NextResponse.json( - { message: "Success. Saved FHIR bundle to Azure Blob Storage: " + ecrId }, - { status: 200 }, - ); + return { + message: "Success. Saved FHIR bundle.", + status: 200, + }; } catch (error: any) { - return NextResponse.json( - { - message: - "Failed to insert FHIR bundle to Azure Blob Storage. " + - error.message, - }, - { status: 500 }, - ); + console.error({ + message: "Failed to save FHIR bundle to Azure Blob Storage.", + error, + ecrId, + }); + return { + message: "Failed to save FHIR bundle.", + status: 500, + }; } }; @@ -141,27 +157,62 @@ export const saveToAzure = async (fhirBundle: Bundle, ecrId: string) => { * @param fhirBundle - The FHIR bundle to be saved. * @param ecrId - The unique identifier for the Electronic Case Reporting (ECR) associated with the FHIR bundle. * @param saveSource - The location to save the FHIR bundle. Valid values are "postgres", "s3", or "azure". - * @returns A `NextResponse` object with a JSON payload indicating the success message. The response content type is set to `application/json`. + * @returns An object containing the status and message. */ export const saveFhirData = async ( fhirBundle: Bundle, ecrId: string, saveSource: string, -) => { +): Promise => { if (saveSource === S3_SOURCE) { - return saveToS3(fhirBundle, ecrId); + return await saveToS3(fhirBundle, ecrId); } else if (saveSource === AZURE_SOURCE) { - return saveToAzure(fhirBundle, ecrId); + return await saveToAzure(fhirBundle, ecrId); } else if (saveSource === POSTGRES_SOURCE) { return await saveToPostgres(fhirBundle, ecrId); } else { - return NextResponse.json( - { - message: - 'Invalid save source. Please provide a valid value for \'saveSource\' ("postgres", "s3", or "azure").', - }, - { status: 400 }, - ); + return { + message: + 'Invalid save source. Please provide a valid value for \'saveSource\' ("postgres", "s3", or "azure").', + status: 400, + }; + } +}; + +/** + * @async + * @function saveFhirMetadata + * @param ecrId - The unique identifier for the Electronic Case Reporting (ECR) associated with the FHIR bundle. + * @param metadataSaveLocation - the location to save the metadata + * @param metadata - The metadata to be saved. + * @returns An object containing the status and message. + */ +const saveFhirMetadata = async ( + ecrId: string, + metadataSaveLocation: "postgres" | "sqlserver" | undefined, + metadata: BundleMetadata | BundleExtendedMetadata, +): Promise => { + try { + if (metadataSaveLocation == "postgres") { + return await saveMetadataToPostgres(metadata as BundleMetadata, ecrId); + } else if (metadataSaveLocation == "sqlserver") { + return await saveMetadataToSqlServer( + metadata as BundleExtendedMetadata, + ecrId, + ); + } else { + return { + message: "Unknown metadataSaveLocation: " + metadataSaveLocation, + status: 400, + }; + } + } catch (error: any) { + const message = "Failed to save FHIR metadata."; + console.error({ message, error, ecrId }); + return { + message, + status: 500, + }; } }; @@ -170,19 +221,16 @@ export const saveFhirData = async ( * @function saveMetadataToSqlServer * @param metadata - The FHIR bundle metadata to be saved. * @param ecrId - The unique identifier for the Electronic Case Reporting (ECR) associated with the FHIR bundle. - * @returns A `NextResponse` object with a JSON payload indicating the success message. The response content type is set to `application/json`. + * @returns An object containing the status and message. */ export const saveMetadataToSqlServer = async ( metadata: BundleExtendedMetadata, ecrId: string, -) => { +): Promise => { let pool = await get_pool(); if (!pool) { - return NextResponse.json( - { message: "Failed to connect to SQL Server." }, - { status: 500 }, - ); + return { message: "Failed to connect to SQL Server.", status: 500 }; } const transaction = new sql.Transaction(pool); @@ -390,34 +438,30 @@ export const saveMetadataToSqlServer = async ( await transaction.commit(); - return NextResponse.json( - { message: "Success. Saved metadata to database: " + ecrId }, - { status: 200 }, - ); + return { + message: "Success. Saved metadata to database.", + status: 200, + }; } catch (error: any) { - console.error("Error inserting metadata to database:", error); - + console.error({ + message: "Failed to insert metadata to sqlserver.", + error, + ecrId, + }); // Rollback the transaction if any error occurs await transaction.rollback(); - return NextResponse.json( - { message: "Failed to insert metadata to database. " + error.message }, - { status: 500 }, - ); - } finally { - // Close the connection pool - if (pool) { - pool.close(); - } + return { + message: "Failed to insert metadata to database.", + status: 500, + }; } } else { - return NextResponse.json( - { - message: - "Only the extended metadata schema is implemented for SQL Server.", - }, - { status: 501 }, - ); + return { + message: + "Only the extended metadata schema is implemented for SQL Server.", + status: 501, + }; } }; @@ -427,13 +471,12 @@ export const saveMetadataToSqlServer = async ( * @function saveMetadataToPostgres * @param metadata - The FHIR bundle metadata to be saved. * @param ecrId - The unique identifier for the Electronic Case Reporting (ECR) associated with the FHIR bundle. - * @returns A promise that resolves when the FHIR bundle metadata is successfully saved to postgres. - * @throws {Error} Throws an error if the FHIR bundle metadata cannot be saved to postgres. + * @returns An object containing the status and message. */ export const saveMetadataToPostgres = async ( metadata: BundleMetadata, ecrId: string, -) => { +): Promise => { const { database, pgPromise } = getDB(); const { ParameterizedQuery: PQ } = pgPromise; @@ -442,7 +485,7 @@ export const saveMetadataToPostgres = async ( await database.tx(async (t) => { // Insert main ECR metadata const saveToEcrData = new PQ({ - text: "INSERT INTO ecr_data (eICR_ID, patient_name_last, patient_name_first, patient_birth_date, data_source, report_date) VALUES ($1, $2, $3, $4, $5, $6) RETURNING eICR_ID", + text: "INSERT INTO ecr_data (eICR_ID, patient_name_last, patient_name_first, patient_birth_date, data_source, report_date) VALUES ($1, $2, $3, $4, $5, $6)", values: [ ecrId, metadata.last_name, @@ -453,7 +496,7 @@ export const saveMetadataToPostgres = async ( ], }); - const ecrData = await t.one(saveToEcrData); + await t.none(saveToEcrData); // Loop through each condition/rule object in rr array if (metadata.rr && metadata.rr.length > 0) { @@ -479,22 +522,24 @@ export const saveMetadataToPostgres = async ( } } } - - return ecrData; } }); // On successful transaction, return response - return NextResponse.json( - { message: "Success. Saved metadata to database: " + ecrId }, - { status: 200 }, - ); + return { + message: "Success. Saved metadata to database.", + status: 200, + }; } catch (error: any) { - console.error("Error inserting metadata to database:", error); - return NextResponse.json( - { message: "Failed to insert metadata to database. " + error.message }, - { status: 500 }, - ); + console.error({ + message: `Error inserting metadata to postgres.`, + error, + ecrId, + }); + return { + message: "Failed to insert metadata to database.", + status: 500, + }; } }; @@ -505,42 +550,30 @@ export const saveMetadataToPostgres = async ( * @param ecrId - The unique identifier for the Electronic Case Reporting (ECR) associated with the FHIR bundle. * @param saveSource - The location to save the FHIR bundle. Valid values are "postgres", "s3", or "azure". * @param metadata - The metadata to be saved with the FHIR bundle. - * @returns A `NextResponse` object with a JSON payload indicating the success message. The response content type is set to `application/json`. - * @throws {Error} Throws an error if the FHIR bundle or metadata cannot be saved. + * @returns An object containing the status and message. */ export const saveWithMetadata = async ( fhirBundle: Bundle, ecrId: string, saveSource: string, metadata: BundleMetadata | BundleExtendedMetadata, -) => { +): Promise => { let fhirDataResult; let metadataResult; const metadataSaveLocation = process.env.METADATA_DATABASE_TYPE; try { - fhirDataResult = await saveFhirData(fhirBundle, ecrId, saveSource); - - if (metadataSaveLocation == "postgres") { - metadataResult = await saveMetadataToPostgres( - metadata as BundleMetadata, - ecrId, - ); - } else if (metadataSaveLocation == "sqlserver") { - metadataResult = await saveMetadataToSqlServer( - metadata as BundleExtendedMetadata, - ecrId, - ); - } else { - metadataResult = NextResponse.json({ - message: "Unknown metadataSaveLocation: " + metadataSaveLocation, - }); - } + [fhirDataResult, metadataResult] = await Promise.all([ + saveFhirData(fhirBundle, ecrId, saveSource), + saveFhirMetadata(ecrId, metadataSaveLocation, metadata as BundleMetadata), + ]); } catch (error: any) { - return NextResponse.json( - { message: "Failed to save FHIR data with metadata. " + error.message }, - { status: 500 }, - ); + const message = "Failed to save FHIR data with metadata."; + console.error({ message, error, ecrId }); + return { + message, + status: 500, + }; } let responseMessage = ""; @@ -558,8 +591,5 @@ export const saveWithMetadata = async ( responseMessage += "Saved metadata."; } - return NextResponse.json( - { message: responseMessage }, - { status: responseStatus }, - ); + return { message: responseMessage, status: responseStatus }; }; diff --git a/containers/ecr-viewer/src/app/api/services/sqlserver_db.ts b/containers/ecr-viewer/src/app/api/services/sqlserver_db.ts index 916c54a469..75cdceaac2 100644 --- a/containers/ecr-viewer/src/app/api/services/sqlserver_db.ts +++ b/containers/ecr-viewer/src/app/api/services/sqlserver_db.ts @@ -5,13 +5,16 @@ import sql from "mssql"; * @returns A promise resolving to a connection pool. */ export const get_pool = async () => { - let pool = await sql.connect({ + return await sql.connect({ user: process.env.SQL_SERVER_USER, password: process.env.SQL_SERVER_PASSWORD, server: process.env.SQL_SERVER_HOST || "localhost", + pool: { + min: 1, + }, options: { trustServerCertificate: true, + connectTimeout: 30000, }, }); - return pool; }; diff --git a/containers/ecr-viewer/src/app/services/listEcrDataService.ts b/containers/ecr-viewer/src/app/services/listEcrDataService.ts index ddde222e7d..2694df2c0e 100644 --- a/containers/ecr-viewer/src/app/services/listEcrDataService.ts +++ b/containers/ecr-viewer/src/app/services/listEcrDataService.ts @@ -138,8 +138,6 @@ async function listEcrDataSqlserver( } catch (error: any) { console.error(error); return Promise.reject(error); - } finally { - pool.close(); } } @@ -265,8 +263,6 @@ const getTotalEcrCountSqlServer = async ( } catch (error: any) { console.error(error); return Promise.reject(error); - } finally { - pool.close(); } }; diff --git a/containers/ecr-viewer/src/app/tests/listEcrDataService.test.tsx b/containers/ecr-viewer/src/app/tests/listEcrDataService.test.tsx index adeff19d6d..3de6f84a4a 100644 --- a/containers/ecr-viewer/src/app/tests/listEcrDataService.test.tsx +++ b/containers/ecr-viewer/src/app/tests/listEcrDataService.test.tsx @@ -355,7 +355,6 @@ describe("listEcrDataService", () => { expect(get_pool).toHaveBeenCalled(); expect(mockPool.request).toHaveBeenCalled(); expect(mockQuery).toHaveBeenCalled(); - expect(mockPool.close).toHaveBeenCalled(); }); }); @@ -384,7 +383,6 @@ describe("listEcrDataService", () => { expect(get_pool).toHaveBeenCalled(); expect(mockPool.request).toHaveBeenCalled(); expect(mockQuery).toHaveBeenCalled(); - expect(mockPool.close).toHaveBeenCalled(); }); }); }); diff --git a/containers/ecr-viewer/src/app/tests/save-fhir-data.test.tsx b/containers/ecr-viewer/src/app/tests/save-fhir-data.test.tsx index 469d693a2b..f655ba66c3 100644 --- a/containers/ecr-viewer/src/app/tests/save-fhir-data.test.tsx +++ b/containers/ecr-viewer/src/app/tests/save-fhir-data.test.tsx @@ -87,12 +87,11 @@ describe("POST Save FHIR Data API Route", () => { const response = await POST(request); const responseJson = await response.json(); expect(response.status).toBe(200); - expect(responseJson.message).toBe( - "Success. Saved FHIR Bundle to S3: 12345", - ); + expect(responseJson.message).toBe("Success. Saved FHIR bundle."); }); it("throws an error when bucket is not found", async () => { + jest.spyOn(console, "error").mockImplementation(() => {}); const request = new NextRequest( "http://localhost:3000/api/save-fhir-data", { @@ -128,9 +127,7 @@ describe("POST Save FHIR Data API Route", () => { const response = await POST(request); const responseJson = await response.json(); expect(response.status).toBe(500); - expect(responseJson.message).toBe( - "Failed to insert data to S3. HTTP Status Code: 403", - ); + expect(responseJson.message).toBe("Failed to save FHIR bundle."); }); it("uses SOURCE environment variable if saveSource parameter is not provided", async () => { @@ -184,9 +181,7 @@ describe("POST Save FHIR Data API Route", () => { const response = await POST(request); const responseJson = await response.json(); expect(response.status).toBe(200); - expect(responseJson.message).toBe( - "Success. Saved FHIR Bundle to S3: 12345", - ); + expect(responseJson.message).toBe("Success. Saved FHIR bundle."); }); it("throws an error when saveSource is invalid", async () => { @@ -232,12 +227,19 @@ describe("POST Save FHIR Data API Route - Azure", () => { const responseJson = await response.json(); expect(response.status).toBe(200); - expect(responseJson.message).toBe( - "Success. Saved FHIR bundle to Azure Blob Storage: 12345", + expect(responseJson.message).toBe("Success. Saved FHIR bundle."); + expect(mockBlockBlobClient.upload).toHaveBeenCalledOnce(); + expect(mockBlockBlobClient.upload).toHaveBeenCalledWith( + JSON.stringify(fakeData("azure").fhirBundle), + 134, + { + blobHTTPHeaders: { blobContentType: "application/json" }, + }, ); }); it("throws an error when Azure upload fails", async () => { + jest.spyOn(console, "error").mockImplementation(() => {}); mockBlockBlobClient.upload.mockRejectedValue({ _response: { status: 400 }, }); @@ -254,8 +256,14 @@ describe("POST Save FHIR Data API Route - Azure", () => { const responseJson = await response.json(); expect(response.status).toBe(500); - expect(responseJson.message).toInclude( - "Failed to insert FHIR bundle to Azure Blob Storage.", + expect(responseJson.message).toInclude("Failed to save FHIR bundle."); + expect(mockBlockBlobClient.upload).toHaveBeenCalledOnce(); + expect(mockBlockBlobClient.upload).toHaveBeenCalledWith( + JSON.stringify(fakeData("azure").fhirBundle), + 134, + { + blobHTTPHeaders: { blobContentType: "application/json" }, + }, ); }); });