@medplum/agent 2.1.16 → 2.1.18
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/README.md +10 -2
- package/package.json +8 -2
- package/babel.config.json +0 -3
- package/esbuild.mjs +0 -28
- package/installer.nsi +0 -238
- package/jest.config.json +0 -8
- package/src/__mocks__/node-windows.ts +0 -9
- package/src/__mocks__/ws.ts +0 -1
- package/src/main.test.ts +0 -220
- package/src/main.ts +0 -244
- package/tsconfig.json +0 -8
package/README.md
CHANGED
|
@@ -9,6 +9,14 @@ On-prem agent for device connectivity.
|
|
|
9
9
|
|
|
10
10
|
## Building
|
|
11
11
|
|
|
12
|
+
Build everything:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm run build:all
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or, build individual components:
|
|
19
|
+
|
|
12
20
|
Build the JS output:
|
|
13
21
|
|
|
14
22
|
```bash
|
|
@@ -18,11 +26,11 @@ npm run build
|
|
|
18
26
|
Build the `.exe` file using [Vercel `pkg`](https://github.com/vercel/pkg):
|
|
19
27
|
|
|
20
28
|
```bash
|
|
21
|
-
npm run
|
|
29
|
+
npm run build:exe
|
|
22
30
|
```
|
|
23
31
|
|
|
24
32
|
Build the installer using [NSIS](https://nsis.sourceforge.io/) (requires `makensis` on your PATH):
|
|
25
33
|
|
|
26
34
|
```bash
|
|
27
|
-
npm run installer
|
|
35
|
+
npm run build:installer
|
|
28
36
|
```
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medplum/agent",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.18",
|
|
4
4
|
"description": "Medplum Agent",
|
|
5
5
|
"homepage": "https://www.medplum.com/",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/medplum/medplum/issues"
|
|
8
|
+
},
|
|
6
9
|
"repository": {
|
|
7
10
|
"type": "git",
|
|
8
11
|
"url": "git+https://github.com/medplum/medplum.git",
|
|
@@ -10,12 +13,15 @@
|
|
|
10
13
|
},
|
|
11
14
|
"license": "Apache-2.0",
|
|
12
15
|
"author": "Medplum <hello@medplum.com>",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
13
19
|
"scripts": {
|
|
14
20
|
"agent": "ts-node src/main.ts",
|
|
15
21
|
"build": "npm run clean && tsc && node esbuild.mjs",
|
|
22
|
+
"build:all": "npm run build && npm run build:exe && npm run build:installer",
|
|
16
23
|
"build:exe": "pkg ./dist/cjs/index.cjs --targets node18-win-x64 --output dist/medplum-agent-win-x64.exe --options no-warnings",
|
|
17
24
|
"build:installer": "makensis installer.nsi",
|
|
18
|
-
"build:all": "npm run build && npm run build:exe && npm run build:installer",
|
|
19
25
|
"clean": "rimraf dist",
|
|
20
26
|
"test": "jest"
|
|
21
27
|
},
|
package/babel.config.json
DELETED
package/esbuild.mjs
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/* global console */
|
|
2
|
-
/* eslint no-console: "off" */
|
|
3
|
-
|
|
4
|
-
import esbuild from 'esbuild';
|
|
5
|
-
import { writeFileSync } from 'fs';
|
|
6
|
-
|
|
7
|
-
const options = {
|
|
8
|
-
entryPoints: ['./src/main.ts'],
|
|
9
|
-
bundle: true,
|
|
10
|
-
platform: 'node',
|
|
11
|
-
loader: { '.js': 'js', '.ts': 'ts' },
|
|
12
|
-
resolveExtensions: ['.js', '.ts'],
|
|
13
|
-
target: 'es2021',
|
|
14
|
-
tsconfig: 'tsconfig.json',
|
|
15
|
-
external: ['iconv-lite', 'pdfmake'],
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
// The single executable application feature only supports running a single embedded CommonJS file.
|
|
19
|
-
// https://nodejs.org/dist/latest-v18.x/docs/api/single-executable-applications.html
|
|
20
|
-
|
|
21
|
-
esbuild
|
|
22
|
-
.build({
|
|
23
|
-
...options,
|
|
24
|
-
format: 'cjs',
|
|
25
|
-
outfile: './dist/cjs/index.cjs',
|
|
26
|
-
})
|
|
27
|
-
.then(() => writeFileSync('./dist/cjs/package.json', '{"type": "commonjs"}'))
|
|
28
|
-
.catch(console.error);
|
package/installer.nsi
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
# Medplum Agent Installer Builder
|
|
2
|
-
# For use with NSIS 3.0+
|
|
3
|
-
# See: https://nsis.sourceforge.io/
|
|
4
|
-
|
|
5
|
-
!define COMPANY_NAME "Medplum"
|
|
6
|
-
!define APP_NAME "Medplum Agent"
|
|
7
|
-
!define SERVICE_NAME "MedplumAgent"
|
|
8
|
-
!define INSTALLER_FILE_NAME "medplum-agent-installer.exe"
|
|
9
|
-
!define DEFAULT_BASE_URL "https://api.medplum.com/"
|
|
10
|
-
|
|
11
|
-
Name "${APP_NAME}"
|
|
12
|
-
OutFile "${INSTALLER_FILE_NAME}"
|
|
13
|
-
VIProductVersion "1.0.0.0"
|
|
14
|
-
VIAddVersionKey ProductName "${APP_NAME}"
|
|
15
|
-
VIAddVersionKey Comments "${APP_NAME}"
|
|
16
|
-
VIAddVersionKey CompanyName "${COMPANY_NAME}"
|
|
17
|
-
VIAddVersionKey LegalCopyright "${COMPANY_NAME}"
|
|
18
|
-
VIAddVersionKey FileDescription "${APP_NAME}"
|
|
19
|
-
VIAddVersionKey FileVersion 1
|
|
20
|
-
VIAddVersionKey ProductVersion 1
|
|
21
|
-
VIAddVersionKey InternalName "${APP_NAME}"
|
|
22
|
-
VIAddVersionKey LegalTrademarks "${COMPANY_NAME}"
|
|
23
|
-
VIAddVersionKey OriginalFilename "${INSTALLER_FILE_NAME}"
|
|
24
|
-
|
|
25
|
-
InstallDir "$PROGRAMFILES64\${APP_NAME}"
|
|
26
|
-
|
|
27
|
-
!include "nsDialogs.nsh"
|
|
28
|
-
|
|
29
|
-
RequestExecutionLevel admin
|
|
30
|
-
|
|
31
|
-
Var WelcomeDialog
|
|
32
|
-
Var WelcomeLabel
|
|
33
|
-
Var alreadyInstalled
|
|
34
|
-
Var baseUrl
|
|
35
|
-
Var clientId
|
|
36
|
-
Var clientSecret
|
|
37
|
-
Var agentId
|
|
38
|
-
|
|
39
|
-
# The onInit handler is called when the installer is nearly finished initializing.
|
|
40
|
-
# See: https://nsis.sourceforge.io/Reference/.onInit
|
|
41
|
-
Function .onInit
|
|
42
|
-
ReadRegStr $0 HKLM "SYSTEM\CurrentControlSet\Services\${SERVICE_NAME}" "ImagePath"
|
|
43
|
-
${If} $0 != ""
|
|
44
|
-
StrCpy $alreadyInstalled 1
|
|
45
|
-
${EndIf}
|
|
46
|
-
FunctionEnd
|
|
47
|
-
|
|
48
|
-
Page custom WelcomePage
|
|
49
|
-
Page custom InputPage InputPageLeave
|
|
50
|
-
Page instfiles
|
|
51
|
-
|
|
52
|
-
# The WelcomePage is a simple static screen that displays a friendly message.
|
|
53
|
-
Function WelcomePage
|
|
54
|
-
nsDialogs::Create 1018
|
|
55
|
-
Pop $WelcomeDialog
|
|
56
|
-
|
|
57
|
-
${If} $WelcomeDialog == error
|
|
58
|
-
Abort
|
|
59
|
-
${EndIf}
|
|
60
|
-
|
|
61
|
-
${NSD_CreateLabel} 0 0 100% 50u "Welcome to the ${APP_NAME} Installer!$\r$\n$\r$\nClick next to continue."
|
|
62
|
-
Pop $WelcomeLabel
|
|
63
|
-
|
|
64
|
-
nsDialogs::Show
|
|
65
|
-
FunctionEnd
|
|
66
|
-
|
|
67
|
-
# The InputPage captures all of the user input for the agent.
|
|
68
|
-
Function InputPage
|
|
69
|
-
${If} $alreadyInstalled == 1
|
|
70
|
-
Abort ; This skips the page
|
|
71
|
-
${EndIf}
|
|
72
|
-
|
|
73
|
-
nsDialogs::Create 1018
|
|
74
|
-
Pop $0
|
|
75
|
-
|
|
76
|
-
StrCpy $baseUrl "${DEFAULT_BASE_URL}"
|
|
77
|
-
${NSD_CreateLabel} 0 0 30% 12u "Base URL:"
|
|
78
|
-
Pop $R0
|
|
79
|
-
${NSD_CreateText} 35% 0 65% 12u $baseUrl
|
|
80
|
-
Pop $R1
|
|
81
|
-
|
|
82
|
-
${NSD_CreateLabel} 0 15u 30% 12u "Client ID:"
|
|
83
|
-
Pop $R2
|
|
84
|
-
${NSD_CreateText} 35% 15u 65% 12u $clientId
|
|
85
|
-
Pop $R3
|
|
86
|
-
|
|
87
|
-
${NSD_CreateLabel} 0 30u 30% 12u "Client Secret:"
|
|
88
|
-
Pop $R4
|
|
89
|
-
${NSD_CreateText} 35% 30u 65% 12u $clientSecret
|
|
90
|
-
Pop $R5
|
|
91
|
-
|
|
92
|
-
${NSD_CreateLabel} 0 45u 30% 12u "Agent ID:"
|
|
93
|
-
Pop $R6
|
|
94
|
-
${NSD_CreateText} 35% 45u 65% 12u $agentId
|
|
95
|
-
Pop $R7
|
|
96
|
-
|
|
97
|
-
${NSD_SetFocus} $R3
|
|
98
|
-
nsDialogs::Show
|
|
99
|
-
FunctionEnd
|
|
100
|
-
|
|
101
|
-
Function InputPageLeave
|
|
102
|
-
${NSD_GetText} $R1 $baseUrl
|
|
103
|
-
${NSD_GetText} $R3 $clientId
|
|
104
|
-
${NSD_GetText} $R5 $clientSecret
|
|
105
|
-
${NSD_GetText} $R7 $agentId
|
|
106
|
-
FunctionEnd
|
|
107
|
-
|
|
108
|
-
# Main installation entry point.
|
|
109
|
-
Section
|
|
110
|
-
DetailPrint "${APP_NAME}"
|
|
111
|
-
SetOutPath "$INSTDIR"
|
|
112
|
-
|
|
113
|
-
${If} $alreadyInstalled == 1
|
|
114
|
-
Call UpgradeApp
|
|
115
|
-
${Else}
|
|
116
|
-
Call InstallApp
|
|
117
|
-
${EndIf}
|
|
118
|
-
|
|
119
|
-
SectionEnd
|
|
120
|
-
|
|
121
|
-
# Upgrade an existing installation.
|
|
122
|
-
# This only copies files, and restarts the Windows Service.
|
|
123
|
-
# It does not modify the existing configuration settings.
|
|
124
|
-
Function UpgradeApp
|
|
125
|
-
|
|
126
|
-
# Stop the service
|
|
127
|
-
DetailPrint "Stopping service..."
|
|
128
|
-
ExecWait "sc.exe stop ${SERVICE_NAME}" $1
|
|
129
|
-
DetailPrint "Exit code $1"
|
|
130
|
-
|
|
131
|
-
# Sleep for 3 seconds to let the service fully stop
|
|
132
|
-
# We cannot write the new version of the exe while the process is running
|
|
133
|
-
DetailPrint "Sleeping..."
|
|
134
|
-
Sleep 3000
|
|
135
|
-
|
|
136
|
-
# Copy the new files to the installation directory
|
|
137
|
-
File dist\medplum-agent-win-x64.exe
|
|
138
|
-
File README.md
|
|
139
|
-
|
|
140
|
-
# Start the service
|
|
141
|
-
DetailPrint "Starting service..."
|
|
142
|
-
ExecWait "sc.exe start ${SERVICE_NAME}" $1
|
|
143
|
-
DetailPrint "Start service returned $1"
|
|
144
|
-
|
|
145
|
-
FunctionEnd
|
|
146
|
-
|
|
147
|
-
# Do the actual installation.
|
|
148
|
-
# Install all of the files.
|
|
149
|
-
# Install the Windows Service.
|
|
150
|
-
Function InstallApp
|
|
151
|
-
# Print user input
|
|
152
|
-
DetailPrint "Base URL: $baseUrl"
|
|
153
|
-
DetailPrint "Client ID: $clientId"
|
|
154
|
-
DetailPrint "Client Secret: $clientSecret"
|
|
155
|
-
DetailPrint "Agent ID: $agentId"
|
|
156
|
-
|
|
157
|
-
# Copy the service files to the root directory
|
|
158
|
-
File ..\..\node_modules\node-shawl\bin\shawl-v1.3.0-legal.txt
|
|
159
|
-
File ..\..\node_modules\node-shawl\bin\shawl-v1.3.0-win64.exe
|
|
160
|
-
File dist\medplum-agent-win-x64.exe
|
|
161
|
-
File README.md
|
|
162
|
-
|
|
163
|
-
# Create the service
|
|
164
|
-
DetailPrint "Creating service..."
|
|
165
|
-
ExecWait "shawl-v1.3.0-win64.exe add --name $\"${SERVICE_NAME}$\" -- $\"$INSTDIR\medplum-agent-win-x64.exe$\" $\"$baseUrl$\" $\"$clientId$\" $\"$clientSecret$\" $\"$agentId$\"" $1
|
|
166
|
-
DetailPrint "Exit code $1"
|
|
167
|
-
|
|
168
|
-
# Set service display name
|
|
169
|
-
DetailPrint "Setting service display name..."
|
|
170
|
-
ExecWait "sc.exe config $\"${SERVICE_NAME}$\" displayname= $\"${APP_NAME}$\"" $1
|
|
171
|
-
DetailPrint "Exit code $1"
|
|
172
|
-
|
|
173
|
-
# Set service description
|
|
174
|
-
DetailPrint "Setting service description..."
|
|
175
|
-
ExecWait "sc.exe description $\"${SERVICE_NAME}$\" $\"Securely connects local devices to ${COMPANY_NAME} cloud$\"" $1
|
|
176
|
-
DetailPrint "Exit code $1"
|
|
177
|
-
|
|
178
|
-
# Set service to start automatically
|
|
179
|
-
DetailPrint "Setting service to start automatically..."
|
|
180
|
-
ExecWait "sc.exe config $\"${SERVICE_NAME}$\" start= auto" $1
|
|
181
|
-
DetailPrint "Exit code $1"
|
|
182
|
-
|
|
183
|
-
# Start the service
|
|
184
|
-
DetailPrint "Starting service..."
|
|
185
|
-
ExecWait "sc.exe start $\"${SERVICE_NAME}$\"" $1
|
|
186
|
-
DetailPrint "Exit code $1"
|
|
187
|
-
|
|
188
|
-
# Create the uninstaller
|
|
189
|
-
DetailPrint "Creating the uninstaller..."
|
|
190
|
-
SetOutPath $INSTDIR
|
|
191
|
-
WriteUninstaller "$INSTDIR\uninstall.exe"
|
|
192
|
-
|
|
193
|
-
# Register the uninstaller
|
|
194
|
-
DetailPrint "Registering the uninstaller..."
|
|
195
|
-
WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}" "DisplayName" "${APP_NAME}"
|
|
196
|
-
WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
|
197
|
-
WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
|
198
|
-
DetailPrint "Uninstaller complete"
|
|
199
|
-
|
|
200
|
-
# Create Start menu shortcuts
|
|
201
|
-
DetailPrint "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Uninstall.lnk"
|
|
202
|
-
CreateDirectory "$SMPROGRAMS\${APP_NAME}"
|
|
203
|
-
CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Uninstall.lnk" "$INSTDIR\uninstall.exe"
|
|
204
|
-
|
|
205
|
-
FunctionEnd
|
|
206
|
-
|
|
207
|
-
# Start the uninstaller
|
|
208
|
-
Section Uninstall
|
|
209
|
-
|
|
210
|
-
# Stop the service
|
|
211
|
-
DetailPrint "Stopping service..."
|
|
212
|
-
ExecWait "sc.exe stop ${SERVICE_NAME}" $1
|
|
213
|
-
DetailPrint "Exit code $1"
|
|
214
|
-
|
|
215
|
-
# Sleep for 3 seconds to let the service fully stop
|
|
216
|
-
# We cannot delete the file until the service is fully stopped
|
|
217
|
-
DetailPrint "Sleeping..."
|
|
218
|
-
Sleep 3000
|
|
219
|
-
|
|
220
|
-
# Deleting the service
|
|
221
|
-
DetailPrint "Deleting service..."
|
|
222
|
-
ExecWait "sc.exe delete ${SERVICE_NAME}" $1
|
|
223
|
-
DetailPrint "Exit code $1"
|
|
224
|
-
|
|
225
|
-
# Get out of the service directory so we can delete it
|
|
226
|
-
SetOutPath "$PROGRAMFILES64"
|
|
227
|
-
|
|
228
|
-
# Uninstall the Start menu shortcuts
|
|
229
|
-
RMDir /r /REBOOTOK "$SMPROGRAMS\${APP_NAME}"
|
|
230
|
-
|
|
231
|
-
# Delete the files
|
|
232
|
-
RMDir /r /REBOOTOK "$INSTDIR"
|
|
233
|
-
|
|
234
|
-
# Unregister the program
|
|
235
|
-
DeleteRegKey HKLM "SYSTEM\CurrentControlSet\Services\${SERVICE_NAME}"
|
|
236
|
-
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}"
|
|
237
|
-
|
|
238
|
-
SectionEnd
|
package/jest.config.json
DELETED
package/src/__mocks__/ws.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { WebSocket as default } from 'mock-socket';
|
package/src/main.test.ts
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { allOk, createReference, Hl7Message, sleep } from '@medplum/core';
|
|
2
|
-
import { Agent, Bot, Endpoint, Resource } from '@medplum/fhirtypes';
|
|
3
|
-
import { Hl7Client, Hl7Server } from '@medplum/hl7';
|
|
4
|
-
import { MockClient } from '@medplum/mock';
|
|
5
|
-
import { Client, Server } from 'mock-socket';
|
|
6
|
-
import { App } from './main';
|
|
7
|
-
|
|
8
|
-
jest.mock('node-windows');
|
|
9
|
-
|
|
10
|
-
const medplum = new MockClient();
|
|
11
|
-
let bot: Bot;
|
|
12
|
-
let endpoint: Endpoint;
|
|
13
|
-
|
|
14
|
-
describe('Agent', () => {
|
|
15
|
-
beforeAll(async () => {
|
|
16
|
-
console.log = jest.fn();
|
|
17
|
-
|
|
18
|
-
medplum.router.router.add('POST', ':resourceType/:id/$execute', async () => {
|
|
19
|
-
return [allOk, {} as Resource];
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
bot = await medplum.createResource<Bot>({ resourceType: 'Bot' });
|
|
23
|
-
|
|
24
|
-
endpoint = await medplum.createResource<Endpoint>({
|
|
25
|
-
resourceType: 'Endpoint',
|
|
26
|
-
address: 'mllp://0.0.0.0:57000',
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('Runs successfully', async () => {
|
|
31
|
-
const mockServer = new Server('wss://example.com/ws/agent');
|
|
32
|
-
|
|
33
|
-
mockServer.on('connection', (socket) => {
|
|
34
|
-
socket.on('message', (data) => {
|
|
35
|
-
const command = JSON.parse((data as Buffer).toString('utf8'));
|
|
36
|
-
if (command.type === 'connect') {
|
|
37
|
-
socket.send(
|
|
38
|
-
Buffer.from(
|
|
39
|
-
JSON.stringify({
|
|
40
|
-
type: 'connected',
|
|
41
|
-
})
|
|
42
|
-
)
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const agent = await medplum.createResource<Agent>({
|
|
49
|
-
resourceType: 'Agent',
|
|
50
|
-
channel: [
|
|
51
|
-
{
|
|
52
|
-
endpoint: createReference(endpoint),
|
|
53
|
-
targetReference: createReference(bot),
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const app = new App(medplum, agent.id as string);
|
|
59
|
-
await app.start();
|
|
60
|
-
app.stop();
|
|
61
|
-
app.stop();
|
|
62
|
-
mockServer.stop();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('Send and receive', async () => {
|
|
66
|
-
const mockServer = new Server('wss://example.com/ws/agent');
|
|
67
|
-
|
|
68
|
-
mockServer.on('connection', (socket) => {
|
|
69
|
-
socket.on('message', (data) => {
|
|
70
|
-
const command = JSON.parse((data as Buffer).toString('utf8'));
|
|
71
|
-
if (command.type === 'connect') {
|
|
72
|
-
socket.send(
|
|
73
|
-
Buffer.from(
|
|
74
|
-
JSON.stringify({
|
|
75
|
-
type: 'connected',
|
|
76
|
-
})
|
|
77
|
-
)
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (command.type === 'transmit') {
|
|
82
|
-
const hl7Message = Hl7Message.parse(command.body);
|
|
83
|
-
const ackMessage = hl7Message.buildAck();
|
|
84
|
-
socket.send(
|
|
85
|
-
Buffer.from(
|
|
86
|
-
JSON.stringify({
|
|
87
|
-
type: 'transmit',
|
|
88
|
-
channel: command.channel,
|
|
89
|
-
remote: command.remote,
|
|
90
|
-
body: ackMessage.toString(),
|
|
91
|
-
})
|
|
92
|
-
)
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const agent = await medplum.createResource<Agent>({
|
|
99
|
-
resourceType: 'Agent',
|
|
100
|
-
channel: [
|
|
101
|
-
{
|
|
102
|
-
name: 'test',
|
|
103
|
-
endpoint: createReference(endpoint),
|
|
104
|
-
targetReference: createReference(bot),
|
|
105
|
-
},
|
|
106
|
-
],
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const app = new App(medplum, agent.id as string);
|
|
110
|
-
await app.start();
|
|
111
|
-
|
|
112
|
-
const client = new Hl7Client({
|
|
113
|
-
host: 'localhost',
|
|
114
|
-
port: 57000,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const response = await client.sendAndWait(
|
|
118
|
-
Hl7Message.parse(
|
|
119
|
-
'MSH|^~\\&|ADT1|MCM|LABADT|MCM|198808181126|SECURITY|ADT^A01|MSG00001|P|2.2\r' +
|
|
120
|
-
'PID|||PATID1234^5^M11||JONES^WILLIAM^A^III||19610615|M-\r' +
|
|
121
|
-
'NK1|1|JONES^BARBARA^K|SPO|||||20011105\r' +
|
|
122
|
-
'PV1|1|I|2000^2012^01||||004777^LEBAUER^SIDNEY^J.|||SUR||-||1|A0-'
|
|
123
|
-
)
|
|
124
|
-
);
|
|
125
|
-
expect(response).toBeDefined();
|
|
126
|
-
expect(response.header.getComponent(9, 1)).toBe('ACK');
|
|
127
|
-
expect(response.segments).toHaveLength(2);
|
|
128
|
-
expect(response.segments[1].name).toBe('MSA');
|
|
129
|
-
|
|
130
|
-
client.close();
|
|
131
|
-
app.stop();
|
|
132
|
-
mockServer.stop();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test('Push', async () => {
|
|
136
|
-
const mockServer = new Server('wss://example.com/ws/agent');
|
|
137
|
-
let mySocket: Client | undefined = undefined;
|
|
138
|
-
|
|
139
|
-
mockServer.on('connection', (socket) => {
|
|
140
|
-
mySocket = socket;
|
|
141
|
-
socket.on('message', (data) => {
|
|
142
|
-
const command = JSON.parse((data as Buffer).toString('utf8'));
|
|
143
|
-
if (command.type === 'connect') {
|
|
144
|
-
socket.send(
|
|
145
|
-
Buffer.from(
|
|
146
|
-
JSON.stringify({
|
|
147
|
-
type: 'connected',
|
|
148
|
-
})
|
|
149
|
-
)
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const agent = await medplum.createResource<Agent>({
|
|
156
|
-
resourceType: 'Agent',
|
|
157
|
-
channel: [
|
|
158
|
-
{
|
|
159
|
-
endpoint: createReference(endpoint),
|
|
160
|
-
targetReference: createReference(bot),
|
|
161
|
-
},
|
|
162
|
-
],
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Start an HL7 listener
|
|
166
|
-
const hl7Messages = [];
|
|
167
|
-
const hl7Server = new Hl7Server((conn) => {
|
|
168
|
-
conn.addEventListener('message', ({ message }) => {
|
|
169
|
-
hl7Messages.push(message);
|
|
170
|
-
conn.send(message.buildAck());
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
hl7Server.start(57001);
|
|
174
|
-
|
|
175
|
-
// Wait for server to start listening
|
|
176
|
-
while (!hl7Server.server?.listening) {
|
|
177
|
-
await sleep(100);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Start the app
|
|
181
|
-
const app = new App(medplum, agent.id as string);
|
|
182
|
-
await app.start();
|
|
183
|
-
|
|
184
|
-
// Wait for the WebSocket to connect
|
|
185
|
-
// eslint-disable-next-line no-unmodified-loop-condition
|
|
186
|
-
while (!mySocket) {
|
|
187
|
-
await sleep(100);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// At this point, we expect the websocket to be connected
|
|
191
|
-
expect(mySocket).toBeDefined();
|
|
192
|
-
|
|
193
|
-
// Send a push message
|
|
194
|
-
const wsClient = mySocket as unknown as Client;
|
|
195
|
-
wsClient.send(
|
|
196
|
-
Buffer.from(
|
|
197
|
-
JSON.stringify({
|
|
198
|
-
type: 'push',
|
|
199
|
-
body:
|
|
200
|
-
'MSH|^~\\&|ADT1|MCM|LABADT|MCM|198808181126|SECURITY|ADT^A01|MSG00001|P|2.2\r' +
|
|
201
|
-
'PID|||PATID1234^5^M11||JONES^WILLIAM^A^III||19610615|M-\r' +
|
|
202
|
-
'NK1|1|JONES^BARBARA^K|SPO|||||20011105\r' +
|
|
203
|
-
'PV1|1|I|2000^2012^01||||004777^LEBAUER^SIDNEY^J.|||SUR||-||1|A0-',
|
|
204
|
-
remote: 'mllp://localhost:57001',
|
|
205
|
-
})
|
|
206
|
-
)
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
// Wait for the HL7 message to be received
|
|
210
|
-
while (hl7Messages.length < 1) {
|
|
211
|
-
await sleep(100);
|
|
212
|
-
}
|
|
213
|
-
expect(hl7Messages.length).toBe(1);
|
|
214
|
-
|
|
215
|
-
// Shutdown everything
|
|
216
|
-
hl7Server.stop();
|
|
217
|
-
app.stop();
|
|
218
|
-
mockServer.stop();
|
|
219
|
-
});
|
|
220
|
-
});
|
package/src/main.ts
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { Hl7Message, MedplumClient, normalizeErrorString } from '@medplum/core';
|
|
2
|
-
import { AgentChannel, Endpoint, Reference } from '@medplum/fhirtypes';
|
|
3
|
-
import { Hl7Client, Hl7Connection, Hl7MessageEvent, Hl7Server } from '@medplum/hl7';
|
|
4
|
-
import { EventLogger } from 'node-windows';
|
|
5
|
-
import WebSocket from 'ws';
|
|
6
|
-
|
|
7
|
-
interface QueueItem {
|
|
8
|
-
channel: string;
|
|
9
|
-
remote: string;
|
|
10
|
-
body: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class App {
|
|
14
|
-
readonly log: EventLogger;
|
|
15
|
-
readonly webSocket: WebSocket;
|
|
16
|
-
readonly webSocketQueue: QueueItem[] = [];
|
|
17
|
-
readonly channels = new Map<string, AgentHl7Channel>();
|
|
18
|
-
readonly hl7Queue: QueueItem[] = [];
|
|
19
|
-
live = false;
|
|
20
|
-
|
|
21
|
-
constructor(
|
|
22
|
-
readonly medplum: MedplumClient,
|
|
23
|
-
readonly agentId: string
|
|
24
|
-
) {
|
|
25
|
-
this.log = {
|
|
26
|
-
info: console.log,
|
|
27
|
-
warn: console.warn,
|
|
28
|
-
error: console.error,
|
|
29
|
-
} as EventLogger;
|
|
30
|
-
|
|
31
|
-
const webSocketUrl = new URL(medplum.getBaseUrl());
|
|
32
|
-
webSocketUrl.protocol = webSocketUrl.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
33
|
-
webSocketUrl.pathname = '/ws/agent';
|
|
34
|
-
this.log.info(`Connecting to WebSocket: ${webSocketUrl.href}`);
|
|
35
|
-
|
|
36
|
-
this.webSocket = new WebSocket(webSocketUrl);
|
|
37
|
-
this.webSocket.binaryType = 'nodebuffer';
|
|
38
|
-
this.webSocket.addEventListener('error', (err) => this.log.error(err.message));
|
|
39
|
-
this.webSocket.addEventListener('open', () => {
|
|
40
|
-
this.webSocket.send(
|
|
41
|
-
JSON.stringify({
|
|
42
|
-
type: 'connect',
|
|
43
|
-
accessToken: medplum.getAccessToken(),
|
|
44
|
-
agentId,
|
|
45
|
-
})
|
|
46
|
-
);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
this.webSocket.addEventListener('message', (e) => {
|
|
50
|
-
try {
|
|
51
|
-
const data = e.data as Buffer;
|
|
52
|
-
const str = data.toString('utf8');
|
|
53
|
-
this.log.info(`Received from WebSocket: ${str.replaceAll('\r', '\n')}`);
|
|
54
|
-
const command = JSON.parse(str);
|
|
55
|
-
switch (command.type) {
|
|
56
|
-
case 'connected':
|
|
57
|
-
this.live = true;
|
|
58
|
-
this.trySendToWebSocket();
|
|
59
|
-
break;
|
|
60
|
-
case 'transmit':
|
|
61
|
-
this.addToHl7Queue(command);
|
|
62
|
-
break;
|
|
63
|
-
case 'push':
|
|
64
|
-
this.pushMessage(command);
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
} catch (err) {
|
|
68
|
-
this.log.error(`WebSocket error: ${normalizeErrorString(err)}`);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async start(): Promise<void> {
|
|
74
|
-
this.log.info('Medplum service starting...');
|
|
75
|
-
|
|
76
|
-
const agent = await this.medplum.readResource('Agent', this.agentId);
|
|
77
|
-
|
|
78
|
-
for (const definition of agent.channel as AgentChannel[]) {
|
|
79
|
-
const endpoint = await this.medplum.readReference(definition.endpoint as Reference<Endpoint>);
|
|
80
|
-
const channel = new AgentHl7Channel(this, definition, endpoint);
|
|
81
|
-
channel.start();
|
|
82
|
-
this.channels.set(definition.name as string, channel);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
this.log.info('Medplum service started successfully');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
stop(): void {
|
|
89
|
-
this.log.info('Medplum service stopping...');
|
|
90
|
-
this.channels.forEach((channel) => channel.stop());
|
|
91
|
-
this.log.info('Medplum service stopped successfully');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
addToWebSocketQueue(message: QueueItem): void {
|
|
95
|
-
this.webSocketQueue.push(message);
|
|
96
|
-
this.trySendToWebSocket();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
addToHl7Queue(message: QueueItem): void {
|
|
100
|
-
this.hl7Queue.push(message);
|
|
101
|
-
this.trySendToHl7Connection();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private trySendToWebSocket(): void {
|
|
105
|
-
if (this.live) {
|
|
106
|
-
while (this.webSocketQueue.length > 0) {
|
|
107
|
-
const msg = this.webSocketQueue.shift();
|
|
108
|
-
if (msg) {
|
|
109
|
-
this.webSocket.send(
|
|
110
|
-
JSON.stringify({
|
|
111
|
-
type: 'transmit',
|
|
112
|
-
accessToken: this.medplum.getAccessToken(),
|
|
113
|
-
...msg,
|
|
114
|
-
})
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private trySendToHl7Connection(): void {
|
|
122
|
-
while (this.hl7Queue.length > 0) {
|
|
123
|
-
const msg = this.hl7Queue.shift();
|
|
124
|
-
if (msg) {
|
|
125
|
-
const channel = this.channels.get(msg.channel);
|
|
126
|
-
if (channel) {
|
|
127
|
-
const connection = channel.connections.get(msg.remote);
|
|
128
|
-
if (connection) {
|
|
129
|
-
connection.hl7Connection.send(Hl7Message.parse(msg.body));
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
private pushMessage(message: QueueItem): void {
|
|
137
|
-
const address = new URL(message.remote);
|
|
138
|
-
const client = new Hl7Client({
|
|
139
|
-
host: address.hostname,
|
|
140
|
-
port: parseInt(address.port, 10),
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
client
|
|
144
|
-
.sendAndWait(Hl7Message.parse(message.body))
|
|
145
|
-
.then((response) => {
|
|
146
|
-
this.log.info(`Response: ${response.toString().replaceAll('\r', '\n')}`);
|
|
147
|
-
})
|
|
148
|
-
.catch((err) => {
|
|
149
|
-
this.log.error(`HL7 error: ${normalizeErrorString(err)}`);
|
|
150
|
-
})
|
|
151
|
-
.finally(() => {
|
|
152
|
-
client.close();
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export class AgentHl7Channel {
|
|
158
|
-
readonly server: Hl7Server;
|
|
159
|
-
readonly connections = new Map<string, AgentHl7ChannelConnection>();
|
|
160
|
-
|
|
161
|
-
constructor(
|
|
162
|
-
readonly app: App,
|
|
163
|
-
readonly definition: AgentChannel,
|
|
164
|
-
readonly endpoint: Endpoint
|
|
165
|
-
) {
|
|
166
|
-
this.server = new Hl7Server((connection) => this.handleNewConnection(connection));
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
start(): void {
|
|
170
|
-
const address = new URL(this.endpoint.address as string);
|
|
171
|
-
this.app.log.info(`Channel starting on ${address}`);
|
|
172
|
-
this.server.start(parseInt(address.port, 10));
|
|
173
|
-
this.app.log.info('Channel started successfully');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
stop(): void {
|
|
177
|
-
this.app.log.info('Channel stopping...');
|
|
178
|
-
this.connections.forEach((connection) => connection.close());
|
|
179
|
-
this.server.stop();
|
|
180
|
-
this.app.log.info('Channel stopped successfully');
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private handleNewConnection(connection: Hl7Connection): void {
|
|
184
|
-
const c = new AgentHl7ChannelConnection(this, connection);
|
|
185
|
-
this.app.log.info(`HL7 connection established: ${c.remote}`);
|
|
186
|
-
this.connections.set(c.remote, c);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export class AgentHl7ChannelConnection {
|
|
191
|
-
readonly remote: string;
|
|
192
|
-
|
|
193
|
-
constructor(
|
|
194
|
-
readonly channel: AgentHl7Channel,
|
|
195
|
-
readonly hl7Connection: Hl7Connection
|
|
196
|
-
) {
|
|
197
|
-
this.remote = `${hl7Connection.socket.remoteAddress}:${hl7Connection.socket.remotePort}`;
|
|
198
|
-
|
|
199
|
-
// Add listener immediately to handle incoming messages
|
|
200
|
-
this.hl7Connection.addEventListener('message', (event) => this.handler(event));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
private async handler(event: Hl7MessageEvent): Promise<void> {
|
|
204
|
-
try {
|
|
205
|
-
this.channel.app.log.info('Received:');
|
|
206
|
-
this.channel.app.log.info(event.message.toString().replaceAll('\r', '\n'));
|
|
207
|
-
this.channel.app.addToWebSocketQueue({
|
|
208
|
-
channel: this.channel.definition.name as string,
|
|
209
|
-
remote: this.remote,
|
|
210
|
-
body: event.message.toString(),
|
|
211
|
-
});
|
|
212
|
-
} catch (err) {
|
|
213
|
-
this.channel.app.log.error(`HL7 error: ${normalizeErrorString(err)}`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
close(): void {
|
|
218
|
-
this.hl7Connection.close();
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async function main(argv: string[]): Promise<void> {
|
|
223
|
-
if (argv.length < 6) {
|
|
224
|
-
console.log('Usage: node medplum-agent.js <baseUrl> <clientId> <clientSecret> <agentId>');
|
|
225
|
-
process.exit(1);
|
|
226
|
-
}
|
|
227
|
-
const [_node, _script, baseUrl, clientId, clientSecret, agentId] = argv;
|
|
228
|
-
|
|
229
|
-
const medplum = new MedplumClient({ baseUrl, clientId });
|
|
230
|
-
await medplum.startClientLogin(clientId, clientSecret);
|
|
231
|
-
|
|
232
|
-
const app = new App(medplum, agentId);
|
|
233
|
-
await app.start();
|
|
234
|
-
|
|
235
|
-
process.on('SIGINT', () => {
|
|
236
|
-
console.log('\ngracefully shutting down from SIGINT (Crtl-C)');
|
|
237
|
-
app.stop();
|
|
238
|
-
process.exit();
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (typeof require !== 'undefined' && require.main === module) {
|
|
243
|
-
main(process.argv).catch(console.error);
|
|
244
|
-
}
|