diff --git a/package.json b/package.json index 89a2a86..25c4c9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clinical-trial-matching-service", - "version": "0.0.14", + "version": "0.1.0", "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", diff --git a/spec/clinicaltrialsgov.spec.ts b/spec/clinicaltrialsgov.spec.ts index 62ee3d4..c7574cc 100644 --- a/spec/clinicaltrialsgov.spec.ts +++ b/spec/clinicaltrialsgov.spec.ts @@ -8,8 +8,9 @@ import * as sqlite3 from 'sqlite3'; // Trial missing summary, inclusion/exclusion criteria, phase and study type import { createClinicalStudy } from './support/clinicalstudy-factory'; -import { createResearchStudy } from './support/researchstudy-factory'; +import { createResearchStudy, createSearchSetEntry } from './support/researchstudy-factory'; import { PagedStudies, Study } from '../src/ctg-api'; +import { SearchBundleEntry } from '../src/searchset'; function specFilePath(specFilePath: string): string { return path.join(__dirname, '../../spec/data', specFilePath); @@ -592,6 +593,81 @@ describe('ClinicalTrialsGovService', () => { }); }); + describe('#updateSearchSetEntries', () => { + let service: ctg.ClinicalTrialsGovService; + let downloadTrialsSpy: jasmine.Spy; + + beforeEach(async () => { + // The service is never initialized + service = createMemoryCTGovService(); + await service.init(); + // TypeScript won't allow us to install spies the "proper" way on private methods + service['downloadTrials'] = downloadTrialsSpy = jasmine.createSpy('downloadTrials').and.callFake(() => { + return Promise.resolve(true); + }); + }); + + // These tests basically are only to ensure that all trials are properly visited when given. + it('updates all the given studies', 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 testSearchSetEntries: SearchBundleEntry[] = [ + createSearchSetEntry('dupe1', 'NCT00000001'), + createSearchSetEntry('missing', 'NCT00000002'), + createSearchSetEntry('dupe2', 'NCT00000001'), + createSearchSetEntry('singleton', 'NCT00000003', 0.5) + ]; + + 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 () => { + await service.updateSearchSetEntries([ + { resource: { resourceType: 'ResearchStudy', status: 'active' }, search: { mode: 'match', score: 0 } } + ]); + expect(downloadTrialsSpy).not.toHaveBeenCalled(); + }); + + it('handles splitting requests', () => { + // Basically, drop the limit to be very low, and make sure we get two calls + service.maxTrialsPerRequest = 2; + + const testSearchSetEntries: SearchBundleEntry[] = [ + createSearchSetEntry('test1', 'NCT00000001'), + createSearchSetEntry('test2', 'NCT00000002'), + createSearchSetEntry('test3', 'NCT00000003'), + createSearchSetEntry('test4', 'NCT00000004', 0.5) + ]; + const testStudy = createClinicalStudy(); + spyOn(service, 'updateResearchStudy'); + const getTrialSpy = jasmine.createSpy('getCachedClinicalStudy').and.callFake(() => { + return Promise.resolve(testStudy); + }); + + service.getCachedClinicalStudy = getTrialSpy; + return expectAsync( + service.updateSearchSetEntries(testSearchSetEntries).then(() => { + expect(downloadTrialsSpy.calls.count()).toEqual(2); + expect(downloadTrialsSpy.calls.argsFor(0)).toEqual([['NCT00000001', 'NCT00000002']]); + expect(downloadTrialsSpy.calls.argsFor(1)).toEqual([['NCT00000003', 'NCT00000004']]); + }) + ).toBeResolved(); + }); + }); + // this functionality is currently unimplemented - this test exists solely to "cover" the method describe('#removeExpiredCacheEntries', () => { it('does nothing', async () => { diff --git a/spec/support/researchstudy-factory.ts b/spec/support/researchstudy-factory.ts index a32fb76..53758e8 100644 --- a/spec/support/researchstudy-factory.ts +++ b/spec/support/researchstudy-factory.ts @@ -1,9 +1,10 @@ import { CLINICAL_TRIAL_IDENTIFIER_CODING_SYSTEM_URL } from '../../src/clinicaltrialsgov'; import type { ResearchStudy as IResearchStudy } from 'fhir/r4'; import { ResearchStudy } from '../../src/research-study'; +import { SearchBundleEntry } from '../../src/searchset'; export function createResearchStudyObject(nctId?: string): ResearchStudy { - const result = new ResearchStudy(nctId ?? "test"); + const result = new ResearchStudy(nctId ?? 'test'); if (nctId) { result.identifier = [ { @@ -32,3 +33,14 @@ export function createResearchStudy(id: string, nctId?: string): IResearchStudy } return result; } + +export function createSearchSetEntry(id: string, nctId?: string, score?: number): SearchBundleEntry { + const result: SearchBundleEntry = { + resource: createResearchStudy(id, nctId), + search: { + mode: 'match', + score: score || 0 + } + }; + return result; +} diff --git a/src/clinicaltrialsgov.ts b/src/clinicaltrialsgov.ts index 8208e85..0b78540 100644 --- a/src/clinicaltrialsgov.ts +++ b/src/clinicaltrialsgov.ts @@ -18,6 +18,7 @@ import { debuglog } from 'util'; import { ResearchStudy } from 'fhir/r4'; +import { SearchBundleEntry as SearchSetEntry } from './searchset'; import { ClinicalTrialsGovAPI, Study } from './clinicaltrialsgov-api'; import { updateResearchStudyWithClinicalStudy } from './study-fhir-converter'; import * as sqlite from 'sqlite'; @@ -490,6 +491,21 @@ export class ClinicalTrialsGovService { } } + async updateSearchSetEntries(entries: SearchSetEntry[]): Promise { + const studies: ResearchStudy[] = entries.map((item) => item.resource as ResearchStudy); + + 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); + }) + ); + + return entries; + } + /** * Tells the cache to delete all expired cached files. Currently this does nothing - entries never expire. It may * make sense to clean up the database every once and a while, but for now, this is a no-op.