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.
Files changed (3) hide show
  1. package/fhir-validator.js +398 -12
  2. package/package.json +1 -1
  3. 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; // Store the 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 { 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;
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
- '-txlog', txLog,
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) { // Only log non-empty lines
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); // 10 second total timeout
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); // Use 'once' to avoid duplicate listeners
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhir-validator-wrapper",
3
- "version": "1.1.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": {
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).