fhirsmith 0.3.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/CHANGELOG.md +42 -0
- package/FHIRsmith.png +0 -0
- package/README.md +277 -0
- package/config-template.json +144 -0
- package/library/folder-setup.js +58 -0
- package/library/html-server.js +166 -0
- package/library/html.js +835 -0
- package/library/i18nsupport.js +259 -0
- package/library/languages.js +779 -0
- package/library/logger-telnet.js +205 -0
- package/library/logger.js +279 -0
- package/library/package-manager.js +876 -0
- package/library/utilities.js +196 -0
- package/library/version-utilities.js +1056 -0
- package/npmprojector/config-example.json +13 -0
- package/npmprojector/indexer.js +394 -0
- package/npmprojector/npmprojector.js +395 -0
- package/npmprojector/readme.md +174 -0
- package/npmprojector/watcher.js +335 -0
- package/package.json +119 -0
- package/packages/package-crawler.js +846 -0
- package/packages/packages-template.html +126 -0
- package/packages/packages.js +2838 -0
- package/passwords.ini +2 -0
- package/publisher/publisher-template.html +208 -0
- package/publisher/publisher.js +2167 -0
- package/publisher/task-draft.js +458 -0
- package/registry/api.js +735 -0
- package/registry/crawler.js +637 -0
- package/registry/model.js +513 -0
- package/registry/readme.md +243 -0
- package/registry/registry-data.json +121015 -0
- package/registry/registry-template.html +126 -0
- package/registry/registry.js +1395 -0
- package/registry/test-runner.js +237 -0
- package/root-template.html +124 -0
- package/server.js +524 -0
- package/shl/private-key.pem +5 -0
- package/shl/public-key.pem +18 -0
- package/shl/shl.js +1125 -0
- package/shl/vhl.js +69 -0
- package/static/FHIRsmith128.png +0 -0
- package/static/FHIRsmith16.png +0 -0
- package/static/FHIRsmith32.png +0 -0
- package/static/FHIRsmith64.png +0 -0
- package/static/assets/css/bootstrap-fhir.css +5302 -0
- package/static/assets/css/bootstrap-glyphicons.css +2 -0
- package/static/assets/css/bootstrap.css +4097 -0
- package/static/assets/css/jquery-ui.css +523 -0
- package/static/assets/css/jquery-ui.structure.css +863 -0
- package/static/assets/css/jquery-ui.structure.min.css +5 -0
- package/static/assets/css/jquery-ui.theme.css +439 -0
- package/static/assets/css/jquery-ui.theme.min.css +5 -0
- package/static/assets/css/jquery.ui.all.css +7 -0
- package/static/assets/css/modules.css +18 -0
- package/static/assets/css/project.css +367 -0
- package/static/assets/css/pygments-manni.css +66 -0
- package/static/assets/css/tags.css +74 -0
- package/static/assets/css/xml.css +2 -0
- package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
- package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
- package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
- package/static/assets/ico/favicon.ico +0 -0
- package/static/assets/ico/favicon.png +0 -0
- package/static/assets/images/fhir-logo-www.png +0 -0
- package/static/assets/images/fhir-logo.png +0 -0
- package/static/assets/images/hl7-logo.png +0 -0
- package/static/assets/images/logo_ansinew.jpg +0 -0
- package/static/assets/images/search.png +0 -0
- package/static/assets/images/stripe.png +0 -0
- package/static/assets/images/target.png +0 -0
- package/static/assets/images/tx-registry-root.gif +0 -0
- package/static/assets/images/tx-registry.png +0 -0
- package/static/assets/images/tx-server.png +0 -0
- package/static/assets/images/tx-version.png +0 -0
- package/static/assets/js/bootstrap.min.js +6 -0
- package/static/assets/js/fhir-gw.js +259 -0
- package/static/assets/js/fhir.js +2 -0
- package/static/assets/js/html5shiv.js +8 -0
- package/static/assets/js/jcookie.js +96 -0
- package/static/assets/js/jquery-ui.min.js +6 -0
- package/static/assets/js/jquery.js +10716 -0
- package/static/assets/js/jquery.min.js +2 -0
- package/static/assets/js/jquery.ui.core.js +314 -0
- package/static/assets/js/jquery.ui.draggable.js +825 -0
- package/static/assets/js/jquery.ui.mouse.js +162 -0
- package/static/assets/js/jquery.ui.resizable.js +842 -0
- package/static/assets/js/jquery.ui.widget.js +268 -0
- package/static/assets/js/json2.js +487 -0
- package/static/assets/js/jtip.js +97 -0
- package/static/assets/js/respond.min.js +6 -0
- package/static/assets/js/statuspage.js +70 -0
- package/static/assets/js/xml.js +2 -0
- package/static/dist/js/bootstrap.js +1964 -0
- package/static/favicon.png +0 -0
- package/static/fhir.css +626 -0
- package/static/icon-fhir-16.png +0 -0
- package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
- package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- package/static/images/ui-icons_222222_256x240.png +0 -0
- package/static/images/ui-icons_228ef1_256x240.png +0 -0
- package/static/images/ui-icons_ef8c08_256x240.png +0 -0
- package/static/images/ui-icons_ffd27a_256x240.png +0 -0
- package/static/images/ui-icons_ffffff_256x240.png +0 -0
- package/static/js/jquery.effects.blind.js +49 -0
- package/static/js/jquery.effects.bounce.js +78 -0
- package/static/js/jquery.effects.clip.js +54 -0
- package/static/js/jquery.effects.core.js +763 -0
- package/static/js/jquery.effects.drop.js +50 -0
- package/static/js/jquery.effects.explode.js +79 -0
- package/static/js/jquery.effects.fade.js +32 -0
- package/static/js/jquery.effects.fold.js +56 -0
- package/static/js/jquery.effects.highlight.js +50 -0
- package/static/js/jquery.effects.pulsate.js +51 -0
- package/static/js/jquery.effects.scale.js +178 -0
- package/static/js/jquery.effects.shake.js +57 -0
- package/static/js/jquery.effects.slide.js +50 -0
- package/static/js/jquery.effects.transfer.js +45 -0
- package/static/js/jquery.ui.accordion.js +611 -0
- package/static/js/jquery.ui.autocomplete.js +612 -0
- package/static/js/jquery.ui.button.js +416 -0
- package/static/js/jquery.ui.datepicker.js +1823 -0
- package/static/js/jquery.ui.dialog.js +878 -0
- package/static/js/jquery.ui.droppable.js +296 -0
- package/static/js/jquery.ui.position.js +252 -0
- package/static/js/jquery.ui.progressbar.js +109 -0
- package/static/js/jquery.ui.selectable.js +266 -0
- package/static/js/jquery.ui.slider.js +666 -0
- package/static/js/jquery.ui.sortable.js +1077 -0
- package/static/js/jquery.ui.tabs.js +758 -0
- package/stats.js +80 -0
- package/test-cache/vsac/vsac-valuesets.db +0 -0
- package/token/nginx_passport_setup.md +383 -0
- package/token/security_guide.md +294 -0
- package/token/token-template.html +330 -0
- package/token/token.js +1300 -0
- package/translations/Messages.properties +1510 -0
- package/translations/Messages_ar.properties +1399 -0
- package/translations/Messages_de.properties +836 -0
- package/translations/Messages_es.properties +737 -0
- package/translations/Messages_fr.properties +1 -0
- package/translations/Messages_ja.properties +893 -0
- package/translations/Messages_nl.properties +1357 -0
- package/translations/Messages_pt.properties +1302 -0
- package/translations/Messages_ru.properties +1 -0
- package/translations/Messages_uz.properties +1 -0
- package/translations/Messages_zh.properties +1 -0
- package/translations/rendering-phrases.properties +1128 -0
- package/translations/rendering-phrases_ar.properties +1091 -0
- package/translations/rendering-phrases_de.properties +6 -0
- package/translations/rendering-phrases_es.properties +6 -0
- package/translations/rendering-phrases_fr.properties +624 -0
- package/translations/rendering-phrases_ja.properties +21 -0
- package/translations/rendering-phrases_nl.properties +970 -0
- package/translations/rendering-phrases_pt.properties +1020 -0
- package/translations/rendering-phrases_ru.properties +1094 -0
- package/translations/rendering-phrases_uz.properties +1 -0
- package/translations/rendering-phrases_zh.properties +1 -0
- package/tx/README.md +418 -0
- package/tx/cm/cm-api.js +110 -0
- package/tx/cm/cm-database.js +735 -0
- package/tx/cm/cm-package.js +325 -0
- package/tx/cs/cs-api.js +789 -0
- package/tx/cs/cs-areacode.js +615 -0
- package/tx/cs/cs-country.js +1110 -0
- package/tx/cs/cs-cpt.js +785 -0
- package/tx/cs/cs-cs.js +1579 -0
- package/tx/cs/cs-currency.js +539 -0
- package/tx/cs/cs-db.js +1321 -0
- package/tx/cs/cs-hgvs.js +329 -0
- package/tx/cs/cs-lang.js +465 -0
- package/tx/cs/cs-loinc.js +1485 -0
- package/tx/cs/cs-mimetypes.js +238 -0
- package/tx/cs/cs-ndc.js +704 -0
- package/tx/cs/cs-omop.js +1025 -0
- package/tx/cs/cs-provider-api.js +43 -0
- package/tx/cs/cs-provider-list.js +37 -0
- package/tx/cs/cs-rxnorm.js +808 -0
- package/tx/cs/cs-snomed.js +1102 -0
- package/tx/cs/cs-ucum.js +514 -0
- package/tx/cs/cs-unii.js +271 -0
- package/tx/cs/cs-uri.js +218 -0
- package/tx/cs/cs-usstates.js +305 -0
- package/tx/dev.fhir.org.yml +14 -0
- package/tx/fixtures/test-cases-setup.json +18 -0
- package/tx/fixtures/test-cases.yml +16 -0
- package/tx/html/codesystem-operations.liquid +25 -0
- package/tx/html/home-metrics.liquid +247 -0
- package/tx/html/operations-form.liquid +148 -0
- package/tx/html/search-form.liquid +62 -0
- package/tx/html/tx-template.html +133 -0
- package/tx/html/valueset-operations.liquid +54 -0
- package/tx/importers/atc-to-fhir.js +316 -0
- package/tx/importers/import-loinc.module.js +1536 -0
- package/tx/importers/import-ndc.module.js +1088 -0
- package/tx/importers/import-rxnorm.module.js +898 -0
- package/tx/importers/import-sct.module.js +2457 -0
- package/tx/importers/import-unii.module.js +601 -0
- package/tx/importers/readme.md +453 -0
- package/tx/importers/subset-loinc.module.js +1081 -0
- package/tx/importers/subset-rxnorm.module.js +938 -0
- package/tx/importers/tx-import-base.js +351 -0
- package/tx/importers/tx-import-settings.js +310 -0
- package/tx/importers/tx-import.js +357 -0
- package/tx/library/canonical-resource.js +88 -0
- package/tx/library/capabilitystatement.js +292 -0
- package/tx/library/codesystem.js +774 -0
- package/tx/library/conceptmap.js +568 -0
- package/tx/library/designations.js +932 -0
- package/tx/library/errors.js +77 -0
- package/tx/library/extensions.js +117 -0
- package/tx/library/namingsystem.js +322 -0
- package/tx/library/operation-outcome.js +127 -0
- package/tx/library/parameters.js +105 -0
- package/tx/library/renderer.js +1559 -0
- package/tx/library/terminologycapabilities.js +418 -0
- package/tx/library/ucum-parsers.js +1029 -0
- package/tx/library/ucum-service.js +370 -0
- package/tx/library/ucum-types.js +1099 -0
- package/tx/library/valueset.js +543 -0
- package/tx/library.js +676 -0
- package/tx/ocl/cm-ocl.js +106 -0
- package/tx/ocl/cs-ocl.js +39 -0
- package/tx/ocl/vs-ocl.js +105 -0
- package/tx/operation-context.js +568 -0
- package/tx/params.js +613 -0
- package/tx/provider.js +403 -0
- package/tx/sct/ecl.js +1560 -0
- package/tx/sct/expressions.js +2077 -0
- package/tx/sct/structures.js +1396 -0
- package/tx/tx-html.js +1063 -0
- package/tx/tx.fhir.org.yml +39 -0
- package/tx/tx.js +927 -0
- package/tx/vs/vs-api.js +112 -0
- package/tx/vs/vs-database.js +786 -0
- package/tx/vs/vs-package.js +358 -0
- package/tx/vs/vs-vsac.js +366 -0
- package/tx/workers/batch-validate.js +129 -0
- package/tx/workers/batch.js +361 -0
- package/tx/workers/closure.js +32 -0
- package/tx/workers/expand.js +1845 -0
- package/tx/workers/lookup.js +407 -0
- package/tx/workers/metadata.js +467 -0
- package/tx/workers/operations.js +34 -0
- package/tx/workers/read.js +164 -0
- package/tx/workers/search.js +384 -0
- package/tx/workers/subsumes.js +334 -0
- package/tx/workers/translate.js +492 -0
- package/tx/workers/validate.js +2504 -0
- package/tx/workers/worker.js +904 -0
- package/tx/xml/capabilitystatement-xml.js +63 -0
- package/tx/xml/codesystem-xml.js +62 -0
- package/tx/xml/conceptmap-xml.js +65 -0
- package/tx/xml/namingsystem-xml.js +65 -0
- package/tx/xml/operationoutcome-xml.js +127 -0
- package/tx/xml/parameters-xml.js +312 -0
- package/tx/xml/terminologycapabilities-xml.js +64 -0
- package/tx/xml/valueset-xml.js +64 -0
- package/tx/xml/xml-base.js +603 -0
- package/vcl/vcl-parser.js +1098 -0
- package/vcl/vcl.js +253 -0
- package/windows-install.js +19 -0
- package/xig/xig-template.html +124 -0
- package/xig/xig.js +3049 -0
package/tx/cs/cs-cs.js
ADDED
|
@@ -0,0 +1,1579 @@
|
|
|
1
|
+
const { CodeSystem} = require("../library/codesystem");
|
|
2
|
+
const { CodeSystemFactoryProvider, CodeSystemProvider, FilterExecutionContext } = require( "./cs-api");
|
|
3
|
+
const { VersionUtilities } = require("../../library/version-utilities");
|
|
4
|
+
const { Language } = require ("../../library/languages");
|
|
5
|
+
const { validateOptionalParameter, getValuePrimitive, validateArrayParameter} = require("../../library/utilities");
|
|
6
|
+
const {Issue} = require("../library/operation-outcome");
|
|
7
|
+
const {Extensions} = require("../library/extensions");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Context class for FHIR CodeSystem provider concepts
|
|
11
|
+
*/
|
|
12
|
+
class FhirCodeSystemProviderContext {
|
|
13
|
+
constructor(code, concept) {
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.concept = concept;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Context class for FHIR CodeSystem provider filter results
|
|
21
|
+
*/
|
|
22
|
+
class FhirCodeSystemProviderFilterContext {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.concepts = []; // Array of {concept, rating} objects
|
|
25
|
+
this.currentIndex = -1;
|
|
26
|
+
this.include = true; // Whether this is an include or exclude filter
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Add a concept to the filter results
|
|
31
|
+
* @param {Object} concept - The concept object
|
|
32
|
+
* @param {number} rating - Search relevance rating (higher = more relevant)
|
|
33
|
+
*/
|
|
34
|
+
add(concept, rating = 0) {
|
|
35
|
+
this.concepts.push({ concept, rating });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sort concepts by rating (highest first)
|
|
40
|
+
*/
|
|
41
|
+
sort() {
|
|
42
|
+
this.concepts.sort((a, b) => b.rating - a.rating);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the total number of concepts in the filter
|
|
47
|
+
* @returns {number} Number of concepts
|
|
48
|
+
*/
|
|
49
|
+
size() {
|
|
50
|
+
return this.concepts.length;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if there are more concepts to iterate
|
|
55
|
+
* @returns {boolean} True if more concepts available
|
|
56
|
+
*/
|
|
57
|
+
hasMore() {
|
|
58
|
+
return this.currentIndex + 1 < this.concepts.length;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Move to next concept and return it
|
|
63
|
+
* @returns {Object|null} Next concept or null if exhausted
|
|
64
|
+
*/
|
|
65
|
+
next() {
|
|
66
|
+
if (this.hasMore()) {
|
|
67
|
+
this.currentIndex++;
|
|
68
|
+
return this.concepts[this.currentIndex].concept;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reset iterator to beginning
|
|
75
|
+
*/
|
|
76
|
+
reset() {
|
|
77
|
+
this.currentIndex = -1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Find a concept by code in the filter results
|
|
82
|
+
* @param {string} code - The code to find
|
|
83
|
+
* @returns {Object|null} The concept if found, null otherwise
|
|
84
|
+
*/
|
|
85
|
+
findConceptByCode(code) {
|
|
86
|
+
for (const item of this.concepts) {
|
|
87
|
+
if (item.concept.code === code) {
|
|
88
|
+
return item.concept;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if a concept is in the filter results
|
|
96
|
+
* @param {Object} concept - The concept to check
|
|
97
|
+
* @returns {boolean} True if concept is in results
|
|
98
|
+
*/
|
|
99
|
+
containsConcept(concept) {
|
|
100
|
+
return this.concepts.some(item => item.concept === concept);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
class FhirCodeSystemProvider extends CodeSystemProvider {
|
|
105
|
+
/**
|
|
106
|
+
* @param {CodeSystem} codeSystem - The primary CodeSystem
|
|
107
|
+
* @param {CodeSystem[]} supplements - Array of supplement CodeSystems
|
|
108
|
+
*/
|
|
109
|
+
constructor(opContext, codeSystem, supplements) {
|
|
110
|
+
super(opContext, supplements);
|
|
111
|
+
|
|
112
|
+
if (codeSystem.content == 'supplements') {
|
|
113
|
+
throw new Issue('error', 'invalid', null, 'CODESYSTEM_CS_NO_SUPPLEMENT', opContext.i18n.translate('CODESYSTEM_CS_NO_SUPPLEMENT', opContext.langs, codeSystem.vurl));
|
|
114
|
+
}
|
|
115
|
+
this.codeSystem = codeSystem;
|
|
116
|
+
this.hasHierarchyFlag = codeSystem.hasHierarchy();
|
|
117
|
+
|
|
118
|
+
// Parse the default language if specified
|
|
119
|
+
this.defaultLanguage = codeSystem.langCode();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============ Metadata Methods ============
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @returns {string} URI and version identifier for the code system
|
|
126
|
+
*/
|
|
127
|
+
name() {
|
|
128
|
+
return this.codeSystem.jsonObj.name || '';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @returns {string} URI for the code system
|
|
133
|
+
*/
|
|
134
|
+
system() {
|
|
135
|
+
return this.codeSystem.jsonObj.url || '';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @returns {string|null} Version for the code system
|
|
140
|
+
*/
|
|
141
|
+
version() {
|
|
142
|
+
return this.codeSystem.jsonObj.version || null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @returns {string|null} valueset for the code system
|
|
147
|
+
*/
|
|
148
|
+
valueSet() {
|
|
149
|
+
return this.codeSystem.jsonObj.valueSet || null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @returns {string} Default language for the code system
|
|
154
|
+
*/
|
|
155
|
+
defLang() {
|
|
156
|
+
return this.defaultLanguage?.toString() || 'en';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @returns {string} Content mode for the CodeSystem
|
|
161
|
+
*/
|
|
162
|
+
contentMode() {
|
|
163
|
+
return this.codeSystem.content;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @returns {string} Description for the code system
|
|
168
|
+
*/
|
|
169
|
+
description() {
|
|
170
|
+
return this.codeSystem.jsonObj.description || this.codeSystem.jsonObj.title || this.codeSystem.jsonObj.name || '';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @returns {string|null} Source package for the code system, if known
|
|
175
|
+
*/
|
|
176
|
+
sourcePackage() {
|
|
177
|
+
return this.codeSystem.sourcePackage;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @returns {number} Total number of concepts in the code system
|
|
182
|
+
*/
|
|
183
|
+
totalCount() {
|
|
184
|
+
return this.codeSystem.codeMap.size;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @returns {Object[]|null} Defined properties for the code system
|
|
189
|
+
*/
|
|
190
|
+
propertyDefinitions() {
|
|
191
|
+
return this.codeSystem.jsonObj.property || null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {Languages} languages - Language specification
|
|
196
|
+
* @returns {boolean} Whether any displays are available for the languages
|
|
197
|
+
*/
|
|
198
|
+
hasAnyDisplays(languages) {
|
|
199
|
+
const langs = this._ensureLanguages(languages);
|
|
200
|
+
|
|
201
|
+
// Check supplements first
|
|
202
|
+
if (this._hasAnySupplementDisplays(langs)) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check if we have English or if no specific languages requested
|
|
207
|
+
if (langs.isEnglishOrNothing()) {
|
|
208
|
+
return true; // We always have displays for concepts
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check if the CodeSystem's language matches requested languages
|
|
212
|
+
if (this.defaultLanguage) {
|
|
213
|
+
for (const requestedLang of langs) {
|
|
214
|
+
if (this.defaultLanguage.matchesForDisplay(requestedLang)) {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check concept designations for matching languages
|
|
221
|
+
for (const concept of this.codeSystem.getAllConcepts()) {
|
|
222
|
+
if (concept.designation && Array.isArray(concept.designation)) {
|
|
223
|
+
for (const designation of concept.designation) {
|
|
224
|
+
if (designation.language) {
|
|
225
|
+
const designationLang = new Language(designation.language);
|
|
226
|
+
for (const requestedLang of langs) {
|
|
227
|
+
if (designationLang.matchesForDisplay(requestedLang)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @returns {boolean} True if there's a hierarchy
|
|
241
|
+
*/
|
|
242
|
+
hasParents() {
|
|
243
|
+
return this.hasHierarchyFlag;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @param {string} checkVersion - First version
|
|
248
|
+
* @param {string} actualVersion - Second version
|
|
249
|
+
* @returns {boolean} True if v1 is more detailed than v2
|
|
250
|
+
*/
|
|
251
|
+
versionIsMoreDetailed(checkVersion, actualVersion) {
|
|
252
|
+
return VersionUtilities.versionMatchesByAlgorithm(checkVersion, actualVersion, this.versionAlgorithm());
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @returns {{status: string, standardsStatus: string, experimental: boolean}|null} Status information
|
|
257
|
+
*/
|
|
258
|
+
status() {
|
|
259
|
+
const cs = this.codeSystem.jsonObj;
|
|
260
|
+
if (!cs.status) return {};
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
status: cs.status,
|
|
264
|
+
standardsStatus: cs.extension?.find(ext =>
|
|
265
|
+
ext.url === 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status'
|
|
266
|
+
)?.valueCode || '',
|
|
267
|
+
experimental: cs.experimental || false
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @param {string|FhirCodeSystemProviderContext} context - Code or context
|
|
273
|
+
* @returns {Promise<string|null>} The correct code for the concept
|
|
274
|
+
*/
|
|
275
|
+
async code(context) {
|
|
276
|
+
|
|
277
|
+
const ctxt = await this.#ensureContext(context);
|
|
278
|
+
return ctxt ? ctxt.code : null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {string} code - The code to locate
|
|
283
|
+
* @returns {Promise<{context: FhirCodeSystemProviderContext|null, message: string|null}>} Locate result
|
|
284
|
+
*/
|
|
285
|
+
async locate(code) {
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
if (!code || typeof code !== 'string') {
|
|
289
|
+
return { context: null, message: 'Empty or invalid code' };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const concept = this.codeSystem.getConceptByCode(code);
|
|
293
|
+
if (concept) {
|
|
294
|
+
return {
|
|
295
|
+
context: new FhirCodeSystemProviderContext(concept.code, concept),
|
|
296
|
+
message: null
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
context: null,
|
|
302
|
+
message: undefined
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Helper method to ensure we have a proper context object
|
|
308
|
+
* @param {string|FhirCodeSystemProviderContext} context - Code or context
|
|
309
|
+
* @returns {Promise<FhirCodeSystemProviderContext|null>} Resolved context
|
|
310
|
+
* @private
|
|
311
|
+
*/
|
|
312
|
+
async #ensureContext(context) {
|
|
313
|
+
if (!context) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (typeof context === 'string') {
|
|
318
|
+
const result = await this.locate(context);
|
|
319
|
+
if (!result.context) {
|
|
320
|
+
throw new Error(result.message ? result.message : `Code '${context}' not found in CodeSystem '${this.system()}'`);
|
|
321
|
+
}
|
|
322
|
+
return result.context;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (context instanceof FhirCodeSystemProviderContext) {
|
|
326
|
+
return context;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
throw new Error("Unknown Type at #ensureContext: " + (typeof context));
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* @param {string|FhirCodeSystemProviderContext} context - Code or context
|
|
333
|
+
* @returns {Promise<string|null>} The best display given the languages in the operation context
|
|
334
|
+
*/
|
|
335
|
+
async display(context) {
|
|
336
|
+
|
|
337
|
+
const ctxt = await this.#ensureContext(context);
|
|
338
|
+
if (!ctxt) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Check supplements first
|
|
343
|
+
const supplementDisplay = this._displayFromSupplements(ctxt.code);
|
|
344
|
+
if (supplementDisplay) {
|
|
345
|
+
return supplementDisplay;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Use language-aware display logic
|
|
349
|
+
if (this.opContext.langs && !this.opContext.langs.isEnglishOrNothing()) {
|
|
350
|
+
// Try to find exact language match in designations
|
|
351
|
+
if (ctxt.concept.designation && Array.isArray(ctxt.concept.designation)) {
|
|
352
|
+
for (const lang of this.opContext.langs.languages) {
|
|
353
|
+
for (const designation of ctxt.concept.designation) {
|
|
354
|
+
if (designation.language) {
|
|
355
|
+
const designationLang = new Language(designation.language);
|
|
356
|
+
if (designationLang.matchesForDisplay(lang)) {
|
|
357
|
+
return designation.value.trim();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Check if the CodeSystem's language matches requested languages
|
|
365
|
+
if (this.defaultLanguage) {
|
|
366
|
+
for (const requestedLang of this.opContext.langs.languages) {
|
|
367
|
+
if (this.defaultLanguage.matchesForDisplay(requestedLang)) {
|
|
368
|
+
return ctxt.concept.display?.trim() || '';
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Default to the concept's display
|
|
375
|
+
return ctxt.concept.display?.trim() || '';
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* @param {string|FhirCodeSystemProviderContext} context - Code or context
|
|
380
|
+
* @returns {Promise<string|null>} The definition for the concept (if available)
|
|
381
|
+
*/
|
|
382
|
+
async definition(context) {
|
|
383
|
+
|
|
384
|
+
const ctxt = await this.#ensureContext(context);
|
|
385
|
+
return ctxt ? (ctxt.concept.definition || null) : null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* @param {string|FhirCodeSystemProviderContext} context - Code or context
|
|
390
|
+
* @returns {Promise<boolean>} If the concept is abstract
|
|
391
|
+
*/
|
|
392
|
+
async isAbstract(context) {
|
|
393
|
+
|
|
394
|
+
const ctxt = await this.#ensureContext(context);
|
|
395
|
+
if (!ctxt) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Check for abstract property
|
|
400
|
+
if (ctxt.concept.property && Array.isArray(ctxt.concept.property)) {
|
|
401
|
+
const abstractProp = ctxt.concept.property.find(p =>
|
|
402
|
+
p.code === 'abstract' ||
|
|
403
|
+
p.code === 'not-selectable' ||
|
|
404
|
+
p.code === 'notSelectable' ||
|
|
405
|
+
p.uri === 'http://hl7.org/fhir/concept-properties#notSelectable'
|
|
406
|
+
);
|
|
407
|
+
if (abstractProp && abstractProp.valueBoolean) {
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* @param {string|FhirCodeSystemProviderContext} context - Code or context
|
|
417
|
+
* @returns {Promise<boolean>} If the concept is inactive
|
|
418
|
+
*/
|
|
419
|
+
async isInactive(context) {
|
|
420
|
+
|
|
421
|
+
const ctxt = await this.#ensureContext(context);
|
|
422
|
+
if (!ctxt) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (ctxt.concept.property && Array.isArray(ctxt.concept.property)) {
|
|
427
|
+
for (const p of ctxt.concept.property) {
|
|
428
|
+
// Check inactive property with boolean value
|
|
429
|
+
if (p.code === 'inactive' && p.valueBoolean === true) {
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
// Check inactive property with code value 'true'
|
|
433
|
+
if (p.code === 'inactive' && p.valueCode === 'true') {
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
// Check status property for inactive or retired
|
|
437
|
+
if (p.code === 'status') {
|
|
438
|
+
const value = p.valueCode || p.valueString || (p.value && p.value.toString());
|
|
439
|
+
if (value === 'inactive' || value === 'retired') {
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Check standards-status extension for withdrawn
|
|
447
|
+
if (ctxt.concept.extension && Array.isArray(ctxt.concept.extension)) {
|
|
448
|
+
const standardsStatus = ctxt.concept.extension.find(e =>
|
|
449
|
+
e.url === 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status'
|
|
450
|
+
);
|
|
451
|
+
if (standardsStatus) {
|
|
452
|
+
const value = standardsStatus.valueCode || standardsStatus.valueString || '';
|
|
453
|
+
if (value.toLowerCase() === 'withdrawn') {
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* @param {string|FhirCodeSystemProviderContext} context - Code or context
|
|
463
|
+
* @returns {Promise<boolean>} If the concept is deprecated
|
|
464
|
+
*/
|
|
465
|
+
async isDeprecated(context) {
|
|
466
|
+
|
|
467
|
+
const ctxt = await this.#ensureContext(context);
|
|
468
|
+
if (!ctxt) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Check for deprecated property or status
|
|
473
|
+
if (ctxt.concept.property && Array.isArray(ctxt.concept.property)) {
|
|
474
|
+
const deprecatedProp = ctxt.concept.property.find(p =>
|
|
475
|
+
p.code === 'deprecated' ||
|
|
476
|
+
p.uri === 'http://hl7.org/fhir/concept-properties#deprecated'
|
|
477
|
+
);
|
|
478
|
+
if (deprecatedProp && deprecatedProp.valueBoolean) {
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Check status property
|
|
483
|
+
const statusProp = ctxt.concept.property.find(p =>
|
|
484
|
+
p.code === 'status' ||
|
|
485
|
+
p.uri === 'http://hl7.org/fhir/concept-properties#status'
|
|
486
|
+
);
|
|
487
|
+
if (statusProp && (statusProp.valueCode === 'deprecated' || statusProp.valueCode === 'retired')) {
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* @param {string|FhirCodeSystemProviderContext} context - Code or context
|
|
497
|
+
* @returns {Promise<string|null>} Status
|
|
498
|
+
*/
|
|
499
|
+
async getStatus(context) {
|
|
500
|
+
|
|
501
|
+
const ctxt = await this.#ensureContext(context);
|
|
502
|
+
if (!ctxt) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
for (let cp of ctxt.concept.property || []) {
|
|
507
|
+
if (cp.code === 'status' || cp.uri === 'http://hl7.org/fhir/concept-properties#status') {
|
|
508
|
+
return getValuePrimitive(cp);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Second pass: check various deprecation/inactive/retired patterns
|
|
513
|
+
for (let cp of ctxt.concept.property || []) {
|
|
514
|
+
if (cp.code === 'deprecated') {
|
|
515
|
+
if (cp.valueBoolean === true) return 'deprecated';
|
|
516
|
+
if (getValuePrimitive(cp) === 'true') return 'deprecated';
|
|
517
|
+
}
|
|
518
|
+
if (cp.code === 'deprecationDate' && cp.valueDateTime) {
|
|
519
|
+
if (new Date(cp.valueDateTime) < new Date()) return 'deprecated';
|
|
520
|
+
}
|
|
521
|
+
if (cp.code === 'inactive') {
|
|
522
|
+
if (cp.valueBoolean === true) return 'inactive';
|
|
523
|
+
if (getValuePrimitive(cp) === 'true') return 'inactive';
|
|
524
|
+
}
|
|
525
|
+
if (cp.code === 'retired') {
|
|
526
|
+
if (cp.valueBoolean === true) return 'retired';
|
|
527
|
+
if (getValuePrimitive(cp) === 'true') return 'retired';
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const ext = (ctxt.concept.extension || []).find(
|
|
531
|
+
e => e.url === 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status'
|
|
532
|
+
);
|
|
533
|
+
if (ext) {
|
|
534
|
+
return ext.valueCode || ext.valueString || '';
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* @param {string|FhirCodeSystemProviderContext} context - Code or context
|
|
542
|
+
* @returns {Promise<string|null>} Assigned itemWeight - if there is one
|
|
543
|
+
*/
|
|
544
|
+
async itemWeight(context) {
|
|
545
|
+
|
|
546
|
+
const ctxt = await this.#ensureContext(context);
|
|
547
|
+
if (!ctxt) {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Check for itemWeight extension
|
|
552
|
+
if (ctxt.concept.extension && Array.isArray(ctxt.concept.extension)) {
|
|
553
|
+
const itemWeightExt = ctxt.concept.extension.find(ext =>
|
|
554
|
+
ext.url === 'http://hl7.org/fhir/StructureDefinition/itemWeight'
|
|
555
|
+
);
|
|
556
|
+
if (itemWeightExt && itemWeightExt.valueDecimal !== undefined) {
|
|
557
|
+
return itemWeightExt.valueDecimal.toString();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Check in supplements
|
|
562
|
+
if (this.supplements) {
|
|
563
|
+
for (const supplement of this.supplements) {
|
|
564
|
+
const supplementConcept = supplement.getConceptByCode(ctxt.code);
|
|
565
|
+
if (supplementConcept && supplementConcept.extension && Array.isArray(supplementConcept.extension)) {
|
|
566
|
+
const itemWeightExt = supplementConcept.extension.find(ext =>
|
|
567
|
+
ext.url === 'http://hl7.org/fhir/StructureDefinition/itemWeight'
|
|
568
|
+
);
|
|
569
|
+
if (itemWeightExt && itemWeightExt.valueDecimal !== undefined) {
|
|
570
|
+
return itemWeightExt.valueDecimal;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* @param {string|FhirCodeSystemProviderContext} context - Code or context
|
|
581
|
+
* @param {ConceptDesignations} designation list
|
|
582
|
+
* @returns {Promise<Designation[]|null>} Whatever designations exist (in all languages)
|
|
583
|
+
*/
|
|
584
|
+
async designations(context, displays) {
|
|
585
|
+
|
|
586
|
+
const ctxt = await this.#ensureContext(context);
|
|
587
|
+
if (!ctxt) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Add main display as a designation
|
|
592
|
+
if (ctxt.concept.display) {
|
|
593
|
+
const displayLang = this.defaultLanguage ? this.defaultLanguage.toString() : null; // 'en';
|
|
594
|
+
displays.addDesignation(true, 'active', displayLang, CodeSystem.makeUseForDisplay(), ctxt.concept.display);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Add concept designations
|
|
598
|
+
if (ctxt.concept.designation && Array.isArray(ctxt.concept.designation)) {
|
|
599
|
+
for (const designation of ctxt.concept.designation) {
|
|
600
|
+
let status = Extensions.readString(designation, "http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status");
|
|
601
|
+
displays.addDesignation(false, status || 'active',
|
|
602
|
+
designation.language || '',
|
|
603
|
+
designation.use || null,
|
|
604
|
+
designation.value,
|
|
605
|
+
designation.extension?.length > 0 ? designation.extension : []
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Add supplement designations
|
|
611
|
+
this._listSupplementDesignations(ctxt.code, displays);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* @param {string|FhirCodeSystemProviderContext} context - Code or context
|
|
616
|
+
* @returns {Promise<Object[]|null>} Extensions, if any
|
|
617
|
+
*/
|
|
618
|
+
async extensions(context) {
|
|
619
|
+
|
|
620
|
+
const ctxt = await this.#ensureContext(context);
|
|
621
|
+
if (!ctxt) {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const extensions = [];
|
|
626
|
+
|
|
627
|
+
// Add extensions from main concept
|
|
628
|
+
if (ctxt.concept.extension && Array.isArray(ctxt.concept.extension)) {
|
|
629
|
+
extensions.push(...ctxt.concept.extension);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Add extensions from supplements
|
|
633
|
+
if (this.supplements) {
|
|
634
|
+
for (const supplement of this.supplements) {
|
|
635
|
+
const supplementConcept = supplement.getConceptByCode(ctxt.code);
|
|
636
|
+
if (supplementConcept && supplementConcept.extension && Array.isArray(supplementConcept.extension)) {
|
|
637
|
+
extensions.push(...supplementConcept.extension);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return extensions.length > 0 ? extensions : null;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* @param {string|FhirCodeSystemProviderContext} context - Code or context
|
|
647
|
+
* @returns {Promise<Object[]|null>} Properties, if any
|
|
648
|
+
*/
|
|
649
|
+
async properties(context) {
|
|
650
|
+
|
|
651
|
+
const ctxt = await this.#ensureContext(context);
|
|
652
|
+
if (!ctxt) {
|
|
653
|
+
return [];
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const properties = [];
|
|
657
|
+
|
|
658
|
+
// Add properties from main concept
|
|
659
|
+
if (ctxt.concept.property && Array.isArray(ctxt.concept.property)) {
|
|
660
|
+
properties.push(...ctxt.concept.property);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Add properties from supplements
|
|
664
|
+
if (this.supplements) {
|
|
665
|
+
for (const supplement of this.supplements) {
|
|
666
|
+
const supplementConcept = supplement.getConceptByCode(ctxt.code);
|
|
667
|
+
if (supplementConcept && supplementConcept.property && Array.isArray(supplementConcept.property)) {
|
|
668
|
+
properties.push(...supplementConcept.property);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return properties;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* @param {string|FhirCodeSystemProviderContext} context - Code or context
|
|
678
|
+
* @returns {Promise<string|null>} Parent, if there is one
|
|
679
|
+
*/
|
|
680
|
+
async parent(context) {
|
|
681
|
+
|
|
682
|
+
const ctxt = await this.#ensureContext(context);
|
|
683
|
+
if (!ctxt) {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Get parents from CodeSystem hierarchy maps
|
|
688
|
+
const parents = this.codeSystem.getParents(ctxt.code);
|
|
689
|
+
return parents.length > 0 ? parents[0] : null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* @param {string|FhirCodeSystemProviderContext} a - First code or context
|
|
694
|
+
* @param {string|FhirCodeSystemProviderContext} b - Second code or context
|
|
695
|
+
* @returns {Promise<boolean>} True if they're the same
|
|
696
|
+
*/
|
|
697
|
+
async sameConcept(a, b) {
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
const ctxtA = await this.#ensureContext(a);
|
|
701
|
+
const ctxtB = await this.#ensureContext(b);
|
|
702
|
+
|
|
703
|
+
if (!ctxtA || !ctxtB) {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return ctxtA.code === ctxtB.code;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* @param {string} code - The code to locate
|
|
712
|
+
* @param {string} parent - The parent code
|
|
713
|
+
* @param {boolean} disallowSelf - Whether to disallow the code being the same as parent
|
|
714
|
+
* @returns {Promise<{context: FhirCodeSystemProviderContext|null, message: string|null}>} Locate result
|
|
715
|
+
*/
|
|
716
|
+
async locateIsA(code, parent, disallowSelf = false) {
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
if (!this.hasParents()) {
|
|
720
|
+
return {
|
|
721
|
+
context: null,
|
|
722
|
+
message: `The CodeSystem ${this.name()} does not have parents`
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// First check if both codes exist
|
|
727
|
+
const codeResult = await this.locate(code);
|
|
728
|
+
if (!codeResult.context) {
|
|
729
|
+
return codeResult;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const parentResult = await this.locate(parent);
|
|
733
|
+
if (!parentResult.context) {
|
|
734
|
+
return {
|
|
735
|
+
context: null,
|
|
736
|
+
message: `Parent code '${parent}' not found in CodeSystem '${this.system()}'`
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Check if code is same as parent
|
|
741
|
+
if (code === parent) {
|
|
742
|
+
if (disallowSelf) {
|
|
743
|
+
return {
|
|
744
|
+
context: null,
|
|
745
|
+
message: `Code '${code}' cannot be the same as its parent`
|
|
746
|
+
};
|
|
747
|
+
} else {
|
|
748
|
+
return codeResult; // Return the code itself
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Check if code is a descendant of parent
|
|
753
|
+
const ancestors = this.codeSystem.getAncestors(code);
|
|
754
|
+
if (ancestors.includes(parent)) {
|
|
755
|
+
return codeResult;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return {
|
|
759
|
+
context: null,
|
|
760
|
+
message: `Code '${code}' is not a descendant of '${parent}'`
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* @param {string} codeA - First code
|
|
766
|
+
* @param {string} codeB - Second code
|
|
767
|
+
* @returns {Promise<string>} 'subsumes', 'subsumed-by', 'equivalent', or 'not-subsumed'
|
|
768
|
+
*/
|
|
769
|
+
async subsumesTest(codeA, codeB) {
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
// Check if both codes exist
|
|
773
|
+
const resultA = await this.locate(codeA);
|
|
774
|
+
if (!resultA.context) {
|
|
775
|
+
throw new Error(`Unknown Code "${codeA}"`);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const resultB = await this.locate(codeB);
|
|
779
|
+
if (!resultB.context) {
|
|
780
|
+
throw new Error(`Unknown Code "${codeB}"`);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Check if they're the same
|
|
784
|
+
if (codeA === codeB) {
|
|
785
|
+
return 'equivalent';
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// If no hierarchy, codes can't subsume each other
|
|
789
|
+
if (!this.hasParents()) {
|
|
790
|
+
return 'not-subsumed';
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Check if A subsumes B (B is descendant of A)
|
|
794
|
+
if (this.codeSystem.isDescendantOf(codeB, codeA)) {
|
|
795
|
+
return 'subsumes';
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Check if B subsumes A (A is descendant of B)
|
|
799
|
+
if (this.codeSystem.isDescendantOf(codeA, codeB)) {
|
|
800
|
+
return 'subsumed-by';
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return 'not-subsumed';
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* @param {string|FhirCodeSystemProviderContext} context - Code or context to iterate from
|
|
808
|
+
* @returns {Promise<Object|null>} A handle that can be passed to nextContext (or null if can't be iterated)
|
|
809
|
+
*/
|
|
810
|
+
async iterator(context) {
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
if (!context) {
|
|
814
|
+
const allCodes = this.codeSystem.getRootConcepts();
|
|
815
|
+
return {
|
|
816
|
+
type: 'all',
|
|
817
|
+
codes: allCodes,
|
|
818
|
+
current: 0,
|
|
819
|
+
total: allCodes.length
|
|
820
|
+
};
|
|
821
|
+
} else {
|
|
822
|
+
const ctxt = await this.#ensureContext(context);
|
|
823
|
+
if (!ctxt) {
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Iterate children of the specified concept
|
|
828
|
+
const children = this.codeSystem.getChildren(ctxt.code);
|
|
829
|
+
return {
|
|
830
|
+
type: 'children',
|
|
831
|
+
parentCode: ctxt.code,
|
|
832
|
+
codes: children,
|
|
833
|
+
current: 0,
|
|
834
|
+
total: children.length
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* @returns {Promise<Object|null>} A handle that can be passed to nextContext (or null if can't be iterated)
|
|
841
|
+
*/
|
|
842
|
+
async iteratorAll() {
|
|
843
|
+
const allCodes = this.codeSystem.getAllCodes();
|
|
844
|
+
return {
|
|
845
|
+
type: 'all',
|
|
846
|
+
codes: allCodes,
|
|
847
|
+
current: 0,
|
|
848
|
+
total: allCodes.length
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* @param {Object} iteratorContext - Iterator context from iterator()
|
|
854
|
+
* @returns {Promise<FhirCodeSystemProviderContext|null>} The next concept, or null
|
|
855
|
+
*/
|
|
856
|
+
async nextContext(iteratorContext) {
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
if (!iteratorContext || iteratorContext.current >= iteratorContext.total) {
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const code = iteratorContext.codes[iteratorContext.current];
|
|
864
|
+
iteratorContext.current++;
|
|
865
|
+
|
|
866
|
+
// Get the concept for this code
|
|
867
|
+
const concept = this.codeSystem.getConceptByCode(code);
|
|
868
|
+
if (!concept) {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return new FhirCodeSystemProviderContext(code, concept);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* @param {FhirCodeSystemProviderContext} ctxt - The context to add properties for
|
|
877
|
+
* @param {string[]} props - The properties requested
|
|
878
|
+
* @param {Object} params - The parameters response to add to
|
|
879
|
+
*/
|
|
880
|
+
async extendLookup(ctxt, props, params) {
|
|
881
|
+
validateArrayParameter(props, 'props', String);
|
|
882
|
+
validateArrayParameter(params, 'params', Object);
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
if (!ctxt || !(ctxt instanceof FhirCodeSystemProviderContext)) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Set abstract status
|
|
890
|
+
if (!params.find(p => p.name == "abstract") && await this.isAbstract(ctxt)) {
|
|
891
|
+
params.push({ name: 'property', part: [ { name: 'code', valueCode: 'abstract' }, { name: 'value', valueBoolean: true } ]});
|
|
892
|
+
}
|
|
893
|
+
// Add properties if requested (or by default)
|
|
894
|
+
if (!props || props.length === 0 || props.includes('*') || props.includes('property')) {
|
|
895
|
+
const properties = await this.properties(ctxt);
|
|
896
|
+
if (properties) {
|
|
897
|
+
for (const property of properties) {
|
|
898
|
+
let parts = [];
|
|
899
|
+
parts.push({ name: 'code', valueCode: property.code });
|
|
900
|
+
|
|
901
|
+
// Add the appropriate value based on the property type
|
|
902
|
+
if (property.valueCode) {
|
|
903
|
+
parts.push({ name: 'value', valueCode: property.valueCode });
|
|
904
|
+
} else if (property.valueString) {
|
|
905
|
+
parts.push({ name: 'value', valueString: property.valueString });
|
|
906
|
+
} else if (property.valueInteger !== undefined) {
|
|
907
|
+
parts.push({ name: 'value', valueInteger: property.valueInteger });
|
|
908
|
+
} else if (property.valueBoolean !== undefined) {
|
|
909
|
+
parts.push({ name: 'value', valueBoolean: property.valueBoolean });
|
|
910
|
+
} else if (property.valueDateTime) {
|
|
911
|
+
parts.push({ name: 'value', valueDateTime: property.valueDateTime });
|
|
912
|
+
} else if (property.valueDecimal !== undefined) {
|
|
913
|
+
parts.push({ name: 'value', valueDecimal: property.valueDecimal });
|
|
914
|
+
} else if (property.valueCoding) {
|
|
915
|
+
parts.push({ name: 'value', valueCoding: property.valueCoding });
|
|
916
|
+
}
|
|
917
|
+
params.push({ name: 'property', part: [...parts]});
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Add parent if requested and exists
|
|
923
|
+
if (!props || props.length === 0 || props.includes('*') || props.includes('parent')) {
|
|
924
|
+
const parentCode = await this.parent(ctxt);
|
|
925
|
+
if (parentCode) {
|
|
926
|
+
let parts = [];
|
|
927
|
+
parts.push({ name: 'code', valueCode: 'parent' });
|
|
928
|
+
parts.push({ name: 'value', valueCode: parentCode });
|
|
929
|
+
parts.push({ name: 'description', valueString: await this.display(parentCode) });
|
|
930
|
+
params.push({ name: 'property', part : [...parts]});
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Add children if requested
|
|
935
|
+
if (!props || props.length === 0 || props.includes('*') || props.includes('child')) {
|
|
936
|
+
const children = this.codeSystem.getChildren(ctxt.code);
|
|
937
|
+
if (children.length > 0) {
|
|
938
|
+
for (const childCode of children) {
|
|
939
|
+
let parts = [];
|
|
940
|
+
parts.push({ name: 'code', valueCode: 'child' });
|
|
941
|
+
parts.push({ name: 'value', valueCode: childCode });
|
|
942
|
+
parts.push({ name: 'description', valueString: await this.display(childCode) });
|
|
943
|
+
params.push({ name: 'property', part : [...parts]});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* @param {boolean} iterate - True if results will be iterated
|
|
951
|
+
* @returns {FilterExecutionContext} Filter context
|
|
952
|
+
*/
|
|
953
|
+
async getPrepContext(iterate) {
|
|
954
|
+
|
|
955
|
+
return new FilterExecutionContext(iterate);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* @param {FilterExecutionContext} filterContext - Filter context
|
|
960
|
+
* @returns {boolean} True if filters are not closed (infinite results possible)
|
|
961
|
+
*/
|
|
962
|
+
// eslint-disable-next-line no-unused-vars
|
|
963
|
+
async filtersNotClosed(filterContext) {
|
|
964
|
+
|
|
965
|
+
return false; // FHIR CodeSystems are typically closed/finite
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Determines if a specific filter is supported
|
|
970
|
+
* @param {string} prop - Property name
|
|
971
|
+
* @param {string} op - Filter operator (=, is-a, descendent-of, etc.)
|
|
972
|
+
* @param {string} value - Filter value
|
|
973
|
+
* @returns {Promise<boolean>} True if filter is supported
|
|
974
|
+
*/
|
|
975
|
+
async doesFilter(prop, op, value) {
|
|
976
|
+
validateOptionalParameter(value, "value", String);
|
|
977
|
+
if (!value) {
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
// Supported hierarchy filters
|
|
981
|
+
if ((prop === 'concept' || prop === 'code') &&
|
|
982
|
+
['is-a', 'descendent-of', 'is-not-a', 'in', '=', 'regex'].includes(op)) {
|
|
983
|
+
return true;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Child existence filter
|
|
987
|
+
if (prop === 'child' && op === 'exists') {
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Property-based filters
|
|
992
|
+
const propertyDefs = this.propertyDefinitions();
|
|
993
|
+
if (propertyDefs) {
|
|
994
|
+
const hasProperty = propertyDefs.some(p => p.code === prop);
|
|
995
|
+
if (hasProperty && ['=', 'in', 'not-in', 'regex'].includes(op)) {
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Known special properties
|
|
1001
|
+
const knownProperties = ['notSelectable', 'status', 'inactive', 'deprecated'];
|
|
1002
|
+
if (knownProperties.includes(prop) && ['=', 'in', 'not-in'].includes(op)) {
|
|
1003
|
+
return true;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Execute filter preparation - returns array of filter contexts
|
|
1011
|
+
* @param {FilterExecutionContext} filterContext - Filter context
|
|
1012
|
+
* @returns {Promise<Array>} Array of filter result sets
|
|
1013
|
+
*/
|
|
1014
|
+
async executeFilters(filterContext) {
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
// Return the accumulated filters from the context
|
|
1018
|
+
return filterContext.filters || [];
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Get the size of a filter result set
|
|
1023
|
+
* @param {FilterExecutionContext} filterContext - Filter context
|
|
1024
|
+
* @param {FhirCodeSystemProviderFilterContext} set - Filter result set
|
|
1025
|
+
* @returns {Promise<number>} Number of concepts in the set
|
|
1026
|
+
*/
|
|
1027
|
+
async filterSize(filterContext, set) {
|
|
1028
|
+
|
|
1029
|
+
return set ? set.size() : 0;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Check if there are more results in the filter set iterator
|
|
1034
|
+
* @param {FilterExecutionContext} filterContext - Filter context
|
|
1035
|
+
* @param {FhirCodeSystemProviderFilterContext} set - Filter result set
|
|
1036
|
+
* @returns {Promise<boolean>} True if more results available
|
|
1037
|
+
*/
|
|
1038
|
+
async filterMore(filterContext, set) {
|
|
1039
|
+
|
|
1040
|
+
if (!set) return false;
|
|
1041
|
+
return set.hasMore();
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Get the current concept from the filter set iterator
|
|
1046
|
+
* @param {FilterExecutionContext} filterContext - Filter context
|
|
1047
|
+
* @param {FhirCodeSystemProviderFilterContext} set - Filter result set
|
|
1048
|
+
* @returns {Promise<FhirCodeSystemProviderContext|null>} Current concept context
|
|
1049
|
+
*/
|
|
1050
|
+
async filterConcept(filterContext, set) {
|
|
1051
|
+
|
|
1052
|
+
if (!set) return null;
|
|
1053
|
+
|
|
1054
|
+
const concept = set.next();
|
|
1055
|
+
return concept ? new FhirCodeSystemProviderContext(concept.code, concept) : null;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Find a specific code in the filter results
|
|
1060
|
+
* @param {FilterExecutionContext} filterContext - Filter context
|
|
1061
|
+
* @param {FhirCodeSystemProviderFilterContext} set - Filter result set
|
|
1062
|
+
* @param {string} code - Code to find
|
|
1063
|
+
* @returns {Promise<FhirCodeSystemProviderContext|string>} Context if found, error message if not
|
|
1064
|
+
*/
|
|
1065
|
+
async filterLocate(filterContext, set, code) {
|
|
1066
|
+
|
|
1067
|
+
if (!set) {
|
|
1068
|
+
return `Code '${code}' not found: no filter results`;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const concept = set.findConceptByCode(code);
|
|
1072
|
+
if (concept) {
|
|
1073
|
+
return new FhirCodeSystemProviderContext(code, concept);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
return null; // `Code '${code}' not found in filter results`;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Check if a concept is in the filter results
|
|
1081
|
+
* @param {FilterExecutionContext} filterContext - Filter context
|
|
1082
|
+
* @param {FhirCodeSystemProviderFilterContext} set - Filter result set
|
|
1083
|
+
* @param {FhirCodeSystemProviderContext} concept - Concept to check
|
|
1084
|
+
* @returns {Promise<boolean|string>} True if found, error message if not
|
|
1085
|
+
*/
|
|
1086
|
+
async filterCheck(filterContext, set, concept) {
|
|
1087
|
+
|
|
1088
|
+
if (!set || !concept) {
|
|
1089
|
+
return 'Invalid filter set or concept';
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const found = set.containsConcept(concept.concept);
|
|
1093
|
+
return found ? true : `Concept '${concept.code}' not found in filter results`;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Clean up filter resources
|
|
1098
|
+
* @param {FilterExecutionContext} filterContext - Filter context
|
|
1099
|
+
*/
|
|
1100
|
+
async filterFinish(filterContext) {
|
|
1101
|
+
|
|
1102
|
+
// Clear any cached data
|
|
1103
|
+
if (filterContext.filters) {
|
|
1104
|
+
filterContext.filters.forEach(filter => {
|
|
1105
|
+
if (filter.reset) {
|
|
1106
|
+
filter.reset();
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
filterContext.filters.length = 0;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Execute text-based search filter
|
|
1114
|
+
* @param {FilterExecutionContext} filterContext - Filter context
|
|
1115
|
+
* @param {string} filter - Search text
|
|
1116
|
+
* @param {boolean} sort - Whether to sort results by relevance
|
|
1117
|
+
* @returns {Promise<FhirCodeSystemProviderFilterContext>} Filter results
|
|
1118
|
+
*/
|
|
1119
|
+
async searchFilter(filterContext, filter, sort) {
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
const results = new FhirCodeSystemProviderFilterContext();
|
|
1123
|
+
const searchTerm = filter.toLowerCase();
|
|
1124
|
+
|
|
1125
|
+
// Search through all concepts
|
|
1126
|
+
const allConcepts = this.codeSystem.getAllConcepts();
|
|
1127
|
+
|
|
1128
|
+
for (const concept of allConcepts) {
|
|
1129
|
+
const rating = this._calculateSearchRating(concept, searchTerm);
|
|
1130
|
+
if (rating > 0) {
|
|
1131
|
+
results.add(concept, rating);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (sort) {
|
|
1136
|
+
results.sort();
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Add to filter context
|
|
1140
|
+
if (!filterContext.filters) {
|
|
1141
|
+
filterContext.filters = [];
|
|
1142
|
+
}
|
|
1143
|
+
filterContext.filters.push(results);
|
|
1144
|
+
|
|
1145
|
+
return results;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Calculate search relevance rating for a concept
|
|
1150
|
+
* @param {Object} concept - The concept to rate
|
|
1151
|
+
* @param {string} searchTerm - The search term (lowercase)
|
|
1152
|
+
* @returns {number} Rating (0 = no match, higher = better match)
|
|
1153
|
+
* @private
|
|
1154
|
+
*/
|
|
1155
|
+
_calculateSearchRating(concept, searchTerm) {
|
|
1156
|
+
let rating = 0;
|
|
1157
|
+
|
|
1158
|
+
// Exact matches get highest rating
|
|
1159
|
+
if (concept.code.toLowerCase() === searchTerm) {
|
|
1160
|
+
rating = 100;
|
|
1161
|
+
} else if (concept.display && concept.display.toLowerCase() === searchTerm) {
|
|
1162
|
+
rating = 100;
|
|
1163
|
+
}
|
|
1164
|
+
// Code starts with search term
|
|
1165
|
+
else if (concept.code.toLowerCase().startsWith(searchTerm)) {
|
|
1166
|
+
rating = 90;
|
|
1167
|
+
}
|
|
1168
|
+
// Display starts with search term
|
|
1169
|
+
else if (concept.display && concept.display.toLowerCase().startsWith(searchTerm)) {
|
|
1170
|
+
const lengthRatio = searchTerm.length / concept.display.length;
|
|
1171
|
+
rating = 80 + (10 * lengthRatio);
|
|
1172
|
+
}
|
|
1173
|
+
// Code contains search term
|
|
1174
|
+
else if (concept.code.toLowerCase().includes(searchTerm)) {
|
|
1175
|
+
rating = 60;
|
|
1176
|
+
}
|
|
1177
|
+
// Display contains search term
|
|
1178
|
+
else if (concept.display && concept.display.toLowerCase().includes(searchTerm)) {
|
|
1179
|
+
rating = 50;
|
|
1180
|
+
}
|
|
1181
|
+
// Definition contains search term
|
|
1182
|
+
else if (concept.definition && concept.definition.toLowerCase().includes(searchTerm)) {
|
|
1183
|
+
rating = 30;
|
|
1184
|
+
}
|
|
1185
|
+
// Check designations
|
|
1186
|
+
else if (concept.designation && Array.isArray(concept.designation)) {
|
|
1187
|
+
for (const designation of concept.designation) {
|
|
1188
|
+
if (designation.value && designation.value.toLowerCase().includes(searchTerm)) {
|
|
1189
|
+
rating = 40;
|
|
1190
|
+
break;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
return rating;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Execute a value set filter
|
|
1200
|
+
* @param {FilterExecutionContext} filterContext - Filter context
|
|
1201
|
+
* @param {string} prop - Property name to filter on
|
|
1202
|
+
* @param {string} op - Filter operator
|
|
1203
|
+
* @param {string} value - Filter value
|
|
1204
|
+
* @returns {Promise<FhirCodeSystemProviderFilterContext>} Filter results
|
|
1205
|
+
*/
|
|
1206
|
+
async filter(filterContext, prop, op, value) {
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
let results = null;
|
|
1210
|
+
|
|
1211
|
+
// Handle concept/code hierarchy filters
|
|
1212
|
+
if ((prop === 'concept' || prop === 'code')) {
|
|
1213
|
+
results = await this._handleConceptFilter(filterContext, op, value);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Handle child existence filter
|
|
1217
|
+
if (prop === 'child' && op === 'exists') {
|
|
1218
|
+
results = await this._handleChildExistsFilter(filterContext, value);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Handle property-based filters
|
|
1222
|
+
const propertyDefs = this.propertyDefinitions();
|
|
1223
|
+
if (propertyDefs) {
|
|
1224
|
+
const propertyDef = propertyDefs.find(p => p.code === prop);
|
|
1225
|
+
if (propertyDef) {
|
|
1226
|
+
results = await this._handlePropertyFilter(filterContext, propertyDef, op, value);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Handle known special properties
|
|
1231
|
+
const knownProperties = ['notSelectable', 'status', 'inactive', 'deprecated'];
|
|
1232
|
+
if (knownProperties.includes(prop)) {
|
|
1233
|
+
results = await this._handleKnownPropertyFilter(filterContext, prop, op, value);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (!results) {
|
|
1237
|
+
throw new Error(`The filter ${prop} ${op} ${value} was not understood`)
|
|
1238
|
+
}
|
|
1239
|
+
// Add to filter context
|
|
1240
|
+
if (!filterContext.filters) {
|
|
1241
|
+
filterContext.filters = [];
|
|
1242
|
+
}
|
|
1243
|
+
filterContext.filters.push(results);
|
|
1244
|
+
|
|
1245
|
+
return results;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Handle concept/code filters (is-a, descendent-of, etc.)
|
|
1250
|
+
* @param {FilterExecutionContext} filterContext - Filter context
|
|
1251
|
+
* @param {string} op - Filter operator
|
|
1252
|
+
* @param {string} value - Filter value (code)
|
|
1253
|
+
* @returns {Promise<FhirCodeSystemProviderFilterContext>} Filter results
|
|
1254
|
+
* @private
|
|
1255
|
+
*/
|
|
1256
|
+
async _handleConceptFilter(filterContext, op, value) {
|
|
1257
|
+
const results = new FhirCodeSystemProviderFilterContext();
|
|
1258
|
+
|
|
1259
|
+
if (op === 'is-a' || op === 'descendent-of') {
|
|
1260
|
+
// Find all descendants of the specified code
|
|
1261
|
+
const includeRoot = (op === 'is-a');
|
|
1262
|
+
await this._addDescendants(results, value, includeRoot);
|
|
1263
|
+
}
|
|
1264
|
+
else if (op === 'is-not-a') {
|
|
1265
|
+
// Find all concepts that are NOT descendants of the specified code
|
|
1266
|
+
const excludeDescendants = this.codeSystem.getDescendants(value);
|
|
1267
|
+
const excludeSet = new Set([value, ...excludeDescendants]);
|
|
1268
|
+
|
|
1269
|
+
const allCodes = this.codeSystem.getAllCodes();
|
|
1270
|
+
for (const code of allCodes) {
|
|
1271
|
+
if (!excludeSet.has(code)) {
|
|
1272
|
+
const concept = this.codeSystem.getConceptByCode(code);
|
|
1273
|
+
if (concept) {
|
|
1274
|
+
results.add(concept, 0);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
else if (op === 'in') {
|
|
1280
|
+
// Value is comma-separated list of codes
|
|
1281
|
+
const codes = value.split(',').map(c => c.trim());
|
|
1282
|
+
for (const code of codes) {
|
|
1283
|
+
const concept = this.codeSystem.getConceptByCode(code);
|
|
1284
|
+
if (concept) {
|
|
1285
|
+
results.add(concept, 0);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
else if (op === '=') {
|
|
1290
|
+
// Exact match
|
|
1291
|
+
const concept = this.codeSystem.getConceptByCode(value);
|
|
1292
|
+
if (concept) {
|
|
1293
|
+
results.add(concept, 0);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
else if (op === 'regex') {
|
|
1297
|
+
// Regular expression match
|
|
1298
|
+
try {
|
|
1299
|
+
const regex = new RegExp('^' + value + '$');
|
|
1300
|
+
const allCodes = this.codeSystem.getAllCodes();
|
|
1301
|
+
for (const code of allCodes) {
|
|
1302
|
+
if (regex.test(code)) {
|
|
1303
|
+
const concept = this.codeSystem.getConceptByCode(code);
|
|
1304
|
+
if (concept) {
|
|
1305
|
+
results.add(concept, 0);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
throw new Error(`Invalid regex pattern: ${value}`);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
return results;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* Add descendants of a code to the results
|
|
1319
|
+
* @param {FhirCodeSystemProviderFilterContext} results - Results to add to
|
|
1320
|
+
* @param {string} ancestorCode - The ancestor code
|
|
1321
|
+
* @param {boolean} includeRoot - Whether to include the root code itself
|
|
1322
|
+
* @private
|
|
1323
|
+
*/
|
|
1324
|
+
async _addDescendants(results, ancestorCode, includeRoot) {
|
|
1325
|
+
const concept = this.codeSystem.getConceptByCode(ancestorCode);
|
|
1326
|
+
if (concept) {
|
|
1327
|
+
if (includeRoot) {
|
|
1328
|
+
results.add(concept, 0);
|
|
1329
|
+
}
|
|
1330
|
+
const descendants = this.codeSystem.getDescendants(ancestorCode);
|
|
1331
|
+
for (const code of descendants) {
|
|
1332
|
+
if (code !== ancestorCode) {
|
|
1333
|
+
const concept = this.codeSystem.getConceptByCode(code);
|
|
1334
|
+
if (concept) {
|
|
1335
|
+
results.add(concept, 0);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Handle child exists filter
|
|
1344
|
+
* @param {FilterExecutionContext} filterContext - Filter context
|
|
1345
|
+
* @param {string} value - 'true' or 'false'
|
|
1346
|
+
* @returns {Promise<FhirCodeSystemProviderFilterContext>} Filter results
|
|
1347
|
+
* @private
|
|
1348
|
+
*/
|
|
1349
|
+
async _handleChildExistsFilter(filterContext, value) {
|
|
1350
|
+
const results = new FhirCodeSystemProviderFilterContext();
|
|
1351
|
+
const wantChildren = (value === 'true');
|
|
1352
|
+
|
|
1353
|
+
const allCodes = this.codeSystem.getAllCodes();
|
|
1354
|
+
for (const code of allCodes) {
|
|
1355
|
+
const hasChildren = this.codeSystem.getChildren(code).length > 0;
|
|
1356
|
+
if (hasChildren === wantChildren) {
|
|
1357
|
+
const concept = this.codeSystem.getConceptByCode(code);
|
|
1358
|
+
if (concept) {
|
|
1359
|
+
results.add(concept, 0);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
return results;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Handle property-based filter
|
|
1369
|
+
* @param {FilterExecutionContext} filterContext - Filter context
|
|
1370
|
+
* @param {Object} propertyDef - Property definition
|
|
1371
|
+
* @param {string} op - Filter operator
|
|
1372
|
+
* @param {string} value - Filter value
|
|
1373
|
+
* @returns {Promise<FhirCodeSystemProviderFilterContext>} Filter results
|
|
1374
|
+
* @private
|
|
1375
|
+
*/
|
|
1376
|
+
async _handlePropertyFilter(filterContext, propertyDef, op, value) {
|
|
1377
|
+
const results = new FhirCodeSystemProviderFilterContext();
|
|
1378
|
+
const allConcepts = this.codeSystem.getAllConcepts();
|
|
1379
|
+
|
|
1380
|
+
for (const concept of allConcepts) {
|
|
1381
|
+
if (this._conceptMatchesPropertyFilter(concept, propertyDef, op, value)) {
|
|
1382
|
+
results.add(concept, 0);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
return results;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Check if concept matches property filter
|
|
1391
|
+
* @param {Object} concept - The concept to check
|
|
1392
|
+
* @param {Object} propertyDef - Property definition
|
|
1393
|
+
* @param {string} op - Filter operator
|
|
1394
|
+
* @param {string} value - Filter value
|
|
1395
|
+
* @returns {boolean} True if concept matches filter
|
|
1396
|
+
* @private
|
|
1397
|
+
*/
|
|
1398
|
+
_conceptMatchesPropertyFilter(concept, propertyDef, op, value) {
|
|
1399
|
+
if (!concept.property || !Array.isArray(concept.property)) {
|
|
1400
|
+
return false;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const properties = concept.property.filter(p => p.code === propertyDef.code);
|
|
1404
|
+
|
|
1405
|
+
if (op === '=') {
|
|
1406
|
+
return properties.some(p => this._getPropertyValue(p) === value);
|
|
1407
|
+
}
|
|
1408
|
+
else if (op === 'in') {
|
|
1409
|
+
const values = value.split(',').map(v => v.trim());
|
|
1410
|
+
return properties.some(p => values.includes(this._getPropertyValue(p)));
|
|
1411
|
+
}
|
|
1412
|
+
else if (op === 'not-in') {
|
|
1413
|
+
const values = value.split(',').map(v => v.trim());
|
|
1414
|
+
return !properties.some(p => values.includes(this._getPropertyValue(p)));
|
|
1415
|
+
}
|
|
1416
|
+
else if (op === 'regex') {
|
|
1417
|
+
try {
|
|
1418
|
+
const regex = new RegExp('^' + value + '$');
|
|
1419
|
+
return properties.some(p => regex.test(this._getPropertyValue(p)));
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
return false;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
return false;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* Get property value as string
|
|
1430
|
+
* @param {Object} property - The property object
|
|
1431
|
+
* @returns {string} Property value
|
|
1432
|
+
* @private
|
|
1433
|
+
*/
|
|
1434
|
+
_getPropertyValue(property) {
|
|
1435
|
+
if (property.valueCode) return property.valueCode;
|
|
1436
|
+
if (property.valueString) return property.valueString;
|
|
1437
|
+
if (property.valueInteger !== undefined) return property.valueInteger.toString();
|
|
1438
|
+
if (property.valueBoolean !== undefined) return property.valueBoolean.toString();
|
|
1439
|
+
if (property.valueDecimal !== undefined) return property.valueDecimal.toString();
|
|
1440
|
+
if (property.valueDateTime) return property.valueDateTime;
|
|
1441
|
+
if (property.valueCoding) return property.valueCoding.code || '';
|
|
1442
|
+
|
|
1443
|
+
return '';
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* Handle known property filters (notSelectable, status, etc.)
|
|
1448
|
+
* @param {FilterExecutionContext} filterContext - Filter context
|
|
1449
|
+
* @param {string} prop - Property name
|
|
1450
|
+
* @param {string} op - Filter operator
|
|
1451
|
+
* @param {string} value - Filter value
|
|
1452
|
+
* @returns {Promise<FhirCodeSystemProviderFilterContext>} Filter results
|
|
1453
|
+
* @private
|
|
1454
|
+
*/
|
|
1455
|
+
async _handleKnownPropertyFilter(filterContext, prop, op, value) {
|
|
1456
|
+
const results = new FhirCodeSystemProviderFilterContext();
|
|
1457
|
+
const allConcepts = this.codeSystem.getAllConcepts();
|
|
1458
|
+
|
|
1459
|
+
for (const concept of allConcepts) {
|
|
1460
|
+
let matches = false;
|
|
1461
|
+
|
|
1462
|
+
if (prop === 'notSelectable') {
|
|
1463
|
+
const abstractProp = (concept.property || []).find(p => p.code === 'abstract' || p.code === 'notSelectable' || p.uri === 'http://hl7.org/fhir/concept-properties#notSelectable');
|
|
1464
|
+
let vv = abstractProp ? String(getValuePrimitive(abstractProp)) : null;
|
|
1465
|
+
if (op === '=') {
|
|
1466
|
+
matches = (vv === value);
|
|
1467
|
+
} else if (op === 'in') {
|
|
1468
|
+
const values = value.split(',').map(v => v.trim());
|
|
1469
|
+
matches = values.includes(vv);
|
|
1470
|
+
} else if (op === 'not-in') {
|
|
1471
|
+
const values = value.split(',').map(v => v.trim());
|
|
1472
|
+
matches = !values.includes(vv);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
else if (prop === 'status') {
|
|
1476
|
+
const status = await this.getStatus(new FhirCodeSystemProviderContext(concept.code, concept));
|
|
1477
|
+
if (op === '=') {
|
|
1478
|
+
matches = (status === value);
|
|
1479
|
+
} else if (op === 'in') {
|
|
1480
|
+
const values = value.split(',').map(v => v.trim());
|
|
1481
|
+
matches = values.includes(status);
|
|
1482
|
+
} else if (op === 'not-in') {
|
|
1483
|
+
const values = value.split(',').map(v => v.trim());
|
|
1484
|
+
matches = !values.includes(status);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
else if (prop === 'inactive') {
|
|
1488
|
+
const isInactive = await this.isInactive(new FhirCodeSystemProviderContext(concept.code, concept));
|
|
1489
|
+
const expectedValue = (value === 'true');
|
|
1490
|
+
matches = (isInactive === expectedValue);
|
|
1491
|
+
}
|
|
1492
|
+
else if (prop === 'deprecated') {
|
|
1493
|
+
const isDeprecated = await this.isDeprecated(new FhirCodeSystemProviderContext(concept.code, concept));
|
|
1494
|
+
const expectedValue = (value === 'true');
|
|
1495
|
+
matches = (isDeprecated === expectedValue);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
if (matches) {
|
|
1499
|
+
results.add(concept, 0);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
return results;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
versionAlgorithm() {
|
|
1507
|
+
return this.codeSystem.versionAlgorithm();
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
versionNeeded() {
|
|
1511
|
+
return this.codeSystem.jsonObj.versionNeeded;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
class FhirCodeSystemFactory extends CodeSystemFactoryProvider {
|
|
1517
|
+
constructor(i18n) {
|
|
1518
|
+
super(i18n);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
defaultVersion() {
|
|
1522
|
+
return 'unknown'; // No default version for FHIR CodeSystems
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/**
|
|
1526
|
+
* Build a FHIR CodeSystem provider
|
|
1527
|
+
* @param {CodeSystem} codeSystem - The FHIR CodeSystem to wrap
|
|
1528
|
+
* @param {CodeSystem[]} supplements - Array of supplement CodeSystems
|
|
1529
|
+
* @returns {FhirCodeSystemProvider} New provider instance
|
|
1530
|
+
*/
|
|
1531
|
+
build(opContext, supplements, codeSystem) {
|
|
1532
|
+
this.recordUse();
|
|
1533
|
+
|
|
1534
|
+
// Validate parameters
|
|
1535
|
+
if (!codeSystem || typeof codeSystem !== 'object') {
|
|
1536
|
+
throw new Error('codeSystem parameter is required and must be a CodeSystem object');
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
if (codeSystem.jsonObj?.resourceType !== 'CodeSystem') {
|
|
1540
|
+
throw new Error('codeSystem must be a FHIR CodeSystem resource');
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// Validate supplements array
|
|
1544
|
+
if (supplements && !Array.isArray(supplements)) {
|
|
1545
|
+
throw new Error('supplements must be an array');
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
if (supplements) {
|
|
1549
|
+
supplements.forEach((supplement, index) => {
|
|
1550
|
+
if (!supplement || typeof supplement !== 'object' || supplement.jsonObj?.resourceType !== 'CodeSystem') {
|
|
1551
|
+
throw new Error(`Supplement ${index} must be a FHIR CodeSystem resource`);
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
return new FhirCodeSystemProvider(opContext, codeSystem, supplements);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// eslint-disable-next-line no-unused-vars
|
|
1560
|
+
async buildKnownValueSet(url, version) {
|
|
1561
|
+
return null;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
name() {
|
|
1565
|
+
return "CodeSystem";
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
id() {
|
|
1569
|
+
return "cs";
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
module.exports = {
|
|
1575
|
+
FhirCodeSystemFactory,
|
|
1576
|
+
FhirCodeSystemProvider,
|
|
1577
|
+
FhirCodeSystemProviderContext,
|
|
1578
|
+
FhirCodeSystemProviderFilterContext
|
|
1579
|
+
};
|