fhirsmith 0.18.1 → 1.0.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.
- package/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +190 -9
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +331 -33
- package/ips/jurisdictions.js +65 -0
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +131 -17
- package/ips/presenter.js +1 -1
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +170 -20
- package/ips/render.js +769 -109
- package/ips/scripts.js +46 -0
- package/ips/store.js +123 -11
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/api.js +179 -1
- package/registry/crawler.js +86 -108
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/configuration.md +34 -0
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +58 -6
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +158 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +50 -16
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +199 -24
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +193 -67
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +861 -95
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +125 -61
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/tho-precedence.js +40 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +314 -67
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +81 -17
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +175 -15
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +44 -17
- package/tx/vs/vs-vsac.js +649 -101
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- package/tx/html/home-metrics.liquid +0 -225
package/tx/workers/metadata.js
CHANGED
|
@@ -376,6 +376,8 @@ class MetadataHandler {
|
|
|
376
376
|
}
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
this.markDefaultVersions(provider, seenSystems);
|
|
380
|
+
|
|
379
381
|
// Convert map to array and sort by URI
|
|
380
382
|
const entries = Array.from(seenSystems.values());
|
|
381
383
|
entries.sort((a, b) => (a.uri || '').localeCompare(b.uri || ''));
|
|
@@ -383,6 +385,39 @@ class MetadataHandler {
|
|
|
383
385
|
return entries.length > 0 ? entries : undefined;
|
|
384
386
|
}
|
|
385
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Mark the version a request that names no version gets - the default - as isDefault. That's
|
|
390
|
+
* what getCodeSystemProvider() finds under the url alone: a native code system (factory) first,
|
|
391
|
+
* else the CodeSystem resource the provider keeps there (the most recent of the versions loaded,
|
|
392
|
+
* see Provider.addCodeSystem). A code system with only one version has that as its default,
|
|
393
|
+
* however it's held
|
|
394
|
+
*/
|
|
395
|
+
markDefaultVersions(provider, seenSystems) {
|
|
396
|
+
for (const [url, entry] of seenSystems) {
|
|
397
|
+
if (!entry.version || entry.version.length === 0) {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
let def = null;
|
|
401
|
+
const factory = provider && provider.codeSystemFactories ? provider.codeSystemFactories.get(url) : null;
|
|
402
|
+
if (factory) {
|
|
403
|
+
def = factory.version();
|
|
404
|
+
} else {
|
|
405
|
+
const cs = provider && provider.codeSystems ? provider.codeSystems.get(url) : null;
|
|
406
|
+
if (cs) {
|
|
407
|
+
def = cs.version || (cs.jsonObj && cs.jsonObj.version) || null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (!def && entry.version.length === 1) {
|
|
411
|
+
def = entry.version[0].code;
|
|
412
|
+
}
|
|
413
|
+
for (const v of entry.version) {
|
|
414
|
+
if (v.code === def) {
|
|
415
|
+
v.isDefault = true;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
386
421
|
/**
|
|
387
422
|
* Add or update a code system entry
|
|
388
423
|
* @param {Map} seenSystems - Map of URL to entry
|
package/tx/workers/search.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const { TerminologyWorker } = require('./worker');
|
|
9
9
|
const {Utilities} = require("../../library/utilities");
|
|
10
|
+
const { VersionUtilities } = require("../../library/version-utilities");
|
|
10
11
|
const {debugLog} = require("../operation-context");
|
|
11
12
|
const {acceptsHtml} = require("../tx-html");
|
|
12
13
|
|
|
@@ -116,6 +117,9 @@ class SearchWorker extends TerminologyWorker {
|
|
|
116
117
|
|
|
117
118
|
// Sort results
|
|
118
119
|
matches = this.sortResults(matches, sort);
|
|
120
|
+
if (summary !== 'count') {
|
|
121
|
+
matches = await this.loadPage(matches, offset, count);
|
|
122
|
+
}
|
|
119
123
|
|
|
120
124
|
// Build and return the bundle
|
|
121
125
|
const bundle = this.buildSearchBundle(
|
|
@@ -142,7 +146,7 @@ class SearchWorker extends TerminologyWorker {
|
|
|
142
146
|
const searchParams = {};
|
|
143
147
|
for (const [key, value] of Object.entries(params)) {
|
|
144
148
|
if (!key.startsWith('_') && value && SearchWorker.ALLOWED_PARAMS.includes(key)) {
|
|
145
|
-
searchParams[key] = key == 'url' ? value : value.toLowerCase();
|
|
149
|
+
searchParams[key] = key == 'url' || key == 'version' ? value : value.toLowerCase();
|
|
146
150
|
}
|
|
147
151
|
}
|
|
148
152
|
|
|
@@ -266,6 +270,44 @@ class SearchWorker extends TerminologyWorker {
|
|
|
266
270
|
return false;
|
|
267
271
|
}
|
|
268
272
|
|
|
273
|
+
/**
|
|
274
|
+
* A provider that doesn't hold its resources in memory can return matches that only carry
|
|
275
|
+
* what a search is sorted by, and a load() that reads the resource (see VSACValueSetProvider):
|
|
276
|
+
* read the ones on this page. One that can't be read (it was rejected when it was parsed) is
|
|
277
|
+
* dropped, and the next match moves up onto the page; the total counts the dropped ones less.
|
|
278
|
+
* A rejected match on a page not asked for is still counted: rejected rows are rare, and
|
|
279
|
+
* finding out exactly which matches are good would mean reading all of them
|
|
280
|
+
*/
|
|
281
|
+
async loadPage(matches, offset, count) {
|
|
282
|
+
let start = offset;
|
|
283
|
+
let end = Math.min(matches.length, offset + count);
|
|
284
|
+
while (start < end) {
|
|
285
|
+
this.deadCheck('search-load');
|
|
286
|
+
const page = [];
|
|
287
|
+
for (let i = start; i < end; i++) {
|
|
288
|
+
if (matches[i].load) {
|
|
289
|
+
page.push(i);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const loaded = await Promise.all(page.map(i => matches[i].load()));
|
|
293
|
+
let dropped = 0;
|
|
294
|
+
page.forEach((i, n) => {
|
|
295
|
+
matches[i] = loaded[n] || null;
|
|
296
|
+
if (!loaded[n]) {
|
|
297
|
+
dropped++;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
if (dropped === 0) {
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
// the matches after the page move up to fill it: read those
|
|
304
|
+
matches = matches.filter(m => m !== null);
|
|
305
|
+
start = end - dropped;
|
|
306
|
+
end = Math.min(matches.length, offset + count);
|
|
307
|
+
}
|
|
308
|
+
return matches;
|
|
309
|
+
}
|
|
310
|
+
|
|
269
311
|
/**
|
|
270
312
|
* Sort results by the specified field
|
|
271
313
|
*/
|
|
@@ -456,6 +498,11 @@ class SearchWorker extends TerminologyWorker {
|
|
|
456
498
|
isMatch = false;
|
|
457
499
|
break;
|
|
458
500
|
}
|
|
501
|
+
} else if (param === 'version') { // version-aware matching
|
|
502
|
+
if (!VersionUtilities.versionSearchMatches(searchValue, json.version, VersionUtilities.versionAlgorithmOf(json))) {
|
|
503
|
+
isMatch = false;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
459
506
|
} else if (param === 'url' || param === 'system') { // exact match
|
|
460
507
|
const propValue = json.url;
|
|
461
508
|
if (propValue !== searchValue) {
|
|
@@ -527,6 +574,11 @@ class SearchWorker extends TerminologyWorker {
|
|
|
527
574
|
isMatch = false;
|
|
528
575
|
break;
|
|
529
576
|
}
|
|
577
|
+
} else if (param === 'version') { // version-aware matching
|
|
578
|
+
if (!VersionUtilities.versionSearchMatches(searchValue, json.version)) {
|
|
579
|
+
isMatch = false;
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
530
582
|
} else if (param === 'url' || param === 'system') { // exact match
|
|
531
583
|
const propValue = json.url;
|
|
532
584
|
if (propValue !== searchValue) {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const regexUtilities = require('../../library/regex-utilities');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A value set filter on a property that a supplement in use defines, and the code system
|
|
5
|
+
* itself doesn't (catalog-group = renal-diabetes on LOINC, with a supplement that gives
|
|
6
|
+
* LOINC codes a catalog-group). The supplement says which codes have which values, so the
|
|
7
|
+
* filter is evaluated from the supplement, the same way whatever the code system and
|
|
8
|
+
* whichever provider serves it - the provider never sees the filter.
|
|
9
|
+
*
|
|
10
|
+
* A filter that selects codes the supplement lists (=, in, regex, exists = true) can drive
|
|
11
|
+
* an expansion on its own; one that selects codes by what the supplement doesn't say
|
|
12
|
+
* (not-in, exists = false) can only narrow what other filters select.
|
|
13
|
+
*/
|
|
14
|
+
class SupplementPropertyFilter {
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} property
|
|
17
|
+
* @param {string} op
|
|
18
|
+
* @param {string} value
|
|
19
|
+
* @param {CodeSystem[]} supplements - the supplements in use that define the property
|
|
20
|
+
*/
|
|
21
|
+
constructor(property, op, value, supplements) {
|
|
22
|
+
this.property = property;
|
|
23
|
+
this.op = op;
|
|
24
|
+
this.value = value;
|
|
25
|
+
this.values = new Map(); // code -> string[] (the property's values on that code)
|
|
26
|
+
for (const supplement of supplements) {
|
|
27
|
+
this.#scan(supplement.jsonObj.concept || []);
|
|
28
|
+
}
|
|
29
|
+
if (op === 'regex') {
|
|
30
|
+
this.regex = regexUtilities.compile('^(?:' + value + ')$'); // the whole value: a|b is ^(?:a|b)$, not ^a|b$
|
|
31
|
+
} else if (op === 'in' || op === 'not-in') {
|
|
32
|
+
this.list = value.split(',').map(s => s.trim());
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#scan(concepts) {
|
|
37
|
+
for (const concept of concepts) {
|
|
38
|
+
for (const p of concept.property || []) {
|
|
39
|
+
if (p.code === this.property) {
|
|
40
|
+
const v = SupplementPropertyFilter.valueOf(p);
|
|
41
|
+
if (v !== null) {
|
|
42
|
+
if (!this.values.has(concept.code)) {
|
|
43
|
+
this.values.set(concept.code, []);
|
|
44
|
+
}
|
|
45
|
+
this.values.get(concept.code).push(v);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
this.#scan(concept.concept || []);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** the value of a CodeSystem.concept.property, as a string to compare with a filter value */
|
|
54
|
+
static valueOf(p) {
|
|
55
|
+
for (const [k, v] of Object.entries(p)) {
|
|
56
|
+
if (k.startsWith('value') && v !== undefined && v !== null) {
|
|
57
|
+
if (typeof v === 'object') {
|
|
58
|
+
return v.code !== undefined ? String(v.code) : null; // Coding
|
|
59
|
+
}
|
|
60
|
+
return String(v);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static ops() {
|
|
67
|
+
return ['=', 'in', 'not-in', 'regex', 'exists'];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** whether the codes this filter selects are all codes the supplement lists */
|
|
71
|
+
get enumerable() {
|
|
72
|
+
return this.op !== 'not-in' && !(this.op === 'exists' && this.value === 'false');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#valueMatches(v) {
|
|
76
|
+
switch (this.op) {
|
|
77
|
+
case '=': return v === this.value;
|
|
78
|
+
case 'in': return this.list.includes(v);
|
|
79
|
+
case 'regex': return this.regex.test(v);
|
|
80
|
+
default: return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** whether the code passes this filter */
|
|
85
|
+
matches(code) {
|
|
86
|
+
const vals = this.values.get(code) || [];
|
|
87
|
+
switch (this.op) {
|
|
88
|
+
case 'exists': return (vals.length > 0) === (this.value === 'true');
|
|
89
|
+
case 'not-in': return !vals.some(v => this.list.includes(v));
|
|
90
|
+
default: return vals.some(v => this.#valueMatches(v));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** the codes this filter selects (only for an enumerable filter), in the supplement's order */
|
|
95
|
+
codes() {
|
|
96
|
+
return [...this.values.keys()].filter(code => this.matches(code));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
toString() {
|
|
100
|
+
return this.property + ' ' + this.op + ' ' + this.value;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The supplement filter for a ValueSet filter on this code system, or null when the filter is
|
|
106
|
+
* the code system's own (it isn't on a property a supplement in use defines, or the code system
|
|
107
|
+
* does the filter itself, which takes precedence)
|
|
108
|
+
*
|
|
109
|
+
* @param {CodeSystemProvider} cs
|
|
110
|
+
* @param {Object} fc - the ValueSet.compose.include.filter
|
|
111
|
+
* @returns {Promise<SupplementPropertyFilter|null>}
|
|
112
|
+
*/
|
|
113
|
+
async function supplementFilter(cs, fc) {
|
|
114
|
+
const supplements = (cs.supplements || []).filter(s => (s.jsonObj.property || []).some(p => p.code === fc.property));
|
|
115
|
+
if (supplements.length === 0 || !SupplementPropertyFilter.ops().includes(fc.op) || !fc.value) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (await cs.doesFilter(fc.property, fc.op, fc.value)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return new SupplementPropertyFilter(fc.property, fc.op, fc.value, supplements);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Split a ValueSet include's filters into the code system's own, and those on properties
|
|
126
|
+
* that supplements define
|
|
127
|
+
*
|
|
128
|
+
* @param {CodeSystemProvider} cs
|
|
129
|
+
* @param {Object[]} filters
|
|
130
|
+
* @returns {Promise<{base: Object[], supplement: SupplementPropertyFilter[]}>}
|
|
131
|
+
*/
|
|
132
|
+
async function splitFilters(cs, filters) {
|
|
133
|
+
const result = { base: [], supplement: [] };
|
|
134
|
+
for (const fc of filters || []) {
|
|
135
|
+
const sf = await supplementFilter(cs, fc);
|
|
136
|
+
if (sf) {
|
|
137
|
+
result.supplement.push(sf);
|
|
138
|
+
} else {
|
|
139
|
+
result.base.push(fc);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { SupplementPropertyFilter, supplementFilter, splitFilters };
|
package/tx/workers/translate.js
CHANGED
|
@@ -30,6 +30,14 @@ function elementComment(em) {
|
|
|
30
30
|
// checkTargetConcepts leaves it alone. A symbol key, so it never reaches the JSON output
|
|
31
31
|
const TRUSTED_TARGET = Symbol('trustedTarget');
|
|
32
32
|
|
|
33
|
+
/** The source, for a message: quoted, unless it is already a description (see describeSourceSystem). */
|
|
34
|
+
function quoteFrom(from) {
|
|
35
|
+
if (!from) {
|
|
36
|
+
return "'(unspecified)'";
|
|
37
|
+
}
|
|
38
|
+
return from.startsWith("'") ? from : `'${from}'`;
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
class TranslateWorker extends TerminologyWorker {
|
|
34
42
|
/**
|
|
35
43
|
* @param {OperationContext} opContext - Operation context
|
|
@@ -281,6 +289,9 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
281
289
|
}
|
|
282
290
|
}
|
|
283
291
|
|
|
292
|
+
// the no-maps message names the source's version - which, for SNOMED CT, is the edition
|
|
293
|
+
this.sourceDescription = reverse ? null : await this.describeSourceSystem(coding, txp);
|
|
294
|
+
|
|
284
295
|
// If no explicit concept map, we need to find one based on source/target
|
|
285
296
|
if (conceptMaps.length == 0) {
|
|
286
297
|
if (reverse) {
|
|
@@ -289,7 +300,24 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
289
300
|
} else {
|
|
290
301
|
await this.findConceptMapsInAdditionalResources(conceptMaps, coding.system, sourceScope, targetScope, targetSystem);
|
|
291
302
|
await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, coding.system, coding.version, sourceScope, targetScope, targetSystem, coding.code);
|
|
303
|
+
// targetScope with no targetSystem: the code systems (and versions) the value set draws
|
|
304
|
+
// on are the target systems - which is how implicit maps are found, since they belong
|
|
305
|
+
// to code systems, not value sets (a national SNOMED CT edition to the International
|
|
306
|
+
// Edition the value set is built on, say). What they produce is then held to the value
|
|
307
|
+
// set (see doTranslate)
|
|
308
|
+
if (targetScope && !targetSystem) {
|
|
309
|
+
for (const system of await this.systemsOfScope(targetScope)) {
|
|
310
|
+
// as the value set has it (system|version - what an implicit map into one SNOMED CT
|
|
311
|
+
// edition needs), and as the bare system (what a map's group.target usually is)
|
|
312
|
+
const targets = system.includes('|') ? [system, system.substring(0, system.indexOf('|'))] : [system];
|
|
313
|
+
for (const target of targets) {
|
|
314
|
+
await this.findConceptMapsInAdditionalResources(conceptMaps, coding.system, sourceScope, null, target);
|
|
315
|
+
await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, coding.system, coding.version, sourceScope, null, target, coding.code);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
292
319
|
}
|
|
320
|
+
conceptMaps = this.uniqueConceptMaps(conceptMaps);
|
|
293
321
|
}
|
|
294
322
|
|
|
295
323
|
// Whether the map was nominated or found, it only applies if the code is within its own
|
|
@@ -385,6 +413,7 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
385
413
|
|
|
386
414
|
let conceptMaps = [];
|
|
387
415
|
conceptMaps.push(conceptMap);
|
|
416
|
+
this.sourceDescription = await this.describeSourceSystem(coding, txp);
|
|
388
417
|
|
|
389
418
|
// Naming the map by id does not put an out-of-scope code into its scope, any more than
|
|
390
419
|
// naming it by url does: the map either covers this code or it does not.
|
|
@@ -1032,6 +1061,138 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
1032
1061
|
return inScope;
|
|
1033
1062
|
}
|
|
1034
1063
|
|
|
1064
|
+
/**
|
|
1065
|
+
* The source's code system and version, for messages: system|version as given, or - when
|
|
1066
|
+
* no version was given - the system, and the version the server takes it to be (the default
|
|
1067
|
+
* edition, for SNOMED CT). Which version it is decides which maps apply.
|
|
1068
|
+
* @returns {Promise<string>} quoted (see quoteFrom)
|
|
1069
|
+
*/
|
|
1070
|
+
async describeSourceSystem(coding, params) {
|
|
1071
|
+
if (!coding || !coding.system) {
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
if (coding.version) {
|
|
1075
|
+
return `'${coding.system}|${coding.version}'`;
|
|
1076
|
+
}
|
|
1077
|
+
let version = null;
|
|
1078
|
+
try {
|
|
1079
|
+
const cs = await this.findCodeSystem(coding.system, '', params, ['complete', 'fragment'], null, true);
|
|
1080
|
+
version = cs ? cs.version() : null;
|
|
1081
|
+
} catch (e) {
|
|
1082
|
+
version = null;
|
|
1083
|
+
}
|
|
1084
|
+
return version
|
|
1085
|
+
? `'${coding.system}' (no version given, so '${coding.system}|${version}')`
|
|
1086
|
+
: `'${coding.system}' (no version given)`;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* The code systems a value set draws on, as system|version when the include names a version:
|
|
1091
|
+
* the targets to look for maps to when targetScope is given without targetSystem. Value sets
|
|
1092
|
+
* it imports are looked into too (once each).
|
|
1093
|
+
* @returns {Promise<string[]>}
|
|
1094
|
+
*/
|
|
1095
|
+
async systemsOfScope(canonical, seen = new Set()) {
|
|
1096
|
+
if (seen.has(canonical)) {
|
|
1097
|
+
return [];
|
|
1098
|
+
}
|
|
1099
|
+
seen.add(canonical);
|
|
1100
|
+
const vs = await this.resolveScopeValueSet(canonical, 'targetScope parameter');
|
|
1101
|
+
const systems = new Set();
|
|
1102
|
+
const compose = vs.jsonObj?.compose;
|
|
1103
|
+
for (const inc of Array.isArray(compose?.include) ? compose.include : []) {
|
|
1104
|
+
if (inc.system) {
|
|
1105
|
+
systems.add(inc.version ? `${inc.system}|${inc.version}` : inc.system);
|
|
1106
|
+
}
|
|
1107
|
+
for (const imported of Array.isArray(inc.valueSet) ? inc.valueSet : []) {
|
|
1108
|
+
try {
|
|
1109
|
+
for (const s of await this.systemsOfScope(imported, seen)) {
|
|
1110
|
+
systems.add(s);
|
|
1111
|
+
}
|
|
1112
|
+
} catch (e) {
|
|
1113
|
+
// an import that can't be found doesn't stop the others: the membership check
|
|
1114
|
+
// (doTranslate) will report it, if it matters
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
if (!compose) {
|
|
1119
|
+
for (const c of Array.isArray(vs.jsonObj?.expansion?.contains) ? vs.jsonObj.expansion.contains : []) {
|
|
1120
|
+
if (c.system) {
|
|
1121
|
+
systems.add(c.version ? `${c.system}|${c.version}` : c.system);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return [...systems];
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/** The candidate maps, each once (the same map can be found for more than one target). */
|
|
1129
|
+
uniqueConceptMaps(conceptMaps) {
|
|
1130
|
+
const seen = new Set();
|
|
1131
|
+
const result = [];
|
|
1132
|
+
for (const cm of conceptMaps) {
|
|
1133
|
+
const key = cm.url ? `${cm.url}|${cm.version || ''}` : cm;
|
|
1134
|
+
if (!seen.has(key)) {
|
|
1135
|
+
seen.add(key);
|
|
1136
|
+
result.push(cm);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return result;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* targetScope "limits the scope of the $translate operation to target codes that are members
|
|
1144
|
+
* of this value set": a match whose target concept isn't in it is dropped, and a message says
|
|
1145
|
+
* so. A target that can't be checked against it is kept, with a note saying it wasn't checked.
|
|
1146
|
+
*
|
|
1147
|
+
* A target the code system produced itself can name a version the server doesn't hold: an
|
|
1148
|
+
* edition to edition translation names the release of the International Edition that the
|
|
1149
|
+
* national edition was built on, and the server may have another release, or none on its own.
|
|
1150
|
+
* Then it is checked as a code of the edition (the version without its date - whichever
|
|
1151
|
+
* release of it the server has, and what an include that names the edition means), with a
|
|
1152
|
+
* note saying so; and if the server doesn't have the edition either, it is kept, unchecked.
|
|
1153
|
+
*/
|
|
1154
|
+
async holdToTargetScope(result, targetScope, params) {
|
|
1155
|
+
const dropped = [];
|
|
1156
|
+
for (let i = result.length - 1; i >= 0; i--) {
|
|
1157
|
+
const match = result[i];
|
|
1158
|
+
if (match.name !== 'match' || !Array.isArray(match.part)) {
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
const concept = match.part.find(p => p.name === 'concept')?.valueCoding;
|
|
1162
|
+
if (!concept || !concept.system || !concept.code) {
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
let coding = concept;
|
|
1166
|
+
if (concept.version && !(await this.resolveCode(concept.system, concept.version, concept.code, params)).known) {
|
|
1167
|
+
const edition = concept.version.includes('/version/') ? concept.version.substring(0, concept.version.indexOf('/version/')) : null;
|
|
1168
|
+
if (edition && (await this.resolveCode(concept.system, edition, concept.code, params)).known) {
|
|
1169
|
+
coding = { system: concept.system, version: edition, code: concept.code };
|
|
1170
|
+
match.part.push({ name: 'note', valueString: `Checked against the target scope '${targetScope}' as a code of '${edition}': '${concept.version}' is not available on this server` });
|
|
1171
|
+
} else {
|
|
1172
|
+
match.part.push({ name: 'note', valueString: `'${concept.system}#${concept.code}' could not be checked against the target scope '${targetScope}': '${concept.version}' is not available on this server` });
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
let inScope;
|
|
1177
|
+
try {
|
|
1178
|
+
inScope = await this.codeInScope(targetScope, coding, params, 'targetScope parameter');
|
|
1179
|
+
} catch (e) {
|
|
1180
|
+
match.part.push({ name: 'note', valueString: `'${concept.system}#${concept.code}' could not be checked against the target scope '${targetScope}' (${e.message})` });
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
if (!inScope) {
|
|
1184
|
+
const key = `${concept.system}#${concept.code}`;
|
|
1185
|
+
if (!dropped.includes(key)) {
|
|
1186
|
+
dropped.unshift(key);
|
|
1187
|
+
}
|
|
1188
|
+
result.splice(i, 1);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (dropped.length > 0) {
|
|
1192
|
+
result.push({ name: 'message', valueString: `${dropped.length === 1 ? 'A translation was' : `${dropped.length} translations were`} left out because ${dropped.length === 1 ? 'it is' : 'they are'} not in the target scope '${targetScope}': ${dropped.join(', ')}` });
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1035
1196
|
/**
|
|
1036
1197
|
* The answer when nothing can translate this code: a successful operation with a negative
|
|
1037
1198
|
* result, not an error. Where any candidate map declared a scope, the message names the code,
|
|
@@ -1041,11 +1202,11 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
1041
1202
|
noMapsResponse(coding, sourceScope, targetScope, targetSystem, reverse, message, anyScoped) {
|
|
1042
1203
|
let text = message;
|
|
1043
1204
|
if (!text) {
|
|
1044
|
-
const from = reverse ? (targetSystem || sourceScope) : coding.system;
|
|
1205
|
+
const from = reverse ? (targetSystem || sourceScope) : (this.sourceDescription || coding.system);
|
|
1045
1206
|
const to = reverse ? coding.system : (targetSystem || targetScope);
|
|
1046
1207
|
text = anyScoped
|
|
1047
|
-
? `No ConceptMap that translates from
|
|
1048
|
-
: `No ConceptMap is available to translate from
|
|
1208
|
+
? `No ConceptMap that translates from ${quoteFrom(from)} to '${to || '(unspecified)'}' has '${coding.system}#${coding.code}' in scope`
|
|
1209
|
+
: `No ConceptMap is available to translate from ${quoteFrom(from)} to '${to || '(unspecified)'}'`;
|
|
1049
1210
|
}
|
|
1050
1211
|
return {
|
|
1051
1212
|
resourceType: 'Parameters',
|
|
@@ -1128,6 +1289,9 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
1128
1289
|
// says - but it is not a translation: "The value can only be true if at least one
|
|
1129
1290
|
// returned match has a relationship other than 'not-related-to'" (tx-ecosystem translate-2b)
|
|
1130
1291
|
await this.checkTargetConcepts(result, params);
|
|
1292
|
+
if (targetScope && !reverse) {
|
|
1293
|
+
await this.holdToTargetScope(result, targetScope, params);
|
|
1294
|
+
}
|
|
1131
1295
|
const translated = added && result.some(p => p.name === 'match' && this.isPositiveMatch(p));
|
|
1132
1296
|
result.push({
|
|
1133
1297
|
name: 'result',
|
|
@@ -1177,7 +1341,8 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
1177
1341
|
if (!system || !code) {
|
|
1178
1342
|
return { known: false };
|
|
1179
1343
|
}
|
|
1180
|
-
|
|
1344
|
+
// called per element: a turn for the event loop now and then, so a client that has gone is seen
|
|
1345
|
+
await this.checkAndYield('translate-resolve');
|
|
1181
1346
|
let cs;
|
|
1182
1347
|
try {
|
|
1183
1348
|
cs = await this.findCodeSystem(system, version || '', params, ['complete', 'fragment'], null, true);
|