@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,859 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HL7 FHIR Integration Layer
|
|
3
|
+
*
|
|
4
|
+
* Connects to Electronic Health Record systems using the FHIR R4 standard.
|
|
5
|
+
* Supports Epic, Cerner, AllScripts, and other FHIR-compliant EHR systems.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: This module requires proper HIPAA compliance setup before use.
|
|
8
|
+
* All PHI access must be logged and patient consent must be verified.
|
|
9
|
+
*/
|
|
10
|
+
import { EventEmitter } from 'events';
|
|
11
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
12
|
+
// FHIR CLIENT IMPLEMENTATION
|
|
13
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
14
|
+
export class FHIRClient extends EventEmitter {
|
|
15
|
+
config;
|
|
16
|
+
accessToken;
|
|
17
|
+
tokenExpiry;
|
|
18
|
+
auditLogger;
|
|
19
|
+
constructor(config) {
|
|
20
|
+
super();
|
|
21
|
+
this.config = {
|
|
22
|
+
timeout: 30000,
|
|
23
|
+
retryAttempts: 3,
|
|
24
|
+
...config
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Set audit logger for HIPAA compliance
|
|
29
|
+
*/
|
|
30
|
+
setAuditLogger(logger) {
|
|
31
|
+
this.auditLogger = logger;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Authenticate with the FHIR server
|
|
35
|
+
*/
|
|
36
|
+
async authenticate() {
|
|
37
|
+
const startTime = Date.now();
|
|
38
|
+
try {
|
|
39
|
+
switch (this.config.authType) {
|
|
40
|
+
case 'smart-on-fhir':
|
|
41
|
+
await this.smartOnFhirAuth();
|
|
42
|
+
break;
|
|
43
|
+
case 'client-credentials':
|
|
44
|
+
await this.clientCredentialsAuth();
|
|
45
|
+
break;
|
|
46
|
+
case 'oauth2':
|
|
47
|
+
await this.oauth2Auth();
|
|
48
|
+
break;
|
|
49
|
+
case 'basic':
|
|
50
|
+
// Basic auth doesn't require pre-authentication
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
throw new Error(`Unsupported auth type: ${this.config.authType}`);
|
|
54
|
+
}
|
|
55
|
+
await this.logAudit({
|
|
56
|
+
action: 'authenticate',
|
|
57
|
+
resourceType: 'System',
|
|
58
|
+
outcome: 'success',
|
|
59
|
+
duration: Date.now() - startTime
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
await this.logAudit({
|
|
64
|
+
action: 'authenticate',
|
|
65
|
+
resourceType: 'System',
|
|
66
|
+
outcome: 'failure',
|
|
67
|
+
duration: Date.now() - startTime,
|
|
68
|
+
errorMessage: error instanceof Error ? error.message : 'Unknown error'
|
|
69
|
+
});
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async smartOnFhirAuth() {
|
|
74
|
+
// SMART on FHIR authentication flow
|
|
75
|
+
// This is a simplified version - production would need full OAuth2 flow
|
|
76
|
+
const tokenEndpoint = await this.discoverTokenEndpoint();
|
|
77
|
+
const response = await this.httpRequest(tokenEndpoint, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
81
|
+
},
|
|
82
|
+
body: new URLSearchParams({
|
|
83
|
+
grant_type: 'client_credentials',
|
|
84
|
+
client_id: this.config.clientId,
|
|
85
|
+
client_secret: this.config.clientSecret || '',
|
|
86
|
+
scope: this.config.scopes.join(' ')
|
|
87
|
+
}).toString()
|
|
88
|
+
});
|
|
89
|
+
const data = JSON.parse(response);
|
|
90
|
+
this.accessToken = data.access_token;
|
|
91
|
+
this.tokenExpiry = new Date(Date.now() + (data.expires_in * 1000));
|
|
92
|
+
}
|
|
93
|
+
async clientCredentialsAuth() {
|
|
94
|
+
const tokenEndpoint = `${this.config.baseUrl}/oauth2/token`;
|
|
95
|
+
const response = await this.httpRequest(tokenEndpoint, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: {
|
|
98
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
99
|
+
'Authorization': `Basic ${Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64')}`
|
|
100
|
+
},
|
|
101
|
+
body: new URLSearchParams({
|
|
102
|
+
grant_type: 'client_credentials',
|
|
103
|
+
scope: this.config.scopes.join(' ')
|
|
104
|
+
}).toString()
|
|
105
|
+
});
|
|
106
|
+
const data = JSON.parse(response);
|
|
107
|
+
this.accessToken = data.access_token;
|
|
108
|
+
this.tokenExpiry = new Date(Date.now() + (data.expires_in * 1000));
|
|
109
|
+
}
|
|
110
|
+
async oauth2Auth() {
|
|
111
|
+
// OAuth2 flow - similar to client credentials for server-to-server
|
|
112
|
+
await this.clientCredentialsAuth();
|
|
113
|
+
}
|
|
114
|
+
async discoverTokenEndpoint() {
|
|
115
|
+
const wellKnown = `${this.config.baseUrl}/.well-known/smart-configuration`;
|
|
116
|
+
try {
|
|
117
|
+
const response = await this.httpRequest(wellKnown, { method: 'GET' });
|
|
118
|
+
const config = JSON.parse(response);
|
|
119
|
+
return config.token_endpoint;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Fall back to standard OAuth2 endpoint
|
|
123
|
+
return `${this.config.baseUrl}/oauth2/token`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get a patient by ID
|
|
128
|
+
*/
|
|
129
|
+
async getPatient(patientId) {
|
|
130
|
+
return await this.read('Patient', patientId);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Search for patients
|
|
134
|
+
*/
|
|
135
|
+
async searchPatients(params) {
|
|
136
|
+
return await this.search('Patient', params);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get cancer diagnosis for a patient
|
|
140
|
+
*/
|
|
141
|
+
async getCancerDiagnosis(patientId) {
|
|
142
|
+
const bundle = await this.search('Condition', {
|
|
143
|
+
patient: patientId,
|
|
144
|
+
category: 'encounter-diagnosis',
|
|
145
|
+
'code:below': '363346000' // SNOMED CT code for malignant neoplasm
|
|
146
|
+
});
|
|
147
|
+
const diagnoses = [];
|
|
148
|
+
for (const entry of bundle.entry || []) {
|
|
149
|
+
const condition = entry.resource;
|
|
150
|
+
if (condition.resourceType === 'Condition') {
|
|
151
|
+
diagnoses.push(this.mapConditionToCancerDiagnosis(condition, patientId));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return diagnoses;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Get biomarkers and genomic data for a patient
|
|
158
|
+
*/
|
|
159
|
+
async getBiomarkers(patientId) {
|
|
160
|
+
// Get lab observations
|
|
161
|
+
const labBundle = await this.search('Observation', {
|
|
162
|
+
patient: patientId,
|
|
163
|
+
category: 'laboratory',
|
|
164
|
+
_sort: '-date',
|
|
165
|
+
_count: '100'
|
|
166
|
+
});
|
|
167
|
+
// Get genomic observations
|
|
168
|
+
const genomicBundle = await this.search('Observation', {
|
|
169
|
+
patient: patientId,
|
|
170
|
+
category: 'genomic-variant',
|
|
171
|
+
_sort: '-date',
|
|
172
|
+
_count: '100'
|
|
173
|
+
});
|
|
174
|
+
// Get diagnostic reports (for NGS results)
|
|
175
|
+
const reportBundle = await this.search('DiagnosticReport', {
|
|
176
|
+
patient: patientId,
|
|
177
|
+
category: 'GE', // Genetics
|
|
178
|
+
_sort: '-date',
|
|
179
|
+
_count: '50'
|
|
180
|
+
});
|
|
181
|
+
return this.aggregateBiomarkerData(patientId, labBundle, genomicBundle, reportBundle);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get treatment history for a patient
|
|
185
|
+
*/
|
|
186
|
+
async getTreatmentHistory(patientId) {
|
|
187
|
+
const [medications, procedures] = await Promise.all([
|
|
188
|
+
this.search('MedicationRequest', {
|
|
189
|
+
patient: patientId,
|
|
190
|
+
_sort: '-authoredon',
|
|
191
|
+
_count: '200'
|
|
192
|
+
}),
|
|
193
|
+
this.search('Procedure', {
|
|
194
|
+
patient: patientId,
|
|
195
|
+
_sort: '-date',
|
|
196
|
+
_count: '200'
|
|
197
|
+
})
|
|
198
|
+
]);
|
|
199
|
+
return this.aggregateTreatmentHistory(patientId, medications, procedures);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get all observations for a patient
|
|
203
|
+
*/
|
|
204
|
+
async getObservations(patientId, category) {
|
|
205
|
+
const params = {
|
|
206
|
+
patient: patientId,
|
|
207
|
+
_sort: '-date',
|
|
208
|
+
_count: '100'
|
|
209
|
+
};
|
|
210
|
+
if (category) {
|
|
211
|
+
params.category = category;
|
|
212
|
+
}
|
|
213
|
+
const bundle = await this.search('Observation', params);
|
|
214
|
+
return (bundle.entry || [])
|
|
215
|
+
.map(e => e.resource)
|
|
216
|
+
.filter((r) => r?.resourceType === 'Observation');
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Get all medications for a patient
|
|
220
|
+
*/
|
|
221
|
+
async getMedications(patientId) {
|
|
222
|
+
const bundle = await this.search('MedicationRequest', {
|
|
223
|
+
patient: patientId,
|
|
224
|
+
_sort: '-authoredon'
|
|
225
|
+
});
|
|
226
|
+
return (bundle.entry || [])
|
|
227
|
+
.map(e => e.resource)
|
|
228
|
+
.filter((r) => r?.resourceType === 'MedicationRequest');
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get diagnostic reports for a patient
|
|
232
|
+
*/
|
|
233
|
+
async getDiagnosticReports(patientId) {
|
|
234
|
+
const bundle = await this.search('DiagnosticReport', {
|
|
235
|
+
patient: patientId,
|
|
236
|
+
_sort: '-date'
|
|
237
|
+
});
|
|
238
|
+
return (bundle.entry || [])
|
|
239
|
+
.map(e => e.resource)
|
|
240
|
+
.filter((r) => r?.resourceType === 'DiagnosticReport');
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Create a comprehensive cancer patient summary
|
|
244
|
+
*/
|
|
245
|
+
async getComprehensiveCancerSummary(patientId) {
|
|
246
|
+
const [patient, diagnoses, biomarkers, treatmentHistory, recentLabs] = await Promise.all([
|
|
247
|
+
this.getPatient(patientId),
|
|
248
|
+
this.getCancerDiagnosis(patientId),
|
|
249
|
+
this.getBiomarkers(patientId),
|
|
250
|
+
this.getTreatmentHistory(patientId),
|
|
251
|
+
this.getObservations(patientId, 'laboratory')
|
|
252
|
+
]);
|
|
253
|
+
// Get performance status (ECOG/KPS)
|
|
254
|
+
const performanceObs = await this.search('Observation', {
|
|
255
|
+
patient: patientId,
|
|
256
|
+
code: '89247-1', // LOINC for ECOG performance status
|
|
257
|
+
_sort: '-date',
|
|
258
|
+
_count: '1'
|
|
259
|
+
});
|
|
260
|
+
let performanceStatus;
|
|
261
|
+
const perfEntry = performanceObs.entry?.[0]?.resource;
|
|
262
|
+
if (perfEntry?.valueQuantity?.value !== undefined) {
|
|
263
|
+
performanceStatus = {
|
|
264
|
+
score: perfEntry.valueQuantity.value,
|
|
265
|
+
scale: 'ECOG',
|
|
266
|
+
date: new Date(perfEntry.effectiveDateTime || Date.now())
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
patient,
|
|
271
|
+
diagnoses,
|
|
272
|
+
biomarkers,
|
|
273
|
+
treatmentHistory,
|
|
274
|
+
recentLabs: recentLabs.slice(0, 20),
|
|
275
|
+
performanceStatus
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
279
|
+
// CORE FHIR OPERATIONS
|
|
280
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
281
|
+
/**
|
|
282
|
+
* Read a resource by ID
|
|
283
|
+
*/
|
|
284
|
+
async read(resourceType, id) {
|
|
285
|
+
const url = `${this.config.baseUrl}/${resourceType}/${id}`;
|
|
286
|
+
const startTime = Date.now();
|
|
287
|
+
try {
|
|
288
|
+
const response = await this.httpRequest(url, {
|
|
289
|
+
method: 'GET',
|
|
290
|
+
headers: await this.getAuthHeaders()
|
|
291
|
+
});
|
|
292
|
+
await this.logAudit({
|
|
293
|
+
action: 'read',
|
|
294
|
+
resourceType,
|
|
295
|
+
resourceId: id,
|
|
296
|
+
outcome: 'success',
|
|
297
|
+
duration: Date.now() - startTime
|
|
298
|
+
});
|
|
299
|
+
return JSON.parse(response);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
await this.logAudit({
|
|
303
|
+
action: 'read',
|
|
304
|
+
resourceType,
|
|
305
|
+
resourceId: id,
|
|
306
|
+
outcome: 'failure',
|
|
307
|
+
duration: Date.now() - startTime,
|
|
308
|
+
errorMessage: error instanceof Error ? error.message : 'Unknown error'
|
|
309
|
+
});
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Search for resources
|
|
315
|
+
*/
|
|
316
|
+
async search(resourceType, params) {
|
|
317
|
+
const searchParams = new URLSearchParams(params);
|
|
318
|
+
const url = `${this.config.baseUrl}/${resourceType}?${searchParams.toString()}`;
|
|
319
|
+
const startTime = Date.now();
|
|
320
|
+
try {
|
|
321
|
+
const response = await this.httpRequest(url, {
|
|
322
|
+
method: 'GET',
|
|
323
|
+
headers: await this.getAuthHeaders()
|
|
324
|
+
});
|
|
325
|
+
await this.logAudit({
|
|
326
|
+
action: 'search',
|
|
327
|
+
resourceType,
|
|
328
|
+
outcome: 'success',
|
|
329
|
+
duration: Date.now() - startTime,
|
|
330
|
+
details: { searchParams: params }
|
|
331
|
+
});
|
|
332
|
+
return JSON.parse(response);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
await this.logAudit({
|
|
336
|
+
action: 'search',
|
|
337
|
+
resourceType,
|
|
338
|
+
outcome: 'failure',
|
|
339
|
+
duration: Date.now() - startTime,
|
|
340
|
+
errorMessage: error instanceof Error ? error.message : 'Unknown error'
|
|
341
|
+
});
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Create a resource
|
|
347
|
+
*/
|
|
348
|
+
async create(resource) {
|
|
349
|
+
const url = `${this.config.baseUrl}/${resource.resourceType}`;
|
|
350
|
+
const startTime = Date.now();
|
|
351
|
+
try {
|
|
352
|
+
const response = await this.httpRequest(url, {
|
|
353
|
+
method: 'POST',
|
|
354
|
+
headers: {
|
|
355
|
+
...await this.getAuthHeaders(),
|
|
356
|
+
'Content-Type': 'application/fhir+json'
|
|
357
|
+
},
|
|
358
|
+
body: JSON.stringify(resource)
|
|
359
|
+
});
|
|
360
|
+
await this.logAudit({
|
|
361
|
+
action: 'create',
|
|
362
|
+
resourceType: resource.resourceType,
|
|
363
|
+
outcome: 'success',
|
|
364
|
+
duration: Date.now() - startTime
|
|
365
|
+
});
|
|
366
|
+
return JSON.parse(response);
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
await this.logAudit({
|
|
370
|
+
action: 'create',
|
|
371
|
+
resourceType: resource.resourceType,
|
|
372
|
+
outcome: 'failure',
|
|
373
|
+
duration: Date.now() - startTime,
|
|
374
|
+
errorMessage: error instanceof Error ? error.message : 'Unknown error'
|
|
375
|
+
});
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Update a resource
|
|
381
|
+
*/
|
|
382
|
+
async update(resource) {
|
|
383
|
+
const url = `${this.config.baseUrl}/${resource.resourceType}/${resource.id}`;
|
|
384
|
+
const startTime = Date.now();
|
|
385
|
+
try {
|
|
386
|
+
const response = await this.httpRequest(url, {
|
|
387
|
+
method: 'PUT',
|
|
388
|
+
headers: {
|
|
389
|
+
...await this.getAuthHeaders(),
|
|
390
|
+
'Content-Type': 'application/fhir+json'
|
|
391
|
+
},
|
|
392
|
+
body: JSON.stringify(resource)
|
|
393
|
+
});
|
|
394
|
+
await this.logAudit({
|
|
395
|
+
action: 'update',
|
|
396
|
+
resourceType: resource.resourceType,
|
|
397
|
+
resourceId: resource.id,
|
|
398
|
+
outcome: 'success',
|
|
399
|
+
duration: Date.now() - startTime
|
|
400
|
+
});
|
|
401
|
+
return JSON.parse(response);
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
await this.logAudit({
|
|
405
|
+
action: 'update',
|
|
406
|
+
resourceType: resource.resourceType,
|
|
407
|
+
resourceId: resource.id,
|
|
408
|
+
outcome: 'failure',
|
|
409
|
+
duration: Date.now() - startTime,
|
|
410
|
+
errorMessage: error instanceof Error ? error.message : 'Unknown error'
|
|
411
|
+
});
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
416
|
+
// HELPER METHODS
|
|
417
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
418
|
+
async getAuthHeaders() {
|
|
419
|
+
// Check if token needs refresh
|
|
420
|
+
if (this.tokenExpiry && new Date() >= this.tokenExpiry) {
|
|
421
|
+
await this.authenticate();
|
|
422
|
+
}
|
|
423
|
+
const headers = {
|
|
424
|
+
'Accept': 'application/fhir+json'
|
|
425
|
+
};
|
|
426
|
+
if (this.accessToken) {
|
|
427
|
+
headers['Authorization'] = `Bearer ${this.accessToken}`;
|
|
428
|
+
}
|
|
429
|
+
else if (this.config.authType === 'basic') {
|
|
430
|
+
headers['Authorization'] = `Basic ${Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64')}`;
|
|
431
|
+
}
|
|
432
|
+
return headers;
|
|
433
|
+
}
|
|
434
|
+
async httpRequest(url, options) {
|
|
435
|
+
// Use native fetch in Node.js 18+
|
|
436
|
+
const response = await fetch(url, {
|
|
437
|
+
method: options.method,
|
|
438
|
+
headers: options.headers,
|
|
439
|
+
body: options.body,
|
|
440
|
+
signal: AbortSignal.timeout(this.config.timeout || 30000)
|
|
441
|
+
});
|
|
442
|
+
if (!response.ok) {
|
|
443
|
+
const errorBody = await response.text();
|
|
444
|
+
throw new Error(`FHIR request failed: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
445
|
+
}
|
|
446
|
+
return await response.text();
|
|
447
|
+
}
|
|
448
|
+
mapConditionToCancerDiagnosis(condition, patientId) {
|
|
449
|
+
const icdCoding = condition.code.coding?.find(c => c.system?.includes('icd-10') || c.system?.includes('icd-9'));
|
|
450
|
+
const snomedCoding = condition.code.coding?.find(c => c.system?.includes('snomed'));
|
|
451
|
+
const stageInfo = condition.stage?.[0];
|
|
452
|
+
let stage;
|
|
453
|
+
if (stageInfo?.summary) {
|
|
454
|
+
const stageCode = stageInfo.summary.coding?.[0]?.code || stageInfo.summary.text || '';
|
|
455
|
+
stage = {
|
|
456
|
+
system: this.determineStageSystem(stageCode),
|
|
457
|
+
stage: stageCode
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
patientId,
|
|
462
|
+
conditionId: condition.id,
|
|
463
|
+
cancerType: condition.code.text || condition.code.coding?.[0]?.display || 'Unknown',
|
|
464
|
+
icdCode: icdCoding?.code || '',
|
|
465
|
+
snomedCode: snomedCoding?.code,
|
|
466
|
+
primarySite: condition.bodySite?.[0]?.text || condition.bodySite?.[0]?.coding?.[0]?.display,
|
|
467
|
+
stage,
|
|
468
|
+
diagnosisDate: new Date(condition.onsetDateTime || condition.recordedDate || Date.now()),
|
|
469
|
+
verificationStatus: this.mapVerificationStatus(condition.verificationStatus)
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
determineStageSystem(stageCode) {
|
|
473
|
+
const code = stageCode.toUpperCase();
|
|
474
|
+
if (code.includes('AJCC') || /^[IV]+[ABC]?$/.test(code) || code.includes('TNM')) {
|
|
475
|
+
return 'ajcc';
|
|
476
|
+
}
|
|
477
|
+
if (code.includes('FIGO'))
|
|
478
|
+
return 'figo';
|
|
479
|
+
if (code.includes('RAI'))
|
|
480
|
+
return 'rai';
|
|
481
|
+
if (code.includes('BINET'))
|
|
482
|
+
return 'binet';
|
|
483
|
+
if (code.includes('ISS'))
|
|
484
|
+
return 'iss';
|
|
485
|
+
return 'other';
|
|
486
|
+
}
|
|
487
|
+
mapVerificationStatus(status) {
|
|
488
|
+
const code = status.coding?.[0]?.code?.toLowerCase() || '';
|
|
489
|
+
if (code.includes('confirmed'))
|
|
490
|
+
return 'confirmed';
|
|
491
|
+
if (code.includes('provisional'))
|
|
492
|
+
return 'provisional';
|
|
493
|
+
if (code.includes('differential'))
|
|
494
|
+
return 'differential';
|
|
495
|
+
if (code.includes('refuted'))
|
|
496
|
+
return 'refuted';
|
|
497
|
+
return 'confirmed';
|
|
498
|
+
}
|
|
499
|
+
aggregateBiomarkerData(patientId, labBundle, genomicBundle, reportBundle) {
|
|
500
|
+
const biomarkers = [];
|
|
501
|
+
const genomicAlterations = [];
|
|
502
|
+
let latestDate = new Date(0);
|
|
503
|
+
// Process lab observations
|
|
504
|
+
for (const entry of labBundle.entry || []) {
|
|
505
|
+
const obs = entry.resource;
|
|
506
|
+
if (obs.resourceType !== 'Observation')
|
|
507
|
+
continue;
|
|
508
|
+
const date = new Date(obs.effectiveDateTime || Date.now());
|
|
509
|
+
if (date > latestDate)
|
|
510
|
+
latestDate = date;
|
|
511
|
+
// Map common cancer biomarkers
|
|
512
|
+
const biomarker = this.mapObservationToBiomarker(obs);
|
|
513
|
+
if (biomarker)
|
|
514
|
+
biomarkers.push(biomarker);
|
|
515
|
+
}
|
|
516
|
+
// Process genomic observations
|
|
517
|
+
for (const entry of genomicBundle.entry || []) {
|
|
518
|
+
const obs = entry.resource;
|
|
519
|
+
if (obs.resourceType !== 'Observation')
|
|
520
|
+
continue;
|
|
521
|
+
const date = new Date(obs.effectiveDateTime || Date.now());
|
|
522
|
+
if (date > latestDate)
|
|
523
|
+
latestDate = date;
|
|
524
|
+
const alteration = this.mapObservationToGenomicAlteration(obs);
|
|
525
|
+
if (alteration)
|
|
526
|
+
genomicAlterations.push(alteration);
|
|
527
|
+
}
|
|
528
|
+
// Process diagnostic reports for additional genomic data
|
|
529
|
+
for (const entry of reportBundle.entry || []) {
|
|
530
|
+
const report = entry.resource;
|
|
531
|
+
if (report.resourceType !== 'DiagnosticReport')
|
|
532
|
+
continue;
|
|
533
|
+
const date = new Date(report.effectiveDateTime || Date.now());
|
|
534
|
+
if (date > latestDate)
|
|
535
|
+
latestDate = date;
|
|
536
|
+
}
|
|
537
|
+
if (biomarkers.length === 0 && genomicAlterations.length === 0) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
// Extract special biomarkers
|
|
541
|
+
const tmbObs = this.findBiomarker(biomarkers, ['TMB', 'tumor mutational burden']);
|
|
542
|
+
const msiObs = this.findBiomarker(biomarkers, ['MSI', 'microsatellite']);
|
|
543
|
+
const pdl1Obs = this.findBiomarker(biomarkers, ['PD-L1', 'PDL1']);
|
|
544
|
+
const hrdObs = this.findBiomarker(biomarkers, ['HRD', 'homologous recombination']);
|
|
545
|
+
return {
|
|
546
|
+
patientId,
|
|
547
|
+
collectionDate: latestDate,
|
|
548
|
+
biomarkers,
|
|
549
|
+
genomicAlterations: genomicAlterations.length > 0 ? genomicAlterations : undefined,
|
|
550
|
+
tumorMutationalBurden: tmbObs ? {
|
|
551
|
+
value: typeof tmbObs.value === 'number' ? tmbObs.value : parseFloat(String(tmbObs.value)) || 0,
|
|
552
|
+
unit: 'mutations/Mb',
|
|
553
|
+
status: this.categorizeTMB(tmbObs.value)
|
|
554
|
+
} : undefined,
|
|
555
|
+
microsatelliteInstability: msiObs ? {
|
|
556
|
+
status: this.categorizeMSI(msiObs.value),
|
|
557
|
+
method: 'NGS'
|
|
558
|
+
} : undefined,
|
|
559
|
+
pdl1Expression: pdl1Obs ? {
|
|
560
|
+
score: typeof pdl1Obs.value === 'number' ? pdl1Obs.value : parseFloat(String(pdl1Obs.value)) || 0,
|
|
561
|
+
scoreType: 'TPS'
|
|
562
|
+
} : undefined,
|
|
563
|
+
hrdStatus: hrdObs ? {
|
|
564
|
+
status: hrdObs.status === 'positive' ? 'positive' : 'negative',
|
|
565
|
+
score: typeof hrdObs.value === 'number' ? hrdObs.value : undefined
|
|
566
|
+
} : undefined
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
mapObservationToBiomarker(obs) {
|
|
570
|
+
const name = obs.code.text || obs.code.coding?.[0]?.display;
|
|
571
|
+
if (!name)
|
|
572
|
+
return null;
|
|
573
|
+
let value;
|
|
574
|
+
let status;
|
|
575
|
+
if (obs.valueQuantity?.value !== undefined) {
|
|
576
|
+
value = obs.valueQuantity.value;
|
|
577
|
+
status = 'positive'; // Will be refined based on interpretation
|
|
578
|
+
}
|
|
579
|
+
else if (obs.valueCodeableConcept) {
|
|
580
|
+
value = obs.valueCodeableConcept.text || obs.valueCodeableConcept.coding?.[0]?.display || '';
|
|
581
|
+
status = this.interpretBiomarkerStatus(value);
|
|
582
|
+
}
|
|
583
|
+
else if (obs.valueString) {
|
|
584
|
+
value = obs.valueString;
|
|
585
|
+
status = this.interpretBiomarkerStatus(value);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
// Check interpretation if available
|
|
591
|
+
if (obs.interpretation?.[0]?.coding?.[0]?.code) {
|
|
592
|
+
const interpCode = obs.interpretation[0].coding[0].code;
|
|
593
|
+
if (interpCode === 'POS' || interpCode === 'H')
|
|
594
|
+
status = 'positive';
|
|
595
|
+
else if (interpCode === 'NEG' || interpCode === 'N')
|
|
596
|
+
status = 'negative';
|
|
597
|
+
else if (interpCode === 'IND')
|
|
598
|
+
status = 'equivocal';
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
name,
|
|
602
|
+
value,
|
|
603
|
+
unit: obs.valueQuantity?.unit,
|
|
604
|
+
status,
|
|
605
|
+
loincCode: obs.code.coding?.find(c => c.system?.includes('loinc'))?.code
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
mapObservationToGenomicAlteration(obs) {
|
|
609
|
+
// This would need to be expanded to handle the full mCode/genomics-reporting IG
|
|
610
|
+
const geneComponent = obs.component?.find(c => c.code.coding?.some(coding => coding.code === '48018-6') // Gene studied
|
|
611
|
+
);
|
|
612
|
+
const variantComponent = obs.component?.find(c => c.code.coding?.some(coding => coding.code === '81252-9') // DNA change
|
|
613
|
+
);
|
|
614
|
+
if (!geneComponent || !variantComponent)
|
|
615
|
+
return null;
|
|
616
|
+
const gene = geneComponent.valueCodeableConcept?.text ||
|
|
617
|
+
geneComponent.valueCodeableConcept?.coding?.[0]?.display || '';
|
|
618
|
+
const alteration = variantComponent.valueString ||
|
|
619
|
+
variantComponent.valueCodeableConcept?.text || '';
|
|
620
|
+
if (!gene || !alteration)
|
|
621
|
+
return null;
|
|
622
|
+
return {
|
|
623
|
+
gene,
|
|
624
|
+
alteration,
|
|
625
|
+
type: this.determineAlterationType(alteration)
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
determineAlterationType(alteration) {
|
|
629
|
+
const lower = alteration.toLowerCase();
|
|
630
|
+
if (lower.includes('amp') || lower.includes('gain'))
|
|
631
|
+
return 'amplification';
|
|
632
|
+
if (lower.includes('del') || lower.includes('loss'))
|
|
633
|
+
return 'deletion';
|
|
634
|
+
if (lower.includes('fusion') || lower.includes('::'))
|
|
635
|
+
return 'fusion';
|
|
636
|
+
if (lower.includes('rearr') || lower.includes('transloc'))
|
|
637
|
+
return 'rearrangement';
|
|
638
|
+
return 'mutation';
|
|
639
|
+
}
|
|
640
|
+
interpretBiomarkerStatus(value) {
|
|
641
|
+
const lower = String(value).toLowerCase();
|
|
642
|
+
if (lower.includes('positive') || lower.includes('detected') || lower === 'yes')
|
|
643
|
+
return 'positive';
|
|
644
|
+
if (lower.includes('negative') || lower.includes('not detected') || lower === 'no')
|
|
645
|
+
return 'negative';
|
|
646
|
+
if (lower.includes('equivocal') || lower.includes('indeterminate') || lower.includes('borderline'))
|
|
647
|
+
return 'equivocal';
|
|
648
|
+
return 'positive'; // Default to positive if we have a value
|
|
649
|
+
}
|
|
650
|
+
findBiomarker(biomarkers, keywords) {
|
|
651
|
+
return biomarkers.find(b => keywords.some(k => b.name.toLowerCase().includes(k.toLowerCase())));
|
|
652
|
+
}
|
|
653
|
+
categorizeTMB(value) {
|
|
654
|
+
const numValue = typeof value === 'number' ? value : parseFloat(String(value));
|
|
655
|
+
if (isNaN(numValue))
|
|
656
|
+
return 'low';
|
|
657
|
+
if (numValue >= 10)
|
|
658
|
+
return 'high';
|
|
659
|
+
if (numValue >= 6)
|
|
660
|
+
return 'intermediate';
|
|
661
|
+
return 'low';
|
|
662
|
+
}
|
|
663
|
+
categorizeMSI(value) {
|
|
664
|
+
const strValue = String(value).toUpperCase();
|
|
665
|
+
if (strValue.includes('MSI-H') || strValue.includes('HIGH') || strValue.includes('UNSTABLE'))
|
|
666
|
+
return 'MSI-H';
|
|
667
|
+
if (strValue.includes('MSI-L') || strValue.includes('LOW'))
|
|
668
|
+
return 'MSI-L';
|
|
669
|
+
return 'MSS';
|
|
670
|
+
}
|
|
671
|
+
aggregateTreatmentHistory(patientId, medications, procedures) {
|
|
672
|
+
const treatments = [];
|
|
673
|
+
// Process medications
|
|
674
|
+
for (const entry of medications.entry || []) {
|
|
675
|
+
const med = entry.resource;
|
|
676
|
+
if (med.resourceType !== 'MedicationRequest')
|
|
677
|
+
continue;
|
|
678
|
+
const drugName = med.medicationCodeableConcept?.text ||
|
|
679
|
+
med.medicationCodeableConcept?.coding?.[0]?.display ||
|
|
680
|
+
'Unknown medication';
|
|
681
|
+
const treatment = this.categorizeTreatment(drugName);
|
|
682
|
+
treatments.push({
|
|
683
|
+
id: med.id,
|
|
684
|
+
type: treatment.type,
|
|
685
|
+
regimen: treatment.regimen,
|
|
686
|
+
drugs: [{
|
|
687
|
+
name: drugName,
|
|
688
|
+
dose: med.dosageInstruction?.[0]?.text,
|
|
689
|
+
route: med.dosageInstruction?.[0]?.route?.text,
|
|
690
|
+
rxNormCode: med.medicationCodeableConcept?.coding?.find(c => c.system?.includes('rxnorm'))?.code
|
|
691
|
+
}],
|
|
692
|
+
startDate: new Date(med.authoredOn || Date.now()),
|
|
693
|
+
status: this.mapMedicationStatus(med.status)
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
// Process procedures (surgery, radiation)
|
|
697
|
+
for (const entry of procedures.entry || []) {
|
|
698
|
+
const proc = entry.resource;
|
|
699
|
+
if (proc.resourceType !== 'Procedure')
|
|
700
|
+
continue;
|
|
701
|
+
const procName = proc.code.text || proc.code.coding?.[0]?.display || 'Unknown procedure';
|
|
702
|
+
const isSurgery = this.isSurgicalProcedure(procName);
|
|
703
|
+
const isRadiation = this.isRadiationProcedure(procName);
|
|
704
|
+
if (isSurgery || isRadiation) {
|
|
705
|
+
treatments.push({
|
|
706
|
+
id: proc.id,
|
|
707
|
+
type: isRadiation ? 'radiation' : 'surgery',
|
|
708
|
+
drugs: [],
|
|
709
|
+
startDate: new Date(proc.performedDateTime || proc.performedPeriod?.start || Date.now()),
|
|
710
|
+
endDate: proc.performedPeriod?.end ? new Date(proc.performedPeriod.end) : undefined,
|
|
711
|
+
status: this.mapProcedureStatus(proc.status)
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// Sort by date
|
|
716
|
+
treatments.sort((a, b) => b.startDate.getTime() - a.startDate.getTime());
|
|
717
|
+
return { patientId, treatments };
|
|
718
|
+
}
|
|
719
|
+
categorizeTreatment(drugName) {
|
|
720
|
+
const lower = drugName.toLowerCase();
|
|
721
|
+
// Immunotherapy
|
|
722
|
+
const immunotherapyDrugs = ['pembrolizumab', 'nivolumab', 'ipilimumab', 'atezolizumab', 'durvalumab',
|
|
723
|
+
'avelumab', 'cemiplimab', 'dostarlimab', 'relatlimab', 'tremelimumab'];
|
|
724
|
+
if (immunotherapyDrugs.some(d => lower.includes(d))) {
|
|
725
|
+
return { type: 'immunotherapy' };
|
|
726
|
+
}
|
|
727
|
+
// Targeted therapy
|
|
728
|
+
const targetedDrugs = ['imatinib', 'erlotinib', 'gefitinib', 'osimertinib', 'crizotinib', 'alectinib',
|
|
729
|
+
'palbociclib', 'ribociclib', 'abemaciclib', 'olaparib', 'rucaparib', 'niraparib', 'trastuzumab',
|
|
730
|
+
'pertuzumab', 'lapatinib', 'vemurafenib', 'dabrafenib', 'trametinib', 'sotorasib', 'adagrasib',
|
|
731
|
+
'venetoclax', 'ibrutinib', 'acalabrutinib', 'lenvatinib', 'sorafenib', 'regorafenib', 'cabozantinib',
|
|
732
|
+
'bevacizumab', 'cetuximab', 'panitumumab', 'rituximab'];
|
|
733
|
+
if (targetedDrugs.some(d => lower.includes(d))) {
|
|
734
|
+
return { type: 'targeted-therapy' };
|
|
735
|
+
}
|
|
736
|
+
// Hormone therapy
|
|
737
|
+
const hormoneDrugs = ['tamoxifen', 'letrozole', 'anastrozole', 'exemestane', 'fulvestrant',
|
|
738
|
+
'enzalutamide', 'abiraterone', 'apalutamide', 'darolutamide', 'lupron', 'leuprolide'];
|
|
739
|
+
if (hormoneDrugs.some(d => lower.includes(d))) {
|
|
740
|
+
return { type: 'hormone-therapy' };
|
|
741
|
+
}
|
|
742
|
+
// CAR-T
|
|
743
|
+
const carTDrugs = ['tisagenlecleucel', 'axicabtagene', 'brexucabtagene', 'lisocabtagene',
|
|
744
|
+
'idecabtagene', 'ciltacabtagene', 'kymriah', 'yescarta', 'tecartus', 'breyanzi', 'abecma', 'carvykti'];
|
|
745
|
+
if (carTDrugs.some(d => lower.includes(d))) {
|
|
746
|
+
return { type: 'car-t' };
|
|
747
|
+
}
|
|
748
|
+
// Chemotherapy (catch-all for cytotoxic agents)
|
|
749
|
+
const chemoDrugs = ['carboplatin', 'cisplatin', 'oxaliplatin', 'paclitaxel', 'docetaxel',
|
|
750
|
+
'doxorubicin', 'epirubicin', 'cyclophosphamide', 'fluorouracil', '5-fu', 'capecitabine',
|
|
751
|
+
'gemcitabine', 'pemetrexed', 'etoposide', 'irinotecan', 'vincristine', 'vinblastine',
|
|
752
|
+
'methotrexate', 'cytarabine', 'azacitidine', 'decitabine', 'temozolomide'];
|
|
753
|
+
if (chemoDrugs.some(d => lower.includes(d))) {
|
|
754
|
+
return { type: 'chemotherapy' };
|
|
755
|
+
}
|
|
756
|
+
return { type: 'other' };
|
|
757
|
+
}
|
|
758
|
+
mapMedicationStatus(status) {
|
|
759
|
+
switch (status) {
|
|
760
|
+
case 'active': return 'active';
|
|
761
|
+
case 'completed': return 'completed';
|
|
762
|
+
case 'stopped': return 'stopped';
|
|
763
|
+
case 'on-hold': return 'on-hold';
|
|
764
|
+
case 'draft': return 'planned';
|
|
765
|
+
default: return 'active';
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
mapProcedureStatus(status) {
|
|
769
|
+
switch (status) {
|
|
770
|
+
case 'completed': return 'completed';
|
|
771
|
+
case 'in-progress': return 'active';
|
|
772
|
+
case 'preparation': return 'planned';
|
|
773
|
+
case 'on-hold': return 'on-hold';
|
|
774
|
+
case 'stopped': return 'stopped';
|
|
775
|
+
default: return 'completed';
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
isSurgicalProcedure(name) {
|
|
779
|
+
const surgeryKeywords = ['surgery', 'resection', 'excision', 'mastectomy', 'lobectomy',
|
|
780
|
+
'colectomy', 'gastrectomy', 'prostatectomy', 'nephrectomy', 'hysterectomy',
|
|
781
|
+
'lymphadenectomy', 'biopsy', 'debulking', 'whipple', 'hepatectomy'];
|
|
782
|
+
return surgeryKeywords.some(k => name.toLowerCase().includes(k));
|
|
783
|
+
}
|
|
784
|
+
isRadiationProcedure(name) {
|
|
785
|
+
const radiationKeywords = ['radiation', 'radiotherapy', 'sbrt', 'imrt', 'proton',
|
|
786
|
+
'brachytherapy', 'cyberknife', 'gamma knife', 'external beam'];
|
|
787
|
+
return radiationKeywords.some(k => name.toLowerCase().includes(k));
|
|
788
|
+
}
|
|
789
|
+
async logAudit(event) {
|
|
790
|
+
if (!this.auditLogger)
|
|
791
|
+
return;
|
|
792
|
+
const fullEvent = {
|
|
793
|
+
timestamp: new Date(),
|
|
794
|
+
userId: 'system',
|
|
795
|
+
ipAddress: '0.0.0.0',
|
|
796
|
+
action: event.action || 'unknown',
|
|
797
|
+
resourceType: event.resourceType || 'unknown',
|
|
798
|
+
outcome: event.outcome || 'unknown',
|
|
799
|
+
...event
|
|
800
|
+
};
|
|
801
|
+
try {
|
|
802
|
+
await this.auditLogger(fullEvent);
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
console.error('Failed to log audit event:', error);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
810
|
+
// EHR VENDOR-SPECIFIC ADAPTERS
|
|
811
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
812
|
+
export class EpicFHIRClient extends FHIRClient {
|
|
813
|
+
constructor(config) {
|
|
814
|
+
super({ ...config, ehrVendor: 'epic' });
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Epic-specific: Get MyChart patient context
|
|
818
|
+
*/
|
|
819
|
+
async getMyChartContext(launchToken) {
|
|
820
|
+
// Epic SMART launch context parsing
|
|
821
|
+
const decoded = Buffer.from(launchToken, 'base64').toString();
|
|
822
|
+
return JSON.parse(decoded);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
export class CernerFHIRClient extends FHIRClient {
|
|
826
|
+
constructor(config) {
|
|
827
|
+
super({ ...config, ehrVendor: 'cerner' });
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Cerner-specific: Handle Millennium-specific extensions
|
|
831
|
+
*/
|
|
832
|
+
parseMillenniumExtensions(resource) {
|
|
833
|
+
const extensions = {};
|
|
834
|
+
if ('extension' in resource && resource.extension) {
|
|
835
|
+
for (const ext of resource.extension) {
|
|
836
|
+
if (ext.url.includes('cerner.com')) {
|
|
837
|
+
const key = ext.url.split('/').pop() || ext.url;
|
|
838
|
+
extensions[key] = ext.valueString || ext.valueCode || ext.valueBoolean || ext.valueCoding;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return extensions;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
846
|
+
// FACTORY FUNCTION
|
|
847
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
848
|
+
export function createFHIRClient(config) {
|
|
849
|
+
switch (config.ehrVendor) {
|
|
850
|
+
case 'epic':
|
|
851
|
+
return new EpicFHIRClient(config);
|
|
852
|
+
case 'cerner':
|
|
853
|
+
return new CernerFHIRClient(config);
|
|
854
|
+
default:
|
|
855
|
+
return new FHIRClient(config);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
export default FHIRClient;
|
|
859
|
+
//# sourceMappingURL=fhir.js.map
|