@erosolaraijs/cure 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/dist/bin/cure.d.ts +10 -0
- package/dist/bin/cure.d.ts.map +1 -0
- package/dist/bin/cure.js +169 -0
- package/dist/bin/cure.js.map +1 -0
- package/dist/capabilities/cancerTreatmentCapability.d.ts +167 -0
- package/dist/capabilities/cancerTreatmentCapability.d.ts.map +1 -0
- package/dist/capabilities/cancerTreatmentCapability.js +912 -0
- package/dist/capabilities/cancerTreatmentCapability.js.map +1 -0
- package/dist/capabilities/index.d.ts +2 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +3 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/compliance/hipaa.d.ts +337 -0
- package/dist/compliance/hipaa.d.ts.map +1 -0
- package/dist/compliance/hipaa.js +929 -0
- package/dist/compliance/hipaa.js.map +1 -0
- package/dist/examples/cancerTreatmentDemo.d.ts +21 -0
- package/dist/examples/cancerTreatmentDemo.d.ts.map +1 -0
- package/dist/examples/cancerTreatmentDemo.js +216 -0
- package/dist/examples/cancerTreatmentDemo.js.map +1 -0
- package/dist/integrations/clinicalTrials/clinicalTrialsGov.d.ts +265 -0
- package/dist/integrations/clinicalTrials/clinicalTrialsGov.d.ts.map +1 -0
- package/dist/integrations/clinicalTrials/clinicalTrialsGov.js +808 -0
- package/dist/integrations/clinicalTrials/clinicalTrialsGov.js.map +1 -0
- package/dist/integrations/ehr/fhir.d.ts +455 -0
- package/dist/integrations/ehr/fhir.d.ts.map +1 -0
- package/dist/integrations/ehr/fhir.js +859 -0
- package/dist/integrations/ehr/fhir.js.map +1 -0
- package/dist/integrations/genomics/genomicPlatforms.d.ts +362 -0
- package/dist/integrations/genomics/genomicPlatforms.d.ts.map +1 -0
- package/dist/integrations/genomics/genomicPlatforms.js +1079 -0
- package/dist/integrations/genomics/genomicPlatforms.js.map +1 -0
- package/package.json +52 -0
- package/src/bin/cure.ts +182 -0
- package/src/capabilities/cancerTreatmentCapability.ts +1161 -0
- package/src/capabilities/index.ts +2 -0
- package/src/compliance/hipaa.ts +1365 -0
- package/src/examples/cancerTreatmentDemo.ts +241 -0
- package/src/integrations/clinicalTrials/clinicalTrialsGov.ts +1143 -0
- package/src/integrations/ehr/fhir.ts +1304 -0
- package/src/integrations/genomics/genomicPlatforms.ts +1480 -0
- package/src/ml/outcomePredictor.ts +1301 -0
- package/src/safety/drugInteractions.ts +942 -0
- package/src/validation/retrospectiveValidator.ts +887 -0
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClinicalTrials.gov Integration
|
|
3
|
+
*
|
|
4
|
+
* Provides real-time access to clinical trial data from ClinicalTrials.gov
|
|
5
|
+
* using the official API v2. Enables matching patients to eligible trials
|
|
6
|
+
* based on their cancer type, biomarkers, and treatment history.
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
10
|
+
// CLINICAL TRIALS API CLIENT
|
|
11
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
12
|
+
export class ClinicalTrialsGovClient extends EventEmitter {
|
|
13
|
+
baseUrl = 'https://clinicaltrials.gov/api/v2';
|
|
14
|
+
timeout;
|
|
15
|
+
cache = new Map();
|
|
16
|
+
cacheDuration; // minutes
|
|
17
|
+
constructor(options) {
|
|
18
|
+
super();
|
|
19
|
+
this.timeout = options?.timeout || 30000;
|
|
20
|
+
this.cacheDuration = options?.cacheDuration || 60; // 1 hour default
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Search for clinical trials
|
|
24
|
+
*/
|
|
25
|
+
async searchTrials(params) {
|
|
26
|
+
const cacheKey = JSON.stringify(params);
|
|
27
|
+
const cached = this.getFromCache(cacheKey);
|
|
28
|
+
if (cached)
|
|
29
|
+
return cached;
|
|
30
|
+
const query = this.buildSearchQuery(params);
|
|
31
|
+
const url = `${this.baseUrl}/studies?${query}`;
|
|
32
|
+
const response = await this.httpRequest(url);
|
|
33
|
+
const data = JSON.parse(response);
|
|
34
|
+
const result = {
|
|
35
|
+
trials: (data.studies || []).map((s) => this.mapStudyToTrial(s)),
|
|
36
|
+
totalCount: data.totalCount || 0,
|
|
37
|
+
nextPageToken: data.nextPageToken
|
|
38
|
+
};
|
|
39
|
+
this.setCache(cacheKey, result);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get a specific trial by NCT ID
|
|
44
|
+
*/
|
|
45
|
+
async getTrial(nctId) {
|
|
46
|
+
const cacheKey = `trial:${nctId}`;
|
|
47
|
+
const cached = this.getFromCache(cacheKey);
|
|
48
|
+
if (cached)
|
|
49
|
+
return cached;
|
|
50
|
+
const url = `${this.baseUrl}/studies/${nctId}`;
|
|
51
|
+
const response = await this.httpRequest(url);
|
|
52
|
+
const data = JSON.parse(response);
|
|
53
|
+
const trial = this.mapStudyToTrial(data);
|
|
54
|
+
this.setCache(cacheKey, trial);
|
|
55
|
+
return trial;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get multiple trials by NCT IDs
|
|
59
|
+
*/
|
|
60
|
+
async getTrials(nctIds) {
|
|
61
|
+
return Promise.all(nctIds.map(id => this.getTrial(id)));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Search for trials matching a patient profile
|
|
65
|
+
*/
|
|
66
|
+
async findMatchingTrials(patient) {
|
|
67
|
+
// Build search query based on patient profile
|
|
68
|
+
const searchParams = {
|
|
69
|
+
condition: patient.cancerType,
|
|
70
|
+
status: ['recruiting', 'enrolling-by-invitation', 'not-yet-recruiting'],
|
|
71
|
+
studyType: 'interventional',
|
|
72
|
+
age: patient.age,
|
|
73
|
+
gender: patient.gender,
|
|
74
|
+
pageSize: 100
|
|
75
|
+
};
|
|
76
|
+
// Add location-based filtering
|
|
77
|
+
if (patient.location?.zipCode) {
|
|
78
|
+
searchParams.zipCode = patient.location.zipCode;
|
|
79
|
+
searchParams.distance = patient.maxTravelDistance || 100;
|
|
80
|
+
}
|
|
81
|
+
else if (patient.location?.country) {
|
|
82
|
+
searchParams.country = patient.location.country;
|
|
83
|
+
if (patient.location.state) {
|
|
84
|
+
searchParams.state = patient.location.state;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Add biomarker-specific searches
|
|
88
|
+
if (patient.genomicAlterations && patient.genomicAlterations.length > 0) {
|
|
89
|
+
searchParams.genomicAlterations = patient.genomicAlterations.map(g => `${g.gene} ${g.alteration}`);
|
|
90
|
+
}
|
|
91
|
+
// Execute search
|
|
92
|
+
const searchResult = await this.searchTrials(searchParams);
|
|
93
|
+
// Score and filter trials
|
|
94
|
+
const matches = [];
|
|
95
|
+
for (const trial of searchResult.trials) {
|
|
96
|
+
const match = this.assessTrialMatch(trial, patient);
|
|
97
|
+
if (match.matchScore > 0) {
|
|
98
|
+
matches.push(match);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Sort by match score
|
|
102
|
+
matches.sort((a, b) => b.matchScore - a.matchScore);
|
|
103
|
+
return matches;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Search for trials by biomarker
|
|
107
|
+
*/
|
|
108
|
+
async searchByBiomarker(biomarker, options) {
|
|
109
|
+
const params = {
|
|
110
|
+
biomarkers: [biomarker],
|
|
111
|
+
condition: options?.cancerType,
|
|
112
|
+
status: options?.status || ['recruiting', 'not-yet-recruiting'],
|
|
113
|
+
phase: options?.phase,
|
|
114
|
+
studyType: 'interventional',
|
|
115
|
+
pageSize: 50
|
|
116
|
+
};
|
|
117
|
+
const result = await this.searchTrials(params);
|
|
118
|
+
return result.trials;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Search for trials by drug/intervention
|
|
122
|
+
*/
|
|
123
|
+
async searchByDrug(drugName, options) {
|
|
124
|
+
const params = {
|
|
125
|
+
drugName,
|
|
126
|
+
condition: options?.cancerType,
|
|
127
|
+
status: options?.status || ['recruiting', 'not-yet-recruiting'],
|
|
128
|
+
phase: options?.phase,
|
|
129
|
+
studyType: 'interventional',
|
|
130
|
+
pageSize: 50
|
|
131
|
+
};
|
|
132
|
+
const result = await this.searchTrials(params);
|
|
133
|
+
return result.trials;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get trials for a specific cancer type
|
|
137
|
+
*/
|
|
138
|
+
async getTrialsForCancerType(cancerType, options) {
|
|
139
|
+
const params = {
|
|
140
|
+
condition: cancerType,
|
|
141
|
+
status: ['recruiting', 'not-yet-recruiting', 'enrolling-by-invitation'],
|
|
142
|
+
studyType: 'interventional',
|
|
143
|
+
phase: options?.phase,
|
|
144
|
+
country: options?.location?.country,
|
|
145
|
+
state: options?.location?.state,
|
|
146
|
+
biomarkers: options?.biomarkers,
|
|
147
|
+
pageSize: 100
|
|
148
|
+
};
|
|
149
|
+
const result = await this.searchTrials(params);
|
|
150
|
+
return result.trials;
|
|
151
|
+
}
|
|
152
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
153
|
+
// HELPER METHODS
|
|
154
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
155
|
+
buildSearchQuery(params) {
|
|
156
|
+
const queryParts = [];
|
|
157
|
+
// Condition/disease
|
|
158
|
+
if (params.condition) {
|
|
159
|
+
queryParts.push(`query.cond=${encodeURIComponent(params.condition)}`);
|
|
160
|
+
}
|
|
161
|
+
if (params.conditionTerms && params.conditionTerms.length > 0) {
|
|
162
|
+
queryParts.push(`query.term=${encodeURIComponent(params.conditionTerms.join(' OR '))}`);
|
|
163
|
+
}
|
|
164
|
+
// Intervention/drug
|
|
165
|
+
if (params.intervention) {
|
|
166
|
+
queryParts.push(`query.intr=${encodeURIComponent(params.intervention)}`);
|
|
167
|
+
}
|
|
168
|
+
if (params.drugName) {
|
|
169
|
+
queryParts.push(`query.intr=${encodeURIComponent(params.drugName)}`);
|
|
170
|
+
}
|
|
171
|
+
// Biomarkers/genomic alterations (search in full text)
|
|
172
|
+
if (params.biomarkers && params.biomarkers.length > 0) {
|
|
173
|
+
const biomarkerQuery = params.biomarkers.join(' OR ');
|
|
174
|
+
queryParts.push(`query.term=${encodeURIComponent(biomarkerQuery)}`);
|
|
175
|
+
}
|
|
176
|
+
if (params.genomicAlterations && params.genomicAlterations.length > 0) {
|
|
177
|
+
const genomicQuery = params.genomicAlterations.join(' OR ');
|
|
178
|
+
queryParts.push(`query.term=${encodeURIComponent(genomicQuery)}`);
|
|
179
|
+
}
|
|
180
|
+
// Status filter
|
|
181
|
+
if (params.status && params.status.length > 0) {
|
|
182
|
+
const statusMap = {
|
|
183
|
+
'not-yet-recruiting': 'NOT_YET_RECRUITING',
|
|
184
|
+
'recruiting': 'RECRUITING',
|
|
185
|
+
'enrolling-by-invitation': 'ENROLLING_BY_INVITATION',
|
|
186
|
+
'active-not-recruiting': 'ACTIVE_NOT_RECRUITING',
|
|
187
|
+
'suspended': 'SUSPENDED',
|
|
188
|
+
'terminated': 'TERMINATED',
|
|
189
|
+
'completed': 'COMPLETED',
|
|
190
|
+
'withdrawn': 'WITHDRAWN',
|
|
191
|
+
'unknown': 'UNKNOWN'
|
|
192
|
+
};
|
|
193
|
+
const statuses = params.status.map(s => statusMap[s]).join(',');
|
|
194
|
+
queryParts.push(`filter.overallStatus=${statuses}`);
|
|
195
|
+
}
|
|
196
|
+
// Phase filter
|
|
197
|
+
if (params.phase && params.phase.length > 0) {
|
|
198
|
+
const phaseMap = {
|
|
199
|
+
'early-phase-1': 'EARLY_PHASE1',
|
|
200
|
+
'phase-1': 'PHASE1',
|
|
201
|
+
'phase-1-2': 'PHASE1_PHASE2',
|
|
202
|
+
'phase-2': 'PHASE2',
|
|
203
|
+
'phase-2-3': 'PHASE2_PHASE3',
|
|
204
|
+
'phase-3': 'PHASE3',
|
|
205
|
+
'phase-4': 'PHASE4',
|
|
206
|
+
'not-applicable': 'NA'
|
|
207
|
+
};
|
|
208
|
+
const phases = params.phase.map(p => phaseMap[p]).join(',');
|
|
209
|
+
queryParts.push(`filter.phase=${phases}`);
|
|
210
|
+
}
|
|
211
|
+
// Study type
|
|
212
|
+
if (params.studyType) {
|
|
213
|
+
const studyTypeMap = {
|
|
214
|
+
'interventional': 'INTERVENTIONAL',
|
|
215
|
+
'observational': 'OBSERVATIONAL',
|
|
216
|
+
'expanded-access': 'EXPANDED_ACCESS'
|
|
217
|
+
};
|
|
218
|
+
queryParts.push(`filter.studyType=${studyTypeMap[params.studyType]}`);
|
|
219
|
+
}
|
|
220
|
+
// Location filters
|
|
221
|
+
if (params.country) {
|
|
222
|
+
queryParts.push(`query.locn=${encodeURIComponent(params.country)}`);
|
|
223
|
+
}
|
|
224
|
+
if (params.state) {
|
|
225
|
+
queryParts.push(`query.locn=${encodeURIComponent(params.state)}`);
|
|
226
|
+
}
|
|
227
|
+
if (params.city) {
|
|
228
|
+
queryParts.push(`query.locn=${encodeURIComponent(params.city)}`);
|
|
229
|
+
}
|
|
230
|
+
// Geographic search (if zip code and distance provided)
|
|
231
|
+
if (params.zipCode && params.distance) {
|
|
232
|
+
queryParts.push(`postFilter.geo=distance(${params.zipCode},${params.distance}mi)`);
|
|
233
|
+
}
|
|
234
|
+
// Age filter
|
|
235
|
+
if (params.age) {
|
|
236
|
+
queryParts.push(`aggFilters=ages:adult`); // Simplified - would need more logic
|
|
237
|
+
}
|
|
238
|
+
// Gender filter
|
|
239
|
+
if (params.gender) {
|
|
240
|
+
queryParts.push(`filter.sex=${params.gender.toUpperCase()}`);
|
|
241
|
+
}
|
|
242
|
+
// Results filter
|
|
243
|
+
if (params.hasResults !== undefined) {
|
|
244
|
+
queryParts.push(`filter.results=${params.hasResults}`);
|
|
245
|
+
}
|
|
246
|
+
// Pagination
|
|
247
|
+
queryParts.push(`pageSize=${params.pageSize || 20}`);
|
|
248
|
+
if (params.pageToken) {
|
|
249
|
+
queryParts.push(`pageToken=${params.pageToken}`);
|
|
250
|
+
}
|
|
251
|
+
// Request all needed fields
|
|
252
|
+
queryParts.push('fields=NCTId,BriefTitle,OfficialTitle,OverallStatus,Phase,StudyType,Condition,' +
|
|
253
|
+
'InterventionName,InterventionType,InterventionDescription,EligibilityCriteria,' +
|
|
254
|
+
'MinimumAge,MaximumAge,Gender,LocationFacility,LocationCity,LocationState,LocationCountry,' +
|
|
255
|
+
'LeadSponsorName,StartDate,PrimaryCompletionDate,EnrollmentCount,ArmGroupLabel,' +
|
|
256
|
+
'ArmGroupType,PrimaryOutcomeMeasure,SecondaryOutcomeMeasure,CentralContactName,' +
|
|
257
|
+
'CentralContactPhone,CentralContactEMail,BriefSummary,DetailedDescription');
|
|
258
|
+
return queryParts.join('&');
|
|
259
|
+
}
|
|
260
|
+
mapStudyToTrial(study) {
|
|
261
|
+
const protocol = study.protocolSection || study;
|
|
262
|
+
const identification = protocol.identificationModule || {};
|
|
263
|
+
const status = protocol.statusModule || {};
|
|
264
|
+
const description = protocol.descriptionModule || {};
|
|
265
|
+
const conditions = protocol.conditionsModule || {};
|
|
266
|
+
const design = protocol.designModule || {};
|
|
267
|
+
const arms = protocol.armsInterventionsModule || {};
|
|
268
|
+
const eligibility = protocol.eligibilityModule || {};
|
|
269
|
+
const contacts = protocol.contactsLocationsModule || {};
|
|
270
|
+
const sponsor = protocol.sponsorCollaboratorsModule || {};
|
|
271
|
+
const outcomes = protocol.outcomesModule || {};
|
|
272
|
+
// Map interventions
|
|
273
|
+
const interventions = (arms.interventions || []).map((i) => ({
|
|
274
|
+
type: this.mapInterventionType(i.type),
|
|
275
|
+
name: i.name,
|
|
276
|
+
description: i.description,
|
|
277
|
+
armGroupLabels: i.armGroupLabels,
|
|
278
|
+
otherNames: i.otherNames
|
|
279
|
+
}));
|
|
280
|
+
// Map locations
|
|
281
|
+
const locations = (contacts.locations || []).map((loc) => ({
|
|
282
|
+
facility: loc.facility,
|
|
283
|
+
city: loc.city,
|
|
284
|
+
state: loc.state,
|
|
285
|
+
country: loc.country,
|
|
286
|
+
zip: loc.zip,
|
|
287
|
+
status: this.mapLocationStatus(loc.status),
|
|
288
|
+
contact: loc.contacts?.[0] ? {
|
|
289
|
+
name: loc.contacts[0].name,
|
|
290
|
+
phone: loc.contacts[0].phone,
|
|
291
|
+
email: loc.contacts[0].email
|
|
292
|
+
} : undefined,
|
|
293
|
+
coordinates: loc.geoPoint ? {
|
|
294
|
+
latitude: loc.geoPoint.lat,
|
|
295
|
+
longitude: loc.geoPoint.lon
|
|
296
|
+
} : undefined
|
|
297
|
+
}));
|
|
298
|
+
// Map sponsors
|
|
299
|
+
const sponsors = [];
|
|
300
|
+
if (sponsor.leadSponsor) {
|
|
301
|
+
sponsors.push({
|
|
302
|
+
name: sponsor.leadSponsor.name,
|
|
303
|
+
type: 'sponsor',
|
|
304
|
+
leadOrCollaborator: 'lead'
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
for (const collab of sponsor.collaborators || []) {
|
|
308
|
+
sponsors.push({
|
|
309
|
+
name: collab.name,
|
|
310
|
+
type: 'sponsor',
|
|
311
|
+
leadOrCollaborator: 'collaborator'
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
// Map contacts
|
|
315
|
+
const trialContacts = (contacts.centralContacts || []).map((c) => ({
|
|
316
|
+
name: c.name,
|
|
317
|
+
phone: c.phone,
|
|
318
|
+
email: c.email,
|
|
319
|
+
role: c.role
|
|
320
|
+
}));
|
|
321
|
+
// Map arms
|
|
322
|
+
const trialArms = (arms.armGroups || []).map((arm) => ({
|
|
323
|
+
label: arm.label,
|
|
324
|
+
type: this.mapArmType(arm.type),
|
|
325
|
+
description: arm.description,
|
|
326
|
+
interventions: arm.interventionNames
|
|
327
|
+
}));
|
|
328
|
+
// Map outcomes
|
|
329
|
+
const trialOutcomes = [
|
|
330
|
+
...(outcomes.primaryOutcomes || []).map((o) => ({
|
|
331
|
+
type: 'primary',
|
|
332
|
+
measure: o.measure,
|
|
333
|
+
description: o.description,
|
|
334
|
+
timeFrame: o.timeFrame
|
|
335
|
+
})),
|
|
336
|
+
...(outcomes.secondaryOutcomes || []).map((o) => ({
|
|
337
|
+
type: 'secondary',
|
|
338
|
+
measure: o.measure,
|
|
339
|
+
description: o.description,
|
|
340
|
+
timeFrame: o.timeFrame
|
|
341
|
+
}))
|
|
342
|
+
];
|
|
343
|
+
// Parse eligibility criteria into structured format
|
|
344
|
+
const criteriaText = eligibility.eligibilityCriteria || '';
|
|
345
|
+
const { inclusion, exclusion } = this.parseEligibilityCriteria(criteriaText);
|
|
346
|
+
// Extract biomarker requirements from criteria
|
|
347
|
+
const biomarkerRequirements = this.extractBiomarkerRequirements(criteriaText, interventions);
|
|
348
|
+
return {
|
|
349
|
+
nctId: identification.nctId,
|
|
350
|
+
title: identification.briefTitle || identification.officialTitle || '',
|
|
351
|
+
briefTitle: identification.briefTitle,
|
|
352
|
+
officialTitle: identification.officialTitle,
|
|
353
|
+
status: this.mapTrialStatus(status.overallStatus),
|
|
354
|
+
phase: this.mapTrialPhase(design.phases?.[0]),
|
|
355
|
+
studyType: this.mapStudyType(design.studyType),
|
|
356
|
+
conditions: conditions.conditions || [],
|
|
357
|
+
interventions,
|
|
358
|
+
eligibility: {
|
|
359
|
+
criteria: criteriaText,
|
|
360
|
+
gender: this.mapGender(eligibility.sex),
|
|
361
|
+
minimumAge: eligibility.minimumAge,
|
|
362
|
+
maximumAge: eligibility.maximumAge,
|
|
363
|
+
healthyVolunteers: eligibility.healthyVolunteers === 'Yes',
|
|
364
|
+
inclusionCriteria: inclusion,
|
|
365
|
+
exclusionCriteria: exclusion
|
|
366
|
+
},
|
|
367
|
+
locations,
|
|
368
|
+
sponsors,
|
|
369
|
+
contacts: trialContacts,
|
|
370
|
+
dates: {
|
|
371
|
+
startDate: status.startDateStruct ? new Date(status.startDateStruct.date) : undefined,
|
|
372
|
+
primaryCompletionDate: status.primaryCompletionDateStruct ? new Date(status.primaryCompletionDateStruct.date) : undefined,
|
|
373
|
+
completionDate: status.completionDateStruct ? new Date(status.completionDateStruct.date) : undefined,
|
|
374
|
+
firstPostedDate: status.studyFirstPostDateStruct ? new Date(status.studyFirstPostDateStruct.date) : undefined,
|
|
375
|
+
lastUpdatePostedDate: status.lastUpdatePostDateStruct ? new Date(status.lastUpdatePostDateStruct.date) : undefined
|
|
376
|
+
},
|
|
377
|
+
enrollment: design.enrollmentInfo ? {
|
|
378
|
+
count: design.enrollmentInfo.count,
|
|
379
|
+
type: design.enrollmentInfo.type?.toLowerCase()
|
|
380
|
+
} : undefined,
|
|
381
|
+
arms: trialArms.length > 0 ? trialArms : undefined,
|
|
382
|
+
outcomes: trialOutcomes.length > 0 ? trialOutcomes : undefined,
|
|
383
|
+
biomarkerRequirements: biomarkerRequirements.length > 0 ? biomarkerRequirements : undefined,
|
|
384
|
+
url: `https://clinicaltrials.gov/study/${identification.nctId}`
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
mapTrialStatus(status) {
|
|
388
|
+
const statusMap = {
|
|
389
|
+
'NOT_YET_RECRUITING': 'not-yet-recruiting',
|
|
390
|
+
'RECRUITING': 'recruiting',
|
|
391
|
+
'ENROLLING_BY_INVITATION': 'enrolling-by-invitation',
|
|
392
|
+
'ACTIVE_NOT_RECRUITING': 'active-not-recruiting',
|
|
393
|
+
'SUSPENDED': 'suspended',
|
|
394
|
+
'TERMINATED': 'terminated',
|
|
395
|
+
'COMPLETED': 'completed',
|
|
396
|
+
'WITHDRAWN': 'withdrawn'
|
|
397
|
+
};
|
|
398
|
+
return statusMap[status] || 'unknown';
|
|
399
|
+
}
|
|
400
|
+
mapTrialPhase(phase) {
|
|
401
|
+
const phaseMap = {
|
|
402
|
+
'EARLY_PHASE1': 'early-phase-1',
|
|
403
|
+
'PHASE1': 'phase-1',
|
|
404
|
+
'PHASE1_PHASE2': 'phase-1-2',
|
|
405
|
+
'PHASE2': 'phase-2',
|
|
406
|
+
'PHASE2_PHASE3': 'phase-2-3',
|
|
407
|
+
'PHASE3': 'phase-3',
|
|
408
|
+
'PHASE4': 'phase-4',
|
|
409
|
+
'NA': 'not-applicable'
|
|
410
|
+
};
|
|
411
|
+
return phaseMap[phase] || 'not-applicable';
|
|
412
|
+
}
|
|
413
|
+
mapStudyType(type) {
|
|
414
|
+
const typeMap = {
|
|
415
|
+
'INTERVENTIONAL': 'interventional',
|
|
416
|
+
'OBSERVATIONAL': 'observational',
|
|
417
|
+
'EXPANDED_ACCESS': 'expanded-access'
|
|
418
|
+
};
|
|
419
|
+
return typeMap[type] || 'interventional';
|
|
420
|
+
}
|
|
421
|
+
mapInterventionType(type) {
|
|
422
|
+
const typeMap = {
|
|
423
|
+
'DRUG': 'drug',
|
|
424
|
+
'BIOLOGICAL': 'biological',
|
|
425
|
+
'DEVICE': 'device',
|
|
426
|
+
'PROCEDURE': 'procedure',
|
|
427
|
+
'RADIATION': 'radiation',
|
|
428
|
+
'BEHAVIORAL': 'behavioral',
|
|
429
|
+
'GENETIC': 'genetic',
|
|
430
|
+
'DIETARY_SUPPLEMENT': 'dietary',
|
|
431
|
+
'COMBINATION_PRODUCT': 'combination',
|
|
432
|
+
'OTHER': 'other'
|
|
433
|
+
};
|
|
434
|
+
return typeMap[type] || 'other';
|
|
435
|
+
}
|
|
436
|
+
mapLocationStatus(status) {
|
|
437
|
+
const statusMap = {
|
|
438
|
+
'RECRUITING': 'recruiting',
|
|
439
|
+
'NOT_YET_RECRUITING': 'not-recruiting',
|
|
440
|
+
'ACTIVE_NOT_RECRUITING': 'active',
|
|
441
|
+
'COMPLETED': 'completed',
|
|
442
|
+
'WITHDRAWN': 'withdrawn'
|
|
443
|
+
};
|
|
444
|
+
return statusMap[status] || 'active';
|
|
445
|
+
}
|
|
446
|
+
mapArmType(type) {
|
|
447
|
+
const typeMap = {
|
|
448
|
+
'EXPERIMENTAL': 'experimental',
|
|
449
|
+
'ACTIVE_COMPARATOR': 'active-comparator',
|
|
450
|
+
'PLACEBO_COMPARATOR': 'placebo-comparator',
|
|
451
|
+
'SHAM_COMPARATOR': 'sham-comparator',
|
|
452
|
+
'NO_INTERVENTION': 'no-intervention',
|
|
453
|
+
'OTHER': 'other'
|
|
454
|
+
};
|
|
455
|
+
return typeMap[type] || 'other';
|
|
456
|
+
}
|
|
457
|
+
mapGender(sex) {
|
|
458
|
+
if (sex === 'FEMALE')
|
|
459
|
+
return 'female';
|
|
460
|
+
if (sex === 'MALE')
|
|
461
|
+
return 'male';
|
|
462
|
+
return 'all';
|
|
463
|
+
}
|
|
464
|
+
parseEligibilityCriteria(criteria) {
|
|
465
|
+
const inclusion = [];
|
|
466
|
+
const exclusion = [];
|
|
467
|
+
if (!criteria)
|
|
468
|
+
return { inclusion, exclusion };
|
|
469
|
+
// Try to split by Inclusion/Exclusion headers
|
|
470
|
+
const sections = criteria.split(/(?:Inclusion|Exclusion)\s*Criteria:?/i);
|
|
471
|
+
// Simple parsing - look for patterns
|
|
472
|
+
const lines = criteria.split(/\n|•|·|-\s+|\*\s+|\d+\.\s+/);
|
|
473
|
+
let inExclusion = false;
|
|
474
|
+
for (const line of lines) {
|
|
475
|
+
const trimmed = line.trim();
|
|
476
|
+
if (!trimmed)
|
|
477
|
+
continue;
|
|
478
|
+
// Detect section changes
|
|
479
|
+
if (trimmed.toLowerCase().includes('exclusion')) {
|
|
480
|
+
inExclusion = true;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (trimmed.toLowerCase().includes('inclusion')) {
|
|
484
|
+
inExclusion = false;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
// Add to appropriate list
|
|
488
|
+
if (trimmed.length > 10) { // Filter out very short fragments
|
|
489
|
+
if (inExclusion) {
|
|
490
|
+
exclusion.push(trimmed);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
inclusion.push(trimmed);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return { inclusion, exclusion };
|
|
498
|
+
}
|
|
499
|
+
extractBiomarkerRequirements(criteria, interventions) {
|
|
500
|
+
const requirements = [];
|
|
501
|
+
const criteriaLower = criteria.toLowerCase();
|
|
502
|
+
// Common biomarker patterns
|
|
503
|
+
const biomarkerPatterns = [
|
|
504
|
+
{ pattern: /egfr\s*(mutation|mutant|positive|\+)/i, biomarker: 'EGFR mutation' },
|
|
505
|
+
{ pattern: /egfr\s*(wild[- ]?type|negative|wt)/i, biomarker: 'EGFR wild-type' },
|
|
506
|
+
{ pattern: /alk\s*(rearrangement|fusion|positive|\+)/i, biomarker: 'ALK fusion' },
|
|
507
|
+
{ pattern: /ros1\s*(rearrangement|fusion|positive|\+)/i, biomarker: 'ROS1 fusion' },
|
|
508
|
+
{ pattern: /braf\s*v600[ek]?/i, biomarker: 'BRAF V600' },
|
|
509
|
+
{ pattern: /kras\s*g12c/i, biomarker: 'KRAS G12C' },
|
|
510
|
+
{ pattern: /kras\s*(mutation|mutant|positive)/i, biomarker: 'KRAS mutation' },
|
|
511
|
+
{ pattern: /her2\s*(positive|overexpression|amplification|\+|3\+)/i, biomarker: 'HER2 positive' },
|
|
512
|
+
{ pattern: /her2\s*(negative|\-|0|1\+)/i, biomarker: 'HER2 negative' },
|
|
513
|
+
{ pattern: /brca[12]?\s*(mutation|mutant|positive|pathogenic)/i, biomarker: 'BRCA mutation' },
|
|
514
|
+
{ pattern: /msi[- ]?h(igh)?|microsatellite\s*instability[- ]?high/i, biomarker: 'MSI-H' },
|
|
515
|
+
{ pattern: /mss|microsatellite\s*stable/i, biomarker: 'MSS' },
|
|
516
|
+
{ pattern: /pd[- ]?l1\s*(positive|expression|tps|cps)/i, biomarker: 'PD-L1 positive' },
|
|
517
|
+
{ pattern: /pd[- ]?l1\s*[\u2265>=]\s*(\d+)/i, biomarker: 'PD-L1' },
|
|
518
|
+
{ pattern: /tmb[- ]?h(igh)?|tumor\s*mutational\s*burden[- ]?high/i, biomarker: 'TMB-H' },
|
|
519
|
+
{ pattern: /hrd\s*(positive|deficient)/i, biomarker: 'HRD positive' },
|
|
520
|
+
{ pattern: /ntrk\s*(fusion|rearrangement)/i, biomarker: 'NTRK fusion' },
|
|
521
|
+
{ pattern: /ret\s*(fusion|rearrangement|mutation)/i, biomarker: 'RET alteration' },
|
|
522
|
+
{ pattern: /met\s*(exon\s*14|amplification)/i, biomarker: 'MET alteration' },
|
|
523
|
+
{ pattern: /fgfr[1234]?\s*(alteration|fusion|mutation|amplification)/i, biomarker: 'FGFR alteration' },
|
|
524
|
+
{ pattern: /pik3ca\s*(mutation|mutant)/i, biomarker: 'PIK3CA mutation' },
|
|
525
|
+
{ pattern: /idh[12]\s*(mutation|mutant)/i, biomarker: 'IDH mutation' }
|
|
526
|
+
];
|
|
527
|
+
for (const { pattern, biomarker } of biomarkerPatterns) {
|
|
528
|
+
if (pattern.test(criteria)) {
|
|
529
|
+
// Determine if it's required or excluded based on context
|
|
530
|
+
const match = criteria.match(new RegExp(`.{0,50}${pattern.source}.{0,50}`, 'i'));
|
|
531
|
+
if (match) {
|
|
532
|
+
const context = match[0].toLowerCase();
|
|
533
|
+
let requirement = 'required';
|
|
534
|
+
if (context.includes('exclud') || context.includes('must not') ||
|
|
535
|
+
context.includes('no ') || context.includes('without') ||
|
|
536
|
+
context.includes('ineligible')) {
|
|
537
|
+
requirement = 'excluded';
|
|
538
|
+
}
|
|
539
|
+
requirements.push({
|
|
540
|
+
biomarker,
|
|
541
|
+
requirement
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return requirements;
|
|
547
|
+
}
|
|
548
|
+
assessTrialMatch(trial, patient) {
|
|
549
|
+
let score = 0;
|
|
550
|
+
const matchReasons = [];
|
|
551
|
+
const matchingCriteria = [];
|
|
552
|
+
const potentialExclusions = [];
|
|
553
|
+
const missingInformation = [];
|
|
554
|
+
const biomarkerMatches = [];
|
|
555
|
+
// Check cancer type match
|
|
556
|
+
const cancerMatch = trial.conditions.some(c => c.toLowerCase().includes(patient.cancerType.toLowerCase()) ||
|
|
557
|
+
patient.cancerType.toLowerCase().includes(c.toLowerCase()));
|
|
558
|
+
if (cancerMatch) {
|
|
559
|
+
score += 30;
|
|
560
|
+
matchReasons.push(`Cancer type matches: ${patient.cancerType}`);
|
|
561
|
+
matchingCriteria.push('Cancer type');
|
|
562
|
+
}
|
|
563
|
+
// Check biomarker requirements
|
|
564
|
+
if (trial.biomarkerRequirements && patient.genomicAlterations) {
|
|
565
|
+
for (const req of trial.biomarkerRequirements) {
|
|
566
|
+
const patientHasBiomarker = patient.genomicAlterations.some(g => req.biomarker.toLowerCase().includes(g.gene.toLowerCase()) ||
|
|
567
|
+
req.biomarker.toLowerCase().includes(g.alteration.toLowerCase()));
|
|
568
|
+
if (req.requirement === 'required') {
|
|
569
|
+
if (patientHasBiomarker) {
|
|
570
|
+
score += 25;
|
|
571
|
+
matchReasons.push(`Has required biomarker: ${req.biomarker}`);
|
|
572
|
+
matchingCriteria.push(req.biomarker);
|
|
573
|
+
biomarkerMatches.push({
|
|
574
|
+
biomarker: req.biomarker,
|
|
575
|
+
trialRequirement: 'Required',
|
|
576
|
+
patientValue: 'Present',
|
|
577
|
+
match: true
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
score -= 10;
|
|
582
|
+
potentialExclusions.push(`Missing required biomarker: ${req.biomarker}`);
|
|
583
|
+
biomarkerMatches.push({
|
|
584
|
+
biomarker: req.biomarker,
|
|
585
|
+
trialRequirement: 'Required',
|
|
586
|
+
patientValue: 'Not detected',
|
|
587
|
+
match: false
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
else if (req.requirement === 'excluded') {
|
|
592
|
+
if (patientHasBiomarker) {
|
|
593
|
+
score -= 50;
|
|
594
|
+
potentialExclusions.push(`Has excluded biomarker: ${req.biomarker}`);
|
|
595
|
+
biomarkerMatches.push({
|
|
596
|
+
biomarker: req.biomarker,
|
|
597
|
+
trialRequirement: 'Excluded',
|
|
598
|
+
patientValue: 'Present',
|
|
599
|
+
match: false
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// Check MSI status
|
|
606
|
+
if (patient.msiStatus) {
|
|
607
|
+
const trialMentionsMSI = trial.eligibility.criteria.toLowerCase().includes('msi');
|
|
608
|
+
if (trialMentionsMSI) {
|
|
609
|
+
if (patient.msiStatus === 'MSI-H' && trial.eligibility.criteria.toLowerCase().includes('msi-h')) {
|
|
610
|
+
score += 20;
|
|
611
|
+
matchReasons.push('MSI-H status matches trial requirement');
|
|
612
|
+
matchingCriteria.push('MSI-H');
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
// Check TMB status
|
|
617
|
+
if (patient.tmbLevel === 'high') {
|
|
618
|
+
const trialMentionsTMB = trial.eligibility.criteria.toLowerCase().includes('tmb');
|
|
619
|
+
if (trialMentionsTMB) {
|
|
620
|
+
score += 15;
|
|
621
|
+
matchReasons.push('TMB-High may enhance eligibility');
|
|
622
|
+
matchingCriteria.push('TMB-H');
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// Check age eligibility
|
|
626
|
+
if (patient.age) {
|
|
627
|
+
let ageEligible = true;
|
|
628
|
+
if (trial.eligibility.minimumAge) {
|
|
629
|
+
const minAge = parseInt(trial.eligibility.minimumAge);
|
|
630
|
+
if (!isNaN(minAge) && patient.age < minAge) {
|
|
631
|
+
ageEligible = false;
|
|
632
|
+
potentialExclusions.push(`Below minimum age (${trial.eligibility.minimumAge})`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (trial.eligibility.maximumAge && trial.eligibility.maximumAge !== 'N/A') {
|
|
636
|
+
const maxAge = parseInt(trial.eligibility.maximumAge);
|
|
637
|
+
if (!isNaN(maxAge) && patient.age > maxAge) {
|
|
638
|
+
ageEligible = false;
|
|
639
|
+
potentialExclusions.push(`Above maximum age (${trial.eligibility.maximumAge})`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (ageEligible) {
|
|
643
|
+
score += 5;
|
|
644
|
+
matchingCriteria.push('Age');
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
score -= 30;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
missingInformation.push('Patient age');
|
|
652
|
+
}
|
|
653
|
+
// Check gender eligibility
|
|
654
|
+
if (patient.gender) {
|
|
655
|
+
if (trial.eligibility.gender === 'all' || trial.eligibility.gender === patient.gender) {
|
|
656
|
+
matchingCriteria.push('Gender');
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
score -= 50;
|
|
660
|
+
potentialExclusions.push(`Trial only accepts ${trial.eligibility.gender} patients`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// Check prior therapy exclusions
|
|
664
|
+
if (patient.priorTherapies && patient.priorTherapies.length > 0) {
|
|
665
|
+
const criteriaLower = trial.eligibility.criteria.toLowerCase();
|
|
666
|
+
for (const therapy of patient.priorTherapies) {
|
|
667
|
+
if (criteriaLower.includes(`no prior ${therapy.toLowerCase()}`) ||
|
|
668
|
+
criteriaLower.includes(`not received ${therapy.toLowerCase()}`)) {
|
|
669
|
+
potentialExclusions.push(`Prior ${therapy} may exclude patient`);
|
|
670
|
+
score -= 10;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// Check ECOG status
|
|
675
|
+
if (patient.ecogStatus !== undefined) {
|
|
676
|
+
const ecogMatch = trial.eligibility.criteria.match(/ecog\s*(?:performance\s*status)?\s*(?:of\s*)?(\d)(?:\s*(?:or|to|-)\s*(\d))?/i);
|
|
677
|
+
if (ecogMatch) {
|
|
678
|
+
const maxEcog = parseInt(ecogMatch[2] || ecogMatch[1]);
|
|
679
|
+
if (patient.ecogStatus <= maxEcog) {
|
|
680
|
+
matchingCriteria.push(`ECOG ${patient.ecogStatus}`);
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
potentialExclusions.push(`ECOG ${patient.ecogStatus} may be too high (trial requires ≤${maxEcog})`);
|
|
684
|
+
score -= 20;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
missingInformation.push('ECOG performance status');
|
|
690
|
+
}
|
|
691
|
+
// Boost score for phase based on patient preference
|
|
692
|
+
if (trial.phase === 'phase-3') {
|
|
693
|
+
score += 10;
|
|
694
|
+
matchReasons.push('Phase 3 trial (more established efficacy data)');
|
|
695
|
+
}
|
|
696
|
+
else if (trial.phase === 'phase-2') {
|
|
697
|
+
score += 5;
|
|
698
|
+
}
|
|
699
|
+
// Find nearest location
|
|
700
|
+
let nearestLocation;
|
|
701
|
+
if (patient.location) {
|
|
702
|
+
const recruitingLocations = trial.locations.filter(l => l.status === 'recruiting');
|
|
703
|
+
if (patient.location.coordinates && recruitingLocations.some(l => l.coordinates)) {
|
|
704
|
+
// Calculate distances
|
|
705
|
+
for (const loc of recruitingLocations) {
|
|
706
|
+
if (loc.coordinates) {
|
|
707
|
+
loc.distance = this.calculateDistance(patient.location.coordinates.lat, patient.location.coordinates.lon, loc.coordinates.latitude, loc.coordinates.longitude);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
recruitingLocations.sort((a, b) => (a.distance || 999999) - (b.distance || 999999));
|
|
711
|
+
nearestLocation = recruitingLocations[0];
|
|
712
|
+
if (nearestLocation?.distance && patient.maxTravelDistance) {
|
|
713
|
+
if (nearestLocation.distance <= patient.maxTravelDistance) {
|
|
714
|
+
score += 10;
|
|
715
|
+
matchReasons.push(`Trial site within ${Math.round(nearestLocation.distance)} miles`);
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
score -= 5;
|
|
719
|
+
matchReasons.push(`Nearest site is ${Math.round(nearestLocation.distance)} miles away`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
else if (patient.location.state) {
|
|
724
|
+
nearestLocation = recruitingLocations.find(l => l.state?.toLowerCase() === patient.location?.state?.toLowerCase());
|
|
725
|
+
if (nearestLocation) {
|
|
726
|
+
score += 5;
|
|
727
|
+
matchReasons.push(`Trial site in ${patient.location.state}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
// Determine eligibility status
|
|
732
|
+
let eligibilityStatus;
|
|
733
|
+
if (score >= 50 && potentialExclusions.length === 0) {
|
|
734
|
+
eligibilityStatus = 'likely-eligible';
|
|
735
|
+
}
|
|
736
|
+
else if (score >= 30 && potentialExclusions.length <= 1) {
|
|
737
|
+
eligibilityStatus = 'possibly-eligible';
|
|
738
|
+
}
|
|
739
|
+
else if (score < 0 || potentialExclusions.length >= 3) {
|
|
740
|
+
eligibilityStatus = 'likely-ineligible';
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
eligibilityStatus = 'unknown';
|
|
744
|
+
}
|
|
745
|
+
// Ensure minimum score of 0
|
|
746
|
+
score = Math.max(0, score);
|
|
747
|
+
return {
|
|
748
|
+
trial,
|
|
749
|
+
matchScore: score,
|
|
750
|
+
matchReasons,
|
|
751
|
+
eligibilityAssessment: {
|
|
752
|
+
status: eligibilityStatus,
|
|
753
|
+
matchingCriteria,
|
|
754
|
+
potentialExclusions,
|
|
755
|
+
missingInformation
|
|
756
|
+
},
|
|
757
|
+
nearestLocation,
|
|
758
|
+
biomarkerMatches: biomarkerMatches.length > 0 ? biomarkerMatches : undefined
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
calculateDistance(lat1, lon1, lat2, lon2) {
|
|
762
|
+
// Haversine formula to calculate distance between two points
|
|
763
|
+
const R = 3959; // Earth's radius in miles
|
|
764
|
+
const dLat = this.toRadians(lat2 - lat1);
|
|
765
|
+
const dLon = this.toRadians(lon2 - lon1);
|
|
766
|
+
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
767
|
+
Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) *
|
|
768
|
+
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
|
769
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
770
|
+
return R * c;
|
|
771
|
+
}
|
|
772
|
+
toRadians(degrees) {
|
|
773
|
+
return degrees * (Math.PI / 180);
|
|
774
|
+
}
|
|
775
|
+
async httpRequest(url) {
|
|
776
|
+
const response = await fetch(url, {
|
|
777
|
+
method: 'GET',
|
|
778
|
+
headers: {
|
|
779
|
+
'Accept': 'application/json'
|
|
780
|
+
},
|
|
781
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
782
|
+
});
|
|
783
|
+
if (!response.ok) {
|
|
784
|
+
throw new Error(`ClinicalTrials.gov API error: ${response.status} ${response.statusText}`);
|
|
785
|
+
}
|
|
786
|
+
return await response.text();
|
|
787
|
+
}
|
|
788
|
+
getFromCache(key) {
|
|
789
|
+
const cached = this.cache.get(key);
|
|
790
|
+
if (cached && cached.expiry > new Date()) {
|
|
791
|
+
return cached.data;
|
|
792
|
+
}
|
|
793
|
+
this.cache.delete(key);
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
setCache(key, data) {
|
|
797
|
+
const expiry = new Date(Date.now() + this.cacheDuration * 60 * 1000);
|
|
798
|
+
this.cache.set(key, { data, expiry });
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Clear the cache
|
|
802
|
+
*/
|
|
803
|
+
clearCache() {
|
|
804
|
+
this.cache.clear();
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
export default ClinicalTrialsGovClient;
|
|
808
|
+
//# sourceMappingURL=clinicalTrialsGov.js.map
|