fhirsmith 0.13.3 → 0.14.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 (49) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +48 -4
  3. package/config-template.json +70 -1
  4. package/library/cgroup-memory.js +113 -0
  5. package/library/folder-setup.js +8 -1
  6. package/nginx.md +391 -0
  7. package/package.json +3 -2
  8. package/publisher/git-remote.js +141 -0
  9. package/publisher/github-release.js +195 -0
  10. package/publisher/publisher.js +146 -14
  11. package/server.js +51 -14
  12. package/stats.js +19 -13
  13. package/testing/readme.md +112 -0
  14. package/testing/render.js +482 -0
  15. package/testing/search.js +378 -0
  16. package/testing/store.js +266 -0
  17. package/testing/testing-template.html +126 -0
  18. package/testing/testing.js +486 -0
  19. package/translations/Messages.properties +32 -0
  20. package/tx/closure/closure-store.js +396 -0
  21. package/tx/cs/cs-api.js +6 -1
  22. package/tx/cs/cs-rxnorm.js +35 -8
  23. package/tx/cs/cs-snomed.js +142 -27
  24. package/tx/html/codesystem-operations.liquid +177 -0
  25. package/tx/html/ecl-panel.liquid +93 -0
  26. package/tx/html/home-metrics.liquid +39 -76
  27. package/tx/html/tx-template.html +1 -0
  28. package/tx/library/conceptmap.js +22 -6
  29. package/tx/library/designations.js +12 -3
  30. package/tx/library/extensions.js +2 -2
  31. package/tx/library/operation-outcome.js +3 -1
  32. package/tx/library/renderer.js +42 -5
  33. package/tx/operation-context.js +111 -15
  34. package/tx/provider.js +85 -0
  35. package/tx/sct/expressions.js +128 -44
  36. package/tx/sct/text-index.js +419 -0
  37. package/tx/tx-html.js +426 -44
  38. package/tx/tx.js +117 -9
  39. package/tx/workers/cache-control.js +2 -2
  40. package/tx/workers/closure.js +483 -13
  41. package/tx/workers/expand.js +20 -0
  42. package/tx/workers/metadata.js +29 -4
  43. package/tx/workers/read.js +18 -3
  44. package/tx/workers/subsumes.js +18 -2
  45. package/tx/workers/translate.js +167 -20
  46. package/tx/workers/validate.js +61 -182
  47. package/tx/workers/worker.js +9 -0
  48. package/tx/xversion/xv-conceptmap.js +17 -8
  49. package/utilities/submit-test-report.js +150 -0
@@ -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://testing.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 };