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.
- package/CHANGELOG.md +61 -1
- package/README.md +16 -3
- package/config-template.json +81 -1
- package/library/cgroup-memory.js +113 -0
- package/library/folder-setup.js +8 -1
- package/library/languages.js +15 -0
- package/library/same-origin.js +62 -0
- package/nginx.md +399 -0
- package/package.json +3 -2
- package/publisher/git-remote.js +141 -0
- package/publisher/github-release.js +195 -0
- package/publisher/publisher-template.html +1 -0
- package/publisher/publisher.js +241 -56
- package/registry/registry.js +3 -1
- package/server.js +57 -15
- package/stats.js +19 -13
- package/testing/readme.md +144 -0
- package/testing/render.js +723 -0
- package/testing/search.js +378 -0
- package/testing/store.js +410 -0
- package/testing/testing-template.html +126 -0
- package/testing/testing.js +760 -0
- package/translations/Messages.properties +32 -0
- package/tx/closure/closure-store.js +396 -0
- package/tx/cs/cs-api.js +6 -1
- package/tx/cs/cs-rxnorm.js +35 -8
- package/tx/cs/cs-snomed.js +19 -69
- package/tx/html/codesystem-operations.liquid +177 -0
- package/tx/html/ecl-panel.liquid +93 -0
- package/tx/html/home-metrics.liquid +39 -76
- package/tx/html/tx-template.html +1 -0
- package/tx/library/conceptmap.js +22 -6
- package/tx/library/designations.js +12 -3
- package/tx/library/operation-outcome.js +3 -1
- package/tx/library/renderer.js +42 -5
- package/tx/operation-context.js +80 -17
- package/tx/provider.js +85 -0
- package/tx/sct/editions.js +70 -0
- package/tx/sct/expressions.js +128 -44
- package/tx/tx-html.js +426 -44
- package/tx/tx.js +117 -9
- package/tx/workers/cache-control.js +2 -2
- package/tx/workers/closure.js +483 -13
- package/tx/workers/expand.js +2 -2
- package/tx/workers/metadata.js +8 -3
- package/tx/workers/read.js +18 -3
- package/tx/workers/subsumes.js +18 -2
- package/tx/workers/translate.js +192 -21
- package/tx/workers/validate.js +70 -187
- package/tx/workers/worker.js +12 -35
- package/tx/xversion/xv-conceptmap.js +17 -8
- package/utilities/submit-test-report.js +150 -0
package/tx/workers/worker.js
CHANGED
|
@@ -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-
|
|
272
|
-
'subsumes': 'source-is-
|
|
273
|
-
'narrower': 'source-is-
|
|
274
|
-
'specializes': 'source-is-
|
|
275
|
-
'inexact': '
|
|
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-
|
|
293
|
-
'source-is-
|
|
294
|
-
'not-related-to
|
|
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 };
|