fhirsmith 0.8.5 → 0.9.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 (41) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +52 -22
  3. package/extension-tracker/extension-tracker-template.html +3 -1
  4. package/library/html-server.js +7 -0
  5. package/library/logger.js +234 -194
  6. package/library/regex-utilities.js +13 -0
  7. package/package.json +4 -2
  8. package/packages/packages-template.html +3 -1
  9. package/publisher/publisher-template.html +1 -0
  10. package/publisher/publisher.js +28 -7
  11. package/registry/registry-template.html +3 -1
  12. package/root-bare-template.html +9759 -37
  13. package/root-template.html +3 -2
  14. package/server.js +48 -12
  15. package/translations/Messages.properties +2 -1
  16. package/translations/rendering-phrases.properties +3 -1
  17. package/tx/cs/cs-api.js +4 -0
  18. package/tx/cs/cs-country.js +2 -1
  19. package/tx/cs/cs-cs.js +9 -4
  20. package/tx/cs/cs-loinc.js +2 -1
  21. package/tx/cs/cs-snomed.js +5 -1
  22. package/tx/data/OperationDefinition-ValueSet-related.json +133 -0
  23. package/tx/html/tx-template.html +3 -2
  24. package/tx/importers/atc-to-fhir.js +27 -27
  25. package/tx/library/codesystem.js +4 -0
  26. package/tx/library/renderer.js +20 -4
  27. package/tx/library/ucum-parsers.js +2 -1
  28. package/tx/ocl/cs-ocl.cjs +48 -15
  29. package/tx/ocl/vs-ocl.cjs +57 -34
  30. package/tx/operation-context.js +74 -19
  31. package/tx/tx-html.js +5 -5
  32. package/tx/tx.fhir.org.yml +4 -4
  33. package/tx/tx.js +1 -0
  34. package/tx/vs/vs-database.js +150 -100
  35. package/tx/vs/vs-vsac.js +90 -31
  36. package/tx/workers/expand.js +154 -113
  37. package/tx/workers/metadata.js +6 -3
  38. package/tx/workers/read.js +6 -3
  39. package/tx/workers/related.js +228 -87
  40. package/xig/xig-template.html +3 -1
  41. package/library/logger-telnet.js +0 -205
@@ -37,9 +37,9 @@ class ValueSetDatabase {
37
37
  if (!hasCol) {
38
38
  migrations.push(new Promise((res, rej) => {
39
39
  db.run(
40
- "ALTER TABLE valuesets ADD COLUMN date_first_seen INTEGER DEFAULT 0",
41
- [],
42
- (err) => err ? rej(err) : res()
40
+ "ALTER TABLE valuesets ADD COLUMN date_first_seen INTEGER DEFAULT 0",
41
+ [],
42
+ (err) => err ? rej(err) : res()
43
43
  );
44
44
  }));
45
45
  }
@@ -47,13 +47,22 @@ class ValueSetDatabase {
47
47
  migrations.push(new Promise((res, rej) => {
48
48
  db.run(`
49
49
  CREATE TABLE IF NOT EXISTS vsac_runs (
50
- id INTEGER PRIMARY KEY AUTOINCREMENT,
51
- started_at INTEGER NOT NULL,
52
- finished_at INTEGER,
53
- status TEXT NOT NULL DEFAULT 'running',
54
- error_message TEXT,
55
- total_fetched INTEGER,
56
- total_new INTEGER
50
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
51
+ started_at INTEGER NOT NULL,
52
+ finished_at INTEGER,
53
+ status TEXT NOT NULL DEFAULT 'running',
54
+ error_message TEXT,
55
+ total_fetched INTEGER,
56
+ total_new INTEGER
57
+ )
58
+ `, [], (err) => err ? rej(err) : res());
59
+ }));
60
+ // Ensure vsac_settings table exists (for _lastUpdated tracking etc.)
61
+ migrations.push(new Promise((res, rej) => {
62
+ db.run(`
63
+ CREATE TABLE IF NOT EXISTS vsac_settings (
64
+ key TEXT PRIMARY KEY,
65
+ value TEXT
57
66
  )
58
67
  `, [], (err) => err ? rej(err) : res());
59
68
  }));
@@ -170,69 +179,77 @@ class ValueSetDatabase {
170
179
  db.serialize(() => {
171
180
  // Main value sets table
172
181
  db.run(`
173
- CREATE TABLE valuesets (
174
- id TEXT PRIMARY KEY,
175
- url TEXT,
176
- version TEXT,
177
- date TEXT,
178
- description TEXT,
179
- effectivePeriod_start TEXT,
180
- effectivePeriod_end TEXT,
181
- expansion_identifier TEXT,
182
- name TEXT,
183
- publisher TEXT,
184
- status TEXT,
185
- title TEXT,
186
- content TEXT NOT NULL,
187
- last_seen INTEGER DEFAULT (strftime('%s', 'now')),
188
- date_first_seen INTEGER DEFAULT (strftime('%s', 'now'))
189
- )
182
+ CREATE TABLE valuesets (
183
+ id TEXT PRIMARY KEY,
184
+ url TEXT,
185
+ version TEXT,
186
+ date TEXT,
187
+ description TEXT,
188
+ effectivePeriod_start TEXT,
189
+ effectivePeriod_end TEXT,
190
+ expansion_identifier TEXT,
191
+ name TEXT,
192
+ publisher TEXT,
193
+ status TEXT,
194
+ title TEXT,
195
+ content TEXT NOT NULL,
196
+ last_seen INTEGER DEFAULT (strftime('%s', 'now')),
197
+ date_first_seen INTEGER DEFAULT (strftime('%s', 'now'))
198
+ )
190
199
  `);
191
200
 
192
201
  // Identifiers table (0..* Identifier)
193
202
  db.run(`
194
- CREATE TABLE valueset_identifiers (
195
- valueset_id TEXT,
196
- system TEXT,
197
- value TEXT,
198
- use_code TEXT,
199
- type_system TEXT,
200
- type_code TEXT,
201
- FOREIGN KEY (valueset_id) REFERENCES valuesets(url)
202
- )
203
+ CREATE TABLE valueset_identifiers (
204
+ valueset_id TEXT,
205
+ system TEXT,
206
+ value TEXT,
207
+ use_code TEXT,
208
+ type_system TEXT,
209
+ type_code TEXT,
210
+ FOREIGN KEY (valueset_id) REFERENCES valuesets(url)
211
+ )
203
212
  `);
204
213
 
205
214
  // Jurisdictions table (0..* CodeableConcept with 0..* Coding)
206
215
  db.run(`
207
- CREATE TABLE valueset_jurisdictions (
208
- valueset_id TEXT,
209
- system TEXT,
210
- code TEXT,
211
- display TEXT,
212
- FOREIGN KEY (valueset_id) REFERENCES valuesets(url)
213
- )
216
+ CREATE TABLE valueset_jurisdictions (
217
+ valueset_id TEXT,
218
+ system TEXT,
219
+ code TEXT,
220
+ display TEXT,
221
+ FOREIGN KEY (valueset_id) REFERENCES valuesets(url)
222
+ )
214
223
  `);
215
224
 
216
225
  // Systems table (from compose.include[].system)
217
226
  db.run(`
218
- CREATE TABLE valueset_systems (
219
- valueset_id TEXT,
220
- system TEXT,
221
- version TEXT,
222
- FOREIGN KEY (valueset_id) REFERENCES valuesets(url)
223
- )
227
+ CREATE TABLE valueset_systems (
228
+ valueset_id TEXT,
229
+ system TEXT,
230
+ version TEXT,
231
+ FOREIGN KEY (valueset_id) REFERENCES valuesets(url)
232
+ )
224
233
  `);
225
234
 
226
235
  // Run tracking table
227
236
  db.run(`
228
- CREATE TABLE vsac_runs (
229
- id INTEGER PRIMARY KEY AUTOINCREMENT,
230
- started_at INTEGER NOT NULL,
231
- finished_at INTEGER,
232
- status TEXT NOT NULL DEFAULT 'running',
233
- error_message TEXT,
234
- total_fetched INTEGER,
235
- total_new INTEGER
237
+ CREATE TABLE vsac_runs (
238
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
239
+ started_at INTEGER NOT NULL,
240
+ finished_at INTEGER,
241
+ status TEXT NOT NULL DEFAULT 'running',
242
+ error_message TEXT,
243
+ total_fetched INTEGER,
244
+ total_new INTEGER
245
+ )
246
+ `);
247
+
248
+ // Settings table (key-value store for _lastUpdated tracking etc.)
249
+ db.run(`
250
+ CREATE TABLE IF NOT EXISTS vsac_settings (
251
+ key TEXT PRIMARY KEY,
252
+ value TEXT
236
253
  )
237
254
  `);
238
255
 
@@ -271,9 +288,9 @@ class ValueSetDatabase {
271
288
  const db = await this._getWriteConnection();
272
289
  return new Promise((resolve, reject) => {
273
290
  db.run(
274
- `INSERT INTO vsac_runs (started_at, status) VALUES (strftime('%s','now'), 'running')`,
275
- [],
276
- function(err) { err ? reject(err) : resolve(this.lastID); }
291
+ `INSERT INTO vsac_runs (started_at, status) VALUES (strftime('%s','now'), 'running')`,
292
+ [],
293
+ function(err) { err ? reject(err) : resolve(this.lastID); }
277
294
  );
278
295
  });
279
296
  }
@@ -289,10 +306,10 @@ class ValueSetDatabase {
289
306
  const db = await this._getWriteConnection();
290
307
  return new Promise((resolve, reject) => {
291
308
  db.run(
292
- `UPDATE vsac_runs SET finished_at = strftime('%s','now'), status = 'ok',
293
- total_fetched = ?, total_new = ? WHERE id = ?`,
294
- [totalFetched, totalNew, id],
295
- err => err ? reject(err) : resolve()
309
+ `UPDATE vsac_runs SET finished_at = strftime('%s','now'), status = 'ok',
310
+ total_fetched = ?, total_new = ? WHERE id = ?`,
311
+ [totalFetched, totalNew, id],
312
+ err => err ? reject(err) : resolve()
296
313
  );
297
314
  });
298
315
  }
@@ -307,10 +324,43 @@ class ValueSetDatabase {
307
324
  const db = await this._getWriteConnection();
308
325
  return new Promise((resolve, reject) => {
309
326
  db.run(
310
- `UPDATE vsac_runs SET finished_at = strftime('%s','now'), status = 'error',
311
- error_message = ? WHERE id = ?`,
312
- [errorMessage, id],
313
- err => err ? reject(err) : resolve()
327
+ `UPDATE vsac_runs SET finished_at = strftime('%s','now'), status = 'error',
328
+ error_message = ? WHERE id = ?`,
329
+ [errorMessage, id],
330
+ err => err ? reject(err) : resolve()
331
+ );
332
+ });
333
+ }
334
+
335
+ /**
336
+ * Get a setting value from the vsac_settings table
337
+ * @param {string} key - The setting key
338
+ * @returns {Promise<string|null>} The setting value, or null if not found
339
+ */
340
+ async getSetting(key) {
341
+ const db = await this._getReadConnection();
342
+ return new Promise((resolve, reject) => {
343
+ db.get('SELECT value FROM vsac_settings WHERE key = ?', [key], (err, row) => {
344
+ if (err) reject(err);
345
+ else resolve(row ? row.value : null);
346
+ });
347
+ });
348
+ }
349
+
350
+ /**
351
+ * Set a setting value in the vsac_settings table
352
+ * @param {string} key - The setting key
353
+ * @param {string} value - The setting value
354
+ * @returns {Promise<void>}
355
+ */
356
+ async setSetting(key, value) {
357
+ const db = await this._getWriteConnection();
358
+ return new Promise((resolve, reject) => {
359
+ db.run(
360
+ `INSERT INTO vsac_settings (key, value) VALUES (?, ?)
361
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
362
+ [key, value],
363
+ err => err ? reject(err) : resolve()
314
364
  );
315
365
  });
316
366
  }
@@ -353,24 +403,24 @@ class ValueSetDatabase {
353
403
  const expansionId = valueSet.expansion?.identifier || null;
354
404
 
355
405
  db.run(`
356
- INSERT INTO valuesets (
357
- id, url, version, date, description, effectivePeriod_start, effectivePeriod_end,
358
- expansion_identifier, name, publisher, status, title, content, last_seen, date_first_seen
359
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))
360
- ON CONFLICT(id) DO UPDATE SET
361
- url=excluded.url,
362
- version=excluded.version,
363
- date=excluded.date,
364
- description=excluded.description,
365
- effectivePeriod_start=excluded.effectivePeriod_start,
366
- effectivePeriod_end=excluded.effectivePeriod_end,
367
- expansion_identifier=excluded.expansion_identifier,
368
- name=excluded.name,
369
- publisher=excluded.publisher,
370
- status=excluded.status,
371
- title=excluded.title,
372
- content=excluded.content,
373
- last_seen=strftime('%s', 'now')
406
+ INSERT INTO valuesets (
407
+ id, url, version, date, description, effectivePeriod_start, effectivePeriod_end,
408
+ expansion_identifier, name, publisher, status, title, content, last_seen, date_first_seen
409
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))
410
+ ON CONFLICT(id) DO UPDATE SET
411
+ url=excluded.url,
412
+ version=excluded.version,
413
+ date=excluded.date,
414
+ description=excluded.description,
415
+ effectivePeriod_start=excluded.effectivePeriod_start,
416
+ effectivePeriod_end=excluded.effectivePeriod_end,
417
+ expansion_identifier=excluded.expansion_identifier,
418
+ name=excluded.name,
419
+ publisher=excluded.publisher,
420
+ status=excluded.status,
421
+ title=excluded.title,
422
+ content=excluded.content,
423
+ last_seen=strftime('%s', 'now')
374
424
  `, [
375
425
  valueSet.id,
376
426
  valueSet.url,
@@ -414,10 +464,10 @@ class ValueSetDatabase {
414
464
 
415
465
  return new Promise((resolve, reject) => {
416
466
  db.run(`
417
- update valuesets
418
- set last_seen = strftime('%s', 'now')
419
- where url = ?
420
- and version = ?
467
+ update valuesets
468
+ set last_seen = strftime('%s', 'now')
469
+ where url = ?
470
+ and version = ?
421
471
  `, [
422
472
  valueSet.url,
423
473
  valueSet.version
@@ -466,9 +516,9 @@ class ValueSetDatabase {
466
516
  const typeCode = id.type?.coding?.[0]?.code || null;
467
517
 
468
518
  db.run(`
469
- INSERT INTO valueset_identifiers (
470
- valueset_id, system, value, use_code, type_system, type_code
471
- ) VALUES (?, ?, ?, ?, ?, ?)
519
+ INSERT INTO valueset_identifiers (
520
+ valueset_id, system, value, use_code, type_system, type_code
521
+ ) VALUES (?, ?, ?, ?, ?, ?)
472
522
  `, [
473
523
  valueSet.id,
474
524
  id.system || null,
@@ -490,9 +540,9 @@ class ValueSetDatabase {
490
540
  for (const coding of jurisdiction.coding) {
491
541
  pendingOperations++;
492
542
  db.run(`
493
- INSERT INTO valueset_jurisdictions (
494
- valueset_id, system, code, display
495
- ) VALUES (?, ?, ?, ?)
543
+ INSERT INTO valueset_jurisdictions (
544
+ valueset_id, system, code, display
545
+ ) VALUES (?, ?, ?, ?)
496
546
  `, [
497
547
  valueSet.id,
498
548
  coding.system || null,
@@ -514,7 +564,7 @@ class ValueSetDatabase {
514
564
  pendingOperations++;
515
565
 
516
566
  db.run(`
517
- INSERT INTO valueset_systems (valueset_id, system, version) VALUES (?, ?, ?)
567
+ INSERT INTO valueset_systems (valueset_id, system, version) VALUES (?, ?, ?)
518
568
  `, [valueSet.id, include.system, include.version], function(err) {
519
569
  if (err) {
520
570
  operationError(new Error(`Failed to insert system: ${err.message}`));
@@ -598,12 +648,12 @@ class ValueSetDatabase {
598
648
  async search(spaceId, map, searchParams, elements = null) {
599
649
  // Check if we can optimize by selecting only indexed columns
600
650
  const canOptimize = elements && elements.length > 0 &&
601
- elements.every(e => INDEXED_COLUMNS.includes(e));
651
+ elements.every(e => INDEXED_COLUMNS.includes(e));
602
652
 
603
653
  // Always include 'id' in the columns to select when optimizing
604
654
  const columnsToSelect = canOptimize
605
- ? (elements.includes('id') ? elements : ['id', ...elements])
606
- : null;
655
+ ? (elements.includes('id') ? elements : ['id', ...elements])
656
+ : null;
607
657
 
608
658
  const db = await this._getReadConnection();
609
659
 
package/tx/vs/vs-vsac.js CHANGED
@@ -78,7 +78,6 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
78
78
  if (this.valueSetMap.size == 0) {
79
79
  await this.refreshValueSets();
80
80
  }
81
-
82
81
  // Start periodic refresh
83
82
  this._startRefreshTimer();
84
83
  this.initialized = true;
@@ -174,6 +173,14 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
174
173
  console.log(`VSAC refresh phase 1 done. Total: ${count} with ${ncount} new items`);
175
174
  this.stats.task('VSAC Sync', `VSAC refresh phase 1 done. Total: ${count} with ${ncount} new items`);
176
175
 
176
+ // phase 1b: query for recently updated value sets via _lastUpdated
177
+ let lastUpdatedCount = await this._scanLastUpdated();
178
+ console.log(`VSAC refresh phase 1b done. ${lastUpdatedCount} additional items from _lastUpdated`);
179
+ this.stats.task('VSAC Sync', `Phase 1b: ${lastUpdatedCount} from _lastUpdated`);
180
+
181
+ // deduplicate the queue
182
+ this.queue = [...new Set(this.queue)];
183
+
177
184
  let tracking = { totalFetched: 0, totalNew: 0, count: 0, newCount : 0 };
178
185
  // phase 2: query for history & content
179
186
  this.requeue = [];
@@ -418,8 +425,8 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
418
425
  isRefreshing: this.isRefreshing,
419
426
  refreshIntervalHours: this.refreshIntervalHours,
420
427
  nextRefresh: this.refreshTimer && this.lastRefresh
421
- ? new Date(this.lastRefresh.getTime() + (this.refreshIntervalHours * 60 * 60 * 1000))
422
- : null
428
+ ? new Date(this.lastRefresh.getTime() + (this.refreshIntervalHours * 60 * 60 * 1000))
429
+ : null
423
430
  }
424
431
  };
425
432
  }
@@ -506,8 +513,8 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
506
513
  if (bundle.entry && bundle.entry.length > 0) {
507
514
  // Extract ValueSets from bundle entries
508
515
  const valueSets = bundle.entry
509
- .filter(entry => entry.resource && entry.resource.resourceType === 'ValueSet')
510
- .map(entry => entry.resource);
516
+ .filter(entry => entry.resource && entry.resource.resourceType === 'ValueSet')
517
+ .map(entry => entry.resource);
511
518
  if (valueSets.length > 0) {
512
519
  tracking.totalNew = tracking.totalNew + await this.batchUpsertValueSets(valueSets);
513
520
  tracking.totalFetched += valueSets.length;
@@ -519,6 +526,58 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
519
526
  this.stats.task('VSAC Sync', logMsg);
520
527
  }
521
528
 
529
+ /**
530
+ * Scan VSAC for recently updated value sets using the _lastUpdated parameter.
531
+ * Uses a stored date from the previous run; if none exists, defaults to 4 days ago.
532
+ * Adds any found URLs to this.queue and stores the server's response date for next time.
533
+ * @returns {Promise<number>} Number of value set URLs added to the queue
534
+ * @private
535
+ */
536
+ async _scanLastUpdated() {
537
+ const SETTING_KEY = 'vsac_last_updated_date';
538
+
539
+ let sinceDate = await this.database.getSetting(SETTING_KEY);
540
+ if (!sinceDate) {
541
+ // No stored date — default to 10 days ago
542
+ const d = new Date();
543
+ d.setDate(d.getDate() - 10);
544
+ sinceDate = d.toISOString();
545
+ }
546
+
547
+ let url = `/res/ValueSet/?_lastUpdated=ge${sinceDate}&_offset=0&_count=100&_elements=id,url,version,status`;
548
+ let count = 0;
549
+ let serverDate = null;
550
+
551
+ while (url) {
552
+ console.log(`_lastUpdated scan: ${count} found so far`);
553
+ this.stats.task('VSAC Sync', `_lastUpdated scan: ${count} found`);
554
+
555
+ const bundle = await this._fetchBundle(url);
556
+
557
+ // Capture the server's lastUpdated from the first page
558
+ if (!serverDate && bundle.meta && bundle.meta.lastUpdated) {
559
+ serverDate = bundle.meta.lastUpdated;
560
+ }
561
+
562
+ for (let be of bundle.entry || []) {
563
+ let vs = be.resource;
564
+ if (vs && vs.url) {
565
+ this.queue.push(vs.url);
566
+ count++;
567
+ }
568
+ }
569
+
570
+ url = this._getNextUrl(bundle);
571
+ }
572
+
573
+ // Store the server date for next run
574
+ if (serverDate) {
575
+ await this.database.setSetting(SETTING_KEY, serverDate);
576
+ }
577
+
578
+ return count;
579
+ }
580
+
522
581
  name() {
523
582
  return "VSAC";
524
583
  }
@@ -533,38 +592,38 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
533
592
 
534
593
  const rows = await new Promise((resolve, reject) => {
535
594
  db.all(
536
- `SELECT 'vs' AS kind,
537
- url,
538
- version,
539
- date_first_seen AS ts,
540
- NULL AS status,
541
- NULL AS error_message,
542
- NULL AS finished_at,
543
- NULL AS total_fetched,
544
- NULL AS total_new
595
+ `SELECT 'vs' AS kind,
596
+ url,
597
+ version,
598
+ date_first_seen AS ts,
599
+ NULL AS status,
600
+ NULL AS error_message,
601
+ NULL AS finished_at,
602
+ NULL AS total_fetched,
603
+ NULL AS total_new
545
604
  FROM valuesets
546
- WHERE date_first_seen > 0
547
- UNION ALL
548
- SELECT 'run' AS kind,
549
- NULL,
550
- NULL,
551
- started_at AS ts,
552
- status,
553
- error_message,
554
- finished_at,
555
- total_fetched,
556
- total_new
605
+ WHERE date_first_seen > 0
606
+ UNION ALL
607
+ SELECT 'run' AS kind,
608
+ NULL,
609
+ NULL,
610
+ started_at AS ts,
611
+ status,
612
+ error_message,
613
+ finished_at,
614
+ total_fetched,
615
+ total_new
557
616
  FROM vsac_runs
558
- ORDER BY ts DESC
559
- LIMIT 200`,
560
- [],
561
- (err, rows) => err ? reject(err) : resolve(rows)
617
+ ORDER BY ts DESC
618
+ LIMIT 200`,
619
+ [],
620
+ (err, rows) => err ? reject(err) : resolve(rows)
562
621
  );
563
622
  });
564
623
 
565
624
  const fmt = ts => ts
566
- ? new Date(ts * 1000).toISOString().replace('T', ' ').substring(0, 19) + ' UTC'
567
- : '—';
625
+ ? new Date(ts * 1000).toISOString().replace('T', ' ').substring(0, 19) + ' UTC'
626
+ : '—';
568
627
 
569
628
  let html = '<h3>VSAC Sync History</h3>';
570
629
  html += '<table class="grid">';