fhirsmith 0.5.6 → 0.7.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 (78) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +2 -0
  3. package/configurations/projector.json +21 -0
  4. package/configurations/readme.md +5 -0
  5. package/library/html-server.js +2 -1
  6. package/library/package-manager.js +37 -34
  7. package/library/utilities.js +10 -1
  8. package/library/version-utilities.js +85 -0
  9. package/package.json +1 -1
  10. package/packages/package-crawler.js +144 -52
  11. package/packages/packages.js +15 -7
  12. package/publisher/publisher.js +15 -3
  13. package/registry/api.js +173 -191
  14. package/registry/crawler.js +100 -65
  15. package/registry/model.js +14 -8
  16. package/registry/registry.js +5 -0
  17. package/root-template.html +1 -0
  18. package/server.js +113 -45
  19. package/tx/README.md +4 -4
  20. package/tx/cs/cs-api.js +18 -1
  21. package/tx/cs/cs-base.js +1 -0
  22. package/tx/cs/cs-loinc.js +5 -2
  23. package/tx/cs/cs-provider-api.js +25 -1
  24. package/tx/cs/cs-provider-list.js +2 -2
  25. package/tx/cs/cs-rxnorm.js +9 -2
  26. package/tx/cs/cs-snomed.js +17 -2
  27. package/tx/html/codesystem-operations.liquid +17 -24
  28. package/tx/html/valueset-operations.liquid +46 -52
  29. package/tx/library/canonical-resource.js +6 -1
  30. package/tx/library/codesystem.js +6 -1
  31. package/tx/library/renderer.js +81 -7
  32. package/tx/library.js +145 -13
  33. package/tx/ocl/README.md +236 -0
  34. package/tx/ocl/cache/cache-paths.cjs +32 -0
  35. package/tx/ocl/cache/cache-paths.js +2 -0
  36. package/tx/ocl/cache/cache-utils.cjs +43 -0
  37. package/tx/ocl/cache/cache-utils.js +2 -0
  38. package/tx/ocl/cm-ocl.cjs +531 -0
  39. package/tx/ocl/cm-ocl.js +1 -105
  40. package/tx/ocl/cs-ocl.cjs +1779 -0
  41. package/tx/ocl/cs-ocl.js +1 -38
  42. package/tx/ocl/fingerprint/fingerprint.cjs +67 -0
  43. package/tx/ocl/fingerprint/fingerprint.js +2 -0
  44. package/tx/ocl/http/client.cjs +31 -0
  45. package/tx/ocl/http/client.js +2 -0
  46. package/tx/ocl/http/pagination.cjs +98 -0
  47. package/tx/ocl/http/pagination.js +2 -0
  48. package/tx/ocl/jobs/background-queue.cjs +200 -0
  49. package/tx/ocl/jobs/background-queue.js +2 -0
  50. package/tx/ocl/mappers/concept-mapper.cjs +66 -0
  51. package/tx/ocl/mappers/concept-mapper.js +2 -0
  52. package/tx/ocl/model/concept-filter-context.cjs +51 -0
  53. package/tx/ocl/model/concept-filter-context.js +2 -0
  54. package/tx/ocl/shared/constants.cjs +15 -0
  55. package/tx/ocl/shared/constants.js +2 -0
  56. package/tx/ocl/shared/patches.cjs +224 -0
  57. package/tx/ocl/shared/patches.js +2 -0
  58. package/tx/ocl/vs-ocl.cjs +1848 -0
  59. package/tx/ocl/vs-ocl.js +1 -104
  60. package/tx/operation-context.js +8 -1
  61. package/tx/params.js +24 -3
  62. package/tx/provider.js +51 -2
  63. package/tx/sct/expressions.js +20 -9
  64. package/tx/tx-html.js +144 -51
  65. package/tx/tx.js +10 -2
  66. package/tx/vs/vs-vsac.js +4 -3
  67. package/tx/workers/batch-validate.js +3 -2
  68. package/tx/workers/batch.js +3 -2
  69. package/tx/workers/expand.js +125 -18
  70. package/tx/workers/lookup.js +5 -4
  71. package/tx/workers/read.js +2 -1
  72. package/tx/workers/related.js +3 -2
  73. package/tx/workers/search.js +6 -8
  74. package/tx/workers/subsumes.js +3 -2
  75. package/tx/workers/translate.js +4 -3
  76. package/tx/workers/validate.js +132 -40
  77. package/tx/workers/worker.js +1 -7
  78. package/tx/xversion/xv-terminologyCapabilities.js +1 -1
package/tx/ocl/vs-ocl.js CHANGED
@@ -1,105 +1,2 @@
1
- /**
2
- * Abstract base class for value set providers
3
- * Defines the interface that all value set providers must implement
4
- */
5
- // eslint-disable-next-line no-unused-vars
6
- class OCLValueSetProvider {
7
- /**
8
- * {int} Unique number assigned to this provider
9
- */
10
- spaceId;
1
+ module.exports = require('./vs-ocl.cjs');
11
2
 
12
- /**
13
- * ensure that the ids on the value sets are unique, if they are
14
- * in the global namespace
15
- *
16
- * @param {Set<String>} ids
17
- */
18
- // eslint-disable-next-line no-unused-vars
19
- assignIds(ids) {
20
- throw new Error('assignIds must be implemented by AbstractValueSetProvider subclass');
21
- }
22
-
23
- /**
24
- * Fetches a specific value set by URL and version
25
- * @param {string} url - The URL/identifier of the value set
26
- * @param {string} version - The version of the value set
27
- * @returns {Promise<ValueSet>} The requested value set
28
- * @throws {Error} Must be implemented by subclasses
29
- */
30
- // eslint-disable-next-line no-unused-vars
31
- async fetchValueSet(url, version) {
32
- throw new Error('fetchValueSet must be implemented by subclass');
33
- }
34
-
35
- /**
36
- * Fetches a specific value set by id. ValueSet providers must enforce that value set ids are unique
37
- * either globally (as enforced by assignIds) or in their space
38
- *
39
- * @param {string} id - The id of the value set
40
- * @returns {Promise<ValueSet>} The requested value set
41
- * @throws {Error} Must be implemented by subclasses
42
- */
43
- // eslint-disable-next-line no-unused-vars
44
- async fetchValueSetById(id) {
45
- throw new Error('fetchValueSetById must be implemented by subclass');
46
- }
47
-
48
- /**
49
- * Searches for value sets based on provided criteria
50
- * @param {Array<{name: string, value: string}>} searchParams - List of name/value pairs for search criteria
51
- * @returns {Promise<Array<ValueSet>>} List of matching value sets
52
- * @throws {Error} Must be implemented by subclasses
53
- */
54
- // eslint-disable-next-line no-unused-vars
55
- async searchValueSets(searchParams, elements = null) {
56
- throw new Error('searchValueSets must be implemented by subclass');
57
- }
58
-
59
- /**
60
- *
61
- * @returns {number} total number of value sets
62
- */
63
- vsCount() {
64
- return 0;
65
- }
66
-
67
- /**
68
- * Validates search parameters
69
- * @param {Array<{name: string, value: string}>} searchParams - Search parameters to validate
70
- * @protected
71
- */
72
- _validateSearchParams(searchParams) {
73
- if (!Array.isArray(searchParams)) {
74
- throw new Error('Search parameters must be an array');
75
- }
76
-
77
- for (const param of searchParams) {
78
- if (!param || typeof param !== 'object') {
79
- throw new Error('Each search parameter must be an object');
80
- }
81
- if (typeof param.name !== 'string' || typeof param.value !== 'string') {
82
- throw new Error('Search parameter must have string name and value properties');
83
- }
84
- }
85
- }
86
-
87
- /**
88
- * Validates URL and version parameters
89
- * @param {string} url - URL to validate
90
- * @param {string} version - Version to validate
91
- * @protected
92
- */
93
- _validateFetchParams(url, version) {
94
- if (typeof url !== 'string' || !url.trim()) {
95
- throw new Error('URL must be a non-empty string');
96
- }
97
- if (version != null && typeof version !== 'string') {
98
- throw new Error('Version must be a string');
99
- }
100
- }
101
- }
102
-
103
- module.exports = {
104
- AbstractValueSetProvider
105
- };
@@ -19,6 +19,12 @@ function isDebugging() {
19
19
  );
20
20
  }
21
21
 
22
+ function debugLog(error, message) {
23
+ if (isDebugging()) {
24
+ console.log(error, message);
25
+ }
26
+ }
27
+
22
28
 
23
29
  class TimeTracker {
24
30
  constructor() {
@@ -584,5 +590,6 @@ module.exports = {
584
590
  TimeTracker,
585
591
  ResourceCache,
586
592
  ExpansionCache,
587
- isDebugging
593
+ isDebugging,
594
+ debugLog
588
595
  };
package/tx/params.js CHANGED
@@ -66,6 +66,7 @@ class TxParameters {
66
66
  this.FDisplayWarning = false;
67
67
  this.FMembershipOnly = false;
68
68
  this.FDiagnostics = false;
69
+ this.FVersionsMatch = false;
69
70
 
70
71
  this.hasActiveOnly = false;
71
72
  this.hasExcludeNested = false;
@@ -77,6 +78,7 @@ class TxParameters {
77
78
  this.hasDefaultToLatestVersion = false;
78
79
  this.hasDisplayWarning = false;
79
80
  this.hasMembershipOnly = false;
81
+ this.hasVersionsMatch = false;
80
82
  }
81
83
 
82
84
  readParams(params) {
@@ -199,6 +201,10 @@ class TxParameters {
199
201
  if (getValuePrimitive(p) == true) this.membershipOnly = true;
200
202
  break;
201
203
  }
204
+ case 'versionsMatch' : {
205
+ if (getValuePrimitive(p) == true) this.FVersionsMatch = true;
206
+ break;
207
+ }
202
208
  case 'profile' : {
203
209
  let value = p.resource;
204
210
  if (value && (value.resourceType === 'Parameters' || value.resourceType === 'ExpansionProfile')) {
@@ -387,6 +393,15 @@ class TxParameters {
387
393
  this.hasMembershipOnly = true;
388
394
  }
389
395
 
396
+ get versionsMatch() {
397
+ return this.FVersionsMatch;
398
+ }
399
+
400
+ set versionsMatch(value) {
401
+ this.FVersionsMatch = value;
402
+ this.hasVersionsMatch = true;
403
+ }
404
+ e
390
405
  get versionRules() {
391
406
  return this.FVersionRules;
392
407
  }
@@ -412,6 +427,10 @@ class TxParameters {
412
427
  if (name === 'designation') {
413
428
  this.designations.push(getValuePrimitive(value));
414
429
  }
430
+
431
+ if (name === 'versionsMatch') {
432
+ this.versionsMatch = getValuePrimitive(value) === 'true';
433
+ }
415
434
  }
416
435
  }
417
436
 
@@ -502,6 +521,7 @@ class TxParameters {
502
521
  b('include-designations', this.FIncludeDesignations);
503
522
  b('include-definition', this.FIncludeDefinition);
504
523
  b('membership-only', this.FMembershipOnly);
524
+ b('versions-match', this.FVersionsMatch);
505
525
  b('default-to-latest', this.FDefaultToLatestVersion);
506
526
  b('display-warning', this.FDisplayWarning);
507
527
 
@@ -526,11 +546,11 @@ class TxParameters {
526
546
  };
527
547
 
528
548
  let s = '|'+this.count+'|'+this.limit+'|'+this.offset+
529
- this.FUid + '|' + b(this.FMembershipOnly) + '|' + this.FProperties.join(',') + '|' +
549
+ this.FUid + '|' + b(this.FMembershipOnly) + '|' + b(this.FVersionsMatch)+'|' + this.FProperties.join(',') + '|' +
530
550
  b(this.FActiveOnly) + b(this.FDisplayWarning) + b(this.FExcludeNested) + b(this.FGenerateNarrative) + b(this.FExcludeNotForUI) + b(this.FExcludePostCoordinated) +
531
551
  b(this.FIncludeDesignations) + b(this.FIncludeDefinition) + b(this.hasActiveOnly) + b(this.hasExcludeNested) + b(this.hasGenerateNarrative) +
532
552
  b(this.hasExcludeNotForUI) + b(this.hasExcludePostCoordinated) + b(this.hasIncludeDesignations) + this.sort+'|'+
533
- b(this.hasIncludeDefinition) + b(this.hasDefaultToLatestVersion) + b(this.hasDisplayWarning) + b(this.hasExcludeNotForUI) + b(this.hasMembershipOnly) + b(this.FDefaultToLatestVersion);
553
+ b(this.hasIncludeDefinition) + b(this.hasDefaultToLatestVersion) + b(this.hasDisplayWarning) + b(this.hasExcludeNotForUI) + b(this.hasMembershipOnly) + b(this.hasVersionsMatch) + b(this.FDefaultToLatestVersion);
534
554
 
535
555
  if (this.hasHTTPLanguages) {
536
556
  s = s + this.FHTTPLanguages.asString(true) + '|';
@@ -577,6 +597,7 @@ class TxParameters {
577
597
  this.FIncludeDefinition = other.FIncludeDefinition;
578
598
  this.FUid = other.FUid;
579
599
  this.FMembershipOnly = other.FMembershipOnly;
600
+ this.FVersionsMatch = other.FVersionsMatch;
580
601
  this.FDefaultToLatestVersion = other.FDefaultToLatestVersion;
581
602
  this.FDisplayWarning = other.FDisplayWarning;
582
603
  this.FDiagnostics = other.FDiagnostics;
@@ -588,7 +609,7 @@ class TxParameters {
588
609
  this.hasIncludeDesignations = other.hasIncludeDesignations;
589
610
  this.hasIncludeDefinition = other.hasIncludeDefinition;
590
611
  this.hasDefaultToLatestVersion = other.hasDefaultToLatestVersion;
591
- this.hasMembershipOnly = other.hasMembershipOnly;
612
+ this.hasVersionsMatch = other.hasVersionsMatch;
592
613
  this.hasDisplayWarning = other.hasDisplayWarning;
593
614
  this.sort = other.sort;
594
615
 
package/tx/provider.js CHANGED
@@ -23,6 +23,7 @@ const {PackageConceptMapProvider} = require("./cm/cm-package");
23
23
  class Provider {
24
24
  i18n;
25
25
  fhirVersion;
26
+ context;
26
27
 
27
28
  /**
28
29
  * {Map<String, CodeSystemFactoryProvider>} A list of code system factories that contains all the preloaded native code systems
@@ -34,6 +35,11 @@ class Provider {
34
35
  */
35
36
  codeSystems;
36
37
 
38
+ /**
39
+ * {List<AbstractCodeSystemProvider>} code system providers, for maintaing the code system list
40
+ */
41
+ codeSystemProviders
42
+
37
43
  /**
38
44
  * {List<AbstractValueSetProvider>} A list of value set providers that know how to provide value sets by request
39
45
  */
@@ -153,6 +159,7 @@ class Provider {
153
159
  }
154
160
 
155
161
  getCodeSystemById(opContext, id) {
162
+
156
163
  // Search through codeSystems map for matching id
157
164
  for (const cs of this.codeSystems.values()) {
158
165
  if (opContext) opContext.deadCheck('getCodeSystemById');
@@ -318,9 +325,10 @@ class Provider {
318
325
  factory = this.codeSystemFactories.get(vurlMM);
319
326
  }
320
327
  if (factory != null) {
328
+ let vdesc = version == null ? "" : factory.describeVersion(version);
321
329
  return {
322
- link: this.path+"/CodeSystem/"+factory.id(),
323
- description: factory.name()+(version ? " v"+version : "")
330
+ link: this.path+"/CodeSystem/x-"+factory.id(),
331
+ description: factory.nameBase()+' '+vdesc
324
332
  };
325
333
  }
326
334
  let cs = this.codeSystems.get(vurl);
@@ -423,6 +431,47 @@ class Provider {
423
431
  }
424
432
  }
425
433
  return false;
434
+ }x
435
+
436
+ async updateCodeSystemList() {
437
+ for (let csp of this.codeSystemProviders) {
438
+ let changes = await csp.getCodeSystemChanges(this.fhirVersion, this.context);
439
+ if (changes) {
440
+ for (let cs of changes.added || []) {
441
+ this.addCodeSystem(cs);
442
+ }
443
+ for (let cs of changes.changed || []) {
444
+ this.addCodeSystem(cs);
445
+ }
446
+ for (let cs of changes.deleted || []) {
447
+ this.deleteCodeSystem(cs);
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ addCodeSystem(cs) {
454
+ const existing = this.codeSystems.get(cs.url);
455
+ if (!existing || cs.isMoreRecent(existing)) {
456
+ this.codeSystems.set(cs.url, cs);
457
+ }
458
+ if (cs.version) {
459
+ this.codeSystems.set(cs.vurl, cs);
460
+ }
461
+ }
462
+
463
+ deleteCodeSystem(cs) {
464
+ this.codeSystems.delete(cs.vurl);
465
+ this.codeSystems.delete(cs.url);
466
+ let existing = null;
467
+ for (let t of this.codeSystems.values()) {
468
+ if (!existing || t.isMoreRecent(existing)) {
469
+ existing = t;
470
+ }
471
+ }
472
+ if (existing) {
473
+ this.codeSystems.set(cs.url, cs);
474
+ }
426
475
  }
427
476
 
428
477
  }
@@ -1469,7 +1469,7 @@ class SnomedExpressionServices {
1469
1469
  /**
1470
1470
  * Validate concept reference
1471
1471
  */
1472
- checkConcept(concept, limit) {
1472
+ checkConcept(concept, limits) {
1473
1473
  if (concept.code) {
1474
1474
  const conceptId = BigInt(concept.code);
1475
1475
  const result = this.concepts.findConcept(conceptId);
@@ -1480,13 +1480,24 @@ class SnomedExpressionServices {
1480
1480
  throw new Error(`Concept ${concept.code} not found`);
1481
1481
  }
1482
1482
  }
1483
- if (limit && concept.reference) {
1484
- // if a limit is specified, then the concept has to be a specialization of that.
1485
- let parentRef = this.concepts.findConcept(limit);
1486
- let descendentsRef = this.concepts.getAllDesc(parentRef.index);
1487
- const descendants = this.refs.getReferences(descendentsRef);
1488
- if (descendants && !descendants.includes(concept.reference)) {
1489
- throw new Error(`Concept ${concept.code} is not valid in this context (must be a ${limit})`);
1483
+ if (limits && concept.reference) {
1484
+ // if a limit is specified, then the concept has to be a specialization of one of them.
1485
+ let ok = false;
1486
+ for (const limit of limits) {
1487
+ let parentRef = this.concepts.findConcept(limit);
1488
+ let descendentsRef = this.concepts.getAllDesc(parentRef.index);
1489
+ const descendants = this.refs.getReferences(descendentsRef);
1490
+ if (descendants && descendants.includes(concept.reference)) {
1491
+ ok = true;
1492
+ break;
1493
+ }
1494
+ }
1495
+ if (!ok) {
1496
+ if (limits.length == 1) {
1497
+ throw new Error(`Concept ${concept.code} is not valid in this context (must be a ${limits[0]})`);
1498
+ } else {
1499
+ throw new Error(`Concept ${concept.code} is not valid in this context (must be a descendent of one of ${limits})`);
1500
+ }
1490
1501
  }
1491
1502
  }
1492
1503
 
@@ -1615,7 +1626,7 @@ class SnomedExpressionServices {
1615
1626
  * Validate refinement
1616
1627
  */
1617
1628
  checkRefinement(refinement) {
1618
- this.checkConcept(refinement.name, '410662002');
1629
+ this.checkConcept(refinement.name, ['410662002', '106237007']);
1619
1630
  this.checkExpression(refinement.value);
1620
1631
  }
1621
1632
 
package/tx/tx-html.js CHANGED
@@ -9,6 +9,15 @@ const htmlServer = require('../library/html-server');
9
9
  const Logger = require('../library/logger');
10
10
  const packageJson = require("../package.json");
11
11
  const escape = require('escape-html');
12
+ const {ExpandWorker} = require("./workers/expand");
13
+ const ValueSet = require("./library/valueset");
14
+ const {CodeSystemXML} = require("./xml/codesystem-xml");
15
+ const {ValueSetXML} = require("./xml/valueset-xml");
16
+ const {BundleXML} = require("./xml/bundle-xml");
17
+ const {CapabilityStatementXML} = require("./xml/capabilitystatement-xml");
18
+ const {TerminologyCapabilitiesXML} = require("./xml/terminologycapabilities-xml");
19
+ const {ParametersXML} = require("./xml/parameters-xml");
20
+ const {OperationOutcomeXML} = require("./xml/operationoutcome-xml");
12
21
 
13
22
  const txHtmlLog = Logger.getInstance().child({ module: 'tx-html' });
14
23
 
@@ -57,10 +66,16 @@ function loadTemplate() {
57
66
  class TxHtmlRenderer {
58
67
  renderer;
59
68
  liquid;
69
+ languages;
70
+ i18n;
71
+ path;
60
72
 
61
- constructor(renderer, liquid) {
73
+ constructor(renderer, liquid, languages, i18n, path) {
62
74
  this.renderer = renderer;
63
75
  this.liquid = liquid;
76
+ this.languages = languages;
77
+ this.i18n = i18n;
78
+ this.path = path;
64
79
  }
65
80
 
66
81
  /**
@@ -85,7 +100,7 @@ class TxHtmlRenderer {
85
100
  if (_fmt && typeof _fmt !== 'string') {
86
101
  _fmt = null;
87
102
  }
88
- if (_fmt && _fmt == 'html') {
103
+ if (_fmt && (_fmt == 'html' || _fmt.startsWith('html/'))) {
89
104
  return true;
90
105
  }
91
106
  if (!_fmt) {
@@ -106,6 +121,14 @@ class TxHtmlRenderer {
106
121
  } else {
107
122
  const resourceType = json.resourceType || 'Response';
108
123
 
124
+ let pfx = resourceType;
125
+ if (req.path.includes('$')) {
126
+ let s = req.path.substring(req.path.indexOf('$') + 1).replace(/[^a-zA-Z].*$/, '');
127
+ switch (s) {
128
+ case 'expand': pfx = "Expansion for "+resourceType;
129
+ }
130
+ }
131
+
109
132
  if (resourceType === 'Bundle' && json.type === 'searchset') {
110
133
  // Extract the resource type being searched from self link or entries
111
134
  const selfLink = json.link?.find(l => l.relation === 'self')?.url || '';
@@ -124,11 +147,11 @@ class TxHtmlRenderer {
124
147
  }
125
148
 
126
149
  if (json.id) {
127
- return `${resourceType}/${json.id}`;
150
+ return `${pfx} ${json.id}`;
128
151
  }
129
152
 
130
153
  if (json.name) {
131
- return `${resourceType}: ${json.name}`;
154
+ return `${pfx} ${json.name}`;
132
155
  }
133
156
 
134
157
  return resourceType;
@@ -267,15 +290,27 @@ class TxHtmlRenderer {
267
290
  return await this.buildHomePage(req);
268
291
  } else {
269
292
  try {
293
+ const _fmt = req?.query?._format || req?.query?.format || req?.body?._format;
294
+ const op = req ? req.path.includes("$") : false;
270
295
  const resourceType = json.resourceType;
271
296
 
272
297
  switch (resourceType) {
273
298
  case 'Parameters':
274
299
  return await this.renderParameters(json);
275
300
  case 'CodeSystem':
276
- return await this.renderCodeSystem(json, inBundle);
277
- case 'ValueSet':
278
- return await this.renderValueSet(json, inBundle);
301
+ return await this.renderCodeSystem(json, inBundle, _fmt, op);
302
+ case 'ValueSet': {
303
+ let exp = undefined;
304
+ if (!inBundle && !op && (!_fmt || _fmt == 'html')) {
305
+ try {
306
+ let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
307
+ exp = new ValueSet(await worker.handleInternalExpand(json, req));
308
+ } catch (error) {
309
+ exp = error;
310
+ }
311
+ }
312
+ return await this.renderValueSet(json, inBundle, _fmt, op, exp);
313
+ }
279
314
  case 'ConceptMap':
280
315
  return await this.renderConceptMap(json, inBundle);
281
316
  case 'CapabilityStatement':
@@ -575,35 +610,88 @@ class TxHtmlRenderer {
575
610
  /**
576
611
  * Render CodeSystem resource
577
612
  */
578
- async renderCodeSystem(json, inBundle) {
579
- let html = await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json));
613
+ async renderCodeSystem(json, inBundle, _fmt) {
614
+ if (inBundle) {
615
+ return await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json));
616
+ } else {
617
+ let html = `<ul class="nav nav-tabs">`;
618
+ html += this.tab(!_fmt || _fmt == 'html', json.resourceType, json.resourceType, 'html', json.id);
619
+ html += this.tab(_fmt && _fmt == 'html/json', 'JSON', json.resourceType, 'html/json', json.id);
620
+ html += this.tab(_fmt && _fmt == 'html/xml', 'XML', json.resourceType, 'html/xml', json.id);
621
+ html += this.tab(_fmt && _fmt == 'html/narrative', 'Original Narrative', json.resourceType, 'html/narrative', json.id);
622
+ html += this.tab(_fmt && _fmt == 'html/ops', 'LookUp / Subsumes', json.resourceType, 'html/ops', json.id);
623
+ html += `</ul>`;
624
+
625
+ if (!_fmt || _fmt == 'html') {
626
+ html += await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json));
627
+ } else if (_fmt == "html/json") {
628
+ html += await this.renderResourceJson(json);
629
+ } else if (_fmt == "html/xml") {
630
+ html += await this.renderResourceXml(json);
631
+ } else if (_fmt == "html/narrative") {
632
+ html += await this.renderResourceWithNarrative(json, json.text?.div);
633
+ } else if (_fmt == "html/ops") {
634
+ html += await this.liquid.renderFile('codesystem-operations', {
635
+ opsId: this.generateResourceId(),
636
+ vcSystemId: this.generateResourceId(),
637
+ inferSystemId: this.generateResourceId(),
638
+ url: escape(json.url || '')
639
+ });
640
+ }
580
641
 
581
- if (!inBundle) {
582
- html += await this.liquid.renderFile('codesystem-operations', {
583
- opsId: this.generateResourceId(),
584
- url: escape(json.url || '')
585
- });
586
- }
587
642
 
588
- return html;
643
+ return html;
644
+ }
589
645
  }
590
646
 
647
+ tab(b, name, rtype, type, id) {
648
+ if (b) {
649
+ return `<li class="active"><a href="#">${name}</a></li>`;
650
+ } else {
651
+ return `<li><a href="${this.path}/${rtype}/${id}?_format=${type}">${name}</a></li>`;
652
+ }
653
+ }
591
654
  /**
592
655
  * Render ValueSet resource
593
656
  */
594
- async renderValueSet(json, inBundle) {
595
- let html = await this.renderResourceWithNarrative(json, await this.renderer.renderValueSet(json));
596
-
597
- if (!inBundle) {
598
- html += await this.liquid.renderFile('valueset-operations', {
599
- opsId: this.generateResourceId(),
600
- vcSystemId: this.generateResourceId(),
601
- inferSystemId: this.generateResourceId(),
602
- url: escape(json.url || '')
603
- });
657
+ async renderValueSet(json, inBundle, _fmt, op, exp) {
658
+ if (inBundle || op) {
659
+ return await this.renderResourceWithNarrative(json, await this.renderer.renderValueSet(json));
660
+ } else {
661
+ let html = `<ul class="nav nav-tabs">`;
662
+ html += this.tab(!_fmt || _fmt == 'html', json.resourceType, json.resourceType, 'html', json.id);
663
+ html += this.tab(_fmt && _fmt == 'html/json', 'JSON', json.resourceType, 'html/json', json.id);
664
+ html += this.tab(_fmt && _fmt == 'html/xml', 'XML', json.resourceType, 'html/xml', json.id);
665
+ html += this.tab(_fmt && _fmt == 'html/narrative', 'Original Narrative', json.resourceType, 'html/narrative', json.id);
666
+ html += this.tab(_fmt && _fmt == 'html/ops', 'Expand / Validate', json.resourceType, 'html/ops', json.id);
667
+ html += `</ul>`;
668
+
669
+ if (!_fmt || _fmt == 'html') {
670
+ html += await this.renderResourceWithNarrative(json, await this.renderer.renderValueSet(json));
671
+ if (exp) {
672
+ html += "<h2>Expansion</h2>";
673
+ if (exp instanceof ValueSet) {
674
+ html += await this.renderer.renderVSExpansion(exp.jsonObj, false)
675
+ } else {
676
+ html += `<p>Error: `+exp.message+`</p>`;
677
+ }
678
+ }
679
+ } else if (_fmt == "html/json") {
680
+ html += await this.renderResourceJson(json);
681
+ } else if (_fmt == "html/xml") {
682
+ html += await this.renderResourceXml(json);
683
+ } else if (_fmt == "html/narrative") {
684
+ html += await this.renderResourceWithNarrative(json, json.text?.div);
685
+ } else if (_fmt == "html/ops") {
686
+ html += await this.liquid.renderFile('valueset-operations', {
687
+ opsId: this.generateResourceId(),
688
+ vcSystemId: this.generateResourceId(),
689
+ inferSystemId: this.generateResourceId(),
690
+ url: escape(json.url || '')
691
+ });
692
+ }
693
+ return html;
604
694
  }
605
-
606
- return html;
607
695
  }
608
696
 
609
697
  /**
@@ -1101,9 +1189,7 @@ class TxHtmlRenderer {
1101
1189
  * Render resource with text/div narrative and collapsible JSON source
1102
1190
  */
1103
1191
  async renderResourceWithNarrative(json, rendered) {
1104
- const resourceId = this.generateResourceId();
1105
-
1106
- let html = "";
1192
+ let html = '';
1107
1193
 
1108
1194
  // Show text/div narrative if present
1109
1195
  if (rendered) {
@@ -1113,30 +1199,37 @@ class TxHtmlRenderer {
1113
1199
  } else {
1114
1200
  html += '<div class="narrative">(No Narrative)</div>';
1115
1201
  }
1116
- if (json.text && json.text.div) {
1117
- // Collapsible JSON source
1118
- html += '<div class="xhtml">';
1119
- html += `<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleOriginalNarrative('${resourceId}x')">`;
1120
- html += 'Show Original Narrative</button>';
1121
- html += `<div id="${resourceId}x" class="original-narrative" style="display: none; margin-top: 10px;">`;
1122
-
1123
- html += '<div class="narrative">';
1124
- html += json.text.div; // Already HTML, render as-is
1125
- html += '</div>';
1126
- }
1127
- html += '</div>';
1128
- html += '</div>';
1129
1202
 
1203
+ return html;
1204
+ }
1130
1205
 
1131
- // Collapsible JSON source
1132
- html += '<div class="json-source">';
1133
- html += `<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleJsonSource('${resourceId}')">`;
1134
- html += 'Show JSON Source</button>';
1135
- html += `<div id="${resourceId}" class="json-content" style="display: none; margin-top: 10px;">`;
1206
+ async renderResourceJson(json) {
1207
+ let html = "";
1208
+ html += `<div class="json-content" style="margin-top: 10px;">`;
1136
1209
  html += `<pre>${escape(JSON.stringify(json, null, 2))}</pre>`;
1137
1210
  html += '</div>';
1138
- html += '</div>';
1211
+ return html;
1212
+ }
1213
+
1214
+ convertResourceToXml(res) {
1215
+ switch (res.resourceType) {
1216
+ case "CodeSystem" : return CodeSystemXML.toXml(res);
1217
+ case "ValueSet" : return ValueSetXML.toXml(res);
1218
+ case "Bundle" : return BundleXML.toXml(res, this.fhirVersion);
1219
+ case "CapabilityStatement" : return CapabilityStatementXML.toXml(res, "R5");
1220
+ case "TerminologyCapabilities" : return TerminologyCapabilitiesXML.toXml(res, "R5");
1221
+ case "Parameters": return ParametersXML.toXml(res, this.fhirVersion);
1222
+ case "OperationOutcome": return OperationOutcomeXML.toXml(res, this.fhirVersion);
1223
+ }
1224
+ throw new Error(`Resource type ${res.resourceType} not supported in XML`);
1225
+ }
1139
1226
 
1227
+ async renderResourceXml(json) {
1228
+ let xml = this.convertResourceToXml(json);
1229
+ let html = "";
1230
+ html += `<div class="xml-content" style="margin-top: 10px;">`;
1231
+ html += `<pre>${escape(xml)}</pre>`;
1232
+ html += '</div>';
1140
1233
  return html;
1141
1234
  }
1142
1235
 
@@ -1153,7 +1246,7 @@ class TxHtmlRenderer {
1153
1246
  }
1154
1247
 
1155
1248
  buildSourceOptions(provider) {
1156
- let result = '';
1249
+ let result = '<option value=""></option>';
1157
1250
  result += `<option value="internal">internal</option>`;
1158
1251
  for (let sp of provider.listValueSetSourceCodes()) {
1159
1252
  result += `<option value="${sp}">${sp}</option>`;
package/tx/tx.js CHANGED
@@ -247,6 +247,14 @@ class TXModule {
247
247
  if (this.stats) {
248
248
  this.stats.addTask("Client Cache", "5 min");
249
249
  }
250
+ this.timers.push(setInterval(async () => {
251
+ try {
252
+ await endpointInfo.provider.updateCodeSystemList();
253
+ } catch (error) {
254
+ this.log.error(`Error updating CodeSystem list for ${endpointPath}: ${error.message}`);
255
+ }
256
+ }, 60 * 1000));
257
+ this.log.info(`CodeSystem list update scheduled for ${endpointPath}`);
250
258
  this.timers.push(setInterval(() => {
251
259
  endpointInfo.resourceCache.prune(cacheTimeoutMs);
252
260
  }, pruneIntervalMs));
@@ -300,7 +308,7 @@ class TXModule {
300
308
  // Wrap res.json to intercept and convert to HTML if browser requests it, and log the request
301
309
  const originalJson = res.json.bind(res);
302
310
 
303
- let txhtml = new TxHtmlRenderer(new Renderer(opContext, endpointInfo.provider), this.liquid);
311
+ let txhtml = new TxHtmlRenderer(new Renderer(opContext, endpointInfo.provider), this.liquid, this.languages, this.i18n, endpointInfo.path);
304
312
  res.json = async (data) => {
305
313
  try {
306
314
  const duration = Date.now() - req.txStartTime;
@@ -897,7 +905,7 @@ class TXModule {
897
905
  router.get('/problems.html', async (req, res) => {
898
906
  const start = Date.now();
899
907
  try {
900
- let txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txProvider), this.liquid);
908
+ let txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txProvider), this.liquid, this.languages, this.i18n, req.txEndpoint.path);
901
909
  const problemFinder = new ProblemFinder();
902
910
  const content = await problemFinder.scanValueSets(req.txProvider);
903
911
  const html = await txhtml.renderPage('Problems', '<h3>ValueSet dependencies on unknown CodeSystem/Versions</h3>'+content, req.txEndpoint, req.txStartTime);