Skip to content

Commit

Permalink
Merge pull request #44 from mcode/searchset-update
Browse files Browse the repository at this point in the history
Add in ability to take in take in SearchSet entries.
  • Loading branch information
zlister authored Feb 6, 2024
2 parents 321ecc6 + f7349a6 commit 2872f1e
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 3 deletions.
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.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",
Expand Down
78 changes: 77 additions & 1 deletion spec/clinicaltrialsgov.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 () => {
Expand Down
14 changes: 13 additions & 1 deletion spec/support/researchstudy-factory.ts
Original file line number Diff line number Diff line change
@@ -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 = [
{
Expand Down Expand Up @@ -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;
}
16 changes: 16 additions & 0 deletions src/clinicaltrialsgov.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -490,6 +491,21 @@ export class ClinicalTrialsGovService {
}
}

async updateSearchSetEntries(entries: SearchSetEntry[]): Promise<SearchSetEntry[]> {
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.
Expand Down

0 comments on commit 2872f1e

Please sign in to comment.