Skip to content

Commit

Permalink
Merge pull request #2292 from metriport/1892-shareback-improvements
Browse files Browse the repository at this point in the history
feat(fhir-to-cda): shareback bundle preprocessing
  • Loading branch information
RamilGaripov committed Jun 21, 2024
2 parents d0e9720 + f854a64 commit ff9448a
Show file tree
Hide file tree
Showing 12 changed files with 233 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ export async function createOrUpdateConsolidatedPatientData({

return transformedBundle;
} catch (error) {
const errorMsg = errorToString(error);
const msg = "Error converting and executing fhir bundle resources";
log(`${msg}: ${errorToString(error)}`);
log(`${msg}: ${errorMsg}`);
if (errorMsg.includes("ID")) throw new MetriportError(errorMsg, error, { cxId, patientId });
throw new MetriportError(msg, error, { cxId, patientId });
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { out } from "@metriport/core/util/log";
import { capture } from "@metriport/core/util/notifications";
import { makeFhirToCdaConverter } from "../../../external/fhir-to-cda-converter/converter-factory";
import { toFHIR as toFHIROrganization } from "../../../external/fhir/organization";
import { Bundle } from "../../../routes/medical/schemas/fhir";
import { getOrganizationOrFail } from "../organization/get-organization";

export async function convertFhirToCda({
cxId,
Expand All @@ -18,17 +16,13 @@ export async function convertFhirToCda({
}): Promise<void> {
const { log } = out(`convertFhirToCda - cxId: ${cxId}, patientId: ${patientId}`);
const cdaConverter = makeFhirToCdaConverter();
const organization = await getOrganizationOrFail({ cxId });

try {
const fhirOrganization = toFHIROrganization(organization);
await cdaConverter.requestConvert({
cxId,
patientId,
docId,
bundle: validatedBundle,
organization: fhirOrganization,
orgOid: organization.oid,
});
} catch (error) {
const msg = `Error converting FHIR to CDA`;
Expand Down
100 changes: 100 additions & 0 deletions packages/api/src/command/medical/patient/handle-data-contributions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Patient } from "@metriport/core/domain/patient";
import { toFHIR as toFhirPatient } from "@metriport/core/external/fhir/patient/index";
import { createUploadFilePath } from "@metriport/core/domain/document/upload";
import { uploadFhirBundleToS3 } from "@metriport/core/fhir-to-cda/upload";
import { uuidv7 } from "@metriport/core/util/uuid-v7";
import BadRequestError from "../../../errors/bad-request";
import { toFHIR as toFhirOrganization } from "../../../external/fhir/organization";
import { countResources } from "../../../external/fhir/patient/count-resources";
import { hydrateBundle } from "../../../external/fhir/shared/hydrate-bundle";
import { validateFhirEntries } from "../../../external/fhir/shared/json-validator";
import { Bundle as ValidBundle } from "../../../routes/medical/schemas/fhir";
import { Config } from "../../../shared/config";
import { getOrganizationOrFail } from "../organization/get-organization";
import { createOrUpdateConsolidatedPatientData } from "./consolidated-create";
import { convertFhirToCda } from "./convert-fhir-to-cda";
import { getPatientOrFail } from "./get-patient";

const MAX_RESOURCE_COUNT_PER_REQUEST = 50;
const MAX_RESOURCE_STORED_LIMIT = 1000;

export async function handleDataContribution({
patientId,
cxId,
bundle,
}: {
patientId: string;
cxId: string;
bundle: ValidBundle;
}) {
const [organization, patient] = await Promise.all([
getOrganizationOrFail({ cxId }),
getPatientOrFail({ id: patientId, cxId }),
]);

const fhirOrganization = toFhirOrganization(organization);
const fhirPatient = toFhirPatient(patient);
const docId = uuidv7();
const fhirBundleDestinationKey = createUploadFilePath(
cxId,
patientId,
`${docId}_FHIR_BUNDLE.json`
);
const fullBundle = hydrateBundle(bundle, fhirPatient, fhirOrganization, fhirBundleDestinationKey);
const validatedBundle = validateFhirEntries(fullBundle);
const incomingAmount = validatedBundle.entry.length;

await checkResourceLimit(incomingAmount, patient);
await uploadFhirBundleToS3({
cxId,
patientId,
fhirBundle: validatedBundle,
destinationKey: fhirBundleDestinationKey,
});
const patientDataPromise = async () => {
return createOrUpdateConsolidatedPatientData({
cxId,
patientId: patient.id,
fhirBundle: validatedBundle,
});
};
const convertAndUploadCdaPromise = async () => {
const isValidForCdaConversion = hasCompositionResource(validatedBundle);
if (isValidForCdaConversion) {
await convertFhirToCda({
cxId,
patientId,
docId,
validatedBundle,
});
}
};

return Promise.all([patientDataPromise(), convertAndUploadCdaPromise()]);
}

async function checkResourceLimit(incomingAmount: number, patient: Patient) {
if (!Config.isCloudEnv() || Config.isSandbox()) {
const { total: currentAmount } = await countResources({
patient: { id: patient.id, cxId: patient.cxId },
});
if (currentAmount + incomingAmount > MAX_RESOURCE_STORED_LIMIT) {
throw new BadRequestError(
`Reached maximum number of resources per patient in Sandbox mode.`,
null,
{ currentAmount, incomingAmount, MAX_RESOURCE_STORED_LIMIT }
);
}
// Limit the amount of resources that can be created at once
if (incomingAmount > MAX_RESOURCE_COUNT_PER_REQUEST) {
throw new BadRequestError(`Cannot create this many resources at a time.`, null, {
incomingAmount,
MAX_RESOURCE_COUNT_PER_REQUEST,
});
}
}
}

function hasCompositionResource(bundle: ValidBundle): boolean {
return bundle.entry.some(entry => entry.resource?.resourceType === "Composition");
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { convertFhirBundleToCda } from "@metriport/core/fhir-to-cda/fhir-to-cda";
import { FhirToCdaConverter, FhirToCdaConverterRequest } from "./connector";
import { uploadCdaDocuments } from "@metriport/core/fhir-to-cda/upload";
import { getOrganizationOrFail } from "../../command/medical/organization/get-organization";
import { toFHIR as toFhirOrganization } from "../fhir/organization";
import { FhirToCdaConverter, FhirToCdaConverterRequest } from "./connector";

export class FhirToCdaConverterDirect implements FhirToCdaConverter {
async requestConvert({
cxId,
patientId,
docId,
organization,
bundle,
orgOid,
}: FhirToCdaConverterRequest): Promise<void> {
const converted = convertFhirBundleToCda(bundle, orgOid);
const organization = await getOrganizationOrFail({ cxId });
const fhirOrganization = toFhirOrganization(organization);
const converted = convertFhirBundleToCda(bundle, organization.oid);

await uploadCdaDocuments({
cxId,
patientId,
cdaBundles: converted,
organization,
organization: fhirOrganization,
docId,
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Input } from "@metriport/core/domain/conversion/fhir-to-cda";
import { getLambdaResultPayload, makeLambdaClient } from "@metriport/core/external/aws/lambda";
import { getOrganizationOrFail } from "../../command/medical/organization/get-organization";
import { Config } from "../../shared/config";
import { toFHIR as toFhirOrganization } from "../fhir/organization";
import { FhirToCdaConverter, FhirToCdaConverterRequest } from "./connector";

const region = Config.getAWSRegion();
Expand All @@ -12,17 +15,26 @@ export class FhirToCdaConverterLambda implements FhirToCdaConverter {
patientId,
docId,
bundle,
organization,
}: FhirToCdaConverterRequest): Promise<void> {
if (!fhirToCdaConverterLambdaName) {
throw new Error("FHIR to CDA Converter Lambda Name is undefined");
}
const organization = await getOrganizationOrFail({ cxId });
const fhirOrganization = toFhirOrganization(organization);
const lambdaInput: Input = {
cxId,
patientId,
docId,
bundle,
organization: fhirOrganization,
orgOid: organization.oid,
};

const result = await lambdaClient
.invoke({
FunctionName: fhirToCdaConverterLambdaName,
InvocationType: "RequestResponse",
Payload: JSON.stringify({ cxId, patientId, docId, bundle, organization }),
Payload: JSON.stringify(lambdaInput),
})
.promise();

Expand Down
3 changes: 0 additions & 3 deletions packages/api/src/external/fhir-to-cda-converter/connector.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { Organization } from "@medplum/fhirtypes";
import { Bundle } from "../../routes/medical/schemas/fhir";

export type FhirToCdaConverterRequest = {
cxId: string;
patientId: string;
docId: string;
bundle: Bundle;
organization: Organization;
orgOid: string;
};

export interface FhirToCdaConverter {
Expand Down
55 changes: 55 additions & 0 deletions packages/api/src/external/fhir/shared/hydrate-bundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Extension, Organization, Patient } from "@medplum/fhirtypes";
import { metriportDataSourceExtension } from "@metriport/core/external/fhir/shared/extensions/metriport";
import { isValidUuid, uuidv7 } from "@metriport/core/util/uuid-v7";
import { Bundle as ValidBundle } from "../../../routes/medical/schemas/fhir";

/**
* Adds the Metriport and Document extensions to all the provided resources, ensures that all resources have UUIDs for IDs,
* and adds the Patient and Organization resources to the Bundle
*/
export function hydrateBundle(
bundle: ValidBundle,
patient: Patient,
org: Organization,
fhirBundleDestinationKey: string
): ValidBundle {
const docExtension: Extension = {
url: "https://public.metriport.com/fhir/StructureDefinition/doc-id-extension.json",
valueString: fhirBundleDestinationKey,
};

const bundleWithExtensions = validateUuidsAndAddExtensions(bundle, docExtension);
bundleWithExtensions.entry?.push({ resource: patient });
bundleWithExtensions.entry?.push({ resource: org });

return bundleWithExtensions;
}

type ReplacementIdPair = { old: string; new: string };

function validateUuidsAndAddExtensions(bundle: ValidBundle, docExtension: Extension): ValidBundle {
const replacements: ReplacementIdPair[] = [];
bundle.entry.forEach(entry => {
const oldId = entry.resource.id;
if (!oldId) {
entry.resource.id = uuidv7();
}
if (oldId && !isValidUuid(oldId)) {
replacements.push({
old: oldId,
new: uuidv7(),
});
}
if (entry.resource.extension) {
entry.resource.extension.push(metriportDataSourceExtension);
entry.resource.extension.push(docExtension);
} else {
entry.resource.extension = [metriportDataSourceExtension, docExtension];
}
});
let bundleString = JSON.stringify(bundle);
replacements.forEach((idPair: ReplacementIdPair) => {
bundleString = bundleString.replaceAll(idPair.old, idPair.new);
});
return JSON.parse(bundleString);
}
Loading

0 comments on commit ff9448a

Please sign in to comment.