Skip to content

Commit

Permalink
Add script and cleaner logic sharing
Browse files Browse the repository at this point in the history
  • Loading branch information
lmd59 committed Jun 12, 2024
1 parent 1cbedec commit 8b653e2
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 55 deletions.
1 change: 1 addition & 0 deletions service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"db:reset": "ts-node ./scripts/dbSetup.ts reset",
"db:setup": "ts-node ./scripts/dbSetup.ts create",
"db:loadBundle": "ts-node ./scripts/dbSetup.ts loadBundle",
"db:postBundle": "ts-node ./scripts/dbSetup.ts postBundle",
"lint": "eslint \"./src/**/*.{js,ts}\"",
"lint:fix": "eslint \"./src/**/*.{js,ts}\" --fix",
"prettier": "prettier --check \"./src/**/*.{js,ts}\"",
Expand Down
173 changes: 125 additions & 48 deletions service/scripts/dbSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as dotenv from 'dotenv';
import { MongoError } from 'mongodb';
import { v4 as uuidv4 } from 'uuid';
import path from 'path';
import { DetailedEntry, addIsOwnedExtension, addLibraryIsOwned } from '../src/util/baseUtils';
import { addIsOwnedExtension, addLibraryIsOwned } from '../src/util/baseUtils';
dotenv.config();

const DB_URL = process.env.DATABASE_URL || 'mongodb://localhost:27017/measure-repository';
Expand Down Expand Up @@ -39,10 +39,9 @@ async function deleteCollections() {
/*
* Gathers necessary file path(s) for bundle(s) to upload, then uploads all measure and
* library resources found in the bundle(s).
*
*/
async function loadBundle(fileOrDirectoryPath: string) {
await Connection.connect(DB_URL);
console.log(`Connected to ${DB_URL}`);
async function loadBundle(fileOrDirectoryPath: string, connectionURL?: string) {
const status = fs.statSync(fileOrDirectoryPath);
if (status.isDirectory()) {
const filePaths: string[] = [];
Expand All @@ -60,10 +59,61 @@ async function loadBundle(fileOrDirectoryPath: string) {
});

for (const filePath of filePaths) {
await uploadBundleResources(filePath);
await (connectionURL ? postBundleResources(filePath, connectionURL) : uploadBundleResources(filePath));
}
} else {
await uploadBundleResources(fileOrDirectoryPath);
await (connectionURL
? postBundleResources(fileOrDirectoryPath, connectionURL)
: uploadBundleResources(fileOrDirectoryPath));
}
}

async function transactBundle(bundle: fhir4.Bundle, url: string) {
if (bundle.entry) {
// only upload Measures and Libraries
bundle.entry = bundle.entry.filter(
e => e.resource?.resourceType === 'Measure' || e.resource?.resourceType === 'Library'
);
for (const entry of bundle.entry) {
if (entry.request?.method === 'POST') {
entry.request.method = 'PUT';
}
}
}

try {
console.log(` POST ${url}`);

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

async function postBundleResources(filePath: string, url: string) {
console.log(`Loading bundle from path ${filePath}`);

const data = fs.readFileSync(filePath, 'utf8');
if (data) {
console.log(`POSTing ${filePath.split('/').slice(-1)}...`);
const bundle: fhir4.Bundle = JSON.parse(data);
const entries = bundle.entry as fhir4.BundleEntry<fhir4.FhirResource>[];
// modify bundles before posting
if (entries) {
const modifiedEntries = modifyEntriesForUpload(entries);
bundle.entry = modifiedEntries;
}
transactBundle(bundle, url);
}
}

Expand All @@ -77,56 +127,21 @@ async function uploadBundleResources(filePath: string) {
if (data) {
console.log(`Uploading ${filePath.split('/').slice(-1)}...`);
const bundle: fhir4.Bundle = JSON.parse(data);
const entries = bundle.entry as DetailedEntry[];
const entries = bundle.entry as fhir4.BundleEntry<fhir4.FhirResource>[];
// retrieve each resource and insert into database
if (entries) {
await Connection.connect(DB_URL);
console.log(`Connected to ${DB_URL}`);
let resourcesUploaded = 0;
let notUploaded = 0;
// pre-process to find owned relationships
const ownedUrls: string[] = [];
const modifiedEntries = entries.map(ent => {
// if the artifact is a Measure, get the main Library from the Measure and add the is owned extension on
// that library's entry in the relatedArtifacts of the measure
const { modifiedEntry, url } = addIsOwnedExtension(ent);
if (url) ownedUrls.push(url);
// check if there are other isOwned urls but already in the relatedArtifacts
if (ent.resource?.resourceType === 'Measure' || ent.resource?.resourceType === 'Library') {
ent.resource.relatedArtifact?.forEach(ra => {
if (ra.type === 'composed-of') {
if (
ra.extension?.some(
e => e.url === 'http://hl7.org/fhir/StructureDefinition/artifact-isOwned' && e.valueBoolean === true
)
) {
if (ra.resource) {
ownedUrls.push(ra.resource);
}
}
}
});
}
return modifiedEntry;
});
const modifiedEntries = modifyEntriesForUpload(entries);
const uploads = modifiedEntries.map(async entry => {
// add Library owned extension
entry = addLibraryIsOwned(entry, ownedUrls);
if (
entry.resource?.resourceType &&
(entry.resource?.resourceType === 'Library' || entry.resource?.resourceType === 'Measure')
) {
if (entry.resource?.resourceType === 'Library' || entry.resource?.resourceType === 'Measure') {
// Only upload Library or Measure resources
try {
if (!entry.resource.id) {
entry.resource.id = uuidv4();
}
if (entry.resource?.status != 'active') {
entry.resource.status = 'active';
console.warn(
`Resource ${entry?.resource?.resourceType}/${entry.resource.id} status has been coerced to 'active'.`
);
}
const collection = Connection.db.collection<fhir4.FhirResource>(entry.resource.resourceType);
console.log(`Inserting ${entry?.resource?.resourceType}/${entry.resource.id} into database`);
console.log(`Inserting ${entry.resource.resourceType}/${entry.resource.id} into database`);
await collection.insertOne(entry.resource);
resourcesUploaded += 1;
} catch (e) {
Expand All @@ -140,7 +155,7 @@ async function uploadBundleResources(filePath: string) {
}
}
} else {
if (entry?.resource?.resourceType) {
if (entry.resource?.resourceType) {
notUploaded += 1;
} else {
console.log('Resource or resource type undefined');
Expand All @@ -156,6 +171,52 @@ async function uploadBundleResources(filePath: string) {
}
}

function modifyEntriesForUpload(entries: fhir4.BundleEntry<fhir4.FhirResource>[]) {
// pre-process to find owned relationships
const ownedUrls: string[] = [];
const modifiedEntries = entries.map(ent => {
// if the artifact is a Measure, get the main Library from the Measure and add the is owned extension on
// that library's entry in the relatedArtifacts of the measure
const { modifiedEntry, url } = addIsOwnedExtension(ent);
if (url) ownedUrls.push(url);
// check if there are other isOwned urls but already in the relatedArtifacts
if (ent.resource?.resourceType === 'Measure' || ent.resource?.resourceType === 'Library') {
ent.resource.relatedArtifact?.forEach(ra => {
if (ra.type === 'composed-of') {
if (
ra.extension?.some(
e => e.url === 'http://hl7.org/fhir/StructureDefinition/artifact-isOwned' && e.valueBoolean === true
)
) {
if (ra.resource) {
ownedUrls.push(ra.resource);
}
}
}
});
}
return modifiedEntry;
});
const updatedEntries = modifiedEntries.map(entry => {
// add Library owned extension
const updatedEntry = addLibraryIsOwned(entry, ownedUrls);
if (updatedEntry.resource?.resourceType === 'Library' || updatedEntry.resource?.resourceType === 'Measure') {
// Only upload Library or Measure resources
if (!updatedEntry.resource.id) {
updatedEntry.resource.id = uuidv4();
}
if (updatedEntry.resource.status != 'active') {
updatedEntry.resource.status = 'active';
console.warn(
`Resource ${updatedEntry.resource.resourceType}/${updatedEntry.resource.id} status has been coerced to 'active'.`
);
}
}
return updatedEntry;
});
return updatedEntries;
}

/*
* Inserts the FHIR ModelInfo library into the database
*/
Expand Down Expand Up @@ -216,6 +277,22 @@ if (process.argv[2] === 'delete') {
.finally(() => {
Connection.connection?.close();
});
} else if (process.argv[2] === 'postBundle') {
if (process.argv.length < 4) {
throw new Error('Filename argument required.');
}
let url = 'http://localhost:3000/4_0_1';
if (process.argv.length < 5) {
console.log('Given only filename input. Defaulting service url to http://localhost:3000/4_0_1');
} else {
url = process.argv[4];
}

loadBundle(process.argv[3], url)
.then(() => {
console.log('Done');
})
.catch(console.error);
} else {
console.log('Usage: ts-node src/scripts/dbSetup.ts <create|delete|reset>');
}
2 changes: 1 addition & 1 deletion service/src/services/BaseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ async function uploadResourcesFromBundle(entries: DetailedEntry[]) {
const requestsArray = modifedRequestsArray.map(async entry => {
// add library owned extension
entry = addLibraryIsOwned(entry, ownedUrls);
return insertBundleResources(entry);
return insertBundleResources(entry as DetailedEntry);
});
return Promise.all(requestsArray);
}
Expand Down
4 changes: 2 additions & 2 deletions service/src/util/baseUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type DetailedEntry = fhir4.BundleEntry<FhirResource> & {
*
* returns modified entry and url of owned library
*/
export function addIsOwnedExtension(entry: DetailedEntry) {
export function addIsOwnedExtension(entry: fhir4.BundleEntry<fhir4.FhirResource>) {
if (entry.resource?.resourceType && entry.resource?.resourceType === 'Measure' && entry.resource?.library) {
// get the main Library of the Measure from the library property and the version
const mainLibrary = entry.resource.library[0];
Expand Down Expand Up @@ -70,7 +70,7 @@ export function addIsOwnedExtension(entry: DetailedEntry) {
/**
* Checks ownedUrls for entry url and adds isOwned extension to the resource if found in ownedUrls
*/
export function addLibraryIsOwned(entry: DetailedEntry, ownedUrls: string[]) {
export function addLibraryIsOwned(entry: fhir4.BundleEntry<fhir4.FhirResource>, ownedUrls: string[]) {
// add owned to identified resources (currently assumes these will only be Libraries)
if (entry.resource?.resourceType === 'Library' && entry.resource.url) {
const libraryUrl = entry.resource.version
Expand Down
5 changes: 1 addition & 4 deletions service/src/util/inputUtils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { RequestArgs, RequestQuery, FhirResourceType } from '@projecttacoma/node-fhir-server-core';
import { Filter } from 'mongodb';
import { BadRequestError, ResourceNotFoundError } from './errorUtils';
import { v4 as uuidv4 } from 'uuid';
import { BadRequestError } from './errorUtils';
import _ from 'lodash';
import { loggers } from '@projecttacoma/node-fhir-server-core';
const logger = loggers.get('default');

/*
* Gathers parameters from both the query and the FHIR parameter request body resource
Expand Down

0 comments on commit 8b653e2

Please sign in to comment.