fhir-validator-wrapper 1.1.0 → 1.2.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/fhir-validator.js +398 -12
- package/package.json +1 -1
- package/readme.md +220 -35
package/fhir-validator.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const { spawn } = require('child_process');
|
|
2
2
|
const http = require('http');
|
|
3
3
|
const https = require('https');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
4
6
|
const { URL } = require('url');
|
|
5
7
|
|
|
6
8
|
/**
|
|
@@ -14,11 +16,14 @@ class FhirValidator {
|
|
|
14
16
|
*/
|
|
15
17
|
constructor(validatorJarPath, logger = null) {
|
|
16
18
|
this.validatorJarPath = validatorJarPath;
|
|
17
|
-
this.logger = logger;
|
|
19
|
+
this.logger = logger;
|
|
18
20
|
this.process = null;
|
|
19
21
|
this.port = null;
|
|
20
22
|
this.baseUrl = null;
|
|
21
23
|
this.isReady = false;
|
|
24
|
+
|
|
25
|
+
// Version tracking file sits alongside the JAR
|
|
26
|
+
this.versionFilePath = validatorJarPath + '.version';
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
/**
|
|
@@ -38,10 +43,8 @@ class FhirValidator {
|
|
|
38
43
|
*/
|
|
39
44
|
log(level, message, meta = {}) {
|
|
40
45
|
if (this.logger) {
|
|
41
|
-
// Use the Winston logger if available
|
|
42
46
|
this.logger[level](message, meta);
|
|
43
47
|
} else {
|
|
44
|
-
// Fall back to console
|
|
45
48
|
if (level === 'error') {
|
|
46
49
|
console.error(message, meta);
|
|
47
50
|
} else if (level === 'warn') {
|
|
@@ -52,6 +55,248 @@ class FhirValidator {
|
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Get the latest release information from GitHub
|
|
60
|
+
* @returns {Promise<{version: string, downloadUrl: string}>}
|
|
61
|
+
*/
|
|
62
|
+
async getLatestRelease() {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const options = {
|
|
65
|
+
hostname: 'api.github.com',
|
|
66
|
+
path: '/repos/hapifhir/org.hl7.fhir.core/releases/latest',
|
|
67
|
+
method: 'GET',
|
|
68
|
+
headers: {
|
|
69
|
+
'User-Agent': 'fhir-validator-node',
|
|
70
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const req = https.request(options, (res) => {
|
|
75
|
+
let data = '';
|
|
76
|
+
|
|
77
|
+
res.on('data', (chunk) => {
|
|
78
|
+
data += chunk;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
res.on('end', () => {
|
|
82
|
+
if (res.statusCode !== 200) {
|
|
83
|
+
reject(new Error(`GitHub API returned status ${res.statusCode}: ${data}`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const release = JSON.parse(data);
|
|
89
|
+
const version = release.tag_name;
|
|
90
|
+
|
|
91
|
+
// Find the validator_cli.jar asset
|
|
92
|
+
const asset = release.assets.find(a => a.name === 'validator_cli.jar');
|
|
93
|
+
if (!asset) {
|
|
94
|
+
reject(new Error('validator_cli.jar not found in latest release assets'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
resolve({
|
|
99
|
+
version,
|
|
100
|
+
downloadUrl: asset.browser_download_url,
|
|
101
|
+
publishedAt: release.published_at
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
reject(new Error(`Failed to parse GitHub response: ${error.message}`));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
req.on('error', reject);
|
|
110
|
+
req.setTimeout(30000, () => {
|
|
111
|
+
req.destroy();
|
|
112
|
+
reject(new Error('GitHub API request timeout'));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
req.end();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get the currently installed version
|
|
121
|
+
* @returns {string|null} - The installed version or null if not installed
|
|
122
|
+
*/
|
|
123
|
+
getInstalledVersion() {
|
|
124
|
+
try {
|
|
125
|
+
if (fs.existsSync(this.versionFilePath)) {
|
|
126
|
+
const versionInfo = JSON.parse(fs.readFileSync(this.versionFilePath, 'utf8'));
|
|
127
|
+
return versionInfo.version;
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.log('warn', `Failed to read version file: ${error.message}`);
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Save version information
|
|
137
|
+
* @param {string} version - The version string
|
|
138
|
+
* @param {string} downloadUrl - The URL it was downloaded from
|
|
139
|
+
*/
|
|
140
|
+
saveVersionInfo(version, downloadUrl) {
|
|
141
|
+
const versionInfo = {
|
|
142
|
+
version,
|
|
143
|
+
downloadUrl,
|
|
144
|
+
downloadedAt: new Date().toISOString()
|
|
145
|
+
};
|
|
146
|
+
fs.writeFileSync(this.versionFilePath, JSON.stringify(versionInfo, null, 2));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Download a file from a URL, following redirects
|
|
151
|
+
* @param {string} url - The URL to download from
|
|
152
|
+
* @param {string} destPath - The destination file path
|
|
153
|
+
* @returns {Promise<void>}
|
|
154
|
+
*/
|
|
155
|
+
async downloadFile(url, destPath) {
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
const downloadWithRedirects = (downloadUrl, redirectCount = 0) => {
|
|
158
|
+
if (redirectCount > 5) {
|
|
159
|
+
reject(new Error('Too many redirects'));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const parsedUrl = new URL(downloadUrl);
|
|
164
|
+
const protocol = parsedUrl.protocol === 'https:' ? https : http;
|
|
165
|
+
|
|
166
|
+
const req = protocol.get(downloadUrl, {
|
|
167
|
+
headers: {
|
|
168
|
+
'User-Agent': 'fhir-validator-node'
|
|
169
|
+
}
|
|
170
|
+
}, (res) => {
|
|
171
|
+
// Handle redirects
|
|
172
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
173
|
+
this.log('info', `Following redirect to ${res.headers.location}`);
|
|
174
|
+
downloadWithRedirects(res.headers.location, redirectCount + 1);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (res.statusCode !== 200) {
|
|
179
|
+
reject(new Error(`Download failed with status ${res.statusCode}`));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Ensure directory exists
|
|
184
|
+
const dir = path.dirname(destPath);
|
|
185
|
+
if (!fs.existsSync(dir)) {
|
|
186
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Download to a temp file first, then rename
|
|
190
|
+
const tempPath = destPath + '.download';
|
|
191
|
+
const fileStream = fs.createWriteStream(tempPath);
|
|
192
|
+
|
|
193
|
+
const contentLength = parseInt(res.headers['content-length'], 10);
|
|
194
|
+
let downloadedBytes = 0;
|
|
195
|
+
let lastLoggedPercent = 0;
|
|
196
|
+
|
|
197
|
+
res.on('data', (chunk) => {
|
|
198
|
+
downloadedBytes += chunk.length;
|
|
199
|
+
if (contentLength) {
|
|
200
|
+
const percent = Math.floor((downloadedBytes / contentLength) * 100);
|
|
201
|
+
if (percent >= lastLoggedPercent + 10) {
|
|
202
|
+
this.log('info', `Download progress: ${percent}% (${Math.round(downloadedBytes / 1024 / 1024)}MB / ${Math.round(contentLength / 1024 / 1024)}MB)`);
|
|
203
|
+
lastLoggedPercent = percent;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
res.pipe(fileStream);
|
|
209
|
+
|
|
210
|
+
fileStream.on('finish', () => {
|
|
211
|
+
fileStream.close(() => {
|
|
212
|
+
// Rename temp file to final destination
|
|
213
|
+
fs.renameSync(tempPath, destPath);
|
|
214
|
+
resolve();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
fileStream.on('error', (err) => {
|
|
219
|
+
// Clean up temp file on error
|
|
220
|
+
if (fs.existsSync(tempPath)) {
|
|
221
|
+
fs.unlinkSync(tempPath);
|
|
222
|
+
}
|
|
223
|
+
reject(err);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
req.on('error', reject);
|
|
228
|
+
req.setTimeout(300000, () => { // 5 minute timeout for large file
|
|
229
|
+
req.destroy();
|
|
230
|
+
reject(new Error('Download timeout'));
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
downloadWithRedirects(url);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Ensure the validator JAR is downloaded and up to date
|
|
240
|
+
* @param {Object} [options] - Options for the update check
|
|
241
|
+
* @param {boolean} [options.force=false] - Force download even if current version is up to date
|
|
242
|
+
* @param {boolean} [options.skipUpdateCheck=false] - Skip checking for updates if JAR exists
|
|
243
|
+
* @returns {Promise<{version: string, updated: boolean, downloaded: boolean}>}
|
|
244
|
+
*/
|
|
245
|
+
async ensureValidator(options = {}) {
|
|
246
|
+
const { force = false, skipUpdateCheck = false } = options;
|
|
247
|
+
|
|
248
|
+
const jarExists = fs.existsSync(this.validatorJarPath);
|
|
249
|
+
const installedVersion = this.getInstalledVersion();
|
|
250
|
+
|
|
251
|
+
// If JAR exists and we're skipping update checks, we're done
|
|
252
|
+
if (jarExists && skipUpdateCheck && !force) {
|
|
253
|
+
this.log('info', `Using existing validator JAR (version check skipped)`);
|
|
254
|
+
return {
|
|
255
|
+
version: installedVersion || 'unknown',
|
|
256
|
+
updated: false,
|
|
257
|
+
downloaded: false
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check for latest version
|
|
262
|
+
this.log('info', 'Checking for latest FHIR validator release...');
|
|
263
|
+
const latest = await this.getLatestRelease();
|
|
264
|
+
this.log('info', `Latest version: ${latest.version}`);
|
|
265
|
+
|
|
266
|
+
// Determine if we need to download
|
|
267
|
+
const needsDownload = force ||
|
|
268
|
+
!jarExists ||
|
|
269
|
+
!installedVersion ||
|
|
270
|
+
installedVersion !== latest.version;
|
|
271
|
+
|
|
272
|
+
if (!needsDownload) {
|
|
273
|
+
this.log('info', `Validator is up to date (${installedVersion})`);
|
|
274
|
+
return {
|
|
275
|
+
version: installedVersion,
|
|
276
|
+
updated: false,
|
|
277
|
+
downloaded: false
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Download the JAR
|
|
282
|
+
if (installedVersion && jarExists) {
|
|
283
|
+
this.log('info', `Updating validator from ${installedVersion} to ${latest.version}...`);
|
|
284
|
+
} else {
|
|
285
|
+
this.log('info', `Downloading validator ${latest.version}...`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await this.downloadFile(latest.downloadUrl, this.validatorJarPath);
|
|
289
|
+
this.saveVersionInfo(latest.version, latest.downloadUrl);
|
|
290
|
+
|
|
291
|
+
this.log('info', `Validator ${latest.version} downloaded successfully`);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
version: latest.version,
|
|
295
|
+
updated: installedVersion !== null && installedVersion !== latest.version,
|
|
296
|
+
downloaded: true
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
55
300
|
/**
|
|
56
301
|
* Start the FHIR validator service
|
|
57
302
|
* @param {Object} config - Configuration object
|
|
@@ -61,6 +306,8 @@ class FhirValidator {
|
|
|
61
306
|
* @param {string[]} [config.igs] - Array of implementation guide packages (e.g., ["hl7.fhir.us.core#6.0.0"])
|
|
62
307
|
* @param {number} [config.port=8080] - Port to run the service on
|
|
63
308
|
* @param {number} [config.timeout=30000] - Timeout in ms to wait for service to be ready
|
|
309
|
+
* @param {boolean} [config.autoDownload=true] - Automatically download/update validator JAR
|
|
310
|
+
* @param {boolean} [config.skipUpdateCheck=false] - Skip checking for updates if JAR exists
|
|
64
311
|
* @returns {Promise<void>}
|
|
65
312
|
*/
|
|
66
313
|
async start(config) {
|
|
@@ -68,12 +315,28 @@ class FhirValidator {
|
|
|
68
315
|
throw new Error('Validator service is already running');
|
|
69
316
|
}
|
|
70
317
|
|
|
71
|
-
const {
|
|
318
|
+
const {
|
|
319
|
+
version,
|
|
320
|
+
txServer,
|
|
321
|
+
txLog,
|
|
322
|
+
igs = [],
|
|
323
|
+
port = 8080,
|
|
324
|
+
timeout = 30000,
|
|
325
|
+
autoDownload = true,
|
|
326
|
+
skipUpdateCheck = false
|
|
327
|
+
} = config;
|
|
72
328
|
|
|
73
329
|
if (!version || !txServer || !txLog) {
|
|
74
330
|
throw new Error('version, txServer, and txLog are required');
|
|
75
331
|
}
|
|
76
332
|
|
|
333
|
+
// Ensure validator is downloaded if autoDownload is enabled
|
|
334
|
+
if (autoDownload) {
|
|
335
|
+
await this.ensureValidator({ skipUpdateCheck });
|
|
336
|
+
} else if (!fs.existsSync(this.validatorJarPath)) {
|
|
337
|
+
throw new Error(`Validator JAR not found at ${this.validatorJarPath}. Set autoDownload: true or download manually.`);
|
|
338
|
+
}
|
|
339
|
+
|
|
77
340
|
this.port = port;
|
|
78
341
|
this.baseUrl = `http://localhost:${port}`;
|
|
79
342
|
|
|
@@ -82,7 +345,7 @@ class FhirValidator {
|
|
|
82
345
|
'-jar', this.validatorJarPath,
|
|
83
346
|
'-server', port.toString(),
|
|
84
347
|
'-tx', txServer,
|
|
85
|
-
'-
|
|
348
|
+
'-txLog', txLog,
|
|
86
349
|
'-version', version
|
|
87
350
|
];
|
|
88
351
|
|
|
@@ -115,7 +378,7 @@ class FhirValidator {
|
|
|
115
378
|
lines.forEach(line => {
|
|
116
379
|
// Remove ANSI escape sequences (color codes, etc.)
|
|
117
380
|
const cleanLine = line.replace(/\u001b\[[0-9;]*m/g, '').trim();
|
|
118
|
-
if (cleanLine.length > 1) {
|
|
381
|
+
if (cleanLine.length > 1) {
|
|
119
382
|
this.log('info', `Validator: ${cleanLine}`);
|
|
120
383
|
}
|
|
121
384
|
});
|
|
@@ -366,6 +629,132 @@ class FhirValidator {
|
|
|
366
629
|
});
|
|
367
630
|
}
|
|
368
631
|
|
|
632
|
+
/**
|
|
633
|
+
* Run a terminology server test
|
|
634
|
+
* @param {Object} params - Test parameters
|
|
635
|
+
* @param {string} params.server - The address of the terminology server to test
|
|
636
|
+
* @param {string} params.suiteName - The suite name that contains the test to run
|
|
637
|
+
* @param {string} params.testName - The test name to run
|
|
638
|
+
* @param {string} params.version - What FHIR version to use for the test
|
|
639
|
+
* @param {string} [params.externalFile] - Optional name of messages file
|
|
640
|
+
* @param {string} [params.modes] - Optional comma delimited string of modes
|
|
641
|
+
* @returns {Promise<{result: boolean, message?: string}>}
|
|
642
|
+
*/
|
|
643
|
+
async runTxTest(params) {
|
|
644
|
+
if (!this.isReady) {
|
|
645
|
+
throw new Error('Validator service is not ready');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const { server, suiteName, testName, version, externalFile, modes } = params;
|
|
649
|
+
|
|
650
|
+
if (!server || !suiteName || !testName || !version) {
|
|
651
|
+
throw new Error('server, suiteName, testName, and version are required');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Build query parameters
|
|
655
|
+
const queryParams = new URLSearchParams();
|
|
656
|
+
queryParams.set('server', server);
|
|
657
|
+
queryParams.set('suite', suiteName);
|
|
658
|
+
queryParams.set('test', testName);
|
|
659
|
+
queryParams.set('version', version);
|
|
660
|
+
|
|
661
|
+
if (externalFile) {
|
|
662
|
+
queryParams.set('externalFile', externalFile);
|
|
663
|
+
}
|
|
664
|
+
if (modes) {
|
|
665
|
+
queryParams.set('modes', modes);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const url = `${this.baseUrl}/txTest?${queryParams.toString()}`;
|
|
669
|
+
|
|
670
|
+
return new Promise((resolve) => {
|
|
671
|
+
const parsedUrl = new URL(url);
|
|
672
|
+
const requestOptions = {
|
|
673
|
+
hostname: parsedUrl.hostname,
|
|
674
|
+
port: parsedUrl.port,
|
|
675
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
676
|
+
method: 'GET',
|
|
677
|
+
headers: {
|
|
678
|
+
'Accept': 'application/fhir+json'
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
const req = http.request(requestOptions, (res) => {
|
|
683
|
+
let data = '';
|
|
684
|
+
|
|
685
|
+
res.on('data', (chunk) => {
|
|
686
|
+
data += chunk;
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
res.on('end', () => {
|
|
690
|
+
// Handle HTTP errors
|
|
691
|
+
if (res.statusCode >= 400) {
|
|
692
|
+
resolve({
|
|
693
|
+
result: false,
|
|
694
|
+
message: `HTTP error ${res.statusCode}: ${data}`
|
|
695
|
+
});
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
try {
|
|
700
|
+
const outcome = JSON.parse(data);
|
|
701
|
+
|
|
702
|
+
// Check if it's a valid OperationOutcome
|
|
703
|
+
if (outcome.resourceType !== 'OperationOutcome') {
|
|
704
|
+
resolve({
|
|
705
|
+
result: false,
|
|
706
|
+
message: `Unexpected response type: ${outcome.resourceType || 'unknown'}`
|
|
707
|
+
});
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// No issues means success
|
|
712
|
+
if (!outcome.issue || outcome.issue.length === 0) {
|
|
713
|
+
resolve({ result: true });
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Check for error severity issues
|
|
718
|
+
const errorIssue = outcome.issue.find(issue => issue.severity === 'error');
|
|
719
|
+
if (errorIssue) {
|
|
720
|
+
resolve({
|
|
721
|
+
result: false,
|
|
722
|
+
message: errorIssue.details?.text || errorIssue.diagnostics || 'Test failed with error'
|
|
723
|
+
});
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// No error issues, test passed
|
|
728
|
+
resolve({ result: true });
|
|
729
|
+
|
|
730
|
+
} catch (error) {
|
|
731
|
+
resolve({
|
|
732
|
+
result: false,
|
|
733
|
+
message: `Failed to parse response: ${error.message}`
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
req.on('error', (error) => {
|
|
740
|
+
resolve({
|
|
741
|
+
result: false,
|
|
742
|
+
message: `Request failed: ${error.message}`
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
req.setTimeout(60000, () => {
|
|
747
|
+
req.destroy();
|
|
748
|
+
resolve({
|
|
749
|
+
result: false,
|
|
750
|
+
message: 'Request timeout'
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
req.end();
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
369
758
|
/**
|
|
370
759
|
* Stop the validator service
|
|
371
760
|
* @returns {Promise<void>}
|
|
@@ -383,19 +772,16 @@ class FhirValidator {
|
|
|
383
772
|
}
|
|
384
773
|
this.cleanup();
|
|
385
774
|
resolve();
|
|
386
|
-
}, 10000);
|
|
775
|
+
}, 10000);
|
|
387
776
|
|
|
388
|
-
// Single exit handler
|
|
389
777
|
const onExit = () => {
|
|
390
778
|
clearTimeout(timeout);
|
|
391
779
|
this.cleanup();
|
|
392
780
|
resolve();
|
|
393
781
|
};
|
|
394
782
|
|
|
395
|
-
this.process.once('exit', onExit);
|
|
783
|
+
this.process.once('exit', onExit);
|
|
396
784
|
|
|
397
|
-
// Since Java process is blocking on System.in.read(), SIGTERM likely won't work
|
|
398
|
-
// Go straight to SIGKILL for immediate termination
|
|
399
785
|
this.log('info', 'Stopping validator process...');
|
|
400
786
|
this.process.kill('SIGKILL');
|
|
401
787
|
});
|
|
@@ -421,4 +807,4 @@ class FhirValidator {
|
|
|
421
807
|
}
|
|
422
808
|
}
|
|
423
809
|
|
|
424
|
-
module.exports = FhirValidator;
|
|
810
|
+
module.exports = FhirValidator;
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -30,13 +30,22 @@ There are many ways to contribute:
|
|
|
30
30
|
|
|
31
31
|
## Overview
|
|
32
32
|
|
|
33
|
-
This library manages the lifecycle of the FHIR Validator Java service and provides a clean Node.js interface for validation operations. It handles process management, HTTP communication, and provides typed validation options.
|
|
33
|
+
This library manages the lifecycle of the FHIR Validator Java service and provides a clean Node.js interface for validation operations. It handles automatic downloading of the validator JAR, process management, HTTP communication, and provides typed validation options.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Automatic JAR Management**: Automatically downloads and updates the FHIR Validator CLI JAR from GitHub releases
|
|
38
|
+
- **Version Tracking**: Tracks installed version and checks for updates
|
|
39
|
+
- **Resource Validation**: Validate FHIR resources in JSON or XML format
|
|
40
|
+
- **Profile Validation**: Validate against specific FHIR profiles
|
|
41
|
+
- **Implementation Guide Support**: Load IGs at startup or runtime
|
|
42
|
+
- **Terminology Testing**: Run terminology server tests with `runTxTest`
|
|
34
43
|
|
|
35
44
|
## Prerequisites
|
|
36
45
|
|
|
37
46
|
- Node.js 12.0.0 or higher
|
|
38
47
|
- Java 8 or higher
|
|
39
|
-
-
|
|
48
|
+
- Internet connection (for automatic JAR download, or manually download from [GitHub releases](https://github.com/hapifhir/org.hl7.fhir.core/releases))
|
|
40
49
|
|
|
41
50
|
## Installation
|
|
42
51
|
|
|
@@ -46,21 +55,15 @@ npm install fhir-validator-wrapper
|
|
|
46
55
|
|
|
47
56
|
## Quick Start
|
|
48
57
|
|
|
49
|
-
```bash
|
|
50
|
-
# Run the example with your JAR file
|
|
51
|
-
FHIR_VALIDATOR_JAR_PATH=./your-validator.jar npm start
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Or in code:
|
|
55
|
-
|
|
56
58
|
```javascript
|
|
57
59
|
const FhirValidator = require('fhir-validator-wrapper');
|
|
58
60
|
|
|
59
61
|
async function validateResource() {
|
|
62
|
+
// The JAR will be automatically downloaded if not present
|
|
60
63
|
const validator = new FhirValidator('./validator_cli.jar');
|
|
61
64
|
|
|
62
65
|
try {
|
|
63
|
-
// Start the validator service
|
|
66
|
+
// Start the validator service (auto-downloads JAR if needed)
|
|
64
67
|
await validator.start({
|
|
65
68
|
version: '5.0.0',
|
|
66
69
|
txServer: 'http://tx.fhir.org/r5',
|
|
@@ -89,14 +92,62 @@ async function validateResource() {
|
|
|
89
92
|
|
|
90
93
|
### Constructor
|
|
91
94
|
|
|
92
|
-
#### `new FhirValidator(validatorJarPath)`
|
|
95
|
+
#### `new FhirValidator(validatorJarPath, logger)`
|
|
93
96
|
|
|
94
97
|
Creates a new FHIR validator instance.
|
|
95
98
|
|
|
96
|
-
- `validatorJarPath` (string): Path to the FHIR validator CLI JAR file
|
|
99
|
+
- `validatorJarPath` (string): Path to the FHIR validator CLI JAR file (will be downloaded here if not present)
|
|
100
|
+
- `logger` (Object, optional): Winston logger instance for custom logging
|
|
97
101
|
|
|
98
102
|
### Methods
|
|
99
103
|
|
|
104
|
+
#### `ensureValidator(options)`
|
|
105
|
+
|
|
106
|
+
Checks for and downloads/updates the validator JAR as needed. This is called automatically by `start()` when `autoDownload` is enabled.
|
|
107
|
+
|
|
108
|
+
**Parameters:**
|
|
109
|
+
- `options` (Object, optional): Configuration object
|
|
110
|
+
- `force` (boolean): Force download even if current version is up to date (default: false)
|
|
111
|
+
- `skipUpdateCheck` (boolean): Skip checking for updates if JAR exists (default: false)
|
|
112
|
+
|
|
113
|
+
**Returns:** `Promise<{version: string, updated: boolean, downloaded: boolean}>`
|
|
114
|
+
|
|
115
|
+
**Example:**
|
|
116
|
+
```javascript
|
|
117
|
+
const validator = new FhirValidator('./validator_cli.jar');
|
|
118
|
+
|
|
119
|
+
// Check for updates and download if needed
|
|
120
|
+
const result = await validator.ensureValidator();
|
|
121
|
+
console.log(`Version: ${result.version}`);
|
|
122
|
+
console.log(`Downloaded: ${result.downloaded}`);
|
|
123
|
+
console.log(`Updated: ${result.updated}`);
|
|
124
|
+
|
|
125
|
+
// Force re-download
|
|
126
|
+
await validator.ensureValidator({ force: true });
|
|
127
|
+
|
|
128
|
+
// Skip update check (use existing JAR)
|
|
129
|
+
await validator.ensureValidator({ skipUpdateCheck: true });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### `getLatestRelease()`
|
|
133
|
+
|
|
134
|
+
Fetches the latest release information from GitHub.
|
|
135
|
+
|
|
136
|
+
**Returns:** `Promise<{version: string, downloadUrl: string, publishedAt: string}>`
|
|
137
|
+
|
|
138
|
+
**Example:**
|
|
139
|
+
```javascript
|
|
140
|
+
const latest = await validator.getLatestRelease();
|
|
141
|
+
console.log(`Latest version: ${latest.version}`);
|
|
142
|
+
console.log(`Published: ${latest.publishedAt}`);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### `getInstalledVersion()`
|
|
146
|
+
|
|
147
|
+
Gets the currently installed validator version.
|
|
148
|
+
|
|
149
|
+
**Returns:** `string|null` - The installed version or null if not installed
|
|
150
|
+
|
|
100
151
|
#### `start(config)`
|
|
101
152
|
|
|
102
153
|
Starts the FHIR validator service with the specified configuration.
|
|
@@ -109,6 +160,8 @@ Starts the FHIR validator service with the specified configuration.
|
|
|
109
160
|
- `igs` (string[], optional): Array of implementation guide packages
|
|
110
161
|
- `port` (number, optional): Port to run the service on (default: 8080)
|
|
111
162
|
- `timeout` (number, optional): Startup timeout in milliseconds (default: 30000)
|
|
163
|
+
- `autoDownload` (boolean, optional): Automatically download/update validator JAR (default: true)
|
|
164
|
+
- `skipUpdateCheck` (boolean, optional): Skip checking for updates if JAR exists (default: false)
|
|
112
165
|
|
|
113
166
|
**Returns:** `Promise<void>`
|
|
114
167
|
|
|
@@ -122,7 +175,9 @@ await validator.start({
|
|
|
122
175
|
'hl7.fhir.us.core#6.0.0',
|
|
123
176
|
'hl7.fhir.uv.sdc#3.0.0'
|
|
124
177
|
],
|
|
125
|
-
port: 8080
|
|
178
|
+
port: 8080,
|
|
179
|
+
autoDownload: true, // Download JAR if missing (default)
|
|
180
|
+
skipUpdateCheck: true // Don't check for updates every time
|
|
126
181
|
});
|
|
127
182
|
```
|
|
128
183
|
|
|
@@ -193,6 +248,47 @@ Loads an additional implementation guide at runtime.
|
|
|
193
248
|
await validator.loadIG('hl7.fhir.uv.ips', '1.1.0');
|
|
194
249
|
```
|
|
195
250
|
|
|
251
|
+
#### `runTxTest(params)`
|
|
252
|
+
|
|
253
|
+
Runs a terminology server test against a specified server.
|
|
254
|
+
|
|
255
|
+
**Parameters:**
|
|
256
|
+
- `params` (Object): Test parameters
|
|
257
|
+
- `server` (string): The address of the terminology server to test
|
|
258
|
+
- `suiteName` (string): The suite name that contains the test to run
|
|
259
|
+
- `testName` (string): The test name to run
|
|
260
|
+
- `version` (string): What FHIR version to use for the test
|
|
261
|
+
- `externalFile` (string, optional): Name of messages file
|
|
262
|
+
- `modes` (string, optional): Comma delimited string of modes
|
|
263
|
+
|
|
264
|
+
**Returns:** `Promise<{result: boolean, message?: string}>`
|
|
265
|
+
|
|
266
|
+
**Example:**
|
|
267
|
+
```javascript
|
|
268
|
+
// Run a terminology server test
|
|
269
|
+
const result = await validator.runTxTest({
|
|
270
|
+
server: 'http://tx-dev.fhir.org',
|
|
271
|
+
suiteName: 'metadata',
|
|
272
|
+
testName: 'metadata',
|
|
273
|
+
version: '5.0'
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (result.result) {
|
|
277
|
+
console.log('Test passed!');
|
|
278
|
+
} else {
|
|
279
|
+
console.log('Test failed:', result.message);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// With optional parameters
|
|
283
|
+
const result = await validator.runTxTest({
|
|
284
|
+
server: 'http://tx-dev.fhir.org',
|
|
285
|
+
suiteName: 'expand',
|
|
286
|
+
testName: 'expand-test-1',
|
|
287
|
+
version: '5.0',
|
|
288
|
+
modes: 'lenient,tx-resource-cache'
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
196
292
|
#### `stop()`
|
|
197
293
|
|
|
198
294
|
Stops the validator service and cleans up resources.
|
|
@@ -211,6 +307,79 @@ Performs a health check on the running service.
|
|
|
211
307
|
|
|
212
308
|
**Returns:** `Promise<void>`
|
|
213
309
|
|
|
310
|
+
## Automatic JAR Download
|
|
311
|
+
|
|
312
|
+
The library automatically manages the FHIR Validator CLI JAR file:
|
|
313
|
+
|
|
314
|
+
### Default Behavior
|
|
315
|
+
When you call `start()`, the library will:
|
|
316
|
+
1. Check if the JAR file exists at the specified path
|
|
317
|
+
2. If missing, download the latest version from GitHub releases
|
|
318
|
+
3. Track the version in a `.version` file alongside the JAR
|
|
319
|
+
|
|
320
|
+
### Version Tracking
|
|
321
|
+
Version information is stored in `{jarPath}.version`:
|
|
322
|
+
```json
|
|
323
|
+
{
|
|
324
|
+
"version": "6.3.4",
|
|
325
|
+
"downloadUrl": "https://github.com/hapifhir/org.hl7.fhir.core/releases/...",
|
|
326
|
+
"downloadedAt": "2024-01-15T10:30:00.000Z"
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Update Strategies
|
|
331
|
+
|
|
332
|
+
```javascript
|
|
333
|
+
// Always check for updates (default)
|
|
334
|
+
await validator.start({
|
|
335
|
+
version: '5.0.0',
|
|
336
|
+
txServer: 'http://tx.fhir.org/r5',
|
|
337
|
+
txLog: './txlog.txt'
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Skip update check for faster startup
|
|
341
|
+
await validator.start({
|
|
342
|
+
version: '5.0.0',
|
|
343
|
+
txServer: 'http://tx.fhir.org/r5',
|
|
344
|
+
txLog: './txlog.txt',
|
|
345
|
+
skipUpdateCheck: true
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Disable auto-download entirely (JAR must exist)
|
|
349
|
+
await validator.start({
|
|
350
|
+
version: '5.0.0',
|
|
351
|
+
txServer: 'http://tx.fhir.org/r5',
|
|
352
|
+
txLog: './txlog.txt',
|
|
353
|
+
autoDownload: false
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Manual Download Management
|
|
358
|
+
|
|
359
|
+
```javascript
|
|
360
|
+
const validator = new FhirValidator('./validator_cli.jar');
|
|
361
|
+
|
|
362
|
+
// Check what's available vs installed
|
|
363
|
+
const latest = await validator.getLatestRelease();
|
|
364
|
+
const installed = validator.getInstalledVersion();
|
|
365
|
+
|
|
366
|
+
console.log(`Latest: ${latest.version}`);
|
|
367
|
+
console.log(`Installed: ${installed || 'not installed'}`);
|
|
368
|
+
|
|
369
|
+
// Download/update without starting service
|
|
370
|
+
const result = await validator.ensureValidator();
|
|
371
|
+
|
|
372
|
+
// Force re-download
|
|
373
|
+
await validator.ensureValidator({ force: true });
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Download-Only Mode
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
# Use the example script to just download/update the JAR
|
|
380
|
+
node example.js --download-only
|
|
381
|
+
```
|
|
382
|
+
|
|
214
383
|
## Implementation Guide Loading
|
|
215
384
|
|
|
216
385
|
Implementation guides can be loaded in two ways:
|
|
@@ -297,6 +466,14 @@ await validator.start({
|
|
|
297
466
|
});
|
|
298
467
|
```
|
|
299
468
|
|
|
469
|
+
5. **Skip Update Checks in CI/CD**: For faster builds, skip update checks:
|
|
470
|
+
```javascript
|
|
471
|
+
await validator.start({
|
|
472
|
+
// ... other config
|
|
473
|
+
skipUpdateCheck: true
|
|
474
|
+
});
|
|
475
|
+
```
|
|
476
|
+
|
|
300
477
|
## Testing
|
|
301
478
|
|
|
302
479
|
The library includes comprehensive tests. You can run them in different ways depending on your setup:
|
|
@@ -306,51 +483,59 @@ The library includes comprehensive tests. You can run them in different ways dep
|
|
|
306
483
|
npm run test:unit
|
|
307
484
|
```
|
|
308
485
|
|
|
309
|
-
###
|
|
486
|
+
### GitHub API Tests (requires network)
|
|
310
487
|
```bash
|
|
311
|
-
|
|
312
|
-
FHIR_VALIDATOR_JAR_PATH=./your-validator.jar npm run test:integration
|
|
488
|
+
GITHUB_API_TESTS=1 npm test
|
|
313
489
|
```
|
|
314
490
|
|
|
315
|
-
###
|
|
491
|
+
### Download Tests (downloads ~300MB JAR)
|
|
316
492
|
```bash
|
|
317
|
-
|
|
318
|
-
FHIR_VALIDATOR_JAR_PATH=./your-validator.jar npm run test:manual
|
|
493
|
+
DOWNLOAD_TESTS=1 npm test
|
|
319
494
|
```
|
|
320
495
|
|
|
321
|
-
###
|
|
322
|
-
|
|
323
|
-
The JAR file location can be configured using the `FHIR_VALIDATOR_JAR_PATH` environment variable:
|
|
324
|
-
|
|
496
|
+
### Integration Tests (requires JAR file and network)
|
|
325
497
|
```bash
|
|
326
|
-
#
|
|
327
|
-
|
|
328
|
-
npm test
|
|
498
|
+
# With auto-download
|
|
499
|
+
INTEGRATION_TESTS=1 npm test
|
|
329
500
|
|
|
330
|
-
#
|
|
331
|
-
FHIR_VALIDATOR_JAR_PATH=./
|
|
332
|
-
|
|
333
|
-
# Option 3: Use npm script helper
|
|
334
|
-
npm run test:with-jar
|
|
501
|
+
# With specific JAR path
|
|
502
|
+
FHIR_VALIDATOR_JAR_PATH=./your-validator.jar INTEGRATION_TESTS=1 npm test
|
|
335
503
|
```
|
|
336
504
|
|
|
337
|
-
|
|
505
|
+
### Manual Testing
|
|
506
|
+
```bash
|
|
507
|
+
# Quick manual test (auto-downloads JAR if needed)
|
|
508
|
+
INTEGRATION_TESTS=1 npm run test:manual
|
|
509
|
+
```
|
|
338
510
|
|
|
339
511
|
## Troubleshooting
|
|
340
512
|
|
|
341
513
|
### Common Issues
|
|
342
514
|
|
|
343
515
|
1. **Java not found**: Ensure Java is installed and available in PATH
|
|
344
|
-
2. **JAR
|
|
516
|
+
2. **JAR download fails**: Check internet connection and GitHub accessibility
|
|
345
517
|
3. **Port conflicts**: Change the port if 8080 is already in use
|
|
346
518
|
4. **Memory issues**: Add JVM options by modifying the spawn command if needed
|
|
347
519
|
5. **Network timeouts**: Increase timeout values for slow networks
|
|
520
|
+
6. **GitHub rate limits**: Use `skipUpdateCheck: true` to avoid repeated API calls
|
|
348
521
|
|
|
349
522
|
### Debug Logging
|
|
350
523
|
|
|
351
|
-
The library logs validator stdout/stderr for debugging.
|
|
524
|
+
The library logs validator stdout/stderr for debugging. You can provide a Winston logger for custom logging:
|
|
525
|
+
|
|
526
|
+
```javascript
|
|
527
|
+
const winston = require('winston');
|
|
528
|
+
const logger = winston.createLogger({
|
|
529
|
+
level: 'debug',
|
|
530
|
+
transports: [new winston.transports.Console()]
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const validator = new FhirValidator('./validator_cli.jar', logger);
|
|
534
|
+
// Or set later:
|
|
535
|
+
validator.setLogger(logger);
|
|
536
|
+
```
|
|
352
537
|
|
|
353
538
|
## Support
|
|
354
539
|
|
|
355
540
|
For issues with this wrapper, please file a GitHub issue.
|
|
356
|
-
For FHIR validator issues, see the [official FHIR validator documentation](https://confluence.hl7.org/spaces/FHIR/pages/35718580/Using+the+FHIR+Validator).
|
|
541
|
+
For FHIR validator issues, see the [official FHIR validator documentation](https://confluence.hl7.org/spaces/FHIR/pages/35718580/Using+the+FHIR+Validator).
|