Skip to content

Commit

Permalink
Updated loadBundles script for connectathon and add QICore ModelInfo (#…
Browse files Browse the repository at this point in the history
…112)

* brought back loadBundles script. added QICore-ModelInfo to dbsetup

* a bit of post cthon clean up to loadbundles

* Moved load ecqm-content script to dbSetup.ts

* fix missing / in url replacement

---------

Co-authored-by: Elsa <eperelli@mitre.org>
  • Loading branch information
hossenlopp and elsaperelli authored Oct 4, 2024
1 parent a1535a6 commit eb19b42
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 2 deletions.
2 changes: 1 addition & 1 deletion docker-build.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
npm#!/bin/bash

docker buildx build --platform linux/arm64,linux/amd64 -t tacoma/measure-repository-service:latest -f service.Dockerfile . --push
docker buildx build --platform linux/arm64,linux/amd64 -t tacoma/measure-repository-app:latest -f app.Dockerfile . --push
1 change: 1 addition & 0 deletions service/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules
.DS_Store
cache
ecqm-content-r4-2021
ecqm-content-qicore-2024
6 changes: 6 additions & 0 deletions service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ To load multiple bundles from a directory, run the same script with the desired
npm run db:loadBundle <path to directory>
```

To load [ecqm-content-qicore-2024](https://github.com/cqframework/ecqm-content-qicore-2024) into your database, clone the repository in the service directory of this repository. Then you can run the following script:

```
npm run db:loadEcqmContent <optional service url, default: http://localhost:3000/4_0_1>
```

### Bundle Upload Details

Upon uploading a Measure resource, the Measure's main library is added to the `relatedArtifact` array with an [isOwned extension](https://build.fhir.org/ig/HL7/fhir-extensions/StructureDefinition-artifact-isOwned.html).
Expand Down
1 change: 1 addition & 0 deletions service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"db:setup": "ts-node ./scripts/dbSetup.ts create",
"db:loadBundle": "ts-node ./scripts/dbSetup.ts loadBundle",
"db:postBundle": "ts-node ./scripts/dbSetup.ts postBundle",
"db:loadEcqmContent": "ts-node ./scripts/dbSetup.ts loadEcqmContent",
"lint": "eslint \"./src/**/*.{js,ts}\"",
"lint:fix": "eslint \"./src/**/*.{js,ts}\" --fix",
"prettier": "prettier --check \"./src/**/*.{js,ts}\"",
Expand Down
134 changes: 133 additions & 1 deletion service/scripts/dbSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,123 @@ dotenv.config();

const DB_URL = process.env.DATABASE_URL || 'mongodb://localhost:27017/measure-repository';
const COLLECTION_NAMES = ['Measure', 'Library'];
const ECQM_CONTENT_PATH = 'ecqm-content-qicore-2024/bundles/measure';
const MEASURES_PATH = path.join(ECQM_CONTENT_PATH);

/**
* Gets the paths for the bundles in ecqm-content-qicore-2024
*/
function getBundlePaths(): string[] {
const filePaths: string[] = [];

fs.readdirSync(MEASURES_PATH, { withFileTypes: true }).forEach(ent => {
// if this item is a directory, look for .json files under it
if (ent.isDirectory()) {
fs.readdirSync(path.join(MEASURES_PATH, ent.name), { withFileTypes: true }).forEach(subEnt => {
if (!subEnt.isDirectory() && subEnt.name.endsWith('.json')) {
filePaths.push(path.join(MEASURES_PATH, ent.name, subEnt.name));
}
});
}
});
return filePaths;
}

/**
* Changes the incorrect ecqi.healthit.gov references on the Libraries in
* ecqm-content-qicore-2024 to the correct madie.cms.gov references
*/
async function fixAndPutLibraries(bundle: fhir4.Bundle, url: string) {
const libraries: fhir4.Library[] = bundle.entry
?.filter(entry => entry.resource?.resourceType === 'Library')
.map(entry => entry.resource as fhir4.Library) as fhir4.Library[];

for (const library of libraries) {
console.log(` Library ${library.id}`);
if (library.relatedArtifact) {
for (let index = 0; index < library.relatedArtifact.length; index++) {
const ra = library.relatedArtifact[index];
// if a related artifact is using a ecqi.healthit.gov reference it needs to be
// changed to a madie.cms.gov reference to match the urls the resources use
if (
(ra.type === 'depends-on' || ra.type === 'composed-of') &&
ra.resource?.startsWith('http://ecqi.healthit.gov/ecqms/Library')
) {
const newRef = ra.resource.replace(
'http://ecqi.healthit.gov/ecqms/Library/',
'https://madie.cms.gov/Library/'
);
console.log(` replacing ra ${ra.resource} with ${newRef}`);
ra.resource = newRef;
}
}
}
try {
console.log(` PUT ${url}/Library/${library.id}`);

const resp = await fetch(`${url}/Library/${library.id}`, {
method: 'PUT',
body: JSON.stringify(library),
headers: {
'Content-Type': 'application/json+fhir'
}
});
console.log(` ${resp.status}`);
} catch (e) {
console.error(e);
}
}
}

/**
* Changes the Measures in ecqm-content-qicore-2024 to have status 'active'
* before being added to the database with a PUT
*/
async function putMeasure(bundle: fhir4.Bundle, url: string) {
const measure: fhir4.Measure = bundle.entry?.find(entry => entry.resource?.resourceType === 'Measure')
?.resource as fhir4.Measure;

console.log(` Measure ${measure.id}`);
try {
console.log(` PUT ${url}/Measure/${measure.id}`);
measure.status = 'active';
const resp = await fetch(`${url}/Measure/${measure.id}`, {
method: 'PUT',
body: JSON.stringify(measure),
headers: {
'Content-Type': 'application/json+fhir'
}
});
console.log(` ${resp.status}`);
if (resp.status >= 400) {
const responseBody = await resp.json();
if (responseBody.resourceType === 'OperationOutcome') {
console.log(JSON.stringify(responseBody, null, 2));
}
}
} catch (e) {
console.error(e);
}
}

/**
* Loads ecqm-content-qicore-2024 into the database
*/
async function loadEcqmContent(bundlePaths: string[], url: string) {
for (const path of bundlePaths) {
const bundle = JSON.parse(fs.readFileSync(path, 'utf8')) as fhir4.Bundle;
if (bundle.resourceType === 'Bundle') {
if (bundle?.entry) {
console.log('FILE' + path);
bundle.entry = modifyEntriesForUpload(bundle.entry);
await fixAndPutLibraries(bundle, url);
await putMeasure(bundle, url);
}
} else {
console.warn(`${path} is not a Bundle`);
}
}
}

async function createCollections() {
await Connection.connect(DB_URL);
Expand Down Expand Up @@ -183,7 +300,7 @@ async function uploadBundleResources(filePath: string) {
* Convenience modification of an array of entries to create isOwned relationships and coerce to status active.
* This lets us massage existing data that may not have the appropriate properties needed for a Publishable Measure Repository
*/
function modifyEntriesForUpload(entries: fhir4.BundleEntry<fhir4.FhirResource>[]) {
export function modifyEntriesForUpload(entries: fhir4.BundleEntry<fhir4.FhirResource>[]) {
// pre-process to find owned relationships
const ownedUrls: string[] = [];
const modifiedEntries = entries.map(ent => {
Expand Down Expand Up @@ -236,9 +353,14 @@ async function insertFHIRModelInfoLibrary() {
const fhirModelInfo = fs.readFileSync('scripts/fixtures/Library-FHIR-ModelInfo.json', 'utf8');
const fhirModelInfoLibrary: CRMIShareableLibrary = JSON.parse(fhirModelInfo);

const qicoreModelInfo = fs.readFileSync('scripts/fixtures/Library-QICore-ModelInfo.json', 'utf8');
const qicoreModelInfoLibrary: CRMIShareableLibrary = JSON.parse(qicoreModelInfo);

const collection = Connection.db.collection<FhirArtifact>('Library');
console.log(`Inserting Library/${fhirModelInfoLibrary.id} into database`);
await collection.insertOne(fhirModelInfoLibrary);
console.log(`Inserting Library/${qicoreModelInfoLibrary.id} into database`);
await collection.insertOne(qicoreModelInfoLibrary);
}

if (process.argv[2] === 'delete') {
Expand Down Expand Up @@ -305,6 +427,16 @@ if (process.argv[2] === 'delete') {
console.log('Done');
})
.catch(console.error);
} else if (process.argv[2] === 'loadEcqmContent') {
let url = 'http://localhost:3000/4_0_1';
if (process.argv.length < 4) {
console.log('Defaulting service url to http://localhost:3000/4_0_1');
} else {
url = process.argv[3];
}

const bundlePaths = getBundlePaths();
loadEcqmContent(bundlePaths, url);
} else {
console.log('Usage: ts-node src/scripts/dbSetup.ts <create|delete|reset>');
}
63 changes: 63 additions & 0 deletions service/scripts/fixtures/Library-QICore-ModelInfo.json

Large diffs are not rendered by default.

0 comments on commit eb19b42

Please sign in to comment.