Skip to content

Commit

Permalink
Merge pull request #2232 from metriport/develop
Browse files Browse the repository at this point in the history
RELEASE 2024_06_07 IHE V2
  • Loading branch information
jonahkaye committed Jun 7, 2024
2 parents 45c82f2 + ca04bf6 commit 1168a4a
Show file tree
Hide file tree
Showing 17 changed files with 297 additions and 62 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { buildIheResponseKey } from "../store";

it("should construct the correct file path and upload the file", async () => {
const cxId = "cxId";
const patientId = "patientId";
const requestId = "requestId";
const oid = "oid";
const timestamp = "2024-05-01T00:00:00";
const key = buildIheResponseKey({
type: "xcpd",
cxId,
patientId,
requestId,
oid,
timestamp,
});
expect(key).toEqual(`${cxId}/${patientId}/xcpd/${requestId}_2024-05-01/${oid}.xml`);
});
139 changes: 139 additions & 0 deletions packages/core/src/external/carequality/ihe-gateway-v2/monitor/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import dayjs from "dayjs";
import {
OutboundPatientDiscoveryReq,
OutboundDocumentQueryReq,
OutboundDocumentRetrievalReq,
XCPDGateway,
XCAGateway,
} from "@metriport/ihe-gateway-sdk";
import { S3Utils } from "../../../aws/s3";
import { Config } from "../../../../util/config";
import { out } from "../../../../util/log";

const { log } = out("Storing IHE Responses");

const bucket = Config.getIheResponsesBucketName();
let s3UtilsInstance = new S3Utils(Config.getAWSRegion());

function getS3UtilsInstance(): S3Utils {
return s3UtilsInstance;
}
export function setS3UtilsInstance(s3Utils: S3Utils): void {
s3UtilsInstance = s3Utils;
}

export async function storeXcpdResponses({
response,
outboundRequest,
gateway,
}: {
response: string;
outboundRequest: OutboundPatientDiscoveryReq;
gateway: XCPDGateway;
}) {
try {
if (!bucket) {
return;
}
const s3Utils = getS3UtilsInstance();
const { cxId, patientId, id: requestId, timestamp } = outboundRequest;
const key = buildIheResponseKey({
type: "xcpd",
cxId,
patientId,
requestId,
oid: gateway.oid,
timestamp,
});
await s3Utils.uploadFile({
bucket,
key,
file: Buffer.from(response),
contentType: "application/xml",
});
} catch (error) {
log(`Error storing XCPD response: ${error}`);
}
}

export async function storeDqResponses({
response,
outboundRequest,
gateway,
}: {
response: string;
outboundRequest: OutboundDocumentQueryReq;
gateway: XCAGateway;
}) {
try {
if (!bucket) {
return;
}
const s3Utils = getS3UtilsInstance();
const { cxId, patientId, id: requestId, timestamp } = outboundRequest;
const key = buildIheResponseKey({
type: "dq",
cxId,
patientId,
requestId,
oid: gateway.homeCommunityId,
timestamp,
});
await s3Utils.uploadFile({
bucket,
key,
file: Buffer.from(response),
contentType: "application/xml",
});
} catch (error) {
log(`Error storing DQ response: ${error}`);
}
}

export async function storeDrResponses({
response,
outboundRequest,
gateway,
}: {
response: Buffer;
outboundRequest: OutboundDocumentRetrievalReq;
gateway: XCAGateway;
}) {
try {
if (!bucket) {
return;
}
const s3Utils = getS3UtilsInstance();
const { cxId, patientId, id: requestId, timestamp } = outboundRequest;
const key = buildIheResponseKey({
type: "dr",
cxId,
patientId,
requestId,
oid: gateway.homeCommunityId,
timestamp,
});
await s3Utils.uploadFile({ bucket, key, file: response, contentType: "application/xml" });
} catch (error) {
log(`Error storing DR response: ${error}`);
}
}

export function buildIheResponseKey({
type,
cxId,
patientId,
requestId,
oid,
timestamp,
}: {
type: "xcpd" | "dq" | "dr";
cxId: string;
patientId: string;
requestId: string;
oid: string;
timestamp: string;
}) {
const date = dayjs(timestamp).format("YYYY-MM-DD");
return `${cxId}/${patientId}/${type}/${requestId}_${date}/${oid}.xml`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export const expectedDqDocumentReference = [
language: "en-US",
size: 163264,
title: "Continuity of Care Document",
creation: "2024-03-29T20:31:46.000Z",
creation: "2024-03-29T16:31:46.000Z",
authorInstitution: "Metriport^^^^^^^^^2.16.840.1.113883.3.9621",
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
} from "../../../../shared";
import { successStatus, partialSuccessStatus } from "./constants";
import { capture } from "../../../../../../util/notifications";
import { out } from "../../../../../../util/log";

const { log } = out("DQ Processing");

type Identifier = {
_identificationScheme: string;
Expand Down Expand Up @@ -54,6 +57,15 @@ function getHomeCommunityIdForDr(
return getResponseHomeCommunityId(extrinsicObject);
}

function getCreationTime(time: string | undefined): string | undefined {
try {
return time ? dayjs(time).toISOString() : undefined;
} catch (error) {
log(`Error parsing creation time: ${time}, error: ${error}`);
return undefined;
}
}

function parseDocumentReference(
//eslint-disable-next-line @typescript-eslint/no-explicit-any
extrinsicObject: any,
Expand Down Expand Up @@ -125,14 +137,14 @@ function parseDocumentReference(
capture.error(msg, {
extra: {
extrinsicObject,
repositoryUniqueId,
docUniqueId,
outboundRequest,
},
});
return undefined;
}

const creationTime = String(findSlotValue("creationTime"));

const documentReference: DocumentReference = {
homeCommunityId: getHomeCommunityIdForDr(outboundRequest, extrinsicObject),
repositoryUniqueId,
Expand All @@ -141,7 +153,7 @@ function parseDocumentReference(
language: findSlotValue("languageCode"),
size: sizeValue ? parseInt(sizeValue) : undefined,
title: findClassificationName(XDSDocumentEntryClassCode),
creation: creationTime ? dayjs(creationTime).toISOString() : undefined,
creation: getCreationTime(creationTime),
authorInstitution: findClassificationSlotValue(XDSDocumentEntryAuthor, "authorInstitution"),
};
return documentReference;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export type DocumentResponse = {
};

let s3UtilsInstance = new S3Utils(region);
export function getS3UtilsInstance(): S3Utils {
function getS3UtilsInstance(): S3Utils {
return s3UtilsInstance;
}
export function setS3UtilsInstance(s3Utils: S3Utils): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SamlCertsAndKeys } from "../../../saml/security/types";
import { getTrustedKeyStore, SamlClientResponse, sendSignedXml } from "../../../saml/saml-client";
import { BulkSignedDQ } from "../create/iti38-envelope";
import { out } from "../../../../../../util/log";
import { storeDqResponses } from "../../../monitor/store";

const { log } = out("Sending DQ Requests");
const context = "ihe-gateway-v2-dq-saml-client";
Expand Down Expand Up @@ -39,6 +40,11 @@ export async function sendSignedDQRequests({
request.gateway.homeCommunityId
}`
);
await storeDqResponses({
response,
outboundRequest: request.outboundRequest,
gateway: request.gateway,
});
return {
gateway: request.gateway,
response,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getTrustedKeyStore, sendSignedXmlMtom } from "../../../saml/saml-client
import { MtomAttachments } from "../mtom/parser";
import { BulkSignedDR } from "../create/iti39-envelope";
import { out } from "../../../../../../util/log";
import { storeDrResponses } from "../../../monitor/store";

const { log } = out("Sending DR Requests");
const context = "ihe-gateway-v2-dr-saml-client";
Expand All @@ -31,7 +32,7 @@ export async function sendSignedDRRequests({
const trustedKeyStore = await getTrustedKeyStore();
const requestPromises = signedRequests.map(async (request, index) => {
try {
const mtomParts = await sendSignedXmlMtom({
const { mtomParts, rawResponse } = await sendSignedXmlMtom({
signedXml: request.signedRequest,
url: request.gateway.url,
samlCertsAndKeys,
Expand All @@ -42,6 +43,11 @@ export async function sendSignedDRRequests({
request.gateway.homeCommunityId
}`
);
await storeDrResponses({
response: rawResponse,
outboundRequest: request.outboundRequest,
gateway: request.gateway,
});
return {
gateway: request.gateway,
mtomResponse: mtomParts,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SamlCertsAndKeys } from "../../../saml/security/types";
import { getTrustedKeyStore, SamlClientResponse, sendSignedXml } from "../../../saml/saml-client";
import { BulkSignedXCPD } from "../create/iti55-envelope";
import { out } from "../../../../../../util/log";
import { storeXcpdResponses } from "../../../monitor/store";

const { log } = out("Sending XCPD Requests");

Expand Down Expand Up @@ -37,6 +38,11 @@ export async function sendSignedXCPDRequests({
request.gateway.oid
}`
);
await storeXcpdResponses({
response,
outboundRequest: request.outboundRequest,
gateway: request.gateway,
});
return {
gateway: request.gateway,
response,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export async function sendSignedXmlMtom({
url: string;
samlCertsAndKeys: SamlCertsAndKeys;
trustedKeyStore: string;
}): Promise<MtomAttachments> {
}): Promise<{ mtomParts: MtomAttachments; rawResponse: Buffer }> {
const agent = new https.Agent({
rejectUnauthorized: getRejectUnauthorized(),
requestCert: true,
Expand Down Expand Up @@ -129,7 +129,7 @@ export async function sendSignedXmlMtom({

const boundary = getBoundaryFromMtomResponse(response.headers["content-type"]);
if (boundary) {
return await parseMtomResponse(binaryData, boundary);
return { mtomParts: await parseMtomResponse(binaryData, boundary), rawResponse: binaryData };
}
return convertSoapResponseToMtomResponse(binaryData);
return { mtomParts: convertSoapResponseToMtomResponse(binaryData), rawResponse: binaryData };
}
4 changes: 4 additions & 0 deletions packages/core/src/util/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,8 @@ export class Config {
static getPostHogApiKey(): string | undefined {
return getEnvVar("POST_HOG_API_KEY_SECRET");
}

static getIheResponsesBucketName(): string | undefined {
return getEnvVar("IHE_RESPONSES_BUCKET_NAME");
}
}
1 change: 1 addition & 0 deletions packages/infra/config/env-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ type EnvConfigBase = {
generalBucketName: string;
medicalDocumentsBucketName: string;
medicalDocumentsUploadBucketName: string;
iheResponsesBucketName: string;
fhirConverterBucketName?: string;
analyticsSecretNames?: {
POST_HOG_API_KEY_SECRET: string;
Expand Down
1 change: 1 addition & 0 deletions packages/infra/config/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const config: EnvConfigNonSandbox = {
generalBucketName: "test-bucket",
medicalDocumentsBucketName: "test-bucket",
medicalDocumentsUploadBucketName: "test-upload-bucket",
iheResponsesBucketName: "test-ihe-responses-bucket",
engineeringCxId: "12345678-1234-1234-1234-123456789012",
};
export default config;
1 change: 1 addition & 0 deletions packages/infra/lib/api-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ export class APIStack extends Stack {
apiURL: apiService.loadBalancer.loadBalancerDnsName,
envType: props.config.environmentType,
sentryDsn: props.config.lambdasSentryDSN,
iheResponsesBucketName: props.config.iheResponsesBucketName,
});
}

Expand Down
Loading

0 comments on commit 1168a4a

Please sign in to comment.