Skip to content

Commit 7c40345

Browse files
committed
add forwarding communincation resources to ehr
1 parent 2ad26c1 commit 7c40345

3 files changed

Lines changed: 463 additions & 21 deletions

File tree

src/hooks/hookResources.ts

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import axios from 'axios';
1111
import { ServicePrefetch } from '../rems-cds-hooks/resources/CdsService';
1212
import { hydrate } from '../rems-cds-hooks/prefetch/PrefetchHydrator';
1313
import { getServiceConnection } from './hookProxy';
14+
import { HookSession } from '../lib/schemas/HookSession';
15+
import * as env from 'env-var';
1416

1517
export interface CardRule {
1618
links: Link[];
@@ -96,19 +98,48 @@ export async function handleHook(
9698
if (contextRequest && contextRequest.resourceType === 'MedicationRequest') {
9799

98100
const forwardData = async (hook: Hook, url: string) => {
99-
// remove the auth token before any forwarding occurs
100-
delete hook.fhirAuthorization;
101-
const options = {
102-
method: 'POST',
103-
data: hook,
104-
timeout: 5000,
105-
};
101+
// Store original EHR details before modifying the hook
102+
const originalFhirServer = hook.fhirServer?.toString();
106103

107104
try {
105+
// Create and save HookSession to MongoDB
106+
const session = await HookSession.createFromHook(hook);
107+
108+
console.log(`\n Created HookSession in MongoDB:`);
109+
console.log(` Session ID: ${session._id}`);
110+
console.log(` Patient: ${session.patientId}`);
111+
console.log(` Hook Instance: ${session.hookInstance}`);
112+
console.log(` EHR FHIR Server: ${session.ehrFhirServer}`);
113+
console.log(` Authorization stored: ${session.ehrAuthorization?.access_token ? 'Yes' : 'No'}`);
114+
115+
// Get intermediary's FHIR base URL from environment
116+
const intermediaryFhirUrl = env.get('INTERMEDIARY_FHIR_URL').asString() ||
117+
config.server.backendApiBase ||
118+
`http://localhost:${config.server.port}`;
119+
120+
// Override the fhirServer URL to point to intermediary
121+
hook.fhirServer = new URL(intermediaryFhirUrl);
122+
123+
// Remove authorization before forwarding to REMS Admin
124+
delete hook.fhirAuthorization;
125+
126+
console.log(`\n Forwarding CDS Hook to REMS Admin:`);
127+
console.log(` Original EHR: ${originalFhirServer}`);
128+
console.log(` Overridden to: ${hook.fhirServer}`);
129+
console.log(` REMS Admin will POST Communications to: ${hook.fhirServer}Communication\n`);
130+
131+
const options = {
132+
method: 'POST',
133+
data: hook,
134+
timeout: 5000,
135+
};
136+
108137
const response = await axios(url, options);
109138
res.json(response.data);
110-
} catch (err) {
111-
console.log(err);
139+
140+
} catch (err: any) {
141+
console.error(' Error in forwardData:', err.message);
142+
console.error(err.stack);
112143
res.json({ cards: [] }); // Return fallback response
113144
}
114145
};
@@ -126,7 +157,7 @@ export async function handleHook(
126157
let serviceConnection = await getServiceConnection(drugCode, hook.fhirServer?.toString());
127158
if (serviceConnection) {
128159
const url = serviceConnection.to + hook.hook;
129-
console.log('rems-admin hook url: ' + url);
160+
console.log('REMS Admin hook URL: ' + url);
130161
if (hook.fhirAuthorization && hook.fhirServer && hook.fhirAuthorization.access_token) {
131162
hydrate(getFhirResource, hookPrefetch, hook).then(async hydratedPrefetch => {
132163
if (hydratedPrefetch) {
@@ -227,15 +258,36 @@ export async function handleHook(
227258
return;
228259
}
229260
uniqueUrls.forEach(async (url: string) => {
230-
// remove the auth token before any forwarding occurs
231-
delete hook.fhirAuthorization;
232-
233-
const options = {
234-
method: 'POST',
235-
data: hook
236-
};
261+
// Store original before modification
262+
const originalFhirServer = hook.fhirServer?.toString();
237263

238264
try {
265+
// Create and save HookSession to MongoDB
266+
const session = await HookSession.createFromHook(hook);
267+
268+
console.log(`\n Created HookSession in MongoDB (${hookType}):`);
269+
console.log(` Session ID: ${session._id}`);
270+
console.log(` Patient: ${session.patientId}`);
271+
272+
// Get intermediary's FHIR base URL
273+
const intermediaryFhirUrl = env.get('INTERMEDIARY_FHIR_URL').asString() ||
274+
config.server.backendApiBase ||
275+
`http://localhost:${config.server.port}`;
276+
277+
// Override fhirServer to point to intermediary
278+
hook.fhirServer = new URL(intermediaryFhirUrl);
279+
280+
// Remove authorization
281+
delete hook.fhirAuthorization;
282+
283+
console.log(` Original EHR: ${originalFhirServer}`);
284+
console.log(` Overridden to: ${hook.fhirServer}\n`);
285+
286+
const options = {
287+
method: 'POST',
288+
data: hook
289+
};
290+
239291
const response = await axios(url, options);
240292
cards = [...cards, ...response.data.cards];
241293

@@ -244,8 +296,8 @@ export async function handleHook(
244296
// return the final list of cards
245297
res.json({ cards });
246298
}
247-
} catch (error) {
248-
console.error('Error calling REMS Admin:', error);
299+
} catch (error: any) {
300+
console.error(' Error in processMedications:', error.message);
249301
urlCount--;
250302
if (urlCount <= 0) {
251303
res.json({ cards });

src/lib/schemas/HookSession.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import mongoose, { Document, Model } from 'mongoose';
2+
const { Schema } = mongoose;
3+
4+
// Interface for the document
5+
export interface IHookSession extends Document {
6+
hookInstance: string;
7+
hookType: 'order-sign' | 'order-select' | 'patient-view' | 'encounter-start';
8+
ehrFhirServer: string;
9+
ehrAuthorization?: {
10+
access_token: string;
11+
token_type: string;
12+
expires_in: number;
13+
scope: string;
14+
subject: string;
15+
};
16+
patientId: string;
17+
encounterId?: string;
18+
userId?: string;
19+
createdAt: Date;
20+
lastAccessedAt: Date;
21+
communicationsSent: number;
22+
23+
// Instance methods
24+
incrementCommunications(): Promise<void>;
25+
}
26+
27+
// Interface for the model (static methods)
28+
export interface IHookSessionModel extends Model<IHookSession> {
29+
createFromHook(hook: any, ttlHours?: number): Promise<IHookSession>;
30+
findActiveSession(patientId: string): Promise<IHookSession | null>;
31+
cleanupExpired(): Promise<number>;
32+
}
33+
34+
const HookSessionSchema = new Schema({
35+
// Hook identification
36+
hookInstance: {
37+
type: String,
38+
required: true,
39+
index: true,
40+
description: 'UUID from the CDS Hook hookInstance field'
41+
},
42+
43+
hookType: {
44+
type: String,
45+
required: true,
46+
enum: ['order-sign', 'order-select', 'patient-view', 'encounter-start'],
47+
description: 'Type of CDS Hook that was called'
48+
},
49+
50+
// EHR information
51+
ehrFhirServer: {
52+
type: String,
53+
required: true,
54+
description: 'Original EHR FHIR base URL (e.g., https://ehr.example.com/fhir/r4)'
55+
},
56+
57+
ehrAuthorization: {
58+
access_token: {
59+
type: String,
60+
required: false,
61+
description: 'OAuth access token for the EHR'
62+
},
63+
token_type: {
64+
type: String,
65+
required: false,
66+
default: 'Bearer',
67+
description: 'Token type (typically Bearer)'
68+
},
69+
expires_in: {
70+
type: Number,
71+
required: false,
72+
description: 'Token expiration in seconds'
73+
},
74+
scope: {
75+
type: String,
76+
required: false,
77+
description: 'OAuth scopes granted'
78+
},
79+
subject: {
80+
type: String,
81+
required: false,
82+
description: 'Subject/client ID for the token'
83+
}
84+
},
85+
86+
// Patient context
87+
patientId: {
88+
type: String,
89+
required: true,
90+
index: true,
91+
description: 'Patient ID from hook context'
92+
},
93+
94+
// Optional encounter context
95+
encounterId: {
96+
type: String,
97+
required: false,
98+
index: true,
99+
description: 'Encounter ID from hook context if present'
100+
},
101+
102+
// Practitioner context
103+
userId: {
104+
type: String,
105+
required: false,
106+
description: 'User/Practitioner ID from hook context'
107+
},
108+
109+
// Timestamps
110+
createdAt: {
111+
type: Date,
112+
default: Date.now,
113+
index: true,
114+
description: 'When this session was created'
115+
},
116+
117+
// Tracking
118+
lastAccessedAt: {
119+
type: Date,
120+
default: Date.now,
121+
description: 'Last time this session was accessed'
122+
},
123+
124+
communicationsSent: {
125+
type: Number,
126+
default: 0,
127+
description: 'Number of Communication resources proxied using this session'
128+
}
129+
});
130+
131+
// Index for efficient lookups
132+
HookSessionSchema.index({ patientId: 1, hookInstance: 1 });
133+
134+
// Static method to create a session from a CDS Hook
135+
HookSessionSchema.statics.createFromHook = async function(
136+
hook: any,
137+
): Promise<IHookSession> {
138+
const now = new Date();
139+
140+
let ehrFhirServer = hook.fhirServer?.toString();
141+
142+
const dockerEhrName = process.env.DOCKERED_EHR_CONTAINER_NAME;
143+
console.log('docker ehr env:' + dockerEhrName)
144+
if (dockerEhrName) {
145+
ehrFhirServer = ehrFhirServer
146+
.replace(/localhost/g, dockerEhrName)
147+
.replace(/127\.0\.0\.1/g, dockerEhrName);
148+
}
149+
150+
const session = new this({
151+
hookInstance: hook.hookInstance,
152+
hookType: hook.hook,
153+
ehrFhirServer: ehrFhirServer,
154+
ehrAuthorization: hook.fhirAuthorization,
155+
patientId: hook.context.patientId,
156+
encounterId: hook.context.encounterId,
157+
userId: hook.context.userId,
158+
createdAt: now,
159+
lastAccessedAt: now
160+
});
161+
162+
await session.save();
163+
console.log(` Created HookSession: ${session._id} for patient ${session.patientId}`);
164+
return session;
165+
};
166+
167+
// Static method to find an active session for a patient
168+
HookSessionSchema.statics.findActiveSession = async function(
169+
patientId: string
170+
): Promise<IHookSession | null> {
171+
const now = new Date();
172+
173+
// Find the most recent non-expired session for this patient
174+
const session = await this.findOne({
175+
patientId: patientId,
176+
}).sort({ createdAt: -1 }); // Most recent first
177+
178+
if (session) {
179+
// Update last accessed time
180+
session.lastAccessedAt = now;
181+
await session.save();
182+
}
183+
184+
return session;
185+
};
186+
187+
// Instance method to increment communication counter
188+
HookSessionSchema.methods.incrementCommunications = async function(): Promise<void> {
189+
this.communicationsSent += 1;
190+
this.lastAccessedAt = new Date();
191+
await this.save();
192+
};
193+
194+
export const HookSession = mongoose.model<IHookSession, IHookSessionModel>(
195+
'HookSession',
196+
HookSessionSchema
197+
);

0 commit comments

Comments
 (0)