fhirsmith 0.11.2 → 0.13.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 (68) hide show
  1. package/CHANGELOG.md +377 -2
  2. package/README.md +1 -1
  3. package/config-template.json +31 -1
  4. package/library/languages.js +191 -34
  5. package/library/path-safety.js +51 -0
  6. package/library/request-token.js +56 -0
  7. package/package.json +2 -2
  8. package/packages/packages.js +17 -118
  9. package/publisher/publisher.js +208 -30
  10. package/publisher/validation.js +210 -0
  11. package/server.js +24 -15
  12. package/stats-db.js +127 -0
  13. package/stats.js +172 -6
  14. package/translations/Messages.properties +5 -1
  15. package/tx/README.md +21 -0
  16. package/tx/cs/cs-api.js +30 -1
  17. package/tx/cs/cs-api.md +1 -1
  18. package/tx/cs/cs-cs.js +1 -1
  19. package/tx/cs/cs-icd11.js +805 -0
  20. package/tx/cs/cs-lang.js +331 -85
  21. package/tx/cs/cs-loinc.js +45 -4
  22. package/tx/cs/cs-mimetypes.js +492 -21
  23. package/tx/cs/cs-snomed.js +141 -58
  24. package/tx/cs/cs-ucum.js +33 -3
  25. package/tx/cs/icd11-expressions.js +243 -0
  26. package/tx/importers/export-icd11-codesystem.js +823 -0
  27. package/tx/importers/icd11-schema.md +285 -0
  28. package/tx/importers/import-icd11.js +861 -0
  29. package/tx/importers/import-loinc.module.js +14 -7
  30. package/tx/importers/import-sct.module.js +10 -1
  31. package/tx/library/codesystem.js +21 -27
  32. package/tx/library/conceptmap.js +39 -0
  33. package/tx/library/nesting-limit.js +79 -0
  34. package/tx/library/operation-outcome.js +97 -3
  35. package/tx/library/parameter-check.js +139 -0
  36. package/tx/library/valueset.js +16 -20
  37. package/tx/library.js +23 -1
  38. package/tx/ocl/cache/cache-utils.cjs +5 -3
  39. package/tx/ocl/cs-ocl.cjs +73 -17
  40. package/tx/ocl/jobs/background-queue.cjs +6 -3
  41. package/tx/ocl/mappers/concept-mapper.cjs +9 -1
  42. package/tx/ocl/shared/patches.cjs +48 -1
  43. package/tx/ocl/vs-ocl.cjs +15 -13
  44. package/tx/operation-context.js +220 -10
  45. package/tx/params.js +226 -175
  46. package/tx/sct/expressions.js +677 -15
  47. package/tx/tx.fhir.org.yml +3 -2
  48. package/tx/tx.js +72 -92
  49. package/tx/workers/batch-validate.js +11 -18
  50. package/tx/workers/batch.js +16 -28
  51. package/tx/workers/cache-control.js +87 -17
  52. package/tx/workers/closure.js +3 -8
  53. package/tx/workers/compare.js +6 -34
  54. package/tx/workers/expand.js +108 -60
  55. package/tx/workers/lookup.js +35 -40
  56. package/tx/workers/read.js +6 -40
  57. package/tx/workers/search.js +2 -8
  58. package/tx/workers/subsumes.js +58 -41
  59. package/tx/workers/translate.js +472 -47
  60. package/tx/workers/validate.js +83 -78
  61. package/tx/workers/worker.js +92 -11
  62. package/tx/xml/xml-base.js +264 -77
  63. package/tx/xversion/xv-conceptmap.js +81 -0
  64. package/tx/xversion/xv-parameters.js +8 -0
  65. package/tx/xversion/xv-valueset.js +39 -1
  66. package/utilities/merge-audit.py +178 -0
  67. package/utilities/why-merge-audit.md +39 -0
  68. package/tx/data/snomed-subset.txt +0 -84
@@ -26,17 +26,18 @@ sources:
26
26
  - snomed:sct_us_20230301.cache
27
27
  - snomed:sct_us_20240301.cache
28
28
  - snomed:sct_us_20250901.cache
29
- - snomed:sct_test_20250909.cache
29
+ - snomed:sct_test_20250909b.cache
30
30
  - cpt:CodeSystem-cpt.db|cpt-2023-fragment-0.1.db
31
31
  - omop:omop_v20250227.db
32
32
  - npm:hl7.terminology
33
- - npm:fhir.tx.support.r4
33
+ - npm:fhir.tx.support
34
34
  - npm:ihe.formatcode.fhir
35
35
  - npm:fhir.dicom
36
36
  - npm:hl7.fhir.us.core
37
37
  - npm:us.cdc.phinvads
38
38
  - npm:hl7.fhir.uv.sdc
39
39
  - internal:vsac
40
+ - icd11:icd11-2026-01.db
40
41
 
41
42
  ignored:
42
43
  - urn:iso:std:iso:3166#20210120
package/tx/tx.js CHANGED
@@ -51,6 +51,7 @@ const ClosureWorker = require("./workers/closure");
51
51
  const {BundleXML} = require("./xml/bundle-xml");
52
52
  const ConceptUsageTracker = require("./usage-tracker");
53
53
  const ProblemFinder = require("./problems");
54
+ const { buildOperationOutcome } = require('./library/operation-outcome');
54
55
  // const {writeFileSync} = require("fs");
55
56
 
56
57
  class TXModule {
@@ -249,6 +250,10 @@ class TXModule {
249
250
  // cacheTimeout is in minutes, default to 30 minutes
250
251
  const cacheTimeoutMs = cacheTimeoutMinutes * 60 * 1000;
251
252
  const pruneIntervalMs = 5 * 60 * 1000; // Run every 5 minutes
253
+ // Tell the cache what timeout it is being pruned against, so $cache-control?mode=check
254
+ // can report it and a client can size its keepalive interval to this server rather
255
+ // than guessing at a number it has no way to see.
256
+ endpointInfo.resourceCache.setIdleTimeout(cacheTimeoutMs);
252
257
  if (this.stats) {
253
258
  this.stats.addTask("Client Cache", "5 min");
254
259
  }
@@ -433,14 +438,7 @@ class TXModule {
433
438
  }
434
439
  } catch (e) {
435
440
  this.log.error(`JSON parse error: ${e.message}`);
436
- return res.status(400).json({
437
- resourceType: 'OperationOutcome',
438
- issue: [{
439
- severity: 'error',
440
- code: 'invalid',
441
- diagnostics: `Invalid JSON: ${e.message}`
442
- }]
443
- });
441
+ return res.status(400).json(buildOperationOutcome('error', 'invalid', `Invalid JSON: ${e.message}`));
444
442
  }
445
443
  }
446
444
 
@@ -461,25 +459,11 @@ class TXModule {
461
459
  req.body = this.convertXmlToResource(xmlStr);
462
460
  } catch (e) {
463
461
  this.log.error(`XML parse error: ${e.message}`);
464
- return res.status(400).json({
465
- resourceType: 'OperationOutcome',
466
- issue: [{
467
- severity: 'error',
468
- code: 'invalid',
469
- diagnostics: `Invalid XML: ${e.message}`
470
- }]
471
- });
462
+ return res.status(400).json(buildOperationOutcome('error', 'invalid', `Invalid XML: ${e.message}`));
472
463
  }
473
464
  }
474
465
  } else if (contentType != 'application/x-www-form-urlencoded') {
475
- return res.status(415).json({
476
- resourceType: 'OperationOutcome',
477
- issue: [{
478
- severity: 'error',
479
- code: 'invalid',
480
- diagnostics: `Unsupported Media Type: ${contentType}`
481
- }]
482
- });
466
+ return res.status(415).json(buildOperationOutcome('error', 'invalid', `Unsupported Media Type: ${contentType}`));
483
467
  }
484
468
 
485
469
  if (req.body) {
@@ -491,7 +475,7 @@ class TXModule {
491
475
  app.use(express.urlencoded({ extended: true }));
492
476
 
493
477
  // Set up routes
494
- this.setupRoutes(router);
478
+ this.setupRoutes(router, endpointInfo.path);
495
479
 
496
480
  // Redirect /r5 → /r5/
497
481
  app.use((req, res, next) => {
@@ -512,8 +496,11 @@ class TXModule {
512
496
  /**
513
497
  * Set up routes for an endpoint
514
498
  * @param {express.Router} router - Express router
499
+ * @param {string} endpointPath - the endpoint these routes belong to, e.g.
500
+ * '/tx/r4'. Captured by the handlers below so that every request is
501
+ * counted against the endpoint that served it, not just the operation.
515
502
  */
516
- setupRoutes(router) {
503
+ setupRoutes(router, endpointPath) {
517
504
  const resourceTypes = ['CodeSystem', 'ValueSet', 'ConceptMap'];
518
505
 
519
506
  // ===== Operations =====
@@ -526,7 +513,7 @@ class TXModule {
526
513
  let worker = new LookupWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
527
514
  await worker.handle(req, res);
528
515
  } finally {
529
- this.countRequest('$lookup', Date.now() - start);
516
+ this.countRequest(endpointPath, '$lookup', Date.now() - start);
530
517
  }
531
518
  });
532
519
  router.post('/CodeSystem/\\$lookup', async (req, res) => {
@@ -535,7 +522,7 @@ class TXModule {
535
522
  let worker = new LookupWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
536
523
  await worker.handle(req, res);
537
524
  } finally {
538
- this.countRequest('$lookup', Date.now() - start);
525
+ this.countRequest(endpointPath, '$lookup', Date.now() - start);
539
526
  }
540
527
  });
541
528
 
@@ -546,7 +533,7 @@ class TXModule {
546
533
  let worker = new SubsumesWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
547
534
  await worker.handle(req, res);
548
535
  } finally {
549
- this.countRequest('$subsumes', Date.now() - start);
536
+ this.countRequest(endpointPath, '$subsumes', Date.now() - start);
550
537
  }
551
538
  });
552
539
  router.post('/CodeSystem/\\$subsumes', async (req, res) => {
@@ -555,7 +542,7 @@ class TXModule {
555
542
  let worker = new SubsumesWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
556
543
  await worker.handle(req, res);
557
544
  } finally {
558
- this.countRequest('$subsumes', Date.now() - start);
545
+ this.countRequest(endpointPath, '$subsumes', Date.now() - start);
559
546
  }
560
547
  });
561
548
 
@@ -566,7 +553,7 @@ class TXModule {
566
553
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
567
554
  await worker.handleCodeSystem(req, res);
568
555
  } finally {
569
- this.countRequest('$validate', Date.now() - start);
556
+ this.countRequest(endpointPath, '$validate', Date.now() - start);
570
557
  }
571
558
  });
572
559
  router.post('/CodeSystem/\\$validate-code', async (req, res) => {
@@ -575,7 +562,7 @@ class TXModule {
575
562
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
576
563
  await worker.handleCodeSystem(req, res);
577
564
  } finally {
578
- this.countRequest('$validate', Date.now() - start);
565
+ this.countRequest(endpointPath, '$validate', Date.now() - start);
579
566
  }
580
567
  });
581
568
 
@@ -586,7 +573,7 @@ class TXModule {
586
573
  let worker = new BatchValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
587
574
  await worker.handleCodeSystem(req, res);
588
575
  } finally {
589
- this.countRequest('$batch', Date.now() - start);
576
+ this.countRequest(endpointPath, '$batch', Date.now() - start);
590
577
  }
591
578
  });
592
579
  router.post('/CodeSystem/\\$batch-validate-code', async (req, res) => {
@@ -595,7 +582,7 @@ class TXModule {
595
582
  let worker = new BatchValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
596
583
  await worker.handleCodeSystem(req, res);
597
584
  } finally {
598
- this.countRequest('$batch', Date.now() - start);
585
+ this.countRequest(endpointPath, '$batch', Date.now() - start);
599
586
  }
600
587
  });
601
588
  // ValueSet/$validate-code (GET and POST)
@@ -605,7 +592,7 @@ class TXModule {
605
592
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
606
593
  await worker.handleValueSet(req, res);
607
594
  } finally {
608
- this.countRequest('$validate', Date.now() - start);
595
+ this.countRequest(endpointPath, '$validate', Date.now() - start);
609
596
  }
610
597
  });
611
598
  router.post('/ValueSet/\\$validate-code', async (req, res) => {
@@ -614,7 +601,7 @@ class TXModule {
614
601
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
615
602
  await worker.handleValueSet(req, res);
616
603
  } finally {
617
- this.countRequest('$validate', Date.now() - start);
604
+ this.countRequest(endpointPath, '$validate', Date.now() - start);
618
605
  }
619
606
  });
620
607
 
@@ -625,7 +612,7 @@ class TXModule {
625
612
  let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
626
613
  await worker.handle(req, res);
627
614
  } finally {
628
- this.countRequest('$compare', Date.now() - start);
615
+ this.countRequest(endpointPath, '$compare', Date.now() - start);
629
616
  }
630
617
  });
631
618
  router.post('/ValueSet/\\$compare', async (req, res) => {
@@ -634,7 +621,7 @@ class TXModule {
634
621
  let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
635
622
  await worker.handle(req, res);
636
623
  } finally {
637
- this.countRequest('$compare', Date.now() - start);
624
+ this.countRequest(endpointPath, '$compare', Date.now() - start);
638
625
  }
639
626
  });
640
627
 
@@ -645,7 +632,7 @@ class TXModule {
645
632
  let worker = new BatchValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
646
633
  await worker.handleValueSet(req, res);
647
634
  } finally {
648
- this.countRequest('$batch', Date.now() - start);
635
+ this.countRequest(endpointPath, '$batch', Date.now() - start);
649
636
  }
650
637
  });
651
638
  router.post('/ValueSet/\\$batch-validate-code', async (req, res) => {
@@ -654,7 +641,7 @@ class TXModule {
654
641
  let worker = new BatchValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
655
642
  await worker.handleValueSet(req, res);
656
643
  } finally {
657
- this.countRequest('validate', Date.now() - start);
644
+ this.countRequest(endpointPath, 'validate', Date.now() - start);
658
645
  }
659
646
  });
660
647
 
@@ -665,7 +652,7 @@ class TXModule {
665
652
  let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
666
653
  await worker.handle(req, res, this.log);
667
654
  } finally {
668
- this.countRequest('$expand', Date.now() - start);
655
+ this.countRequest(endpointPath, '$expand', Date.now() - start);
669
656
  }
670
657
  });
671
658
  router.post('/ValueSet/\\$expand', async (req, res) => {
@@ -674,7 +661,7 @@ class TXModule {
674
661
  let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
675
662
  await worker.handle(req, res, this.log);
676
663
  } finally {
677
- this.countRequest('$expand', Date.now() - start);
664
+ this.countRequest(endpointPath, '$expand', Date.now() - start);
678
665
  }
679
666
  });
680
667
 
@@ -685,7 +672,7 @@ class TXModule {
685
672
  let worker = new CacheControlWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
686
673
  await worker.handle(req, res, this.log);
687
674
  } finally {
688
- this.countRequest('$cache-control', Date.now() - start);
675
+ this.countRequest(endpointPath, '$cache-control', Date.now() - start);
689
676
  }
690
677
  });
691
678
  router.post('/\\$cache-control', async (req, res) => {
@@ -694,7 +681,7 @@ class TXModule {
694
681
  let worker = new CacheControlWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
695
682
  await worker.handle(req, res, this.log);
696
683
  } finally {
697
- this.countRequest('$cache-control', Date.now() - start);
684
+ this.countRequest(endpointPath, '$cache-control', Date.now() - start);
698
685
  }
699
686
  });
700
687
 
@@ -705,7 +692,7 @@ class TXModule {
705
692
  let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
706
693
  await worker.handle(req, res, this.log);
707
694
  } finally {
708
- this.countRequest('$translate', Date.now() - start);
695
+ this.countRequest(endpointPath, '$translate', Date.now() - start);
709
696
  }
710
697
  });
711
698
  router.post('/ConceptMap/\\$translate', async (req, res) => {
@@ -714,7 +701,7 @@ class TXModule {
714
701
  let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
715
702
  await worker.handle(req, res, this.log);
716
703
  } finally {
717
- this.countRequest('$translate', Date.now() - start);
704
+ this.countRequest(endpointPath, '$translate', Date.now() - start);
718
705
  }
719
706
  });
720
707
 
@@ -725,7 +712,7 @@ class TXModule {
725
712
  let worker = new ClosureWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
726
713
  await worker.handle(req, res, this.log);
727
714
  } finally {
728
- this.countRequest('$closure', Date.now() - start);
715
+ this.countRequest(endpointPath, '$closure', Date.now() - start);
729
716
  }
730
717
  });
731
718
  router.post('/ConceptMap/\\$closure', async (req, res) => {
@@ -734,7 +721,7 @@ class TXModule {
734
721
  let worker = new ClosureWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
735
722
  await worker.handle(req, res, this.log);
736
723
  } finally {
737
- this.countRequest('$closure', Date.now() - start);
724
+ this.countRequest(endpointPath, '$closure', Date.now() - start);
738
725
  }
739
726
  });
740
727
 
@@ -747,7 +734,7 @@ class TXModule {
747
734
  let worker = new LookupWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
748
735
  await worker.handleInstance(req, res);
749
736
  } finally {
750
- this.countRequest('$lookup', Date.now() - start);
737
+ this.countRequest(endpointPath, '$lookup', Date.now() - start);
751
738
  }
752
739
  });
753
740
  router.post('/CodeSystem/:id/\\$lookup', async (req, res) => {
@@ -756,7 +743,7 @@ class TXModule {
756
743
  let worker = new LookupWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
757
744
  await worker.handleInstance(req, res);
758
745
  } finally {
759
- this.countRequest('$lookup', Date.now() - start);
746
+ this.countRequest(endpointPath, '$lookup', Date.now() - start);
760
747
  }
761
748
  });
762
749
 
@@ -767,7 +754,7 @@ class TXModule {
767
754
  let worker = new SubsumesWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
768
755
  await worker.handleInstance(req, res);
769
756
  } finally {
770
- this.countRequest('$subsumes', Date.now() - start);
757
+ this.countRequest(endpointPath, '$subsumes', Date.now() - start);
771
758
  }
772
759
  });
773
760
  router.post('/CodeSystem/:id/\\$subsumes', async (req, res) => {
@@ -776,7 +763,7 @@ class TXModule {
776
763
  let worker = new SubsumesWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
777
764
  await worker.handleInstance(req, res);
778
765
  } finally {
779
- this.countRequest('$subsumes', Date.now() - start);
766
+ this.countRequest(endpointPath, '$subsumes', Date.now() - start);
780
767
  }
781
768
  });
782
769
 
@@ -787,7 +774,7 @@ class TXModule {
787
774
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
788
775
  await worker.handleCodeSystemInstance(req, res, this.log);
789
776
  } finally {
790
- this.countRequest('$validate', Date.now() - start);
777
+ this.countRequest(endpointPath, '$validate', Date.now() - start);
791
778
  }
792
779
  });
793
780
  router.post('/CodeSystem/:id/\\$validate-code', async (req, res) => {
@@ -796,7 +783,7 @@ class TXModule {
796
783
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
797
784
  await worker.handleCodeSystemInstance(req, res, this.log);
798
785
  } finally {
799
- this.countRequest('$validate', Date.now() - start);
786
+ this.countRequest(endpointPath, '$validate', Date.now() - start);
800
787
  }
801
788
 
802
789
  });
@@ -808,7 +795,7 @@ class TXModule {
808
795
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
809
796
  await worker.handleValueSetInstance(req, res, this.log);
810
797
  } finally {
811
- this.countRequest('$validate', Date.now() - start);
798
+ this.countRequest(endpointPath, '$validate', Date.now() - start);
812
799
  }
813
800
  });
814
801
  router.post('/ValueSet/:id/\\$validate-code', async (req, res) => {
@@ -817,7 +804,7 @@ class TXModule {
817
804
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
818
805
  await worker.handleValueSetInstance(req, res, this.log);
819
806
  } finally {
820
- this.countRequest('$validate', Date.now() - start);
807
+ this.countRequest(endpointPath, '$validate', Date.now() - start);
821
808
  }
822
809
  });
823
810
 
@@ -829,7 +816,7 @@ class TXModule {
829
816
  let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
830
817
  await worker.handleInstance(req, res, this.log);
831
818
  } finally {
832
- this.countRequest('$compare', Date.now() - start);
819
+ this.countRequest(endpointPath, '$compare', Date.now() - start);
833
820
  }
834
821
  });
835
822
  router.post('/ValueSet/:id/\\$compare', async (req, res) => {
@@ -838,7 +825,7 @@ class TXModule {
838
825
  let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
839
826
  await worker.handleInstance(req, res, this.log);
840
827
  } finally {
841
- this.countRequest('$compare', Date.now() - start);
828
+ this.countRequest(endpointPath, '$compare', Date.now() - start);
842
829
  }
843
830
  });
844
831
 
@@ -849,7 +836,7 @@ class TXModule {
849
836
  let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
850
837
  await worker.handleInstance(req, res, this.log);
851
838
  } finally {
852
- this.countRequest('$expand', Date.now() - start);
839
+ this.countRequest(endpointPath, '$expand', Date.now() - start);
853
840
  }
854
841
  });
855
842
  router.post('/ValueSet/:id/\\$expand', async (req, res) => {
@@ -858,7 +845,7 @@ class TXModule {
858
845
  let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
859
846
  await worker.handleInstance(req, res, this.log);
860
847
  } finally {
861
- this.countRequest('$expand', Date.now() - start);
848
+ this.countRequest(endpointPath, '$expand', Date.now() - start);
862
849
  }
863
850
  });
864
851
 
@@ -869,7 +856,7 @@ class TXModule {
869
856
  let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
870
857
  await worker.handleInstance(req, res, this.log);
871
858
  } finally {
872
- this.countRequest('$translate', Date.now() - start);
859
+ this.countRequest(endpointPath, '$translate', Date.now() - start);
873
860
  }
874
861
  });
875
862
  router.post('/ConceptMap/:id/\\$translate', async (req, res) => {
@@ -878,7 +865,7 @@ class TXModule {
878
865
  let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
879
866
  await worker.handleInstance(req, res, this.log);
880
867
  } finally {
881
- this.countRequest('$translate', Date.now() - start);
868
+ this.countRequest(endpointPath, '$translate', Date.now() - start);
882
869
  }
883
870
  });
884
871
 
@@ -900,7 +887,7 @@ class TXModule {
900
887
  let worker = new ReadWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
901
888
  await worker.handle(req, res, resourceType);
902
889
  } finally {
903
- this.countRequest('read', Date.now() - start);
890
+ this.countRequest(endpointPath, 'read', Date.now() - start);
904
891
  }
905
892
  });
906
893
  }
@@ -913,7 +900,7 @@ class TXModule {
913
900
  let worker = new SearchWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
914
901
  await worker.handle(req, res, resourceType);
915
902
  } finally {
916
- this.countRequest('search', Date.now() - start);
903
+ this.countRequest(endpointPath, 'search', Date.now() - start);
917
904
  }
918
905
  });
919
906
  router.post(`/${resourceType}/_search`, async (req, res) => {
@@ -922,7 +909,7 @@ class TXModule {
922
909
  let worker = new SearchWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
923
910
  await worker.handle(req, res, resourceType);
924
911
  } finally {
925
- this.countRequest('search', Date.now() - start);
912
+ this.countRequest(endpointPath, 'search', Date.now() - start);
926
913
  }
927
914
  });
928
915
  }
@@ -940,7 +927,7 @@ class TXModule {
940
927
  ));
941
928
  }
942
929
  } finally {
943
- this.countRequest('$read', Date.now() - start);
930
+ this.countRequest(endpointPath, '$read', Date.now() - start);
944
931
  }
945
932
  });
946
933
  }
@@ -951,7 +938,7 @@ class TXModule {
951
938
  let worker = new OperationsWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
952
939
  await worker.handle(req, res);
953
940
  } finally {
954
- this.countRequest('$op', Date.now() - start);
941
+ this.countRequest(endpointPath, '$op', Date.now() - start);
955
942
  }
956
943
  });
957
944
 
@@ -965,7 +952,7 @@ class TXModule {
965
952
  res.setHeader('Content-Type', 'text/html');
966
953
  res.send(html);
967
954
  } finally {
968
- this.countRequest('problems', Date.now() - start);
955
+ this.countRequest(endpointPath, 'problems', Date.now() - start);
969
956
  }
970
957
  });
971
958
 
@@ -980,7 +967,7 @@ class TXModule {
980
967
  res.status(500).json(this.operationOutcome('error', 'exception', error.message));
981
968
  }
982
969
  } finally {
983
- this.countRequest('metadata', Date.now() - start);
970
+ this.countRequest(endpointPath, 'metadata', Date.now() - start);
984
971
  }
985
972
  });
986
973
 
@@ -995,7 +982,7 @@ class TXModule {
995
982
  res.status(500).json(this.operationOutcome('error', 'exception', error.message));
996
983
  }
997
984
  } finally {
998
- this.countRequest('$versions', Date.now() - start);
985
+ this.countRequest(endpointPath, '$versions', Date.now() - start);
999
986
  }
1000
987
  });
1001
988
 
@@ -1003,16 +990,9 @@ class TXModule {
1003
990
  router.get('/', async (req, res) => {
1004
991
  const start = Date.now();
1005
992
  try {
1006
- await res.json({
1007
- resourceType: 'OperationOutcome',
1008
- issue: [{
1009
- severity: 'information',
1010
- code: 'informational',
1011
- diagnostics: `FHIR Terminology Server - FHIR v${req.txEndpoint.fhirVersion}`
1012
- }]
1013
- });
993
+ await res.json(buildOperationOutcome('information', 'informational', `FHIR Terminology Server - FHIR v${req.txEndpoint.fhirVersion}`));
1014
994
  } finally {
1015
- this.countRequest('home', Date.now() - start);
995
+ this.countRequest(endpointPath, 'home', Date.now() - start);
1016
996
  }
1017
997
  });
1018
998
 
@@ -1037,7 +1017,7 @@ class TXModule {
1037
1017
  this.log.error(`Error rendering info page for ${req.params.id}: ${error.message}`);
1038
1018
  res.status(500).send('Internal server error');
1039
1019
  } finally {
1040
- this.countRequest('info', Date.now() - start);
1020
+ this.countRequest(endpointPath, 'info', Date.now() - start);
1041
1021
  }
1042
1022
  };
1043
1023
  router.get('/info/:id', infoHandler);
@@ -1118,17 +1098,17 @@ class TXModule {
1118
1098
  }
1119
1099
 
1120
1100
  /**
1121
- * Build an OperationOutcome for errors
1101
+ * Build an OperationOutcome. Delegates to the shared builder so that every outcome this
1102
+ * server emits has details.text and a tx-issue-type coding: diagnostics is stripped by
1103
+ * the test harness, so nothing a caller needs may live there.
1104
+ * @param {string} severity - error, warning, information
1105
+ * @param {string} code - FHIR issue type
1106
+ * @param {string} message - the human readable account of the problem
1107
+ * @param {string} [txIssueType] - tx-issue-type; defaulted from code when not given
1108
+ * @returns {Object} OperationOutcome resource
1122
1109
  */
1123
- operationOutcome(severity, code, message) {
1124
- return {
1125
- resourceType: 'OperationOutcome',
1126
- issue: [{
1127
- severity,
1128
- code,
1129
- diagnostics: message
1130
- }]
1131
- };
1110
+ operationOutcome(severity, code, message, txIssueType = null) {
1111
+ return buildOperationOutcome(severity, code, message, txIssueType);
1132
1112
  }
1133
1113
 
1134
1114
  /**
@@ -1214,9 +1194,9 @@ class TXModule {
1214
1194
  return data;
1215
1195
  }
1216
1196
 
1217
- countRequest(name, tat) {
1197
+ countRequest(endpoint, name, tat) {
1218
1198
  if (this.stats) {
1219
- this.stats.countRequest(name, tat);
1199
+ this.stats.countRequest(name, tat, endpoint);
1220
1200
  }
1221
1201
  }
1222
1202
 
@@ -13,7 +13,7 @@
13
13
  //
14
14
 
15
15
  const { TerminologyWorker } = require('./worker');
16
- const {OperationOutcome, Issue} = require("../library/operation-outcome");
16
+ const { OperationOutcome, Issue, buildOperationOutcome, outcomeFromError } = require('../library/operation-outcome');
17
17
  const {Parameters} = require("../library/parameters");
18
18
  const {ValidateWorker} = require("./validate");
19
19
  const {debugLog} = require("../operation-context");
@@ -137,7 +137,7 @@ class BatchValidateWorker extends TerminologyWorker {
137
137
  op.addIssue(error);
138
138
  output.push({name: "validation", resource : op.jsonObj});
139
139
  } else {
140
- output.push({name: "validation", resource : this.operationOutcome('error', error.issueCode || 'exception', error.message) } );
140
+ output.push({name: "validation", resource : outcomeFromError(error) } );
141
141
  }
142
142
  }
143
143
  }
@@ -156,8 +156,7 @@ class BatchValidateWorker extends TerminologyWorker {
156
156
  oo.addIssue(error);
157
157
  return res.status(error.statusCode || 500).json(oo.jsonObj);
158
158
  }
159
- return res.status(error.statusCode || 500).json(this.operationOutcome(
160
- 'error', error.issueCode || 'exception', error.message));
159
+ return res.status(error.statusCode || 500).json(outcomeFromError(error));
161
160
  }
162
161
  }
163
162
 
@@ -187,8 +186,11 @@ class BatchValidateWorker extends TerminologyWorker {
187
186
  return false;
188
187
  }
189
188
  if (!cache.has(cacheId)) {
190
- throw new Issue('error', 'not-found', null, 'CACHE_ID_UNKNOWN',
191
- this.i18n.translate('CACHE_ID_UNKNOWN', this.opContext.langs, [cacheId]),
189
+ // describeMissing names which fate the id met (never issued here / closed by
190
+ // the client / idle-expired); the coding stays cache-id-unknown either way.
191
+ const { messageId, params: msgParams } = cache.describeMissing(cacheId);
192
+ throw new Issue('error', 'not-found', null, messageId,
193
+ this.i18n.translate(messageId, this.opContext.langs, msgParams),
192
194
  'cache-id-unknown', 404);
193
195
  }
194
196
  if (cache.isSealed(cacheId)) {
@@ -215,18 +217,9 @@ class BatchValidateWorker extends TerminologyWorker {
215
217
  /**
216
218
  * Build an OperationOutcome
217
219
  */
218
- operationOutcome(severity, code, message) {
219
- return {
220
- resourceType: 'OperationOutcome',
221
- issue: [{
222
- severity,
223
- code,
224
- details: {
225
- text: message
226
- },
227
- diagnostics: message
228
- }]
229
- };
220
+ operationOutcome(severity, code, message, txIssueType = null) {
221
+ // the shared builder, so that every outcome has details.text and a tx-issue-type coding
222
+ return buildOperationOutcome(severity, code, message, txIssueType);
230
223
  }
231
224
 
232
225
  hasValueSet(parameter) {