fhirsmith 0.8.4 → 0.8.6

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.
@@ -74,6 +74,8 @@ class CodeSystem extends CanonicalResource {
74
74
  */
75
75
  childToParentsMap = new Map();
76
76
 
77
+ hasMultiHierarchy = false;
78
+
77
79
  /**
78
80
  * Static factory method for convenience
79
81
  * @param {string} jsonString - JSON string representation of CodeSystem
@@ -326,6 +328,8 @@ class CodeSystem extends CanonicalResource {
326
328
 
327
329
  // Third pass: handle nested concept structures
328
330
  this._buildNestedHierarchy(this.jsonObj.concept);
331
+
332
+ this.hasMultiHierarchy = Array.from(this.childToParentsMap.values()).some(parents => parents.length > 1);
329
333
  }
330
334
 
331
335
  /**
@@ -133,7 +133,7 @@ class Renderer {
133
133
  return result;
134
134
  }
135
135
 
136
- async renderMetadataTable(res, tbl) {
136
+ async renderMetadataTable(res, tbl, sourcePackage) {
137
137
  this.renderMetadataVersion(res, tbl);
138
138
  await this.renderMetadataProfiles(res, tbl);
139
139
  this.renderMetadataTags(res, tbl);
@@ -141,8 +141,8 @@ class Renderer {
141
141
  this.renderMetadataLastUpdated(res, tbl);
142
142
  this.renderMetadataSource(res, tbl);
143
143
  this.renderProperty(tbl, 'TEST_PLAN_LANG', res.language);
144
- this.renderProperty(tbl, 'GENERAL_DEFINING_URL', res.url);
145
- this.renderProperty(tbl, 'GENERAL_VER', res.version);
144
+ this.renderPropertyCopy(tbl, 'GENERAL_DEFINING_URL', res.url);
145
+ this.renderPropertyCopy(tbl, 'GENERAL_VER', res.version);
146
146
  this.renderProperty(tbl, 'GENERAL_NAME', res.name);
147
147
  this.renderProperty(tbl, 'GENERAL_TITLE', res.title);
148
148
  this.renderProperty(tbl, 'GENERAL_STATUS', res.status);
@@ -150,9 +150,15 @@ class Renderer {
150
150
  this.renderPropertyMD(tbl, 'GENERAL_PURPOSE', res.purpose);
151
151
  this.renderProperty(tbl, 'CANON_REND_PUBLISHER', res.publisher);
152
152
  this.renderProperty(tbl, 'CANON_REND_COMMITTEE', Extensions.readString(res, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-wg'));
153
- this.renderProperty(tbl, 'GENERAL_COPYRIGHT', res.copyright);
153
+ this.renderPropertyCopy(tbl, 'GENERAL_COPYRIGHT', res.copyright);
154
154
  this.renderProperty(tbl, 'EXT_FMM_LEVEL', Extensions.readString(res, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm'));
155
155
  this.renderProperty(tbl, 'PAT_PERIOD', res.effectivePeriod);
156
+ this.renderPropertyLink(tbl, 'WEB_SOURCE', Extensions.readString(res, 'http://hl7.org/fhir/StructureDefinition/web-source'));
157
+ this.renderProperty(tbl, 'GENERAL_SOURCE', sourcePackage);
158
+ for (let ext of Extensions.list(res, 'http://hl7.org/fhir/uv/application-feature/StructureDefinition/feature')) {
159
+ this.renderProperty(tbl, 'FEATURE', this.featureSummary(ext));
160
+ }
161
+
156
162
 
157
163
  // capability statement things
158
164
  this.renderProperty(tbl, 'Kind', res.kind);
@@ -164,7 +170,6 @@ class Renderer {
164
170
  }
165
171
  }
166
172
  this.renderProperty(tbl, 'GENERAL_URL', res.implementation?.url);
167
- this.renderProperty(tbl, 'Kind', res.kind);
168
173
  this.renderProperty(tbl, 'EX_SCEN_FVER', res.fhirVersion);
169
174
 
170
175
  if (res.content === 'supplement' && res.supplements) {
@@ -253,15 +258,32 @@ class Renderer {
253
258
  }
254
259
  }
255
260
 
256
- async renderPropertyLink(tbl, msgId, value) {
261
+ renderPropertyCopy(tbl, msgId, value) {
257
262
  if (value) {
258
263
  let tr = tbl.tr();
259
264
  tr.td().b().tx(this.translate(msgId));
260
- const linkinfo = await this.linkResolver.resolveURL(this.opContext, value);
261
- if (linkinfo) {
262
- tr.td().ah(linkinfo.link).tx(linkinfo.description);
265
+ if (value instanceof Object) {
266
+ tr.td().tx("todo");
263
267
  } else {
264
- tr.td().tx(value);
268
+ let td = tr.td();
269
+ let span = td.span("copy-text");
270
+ span.tx(value);
271
+ let btn = span.button();
272
+ btn.attr("class", "btn-copy");
273
+ btn.attr("data-clipboard-text", value);
274
+ btn.attr("data-original-title", this.translate('GENERAL_COPY'));
275
+ }
276
+ }
277
+ }
278
+
279
+ renderPropertyLink(tbl, msgId, value) {
280
+ if (value) {
281
+ let tr = tbl.tr();
282
+ tr.td().b().tx(this.translate(msgId));
283
+ if (value instanceof Object) {
284
+ tr.td().tx("todo");
285
+ } else {
286
+ tr.td().ah(value).tx(value);
265
287
  }
266
288
  }
267
289
  }
@@ -346,7 +368,7 @@ class Renderer {
346
368
  return div_.toString();
347
369
  }
348
370
 
349
- async renderCodeSystem(cs) {
371
+ async renderCodeSystem(cs, sourcePackage) {
350
372
  if (cs.json) {
351
373
  cs = cs.json;
352
374
  }
@@ -355,7 +377,7 @@ class Renderer {
355
377
 
356
378
  // Metadata table
357
379
  div_.h3().tx("Properties");
358
- await this.renderMetadataTable(cs, div_.table("grid"));
380
+ await this.renderMetadataTable(cs, div_.table("grid"), sourcePackage);
359
381
 
360
382
  // Code system properties
361
383
  const hasProps = this.generateProperties(div_, cs);
@@ -2209,6 +2231,18 @@ class Renderer {
2209
2231
  }
2210
2232
  }
2211
2233
 
2234
+ featureSummary(ext) {
2235
+ let defn = Extensions.readString(ext, 'definition');
2236
+ let value = Extensions.readString(ext, 'value');
2237
+ switch (defn) {
2238
+ case 'http://hl7.org/fhir/uv/tx-tests/FeatureDefinition/test-version':
2239
+ return 'Tx Test version = ' + value;
2240
+ case 'http://hl7.org/fhir/uv/tx-ecosystem/FeatureDefinition/CodeSystemAsParameter':
2241
+ return 'CodeSystems as parameters = ' + value;
2242
+ default:
2243
+ return defn+' = ' + value;
2244
+ }
2245
+ }
2212
2246
  }
2213
2247
 
2214
2248
  module.exports = { Renderer };
@@ -15,7 +15,7 @@ function isDebugging() {
15
15
  }
16
16
  // Also check for debug flags in case inspector not yet attached
17
17
  return process.execArgv.some(arg =>
18
- arg.includes('--inspect') || arg.includes('--debug')
18
+ arg.includes('--inspect') || arg.includes('--debug')
19
19
  );
20
20
  }
21
21
 
@@ -233,16 +233,16 @@ class ExpansionCache {
233
233
  // Resources are now CodeSystem/ValueSet wrappers, not raw JSON
234
234
  if (additionalResources && additionalResources.length > 0) {
235
235
  const resourceHashes = additionalResources
236
- .map(r => {
237
- // Get the JSON object from wrapper or use directly
238
- const json = r.jsonObj || r;
239
- // Create a content hash for this resource
240
- return crypto.createHash('sha256')
241
- .update(JSON.stringify(json))
242
- .digest('hex')
243
- .substring(0, 16); // Use first 16 chars for brevity
244
- })
245
- .sort();
236
+ .map(r => {
237
+ // Get the JSON object from wrapper or use directly
238
+ const json = r.jsonObj || r;
239
+ // Create a content hash for this resource
240
+ return crypto.createHash('sha256')
241
+ .update(JSON.stringify(json))
242
+ .digest('hex')
243
+ .substring(0, 16); // Use first 16 chars for brevity
244
+ })
245
+ .sort();
246
246
  keyParts.push(`additional:${resourceHashes.join(',')}`);
247
247
  }
248
248
 
@@ -314,7 +314,7 @@ class ExpansionCache {
314
314
 
315
315
  // Get entries sorted by lastUsed (oldest first)
316
316
  const entries = Array.from(this.cache.entries())
317
- .sort((a, b) => a[1].lastUsed - b[1].lastUsed);
317
+ .sort((a, b) => a[1].lastUsed - b[1].lastUsed);
318
318
 
319
319
  const toEvict = Math.min(count, entries.length);
320
320
  for (let i = 0; i < toEvict; i++) {
@@ -413,7 +413,29 @@ class ExpansionCache {
413
413
  }
414
414
 
415
415
 
416
+ /**
417
+ * Read the cgroup memory limit once at startup.
418
+ * Returns the byte limit, or 0 if unavailable (disables the check).
419
+ */
420
+ function readMemoryLimit() {
421
+ try {
422
+ const raw = require('fs').readFileSync('/sys/fs/cgroup/memory.max', 'utf8').trim();
423
+ if (raw === 'max') return 0; // no cgroup limit
424
+ return parseInt(raw);
425
+ } catch {
426
+ return 0; // not on Linux / no cgroup
427
+ }
428
+ }
429
+
430
+ const MEMORY_LIMIT = readMemoryLimit();
431
+ const MEMORY_FRACTION = 0.98;
432
+ const MEMORY_THRESHOLD = MEMORY_LIMIT > 0 ? MEMORY_LIMIT * MEMORY_FRACTION : 0; // 90% of cgroup limit
433
+ const CHECK_FREQUENCY = 100;
434
+
416
435
  class OperationContext {
436
+ // Shared counter across all instances — only check RSS every CHECK_FREQUENCY calls
437
+ static _checkCounter = 0;
438
+
417
439
  constructor(langs, i18n = null, id = null, timeLimit = 30, resourceCache = null, expansionCache = null) {
418
440
  this.i18n = i18n;
419
441
  this.langs = this._ensureLanguages(langs);
@@ -445,8 +467,8 @@ class OperationContext {
445
467
  */
446
468
  copy() {
447
469
  const newContext = new OperationContext(
448
- this.langs, this.i18n, this.id, this.timeLimit / 1000,
449
- this.resourceCache, this.expansionCache
470
+ this.langs, this.i18n, this.id, this.timeLimit / 1000,
471
+ this.resourceCache, this.expansionCache
450
472
  );
451
473
  newContext.contexts = [...this.contexts];
452
474
  newContext.startTime = this.startTime;
@@ -458,31 +480,64 @@ class OperationContext {
458
480
  }
459
481
 
460
482
  /**
461
- * Check if operation has exceeded time limit
483
+ * Check if operation has exceeded time limit, or is pushing is over the memory limit
462
484
  * Skipped when running under debugger
485
+ *
486
+ * note: if the server pushes over the memory limit for the process, the process is terminated.
487
+ * the memory check here is intended to prevent process termination on the grounds that some
488
+ * big operation is pushing the limit. It might not be the big operation that is terminated first,
489
+ * but eventually it'll get terminated.
490
+ *
491
+ * this is called a *lot* so it's important to be efficient. Only check every CHECK_FREQUENCY
492
+ * times means that there could be a small overrun, but it's called often enough that the
493
+ * overrun won't be that signiifcant
494
+ *
463
495
  * @param {string} place - Location identifier for debugging
464
496
  * @returns {boolean} true if operation should be terminated
465
497
  */
466
498
  deadCheck(place = 'unknown') {
467
- // Skip time limit checks when debugging
468
499
  if (this.debugging) {
469
500
  return false;
470
501
  }
471
502
 
472
- const elapsed = performance.now() - this.startTime;
503
+ OperationContext._checkCounter++;
504
+ if (OperationContext._checkCounter < CHECK_FREQUENCY) {
505
+ return false;
506
+ }
507
+ OperationContext._checkCounter = 0;
473
508
 
509
+ // Time check
510
+ const elapsed = performance.now() - this.startTime;
474
511
  if (elapsed > this.timeLimit) {
475
512
  const timeInSeconds = Math.round(this.timeLimit / 1000);
476
513
  this.log(`Operation took too long @ ${place} (${this.constructor.name})`);
477
-
478
- const error = new Issue("error", "too-costly", null, `Operation exceeded time limit of ${timeInSeconds} seconds at ${place}`);
514
+ const error = new Issue("error", "too-costly", null,
515
+ `Operation exceeded time limit of ${timeInSeconds} seconds at ${place}`);
479
516
  error.diagnostics = this.diagnostics();
480
517
  throw error;
481
518
  }
482
519
 
520
+ // Memory check (piggyback on same sample)
521
+ if (MEMORY_THRESHOLD > 0) {
522
+ const rss = process.memoryUsage.rss();
523
+ if (rss > MEMORY_THRESHOLD) {
524
+ const usedGB = (rss / 1024 / 1024 / 1024).toFixed(1);
525
+ const limitGB = (MEMORY_LIMIT / 1024 / 1024 / 1024).toFixed(1);
526
+ this.log(`Memory Limit: ${usedGB} GB of ${limitGB} GB limit @ ${place}`);
527
+ const error = new Issue("error", "too-costly", null,
528
+ `Operation aborted: server memory usage (${usedGB} GB) exceeds safe threshold (${MEMORY_FRACTION * 100}% of ${limitGB} GB limit) at ${place}`);
529
+ error.diagnostics = this.diagnostics();
530
+ throw error;
531
+ }
532
+ }
533
+
483
534
  return false;
484
535
  }
485
536
 
537
+ unSeeAll() {
538
+ this.contexts = [];
539
+ }
540
+
486
541
  /**
487
542
  * Track a context URL and detect circular references
488
543
  * @param {string} vurl - Value set URL to track
package/tx/tx-html.js CHANGED
@@ -319,7 +319,7 @@ class TxHtmlRenderer {
319
319
  case 'Parameters':
320
320
  return await this.renderParameters(json);
321
321
  case 'CodeSystem':
322
- return await this.renderCodeSystem(json, inBundle, _fmt, op);
322
+ return await this.renderCodeSystem(json, inBundle, _fmt, op, req.sourcePackage);
323
323
  case 'ValueSet': {
324
324
  let exp = undefined;
325
325
  if (!inBundle && !op && (!_fmt || _fmt == 'html')) {
@@ -330,10 +330,10 @@ class TxHtmlRenderer {
330
330
  exp = error;
331
331
  }
332
332
  }
333
- return await this.renderValueSet(json, inBundle, _fmt, op, exp);
333
+ return await this.renderValueSet(json, inBundle, _fmt, op, exp, req.sourcePackage);
334
334
  }
335
335
  case 'ConceptMap':
336
- return await this.renderConceptMap(json, inBundle, _fmt, op);
336
+ return await this.renderConceptMap(json, inBundle, _fmt, op, req.sourcePackage);
337
337
  case 'CapabilityStatement':
338
338
  return await this.renderCapabilityStatement(json, inBundle);
339
339
  case 'TerminologyCapabilities':
@@ -632,7 +632,7 @@ class TxHtmlRenderer {
632
632
  /**
633
633
  * Render CodeSystem resource
634
634
  */
635
- async renderCodeSystem(json, inBundle, _fmt) {
635
+ async renderCodeSystem(json, inBundle, _fmt, op, sourcePackage) {
636
636
  if (inBundle) {
637
637
  return await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json));
638
638
  } else {
@@ -645,7 +645,7 @@ class TxHtmlRenderer {
645
645
  html += `</ul>`;
646
646
 
647
647
  if (!_fmt || _fmt == 'html') {
648
- html += await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json));
648
+ html += await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json, sourcePackage));
649
649
  } else if (_fmt == "html/json") {
650
650
  html += await this.renderResourceJson(json);
651
651
  } else if (_fmt == "html/xml") {
@@ -12,7 +12,7 @@ sources:
12
12
  - internal:hgvs
13
13
  - ucum:tx/data/ucum-essence.xml
14
14
  - loinc:loinc-2.77-a.db
15
- - loinc!:loinc-2.81-b.db
15
+ - loinc!:Loinc_2.82.db
16
16
  - rxnorm:rxnorm_03022026.db
17
17
  - ndc:ndc-20211101.db
18
18
  - unii:unii_20240622.db
package/tx/tx.js CHANGED
@@ -173,6 +173,7 @@ class TXModule {
173
173
  this.metadataHandler = new MetadataHandler({
174
174
  baseUrl: config.baseUrl,
175
175
  serverVersion: packageJson.version,
176
+ txVersion: packageJson.txVersion,
176
177
  softwareName: config.softwareName || 'FHIRsmith',
177
178
  name: config.name || 'FHIRTerminologyServer',
178
179
  title: config.title || 'FHIR Terminology Server',