fhirsmith 0.18.2 → 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 +149 -0
- 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 +132 -59
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +74 -21
- package/ips/presenter.js +1 -1
- package/ips/readme.md +132 -25
- package/ips/render.js +466 -77
- package/ips/store.js +79 -22
- 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/crawler.js +84 -108
- package/registry/documentation/configuration.md +34 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +5 -4
- 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 +154 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +29 -1
- 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 +148 -14
- 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 +185 -66
- 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 +608 -64
- 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 +57 -58
- 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/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 +278 -42
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/ecl.js +47 -6
- 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 +166 -13
- 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 +27 -4
- package/tx/vs/vs-vsac.js +648 -100
- 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/cs/cs-snomed.js
CHANGED
|
@@ -16,9 +16,11 @@ const {ConceptMap} = require("../library/conceptmap");
|
|
|
16
16
|
const {ECLLexer, ECLParser, ECLNodeType, ECLTokenType} = require("../sct/ecl");
|
|
17
17
|
const {Issue} = require("../library/operation-outcome");
|
|
18
18
|
const {debugLog} = require("../operation-context");
|
|
19
|
-
const {editionName, editionCode} = require("../sct/editions");
|
|
19
|
+
const {editionName, editionCode, SNOMED_LINKABLE_EDITIONS} = require("../sct/editions");
|
|
20
20
|
const {SnomedTextIndex, rankMatches, filterTokens} = require("../sct/text-index");
|
|
21
21
|
const Logger = require("../../library/logger");
|
|
22
|
+
const {Language} = require("../../library/languages");
|
|
23
|
+
const regexUtilities = require("../../library/regex-utilities");
|
|
22
24
|
|
|
23
25
|
// Symbol key under which a resolved ECL filter analysis is memoised directly on
|
|
24
26
|
// the ValueSet compose.include.filter element (`fc`). Symbol-keyed and defined
|
|
@@ -30,6 +32,7 @@ const Logger = require("../../library/logger");
|
|
|
30
32
|
// _cacheEcl): a wildcard's materialised form depends on forIteration and is not
|
|
31
33
|
// shared - it simply recomputes.
|
|
32
34
|
const SNOMED_FILTER_ANALYSIS = Symbol('snomedFilterAnalysis');
|
|
35
|
+
const SNOMED_DISPLAY_REGEX = Symbol('snomedDisplayRegex');
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
38
|
* Alternate identifier schemes (sct2_Identifier) that FHIRsmith knows how to present:
|
|
@@ -58,6 +61,8 @@ const SCT_LANGUAGE_CODES = {
|
|
|
58
61
|
9: 'cs'
|
|
59
62
|
};
|
|
60
63
|
|
|
64
|
+
const SCT_LANGUAGE_BYTES = Object.fromEntries(Object.entries(SCT_LANGUAGE_CODES).map(([b, c]) => [c, Number(b)]));
|
|
65
|
+
|
|
61
66
|
// ECL dialect aliases (ECL specification, appendix "Dialect aliases") to the
|
|
62
67
|
// language reference set they denote. Used by {{ D dialect = en-gb }}.
|
|
63
68
|
const ECL_DIALECT_ALIASES = {
|
|
@@ -77,6 +82,7 @@ const ECL_DIALECT_ALIASES = {
|
|
|
77
82
|
};
|
|
78
83
|
|
|
79
84
|
// Context kinds matching Pascal enum
|
|
85
|
+
|
|
80
86
|
const SnomedProviderContextKind = {
|
|
81
87
|
CODE: 0,
|
|
82
88
|
EXPRESSION: 1
|
|
@@ -146,14 +152,54 @@ class SnomedFilterContext {
|
|
|
146
152
|
}
|
|
147
153
|
|
|
148
154
|
class SnomedPrep {
|
|
149
|
-
constructor() {
|
|
155
|
+
constructor(languages = null) {
|
|
150
156
|
this.filters = [];
|
|
157
|
+
this.languages = languages; // the languages the request named, or null
|
|
151
158
|
}
|
|
152
159
|
}
|
|
153
160
|
|
|
154
161
|
/**
|
|
155
162
|
* Core SNOMED services providing access to structures and expression processing
|
|
156
163
|
*/
|
|
164
|
+
/**
|
|
165
|
+
* A matcher for an ECL wild: term: case-insensitive, anchored at both ends, '*' matching any
|
|
166
|
+
* run of characters (a run of '*'s the same as one) and every other character literal.
|
|
167
|
+
*
|
|
168
|
+
* No regex: a native RegExp built from '*' -> '.*' backtracks for a time that grows as the
|
|
169
|
+
* term length to the power of the number of '*'s (one $expand burned 90 s of CPU), and re2js,
|
|
170
|
+
* though linear, is 10-70 times slower than this over a whole edition's descriptions. The
|
|
171
|
+
* pieces between the '*'s are literal, so the term must start with the first and end with
|
|
172
|
+
* the last, and each one in between is found with indexOf after the one before - taking the
|
|
173
|
+
* leftmost match of each is always right for such a pattern. Linear, with no backtracking.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} value - the wild: term
|
|
176
|
+
* @returns {function(string): boolean}
|
|
177
|
+
*/
|
|
178
|
+
function wildTermMatcher(value) {
|
|
179
|
+
const parts = value.toLowerCase().split(/\*+/);
|
|
180
|
+
const first = parts[0];
|
|
181
|
+
const last = parts[parts.length - 1];
|
|
182
|
+
if (parts.length === 1) {
|
|
183
|
+
return (term) => term.toLowerCase() === first;
|
|
184
|
+
}
|
|
185
|
+
const middle = parts.slice(1, -1).filter(p => p.length > 0);
|
|
186
|
+
return (term) => {
|
|
187
|
+
const t = term.toLowerCase();
|
|
188
|
+
if (t.length < first.length + last.length || !t.startsWith(first)) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
let pos = first.length;
|
|
192
|
+
for (const part of middle) {
|
|
193
|
+
const i = t.indexOf(part, pos);
|
|
194
|
+
if (i < 0) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
pos = i + part.length;
|
|
198
|
+
}
|
|
199
|
+
return t.length - pos >= last.length && t.endsWith(last);
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
157
203
|
class SnomedServices {
|
|
158
204
|
constructor(sharedData) {
|
|
159
205
|
// Core data structures
|
|
@@ -659,6 +705,9 @@ class SnomedServices {
|
|
|
659
705
|
}
|
|
660
706
|
}
|
|
661
707
|
|
|
708
|
+
// The concept's active outbound relationships. Inactive rows are imported (they are in the
|
|
709
|
+
// cache) but ignored altogether, as agreed with the SNOMED leads: a retired row no longer
|
|
710
|
+
// describes the concept, so no filter, lookup or refinement may see it.
|
|
662
711
|
getConceptRelationships(reference) {
|
|
663
712
|
try {
|
|
664
713
|
const concept = this.concepts.getConcept(reference);
|
|
@@ -666,7 +715,7 @@ class SnomedServices {
|
|
|
666
715
|
|
|
667
716
|
if (relRef === 0) return [];
|
|
668
717
|
|
|
669
|
-
return this.refs.getReferences(relRef) || [];
|
|
718
|
+
return (this.refs.getReferences(relRef) || []).filter(relIdx => this.relationships.isActive(relIdx));
|
|
670
719
|
} catch (error) {
|
|
671
720
|
return [];
|
|
672
721
|
}
|
|
@@ -839,6 +888,35 @@ class SnomedServices {
|
|
|
839
888
|
return result;
|
|
840
889
|
}
|
|
841
890
|
|
|
891
|
+
// descendent-leaf (R5): the concept's descendants that have no children of their own
|
|
892
|
+
filterDescendentLeaf(id) {
|
|
893
|
+
const result = new SnomedFilterContext();
|
|
894
|
+
const conceptResult = this.concepts.findConcept(id);
|
|
895
|
+
|
|
896
|
+
if (!conceptResult.found) {
|
|
897
|
+
throw new Error(`The SNOMED CT Concept ${id} is not known`);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
result.descendants = this.getConceptDescendants(conceptResult.index)
|
|
901
|
+
.filter(ref => this.getConceptChildren(ref).length === 0);
|
|
902
|
+
return result;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// is-not-a (R5): every concept except the concept itself and its descendants - the
|
|
906
|
+
// same population as the ECL * MINUS << id
|
|
907
|
+
filterIsNotA(id, opContext = null) {
|
|
908
|
+
const result = new SnomedFilterContext();
|
|
909
|
+
const conceptResult = this.concepts.findConcept(id);
|
|
910
|
+
|
|
911
|
+
if (!conceptResult.found) {
|
|
912
|
+
throw new Error(`The SNOMED CT Concept ${id} is not known`);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const excluded = new Set([conceptResult.index, ...this.getConceptDescendants(conceptResult.index)]);
|
|
916
|
+
result.descendants = this._eclEnumerateAllConcepts(opContext).filter(ref => !excluded.has(ref));
|
|
917
|
+
return result;
|
|
918
|
+
}
|
|
919
|
+
|
|
842
920
|
filterChildOf(id = true) {
|
|
843
921
|
const result = new SnomedFilterContext();
|
|
844
922
|
const conceptResult = this.concepts.findConcept(id);
|
|
@@ -886,10 +964,25 @@ class SnomedServices {
|
|
|
886
964
|
}
|
|
887
965
|
|
|
888
966
|
|
|
967
|
+
// concept in A,B,C: any of the listed concepts; a listed reference set stands for its
|
|
968
|
+
// members. Members are kept as reference set member records ({ref}), which is what every
|
|
969
|
+
// consumer of set.members reads, so a plain concept is wrapped the same way; and each
|
|
970
|
+
// concept appears once, however many times it is named or reached.
|
|
889
971
|
filterIn(idList) {
|
|
890
972
|
const result = new SnomedFilterContext();
|
|
891
|
-
|
|
973
|
+
const members = [];
|
|
974
|
+
const seen = new Set();
|
|
975
|
+
const add = (member) => {
|
|
976
|
+
if (!seen.has(member.ref)) {
|
|
977
|
+
seen.add(member.ref);
|
|
978
|
+
members.push(member);
|
|
979
|
+
}
|
|
980
|
+
};
|
|
892
981
|
for (let id of idList.split(',')) {
|
|
982
|
+
id = id.trim();
|
|
983
|
+
if (!id) {
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
893
986
|
const conceptResult = this.concepts.findConcept(id);
|
|
894
987
|
|
|
895
988
|
if (!conceptResult.found) {
|
|
@@ -898,9 +991,11 @@ class SnomedServices {
|
|
|
898
991
|
|
|
899
992
|
const refSetIndex = this.getConceptRefSet(conceptResult.index, false);
|
|
900
993
|
if (refSetIndex === 0) {
|
|
901
|
-
|
|
994
|
+
add({ ref: conceptResult.index });
|
|
902
995
|
} else {
|
|
903
|
-
|
|
996
|
+
for (const member of this.refSetMembers.getMembers(refSetIndex) || []) {
|
|
997
|
+
add(member);
|
|
998
|
+
}
|
|
904
999
|
}
|
|
905
1000
|
}
|
|
906
1001
|
result.members = members;
|
|
@@ -1700,8 +1795,7 @@ class SnomedServices {
|
|
|
1700
1795
|
*/
|
|
1701
1796
|
_eclTermMatcher = function (v) {
|
|
1702
1797
|
if (v.kind === 'wild') {
|
|
1703
|
-
|
|
1704
|
-
return (term) => re.test(term);
|
|
1798
|
+
return wildTermMatcher(v.value);
|
|
1705
1799
|
}
|
|
1706
1800
|
const words = v.value.toLowerCase().split(/\s+/).filter(w => w.length > 0);
|
|
1707
1801
|
return (term) => {
|
|
@@ -2439,16 +2533,20 @@ class SnomedServices {
|
|
|
2439
2533
|
* matches for a typical multi-word filter, with the event loop blocked for
|
|
2440
2534
|
* the whole scan (tx.fhir.org incident 2026-09-17).
|
|
2441
2535
|
*
|
|
2536
|
+
* Only descriptions in `lang` take part, when it is given (a text filter searches one
|
|
2537
|
+
* language: see SnomedProvider.searchFilter).
|
|
2538
|
+
*
|
|
2442
2539
|
* @param {SearchFilterText} searchText
|
|
2443
2540
|
* @param {boolean} includeInactive - include inactive concepts
|
|
2444
2541
|
* @param {OperationContext|null} opContext - for yielding / deadline checks
|
|
2542
|
+
* @param {number|null} lang - the description language byte, or null for all
|
|
2445
2543
|
* @returns {Promise<SnomedFilterContext>}
|
|
2446
2544
|
*/
|
|
2447
|
-
async searchFilter(searchText, includeInactive = false, opContext = null) {
|
|
2545
|
+
async searchFilter(searchText, includeInactive = false, opContext = null, lang = null) {
|
|
2448
2546
|
const filterText = (searchText && searchText.filter ? searchText.filter : '').toLowerCase().trim();
|
|
2449
2547
|
const tokens = filterTokens(filterText);
|
|
2450
2548
|
if (tokens.length > 0) {
|
|
2451
|
-
const index = await this.getTextIndex(opContext);
|
|
2549
|
+
const index = await this.getTextIndex(opContext, lang);
|
|
2452
2550
|
if (index) {
|
|
2453
2551
|
const result = new SnomedFilterContext();
|
|
2454
2552
|
const ordinals = index.search(tokens, opContext);
|
|
@@ -2459,7 +2557,16 @@ class SnomedServices {
|
|
|
2459
2557
|
return result;
|
|
2460
2558
|
}
|
|
2461
2559
|
}
|
|
2462
|
-
return await this.scanSearch(filterText, includeInactive, opContext);
|
|
2560
|
+
return await this.scanSearch(filterText, includeInactive, opContext, lang);
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
// whether lang is the only language the edition has active descriptions in (then there is
|
|
2564
|
+
// nothing to restrict - the International Edition is all English)
|
|
2565
|
+
#onlyLanguage(lang) {
|
|
2566
|
+
if (!this._activeDescLanguages) {
|
|
2567
|
+
this._activeDescLanguages = this.descriptions.activeLanguages();
|
|
2568
|
+
}
|
|
2569
|
+
return this._activeDescLanguages.size === 1 && this._activeDescLanguages.has(lang);
|
|
2463
2570
|
}
|
|
2464
2571
|
|
|
2465
2572
|
/**
|
|
@@ -2470,28 +2577,53 @@ class SnomedServices {
|
|
|
2470
2577
|
* @param {OperationContext|null} opContext
|
|
2471
2578
|
* @returns {Promise<SnomedTextIndex|null>}
|
|
2472
2579
|
*/
|
|
2473
|
-
async getTextIndex(opContext = null) {
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2580
|
+
async getTextIndex(opContext = null, lang = null) {
|
|
2581
|
+
// One index per language the edition is searched in; an edition with only one language
|
|
2582
|
+
// (the International Edition) has just the one, for all its descriptions
|
|
2583
|
+
if (lang === null || this.#onlyLanguage(lang)) {
|
|
2584
|
+
if (this._textIndex) {
|
|
2585
|
+
return this._textIndex;
|
|
2586
|
+
}
|
|
2587
|
+
if (!this._textIndexPromise) {
|
|
2588
|
+
this._textIndexPromise = this.#buildTextIndex(null, index => { this._textIndex = index; }, () => { this._textIndexPromise = null; });
|
|
2589
|
+
}
|
|
2590
|
+
return await this.#awaitTextIndex(this._textIndexPromise, opContext);
|
|
2591
|
+
}
|
|
2592
|
+
if (!this._textIndexes) {
|
|
2593
|
+
this._textIndexes = new Map();
|
|
2594
|
+
this._textIndexPromises = new Map();
|
|
2595
|
+
}
|
|
2596
|
+
if (this._textIndexes.has(lang)) {
|
|
2597
|
+
return this._textIndexes.get(lang);
|
|
2598
|
+
}
|
|
2599
|
+
if (!this._textIndexPromises.has(lang)) {
|
|
2600
|
+
this._textIndexPromises.set(lang, this.#buildTextIndex(lang, index => { this._textIndexes.set(lang, index); },
|
|
2601
|
+
() => { this._textIndexPromises.delete(lang); }));
|
|
2602
|
+
}
|
|
2603
|
+
return await this.#awaitTextIndex(this._textIndexPromises.get(lang), opContext);
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
#buildTextIndex(lang, onBuilt, onFailed) {
|
|
2607
|
+
const started = Date.now();
|
|
2608
|
+
// Logger fetched lazily: importers and tests require this module too
|
|
2609
|
+
const textIndexLog = Logger.getInstance().child({module: 'tx-sct'});
|
|
2610
|
+
const which = lang === null ? '' : ` (language ${lang})`;
|
|
2611
|
+
return SnomedTextIndex.build(this, lang).then(index => {
|
|
2612
|
+
onBuilt(index);
|
|
2613
|
+
textIndexLog.info(`SNOMED text index for ${this.versionUri}${which}: ${index.words.length} words, built in ${Date.now() - started}ms`);
|
|
2614
|
+
return index;
|
|
2615
|
+
}, error => {
|
|
2616
|
+
onFailed();
|
|
2617
|
+
textIndexLog.error(`SNOMED text index build failed for ${this.versionUri}${which}: ${error.message}`);
|
|
2618
|
+
return null;
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
async #awaitTextIndex(promise, opContext) {
|
|
2491
2623
|
if (opContext && typeof opContext.waitFor === 'function') {
|
|
2492
|
-
return await opContext.waitFor(
|
|
2624
|
+
return await opContext.waitFor(promise, 'sct:textIndex');
|
|
2493
2625
|
}
|
|
2494
|
-
return await
|
|
2626
|
+
return await promise;
|
|
2495
2627
|
}
|
|
2496
2628
|
|
|
2497
2629
|
/**
|
|
@@ -2505,7 +2637,7 @@ class SnomedServices {
|
|
|
2505
2637
|
* @param {OperationContext|null} opContext
|
|
2506
2638
|
* @returns {Promise<SnomedFilterContext>}
|
|
2507
2639
|
*/
|
|
2508
|
-
async scanSearch(filterText, includeInactive = false, opContext = null) {
|
|
2640
|
+
async scanSearch(filterText, includeInactive = false, opContext = null, lang = null) {
|
|
2509
2641
|
const result = new SnomedFilterContext();
|
|
2510
2642
|
const searchTerms = filterText.split(/\s+/).filter(t => t.length > 0);
|
|
2511
2643
|
const matches = [];
|
|
@@ -2537,7 +2669,7 @@ class SnomedServices {
|
|
|
2537
2669
|
|
|
2538
2670
|
for (const descIndex of descriptionIndices) {
|
|
2539
2671
|
const description = this.descriptions.getDescription(descIndex);
|
|
2540
|
-
if (description.active) {
|
|
2672
|
+
if (description.active && (lang === null || description.lang === lang)) {
|
|
2541
2673
|
const term = this.strings.getEntry(description.iDesc).toLowerCase();
|
|
2542
2674
|
if (searchTerms.every(searchTerm => term.includes(searchTerm))) {
|
|
2543
2675
|
matchFound = true;
|
|
@@ -2691,6 +2823,10 @@ class SnomedProvider extends BaseCSServices {
|
|
|
2691
2823
|
if (disp) return disp;
|
|
2692
2824
|
|
|
2693
2825
|
if (ctxt.isComplex()) {
|
|
2826
|
+
// an extension-codes supplement in scope can give a post-coordinated expression a display;
|
|
2827
|
+
// otherwise it is generated from the expression
|
|
2828
|
+
const ext = this.#extensionCodeDisplay(ctxt);
|
|
2829
|
+
if (ext) return ext;
|
|
2694
2830
|
return this.sct.expressionServices.renderExpression(ctxt.expression, SnomedServicesRenderOption.FillMissing);
|
|
2695
2831
|
} else {
|
|
2696
2832
|
return this.sct.getDisplayName(ctxt.getReference(), this.sct.defaultLanguage);
|
|
@@ -2739,8 +2875,11 @@ class SnomedProvider extends BaseCSServices {
|
|
|
2739
2875
|
|
|
2740
2876
|
|
|
2741
2877
|
if (ctxt.isComplex()) {
|
|
2742
|
-
// For complex expressions
|
|
2743
|
-
|
|
2878
|
+
// For complex expressions: the display an extension-codes supplement in scope gives it (first,
|
|
2879
|
+
// so it is the preferred display), and the display generated from the expression, which stays
|
|
2880
|
+
// a valid display
|
|
2881
|
+
this._listSupplementDesignations(this.#extensionCodeKey(ctxt), displays, true);
|
|
2882
|
+
const display = this.sct.expressionServices.renderExpression(ctxt.expression, SnomedServicesRenderOption.FillMissing);
|
|
2744
2883
|
if (display) {
|
|
2745
2884
|
displays.addDesignation(true, 'active', 'en-US', DesignationUse.PREFERRED, display);
|
|
2746
2885
|
}
|
|
@@ -2857,6 +2996,30 @@ class SnomedProvider extends BaseCSServices {
|
|
|
2857
2996
|
}
|
|
2858
2997
|
}
|
|
2859
2998
|
|
|
2999
|
+
/**
|
|
3000
|
+
* The code an extension-codes supplement gives a post-coordinated expression under: the expression
|
|
3001
|
+
* as it was written, or, if no supplement in scope has that, the expression in its minimal rendering
|
|
3002
|
+
* (so that a supplement written without spaces or terms matches an expression written with them).
|
|
3003
|
+
*
|
|
3004
|
+
* @param {SnomedExpressionContext} ctxt - a complex expression
|
|
3005
|
+
* @returns {string} the code to look for in the supplements
|
|
3006
|
+
*/
|
|
3007
|
+
#extensionCodeKey(ctxt) {
|
|
3008
|
+
const source = ctxt.getCode();
|
|
3009
|
+
if (source && this._extensionCodeDisplay(source)) {
|
|
3010
|
+
return source;
|
|
3011
|
+
}
|
|
3012
|
+
return this.sct.expressionServices.renderExpression(ctxt.expression, SnomedServicesRenderOption.Minimal);
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
/**
|
|
3016
|
+
* @param {SnomedExpressionContext} ctxt - a complex expression
|
|
3017
|
+
* @returns {string|null} the display an extension-codes supplement in scope gives it, if one does
|
|
3018
|
+
*/
|
|
3019
|
+
#extensionCodeDisplay(ctxt) {
|
|
3020
|
+
return this._extensionCodeDisplay(this.#extensionCodeKey(ctxt));
|
|
3021
|
+
}
|
|
3022
|
+
|
|
2860
3023
|
getLanguageCode(langIndex) {
|
|
2861
3024
|
return SCT_LANGUAGE_CODES[langIndex] || 'en';
|
|
2862
3025
|
}
|
|
@@ -2978,15 +3141,50 @@ class SnomedProvider extends BaseCSServices {
|
|
|
2978
3141
|
return SnomedExpressionContext.fromReference(key);
|
|
2979
3142
|
}
|
|
2980
3143
|
|
|
3144
|
+
// The FHIR value for a concrete value in $lookup. A number is typed by the attribute's MRCM
|
|
3145
|
+
// range where it has one - so #4000 for a dec() attribute is a decimal, as the concept model
|
|
3146
|
+
// defines it - and otherwise by how it is written (#4000 an integer, #2.5 a decimal).
|
|
3147
|
+
#concreteLookupValue(cv) {
|
|
3148
|
+
const c = cv.concrete;
|
|
3149
|
+
switch (c.kind) {
|
|
3150
|
+
case 'string':
|
|
3151
|
+
return { valueString: c.value };
|
|
3152
|
+
case 'boolean':
|
|
3153
|
+
return { valueBoolean: c.value };
|
|
3154
|
+
case 'number': {
|
|
3155
|
+
let type = c.decimal ? 'dec' : 'int';
|
|
3156
|
+
const services = this.sct.expressionServices;
|
|
3157
|
+
const constraint = services && services.mrcmAttributeRanges ? services.mrcmAttributeRanges().get(cv.relType) : null;
|
|
3158
|
+
const range = constraint ? services.parseConcreteRange(constraint) : null;
|
|
3159
|
+
if (range && (range.type === 'dec' || range.type === 'int')) {
|
|
3160
|
+
type = range.type;
|
|
3161
|
+
}
|
|
3162
|
+
return type === 'int' && Number.isInteger(c.value) ? { valueInteger: c.value } : { valueDecimal: c.value };
|
|
3163
|
+
}
|
|
3164
|
+
default:
|
|
3165
|
+
return null;
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
/**
|
|
3170
|
+
* The concept's properties for $expand: built by extendLookup, so that an expansion reports
|
|
3171
|
+
* the same properties $lookup does (see CodeSystemProvider._propertiesFromLookup)
|
|
3172
|
+
*/
|
|
3173
|
+
async properties(context, names) {
|
|
3174
|
+
return this._propertiesFromLookup(context, names);
|
|
3175
|
+
}
|
|
3176
|
+
|
|
2981
3177
|
async extendLookup(context, props, params) {
|
|
2982
3178
|
const ctxt = await this.#ensureContext(context);
|
|
2983
3179
|
if (ctxt) {
|
|
2984
3180
|
if (!(ctxt instanceof SnomedExpressionContext) || ctxt.expression?.concepts.length == 1) {
|
|
2985
|
-
// Only what was asked for, if anything was (see _hasProp): effectiveTime,
|
|
3181
|
+
// Only what was asked for, if anything was (see _hasProp): effectiveTime, moduleId,
|
|
2986
3182
|
// parent, child and the defining relationships are returned by default but are not
|
|
2987
3183
|
// important enough to return when the client named the properties it wants.
|
|
2988
3184
|
// The mappings (alternate identifiers such as the LOINC code) are returned by
|
|
2989
|
-
// default too, and when asked for by name.
|
|
3185
|
+
// default too, and when asked for by name. property=* asks for every property the
|
|
3186
|
+
// SNOMED CT on FHIR page defines (adding sufficientlyDefined and semanticTag) except
|
|
3187
|
+
// the normal forms, which are computed and have to be asked for by name.
|
|
2990
3188
|
if (this._hasProp(props, 'effectiveTime', true)) {
|
|
2991
3189
|
const time = this.sct.concepts.getConcept(ctxt.getReference()).effectiveTime;
|
|
2992
3190
|
// Pascal TDateTime epoch (1899-12-30) computed in UTC so the formatted
|
|
@@ -3016,11 +3214,35 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3016
3214
|
}
|
|
3017
3215
|
}
|
|
3018
3216
|
|
|
3019
|
-
|
|
3217
|
+
// moduleId, as the SNOMED CT on FHIR page names it ("module" is still understood)
|
|
3218
|
+
if (this._hasProp(props, 'moduleId', true) || SnomedProvider.#askedFor(props, 'module')) {
|
|
3020
3219
|
const moduleId = this.sct.concepts.getModuleId(ctxt.getReference());
|
|
3021
3220
|
if (moduleId) {
|
|
3022
3221
|
const code = this.sct.getConceptId(moduleId);
|
|
3023
|
-
this._addCodeProperty(params, 'property', '
|
|
3222
|
+
this._addCodeProperty(params, 'property', 'moduleId', code, null, this.sct.getDisplayName(moduleId));
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
// returned for property=* or by name, not by default
|
|
3227
|
+
if (this._hasProp(props, 'sufficientlyDefined', false)) {
|
|
3228
|
+
params.push({
|
|
3229
|
+
name: 'property',
|
|
3230
|
+
part: [
|
|
3231
|
+
{name: 'code', valueCode: 'sufficientlyDefined'},
|
|
3232
|
+
{name: 'value', valueBoolean: !this.sct.isPrimitive(ctxt.getReference())}
|
|
3233
|
+
]
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
if (this._hasProp(props, 'semanticTag', false)) {
|
|
3237
|
+
const tag = this.#semanticTag(ctxt.getReference());
|
|
3238
|
+
if (tag) {
|
|
3239
|
+
params.push({
|
|
3240
|
+
name: 'property',
|
|
3241
|
+
part: [
|
|
3242
|
+
{name: 'code', valueCode: 'semanticTag'},
|
|
3243
|
+
{name: 'value', valueString: tag}
|
|
3244
|
+
]
|
|
3245
|
+
});
|
|
3024
3246
|
}
|
|
3025
3247
|
}
|
|
3026
3248
|
|
|
@@ -3041,8 +3263,28 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3041
3263
|
}
|
|
3042
3264
|
}
|
|
3043
3265
|
|
|
3266
|
+
// concrete values (strengths, counts, ...): not relationships to concepts, so held apart
|
|
3267
|
+
for (const cv of (this.sct.concreteValues && this.sct.concreteValues.get(ctxt.getReference())) || []) {
|
|
3268
|
+
const relType = this.sct.getConceptId(cv.relType);
|
|
3269
|
+
if (this._hasProp(props, relType, true)) {
|
|
3270
|
+
const value = this.#concreteLookupValue(cv);
|
|
3271
|
+
if (value) {
|
|
3272
|
+
params.push({
|
|
3273
|
+
name: 'property',
|
|
3274
|
+
part: [
|
|
3275
|
+
{name: 'code', valueCode: relType},
|
|
3276
|
+
{name: 'value', ...value},
|
|
3277
|
+
{name: 'code-display', valueString: this.sct.getDisplayName(cv.relType)}
|
|
3278
|
+
]
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3044
3284
|
this.#addMappingProperties(ctxt, props, params);
|
|
3045
3285
|
}
|
|
3286
|
+
// the normal forms: only when asked for by name (not by default, not by property=*)
|
|
3287
|
+
this.#addNormalFormProperties(ctxt, props, params);
|
|
3046
3288
|
if (ctxt instanceof SnomedExpressionContext) {
|
|
3047
3289
|
// ignore concepts for now, but list refinements and refinement groups
|
|
3048
3290
|
for (const refinement of ctxt.expression.refinements) {
|
|
@@ -3057,6 +3299,63 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3057
3299
|
}
|
|
3058
3300
|
}
|
|
3059
3301
|
|
|
3302
|
+
// Whether props names this property explicitly (property=* does not count)
|
|
3303
|
+
static #askedFor(props, name) {
|
|
3304
|
+
const lower = name.toLowerCase();
|
|
3305
|
+
return !!props && props.some(p => p && p.toLowerCase() === lower);
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
// The semantic tag: the bracketed suffix of the concept's active fully specified name
|
|
3309
|
+
#semanticTag(reference) {
|
|
3310
|
+
const K = this.sct._displayConstants();
|
|
3311
|
+
const concept = this.sct.concepts.getConcept(reference);
|
|
3312
|
+
const descIndices = concept.descriptions ? (this.sct.refs.getReferences(concept.descriptions) || []) : [];
|
|
3313
|
+
for (const descIndex of descIndices) {
|
|
3314
|
+
const d = this.sct.descriptions.getDescription(descIndex);
|
|
3315
|
+
if (d.active && d.kind === K.fsn) {
|
|
3316
|
+
const m = /\(([^()]+)\)\s*$/.exec(this.sct.strings.getEntry(d.iDesc));
|
|
3317
|
+
if (m) {
|
|
3318
|
+
return m[1];
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
return null;
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
// normalForm (with terms) and normalFormTerse (without), for a concept or an expression
|
|
3326
|
+
#addNormalFormProperties(ctxt, props, params) {
|
|
3327
|
+
const wantFull = SnomedProvider.#askedFor(props, 'normalForm');
|
|
3328
|
+
const wantTerse = SnomedProvider.#askedFor(props, 'normalFormTerse');
|
|
3329
|
+
if (!wantFull && !wantTerse) {
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
const services = this.sct.expressionServices;
|
|
3333
|
+
let nf;
|
|
3334
|
+
if (ctxt instanceof SnomedExpressionContext && ctxt.isComplex()) {
|
|
3335
|
+
nf = services.normaliseExpression(ctxt.expression);
|
|
3336
|
+
} else {
|
|
3337
|
+
nf = services.createNormalForm(ctxt.getReference());
|
|
3338
|
+
}
|
|
3339
|
+
if (wantFull) {
|
|
3340
|
+
params.push({
|
|
3341
|
+
name: 'property',
|
|
3342
|
+
part: [
|
|
3343
|
+
{name: 'code', valueCode: 'normalForm'},
|
|
3344
|
+
{name: 'value', valueString: services.renderExpression(nf, SnomedServicesRenderOption.FillMissing)}
|
|
3345
|
+
]
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
if (wantTerse) {
|
|
3349
|
+
params.push({
|
|
3350
|
+
name: 'property',
|
|
3351
|
+
part: [
|
|
3352
|
+
{name: 'code', valueCode: 'normalFormTerse'},
|
|
3353
|
+
{name: 'value', valueString: services.renderExpression(nf, SnomedServicesRenderOption.Minimal)}
|
|
3354
|
+
]
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3060
3359
|
/**
|
|
3061
3360
|
* The concept's alternate identifiers in the schemes we know (IDENTIFIER_SCHEMES), as
|
|
3062
3361
|
* Coding-valued properties named for the scheme - e.g. loinc = http://loinc.org#2160-0.
|
|
@@ -3100,15 +3399,16 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3100
3399
|
|
|
3101
3400
|
// Filter support
|
|
3102
3401
|
async doesFilter(prop, op, value) {
|
|
3402
|
+
prop = SnomedProvider.#filterProperty(prop);
|
|
3103
3403
|
if (prop === 'concept') {
|
|
3104
3404
|
const id = this.sct.stringToIdOrZero(value);
|
|
3105
|
-
if (id !== 0n && ['=', 'is-a', 'descendent-of', 'in', 'generalizes', 'child-of'].includes(op)) {
|
|
3405
|
+
if (id !== 0n && ['=', 'is-a', 'descendent-of', 'in', 'generalizes', 'child-of', 'descendent-leaf', 'is-not-a'].includes(op)) {
|
|
3106
3406
|
return this.sct.conceptExists(value);
|
|
3107
3407
|
}
|
|
3108
3408
|
if (op === 'in' && value.includes(',')) {
|
|
3109
3409
|
let ok = true;
|
|
3110
3410
|
for (const idStr of value.split(',')) {
|
|
3111
|
-
const id = this.sct.stringToIdOrZero(idStr);
|
|
3411
|
+
const id = this.sct.stringToIdOrZero(idStr.trim());
|
|
3112
3412
|
if (id === 0n) {
|
|
3113
3413
|
ok = false;
|
|
3114
3414
|
break;
|
|
@@ -3121,6 +3421,15 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3121
3421
|
return op === '=' && ['true', 'false'].includes(value);
|
|
3122
3422
|
}
|
|
3123
3423
|
|
|
3424
|
+
if (prop === 'display' && op === 'regex') {
|
|
3425
|
+
try {
|
|
3426
|
+
SnomedProvider.#displayRegex(value);
|
|
3427
|
+
return true;
|
|
3428
|
+
} catch (e) {
|
|
3429
|
+
return false;
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3124
3433
|
if (prop === 'moduleId') {
|
|
3125
3434
|
const id = this.sct.stringToIdOrZero(value);
|
|
3126
3435
|
return id !== 0n && op === '=';
|
|
@@ -3148,9 +3457,8 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3148
3457
|
}
|
|
3149
3458
|
|
|
3150
3459
|
// eslint-disable-next-line no-unused-vars
|
|
3151
|
-
async getPrepContext(iterate) {
|
|
3152
|
-
|
|
3153
|
-
return new SnomedPrep(); // Simple filter context
|
|
3460
|
+
async getPrepContext(iterate, languages = null) {
|
|
3461
|
+
return new SnomedPrep(languages);
|
|
3154
3462
|
}
|
|
3155
3463
|
|
|
3156
3464
|
// Cache key pinning resolved analysis to the SNOMED edition url + version.
|
|
@@ -3202,7 +3510,84 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3202
3510
|
});
|
|
3203
3511
|
}
|
|
3204
3512
|
|
|
3513
|
+
// Value sets in production say "code" where SNOMED defines "concept" (users don't tell
|
|
3514
|
+
// them apart), so the two are the same filter property here, as they are for code
|
|
3515
|
+
// systems defined as resources (cs-cs.js).
|
|
3516
|
+
static #filterProperty(prop) {
|
|
3517
|
+
return prop === 'code' ? 'concept' : prop;
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
// display regex: matches a concept with an active description (FSN or synonym - a text
|
|
3521
|
+
// definition is not a display), in one language, whose whole term matches the pattern
|
|
3522
|
+
// (anchored, as for regex filters on code systems defined as resources), case-sensitively. RE2, so no catastrophic backtracking. The language is
|
|
3523
|
+
// the first one the request named that the edition has descriptions in, else the
|
|
3524
|
+
// edition's default.
|
|
3525
|
+
static #displayRegex(pattern) {
|
|
3526
|
+
return regexUtilities.compile('^(?:' + pattern + ')$');
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
#filterLanguage(languages) {
|
|
3530
|
+
if (!this.sct._activeDescLanguages) {
|
|
3531
|
+
this.sct._activeDescLanguages = this.sct.descriptions.activeLanguages();
|
|
3532
|
+
}
|
|
3533
|
+
const present = this.sct._activeDescLanguages;
|
|
3534
|
+
for (const lang of languages || []) {
|
|
3535
|
+
const code = SCT_LANGUAGE_BYTES[(lang.language || '').toLowerCase()];
|
|
3536
|
+
if (code !== undefined && present.has(code)) {
|
|
3537
|
+
return code;
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
return this.sct.defaultLanguage || 1;
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
// the concept index of 900000000000550004 |Definition|, or -1
|
|
3544
|
+
#definitionKind() {
|
|
3545
|
+
if (this.sct._definitionKind === undefined) {
|
|
3546
|
+
const r = this.sct.concepts.findConcept(900000000000550004n);
|
|
3547
|
+
this.sct._definitionKind = r.found ? r.index : -1;
|
|
3548
|
+
}
|
|
3549
|
+
return this.sct._definitionKind;
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
#displayMatches(set, reference) {
|
|
3553
|
+
const definition = this.#definitionKind();
|
|
3554
|
+
const concept = this.sct.concepts.getConcept(reference);
|
|
3555
|
+
const descIndices = concept.descriptions ? (this.sct.refs.getReferences(concept.descriptions) || []) : [];
|
|
3556
|
+
for (const descIndex of descIndices) {
|
|
3557
|
+
const d = this.sct.descriptions.getDescription(descIndex);
|
|
3558
|
+
if (d.active && d.lang === set.displayLang && d.kind !== definition && set.displayRegex.test(this.sct.strings.getEntry(d.iDesc))) {
|
|
3559
|
+
return true;
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
return false;
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
// The scan for an iterated display regex: every active description in the language,
|
|
3566
|
+
// a chunk at a time, yielding between chunks so the server keeps serving meanwhile.
|
|
3567
|
+
async #populateDisplayRegex(set) {
|
|
3568
|
+
if (set.populationDone) {
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
const CHUNK = 10000;
|
|
3572
|
+
const found = new Set();
|
|
3573
|
+
const test = (term) => set.displayRegex.test(term);
|
|
3574
|
+
const definition = this.#definitionKind();
|
|
3575
|
+
let count = Infinity;
|
|
3576
|
+
for (let start = 0; start < count; start += CHUNK) {
|
|
3577
|
+
if (this.opContext) {
|
|
3578
|
+
await this.opContext.checkAndYield('snomed:displayRegex');
|
|
3579
|
+
}
|
|
3580
|
+
count = this.sct.descriptions.matchActiveTerms(start, start + CHUNK, set.displayLang, this.sct.strings, test, found, definition);
|
|
3581
|
+
}
|
|
3582
|
+
set.descendants = [...found].sort((a, b) => a - b);
|
|
3583
|
+
set.populationDone = true;
|
|
3584
|
+
if (set.displayCache) {
|
|
3585
|
+
set.displayCache.map.set(set.displayCache.key, set.descendants);
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3205
3589
|
async filter(filterContext, forIteration, prop, op, value, fc) {
|
|
3590
|
+
prop = SnomedProvider.#filterProperty(prop);
|
|
3206
3591
|
|
|
3207
3592
|
if (prop === 'concept') {
|
|
3208
3593
|
const id = this.sct.stringToIdOrZero(value);
|
|
@@ -3231,6 +3616,14 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3231
3616
|
filterContext.filters.push(this.sct.filterGeneralizes(id, false));
|
|
3232
3617
|
return null;
|
|
3233
3618
|
}
|
|
3619
|
+
case 'descendent-leaf': {
|
|
3620
|
+
filterContext.filters.push(this.sct.filterDescendentLeaf(id));
|
|
3621
|
+
return null;
|
|
3622
|
+
}
|
|
3623
|
+
case 'is-not-a': {
|
|
3624
|
+
filterContext.filters.push(this.sct.filterIsNotA(id, this.opContext));
|
|
3625
|
+
return null;
|
|
3626
|
+
}
|
|
3234
3627
|
case 'in': {
|
|
3235
3628
|
filterContext.filters.push(this.sct.filterIn(value));
|
|
3236
3629
|
return null;
|
|
@@ -3240,6 +3633,31 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3240
3633
|
}
|
|
3241
3634
|
}
|
|
3242
3635
|
|
|
3636
|
+
if (prop === 'display' && op === 'regex') {
|
|
3637
|
+
const result = new SnomedFilterContext();
|
|
3638
|
+
result.displayRegex = SnomedProvider.#displayRegex(value);
|
|
3639
|
+
result.displayLang = this.#filterLanguage(filterContext.languages);
|
|
3640
|
+
// Like ECL, a cached value set's filter keeps what it resolved to: here, per edition
|
|
3641
|
+
// and language, the concepts the scan found, so the scan is paid once.
|
|
3642
|
+
if (fc) {
|
|
3643
|
+
let map = fc[SNOMED_DISPLAY_REGEX];
|
|
3644
|
+
if (!map) {
|
|
3645
|
+
map = new Map();
|
|
3646
|
+
Object.defineProperty(fc, SNOMED_DISPLAY_REGEX, { value: map, enumerable: false, configurable: true, writable: true });
|
|
3647
|
+
}
|
|
3648
|
+
const key = this._snomedFilterCacheKey() + '|' + result.displayLang;
|
|
3649
|
+
const cached = map.get(key);
|
|
3650
|
+
if (cached) {
|
|
3651
|
+
result.descendants = cached;
|
|
3652
|
+
result.populationDone = true;
|
|
3653
|
+
} else {
|
|
3654
|
+
result.displayCache = { map, key };
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
filterContext.filters.push(result);
|
|
3658
|
+
return null;
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3243
3661
|
if (prop === 'inactive') {
|
|
3244
3662
|
if (value !== 'true' && value !== 'false') {
|
|
3245
3663
|
throw new Error(`Invalid filter value: ${value}`);
|
|
@@ -3352,6 +3770,9 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3352
3770
|
}
|
|
3353
3771
|
|
|
3354
3772
|
async filterSize(filterContext, set) {
|
|
3773
|
+
if (set.displayRegex) {
|
|
3774
|
+
await this.#populateDisplayRegex(set);
|
|
3775
|
+
}
|
|
3355
3776
|
// A wildcard (`*`) set is lazy - it carries the eclWildcard flag rather than a
|
|
3356
3777
|
// materialised descendant list - so count the active concepts it denotes.
|
|
3357
3778
|
if (set.eclWildcard && (!set.descendants || set.descendants.length === 0)) {
|
|
@@ -3370,6 +3791,9 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3370
3791
|
|
|
3371
3792
|
async filterMore(filterContext, set) {
|
|
3372
3793
|
set.cursor = set.cursor || 0;
|
|
3794
|
+
if (set.displayRegex) {
|
|
3795
|
+
await this.#populateDisplayRegex(set);
|
|
3796
|
+
}
|
|
3373
3797
|
this.#ensurePopulated(set);
|
|
3374
3798
|
const size = await this.filterSize(filterContext, set);
|
|
3375
3799
|
return set.cursor < size;
|
|
@@ -3428,6 +3852,8 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3428
3852
|
let concept = this.sct.concepts.getConcept(reference);
|
|
3429
3853
|
let moduleId = this.sct.concepts.getModuleId(concept.index);
|
|
3430
3854
|
found = moduleId === set.moduleId;
|
|
3855
|
+
} else if (set.displayRegex) {
|
|
3856
|
+
found = this.#displayMatches(set, reference);
|
|
3431
3857
|
} else if (set.propProp || set.propValue) {
|
|
3432
3858
|
found = false;
|
|
3433
3859
|
const relationships = this.sct.getConceptRelationships(reference);
|
|
@@ -3477,6 +3903,10 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3477
3903
|
return this.sct.concepts.getModuleId(reference) === set.moduleId;
|
|
3478
3904
|
}
|
|
3479
3905
|
|
|
3906
|
+
if (set.displayRegex) {
|
|
3907
|
+
return this.#displayMatches(set, reference);
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3480
3910
|
if (set.propProp || set.propValue) {
|
|
3481
3911
|
const relationships = this.sct.getConceptRelationships(reference);
|
|
3482
3912
|
for (let relationshipRef of relationships) {
|
|
@@ -3597,8 +4027,23 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3597
4027
|
// Search filter
|
|
3598
4028
|
// eslint-disable-next-line no-unused-vars
|
|
3599
4029
|
async searchFilter(filterContext, filter, sort) {
|
|
3600
|
-
// `sort` is not a match-mode flag: results are always ranked.
|
|
3601
|
-
|
|
4030
|
+
// `sort` is not a match-mode flag: results are always ranked. Only descriptions in one
|
|
4031
|
+
// language take part, chosen as for the display filter: the first language the request
|
|
4032
|
+
// names that the edition has descriptions in, else the edition's default; and the codes
|
|
4033
|
+
// whose designations in that language, in a supplement in use, match
|
|
4034
|
+
const lang = this.#filterLanguage(filterContext.languages);
|
|
4035
|
+
let f = await this.sct.searchFilter(filter, false, this.opContext, lang);
|
|
4036
|
+
if (this.supplements && this.supplements.length > 0) {
|
|
4037
|
+
const have = new Set(f.matches.map(m => m.index));
|
|
4038
|
+
for (const code of this._supplementSearchCodes(filter, new Language(SCT_LANGUAGE_CODES[lang] || 'en'))) {
|
|
4039
|
+
const id = this.sct.stringToIdOrZero(code);
|
|
4040
|
+
const r = id !== 0n ? this.sct.concepts.findConcept(id) : null;
|
|
4041
|
+
if (r && r.found && !have.has(r.index) && this.sct.isActive(r.index)) {
|
|
4042
|
+
f.matches.push({ index: r.index, term: this.sct.concepts.getConceptId(r.index), priority: 10 });
|
|
4043
|
+
have.add(r.index);
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
3602
4047
|
filterContext.filters.push(f);
|
|
3603
4048
|
return f;
|
|
3604
4049
|
}
|
|
@@ -3963,9 +4408,12 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3963
4408
|
* Factory for creating SNOMED services and providers
|
|
3964
4409
|
*/
|
|
3965
4410
|
class SnomedServicesFactory extends CodeSystemFactoryProvider {
|
|
3966
|
-
constructor(i18n, filePath) {
|
|
4411
|
+
constructor(i18n, filePath, loadMode = 'read', log = null) {
|
|
3967
4412
|
super(i18n);
|
|
3968
4413
|
this.filePath = filePath;
|
|
4414
|
+
this.loadMode = loadMode; // 'read', 'mmap' or 'prewarm' - see SnomedFileReader
|
|
4415
|
+
this.log = log;
|
|
4416
|
+
this.mapped = false; // set by load(): whether the cache file really is memory-mapped
|
|
3969
4417
|
this.uses = 0;
|
|
3970
4418
|
this._loaded = false;
|
|
3971
4419
|
this._sharedData = null;
|
|
@@ -3979,6 +4427,26 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
|
|
|
3979
4427
|
return this._sharedData.versionUri;
|
|
3980
4428
|
}
|
|
3981
4429
|
|
|
4430
|
+
/**
|
|
4431
|
+
* A concept links to snomed.info, which redirects to a browser: in its edition when that is
|
|
4432
|
+
* one snomed.info knows, otherwise without an edition. An expression links to the browser's
|
|
4433
|
+
* home page. The same links as SnomedUtilities.getSctLink in org.hl7.fhir.core.
|
|
4434
|
+
*/
|
|
4435
|
+
codeLink(context) {
|
|
4436
|
+
if (!context || !context.expression) {
|
|
4437
|
+
return undefined;
|
|
4438
|
+
}
|
|
4439
|
+
const code = context.isSimple() ? (context.source || context.expression.concepts[0].code) : null;
|
|
4440
|
+
if (!code || !/^\d+$/.test(code)) {
|
|
4441
|
+
return 'https://browser.ihtsdotools.org/';
|
|
4442
|
+
}
|
|
4443
|
+
const m = /^http:\/\/snomed\.info\/sct\/(\d+)/.exec(this.version() || '');
|
|
4444
|
+
if (m && SNOMED_LINKABLE_EDITIONS.has(m[1])) {
|
|
4445
|
+
return `http://snomed.info/sct/${m[1]}/id/${code}`;
|
|
4446
|
+
}
|
|
4447
|
+
return `http://snomed.info/id/${code}`;
|
|
4448
|
+
}
|
|
4449
|
+
|
|
3982
4450
|
getPartialVersion() {
|
|
3983
4451
|
let ver = this.version();
|
|
3984
4452
|
if (ver.includes("/version")) {
|
|
@@ -3997,39 +4465,86 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
|
|
|
3997
4465
|
* http://snomed.info/sct?fhir_vs=refset – list of reference sets
|
|
3998
4466
|
* http://snomed.info/sct?fhir_vs=refset/<id> – members of a reference set
|
|
3999
4467
|
* http://snomed.info/sct?fhir_vs=isa/<id> – concept and descendants
|
|
4468
|
+
* http://snomed.info/sct?fhir_vs=ecl/<ecl> – the concepts an ECL expression selects
|
|
4000
4469
|
*
|
|
4001
4470
|
* The URL may optionally include edition and/or version segments:
|
|
4002
4471
|
* http://snomed.info/sct/<edition>?fhir_vs...
|
|
4003
4472
|
* http://snomed.info/sct/<edition>/version/<ver>?fhir_vs...
|
|
4004
4473
|
*
|
|
4474
|
+
* Which edition and version the value set is drawn from is the provider's choice, made
|
|
4475
|
+
* with implicitValueSetForm(): the generic form from the default edition, the edition
|
|
4476
|
+
* form from the latest version of that edition, the version form from that version.
|
|
4477
|
+
* For the edition and version forms, the value set's include names this version, so that
|
|
4478
|
+
* it is expanded against it; the generic form leaves the version to the request.
|
|
4479
|
+
*
|
|
4005
4480
|
* @param {string} url - The ValueSet URL to resolve
|
|
4006
4481
|
* @returns {object|null} A FHIR ValueSet JSON object, or null if the URL is not recognised
|
|
4007
4482
|
*/
|
|
4008
4483
|
async buildKnownValueSet(url, version) {
|
|
4009
|
-
if (!url.startsWith(
|
|
4484
|
+
if (!url.startsWith('http://snomed.info/')) {
|
|
4010
4485
|
return null;
|
|
4011
4486
|
}
|
|
4487
|
+
await this.#ensureLoaded();
|
|
4012
4488
|
if (version != null && !this.version().startsWith(version)) {
|
|
4013
4489
|
return null;
|
|
4014
4490
|
}
|
|
4491
|
+
const form = this.implicitValueSetForm(url);
|
|
4492
|
+
if (!form) {
|
|
4493
|
+
return null;
|
|
4494
|
+
}
|
|
4495
|
+
const id = form.query;
|
|
4496
|
+
const vs = await this.#buildImplicitValueSet(url, id);
|
|
4497
|
+
if (vs && form.form !== 'generic') {
|
|
4498
|
+
for (const include of vs.compose.include) {
|
|
4499
|
+
include.version = this.version();
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
return vs;
|
|
4503
|
+
}
|
|
4015
4504
|
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4505
|
+
/**
|
|
4506
|
+
* Whether url is an implicit value set of this edition, and in which form:
|
|
4507
|
+
* generic - http://snomed.info/sct?fhir_vs... (any edition)
|
|
4508
|
+
* edition - http://snomed.info/sct/<edition>?fhir_vs... (this edition, no version given)
|
|
4509
|
+
* version - http://snomed.info/sct/<edition>/version/<date>?fhir_vs... (this version)
|
|
4510
|
+
* The edition and version forms are also recognised under this version's own base
|
|
4511
|
+
* (http://snomed.info/xsct/... for the test edition).
|
|
4512
|
+
*
|
|
4513
|
+
* @param {string} url
|
|
4514
|
+
* @returns {{form: string, query: string}|null}
|
|
4515
|
+
*/
|
|
4516
|
+
implicitValueSetForm(url) {
|
|
4517
|
+
if (!url || !this._sharedData) {
|
|
4518
|
+
return null;
|
|
4519
|
+
}
|
|
4520
|
+
const qIdx = url.indexOf('?fhir_vs');
|
|
4021
4521
|
if (qIdx === -1) {
|
|
4022
4522
|
return null;
|
|
4023
4523
|
}
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
url.startsWith(`http://snomed.info/sct/${this.edition}/version/${this.version()}?fhir_vs`)) {
|
|
4028
|
-
id = url.substring(qIdx);
|
|
4029
|
-
} else {
|
|
4524
|
+
const base = url.substring(0, qIdx);
|
|
4525
|
+
const query = url.substring(qIdx);
|
|
4526
|
+
if (query !== '?fhir_vs' && !query.startsWith('?fhir_vs=')) {
|
|
4030
4527
|
return null;
|
|
4031
4528
|
}
|
|
4529
|
+
const versionUri = this.version();
|
|
4530
|
+
const edition = this._sharedData.edition;
|
|
4531
|
+
const date = this._sharedData.version;
|
|
4532
|
+
const editionBases = new Set([`http://snomed.info/sct/${edition}`, this.getPartialVersion()]);
|
|
4533
|
+
const versionBases = new Set([versionUri, `http://snomed.info/sct/${edition}/version/${date}`]);
|
|
4534
|
+
if (base === 'http://snomed.info/sct') {
|
|
4535
|
+
return { form: 'generic', query };
|
|
4536
|
+
}
|
|
4537
|
+
if (editionBases.has(base)) {
|
|
4538
|
+
return { form: 'edition', query };
|
|
4539
|
+
}
|
|
4540
|
+
if (versionBases.has(base)) {
|
|
4541
|
+
return { form: 'version', query };
|
|
4542
|
+
}
|
|
4543
|
+
return null;
|
|
4544
|
+
}
|
|
4032
4545
|
|
|
4546
|
+
async #buildImplicitValueSet(url, id) {
|
|
4547
|
+
const URI_SNOMED = 'http://snomed.info/sct';
|
|
4033
4548
|
const now = new Date().toISOString();
|
|
4034
4549
|
|
|
4035
4550
|
if (id === '?fhir_vs=refset') {
|
|
@@ -4043,7 +4558,6 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
|
|
|
4043
4558
|
resourceType: 'ValueSet',
|
|
4044
4559
|
url,
|
|
4045
4560
|
status: 'active',
|
|
4046
|
-
version: this.versionDate,
|
|
4047
4561
|
name: 'SNOMEDCTReferenceSetList',
|
|
4048
4562
|
title: 'SNOMED CT Reference Set List',
|
|
4049
4563
|
description: 'Reference Sets defined in this SNOMED-CT version',
|
|
@@ -4063,7 +4577,6 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
|
|
|
4063
4577
|
resourceType: 'ValueSet',
|
|
4064
4578
|
url,
|
|
4065
4579
|
status: 'active',
|
|
4066
|
-
version: this.versionDate,
|
|
4067
4580
|
name: 'ALLSNOMEDCT',
|
|
4068
4581
|
title: 'SNOMED CT Reference Set (All of SNOMED CT)',
|
|
4069
4582
|
description: 'SNOMED CT Reference Set (All of SNOMED CT)',
|
|
@@ -4090,7 +4603,6 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
|
|
|
4090
4603
|
resourceType: 'ValueSet',
|
|
4091
4604
|
url,
|
|
4092
4605
|
status: 'active',
|
|
4093
|
-
version: this.versionDate,
|
|
4094
4606
|
name: 'SNOMEDCTRefSet' + refsetId,
|
|
4095
4607
|
title: 'SNOMED CT Reference Set ' + refsetId,
|
|
4096
4608
|
description: this.snomedServices.getDisplayName(ref.index),
|
|
@@ -4108,6 +4620,38 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
|
|
|
4108
4620
|
};
|
|
4109
4621
|
}
|
|
4110
4622
|
|
|
4623
|
+
if (id.startsWith('?fhir_vs=ecl/')) {
|
|
4624
|
+
// the ECL is URL-encoded in the value set URL; accept it already decoded as well
|
|
4625
|
+
let ecl = id.substring(13);
|
|
4626
|
+
try {
|
|
4627
|
+
ecl = decodeURIComponent(ecl);
|
|
4628
|
+
} catch (e) {
|
|
4629
|
+
// not encoded (or not validly): use it as it stands
|
|
4630
|
+
}
|
|
4631
|
+
if (!ecl.trim()) {
|
|
4632
|
+
return null;
|
|
4633
|
+
}
|
|
4634
|
+
return {
|
|
4635
|
+
resourceType: 'ValueSet',
|
|
4636
|
+
url,
|
|
4637
|
+
status: 'active',
|
|
4638
|
+
name: 'SNOMEDCTECL',
|
|
4639
|
+
title: 'SNOMED CT ECL ' + ecl,
|
|
4640
|
+
description: 'The SNOMED CT concepts selected by the expression constraint ' + ecl,
|
|
4641
|
+
date: now,
|
|
4642
|
+
compose: {
|
|
4643
|
+
include: [{
|
|
4644
|
+
system: URI_SNOMED,
|
|
4645
|
+
filter: [{
|
|
4646
|
+
property: 'constraint',
|
|
4647
|
+
op: '=',
|
|
4648
|
+
value: ecl,
|
|
4649
|
+
}],
|
|
4650
|
+
}],
|
|
4651
|
+
},
|
|
4652
|
+
};
|
|
4653
|
+
}
|
|
4654
|
+
|
|
4111
4655
|
if (id.startsWith('?fhir_vs=isa/')) {
|
|
4112
4656
|
const conceptId = id.substring(13);
|
|
4113
4657
|
let ref = this.snomedServices.concepts.findConcept(conceptId);
|
|
@@ -4118,7 +4662,6 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
|
|
|
4118
4662
|
resourceType: 'ValueSet',
|
|
4119
4663
|
url,
|
|
4120
4664
|
status: 'active',
|
|
4121
|
-
version: this.versionDate,
|
|
4122
4665
|
name: 'SNOMEDCTConcept' + conceptId,
|
|
4123
4666
|
title: 'SNOMED CT Concept ' + conceptId + ' and descendants',
|
|
4124
4667
|
description: 'All Snomed CT concepts for ' + this.snomedServices.getDisplayName(ref.index),
|
|
@@ -4145,8 +4688,9 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
|
|
|
4145
4688
|
}
|
|
4146
4689
|
|
|
4147
4690
|
async load() {
|
|
4148
|
-
const reader = new SnomedFileReader(this.filePath);
|
|
4691
|
+
const reader = new SnomedFileReader(this.filePath, this.loadMode, this.log);
|
|
4149
4692
|
this._sharedData = await reader.loadSnomedData();
|
|
4693
|
+
this.mapped = reader.mapped;
|
|
4150
4694
|
this.snomedServices = new SnomedServices(this._sharedData);
|
|
4151
4695
|
this._loaded = true;
|
|
4152
4696
|
}
|