diff --git a/containers/ecr-viewer/src/app/api/fhir-data/fhir-data-service.ts b/containers/ecr-viewer/src/app/api/fhir-data/fhir-data-service.ts index 7ebe0e4fb..30f826534 100644 --- a/containers/ecr-viewer/src/app/api/fhir-data/fhir-data-service.ts +++ b/containers/ecr-viewer/src/app/api/fhir-data/fhir-data-service.ts @@ -1,26 +1,72 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { GetObjectCommand } from "@aws-sdk/client-s3"; import { BlobClient, BlobDownloadResponseParsed, BlobServiceClient, } from "@azure/storage-blob"; -import { loadYamlConfig, streamToJson } from "../utils"; +import { + AZURE_SOURCE, + POSTGRES_SOURCE, + S3_SOURCE, + loadYamlConfig, + streamToJson, +} from "../utils"; import { getDB } from "../services/postgres_db"; import { s3Client } from "../services/s3Client"; const UNKNOWN_ECR_ID = "eCR ID not found"; +type FhirDataResponse = { + payload: { fhirBundle: any } | { message: string }; + status: number; +}; + +/** + * Get the fhir data for a given ECR ID + * @param ecr_id The id of the ecr to fetch + * @returns NextResponse with the ecr or error data + */ +export async function get_fhir_data(ecr_id: string | null) { + let res: FhirDataResponse; + if (process.env.SOURCE === S3_SOURCE) { + res = await get_s3(ecr_id); + } else if (process.env.SOURCE === AZURE_SOURCE) { + res = await get_azure(ecr_id); + } else if (process.env.SOURCE === POSTGRES_SOURCE) { + res = await get_postgres(ecr_id); + } else { + res = { payload: { message: "Invalid source" }, status: 500 }; + } + const { status, payload } = res; + if (status !== 200) { + return NextResponse.json(payload, { status }); + } + + const mappings = loadYamlConfig(); + if (!mappings) { + console.error("Unable to load FHIR mappings"); + return NextResponse.json( + { message: "Internal system error" }, + { status: 500 }, + ); + } + + return NextResponse.json( + { ...payload, fhirPathMappings: mappings }, + { status }, + ); +} + /** * Retrieves FHIR data from PostgreSQL database based on eCR ID. - * @param request - The NextRequest object containing the request information. - * @returns A promise resolving to a NextResponse object. + * @param ecr_id - The id of the ecr to fetch. + * @returns A promise resolving to the data and status. */ -export const get_postgres = async (request: NextRequest) => { +export const get_postgres = async ( + ecr_id: string | null, +): Promise => { const { database, pgPromise } = getDB(); - const params = request.nextUrl.searchParams; - const ecr_id = params.get("id") || null; - const mappings = loadYamlConfig(); const { ParameterizedQuery: PQ } = pgPromise; const findFhir = new PQ({ @@ -28,30 +74,26 @@ export const get_postgres = async (request: NextRequest) => { values: [ecr_id], }); try { - if (!mappings) throw Error("no mappings!"); const entry = await database.one(findFhir); - return NextResponse.json( - { fhirBundle: entry.data, fhirPathMappings: mappings }, - { status: 200 }, - ); + return { payload: { fhirBundle: entry.data }, status: 200 }; } catch (error: any) { console.error("Error fetching data:", error); if (error.message == "No data returned from the query.") { - return NextResponse.json({ message: UNKNOWN_ECR_ID }, { status: 404 }); + return { payload: { message: UNKNOWN_ECR_ID }, status: 404 }; } else { - return NextResponse.json({ message: error.message }, { status: 500 }); + return { payload: { message: error.message }, status: 500 }; } } }; /** * Retrieves FHIR data from S3 based on eCR ID. - * @param request - The NextRequest object containing the request information. + * @param ecr_id - The id of the ecr to fetch. * @returns A promise resolving to a NextResponse object. */ -export const get_s3 = async (request: NextRequest) => { - const params = request.nextUrl.searchParams; - const ecr_id = params.get("id"); +export const get_s3 = async ( + ecr_id: string | null, +): Promise => { const bucketName = process.env.ECR_BUCKET_NAME; const objectKey = `${ecr_id}.json`; // This could also come from the request, e.g., req.query.key @@ -64,34 +106,30 @@ export const get_s3 = async (request: NextRequest) => { const { Body } = await s3Client.send(command); const content = await streamToJson(Body); - return NextResponse.json( - { fhirBundle: content, fhirPathMappings: loadYamlConfig() }, - { status: 200 }, - ); + return { payload: { fhirBundle: content }, status: 200 }; } catch (error: any) { console.error("S3 GetObject error:", error); if (error?.Code === "NoSuchKey") { - return NextResponse.json({ message: UNKNOWN_ECR_ID }, { status: 404 }); + return { payload: { message: UNKNOWN_ECR_ID }, status: 404 }; } else { - return NextResponse.json({ message: error.message }, { status: 500 }); + return { payload: { message: error.message }, status: 500 }; } } }; /** * Retrieves FHIR data from Azure Blob Storage based on eCR ID. - * @param request - The NextRequest object containing the request information. + * @param ecr_id - The id of the ecr to fetch. * @returns A promise resolving to a NextResponse object. */ -export const get_azure = async (request: NextRequest) => { +export const get_azure = async ( + ecr_id: string | null, +): Promise => { // TODO: Make this global after we get Azure access const blobClient = BlobServiceClient.fromConnectionString( process.env.AZURE_STORAGE_CONNECTION_STRING!, ); - const params = request.nextUrl.searchParams; - const ecr_id = params.get("id"); - if (!process.env.AZURE_CONTAINER_NAME) throw Error("Azure container name not found"); @@ -106,19 +144,19 @@ export const get_azure = async (request: NextRequest) => { await blockBlobClient.download(); const content = await streamToJson(downloadResponse.readableStreamBody); - return NextResponse.json( - { fhirBundle: content, fhirPathMappings: loadYamlConfig() }, - { status: 200 }, - ); + return { + payload: { fhirBundle: content }, + status: 200, + }; } catch (error: any) { console.error( "Failed to download the FHIR data from Azure Blob Storage:", error, ); if (error?.statusCode === 404) { - return NextResponse.json({ message: UNKNOWN_ECR_ID }, { status: 404 }); + return { payload: { message: UNKNOWN_ECR_ID }, status: 404 }; } else { - return NextResponse.json({ message: error.message }, { status: 500 }); + return { payload: { message: error.message }, status: 500 }; } } }; diff --git a/containers/ecr-viewer/src/app/api/fhir-data/route.ts b/containers/ecr-viewer/src/app/api/fhir-data/route.ts index 0b07b3c34..1bcf8e573 100644 --- a/containers/ecr-viewer/src/app/api/fhir-data/route.ts +++ b/containers/ecr-viewer/src/app/api/fhir-data/route.ts @@ -1,6 +1,5 @@ -import { NextRequest, NextResponse } from "next/server"; -import { get_s3, get_azure, get_postgres } from "./fhir-data-service"; -import { S3_SOURCE, AZURE_SOURCE, POSTGRES_SOURCE } from "@/app/api/utils"; +import { NextRequest } from "next/server"; +import { get_fhir_data } from "./fhir-data-service"; /** * Handles GET requests by fetching data from different sources based on the environment configuration. @@ -13,13 +12,7 @@ import { S3_SOURCE, AZURE_SOURCE, POSTGRES_SOURCE } from "@/app/api/utils"; * may vary based on the source and is thus marked as `unknown`. */ export async function GET(request: NextRequest) { - if (process.env.SOURCE === S3_SOURCE) { - return get_s3(request); - } else if (process.env.SOURCE === AZURE_SOURCE) { - return await get_azure(request); - } else if (process.env.SOURCE === POSTGRES_SOURCE) { - return await get_postgres(request); - } else { - return NextResponse.json({ message: "Invalid source" }, { status: 500 }); - } + const params = request.nextUrl.searchParams; + const ecr_id = params.get("id") || null; + return get_fhir_data(ecr_id); } diff --git a/containers/ecr-viewer/src/app/tests/api/fhir-data/route.test.ts b/containers/ecr-viewer/src/app/tests/api/fhir-data/route.test.ts index c4d23f041..3045e79bb 100644 --- a/containers/ecr-viewer/src/app/tests/api/fhir-data/route.test.ts +++ b/containers/ecr-viewer/src/app/tests/api/fhir-data/route.test.ts @@ -2,18 +2,12 @@ * @jest-environment node */ import { GET } from "@/app/api/fhir-data/route"; -import { - get_postgres, - get_s3, - get_azure, -} from "@/app/api/fhir-data/fhir-data-service"; -import { S3_SOURCE, AZURE_SOURCE, POSTGRES_SOURCE } from "@/app/api/utils"; +import { get_fhir_data } from "@/app/api/fhir-data/fhir-data-service"; +import { POSTGRES_SOURCE } from "@/app/api/utils"; import { NextRequest, NextResponse } from "next/server"; jest.mock("../../../api/fhir-data/fhir-data-service", () => ({ - get_postgres: jest.fn(), - get_s3: jest.fn(), - get_azure: jest.fn(), + get_fhir_data: jest.fn(), })); const emptyResponse = { fhirBundle: [], fhirPathMappings: [] }; @@ -24,43 +18,8 @@ describe("GET fhir-data", () => { jest.resetAllMocks(); }); - it("should return a 200 response with postgres bundle when source is postgres", async () => { - process.env.SOURCE = POSTGRES_SOURCE; - (get_postgres as jest.Mock).mockResolvedValue( - NextResponse.json(emptyResponse, { status: 200 }), - ); - - const response = await GET( - new NextRequest(new URL("https://example.com/api/fhir-data?id=123")), - ); - - expect(get_postgres).toHaveBeenCalledOnce(); - expect(get_s3).not.toHaveBeenCalled(); - expect(get_azure).not.toHaveBeenCalled(); - expect(response.status).toEqual(200); - expect(await response.json()).toEqual(emptyResponse); - }); - - it("should return a 200 response with s3 bundle when source is s3", async () => { - process.env.SOURCE = S3_SOURCE; - (get_s3 as jest.Mock).mockResolvedValue( - NextResponse.json(emptyResponse, { status: 200 }), - ); - - const response = await GET( - new NextRequest(new URL("https://example.com/api/fhir-data?id=123")), - ); - - expect(get_s3).toHaveBeenCalledOnce(); - expect(get_postgres).not.toHaveBeenCalled(); - expect(get_azure).not.toHaveBeenCalled(); - expect(response.status).toEqual(200); - expect(await response.json()).toEqual(emptyResponse); - }); - - it("should return a 200 response with azure bundle when source is azure", async () => { - process.env.SOURCE = AZURE_SOURCE; - (get_azure as jest.Mock).mockResolvedValue( + it("should defer to get_fhir_data service function", async () => { + (get_fhir_data as jest.Mock).mockResolvedValue( NextResponse.json(emptyResponse, { status: 200 }), ); @@ -68,24 +27,8 @@ describe("GET fhir-data", () => { new NextRequest(new URL("https://example.com/api/fhir-data?id=123")), ); - expect(get_azure).toHaveBeenCalledOnce(); - expect(get_s3).not.toHaveBeenCalled(); - expect(get_postgres).not.toHaveBeenCalled(); + expect(get_fhir_data).toHaveBeenCalledOnce(); expect(response.status).toEqual(200); expect(await response.json()).toEqual(emptyResponse); }); - - it("should return a 500 response when METADATA_DATABASE_TYPE is invalid", async () => { - (process.env.SOURCE as any) = "p0$+gre$"; - - const response = await GET( - new NextRequest(new URL("https://example.com/api/fhir-data?id=123")), - ); - - jest.spyOn(console, "error").mockImplementation(); - expect(response.status).toEqual(500); - expect(await response.json()).toEqual({ - message: "Invalid source", - }); - }); }); diff --git a/containers/ecr-viewer/src/app/tests/api/fhir-data/service.test.ts b/containers/ecr-viewer/src/app/tests/api/fhir-data/service.test.ts index 6355a2d52..911c53788 100644 --- a/containers/ecr-viewer/src/app/tests/api/fhir-data/service.test.ts +++ b/containers/ecr-viewer/src/app/tests/api/fhir-data/service.test.ts @@ -3,15 +3,15 @@ */ import { get_azure, + get_fhir_data, get_postgres, get_s3, } from "@/app/api/fhir-data/fhir-data-service"; import { getDB } from "@/app/api/services/postgres_db"; -// import { GetObjectCommand } from "@aws-sdk/client-s3"; import { s3Client } from "@/app/api/services/s3Client"; +import { AZURE_SOURCE, POSTGRES_SOURCE, S3_SOURCE } from "@/app/api/utils"; import { BlobServiceClient } from "@azure/storage-blob"; -import { NextRequest } from "next/server"; jest.mock("../../../api/services/postgres_db", () => ({ getDB: jest.fn(), @@ -26,10 +26,39 @@ jest.mock("@azure/storage-blob", () => ({ })); const DEFAULT_MAPPINGS = { key: "value" }; -jest.mock("../../../api/utils", () => ({ - loadYamlConfig: () => DEFAULT_MAPPINGS, - streamToJson: (body: string) => body, -})); +jest.mock("../../../api/utils", () => { + const originalModule = jest.requireActual("../../../api/utils"); + return { + ...originalModule, + loadYamlConfig: () => DEFAULT_MAPPINGS, + streamToJson: (body: string) => body, + }; +}); + +const defaultFhirBundle = "hi"; +const simpleResponse = { + fhirBundle: defaultFhirBundle, + fhirPathMappings: DEFAULT_MAPPINGS, +}; + +describe("get_fhir_data", () => { + afterEach(() => { + process.env.SOURCE = POSTGRES_SOURCE; + jest.resetAllMocks(); + }); + + it("should return a 500 response when METADATA_DATABASE_TYPE is invalid", async () => { + (process.env.SOURCE as any) = "p0$+gre$"; + + const response = await get_fhir_data("123"); + + jest.spyOn(console, "error").mockImplementation(); + expect(response.status).toEqual(500); + expect(await response.json()).toEqual({ + message: "Invalid source", + }); + }); +}); describe("get_postgres", () => { const mockDatabase = { @@ -52,26 +81,38 @@ describe("get_postgres", () => { }); afterEach(() => { + process.env.SOURCE = POSTGRES_SOURCE; jest.resetAllMocks(); }); it("should return ecr when database query succeeds", async () => { const mockECR = { - data: "hi", + data: defaultFhirBundle, }; mockDatabase.one.mockReturnValue(mockECR); - const response = await get_postgres( - new NextRequest(new URL("https://example.com/api/fhir-data?id=123")), - ); + const response = await get_postgres("123"); expect(response.status).toEqual(200); - expect(await response.json()).toEqual({ - fhirBundle: "hi", - fhirPathMappings: DEFAULT_MAPPINGS, + expect(response.payload).toEqual({ + fhirBundle: defaultFhirBundle, }); expect(mockDatabase.one).toHaveBeenCalledTimes(1); }); + it("should be called by get_fhir_data when source is postgres", async () => { + process.env.SOURCE = POSTGRES_SOURCE; + const mockECR = { + data: defaultFhirBundle, + }; + mockDatabase.one.mockReturnValue(mockECR); + + const response = await get_fhir_data("123"); + + expect(response.status).toEqual(200); + expect(await response.json()).toEqual(simpleResponse); + expect(mockDatabase.one).toHaveBeenCalledTimes(1); + }); + it("should return an 404 error response when id unknown", async () => { jest.spyOn(console, "error").mockImplementation(() => {}); @@ -79,11 +120,9 @@ describe("get_postgres", () => { mockDatabase.one.mockImplementation(async () => { throw new Error(errorMessage); }); - const response = await get_postgres( - new NextRequest(new URL("https://example.com/api/fhir-data?id=123")), - ); + const response = await get_postgres("123"); expect(response.status).toEqual(404); - expect(await response.json()).toEqual({ message: "eCR ID not found" }); + expect(response.payload).toEqual({ message: "eCR ID not found" }); expect(mockDatabase.one).toHaveBeenCalledTimes(1); }); @@ -94,45 +133,49 @@ describe("get_postgres", () => { mockDatabase.one.mockImplementation(async () => { throw new Error(errorMessage); }); - const response = await get_postgres( - new NextRequest(new URL("https://example.com/api/fhir-data?id=123")), - ); + const response = await get_postgres("123"); expect(response.status).toEqual(500); - expect(await response.json()).toEqual({ message: "Oh no!" }); + expect(response.payload).toEqual({ message: "Oh no!" }); expect(mockDatabase.one).toHaveBeenCalledTimes(1); }); }); describe("get_s3", () => { afterEach(() => { + process.env.SOURCE = POSTGRES_SOURCE; jest.resetAllMocks(); }); it("should return ecr when database query succeeds", async () => { - s3Client.send = jest.fn().mockReturnValue({ Body: "hi" }); - const response = await get_s3( - new NextRequest(new URL("https://example.com/api/fhir-data?id=123")), - ); + s3Client.send = jest.fn().mockReturnValue({ Body: defaultFhirBundle }); + const response = await get_s3("123"); expect(response.status).toEqual(200); - expect(await response.json()).toEqual({ - fhirBundle: "hi", - fhirPathMappings: DEFAULT_MAPPINGS, + expect(response.payload).toEqual({ + fhirBundle: defaultFhirBundle, }); expect(s3Client.send).toHaveBeenCalledTimes(1); }); + it("should be called by get_fhir_data when source is S3", async () => { + process.env.SOURCE = S3_SOURCE; + s3Client.send = jest.fn().mockReturnValue({ Body: defaultFhirBundle }); + const response = await get_fhir_data("123"); + + expect(response.status).toEqual(200); + expect(await response.json()).toEqual(simpleResponse); + expect(s3Client.send).toHaveBeenCalledTimes(1); + }); + it("should return an 404 error response when id unknown", async () => { jest.spyOn(console, "error").mockImplementation(() => {}); s3Client.send = jest.fn().mockImplementation(async () => { throw { Code: "NoSuchKey", message: "No such Key" }; }); - const response = await get_s3( - new NextRequest(new URL("https://example.com/api/fhir-data?id=123")), - ); + const response = await get_s3("123"); expect(response.status).toEqual(404); - expect(await response.json()).toEqual({ message: "eCR ID not found" }); + expect(response.payload).toEqual({ message: "eCR ID not found" }); expect(s3Client.send).toHaveBeenCalledTimes(1); }); @@ -142,11 +185,9 @@ describe("get_s3", () => { s3Client.send = jest.fn().mockImplementation(async () => { throw { Code: "Something else", message: "Oh no!" }; }); - const response = await get_s3( - new NextRequest(new URL("https://example.com/api/fhir-data?id=123")), - ); + const response = await get_s3("123"); expect(response.status).toEqual(500); - expect(await response.json()).toEqual({ message: "Oh no!" }); + expect(response.payload).toEqual({ message: "Oh no!" }); expect(s3Client.send).toHaveBeenCalledTimes(1); }); }); @@ -171,36 +212,44 @@ describe("get_azure", () => { }); afterEach(() => { + process.env.SOURCE = POSTGRES_SOURCE; jest.resetAllMocks(); }); it("should return ecr when database query succeeds", async () => { blockBlobClient.download = jest .fn() - .mockReturnValue({ readableStreamBody: "hi" }); - const response = await get_azure( - new NextRequest(new URL("https://example.com/api/fhir-data?id=123")), - ); + .mockReturnValue({ readableStreamBody: defaultFhirBundle }); + const response = await get_azure("123"); expect(response.status).toEqual(200); - expect(await response.json()).toEqual({ - fhirBundle: "hi", - fhirPathMappings: DEFAULT_MAPPINGS, + expect(response.payload).toEqual({ + fhirBundle: defaultFhirBundle, }); expect(blockBlobClient.download).toHaveBeenCalledTimes(1); }); + it("should be called by get_fhir_data when source is azure", async () => { + process.env.SOURCE = AZURE_SOURCE; + blockBlobClient.download = jest + .fn() + .mockReturnValue({ readableStreamBody: defaultFhirBundle }); + const response = await get_fhir_data("123"); + + expect(response.status).toEqual(200); + expect(await response.json()).toEqual(simpleResponse); + expect(blockBlobClient.download).toHaveBeenCalledTimes(1); + }); + it("should return an 404 error response when id unknown", async () => { jest.spyOn(console, "error").mockImplementation(() => {}); blockBlobClient.download = jest.fn().mockImplementation(async () => { throw { statusCode: 404, code: "ResourceNotFound" }; }); - const response = await get_azure( - new NextRequest(new URL("https://example.com/api/fhir-data?id=123")), - ); + const response = await get_azure("123"); expect(response.status).toEqual(404); - expect(await response.json()).toEqual({ message: "eCR ID not found" }); + expect(response.payload).toEqual({ message: "eCR ID not found" }); expect(blockBlobClient.download).toHaveBeenCalledTimes(1); }); @@ -210,11 +259,9 @@ describe("get_azure", () => { blockBlobClient.download = jest.fn().mockImplementation(async () => { throw { statusCode: 409, message: "Oh no!" }; }); - const response = await get_azure( - new NextRequest(new URL("https://example.com/api/fhir-data?id=123")), - ); + const response = await get_azure("123"); expect(response.status).toEqual(500); - expect(await response.json()).toEqual({ message: "Oh no!" }); + expect(response.payload).toEqual({ message: "Oh no!" }); expect(blockBlobClient.download).toHaveBeenCalledTimes(1); }); });