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: move dispatching logic into fhir-data-service, dry out code #3130

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
110 changes: 74 additions & 36 deletions containers/ecr-viewer/src/app/api/fhir-data/fhir-data-service.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,99 @@
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<FhirDataResponse> => {
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({
text: "SELECT * FROM fhir WHERE ecr_id = $1",
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<FhirDataResponse> => {
const bucketName = process.env.ECR_BUCKET_NAME;
const objectKey = `${ecr_id}.json`; // This could also come from the request, e.g., req.query.key

Expand All @@ -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<FhirDataResponse> => {
// 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");

Expand All @@ -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 };
}
}
};
17 changes: 5 additions & 12 deletions containers/ecr-viewer/src/app/api/fhir-data/route.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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);
}
69 changes: 6 additions & 63 deletions containers/ecr-viewer/src/app/tests/api/fhir-data/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [] };
Expand All @@ -24,68 +18,17 @@ 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 }),
);

const response = await GET(
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",
});
});
});
Loading
Loading