-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2292 from metriport/1892-shareback-improvements
feat(fhir-to-cda): shareback bundle preprocessing
- Loading branch information
Showing
12 changed files
with
233 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
packages/api/src/command/medical/patient/handle-data-contributions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} |
13 changes: 8 additions & 5 deletions
13
packages/api/src/external/fhir-to-cda-converter/connector-direct.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.