Skip to content
This repository has been archived by the owner on Dec 5, 2024. It is now read-only.

Commit

Permalink
Merge pull request #49 from mcode/update-fhir-types
Browse files Browse the repository at this point in the history
Update FHIR type library, service library, and dependencies
  • Loading branch information
zlister authored Dec 21, 2022
2 parents 4c3f379 + f878d4c commit 2fd277f
Show file tree
Hide file tree
Showing 10 changed files with 3,022 additions and 2,955 deletions.
4,681 changes: 2,384 additions & 2,297 deletions package-lock.json

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,32 @@
"license": "Apache-2.0",
"dependencies": {
"body-parser": "^1.19.0",
"clinical-trial-matching-service": "^0.0.3",
"clinical-trial-matching-service": "^0.0.7",
"dotenv-flow": "^3.2.0",
"express": "^4.17.1",
"fhirclient": "^2.3.11",
"fhirpath": "^2.7.4",
"fhirpath": "^3.3.0",
"node-fetch": "^2.6.1"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@types/dotenv-flow": "^3.0.0",
"@types/express": "^4.17.11",
"@types/jasmine": "^3.6.9",
"@types/node": "^14.14.41",
"@types/fhir": "^0.0.35",
"@types/jasmine": "^4.3.1",
"@types/node": "^18.11.17",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"eslint": "^7.24.0",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-prettier": "^3.4.0",
"@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.47.0",
"eslint": "^8.30.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"fhir": "^4.8.2",
"jasmine": "^3.7.0",
"jasmine": "^4.5.0",
"nock": "^13.0.11",
"nyc": "^15.1.0",
"prettier": "^2.1.0",
"supertest": "^6.1.3",
"ts-node": "^9.1.1",
"ts-node": "^10.9.1",
"typescript": "^4.2.4"
}
}
1,120 changes: 561 additions & 559 deletions spec/mcode.spec.ts

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions spec/server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('TrailScopeService', () => {
return request(server).get('/').set('Accept', 'application/json').expect(200);
});

it('uses the query runner', (done) => {
it('uses the query runner', () => {
const runQuery = spyOn(service.queryRunner, 'runQuery').and.callFake(() => {
return Promise.resolve(new SearchSet([]));
});
Expand All @@ -30,9 +30,8 @@ describe('TrailScopeService', () => {
.send({ resourceType: 'Bundle', type: 'collection', entry: [] })
.set('Accept', 'application/json')
.expect(200)
.end(() => {
.then(() => {
expect(runQuery).toHaveBeenCalled();
done();
});
});
});
Expand Down
56 changes: 32 additions & 24 deletions spec/trialscope.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { ClinicalTrialsGovService, ResearchStudy } from 'clinical-trial-matching-service';
import { Bundle, BundleEntry, FhirResource } from 'fhir/r4';
// For spying purposes:
import nock from 'nock';

import {
isTrialScopeResponse,
isTrialScopeErrorResponse,
Expand All @@ -6,9 +11,6 @@ import {
TrialScopeServerError,
TrialScopeTrial
} from '../src/trialscope';
import { fhir, ClinicalTrialsGovService, ResearchStudy } from 'clinical-trial-matching-service';
// For spying purposes:
import nock from 'nock';

describe('isTrialScopeResponse', () => {
it('returns false with a non-object', () => {
Expand Down Expand Up @@ -45,12 +47,12 @@ describe('isTrialScopeErrorResponse', () => {
describe('TrialScopeQuery', () => {
it('ignores bad entries', () => {
// This involves a bit of lying to TypeScript
const patientBundle: fhir.Bundle = {
const patientBundle: Bundle = {
resourceType: 'Bundle',
type: 'collection',
entry: []
};
patientBundle.entry.push(({ invalid: true } as unknown) as fhir.BundleEntry);
patientBundle.entry?.push({ invalid: true } as unknown as BundleEntry);
new TrialScopeQuery(patientBundle);
// Success is the object being created at all
});
Expand Down Expand Up @@ -126,14 +128,15 @@ describe('TrialScopeQuery', () => {
// TypeScript assumes extra fields are an error, although in this case the
// FHIR types supplies are somewhat intentionally sparse as they're not a
// full definition
const condition: fhir.Resource = {
const condition: FhirResource = {
resourceType: 'Condition',
meta: {
profile: [
'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-primary-cancer-condition',
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-condition'
]
},
subject: {},
clinicalStatus: {
coding: [
{
Expand Down Expand Up @@ -174,7 +177,7 @@ describe('TrialScopeQuery', () => {
}
}
]
} as fhir.Resource;
};
const query = new TrialScopeQuery({
resourceType: 'Bundle',
type: 'collection',
Expand Down Expand Up @@ -203,15 +206,19 @@ describe('TrialScopeQuery', () => {
// currently the case
let m = /mcode:\s*{(.*?)}/.exec(graphQL);
expect(m).toBeTruthy();
const mcodeFilters = m[1];
expect(mcodeFilters).toMatch(/primaryCancer:\s*BREAST_CANCER/);
if (m) {
const mcodeFilters = m[1];
expect(mcodeFilters).toMatch(/primaryCancer:\s*BREAST_CANCER/);
}
m = /baseFilters:\s*{(.*?)}/.exec(graphQL);
expect(m).toBeTruthy();
const baseFilters = m[1];
expect(baseFilters).toMatch(/zipCode:\s*"01234"/);
expect(baseFilters).toMatch(/travelRadius:\s*15\b/);
expect(baseFilters).toMatch(/phase:\s*PHASE_1\b/);
expect(baseFilters).toMatch(/recruitmentStatus:\s*RECRUITING\b/);
if (m) {
const baseFilters = m[1];
expect(baseFilters).toMatch(/zipCode:\s*"01234"/);
expect(baseFilters).toMatch(/travelRadius:\s*15\b/);
expect(baseFilters).toMatch(/phase:\s*PHASE_1\b/);
expect(baseFilters).toMatch(/recruitmentStatus:\s*RECRUITING\b/);
}
});

it("excludes parameters that weren't included", () => {
Expand All @@ -234,11 +241,13 @@ describe('TrialScopeQuery', () => {
const graphQL = query.toQuery();
const m = /baseFilters:\s*{(.*?)}/.exec(graphQL);
expect(m).toBeTruthy();
const baseFilters = m[1];
expect(baseFilters).toMatch(/zipCode:\s*"98765"/);
expect(baseFilters).not.toMatch('travelRadius');
expect(baseFilters).not.toMatch('phase');
expect(baseFilters).not.toMatch('recruitmentStatus');
if (m) {
const baseFilters = m[1];
expect(baseFilters).toMatch(/zipCode:\s*"98765"/);
expect(baseFilters).not.toMatch('travelRadius');
expect(baseFilters).not.toMatch('phase');
expect(baseFilters).not.toMatch('recruitmentStatus');
}
});
});

Expand All @@ -256,8 +265,6 @@ describe('TrialScopeQueryRunner', () => {
});
afterEach(() => {
expect(scope.isDone()).toBeTrue();
interceptor = null;
scope = null;
});

it('handles an empty response', () => {
Expand Down Expand Up @@ -308,7 +315,7 @@ describe('TrialScopeQueryRunner', () => {
});

describe('runQuery', () => {
let patientBundle: fhir.Bundle;
let patientBundle: Bundle;
beforeEach(() => {
// This is basically the minimum bundle required to run a query
patientBundle = {
Expand Down Expand Up @@ -460,8 +467,9 @@ describe('TrialScopeQueryRunner', () => {
expected.enrollment = [reference];
// For the sake of this test, kill the createResourceId functions
// (it shouldn't be enumerable anyway)
expected.createReferenceId = null;
(actual.entry[i].resource as ResearchStudy).createReferenceId = null;
// Note this involves lying to the TypeScript compiler
(expected as unknown as { createReferenceId: null }).createReferenceId = null;
(actual.entry[i].resource as unknown as { createReferenceId: null }).createReferenceId = null;
expect(actual.entry[i].resource).toEqual(expected);
}
expect(actual.entry[0].search.score).toEqual(1);
Expand Down
43 changes: 8 additions & 35 deletions src/mcode.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,16 @@
import { fhirclient } from 'fhirclient/lib/types';
import { Bundle, Coding, Quantity, Ratio, Resource } from 'fhir/r4';
import * as fhirpath from 'fhirpath';

import { CodeProfile, ProfileSystemCodes } from './profileSystemLogic';

import profile_system_codes_json from '../data/profile-system-codes-json.json';
import { fhir } from 'clinical-trial-matching-service';

const profile_system_codes = profile_system_codes_json as ProfileSystemCodes;

export type FHIRPath = string;
// fhirpath now has official TypeScript types. Unfortunately, the result they use is "any"
export type PathLookupResult = Record<string, unknown> | string | number;

export interface Coding {
system?: string;
code?: string;
display?: string;
}

export interface Quantity {
value?: number | string;
comparator?: string;
unit?: string;
system?: string;
code?: string;
}

export interface Ratio {
numerator?: Quantity;
denominator?: Quantity;
}

export interface PrimaryCancerCondition {
clinicalStatus?: Coding[];
coding?: Coding[];
Expand Down Expand Up @@ -89,7 +69,7 @@ export class ExtractedMCODE {
ecogPerformaceStatus: number;
karnofskyPerformanceStatus: number;

constructor(patientBundle: fhir.Bundle) {
constructor(patientBundle: Bundle | null) {
if (patientBundle != null) {
for (const entry of patientBundle.entry) {
if (!('resource' in entry)) {
Expand Down Expand Up @@ -319,12 +299,8 @@ export class ExtractedMCODE {
}
}

lookup(
resource: fhirclient.FHIR.Resource,
path: FHIRPath,
environment?: { [key: string]: string }
): PathLookupResult[] {
return fhirpath.evaluate(resource, path, environment);
lookup(resource: Resource, path: FHIRPath, environment?: { [key: string]: string }): PathLookupResult[] {
return fhirpath.evaluate(resource, path, environment) as PathLookupResult[];
}
resourceProfile(profiles: PathLookupResult[], key: string): boolean {
for (const profile of profiles) {
Expand Down Expand Up @@ -1299,13 +1275,10 @@ export class ExtractedMCODE {
const system = this.normalizeCodeSystem(coding.system);
for (const sheetName of sheetNames) {
let codeProfile: CodeProfile = undefined;
try {
codeProfile = profile_system_codes[sheetName] as CodeProfile; // Pull the codes for the profile
} finally {
if (codeProfile == undefined) {
console.error('Code Profile ' + sheetName + ' is undefined.');
continue;
}
codeProfile = profile_system_codes[sheetName]; // Pull the codes for the profile
if (codeProfile == undefined) {
console.error('Code Profile ' + sheetName + ' is undefined.');
continue;
}

let codeSet: { code: string }[] = codeProfile[system] as { code: string }[]; // Pull the system codes from the codes
Expand Down
6 changes: 3 additions & 3 deletions src/profileSystemLogic.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Coding } from './mcode';
import { Coding } from 'fhir/r4';

export interface ProfileType {
types: string[];
}
export interface ProfileSystemCodes {
[key: string] : CodeProfile;
[key: string]: CodeProfile;
'Cancer-Skin'?: CodeProfile;
'Treatment-Pertuzumab'?: CodeProfile;
'Treatment-SRS-Brain'?: CodeProfile;
Expand Down Expand Up @@ -45,7 +45,7 @@ export interface ProfileSystemCodes {
'Biomarker-ER'?: CodeProfile;
}
export interface CodeProfile {
[key: string] : { code: string }[];
[key: string]: { code: string }[];
SNOMED?: { code: string }[];
RxNorm?: { code: string }[];
ICD10?: { code: string }[];
Expand Down
10 changes: 6 additions & 4 deletions src/research-study-mapping.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ResearchStudy, fhir, convertStringsToCodeableConcept } from 'clinical-trial-matching-service';
import { ResearchStudy, convertStringsToCodeableConcept } from 'clinical-trial-matching-service';
import { TrialScopeTrial } from './trialscope';

type ResearchStudyStatus = ResearchStudy['status'];

// Mappings between trialscope value sets and FHIR value sets
const phaseCodeMap = new Map<string, string>([
['Early Phase 1', 'early-phase-1'],
Expand All @@ -13,7 +15,7 @@ const phaseCodeMap = new Map<string, string>([
['Phase 4', 'phase-4']
]);

const statusMap = new Map<string, fhir.ResearchStudyStatus>([
const statusMap = new Map<string, ResearchStudyStatus>([
['Active, not recruiting', 'closed-to-accrual'],
['Approved for marketing', 'approved'],
['Available', 'active'],
Expand All @@ -22,7 +24,7 @@ const statusMap = new Map<string, fhir.ResearchStudyStatus>([
['Recruiting', 'active']
]);

function convertStatus(tsStatus: string): fhir.ResearchStudyStatus {
function convertStatus(tsStatus: string): ResearchStudyStatus {
return statusMap.get(tsStatus);
}

Expand Down Expand Up @@ -55,7 +57,7 @@ export function convertTrialScopeToResearchStudy(trial: TrialScopeTrial, id: num
};
}
if (trial.studyType) {
result.category = [{ text: "Study Type: " + trial.studyType }];
result.category = [{ text: 'Study Type: ' + trial.studyType }];
}
if (trial.conditions) {
const conditions = convertStringsToCodeableConcept(trial.conditions);
Expand Down
14 changes: 5 additions & 9 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import { ClinicalTrialsGovService, ClinicalTrialMatchingService, configFromEnv } from 'clinical-trial-matching-service';
import * as dotenv from 'dotenv-flow';
import express from 'express';
import TrialScopeQueryRunner from './trialscope';
import { Bundle } from 'fhir/r4';

import {
fhir,
ClinicalTrialsGovService,
ClinicalTrialMatchingService,
configFromEnv
} from 'clinical-trial-matching-service';
import * as dotenv from 'dotenv-flow';
import TrialScopeQueryRunner from './trialscope';

export class TrialScopeService extends ClinicalTrialMatchingService {
queryRunner: TrialScopeQueryRunner;
backupService: ClinicalTrialsGovService;

constructor(config: Record<string, string | number>) {
super((patientBundle: fhir.Bundle) => {
super((patientBundle: Bundle) => {
return this.queryRunner.runQuery(patientBundle);
}, config);

Expand Down
Loading

0 comments on commit 2fd277f

Please sign in to comment.