fhirsmith 0.13.4 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/README.md +16 -3
  3. package/config-template.json +81 -1
  4. package/library/cgroup-memory.js +113 -0
  5. package/library/folder-setup.js +8 -1
  6. package/library/languages.js +15 -0
  7. package/library/same-origin.js +62 -0
  8. package/nginx.md +399 -0
  9. package/package.json +3 -2
  10. package/publisher/git-remote.js +141 -0
  11. package/publisher/github-release.js +195 -0
  12. package/publisher/publisher-template.html +1 -0
  13. package/publisher/publisher.js +241 -56
  14. package/registry/registry.js +3 -1
  15. package/server.js +57 -15
  16. package/stats.js +19 -13
  17. package/testing/readme.md +144 -0
  18. package/testing/render.js +723 -0
  19. package/testing/search.js +378 -0
  20. package/testing/store.js +410 -0
  21. package/testing/testing-template.html +126 -0
  22. package/testing/testing.js +760 -0
  23. package/translations/Messages.properties +32 -0
  24. package/tx/closure/closure-store.js +396 -0
  25. package/tx/cs/cs-api.js +6 -1
  26. package/tx/cs/cs-rxnorm.js +35 -8
  27. package/tx/cs/cs-snomed.js +19 -69
  28. package/tx/html/codesystem-operations.liquid +177 -0
  29. package/tx/html/ecl-panel.liquid +93 -0
  30. package/tx/html/home-metrics.liquid +39 -76
  31. package/tx/html/tx-template.html +1 -0
  32. package/tx/library/conceptmap.js +22 -6
  33. package/tx/library/designations.js +12 -3
  34. package/tx/library/operation-outcome.js +3 -1
  35. package/tx/library/renderer.js +42 -5
  36. package/tx/operation-context.js +80 -17
  37. package/tx/provider.js +85 -0
  38. package/tx/sct/editions.js +70 -0
  39. package/tx/sct/expressions.js +128 -44
  40. package/tx/tx-html.js +426 -44
  41. package/tx/tx.js +117 -9
  42. package/tx/workers/cache-control.js +2 -2
  43. package/tx/workers/closure.js +483 -13
  44. package/tx/workers/expand.js +2 -2
  45. package/tx/workers/metadata.js +8 -3
  46. package/tx/workers/read.js +18 -3
  47. package/tx/workers/subsumes.js +18 -2
  48. package/tx/workers/translate.js +192 -21
  49. package/tx/workers/validate.js +70 -187
  50. package/tx/workers/worker.js +12 -35
  51. package/tx/xversion/xv-conceptmap.js +17 -8
  52. package/utilities/submit-test-report.js +150 -0
@@ -7,6 +7,7 @@ const {Issue} = require("../library/operation-outcome");
7
7
  const {Languages} = require("../../library/languages");
8
8
  const {ConceptMap} = require("../library/conceptmap");
9
9
  const {Renderer} = require("../library/renderer");
10
+ const {editionName} = require("../sct/editions");
10
11
 
11
12
  // The cache-id travels as an HTTP header (not an operation parameter) so proxies /
12
13
  // load-balancers can act on it and the server can reject it before parsing the body.
@@ -33,6 +34,15 @@ class TerminologyWorker {
33
34
  foundParameters = [];
34
35
  renderer;
35
36
 
37
+ // ValueSetChecker reads both of these off whatever worker it is given, so they belong to
38
+ // every worker that can construct one - not just the validate and expand workers that
39
+ // happen to have declared them first. A worker without them fails inside the checker with
40
+ // "Cannot read properties of undefined (reading 'size')", a long way from the cause.
41
+ // ValidateWorker and ExpandWorker redeclare them; a subclass field initialiser overrides
42
+ // the base one, so their behaviour is unchanged.
43
+ requiredSupplements = new Set();
44
+ usedSupplements = new Set();
45
+
36
46
  /**
37
47
  * @param {OperationContext} opContext - Operation context
38
48
  * @param {Logger} log - Provider for code systems and resources
@@ -1091,42 +1101,9 @@ function Unknown_Code_in_VersionSCT(url, version) {
1091
1101
  function SCTVersion(url, ver) {
1092
1102
  if (url !== 'http://snomed.info/sct' || !ver) {
1093
1103
  return '';
1094
- } else {
1095
- let result = 'unknown';
1096
- let s = ver.split('/');
1097
- if (s.length >= 5) {
1098
- if (s[4] === '900000000000207008') result = 'International Edition';
1099
- else if (s[4] === '449081005') result = 'International Spanish Edition';
1100
- else if (s[4] === '11000221109') result = 'Argentinian Edition';
1101
- else if (s[4] === '32506021000036107') result = 'Australian Edition (with drug extension)';
1102
- else if (s[4] === '11000234105') result = 'Austrian Edition';
1103
- else if (s[4] === '11000172109') result = 'Belgian Edition';
1104
- else if (s[4] === '20621000087109') result = 'Canadian English Edition';
1105
- else if (s[4] === '20611000087101') result = 'Canadian Canadian French Edition';
1106
- else if (s[4] === '11000279109') result = 'Czech Edition';
1107
- else if (s[4] === '554471000005108') result = 'Danish Edition';
1108
- else if (s[4] === '11000181102') result = 'Estonian Edition';
1109
- else if (s[4] === '11000229106') result = 'Finnish Edition';
1110
- else if (s[4] === '11000274103') result = 'German Edition';
1111
- else if (s[4] === '1121000189102') result = 'Indian Edition';
1112
- else if (s[4] === '827022005') result = 'IPS Terminology';
1113
- else if (s[4] === '11000220105') result = 'Irish Edition';
1114
- else if (s[4] === '11000146104') result = 'Netherlands Edition';
1115
- else if (s[4] === '21000210109') result = 'New Zealand Edition';
1116
- else if (s[4] === '51000202101') result = 'Norwegian Edition';
1117
- else if (s[4] === '11000267109') result = 'Republic of Korea Edition (South Korea)';
1118
- else if (s[4] === '900000001000122104') result = 'Spanish National Edition';
1119
- else if (s[4] === '45991000052106') result = 'Swedish Edition';
1120
- else if (s[4] === '2011000195101') result = 'Swiss Edition';
1121
- else if (s[4] === '83821000000107') result = 'UK Edition';
1122
- else if (s[4] === '999000021000000109') result = 'UK Clinical Edition';
1123
- else if (s[4] === '5631000179106') result = 'Uruguay Edition';
1124
- else if (s[4] === '21000325107') result = 'Chilean Edition';
1125
- else if (s[4] === '731000124108') result = 'US Edition';
1126
- else if (s[4] === '5991000124107') result = 'US Edition (with ICD-10-CM maps)';
1127
- }
1128
- return result;
1129
1104
  }
1105
+ const s = ver.split('/');
1106
+ return (s.length >= 5 && editionName(s[4])) || 'unknown';
1130
1107
  }
1131
1108
 
1132
1109
  module.exports = {
@@ -263,16 +263,23 @@ function conceptMapR5ToR3(r5Obj) {
263
263
  * @returns {string} R5 relationship value
264
264
  * @private
265
265
  */
266
+ // The two vocabularies name the relationship from opposite ends. R3/R4 equivalence
267
+ // describes the TARGET ('wider' = the target is wider than the source); R5 relationship
268
+ // describes the SOURCE ('source-is-narrower-than-target'). So wider <-> narrower-than-
269
+ // target, and narrower <-> broader-than-target. These tables follow the Java convertors
270
+ // (ConceptMap30_50 / ConceptMap40_50), which are the reference.
266
271
  function convertEquivalenceToRelationship(equivalence) {
267
272
  const equivalenceToRelationship = {
268
273
  'relatedto': 'related-to',
269
274
  'equivalent': 'equivalent',
270
275
  'equal': 'equivalent',
271
- 'wider': 'source-is-broader-than-target',
272
- 'subsumes': 'source-is-broader-than-target',
273
- 'narrower': 'source-is-narrower-than-target',
274
- 'specializes': 'source-is-narrower-than-target',
275
- 'inexact': 'not-related-to',
276
+ 'wider': 'source-is-narrower-than-target',
277
+ 'subsumes': 'source-is-narrower-than-target',
278
+ 'narrower': 'source-is-broader-than-target',
279
+ 'specializes': 'source-is-broader-than-target',
280
+ 'inexact': 'related-to',
281
+ // Java gives no relationship for 'unmatched' (a target-less 'unmatched' is lifted to
282
+ // element.noMap before this is reached); R5 requires one, so not-related-to it is
276
283
  'unmatched': 'not-related-to',
277
284
  'disjoint': 'not-related-to'
278
285
  };
@@ -289,9 +296,11 @@ function convertRelationshipToEquivalence(relationship) {
289
296
  const relationshipToEquivalence = {
290
297
  'related-to': 'relatedto',
291
298
  'equivalent': 'equivalent',
292
- 'source-is-broader-than-target': 'wider',
293
- 'source-is-narrower-than-target': 'narrower',
294
- 'not-related-to': 'unmatched'
299
+ 'source-is-narrower-than-target': 'wider',
300
+ 'source-is-broader-than-target': 'narrower',
301
+ // 'unmatched' is reserved for "no map" (see conceptMapToR5); a real not-related-to
302
+ // target is disjoint, as in the Java convertors
303
+ 'not-related-to': 'disjoint'
295
304
  };
296
305
  return relationshipToEquivalence[relationship] || 'relatedto';
297
306
  }
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ //
3
+ // Copyright 2026, Health Intersections Pty Ltd (http://www.healthintersections.com.au)
4
+ //
5
+ // Licensed under BSD-3: https://opensource.org/license/bsd-3-clause
6
+ //
7
+
8
+ /**
9
+ * Submit the tx test report (test-cases-report.json, written by the tx test run) to a
10
+ * FHIRsmith /testing server.
11
+ *
12
+ * node utilities/submit-test-report.js [-server url] [-file path] [-token token] [-header name]
13
+ *
14
+ * -server the /testing base; default https://testing.fhir.org/testing
15
+ * -file default test-cases-report.json in the FHIRsmith root
16
+ * -token if the server requires one; or set FHIRSMITH_TESTING_TOKEN
17
+ * -header the header the token goes in; default Authorization (sent as "Bearer <token>")
18
+ *
19
+ * Exits 0 when the server accepted the report, 1 otherwise.
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+
25
+ const DEFAULT_SERVER = 'https://npm.fhir.org/testing';
26
+ const DEFAULT_FILE = path.join(__dirname, '..', 'test-cases-report.json');
27
+
28
+ function usage(msg) {
29
+ if (msg) {
30
+ console.error(msg);
31
+ }
32
+ console.error('Usage: node utilities/submit-test-report.js [-server url] [-file path] [-token token] [-header name]');
33
+ process.exit(1);
34
+ }
35
+
36
+ function parseArgs(argv) {
37
+ const opts = {
38
+ server: DEFAULT_SERVER,
39
+ file: DEFAULT_FILE,
40
+ token: process.env.FHIRSMITH_TESTING_TOKEN || null,
41
+ header: 'Authorization'
42
+ };
43
+ for (let i = 0; i < argv.length; i++) {
44
+ const a = argv[i].replace(/^--?/, '');
45
+ if (a === 'help' || a === 'h') {
46
+ usage();
47
+ }
48
+ if (!['server', 'file', 'token', 'header'].includes(a)) {
49
+ usage(`Unknown parameter: ${argv[i]}`);
50
+ }
51
+ if (i + 1 >= argv.length) {
52
+ usage(`Missing value for ${argv[i]}`);
53
+ }
54
+ opts[a] = argv[++i];
55
+ }
56
+ return opts;
57
+ }
58
+
59
+ /** The TestReport endpoint for a server base, whether or not it was given with /TestReport. */
60
+ function endpoint(server) {
61
+ const base = server.replace(/\/+$/, '');
62
+ return /\/TestReport$/.test(base) ? base : base + '/TestReport';
63
+ }
64
+
65
+ /** Warn if the report doesn't describe the version being released. */
66
+ function checkVersion(report) {
67
+ const pkg = require('../package.json');
68
+ const fhirsmith = (report.participant || []).find(p => p && p.uri === 'https://github.com/HealthIntersections/FHIRsmith');
69
+ const reported = fhirsmith && typeof fhirsmith.version === 'string' ? fhirsmith.version : null;
70
+ if (reported && reported !== pkg.version) {
71
+ console.warn(`Warning: the report is from FHIRsmith v${reported}, but package.json is v${pkg.version}`);
72
+ } else if (reported && /snapshot/i.test(reported)) {
73
+ console.warn(`Warning: the report is from a snapshot (v${reported}), not a release`);
74
+ }
75
+ }
76
+
77
+ function describeOutcome(text) {
78
+ try {
79
+ const oo = JSON.parse(text);
80
+ if (oo.resourceType === 'OperationOutcome' && Array.isArray(oo.issue)) {
81
+ return oo.issue.map(i => ` ${i.severity}: ${(i.details && i.details.text) || i.diagnostics || i.code}`).join('\n');
82
+ }
83
+ } catch {
84
+ // not JSON - fall through to the raw text
85
+ }
86
+ return ' ' + text.substring(0, 1000);
87
+ }
88
+
89
+ async function main() {
90
+ const opts = parseArgs(process.argv.slice(2));
91
+
92
+ if (!fs.existsSync(opts.file)) {
93
+ console.error(`No report at ${opts.file} - run the tx tests first (npx jest tests/tx/test-cases.test.js)`);
94
+ process.exit(1);
95
+ }
96
+ const body = fs.readFileSync(opts.file, 'utf8');
97
+ let report;
98
+ try {
99
+ report = JSON.parse(body);
100
+ } catch (e) {
101
+ console.error(`${opts.file} is not valid JSON: ${e.message}`);
102
+ process.exit(1);
103
+ }
104
+ if (report.resourceType !== 'TestReport') {
105
+ console.error(`${opts.file} is not a TestReport`);
106
+ process.exit(1);
107
+ }
108
+ checkVersion(report);
109
+
110
+ const url = endpoint(opts.server);
111
+ const headers = {
112
+ 'Content-Type': 'application/fhir+json',
113
+ 'Accept': 'application/fhir+json',
114
+ 'Prefer': 'return=minimal'
115
+ };
116
+ if (opts.token) {
117
+ headers[opts.header] = opts.header.toLowerCase() === 'authorization' ? `Bearer ${opts.token}` : opts.token;
118
+ }
119
+
120
+ console.log(`Submitting ${path.basename(opts.file)} (${report.result}, ${Array.isArray(report.test) ? report.test.length : 0} tests) to ${url}`);
121
+ let res;
122
+ try {
123
+ res = await fetch(url, { method: 'POST', headers, body });
124
+ } catch (e) {
125
+ console.error(`Could not reach ${url}: ${e.cause ? e.cause.message : e.message}`);
126
+ process.exit(1);
127
+ }
128
+ const text = await res.text();
129
+ if (res.status !== 201) {
130
+ console.error(`The server refused the report: HTTP ${res.status}`);
131
+ if (text) {
132
+ console.error(describeOutcome(text));
133
+ }
134
+ if (res.status === 401 && !opts.token) {
135
+ console.error('The server requires a token: use -token, or set FHIRSMITH_TESTING_TOKEN');
136
+ }
137
+ process.exit(1);
138
+ }
139
+ const location = res.headers.get('location');
140
+ console.log(`Accepted: ${location || '(no Location returned)'}`);
141
+ }
142
+
143
+ if (require.main === module) {
144
+ main().catch(e => {
145
+ console.error(e.stack || e.message);
146
+ process.exit(1);
147
+ });
148
+ }
149
+
150
+ module.exports = { parseArgs, endpoint };