/** * HL7 FHIR Integration Layer * * Connects to Electronic Health Record systems using the FHIR R4 standard. * Supports Epic, Cerner, AllScripts, and other FHIR-compliant EHR systems. * * IMPORTANT: This module requires proper HIPAA compliance setup before use. * All PHI access must be logged and patient consent must be verified. */ import { EventEmitter } from 'events'; // ═══════════════════════════════════════════════════════════════════════════════ // FHIR R4 TYPE DEFINITIONS // ═══════════════════════════════════════════════════════════════════════════════ export interface FHIRConfig { baseUrl: string; clientId: string; clientSecret?: string; scopes: string[]; authType: 'smart-on-fhir' | 'basic' | 'oauth2' | 'client-credentials'; ehrVendor: 'epic' | 'cerner' | 'allscripts' | 'meditech' | 'generic'; timeout?: number; retryAttempts?: number; } export interface FHIRPatient { resourceType: 'Patient'; id: string; identifier: FHIRIdentifier[]; name: FHIRHumanName[]; gender: 'male' | 'female' | 'other' | 'unknown'; birthDate: string; address?: FHIRAddress[]; telecom?: FHIRContactPoint[]; maritalStatus?: FHIRCodeableConcept; communication?: { language: FHIRCodeableConcept; preferred?: boolean }[]; extension?: FHIRExtension[]; } export interface FHIRIdentifier { system: string; value: string; type?: FHIRCodeableConcept; use?: 'usual' | 'official' | 'temp' | 'secondary' | 'old'; } export interface FHIRHumanName { use?: 'usual' | 'official' | 'temp' | 'nickname' | 'anonymous' | 'old' | 'maiden'; family?: string; given?: string[]; prefix?: string[]; suffix?: string[]; } export interface FHIRAddress { use?: 'home' | 'work' | 'temp' | 'old' | 'billing'; type?: 'postal' | 'physical' | 'both'; line?: string[]; city?: string; state?: string; postalCode?: string; country?: string; } export interface FHIRContactPoint { system?: 'phone' | 'fax' | 'email' | 'pager' | 'url' | 'sms' | 'other'; value?: string; use?: 'home' | 'work' | 'temp' | 'old' | 'mobile'; } export interface FHIRCodeableConcept { coding?: FHIRCoding[]; text?: string; } export interface FHIRCoding { system?: string; version?: string; code?: string; display?: string; } export interface FHIRExtension { url: string; valueString?: string; valueCode?: string; valueDecimal?: number; valueBoolean?: boolean; valueCoding?: FHIRCoding; } export interface FHIRCondition { resourceType: 'Condition'; id: string; clinicalStatus: FHIRCodeableConcept; verificationStatus: FHIRCodeableConcept; category: FHIRCodeableConcept[]; severity?: FHIRCodeableConcept; code: FHIRCodeableConcept; bodySite?: FHIRCodeableConcept[]; subject: FHIRReference; onsetDateTime?: string; recordedDate?: string; stage?: { summary?: FHIRCodeableConcept; assessment?: FHIRReference[]; type?: FHIRCodeableConcept; }[]; evidence?: { code?: FHIRCodeableConcept[]; detail?: FHIRReference[]; }[]; } export interface FHIRObservation { resourceType: 'Observation'; id: string; status: 'registered' | 'preliminary' | 'final' | 'amended' | 'corrected' | 'cancelled' | 'entered-in-error' | 'unknown'; category?: FHIRCodeableConcept[]; code: FHIRCodeableConcept; subject: FHIRReference; effectiveDateTime?: string; valueQuantity?: FHIRQuantity; valueCodeableConcept?: FHIRCodeableConcept; valueString?: string; interpretation?: FHIRCodeableConcept[]; referenceRange?: { low?: FHIRQuantity; high?: FHIRQuantity; type?: FHIRCodeableConcept; text?: string; }[]; component?: { code: FHIRCodeableConcept; valueQuantity?: FHIRQuantity; valueCodeableConcept?: FHIRCodeableConcept; valueString?: string; }[]; } export interface FHIRQuantity { value?: number; comparator?: '<' | '<=' | '>=' | '>'; unit?: string; system?: string; code?: string; } export interface FHIRReference { reference?: string; type?: string; identifier?: FHIRIdentifier; display?: string; } export interface FHIRMedicationRequest { resourceType: 'MedicationRequest'; id: string; status: 'active' | 'on-hold' | 'cancelled' | 'completed' | 'entered-in-error' | 'stopped' | 'draft' | 'unknown'; intent: 'proposal' | 'plan' | 'order' | 'original-order' | 'reflex-order' | 'filler-order' | 'instance-order' | 'option'; medicationCodeableConcept?: FHIRCodeableConcept; medicationReference?: FHIRReference; subject: FHIRReference; authoredOn?: string; requester?: FHIRReference; dosageInstruction?: FHIRDosage[]; } export interface FHIRDosage { sequence?: number; text?: string; timing?: { repeat?: { frequency?: number; period?: number; periodUnit?: 's' | 'min' | 'h' | 'd' | 'wk' | 'mo' | 'a'; }; }; route?: FHIRCodeableConcept; doseAndRate?: { doseQuantity?: FHIRQuantity; rateQuantity?: FHIRQuantity; }[]; } export interface FHIRDiagnosticReport { resourceType: 'DiagnosticReport'; id: string; status: 'registered' | 'partial' | 'preliminary' | 'final' | 'amended' | 'corrected' | 'appended' | 'cancelled' | 'entered-in-error' | 'unknown'; category?: FHIRCodeableConcept[]; code: FHIRCodeableConcept; subject: FHIRReference; effectiveDateTime?: string; issued?: string; performer?: FHIRReference[]; result?: FHIRReference[]; conclusion?: string; conclusionCode?: FHIRCodeableConcept[]; presentedForm?: { contentType?: string; data?: string; url?: string; title?: string; }[]; } export interface FHIRProcedure { resourceType: 'Procedure'; id: string; status: 'preparation' | 'in-progress' | 'not-done' | 'on-hold' | 'stopped' | 'completed' | 'entered-in-error' | 'unknown'; code: FHIRCodeableConcept; subject: FHIRReference; performedDateTime?: string; performedPeriod?: { start?: string; end?: string }; performer?: { actor: FHIRReference; function?: FHIRCodeableConcept }[]; bodySite?: FHIRCodeableConcept[]; outcome?: FHIRCodeableConcept; report?: FHIRReference[]; } export interface FHIRBundle { resourceType: 'Bundle'; id?: string; type: 'document' | 'message' | 'transaction' | 'transaction-response' | 'batch' | 'batch-response' | 'history' | 'searchset' | 'collection'; total?: number; link?: { relation: string; url: string }[]; entry?: { fullUrl?: string; resource?: FHIRResource; search?: { mode?: 'match' | 'include' | 'outcome'; score?: number }; }[]; } export type FHIRResource = FHIRPatient | FHIRCondition | FHIRObservation | FHIRMedicationRequest | FHIRDiagnosticReport | FHIRProcedure | FHIRBundle; // ═══════════════════════════════════════════════════════════════════════════════ // CANCER-SPECIFIC FHIR PROFILES // ═══════════════════════════════════════════════════════════════════════════════ export interface CancerDiagnosis { patientId: string; conditionId: string; cancerType: string; icdCode: string; snomedCode?: string; histology?: string; primarySite?: string; laterality?: 'left' | 'right' | 'bilateral' | 'not-applicable'; stage?: { system: 'ajcc' | 'figo' | 'rai' | 'binet' | 'iss' | 'other'; stage: string; tnm?: { t?: string; n?: string; m?: string }; grade?: string; }; diagnosisDate: Date; verificationStatus: 'confirmed' | 'provisional' | 'differential' | 'refuted'; } export interface CancerBiomarkers { patientId: string; collectionDate: Date; biomarkers: { name: string; value: string | number; unit?: string; status: 'positive' | 'negative' | 'equivocal' | 'not-tested'; method?: string; loincCode?: string; }[]; genomicAlterations?: { gene: string; alteration: string; type: 'mutation' | 'amplification' | 'deletion' | 'fusion' | 'rearrangement'; variantAlleleFrequency?: number; pathogenicity?: 'pathogenic' | 'likely-pathogenic' | 'vus' | 'likely-benign' | 'benign'; }[]; tumorMutationalBurden?: { value: number; unit: 'mutations/Mb'; status: 'high' | 'intermediate' | 'low'; threshold?: number; }; microsatelliteInstability?: { status: 'MSI-H' | 'MSI-L' | 'MSS'; method: 'PCR' | 'NGS' | 'IHC'; }; pdl1Expression?: { score: number; scoreType: 'TPS' | 'CPS' | 'IC'; antibody?: string; }; hrdStatus?: { status: 'positive' | 'negative'; score?: number; components?: { loh?: number; tai?: number; lst?: number; }; }; } export interface TreatmentHistory { patientId: string; treatments: { id: string; type: 'chemotherapy' | 'immunotherapy' | 'targeted-therapy' | 'radiation' | 'surgery' | 'hormone-therapy' | 'car-t' | 'other'; regimen?: string; drugs?: { name: string; dose?: string; route?: string; rxNormCode?: string }[]; startDate: Date; endDate?: Date; status: 'planned' | 'active' | 'completed' | 'stopped' | 'on-hold'; cycles?: number; response?: 'CR' | 'PR' | 'SD' | 'PD' | 'NE'; responseDate?: Date; reasonStopped?: string; adverseEvents?: { name: string; grade: number; ctcaeCode?: string }[]; }[]; } // ═══════════════════════════════════════════════════════════════════════════════ // FHIR CLIENT IMPLEMENTATION // ═══════════════════════════════════════════════════════════════════════════════ export class FHIRClient extends EventEmitter { private config: FHIRConfig; private accessToken?: string; private tokenExpiry?: Date; private auditLogger?: (event: AuditEvent) => Promise; constructor(config: FHIRConfig) { super(); this.config = { timeout: 30000, retryAttempts: 3, ...config }; } /** * Set audit logger for HIPAA compliance */ setAuditLogger(logger: (event: AuditEvent) => Promise): void { this.auditLogger = logger; } /** * Authenticate with the FHIR server */ async authenticate(): Promise { const startTime = Date.now(); try { switch (this.config.authType) { case 'smart-on-fhir': await this.smartOnFhirAuth(); break; case 'client-credentials': await this.clientCredentialsAuth(); break; case 'oauth2': await this.oauth2Auth(); break; case 'basic': // Basic auth doesn't require pre-authentication break; default: throw new Error(`Unsupported auth type: ${this.config.authType}`); } await this.logAudit({ action: 'authenticate', resourceType: 'System', outcome: 'success', duration: Date.now() - startTime }); } catch (error) { await this.logAudit({ action: 'authenticate', resourceType: 'System', outcome: 'failure', duration: Date.now() - startTime, errorMessage: error instanceof Error ? error.message : 'Unknown error' }); throw error; } } private async smartOnFhirAuth(): Promise { // SMART on FHIR authentication flow // This is a simplified version - production would need full OAuth2 flow const tokenEndpoint = await this.discoverTokenEndpoint(); const response = await this.httpRequest(tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: this.config.clientId, client_secret: this.config.clientSecret || '', scope: this.config.scopes.join(' ') }).toString() }); const data = JSON.parse(response); this.accessToken = data.access_token; this.tokenExpiry = new Date(Date.now() + (data.expires_in * 1000)); } private async clientCredentialsAuth(): Promise { const tokenEndpoint = `${this.config.baseUrl}/oauth2/token`; const response = await this.httpRequest(tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Basic ${Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64')}` }, body: new URLSearchParams({ grant_type: 'client_credentials', scope: this.config.scopes.join(' ') }).toString() }); const data = JSON.parse(response); this.accessToken = data.access_token; this.tokenExpiry = new Date(Date.now() + (data.expires_in * 1000)); } private async oauth2Auth(): Promise { // OAuth2 flow - similar to client credentials for server-to-server await this.clientCredentialsAuth(); } private async discoverTokenEndpoint(): Promise { const wellKnown = `${this.config.baseUrl}/.well-known/smart-configuration`; try { const response = await this.httpRequest(wellKnown, { method: 'GET' }); const config = JSON.parse(response); return config.token_endpoint; } catch { // Fall back to standard OAuth2 endpoint return `${this.config.baseUrl}/oauth2/token`; } } /** * Get a patient by ID */ async getPatient(patientId: string): Promise { return await this.read('Patient', patientId); } /** * Search for patients */ async searchPatients(params: Record): Promise { return await this.search('Patient', params); } /** * Get cancer diagnosis for a patient */ async getCancerDiagnosis(patientId: string): Promise { const bundle = await this.search('Condition', { patient: patientId, category: 'encounter-diagnosis', 'code:below': '363346000' // SNOMED CT code for malignant neoplasm }); const diagnoses: CancerDiagnosis[] = []; for (const entry of bundle.entry || []) { const condition = entry.resource as FHIRCondition; if (condition.resourceType === 'Condition') { diagnoses.push(this.mapConditionToCancerDiagnosis(condition, patientId)); } } return diagnoses; } /** * Get biomarkers and genomic data for a patient */ async getBiomarkers(patientId: string): Promise { // Get lab observations const labBundle = await this.search('Observation', { patient: patientId, category: 'laboratory', _sort: '-date', _count: '100' }); // Get genomic observations const genomicBundle = await this.search('Observation', { patient: patientId, category: 'genomic-variant', _sort: '-date', _count: '100' }); // Get diagnostic reports (for NGS results) const reportBundle = await this.search('DiagnosticReport', { patient: patientId, category: 'GE', // Genetics _sort: '-date', _count: '50' }); return this.aggregateBiomarkerData(patientId, labBundle, genomicBundle, reportBundle); } /** * Get treatment history for a patient */ async getTreatmentHistory(patientId: string): Promise { const [medications, procedures] = await Promise.all([ this.search('MedicationRequest', { patient: patientId, _sort: '-authoredon', _count: '200' }), this.search('Procedure', { patient: patientId, _sort: '-date', _count: '200' }) ]); return this.aggregateTreatmentHistory(patientId, medications, procedures); } /** * Get all observations for a patient */ async getObservations(patientId: string, category?: string): Promise { const params: Record = { patient: patientId, _sort: '-date', _count: '100' }; if (category) { params.category = category; } const bundle = await this.search('Observation', params); return (bundle.entry || []) .map(e => e.resource) .filter((r): r is FHIRObservation => r?.resourceType === 'Observation'); } /** * Get all medications for a patient */ async getMedications(patientId: string): Promise { const bundle = await this.search('MedicationRequest', { patient: patientId, _sort: '-authoredon' }); return (bundle.entry || []) .map(e => e.resource) .filter((r): r is FHIRMedicationRequest => r?.resourceType === 'MedicationRequest'); } /** * Get diagnostic reports for a patient */ async getDiagnosticReports(patientId: string): Promise { const bundle = await this.search('DiagnosticReport', { patient: patientId, _sort: '-date' }); return (bundle.entry || []) .map(e => e.resource) .filter((r): r is FHIRDiagnosticReport => r?.resourceType === 'DiagnosticReport'); } /** * Create a comprehensive cancer patient summary */ async getComprehensiveCancerSummary(patientId: string): Promise<{ patient: FHIRPatient; diagnoses: CancerDiagnosis[]; biomarkers: CancerBiomarkers | null; treatmentHistory: TreatmentHistory; recentLabs: FHIRObservation[]; performanceStatus?: { score: number; scale: 'ECOG' | 'KPS'; date: Date }; }> { const [patient, diagnoses, biomarkers, treatmentHistory, recentLabs] = await Promise.all([ this.getPatient(patientId), this.getCancerDiagnosis(patientId), this.getBiomarkers(patientId), this.getTreatmentHistory(patientId), this.getObservations(patientId, 'laboratory') ]); // Get performance status (ECOG/KPS) const performanceObs = await this.search('Observation', { patient: patientId, code: '89247-1', // LOINC for ECOG performance status _sort: '-date', _count: '1' }); let performanceStatus: { score: number; scale: 'ECOG' | 'KPS'; date: Date } | undefined; const perfEntry = performanceObs.entry?.[0]?.resource as FHIRObservation; if (perfEntry?.valueQuantity?.value !== undefined) { performanceStatus = { score: perfEntry.valueQuantity.value, scale: 'ECOG', date: new Date(perfEntry.effectiveDateTime || Date.now()) }; } return { patient, diagnoses, biomarkers, treatmentHistory, recentLabs: recentLabs.slice(0, 20), performanceStatus }; } // ═══════════════════════════════════════════════════════════════════════════════ // CORE FHIR OPERATIONS // ═══════════════════════════════════════════════════════════════════════════════ /** * Read a resource by ID */ async read(resourceType: string, id: string): Promise { const url = `${this.config.baseUrl}/${resourceType}/${id}`; const startTime = Date.now(); try { const response = await this.httpRequest(url, { method: 'GET', headers: await this.getAuthHeaders() }); await this.logAudit({ action: 'read', resourceType, resourceId: id, outcome: 'success', duration: Date.now() - startTime }); return JSON.parse(response) as T; } catch (error) { await this.logAudit({ action: 'read', resourceType, resourceId: id, outcome: 'failure', duration: Date.now() - startTime, errorMessage: error instanceof Error ? error.message : 'Unknown error' }); throw error; } } /** * Search for resources */ async search(resourceType: string, params: Record): Promise { const searchParams = new URLSearchParams(params); const url = `${this.config.baseUrl}/${resourceType}?${searchParams.toString()}`; const startTime = Date.now(); try { const response = await this.httpRequest(url, { method: 'GET', headers: await this.getAuthHeaders() }); await this.logAudit({ action: 'search', resourceType, outcome: 'success', duration: Date.now() - startTime, details: { searchParams: params } }); return JSON.parse(response) as FHIRBundle; } catch (error) { await this.logAudit({ action: 'search', resourceType, outcome: 'failure', duration: Date.now() - startTime, errorMessage: error instanceof Error ? error.message : 'Unknown error' }); throw error; } } /** * Create a resource */ async create(resource: T): Promise { const url = `${this.config.baseUrl}/${resource.resourceType}`; const startTime = Date.now(); try { const response = await this.httpRequest(url, { method: 'POST', headers: { ...await this.getAuthHeaders(), 'Content-Type': 'application/fhir+json' }, body: JSON.stringify(resource) }); await this.logAudit({ action: 'create', resourceType: resource.resourceType, outcome: 'success', duration: Date.now() - startTime }); return JSON.parse(response) as T; } catch (error) { await this.logAudit({ action: 'create', resourceType: resource.resourceType, outcome: 'failure', duration: Date.now() - startTime, errorMessage: error instanceof Error ? error.message : 'Unknown error' }); throw error; } } /** * Update a resource */ async update(resource: T & { id: string }): Promise { const url = `${this.config.baseUrl}/${resource.resourceType}/${resource.id}`; const startTime = Date.now(); try { const response = await this.httpRequest(url, { method: 'PUT', headers: { ...await this.getAuthHeaders(), 'Content-Type': 'application/fhir+json' }, body: JSON.stringify(resource) }); await this.logAudit({ action: 'update', resourceType: resource.resourceType, resourceId: resource.id, outcome: 'success', duration: Date.now() - startTime }); return JSON.parse(response) as T; } catch (error) { await this.logAudit({ action: 'update', resourceType: resource.resourceType, resourceId: resource.id, outcome: 'failure', duration: Date.now() - startTime, errorMessage: error instanceof Error ? error.message : 'Unknown error' }); throw error; } } // ═══════════════════════════════════════════════════════════════════════════════ // HELPER METHODS // ═══════════════════════════════════════════════════════════════════════════════ private async getAuthHeaders(): Promise> { // Check if token needs refresh if (this.tokenExpiry && new Date() >= this.tokenExpiry) { await this.authenticate(); } const headers: Record = { 'Accept': 'application/fhir+json' }; if (this.accessToken) { headers['Authorization'] = `Bearer ${this.accessToken}`; } else if (this.config.authType === 'basic') { headers['Authorization'] = `Basic ${Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64')}`; } return headers; } private async httpRequest(url: string, options: { method: string; headers?: Record; body?: string; }): Promise { // Use native fetch in Node.js 18+ const response = await fetch(url, { method: options.method, headers: options.headers, body: options.body, signal: AbortSignal.timeout(this.config.timeout || 30000) }); if (!response.ok) { const errorBody = await response.text(); throw new Error(`FHIR request failed: ${response.status} ${response.statusText} - ${errorBody}`); } return await response.text(); } private mapConditionToCancerDiagnosis(condition: FHIRCondition, patientId: string): CancerDiagnosis { const icdCoding = condition.code.coding?.find(c => c.system?.includes('icd-10') || c.system?.includes('icd-9') ); const snomedCoding = condition.code.coding?.find(c => c.system?.includes('snomed') ); const stageInfo = condition.stage?.[0]; let stage: CancerDiagnosis['stage']; if (stageInfo?.summary) { const stageCode = stageInfo.summary.coding?.[0]?.code || stageInfo.summary.text || ''; stage = { system: this.determineStageSystem(stageCode), stage: stageCode }; } return { patientId, conditionId: condition.id, cancerType: condition.code.text || condition.code.coding?.[0]?.display || 'Unknown', icdCode: icdCoding?.code || '', snomedCode: snomedCoding?.code, primarySite: condition.bodySite?.[0]?.text || condition.bodySite?.[0]?.coding?.[0]?.display, stage, diagnosisDate: new Date(condition.onsetDateTime || condition.recordedDate || Date.now()), verificationStatus: this.mapVerificationStatus(condition.verificationStatus) }; } private determineStageSystem(stageCode: string): 'ajcc' | 'figo' | 'rai' | 'binet' | 'iss' | 'other' { const code = stageCode.toUpperCase(); if (code.includes('AJCC') || /^[IV]+[ABC]?$/.test(code) || code.includes('TNM')) { return 'ajcc'; } if (code.includes('FIGO')) return 'figo'; if (code.includes('RAI')) return 'rai'; if (code.includes('BINET')) return 'binet'; if (code.includes('ISS')) return 'iss'; return 'other'; } private mapVerificationStatus(status: FHIRCodeableConcept): CancerDiagnosis['verificationStatus'] { const code = status.coding?.[0]?.code?.toLowerCase() || ''; if (code.includes('confirmed')) return 'confirmed'; if (code.includes('provisional')) return 'provisional'; if (code.includes('differential')) return 'differential'; if (code.includes('refuted')) return 'refuted'; return 'confirmed'; } private aggregateBiomarkerData( patientId: string, labBundle: FHIRBundle, genomicBundle: FHIRBundle, reportBundle: FHIRBundle ): CancerBiomarkers | null { const biomarkers: CancerBiomarkers['biomarkers'] = []; const genomicAlterations: CancerBiomarkers['genomicAlterations'] = []; let latestDate = new Date(0); // Process lab observations for (const entry of labBundle.entry || []) { const obs = entry.resource as FHIRObservation; if (obs.resourceType !== 'Observation') continue; const date = new Date(obs.effectiveDateTime || Date.now()); if (date > latestDate) latestDate = date; // Map common cancer biomarkers const biomarker = this.mapObservationToBiomarker(obs); if (biomarker) biomarkers.push(biomarker); } // Process genomic observations for (const entry of genomicBundle.entry || []) { const obs = entry.resource as FHIRObservation; if (obs.resourceType !== 'Observation') continue; const date = new Date(obs.effectiveDateTime || Date.now()); if (date > latestDate) latestDate = date; const alteration = this.mapObservationToGenomicAlteration(obs); if (alteration) genomicAlterations.push(alteration); } // Process diagnostic reports for additional genomic data for (const entry of reportBundle.entry || []) { const report = entry.resource as FHIRDiagnosticReport; if (report.resourceType !== 'DiagnosticReport') continue; const date = new Date(report.effectiveDateTime || Date.now()); if (date > latestDate) latestDate = date; } if (biomarkers.length === 0 && genomicAlterations.length === 0) { return null; } // Extract special biomarkers const tmbObs = this.findBiomarker(biomarkers, ['TMB', 'tumor mutational burden']); const msiObs = this.findBiomarker(biomarkers, ['MSI', 'microsatellite']); const pdl1Obs = this.findBiomarker(biomarkers, ['PD-L1', 'PDL1']); const hrdObs = this.findBiomarker(biomarkers, ['HRD', 'homologous recombination']); return { patientId, collectionDate: latestDate, biomarkers, genomicAlterations: genomicAlterations.length > 0 ? genomicAlterations : undefined, tumorMutationalBurden: tmbObs ? { value: typeof tmbObs.value === 'number' ? tmbObs.value : parseFloat(String(tmbObs.value)) || 0, unit: 'mutations/Mb', status: this.categorizeTMB(tmbObs.value) } : undefined, microsatelliteInstability: msiObs ? { status: this.categorizeMSI(msiObs.value), method: 'NGS' } : undefined, pdl1Expression: pdl1Obs ? { score: typeof pdl1Obs.value === 'number' ? pdl1Obs.value : parseFloat(String(pdl1Obs.value)) || 0, scoreType: 'TPS' } : undefined, hrdStatus: hrdObs ? { status: hrdObs.status === 'positive' ? 'positive' : 'negative', score: typeof hrdObs.value === 'number' ? hrdObs.value : undefined } : undefined }; } private mapObservationToBiomarker(obs: FHIRObservation): CancerBiomarkers['biomarkers'][0] | null { const name = obs.code.text || obs.code.coding?.[0]?.display; if (!name) return null; let value: string | number; let status: 'positive' | 'negative' | 'equivocal' | 'not-tested'; if (obs.valueQuantity?.value !== undefined) { value = obs.valueQuantity.value; status = 'positive'; // Will be refined based on interpretation } else if (obs.valueCodeableConcept) { value = obs.valueCodeableConcept.text || obs.valueCodeableConcept.coding?.[0]?.display || ''; status = this.interpretBiomarkerStatus(value); } else if (obs.valueString) { value = obs.valueString; status = this.interpretBiomarkerStatus(value); } else { return null; } // Check interpretation if available if (obs.interpretation?.[0]?.coding?.[0]?.code) { const interpCode = obs.interpretation[0].coding[0].code; if (interpCode === 'POS' || interpCode === 'H') status = 'positive'; else if (interpCode === 'NEG' || interpCode === 'N') status = 'negative'; else if (interpCode === 'IND') status = 'equivocal'; } return { name, value, unit: obs.valueQuantity?.unit, status, loincCode: obs.code.coding?.find(c => c.system?.includes('loinc'))?.code }; } private mapObservationToGenomicAlteration(obs: FHIRObservation): CancerBiomarkers['genomicAlterations'][0] | null { // This would need to be expanded to handle the full mCode/genomics-reporting IG const geneComponent = obs.component?.find(c => c.code.coding?.some(coding => coding.code === '48018-6') // Gene studied ); const variantComponent = obs.component?.find(c => c.code.coding?.some(coding => coding.code === '81252-9') // DNA change ); if (!geneComponent || !variantComponent) return null; const gene = geneComponent.valueCodeableConcept?.text || geneComponent.valueCodeableConcept?.coding?.[0]?.display || ''; const alteration = variantComponent.valueString || variantComponent.valueCodeableConcept?.text || ''; if (!gene || !alteration) return null; return { gene, alteration, type: this.determineAlterationType(alteration) }; } private determineAlterationType(alteration: string): 'mutation' | 'amplification' | 'deletion' | 'fusion' | 'rearrangement' { const lower = alteration.toLowerCase(); if (lower.includes('amp') || lower.includes('gain')) return 'amplification'; if (lower.includes('del') || lower.includes('loss')) return 'deletion'; if (lower.includes('fusion') || lower.includes('::')) return 'fusion'; if (lower.includes('rearr') || lower.includes('transloc')) return 'rearrangement'; return 'mutation'; } private interpretBiomarkerStatus(value: string | number): 'positive' | 'negative' | 'equivocal' | 'not-tested' { const lower = String(value).toLowerCase(); if (lower.includes('positive') || lower.includes('detected') || lower === 'yes') return 'positive'; if (lower.includes('negative') || lower.includes('not detected') || lower === 'no') return 'negative'; if (lower.includes('equivocal') || lower.includes('indeterminate') || lower.includes('borderline')) return 'equivocal'; return 'positive'; // Default to positive if we have a value } private findBiomarker(biomarkers: CancerBiomarkers['biomarkers'], keywords: string[]): CancerBiomarkers['biomarkers'][0] | undefined { return biomarkers.find(b => keywords.some(k => b.name.toLowerCase().includes(k.toLowerCase())) ); } private categorizeTMB(value: string | number): 'high' | 'intermediate' | 'low' { const numValue = typeof value === 'number' ? value : parseFloat(String(value)); if (isNaN(numValue)) return 'low'; if (numValue >= 10) return 'high'; if (numValue >= 6) return 'intermediate'; return 'low'; } private categorizeMSI(value: string | number): 'MSI-H' | 'MSI-L' | 'MSS' { const strValue = String(value).toUpperCase(); if (strValue.includes('MSI-H') || strValue.includes('HIGH') || strValue.includes('UNSTABLE')) return 'MSI-H'; if (strValue.includes('MSI-L') || strValue.includes('LOW')) return 'MSI-L'; return 'MSS'; } private aggregateTreatmentHistory( patientId: string, medications: FHIRBundle, procedures: FHIRBundle ): TreatmentHistory { const treatments: TreatmentHistory['treatments'] = []; // Process medications for (const entry of medications.entry || []) { const med = entry.resource as FHIRMedicationRequest; if (med.resourceType !== 'MedicationRequest') continue; const drugName = med.medicationCodeableConcept?.text || med.medicationCodeableConcept?.coding?.[0]?.display || 'Unknown medication'; const treatment = this.categorizeTreatment(drugName); treatments.push({ id: med.id, type: treatment.type, regimen: treatment.regimen, drugs: [{ name: drugName, dose: med.dosageInstruction?.[0]?.text, route: med.dosageInstruction?.[0]?.route?.text, rxNormCode: med.medicationCodeableConcept?.coding?.find(c => c.system?.includes('rxnorm') )?.code }], startDate: new Date(med.authoredOn || Date.now()), status: this.mapMedicationStatus(med.status) }); } // Process procedures (surgery, radiation) for (const entry of procedures.entry || []) { const proc = entry.resource as FHIRProcedure; if (proc.resourceType !== 'Procedure') continue; const procName = proc.code.text || proc.code.coding?.[0]?.display || 'Unknown procedure'; const isSurgery = this.isSurgicalProcedure(procName); const isRadiation = this.isRadiationProcedure(procName); if (isSurgery || isRadiation) { treatments.push({ id: proc.id, type: isRadiation ? 'radiation' : 'surgery', drugs: [], startDate: new Date(proc.performedDateTime || proc.performedPeriod?.start || Date.now()), endDate: proc.performedPeriod?.end ? new Date(proc.performedPeriod.end) : undefined, status: this.mapProcedureStatus(proc.status) }); } } // Sort by date treatments.sort((a, b) => b.startDate.getTime() - a.startDate.getTime()); return { patientId, treatments }; } private categorizeTreatment(drugName: string): { type: TreatmentHistory['treatments'][0]['type']; regimen?: string } { const lower = drugName.toLowerCase(); // Immunotherapy const immunotherapyDrugs = ['pembrolizumab', 'nivolumab', 'ipilimumab', 'atezolizumab', 'durvalumab', 'avelumab', 'cemiplimab', 'dostarlimab', 'relatlimab', 'tremelimumab']; if (immunotherapyDrugs.some(d => lower.includes(d))) { return { type: 'immunotherapy' }; } // Targeted therapy const targetedDrugs = ['imatinib', 'erlotinib', 'gefitinib', 'osimertinib', 'crizotinib', 'alectinib', 'palbociclib', 'ribociclib', 'abemaciclib', 'olaparib', 'rucaparib', 'niraparib', 'trastuzumab', 'pertuzumab', 'lapatinib', 'vemurafenib', 'dabrafenib', 'trametinib', 'sotorasib', 'adagrasib', 'venetoclax', 'ibrutinib', 'acalabrutinib', 'lenvatinib', 'sorafenib', 'regorafenib', 'cabozantinib', 'bevacizumab', 'cetuximab', 'panitumumab', 'rituximab']; if (targetedDrugs.some(d => lower.includes(d))) { return { type: 'targeted-therapy' }; } // Hormone therapy const hormoneDrugs = ['tamoxifen', 'letrozole', 'anastrozole', 'exemestane', 'fulvestrant', 'enzalutamide', 'abiraterone', 'apalutamide', 'darolutamide', 'lupron', 'leuprolide']; if (hormoneDrugs.some(d => lower.includes(d))) { return { type: 'hormone-therapy' }; } // CAR-T const carTDrugs = ['tisagenlecleucel', 'axicabtagene', 'brexucabtagene', 'lisocabtagene', 'idecabtagene', 'ciltacabtagene', 'kymriah', 'yescarta', 'tecartus', 'breyanzi', 'abecma', 'carvykti']; if (carTDrugs.some(d => lower.includes(d))) { return { type: 'car-t' }; } // Chemotherapy (catch-all for cytotoxic agents) const chemoDrugs = ['carboplatin', 'cisplatin', 'oxaliplatin', 'paclitaxel', 'docetaxel', 'doxorubicin', 'epirubicin', 'cyclophosphamide', 'fluorouracil', '5-fu', 'capecitabine', 'gemcitabine', 'pemetrexed', 'etoposide', 'irinotecan', 'vincristine', 'vinblastine', 'methotrexate', 'cytarabine', 'azacitidine', 'decitabine', 'temozolomide']; if (chemoDrugs.some(d => lower.includes(d))) { return { type: 'chemotherapy' }; } return { type: 'other' }; } private mapMedicationStatus(status: FHIRMedicationRequest['status']): TreatmentHistory['treatments'][0]['status'] { switch (status) { case 'active': return 'active'; case 'completed': return 'completed'; case 'stopped': return 'stopped'; case 'on-hold': return 'on-hold'; case 'draft': return 'planned'; default: return 'active'; } } private mapProcedureStatus(status: FHIRProcedure['status']): TreatmentHistory['treatments'][0]['status'] { switch (status) { case 'completed': return 'completed'; case 'in-progress': return 'active'; case 'preparation': return 'planned'; case 'on-hold': return 'on-hold'; case 'stopped': return 'stopped'; default: return 'completed'; } } private isSurgicalProcedure(name: string): boolean { const surgeryKeywords = ['surgery', 'resection', 'excision', 'mastectomy', 'lobectomy', 'colectomy', 'gastrectomy', 'prostatectomy', 'nephrectomy', 'hysterectomy', 'lymphadenectomy', 'biopsy', 'debulking', 'whipple', 'hepatectomy']; return surgeryKeywords.some(k => name.toLowerCase().includes(k)); } private isRadiationProcedure(name: string): boolean { const radiationKeywords = ['radiation', 'radiotherapy', 'sbrt', 'imrt', 'proton', 'brachytherapy', 'cyberknife', 'gamma knife', 'external beam']; return radiationKeywords.some(k => name.toLowerCase().includes(k)); } private async logAudit(event: Partial): Promise { if (!this.auditLogger) return; const fullEvent: AuditEvent = { timestamp: new Date(), userId: 'system', ipAddress: '0.0.0.0', action: event.action || 'unknown', resourceType: event.resourceType || 'unknown', outcome: event.outcome || 'unknown', ...event }; try { await this.auditLogger(fullEvent); } catch (error) { console.error('Failed to log audit event:', error); } } } // ═══════════════════════════════════════════════════════════════════════════════ // AUDIT EVENT TYPE // ═══════════════════════════════════════════════════════════════════════════════ export interface AuditEvent { timestamp: Date; userId: string; ipAddress: string; action: string; resourceType: string; resourceId?: string; outcome: 'success' | 'failure' | 'unknown'; duration?: number; errorMessage?: string; details?: Record; } // ═══════════════════════════════════════════════════════════════════════════════ // EHR VENDOR-SPECIFIC ADAPTERS // ═══════════════════════════════════════════════════════════════════════════════ export class EpicFHIRClient extends FHIRClient { constructor(config: Omit) { super({ ...config, ehrVendor: 'epic' }); } /** * Epic-specific: Get MyChart patient context */ async getMyChartContext(launchToken: string): Promise<{ patient: string; encounter?: string; practitioner?: string; }> { // Epic SMART launch context parsing const decoded = Buffer.from(launchToken, 'base64').toString(); return JSON.parse(decoded); } } export class CernerFHIRClient extends FHIRClient { constructor(config: Omit) { super({ ...config, ehrVendor: 'cerner' }); } /** * Cerner-specific: Handle Millennium-specific extensions */ parseMillenniumExtensions(resource: FHIRResource): Record { const extensions: Record = {}; if ('extension' in resource && resource.extension) { for (const ext of resource.extension) { if (ext.url.includes('cerner.com')) { const key = ext.url.split('/').pop() || ext.url; extensions[key] = ext.valueString || ext.valueCode || ext.valueBoolean || ext.valueCoding; } } } return extensions; } } // ═══════════════════════════════════════════════════════════════════════════════ // FACTORY FUNCTION // ═══════════════════════════════════════════════════════════════════════════════ export function createFHIRClient(config: FHIRConfig): FHIRClient { switch (config.ehrVendor) { case 'epic': return new EpicFHIRClient(config); case 'cerner': return new CernerFHIRClient(config); default: return new FHIRClient(config); } } export default FHIRClient;