fhir-validator-wrapper 1.0.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.
Files changed (3) hide show
  1. package/fhir-validator.js +451 -37
  2. package/package.json +3 -3
  3. package/readme.md +220 -35
package/fhir-validator.js CHANGED
@@ -1,18 +1,300 @@
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
  /**
7
9
  * Node.js wrapper for the FHIR Validator HTTP Service
8
10
  */
9
11
  class FhirValidator {
10
- constructor(validatorJarPath) {
12
+ /**
13
+ * Create a new FHIR Validator instance
14
+ * @param {string} validatorJarPath - Path to the validator JAR file
15
+ * @param {Object} [logger] - Winston logger instance (optional)
16
+ */
17
+ constructor(validatorJarPath, logger = null) {
11
18
  this.validatorJarPath = validatorJarPath;
19
+ this.logger = logger;
12
20
  this.process = null;
13
21
  this.port = null;
14
22
  this.baseUrl = null;
15
23
  this.isReady = false;
24
+
25
+ // Version tracking file sits alongside the JAR
26
+ this.versionFilePath = validatorJarPath + '.version';
27
+ }
28
+
29
+ /**
30
+ * Set a logger after initialization
31
+ * @param {Object} logger - Winston logger instance
32
+ */
33
+ setLogger(logger) {
34
+ this.logger = logger;
35
+ }
36
+
37
+ /**
38
+ * Log a message with the appropriate level
39
+ * @private
40
+ * @param {string} level - Log level ('info', 'error', 'warn')
41
+ * @param {string} message - Message to log
42
+ * @param {Object} [meta] - Optional metadata
43
+ */
44
+ log(level, message, meta = {}) {
45
+ if (this.logger) {
46
+ this.logger[level](message, meta);
47
+ } else {
48
+ if (level === 'error') {
49
+ console.error(message, meta);
50
+ } else if (level === 'warn') {
51
+ console.warn(message, meta);
52
+ } else {
53
+ console.log(message, meta);
54
+ }
55
+ }
56
+ }
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
+ };
16
298
  }
17
299
 
18
300
  /**
@@ -24,6 +306,8 @@ class FhirValidator {
24
306
  * @param {string[]} [config.igs] - Array of implementation guide packages (e.g., ["hl7.fhir.us.core#6.0.0"])
25
307
  * @param {number} [config.port=8080] - Port to run the service on
26
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
27
311
  * @returns {Promise<void>}
28
312
  */
29
313
  async start(config) {
@@ -31,12 +315,28 @@ class FhirValidator {
31
315
  throw new Error('Validator service is already running');
32
316
  }
33
317
 
34
- const { version, txServer, txLog, igs = [], port = 8080, timeout = 30000 } = config;
318
+ const {
319
+ version,
320
+ txServer,
321
+ txLog,
322
+ igs = [],
323
+ port = 8080,
324
+ timeout = 30000,
325
+ autoDownload = true,
326
+ skipUpdateCheck = false
327
+ } = config;
35
328
 
36
329
  if (!version || !txServer || !txLog) {
37
330
  throw new Error('version, txServer, and txLog are required');
38
331
  }
39
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
+
40
340
  this.port = port;
41
341
  this.baseUrl = `http://localhost:${port}`;
42
342
 
@@ -45,7 +345,7 @@ class FhirValidator {
45
345
  '-jar', this.validatorJarPath,
46
346
  '-server', port.toString(),
47
347
  '-tx', txServer,
48
- '-txlog', txLog,
348
+ '-txLog', txLog,
49
349
  '-version', version
50
350
  ];
51
351
 
@@ -54,7 +354,7 @@ class FhirValidator {
54
354
  args.push('-ig', ig);
55
355
  }
56
356
 
57
- console.log(`Starting FHIR validator with command: java ${args.join(' ')}`);
357
+ this.log('info', `Starting FHIR validator with command: java ${args.join(' ')}`);
58
358
 
59
359
  // Spawn the Java process
60
360
  this.process = spawn('java', args, {
@@ -63,12 +363,12 @@ class FhirValidator {
63
363
 
64
364
  // Handle process events
65
365
  this.process.on('error', (error) => {
66
- console.error('Failed to start validator process:', error);
366
+ this.log('error', 'Failed to start validator process:', error);
67
367
  throw error;
68
368
  });
69
369
 
70
370
  this.process.on('exit', (code, signal) => {
71
- console.log(`Validator process exited with code ${code} and signal ${signal}`);
371
+ this.log('info', `Validator process exited with code ${code} and signal ${signal}`);
72
372
  this.cleanup();
73
373
  });
74
374
 
@@ -78,19 +378,19 @@ class FhirValidator {
78
378
  lines.forEach(line => {
79
379
  // Remove ANSI escape sequences (color codes, etc.)
80
380
  const cleanLine = line.replace(/\u001b\[[0-9;]*m/g, '').trim();
81
- if (cleanLine.length > 1) { // Only log non-empty lines
82
- console.log(`Validator: ${cleanLine}`);
381
+ if (cleanLine.length > 1) {
382
+ this.log('info', `Validator: ${cleanLine}`);
83
383
  }
84
384
  });
85
385
  });
86
386
 
87
387
  this.process.stderr.on('data', (data) => {
88
- console.error(`Validator-err: ${data}`);
388
+ this.log('error', `Validator-err: ${data}`);
89
389
  });
90
390
 
91
391
  // Wait for the service to be ready
92
392
  await this.waitForReady(timeout);
93
- console.log('FHIR validator service is ready');
393
+ this.log('info', 'FHIR validator service is ready');
94
394
  }
95
395
 
96
396
  /**
@@ -100,7 +400,7 @@ class FhirValidator {
100
400
  */
101
401
  async waitForReady(timeout) {
102
402
  const startTime = Date.now();
103
-
403
+
104
404
  while (Date.now() - startTime < timeout) {
105
405
  try {
106
406
  await this.healthCheck();
@@ -111,7 +411,7 @@ class FhirValidator {
111
411
  await new Promise(resolve => setTimeout(resolve, 1000));
112
412
  }
113
413
  }
114
-
414
+
115
415
  throw new Error(`Validator service did not become ready within ${timeout}ms`);
116
416
  }
117
417
 
@@ -181,7 +481,7 @@ class FhirValidator {
181
481
 
182
482
  // Build query parameters
183
483
  const queryParams = new URLSearchParams();
184
-
484
+
185
485
  if (options.profiles && options.profiles.length > 0) {
186
486
  queryParams.set('profiles', options.profiles.join(','));
187
487
  }
@@ -216,11 +516,11 @@ class FhirValidator {
216
516
 
217
517
  const req = http.request(requestOptions, (res) => {
218
518
  let data = '';
219
-
519
+
220
520
  res.on('data', (chunk) => {
221
521
  data += chunk;
222
522
  });
223
-
523
+
224
524
  res.on('end', () => {
225
525
  try {
226
526
  const result = JSON.parse(data);
@@ -232,7 +532,7 @@ class FhirValidator {
232
532
  });
233
533
 
234
534
  req.on('error', reject);
235
-
535
+
236
536
  req.setTimeout(30000, () => {
237
537
  req.destroy();
238
538
  reject(new Error('Validation request timeout'));
@@ -254,7 +554,7 @@ class FhirValidator {
254
554
  if (!Buffer.isBuffer(resourceBytes)) {
255
555
  throw new Error('resourceBytes must be a Buffer');
256
556
  }
257
-
557
+
258
558
  return this.validate(resourceBytes, options);
259
559
  }
260
560
 
@@ -268,7 +568,7 @@ class FhirValidator {
268
568
  if (typeof resourceObject !== 'object' || resourceObject === null) {
269
569
  throw new Error('resourceObject must be an object');
270
570
  }
271
-
571
+
272
572
  return this.validate(resourceObject, options);
273
573
  }
274
574
 
@@ -303,11 +603,11 @@ class FhirValidator {
303
603
 
304
604
  const req = http.request(requestOptions, (res) => {
305
605
  let data = '';
306
-
606
+
307
607
  res.on('data', (chunk) => {
308
608
  data += chunk;
309
609
  });
310
-
610
+
311
611
  res.on('end', () => {
312
612
  try {
313
613
  const result = JSON.parse(data);
@@ -319,7 +619,7 @@ class FhirValidator {
319
619
  });
320
620
 
321
621
  req.on('error', reject);
322
-
622
+
323
623
  req.setTimeout(30000, () => {
324
624
  req.destroy();
325
625
  reject(new Error('Load IG request timeout'));
@@ -329,6 +629,132 @@ class FhirValidator {
329
629
  });
330
630
  }
331
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
+
332
758
  /**
333
759
  * Stop the validator service
334
760
  * @returns {Promise<void>}
@@ -340,36 +766,24 @@ class FhirValidator {
340
766
 
341
767
  return new Promise((resolve, reject) => {
342
768
  const timeout = setTimeout(() => {
343
- console.warn('Force killing validator process after timeout');
769
+ this.log('warn', 'Force killing validator process after timeout');
344
770
  if (this.process && !this.process.killed) {
345
771
  this.process.kill('SIGKILL');
346
772
  }
347
773
  this.cleanup();
348
774
  resolve();
349
- }, 10000); // 10 second total timeout
775
+ }, 10000);
350
776
 
351
- // Single exit handler
352
777
  const onExit = () => {
353
778
  clearTimeout(timeout);
354
779
  this.cleanup();
355
780
  resolve();
356
781
  };
357
782
 
358
- this.process.once('exit', onExit); // Use 'once' to avoid duplicate listeners
783
+ this.process.once('exit', onExit);
359
784
 
360
- // Since Java process is blocking on System.in.read(), SIGTERM likely won't work
361
- // Go straight to SIGKILL for immediate termination
362
- console.log('Stopping validator process...');
785
+ this.log('info', 'Stopping validator process...');
363
786
  this.process.kill('SIGKILL');
364
-
365
- // Backup: try SIGTERM first, then SIGKILL after 2 seconds
366
- // this.process.kill('SIGTERM');
367
- // setTimeout(() => {
368
- // if (this.process && !this.process.killed) {
369
- // console.log('Escalating to SIGKILL...');
370
- // this.process.kill('SIGKILL');
371
- // }
372
- // }, 2000);
373
787
  });
374
788
  }
375
789
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhir-validator-wrapper",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Node.js wrapper for the HL7 FHIR Validator CLI",
5
5
  "main": "fhir-validator.js",
6
6
  "scripts": {
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "https://github.com/FHIR/fhir-validator-wrapper.git"
29
+ "url": "git+https://github.com/FHIR/fhir-validator-wrapper.git"
30
30
  },
31
31
  "bugs": {
32
32
  "url": "https://github.com/FHIR/fhir-validator-wrapper/issues"
@@ -41,4 +41,4 @@
41
41
  "README.md",
42
42
  "LICENSE"
43
43
  ]
44
- }
44
+ }
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
- - FHIR Validator CLI JAR file (download from [GitHub releases](https://github.com/hapifhir/org.hl7.fhir.core/releases))
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
- ### Integration Tests (requires JAR file)
486
+ ### GitHub API Tests (requires network)
310
487
  ```bash
311
- # Set the JAR path and run integration tests
312
- FHIR_VALIDATOR_JAR_PATH=./your-validator.jar npm run test:integration
488
+ GITHUB_API_TESTS=1 npm test
313
489
  ```
314
490
 
315
- ### Manual Testing
491
+ ### Download Tests (downloads ~300MB JAR)
316
492
  ```bash
317
- # Quick manual test with your JAR
318
- FHIR_VALIDATOR_JAR_PATH=./your-validator.jar npm run test:manual
493
+ DOWNLOAD_TESTS=1 npm test
319
494
  ```
320
495
 
321
- ### Configuration
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
- # Option 1: Set environment variable
327
- export FHIR_VALIDATOR_JAR_PATH=/path/to/your/validator.jar
328
- npm test
498
+ # With auto-download
499
+ INTEGRATION_TESTS=1 npm test
329
500
 
330
- # Option 2: Inline with command
331
- FHIR_VALIDATOR_JAR_PATH=./validator_cli.jar npm test
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
- **Default behavior**: If no environment variable is set, tests will look for `./validator_cli.jar`
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 file not found**: Verify the validator JAR path is correct
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. Check console output for Java process messages.
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).