Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,21 @@ MigrationBackup/

# Ionide (cross platform F# VS Code tools) working folder
.ionide/

# Node.js / TypeScript
node_modules/
dist/
*.tsbuildinfo

# Azure Functions
local.settings.json
.vscode/

# Deployment artifacts
deployment-info.json
*.zip

# Environment files
.env
.env.local
.env.*.local
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@

This repository contains automation to deploy [OpenEMR](https://www.open-emr.org/) on Azure. Great work being done on this project on the [OpenEMR Repo](https://github.com/openemr/openemr).

## Quick Start: Integrated Deployment (Recommended)

For a complete, production-ready deployment including OpenEMR, Azure Health Data Services (AHDS), and automatic FHIR synchronization:

**[β†’ See Integrated Deployment Guide](integrated-deployment/README.md)**

This automated deployment includes:
- βœ… OpenEMR 7.0.2 on Azure VM
- βœ… Azure Health Data Services (FHIR R4)
- βœ… FHIR Connector with automatic sync every minute
- βœ… Complete configuration with minimal user interaction
- βœ… Verification script to validate the integration

```bash
cd integrated-deployment
./deploy.sh
```

## All-in-one:
### OpenEMR+MySQL Containers with Docker Compose on an Azure IaaS Virtual Machine

Expand Down
3 changes: 1 addition & 2 deletions all-in-one/azuredeploy.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
"branch": {
"type": "string",
"defaultValue": "main",
"allowedValues": ["main", "dev"],
"metadata": {
"description": "Git branch to deploy (main or dev)."
"description": "Git branch to deploy (e.g., main, dev, or custom branch name)."
}
},
"vmName": {
Expand Down
65 changes: 65 additions & 0 deletions fhir-connector/functions/autoSync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { app, InvocationContext, Timer } from '@azure/functions';
import { OpenEMRClient } from '../src/openemr-client';
import { AHDSClient } from '../src/ahds-client';
import { FHIRSyncService } from '../src/sync-service';

/**
* Timer-triggered Azure Function that automatically syncs FHIR resources every minute
* This function searches for all patients modified in the last 2 minutes and syncs them with their observations
*/
export async function autoSync(myTimer: Timer, context: InvocationContext): Promise<void> {
context.log('Auto-sync timer trigger function started at:', new Date().toISOString());

try {
// Initialize clients
const openemrClient = new OpenEMRClient({
baseUrl: process.env.OPENEMR_BASE_URL || '',
clientId: process.env.OPENEMR_CLIENT_ID || '',
clientSecret: process.env.OPENEMR_CLIENT_SECRET || '',
});

const ahdsClient = new AHDSClient({
fhirEndpoint: process.env.AHDS_FHIR_ENDPOINT || '',
tenantId: process.env.AHDS_TENANT_ID || '',
clientId: process.env.AHDS_CLIENT_ID || '',
clientSecret: process.env.AHDS_CLIENT_SECRET || '',
});

// Create sync service
const syncService = new FHIRSyncService(openemrClient, ahdsClient);

// Sync all patients (in a real production scenario, you'd filter by _lastUpdated)
// For POC, we sync all patients every minute
context.log('Searching for patients to sync...');
const results = await syncService.syncPatients({});

const successCount = results.filter(r => r.success).length;
const failureCount = results.filter(r => !r.success).length;

context.log(`Auto-sync completed: ${successCount} succeeded, ${failureCount} failed`);

// For each successfully synced patient, sync their observations
for (const result of results) {
if (result.success && result.resourceId) {
try {
const obsResults = await syncService.syncObservationsForPatient(result.resourceId);
const obsSuccess = obsResults.filter(r => r.success).length;
const obsFailed = obsResults.filter(r => !r.success).length;
context.log(`Synced observations for Patient/${result.resourceId}: ${obsSuccess} succeeded, ${obsFailed} failed`);
} catch (error) {
context.error(`Failed to sync observations for Patient/${result.resourceId}:`, error);
}
}
}

context.log('Auto-sync completed successfully');
} catch (error) {
context.error('Auto-sync failed:', error);
throw error;
}
}

app.timer('autoSync', {
schedule: '0 */1 * * * *', // Every minute
handler: autoSync,
});
2 changes: 1 addition & 1 deletion fhir-connector/functions/syncObservation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export async function syncObservation(request: HttpRequest, context: InvocationC
status: 500,
jsonBody: {
error: 'Internal server error',
message: error?.message ?? String(error),
message: error instanceof Error ? error.message : String(error),
},
};
}
Expand Down
2 changes: 1 addition & 1 deletion fhir-connector/functions/syncPatient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export async function syncPatient(request: HttpRequest, context: InvocationConte
status: 500,
jsonBody: {
error: 'Internal server error',
message: error?.message ?? String(error),
message: error instanceof Error ? error.message : String(error),
},
};
}
Expand Down
2 changes: 1 addition & 1 deletion fhir-connector/functions/syncPatientWithObservations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export async function syncPatientWithObservations(
status: 500,
jsonBody: {
error: 'Internal server error',
message: error?.message ?? String(error),
message: error instanceof Error ? error.message : String(error),
},
};
}
Expand Down
16 changes: 8 additions & 8 deletions fhir-connector/src/ahds-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class AHDSClient {
console.log('Successfully authenticated with Azure Health Data Services');
} catch (error) {
console.error('Failed to authenticate with AHDS:', error);
throw new Error(`AHDS authentication failed: ${error.message}`);
throw new Error(`AHDS authentication failed: ${error instanceof Error ? error.message : String(error)}`);
}
}

Expand Down Expand Up @@ -81,9 +81,9 @@ export class AHDSClient {

console.log(`Successfully upserted ${resourceType}/${resourceId || response.data.id}`);
return response.data;
} catch (error) {
console.error(`Failed to upsert ${resourceType}:`, error.response?.data || error.message);
throw new Error(`Failed to upsert FHIR resource: ${error.message}`);
} catch (error: any) {
console.error(`Failed to upsert ${resourceType}:`, error?.response?.data || error?.message || String(error));
throw new Error(`Failed to upsert FHIR resource: ${error instanceof Error ? error.message : String(error)}`);
}
}

Expand All @@ -101,12 +101,12 @@ export class AHDSClient {
},
});
return response.data;
} catch (error) {
if (error.response?.status === 404) {
} catch (error: any) {
if (error?.response?.status === 404) {
return null;
}
console.error(`Failed to get ${resourceType}/${resourceId}:`, error);
throw new Error(`Failed to get FHIR resource: ${error.message}`);
throw new Error(`Failed to get FHIR resource: ${error instanceof Error ? error.message : String(error)}`);
}
}

Expand All @@ -127,7 +127,7 @@ export class AHDSClient {
return response.data;
} catch (error) {
console.error(`Failed to search ${resourceType}:`, error);
throw new Error(`Failed to search FHIR resources: ${error?.message ?? String(error)}`);
throw new Error(`Failed to search FHIR resources: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
8 changes: 4 additions & 4 deletions fhir-connector/src/openemr-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class OpenEMRClient {
console.log('Successfully authenticated with OpenEMR');
} catch (error) {
console.error('Failed to authenticate with OpenEMR:', error);
throw new Error(`OpenEMR authentication failed: ${error?.message ?? String(error)}`);
throw new Error(`OpenEMR authentication failed: ${error instanceof Error ? error.message : String(error)}`);
}
}

Expand Down Expand Up @@ -76,7 +76,7 @@ export class OpenEMRClient {
return response.data;
} catch (error) {
console.error(`Failed to get ${resourceType}/${resourceId}:`, error);
throw new Error(`Failed to retrieve FHIR resource: ${error.message}`);
throw new Error(`Failed to retrieve FHIR resource: ${error instanceof Error ? error.message : String(error)}`);
}
}

Expand All @@ -100,7 +100,7 @@ export class OpenEMRClient {
return response.data;
} catch (error) {
console.error(`Failed to search ${resourceType}:`, error);
throw new Error(`Failed to search FHIR resources: ${error.message}`);
throw new Error(`Failed to search FHIR resources: ${error instanceof Error ? error.message : String(error)}`);
}
}

Expand All @@ -120,7 +120,7 @@ export class OpenEMRClient {
return response.data;
} catch (error) {
console.error('Failed to get capability statement:', error);
throw new Error(`Failed to get capability statement: ${error?.message ?? String(error)}`);
throw new Error(`Failed to get capability statement: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
10 changes: 5 additions & 5 deletions fhir-connector/src/sync-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ export class FHIRSyncService {
operation: () => Promise<T>,
operationName: string
): Promise<T> {
let lastError: Error | null = null;
let lastError: Error = new Error('Unknown error');

for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
console.warn(`${operationName} failed (attempt ${attempt}/${this.maxRetries}):`, error?.message ?? String(error));
lastError = error instanceof Error ? error : new Error(String(error));
console.warn(`${operationName} failed (attempt ${attempt}/${this.maxRetries}):`, lastError.message);

if (attempt < this.maxRetries) {
const delay = this.retryDelay * Math.pow(2, attempt - 1); // exponential backoff
Expand Down Expand Up @@ -80,7 +80,7 @@ export class FHIRSyncService {
success: false,
resourceType: 'Patient',
resourceId: patientId,
error: error.message,
error: error instanceof Error ? error.message : String(error),
};
}
}
Expand Down Expand Up @@ -119,7 +119,7 @@ export class FHIRSyncService {
success: false,
resourceType: 'Observation',
resourceId: observationId,
error: error?.message ?? String(error),
error: error instanceof Error ? error.message : String(error),
};
}
}
Expand Down
Loading