Skip to content

Commit

Permalink
Entirely replace passed studies
Browse files Browse the repository at this point in the history
  • Loading branch information
dmpotter44 committed Feb 14, 2024
1 parent 05d83ba commit 2c93be2
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 98 deletions.
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clinical-trial-matching-service",
"version": "0.1.0",
"version": "0.1.1",
"description": "Provides a core library for interacting with the clinical-trial-matching-engine",
"homepage": "https://github.com/mcode/clinical-trial-matching-service",
"bugs": "https://github.com/mcode/clinical-trial-matching-service/issues",
Expand Down
56 changes: 16 additions & 40 deletions spec/clinicaltrialsgov.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,15 @@ describe('.findNCTNumber', () => {
identifier: [
{
system: 'other',
value: 'ignoreme'
value: 'NCT12345678'
},
{
system: ctg.CLINICAL_TRIAL_IDENTIFIER_CODING_SYSTEM_URL,
value: 'test'
value: 'NCT87654321'
}
]
})
).toEqual('test');
).toEqual('NCT87654321');
});
it('finds an NCT number based on regexp', () => {
expect(
Expand Down Expand Up @@ -173,18 +173,17 @@ describe('.findNCTNumber', () => {
});

describe('findNCTNumbers', () => {
it('builds a map', () => {
it('builds a set', () => {
const studies: ResearchStudy[] = [
createResearchStudy('no-NCTID'),
createResearchStudy('dupe1', 'NCT00000001'),
createResearchStudy('singleton', 'NCT12345678'),
createResearchStudy('dupe2', 'NCT00000001'),
createResearchStudy('dupe3', 'NCT00000001')
createResearchStudy('dupe3', 'NCT00000001'),
createResearchStudy('invalid', 'ignore this ID')
];
const map = ctg.findNCTNumbers(studies);
expect(map.size).toEqual(2);
expect(map.get('NCT12345678')).toEqual(studies[2]);
expect(map.get('NCT00000001')).toEqual([studies[1], studies[3], studies[4]]);
const set = ctg.findNCTNumbers(studies);
expect(set).toEqual(new Set<string>(['NCT12345678', 'NCT00000001']));
});
});

Expand Down Expand Up @@ -533,31 +532,26 @@ describe('ClinicalTrialsGovService', () => {
});

// These tests basically are only to ensure that all trials are properly visited when given.
it('updates all the given studies', () => {
it('updates studies with proper IDs', async () => {
// Our test studies contain the same NCT ID twice to make sure that works as expected, as well as a NCT ID that
// download spy will return null for to indicate a failure.
const testStudies: ResearchStudy[] = [
createResearchStudy('dupe1', 'NCT00000001'),
createResearchStudy('missing', 'NCT00000002'),
createResearchStudy('dupe2', 'NCT00000001'),
createResearchStudy('singleton', 'NCT00000003')
createResearchStudy('singleton', 'NCT00000003'),
createResearchStudy('invalidid', 'not an NCT id')
];
const testStudy = createClinicalStudy();
const updateSpy = spyOn(service, 'updateResearchStudy');
const getTrialSpy = jasmine.createSpy('getCachedClinicalStudy').and.callFake((nctId: string) => {
return Promise.resolve(nctId === 'NCT00000002' ? null : testStudy);
});
service.getCachedClinicalStudy = getTrialSpy;
return expectAsync(
service.updateResearchStudies(testStudies).then(() => {
expect(downloadTrialsSpy).toHaveBeenCalledOnceWith(['NCT00000001', 'NCT00000002', 'NCT00000003']);
// Update should have been called three times: twice for the NCT00000001 studies, and once for the NCT00000003 study
expect(updateSpy).toHaveBeenCalledWith(testStudies[0], testStudy);
expect(updateSpy).not.toHaveBeenCalledWith(testStudies[1], testStudy);
expect(updateSpy).toHaveBeenCalledWith(testStudies[2], testStudy);
expect(updateSpy).toHaveBeenCalledWith(testStudies[3], testStudy);
})
).toBeResolved();
const result = await service.updateResearchStudies(testStudies);
expect(downloadTrialsSpy).toHaveBeenCalledOnceWith(['NCT00000001', 'NCT00000002', 'NCT00000003']);
// Expect the invalid ID to be left unchanged
console.log(JSON.stringify(result, null, 2));
expect(result[4]).toEqual(testStudies[4]);
});

it('does nothing if no studies have NCT IDs', () => {
Expand All @@ -578,7 +572,6 @@ describe('ClinicalTrialsGovService', () => {
createResearchStudy('test4', 'NCT00000004')
];
const testStudy = createClinicalStudy();
spyOn(service, 'updateResearchStudy');
const getTrialSpy = jasmine.createSpy('getCachedClinicalStudy').and.callFake(() => {
return Promise.resolve(testStudy);
});
Expand Down Expand Up @@ -619,19 +612,13 @@ describe('ClinicalTrialsGovService', () => {
];

const testStudy = createClinicalStudy();
const updateSpy = spyOn(service, 'updateResearchStudy');
const getTrialSpy = jasmine.createSpy('getCachedClinicalStudy').and.callFake((nctId: string) => {
return Promise.resolve(nctId === 'NCT00000002' ? null : testStudy);
});

service.getCachedClinicalStudy = getTrialSpy;
await service.updateSearchSetEntries(testSearchSetEntries);
expect(downloadTrialsSpy).toHaveBeenCalledOnceWith(['NCT00000001', 'NCT00000002', 'NCT00000003']);
// Update should have been called three times: twice for the NCT00000001 studies, and once for the NCT00000003 study
expect(updateSpy).toHaveBeenCalledWith(testSearchSetEntries[0].resource as ResearchStudy, testStudy);
expect(updateSpy).not.toHaveBeenCalledWith(testSearchSetEntries[1].resource as ResearchStudy, testStudy);
expect(updateSpy).toHaveBeenCalledWith(testSearchSetEntries[2].resource as ResearchStudy, testStudy);
expect(updateSpy).toHaveBeenCalledWith(testSearchSetEntries[3].resource as ResearchStudy, testStudy);
});

it('does nothing if no studies have NCT IDs', async () => {
Expand All @@ -652,7 +639,6 @@ describe('ClinicalTrialsGovService', () => {
createSearchSetEntry('test4', 'NCT00000004', 0.5)
];
const testStudy = createClinicalStudy();
spyOn(service, 'updateResearchStudy');
const getTrialSpy = jasmine.createSpy('getCachedClinicalStudy').and.callFake(() => {
return Promise.resolve(testStudy);
});
Expand Down Expand Up @@ -936,14 +922,4 @@ describe('ClinicalTrialsGovService', () => {
expect(result?.study_json).toEqual(JSON.stringify(study));
});
});

describe('#updateResearchStudy', () => {
it('forwards to createResearchStudyFromClinicalStudy', () => {
const service = createMemoryCTGovService();
const testResearchStudy = createResearchStudy('test');
const testClinicalStudy = createClinicalStudy();
service.updateResearchStudy(testResearchStudy, testClinicalStudy);
// There's no really good way to verify this worked. For now, it not blowing up is good enough.
});
});
});
2 changes: 2 additions & 0 deletions spec/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ describe('index', () => {
expect(ctms.CodeSystemEnum).toBeDefined();
expect(ctms.ClientError).toBeDefined();
expect(ctms.ServerError).toBeDefined();
expect(ctms.FHIRDate).toBeDefined();
expect(ctms.resourceContainsProfile).toBeDefined();
// This is dumb but otherwise Istanbul complains they were never called
expect(new ctms.ClientError('example')).toBeInstanceOf(Error);
expect(new ctms.ServerError('example')).toBeInstanceOf(Error);
Expand Down
89 changes: 37 additions & 52 deletions src/clinicaltrialsgov.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ export function formatNCTNumber(nctNumber: number): string {
export function findNCTNumber(study: ResearchStudy): NCTNumber | null {
if (study.identifier && Array.isArray(study.identifier) && study.identifier.length > 0) {
for (const identifier of study.identifier) {
if (identifier.system === CLINICAL_TRIAL_IDENTIFIER_CODING_SYSTEM_URL && typeof identifier.value === 'string')
if (
identifier.system === CLINICAL_TRIAL_IDENTIFIER_CODING_SYSTEM_URL &&
typeof identifier.value === 'string' &&
isValidNCTNumber(identifier.value)
)
return identifier.value;
}
// Fallback: regexp match
Expand All @@ -98,26 +102,15 @@ export function findNCTNumber(study: ResearchStudy): NCTNumber | null {

/**
* Finds all the NCT numbers within the given list of studies. Returns a map of
* NCT numbers to the research studies they match. If multiple research studies
* have the same NCT ID, then an array will be used to contain all matching
* studies. (This should be very uncommon but is supported anyway.)
* NCT numbers to the research studies they match.
* @param studies the NCT numbers found, if any
*/
export function findNCTNumbers(studies: ResearchStudy[]): Map<NCTNumber, ResearchStudy | Array<ResearchStudy>> {
const result = new Map<NCTNumber, ResearchStudy | Array<ResearchStudy>>();
export function findNCTNumbers(studies: ResearchStudy[]): Set<NCTNumber> {
const result = new Set<NCTNumber>();
for (const study of studies) {
const nctId = findNCTNumber(study);
if (nctId !== null) {
const existing = result.get(nctId);
if (existing === undefined) {
result.set(nctId, study);
} else {
if (Array.isArray(existing)) {
existing.push(study);
} else {
result.set(nctId, [existing, study]);
}
}
result.add(nctId);
}
}
return result;
Expand Down Expand Up @@ -455,40 +448,38 @@ export class ClinicalTrialsGovService {
* occurs).
*
* @param studies the studies to attempt to update
* @returns a Promise that resolves when the studies are updated. It will resolve with the same array that was passed
* in - this updates the given objects, it does not clone them and create new ones.
* @returns a Promise that resolves with updated studies
*/
async updateResearchStudies(studies: ResearchStudy[]): Promise<ResearchStudy[]> {
const nctIdMap = findNCTNumbers(studies);
if (nctIdMap.size === 0) {
const nctIds = Array.from(findNCTNumbers(studies));
if (nctIds.length === 0) {
// Nothing to do
return studies;
}
const nctIds = Array.from(nctIdMap.keys());
this.log('Updating research studies with NCT IDs %j', nctIds);
// Make sure the NCT numbers are in the cache
await this.ensureTrialsAvailable(nctIds);
// Update the items in the list
const updatedTrials = new Map<string, ResearchStudy>();
await Promise.all(
Array.from(nctIdMap.entries()).map(([nctId, study]) => this.updateResearchStudyFromCache(nctId, study))
Array.from(nctIds).map(async (nctId) => {
const study = await this.getCachedClinicalStudy(nctId);
if (study) {
updatedTrials.set(nctId, createResearchStudyFromClinicalStudy(study));
}
})
);
return studies;
}

private async updateResearchStudyFromCache(nctId: string, originals: ResearchStudy | ResearchStudy[]): Promise<void> {
const clinicalStudy = await this.getCachedClinicalStudy(nctId);
// Make sure we have data to use - cache can be missing NCT IDs even after requesting them if the NCT is missing
// from the origin service
if (clinicalStudy !== null) {
// Update whatever trials we have, which will either be an array or single object
if (Array.isArray(originals)) {
for (const s of originals) {
this.updateResearchStudy(s, clinicalStudy);
return studies.map<ResearchStudy>((study) => {
const id = findNCTNumber(study);
if (id) {
// It's possible some IDs may not exist and may be left alone
const updated = updatedTrials.get(id);
if (updated) {
return updated;
}
} else {
this.updateResearchStudy(originals, clinicalStudy);
}
}
return study;
});
}

async updateSearchSetEntries(entries: SearchSetEntry[]): Promise<SearchSetEntry[]> {
Expand All @@ -497,9 +488,15 @@ export class ClinicalTrialsGovService {
await this.ensureTrialsAvailable(studies);

await Promise.all(
entries.map((entry) => {
const nctId = findNCTNumber(entry.resource as ResearchStudy) || '';
return this.updateResearchStudyFromCache(nctId, entry.resource as ResearchStudy);
entries.map(async (entry) => {
const resource = entry.resource as ResearchStudy;
const nctId = findNCTNumber(resource);
if (nctId) {
const study = await this.getCachedClinicalStudy(nctId);
if (study) {
entry.resource = createResearchStudyFromClinicalStudy(study);
}
}
})
);

Expand Down Expand Up @@ -681,18 +678,6 @@ export class ClinicalTrialsGovService {
return null;
}

/**
* The provides a stub that handles updating the research study with data from a clinical study downloaded from the
* ClinicalTrials.gov website. This primarily exists as a stub to allow the exact process which updates a research
* study to be overridden if necessary.
*
* @param researchStudy the research study to update
* @param clinicalStudy the clinical study to update it with
*/
updateResearchStudy(researchStudy: ResearchStudy, clinicalStudy: Study): void {
createResearchStudyFromClinicalStudy(clinicalStudy, researchStudy);
}

/**
* Creates and initializes a new service for retrieving data from http://clinicaltrials.gov/.
*
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
export * from './research-study';
export * from './searchset';
export * from './clinicaltrialsgov';
export * from './fhir-util';
export { CodeMapper, CodeSystemEnum } from './codeMapper';
export * from './mcodeextractor';
export { Study } from './ctg-api';
Expand Down
24 changes: 24 additions & 0 deletions src/study-fhir-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from 'fhir/r4';
import { isFhirDate } from './fhir-type-guards';
import { addContainedResource, addToContainer } from './research-study';
import { CLINICAL_TRIAL_IDENTIFIER_CODING_SYSTEM_URL } from './clinicaltrialsgov';

const PHASE_MAP = new Map<Phase, string>([
[Phase.NA, 'n-a'],
Expand Down Expand Up @@ -103,6 +104,29 @@ export function createResearchStudyFromClinicalStudy(study: Study, result?: Rese
return result;
}

// Fill out basic information
const identificationModule = protocolSection.identificationModule;
if (identificationModule) {
const nctId = identificationModule.nctId;
if (nctId) {
result.identifier = [
{
system: CLINICAL_TRIAL_IDENTIFIER_CODING_SYSTEM_URL,
value: nctId
}
];
}
const briefTitle = identificationModule.briefTitle;
if (briefTitle) {
result.title = briefTitle;
} else {
const officialTitle = identificationModule.officialTitle;
if (officialTitle) {
result.title = officialTitle;
}
}
}

const eligibility = protocolSection.eligibilityModule;
if (eligibility) {
const criteria = eligibility.eligibilityCriteria;
Expand Down

0 comments on commit 2c93be2

Please sign in to comment.