fhir-validator-wrapper 1.1.0 → 1.2.1

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 +416 -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,22 @@ 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';
27
+ this.version = undefined;
28
+ }
29
+
30
+ /**
31
+ * @returns what version the validator jar reports that it is
32
+ */
33
+ jarVersion() {
34
+ return this.version;
22
35
  }
23
36
 
24
37
  /**
@@ -38,10 +51,8 @@ class FhirValidator {
38
51
  */
39
52
  log(level, message, meta = {}) {
40
53
  if (this.logger) {
41
- // Use the Winston logger if available
42
54
  this.logger[level](message, meta);
43
55
  } else {
44
- // Fall back to console
45
56
  if (level === 'error') {
46
57
  console.error(message, meta);
47
58
  } else if (level === 'warn') {
@@ -52,6 +63,248 @@ class FhirValidator {
52
63
  }
53
64
  }
54
65
 
66
+ /**
67
+ * Get the latest release information from GitHub
68
+ * @returns {Promise<{version: string, downloadUrl: string}>}
69
+ */
70
+ async getLatestRelease() {
71
+ return new Promise((resolve, reject) => {
72
+ const options = {
73
+ hostname: 'api.github.com',
74
+ path: '/repos/hapifhir/org.hl7.fhir.core/releases/latest',
75
+ method: 'GET',
76
+ headers: {
77
+ 'User-Agent': 'fhir-validator-node',
78
+ 'Accept': 'application/vnd.github.v3+json'
79
+ }
80
+ };
81
+
82
+ const req = https.request(options, (res) => {
83
+ let data = '';
84
+
85
+ res.on('data', (chunk) => {
86
+ data += chunk;
87
+ });
88
+
89
+ res.on('end', () => {
90
+ if (res.statusCode !== 200) {
91
+ reject(new Error(`GitHub API returned status ${res.statusCode}: ${data}`));
92
+ return;
93
+ }
94
+
95
+ try {
96
+ const release = JSON.parse(data);
97
+ const version = release.tag_name;
98
+
99
+ // Find the validator_cli.jar asset
100
+ const asset = release.assets.find(a => a.name === 'validator_cli.jar');
101
+ if (!asset) {
102
+ reject(new Error('validator_cli.jar not found in latest release assets'));
103
+ return;
104
+ }
105
+
106
+ resolve({
107
+ version,
108
+ downloadUrl: asset.browser_download_url,
109
+ publishedAt: release.published_at
110
+ });
111
+ } catch (error) {
112
+ reject(new Error(`Failed to parse GitHub response: ${error.message}`));
113
+ }
114
+ });
115
+ });
116
+
117
+ req.on('error', reject);
118
+ req.setTimeout(30000, () => {
119
+ req.destroy();
120
+ reject(new Error('GitHub API request timeout'));
121
+ });
122
+
123
+ req.end();
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Get the currently installed version
129
+ * @returns {string|null} - The installed version or null if not installed
130
+ */
131
+ getInstalledVersion() {
132
+ try {
133
+ if (fs.existsSync(this.versionFilePath)) {
134
+ const versionInfo = JSON.parse(fs.readFileSync(this.versionFilePath, 'utf8'));
135
+ return versionInfo.version;
136
+ }
137
+ } catch (error) {
138
+ this.log('warn', `Failed to read version file: ${error.message}`);
139
+ }
140
+ return null;
141
+ }
142
+
143
+ /**
144
+ * Save version information
145
+ * @param {string} version - The version string
146
+ * @param {string} downloadUrl - The URL it was downloaded from
147
+ */
148
+ saveVersionInfo(version, downloadUrl) {
149
+ const versionInfo = {
150
+ version,
151
+ downloadUrl,
152
+ downloadedAt: new Date().toISOString()
153
+ };
154
+ fs.writeFileSync(this.versionFilePath, JSON.stringify(versionInfo, null, 2));
155
+ }
156
+
157
+ /**
158
+ * Download a file from a URL, following redirects
159
+ * @param {string} url - The URL to download from
160
+ * @param {string} destPath - The destination file path
161
+ * @returns {Promise<void>}
162
+ */
163
+ async downloadFile(url, destPath) {
164
+ return new Promise((resolve, reject) => {
165
+ const downloadWithRedirects = (downloadUrl, redirectCount = 0) => {
166
+ if (redirectCount > 5) {
167
+ reject(new Error('Too many redirects'));
168
+ return;
169
+ }
170
+
171
+ const parsedUrl = new URL(downloadUrl);
172
+ const protocol = parsedUrl.protocol === 'https:' ? https : http;
173
+
174
+ const req = protocol.get(downloadUrl, {
175
+ headers: {
176
+ 'User-Agent': 'fhir-validator-node'
177
+ }
178
+ }, (res) => {
179
+ // Handle redirects
180
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
181
+ this.log('info', `Following redirect to ${res.headers.location}`);
182
+ downloadWithRedirects(res.headers.location, redirectCount + 1);
183
+ return;
184
+ }
185
+
186
+ if (res.statusCode !== 200) {
187
+ reject(new Error(`Download failed with status ${res.statusCode}`));
188
+ return;
189
+ }
190
+
191
+ // Ensure directory exists
192
+ const dir = path.dirname(destPath);
193
+ if (!fs.existsSync(dir)) {
194
+ fs.mkdirSync(dir, { recursive: true });
195
+ }
196
+
197
+ // Download to a temp file first, then rename
198
+ const tempPath = destPath + '.download';
199
+ const fileStream = fs.createWriteStream(tempPath);
200
+
201
+ const contentLength = parseInt(res.headers['content-length'], 10);
202
+ let downloadedBytes = 0;
203
+ let lastLoggedPercent = 0;
204
+
205
+ res.on('data', (chunk) => {
206
+ downloadedBytes += chunk.length;
207
+ if (contentLength) {
208
+ const percent = Math.floor((downloadedBytes / contentLength) * 100);
209
+ if (percent >= lastLoggedPercent + 10) {
210
+ this.log('info', `Download progress: ${percent}% (${Math.round(downloadedBytes / 1024 / 1024)}MB / ${Math.round(contentLength / 1024 / 1024)}MB)`);
211
+ lastLoggedPercent = percent;
212
+ }
213
+ }
214
+ });
215
+
216
+ res.pipe(fileStream);
217
+
218
+ fileStream.on('finish', () => {
219
+ fileStream.close(() => {
220
+ // Rename temp file to final destination
221
+ fs.renameSync(tempPath, destPath);
222
+ resolve();
223
+ });
224
+ });
225
+
226
+ fileStream.on('error', (err) => {
227
+ // Clean up temp file on error
228
+ if (fs.existsSync(tempPath)) {
229
+ fs.unlinkSync(tempPath);
230
+ }
231
+ reject(err);
232
+ });
233
+ });
234
+
235
+ req.on('error', reject);
236
+ req.setTimeout(300000, () => { // 5 minute timeout for large file
237
+ req.destroy();
238
+ reject(new Error('Download timeout'));
239
+ });
240
+ };
241
+
242
+ downloadWithRedirects(url);
243
+ });
244
+ }
245
+
246
+ /**
247
+ * Ensure the validator JAR is downloaded and up to date
248
+ * @param {Object} [options] - Options for the update check
249
+ * @param {boolean} [options.force=false] - Force download even if current version is up to date
250
+ * @param {boolean} [options.skipUpdateCheck=false] - Skip checking for updates if JAR exists
251
+ * @returns {Promise<{version: string, updated: boolean, downloaded: boolean}>}
252
+ */
253
+ async ensureValidator(options = {}) {
254
+ const { force = false, skipUpdateCheck = false } = options;
255
+
256
+ const jarExists = fs.existsSync(this.validatorJarPath);
257
+ const installedVersion = this.getInstalledVersion();
258
+
259
+ // If JAR exists and we're skipping update checks, we're done
260
+ if (jarExists && skipUpdateCheck && !force) {
261
+ this.log('info', `Using existing validator JAR (version check skipped)`);
262
+ return {
263
+ version: installedVersion || 'unknown',
264
+ updated: false,
265
+ downloaded: false
266
+ };
267
+ }
268
+
269
+ // Check for latest version
270
+ this.log('info', 'Checking for latest FHIR validator release...');
271
+ const latest = await this.getLatestRelease();
272
+ this.log('info', `Latest version: ${latest.version}`);
273
+
274
+ // Determine if we need to download
275
+ const needsDownload = force ||
276
+ !jarExists ||
277
+ !installedVersion ||
278
+ installedVersion !== latest.version;
279
+
280
+ if (!needsDownload) {
281
+ this.log('info', `Validator is up to date (${installedVersion})`);
282
+ return {
283
+ version: installedVersion,
284
+ updated: false,
285
+ downloaded: false
286
+ };
287
+ }
288
+
289
+ // Download the JAR
290
+ if (installedVersion && jarExists) {
291
+ this.log('info', `Updating validator from ${installedVersion} to ${latest.version}...`);
292
+ } else {
293
+ this.log('info', `Downloading validator ${latest.version}...`);
294
+ }
295
+
296
+ await this.downloadFile(latest.downloadUrl, this.validatorJarPath);
297
+ this.saveVersionInfo(latest.version, latest.downloadUrl);
298
+
299
+ this.log('info', `Validator ${latest.version} downloaded successfully`);
300
+
301
+ return {
302
+ version: latest.version,
303
+ updated: installedVersion !== null && installedVersion !== latest.version,
304
+ downloaded: true
305
+ };
306
+ }
307
+
55
308
  /**
56
309
  * Start the FHIR validator service
57
310
  * @param {Object} config - Configuration object
@@ -61,6 +314,8 @@ class FhirValidator {
61
314
  * @param {string[]} [config.igs] - Array of implementation guide packages (e.g., ["hl7.fhir.us.core#6.0.0"])
62
315
  * @param {number} [config.port=8080] - Port to run the service on
63
316
  * @param {number} [config.timeout=30000] - Timeout in ms to wait for service to be ready
317
+ * @param {boolean} [config.autoDownload=true] - Automatically download/update validator JAR
318
+ * @param {boolean} [config.skipUpdateCheck=false] - Skip checking for updates if JAR exists
64
319
  * @returns {Promise<void>}
65
320
  */
66
321
  async start(config) {
@@ -68,12 +323,28 @@ class FhirValidator {
68
323
  throw new Error('Validator service is already running');
69
324
  }
70
325
 
71
- const { version, txServer, txLog, igs = [], port = 8080, timeout = 30000 } = config;
326
+ const {
327
+ version,
328
+ txServer,
329
+ txLog,
330
+ igs = [],
331
+ port = 8080,
332
+ timeout = 30000,
333
+ autoDownload = true,
334
+ skipUpdateCheck = false
335
+ } = config;
72
336
 
73
337
  if (!version || !txServer || !txLog) {
74
338
  throw new Error('version, txServer, and txLog are required');
75
339
  }
76
340
 
341
+ // Ensure validator is downloaded if autoDownload is enabled
342
+ if (autoDownload) {
343
+ await this.ensureValidator({ skipUpdateCheck });
344
+ } else if (!fs.existsSync(this.validatorJarPath)) {
345
+ throw new Error(`Validator JAR not found at ${this.validatorJarPath}. Set autoDownload: true or download manually.`);
346
+ }
347
+
77
348
  this.port = port;
78
349
  this.baseUrl = `http://localhost:${port}`;
79
350
 
@@ -82,7 +353,7 @@ class FhirValidator {
82
353
  '-jar', this.validatorJarPath,
83
354
  '-server', port.toString(),
84
355
  '-tx', txServer,
85
- '-txlog', txLog,
356
+ '-txLog', txLog,
86
357
  '-version', version
87
358
  ];
88
359
 
@@ -115,7 +386,8 @@ class FhirValidator {
115
386
  lines.forEach(line => {
116
387
  // Remove ANSI escape sequences (color codes, etc.)
117
388
  const cleanLine = line.replace(/\u001b\[[0-9;]*m/g, '').trim();
118
- if (cleanLine.length > 1) { // Only log non-empty lines
389
+ if (cleanLine.length > 1) {
390
+ this.checkForVersion(cleanLine);
119
391
  this.log('info', `Validator: ${cleanLine}`);
120
392
  }
121
393
  });
@@ -366,6 +638,132 @@ class FhirValidator {
366
638
  });
367
639
  }
368
640
 
641
+ /**
642
+ * Run a terminology server test
643
+ * @param {Object} params - Test parameters
644
+ * @param {string} params.server - The address of the terminology server to test
645
+ * @param {string} params.suiteName - The suite name that contains the test to run
646
+ * @param {string} params.testName - The test name to run
647
+ * @param {string} params.version - What FHIR version to use for the test
648
+ * @param {string} [params.externalFile] - Optional name of messages file
649
+ * @param {string} [params.modes] - Optional comma delimited string of modes
650
+ * @returns {Promise<{result: boolean, message?: string}>}
651
+ */
652
+ async runTxTest(params) {
653
+ if (!this.isReady) {
654
+ throw new Error('Validator service is not ready');
655
+ }
656
+
657
+ const { server, suiteName, testName, version, externalFile, modes } = params;
658
+
659
+ if (!server || !suiteName || !testName || !version) {
660
+ throw new Error('server, suiteName, testName, and version are required');
661
+ }
662
+
663
+ // Build query parameters
664
+ const queryParams = new URLSearchParams();
665
+ queryParams.set('server', server);
666
+ queryParams.set('suite', suiteName);
667
+ queryParams.set('test', testName);
668
+ queryParams.set('version', version);
669
+
670
+ if (externalFile) {
671
+ queryParams.set('externalFile', externalFile);
672
+ }
673
+ if (modes) {
674
+ queryParams.set('modes', modes);
675
+ }
676
+
677
+ const url = `${this.baseUrl}/txTest?${queryParams.toString()}`;
678
+
679
+ return new Promise((resolve) => {
680
+ const parsedUrl = new URL(url);
681
+ const requestOptions = {
682
+ hostname: parsedUrl.hostname,
683
+ port: parsedUrl.port,
684
+ path: parsedUrl.pathname + parsedUrl.search,
685
+ method: 'GET',
686
+ headers: {
687
+ 'Accept': 'application/fhir+json'
688
+ }
689
+ };
690
+
691
+ const req = http.request(requestOptions, (res) => {
692
+ let data = '';
693
+
694
+ res.on('data', (chunk) => {
695
+ data += chunk;
696
+ });
697
+
698
+ res.on('end', () => {
699
+ // Handle HTTP errors
700
+ if (res.statusCode >= 400) {
701
+ resolve({
702
+ result: false,
703
+ message: `HTTP error ${res.statusCode}: ${data}`
704
+ });
705
+ return;
706
+ }
707
+
708
+ try {
709
+ const outcome = JSON.parse(data);
710
+
711
+ // Check if it's a valid OperationOutcome
712
+ if (outcome.resourceType !== 'OperationOutcome') {
713
+ resolve({
714
+ result: false,
715
+ message: `Unexpected response type: ${outcome.resourceType || 'unknown'}`
716
+ });
717
+ return;
718
+ }
719
+
720
+ // No issues means success
721
+ if (!outcome.issue || outcome.issue.length === 0) {
722
+ resolve({ result: true });
723
+ return;
724
+ }
725
+
726
+ // Check for error severity issues
727
+ const errorIssue = outcome.issue.find(issue => issue.severity === 'error');
728
+ if (errorIssue) {
729
+ resolve({
730
+ result: false,
731
+ message: errorIssue.details?.text || errorIssue.diagnostics || 'Test failed with error'
732
+ });
733
+ return;
734
+ }
735
+
736
+ // No error issues, test passed
737
+ resolve({ result: true });
738
+
739
+ } catch (error) {
740
+ resolve({
741
+ result: false,
742
+ message: `Failed to parse response: ${error.message}`
743
+ });
744
+ }
745
+ });
746
+ });
747
+
748
+ req.on('error', (error) => {
749
+ resolve({
750
+ result: false,
751
+ message: `Request failed: ${error.message}`
752
+ });
753
+ });
754
+
755
+ req.setTimeout(60000, () => {
756
+ req.destroy();
757
+ resolve({
758
+ result: false,
759
+ message: 'Request timeout'
760
+ });
761
+ });
762
+
763
+ req.end();
764
+ });
765
+ }
766
+
369
767
  /**
370
768
  * Stop the validator service
371
769
  * @returns {Promise<void>}
@@ -383,19 +781,16 @@ class FhirValidator {
383
781
  }
384
782
  this.cleanup();
385
783
  resolve();
386
- }, 10000); // 10 second total timeout
784
+ }, 10000);
387
785
 
388
- // Single exit handler
389
786
  const onExit = () => {
390
787
  clearTimeout(timeout);
391
788
  this.cleanup();
392
789
  resolve();
393
790
  };
394
791
 
395
- this.process.once('exit', onExit); // Use 'once' to avoid duplicate listeners
792
+ this.process.once('exit', onExit);
396
793
 
397
- // Since Java process is blocking on System.in.read(), SIGTERM likely won't work
398
- // Go straight to SIGKILL for immediate termination
399
794
  this.log('info', 'Stopping validator process...');
400
795
  this.process.kill('SIGKILL');
401
796
  });
@@ -419,6 +814,15 @@ class FhirValidator {
419
814
  isRunning() {
420
815
  return this.process !== null && this.isReady;
421
816
  }
817
+
818
+ checkForVersion(cleanLine) {
819
+ if (!this.version) {
820
+ if (cleanLine.startsWith('FHIR Validation tool Version')) {
821
+ let parts = cleanLine.split(' ');
822
+ this.version = parts[4];
823
+ }
824
+ }
825
+ }
422
826
  }
423
827
 
424
- module.exports = FhirValidator;
828
+ 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.1",
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).