fhirsmith 0.16.1 → 0.18.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 +130 -0
- package/README.md +105 -30
- package/config-template.json +130 -4
- package/extension-tracker/extension-tracker.js +111 -3
- package/extension-tracker/readme.md +3 -0
- package/ips/ai-client.js +263 -0
- package/ips/composer.js +805 -0
- package/ips/editor-schema.js +289 -0
- package/ips/editor.js +290 -0
- package/ips/fhirpath-ips.js +397 -0
- package/ips/ips-codeable-concepts.md +1742 -0
- package/ips/ips-template.html +125 -0
- package/ips/ips-valuesets.js +159 -0
- package/ips/ips.js +1736 -0
- package/ips/membership.js +179 -0
- package/ips/pipeline.js +1012 -0
- package/ips/presenter.js +974 -0
- package/ips/profile-editor.js +578 -0
- package/ips/profiles.js +506 -0
- package/ips/readme.md +460 -0
- package/ips/render.js +1482 -0
- package/ips/search-params.js +83 -0
- package/ips/search.js +667 -0
- package/ips/share.js +178 -0
- package/ips/store.js +650 -0
- package/ips/terminology.js +395 -0
- package/ips/tx-client.js +202 -0
- package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
- package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
- package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
- package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
- package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
- package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
- package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
- package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
- package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
- package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
- package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
- package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
- package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-medication.json +50 -0
- package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
- package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
- package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
- package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
- package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
- package/ips/valuesets/ValueSet-ips-route.json +46 -0
- package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
- package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
- package/ips/valuesets/elements.json +44 -0
- package/library/daily-report.js +422 -0
- package/library/fhir-search-values.js +138 -0
- package/library/html-server.js +32 -23
- package/library/html.js +9 -0
- package/library/mailer.js +77 -0
- package/library/narrative-check.js +222 -0
- package/library/npm-audit.js +25 -0
- package/library/package-manager.js +4 -2
- package/library/sqlite-session-store.js +29 -1
- package/package.json +8 -2
- package/packages/package-crawler.js +189 -82
- package/packages/packages.js +36 -63
- package/packages/utilities/dedupe-versions.js +22 -8
- package/publisher/publisher.js +29 -0
- package/registry/auth-store.js +366 -0
- package/registry/auth.js +872 -0
- package/registry/crawler.js +48 -1
- package/registry/documentation/api.md +162 -0
- package/registry/documentation/configuration.md +93 -0
- package/registry/documentation/documents.md +74 -0
- package/registry/documentation/internals.md +151 -0
- package/registry/documentation/readme.md +100 -0
- package/registry/documentation/security.md +199 -0
- package/registry/documents.js +309 -0
- package/registry/openapi.yaml +30 -0
- package/registry/registry-template.html +1 -1
- package/registry/registry.js +247 -54
- package/registry/uptime.js +154 -0
- package/release.sh +80 -0
- package/server.js +105 -44
- package/shl/shl.js +64 -9
- package/stats-db.js +93 -0
- package/stats.js +158 -1
- package/testing/readme.md +1 -1
- package/testing/search.js +2 -108
- package/testing/store.js +5 -0
- package/testing/testing.js +20 -0
- package/token/token-template.html +5 -4
- package/token/token.js +17 -10
- package/translations/Messages.properties +9 -0
- package/tx/README.md +52 -0
- package/tx/cm/cm-database.js +62 -0
- package/tx/cm/cm-package.js +20 -3
- package/tx/cs/cs-api.js +90 -14
- package/tx/cs/cs-cs.js +142 -19
- package/tx/cs/cs-icd11.js +366 -112
- package/tx/cs/cs-icf.js +418 -0
- package/tx/cs/cs-loinc.js +79 -39
- package/tx/cs/cs-omop.js +10 -3
- package/tx/cs/cs-snomed.js +918 -55
- package/tx/cs/cs-ucum.js +6 -0
- package/tx/cs/icd11-expressions.js +191 -78
- package/tx/importers/export-icd11-codesystem.js +58 -12
- package/tx/importers/icd11-schema.md +9 -5
- package/tx/library.js +107 -6
- package/tx/ocl/README.md +62 -6
- package/tx/ocl/cm-ocl.cjs +166 -44
- package/tx/ocl/cs-ocl.cjs +163 -13
- package/tx/ocl/resolve/public-access.js +143 -0
- package/tx/ocl/resolve/reference-resolver.js +458 -0
- package/tx/ocl/vs-ocl.cjs +167 -4
- package/tx/openapi-operations.js +4 -0
- package/tx/params.js +8 -1
- package/tx/problems.js +7 -5
- package/tx/provider.js +21 -5
- package/tx/sct/documentation.md +251 -0
- package/tx/sct/ecl.js +758 -46
- package/tx/sct/expressions.js +113 -52
- package/tx/syndication/syndication-feed.js +218 -0
- package/tx/syndication/syndication-store.js +415 -0
- package/tx/tx-html.js +37 -8
- package/tx/tx.fhir.org.yml +2 -3
- package/tx/tx.js +148 -5
- package/tx/vs/vs-database.js +26 -0
- package/tx/vs/vs-package.js +42 -10
- package/tx/vs/vs-vsac.js +25 -6
- package/tx/workers/expand.js +34 -7
- package/tx/workers/lookup.js +61 -6
- package/tx/workers/metadata.js +6 -1
- package/tx/workers/subsumes.js +1 -1
- package/tx/workers/translate.js +166 -15
- package/tx/workers/validate.js +128 -6
- package/utilities/make-sct-test-extension.js +264 -0
- package/utilities/submit-test-report.js +1 -1
- package/xig/xig.js +120 -12
- package/gr.sh +0 -3
- package/registry/readme.md +0 -124
package/tx/cs/cs-cs.js
CHANGED
|
@@ -19,6 +19,63 @@ const regexUtilities = require("../../library/regex-utilities");
|
|
|
19
19
|
// filter results do not depend on forIteration, so one form is stored per version.
|
|
20
20
|
const FILTER_ANALYSIS = Symbol('filterAnalysis');
|
|
21
21
|
|
|
22
|
+
// The texts of each concept as a text filter compares them, by concept. Folding is
|
|
23
|
+
// the costly part of a search over a large code system, so it is done once per
|
|
24
|
+
// concept, and dropped with the code system.
|
|
25
|
+
const SEARCH_TEXTS = new WeakMap();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lower case without accents, so "colera" finds "Cólera". Only marks on Latin, Greek and
|
|
29
|
+
* Cyrillic letters are accents: in other scripts, such as kana or Devanagari, marks are
|
|
30
|
+
* part of how a word is spelt, and dropping them would make "がん" find "かん".
|
|
31
|
+
*/
|
|
32
|
+
function foldSearchText(text) {
|
|
33
|
+
return text.normalize('NFD')
|
|
34
|
+
.replace(/([\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}])\p{M}+/gu, '$1')
|
|
35
|
+
.toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A text as a text filter compares it: `text` folded, and `words` the same with each
|
|
40
|
+
* run of other characters made one space and a space in front, so that " " + word
|
|
41
|
+
* finds where a word starts.
|
|
42
|
+
*/
|
|
43
|
+
function searchText(text) {
|
|
44
|
+
const folded = foldSearchText(text);
|
|
45
|
+
return { text: folded, words: ' ' + folded.replace(/[^\p{L}\p{N}]+/gu, ' ') };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function conceptSearchTexts(concept) {
|
|
49
|
+
let texts = SEARCH_TEXTS.get(concept);
|
|
50
|
+
if (texts === undefined) {
|
|
51
|
+
texts = {
|
|
52
|
+
code: foldSearchText(concept.code),
|
|
53
|
+
display: concept.display ? searchText(concept.display) : null,
|
|
54
|
+
definition: concept.definition ? foldSearchText(concept.definition) : null,
|
|
55
|
+
designations: Array.isArray(concept.designation)
|
|
56
|
+
? concept.designation.filter(designation => designation.value).map(designation => searchText(designation.value))
|
|
57
|
+
: []
|
|
58
|
+
};
|
|
59
|
+
SEARCH_TEXTS.set(concept, texts);
|
|
60
|
+
}
|
|
61
|
+
return texts;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether every word starts a word of the text.
|
|
66
|
+
*/
|
|
67
|
+
function startsWords(text, words) {
|
|
68
|
+
return words.every(word => text.words.includes(' ' + word));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* How much of the text the words cover, from 0 to 1, counting one space between words
|
|
73
|
+
* whatever separated them in the search.
|
|
74
|
+
*/
|
|
75
|
+
function wordsRatio(words, text) {
|
|
76
|
+
return Math.min(1, words.join(' ').length / text.text.length);
|
|
77
|
+
}
|
|
78
|
+
|
|
22
79
|
/**
|
|
23
80
|
* Context class for FHIR CodeSystem provider concepts
|
|
24
81
|
*/
|
|
@@ -114,7 +171,35 @@ class FhirCodeSystemProviderFilterContext {
|
|
|
114
171
|
}
|
|
115
172
|
}
|
|
116
173
|
|
|
174
|
+
// Code systems loaded as ordinary CodeSystem resources (from a package, or supplied by a
|
|
175
|
+
// client) that need behaviour beyond the resource: a grammar, say. Keyed on the CodeSystem
|
|
176
|
+
// url; the class extends FhirCodeSystemProvider, so the content still comes from the
|
|
177
|
+
// resource. Required lazily, because the subclasses require this module.
|
|
178
|
+
let specialisedProviders = null;
|
|
179
|
+
|
|
180
|
+
function providerClassFor(url) {
|
|
181
|
+
if (specialisedProviders === null) {
|
|
182
|
+
specialisedProviders = new Map([
|
|
183
|
+
['http://hl7.org/fhir/sid/icf', require('./cs-icf').IcfCodeSystemProvider]
|
|
184
|
+
]);
|
|
185
|
+
}
|
|
186
|
+
return specialisedProviders.get(url) || FhirCodeSystemProvider;
|
|
187
|
+
}
|
|
188
|
+
|
|
117
189
|
class FhirCodeSystemProvider extends BaseCSServices {
|
|
190
|
+
/**
|
|
191
|
+
* The provider for a CodeSystem resource: a FhirCodeSystemProvider, or the subclass
|
|
192
|
+
* registered for its url. Always use this rather than the constructor.
|
|
193
|
+
* @param {OperationContext} opContext
|
|
194
|
+
* @param {CodeSystem} codeSystem
|
|
195
|
+
* @param {CodeSystem[]} supplements
|
|
196
|
+
* @returns {FhirCodeSystemProvider}
|
|
197
|
+
*/
|
|
198
|
+
static create(opContext, codeSystem, supplements) {
|
|
199
|
+
const cls = providerClassFor(codeSystem.url);
|
|
200
|
+
return new cls(opContext, codeSystem, supplements);
|
|
201
|
+
}
|
|
202
|
+
|
|
118
203
|
/**
|
|
119
204
|
* @param {CodeSystem} codeSystem - The primary CodeSystem
|
|
120
205
|
* @param {CodeSystem[]} supplements - Array of supplement CodeSystems
|
|
@@ -140,6 +225,33 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
140
225
|
return this.codeSystem.jsonObj.name || '';
|
|
141
226
|
}
|
|
142
227
|
|
|
228
|
+
/**
|
|
229
|
+
* $lookup's display name: CodeSystem.title, translated where the resource carries a
|
|
230
|
+
* translation for one of the request's languages, else the title as written, else name.
|
|
231
|
+
* @param {Languages} [languages] - the request's working languages
|
|
232
|
+
* @returns {string}
|
|
233
|
+
*/
|
|
234
|
+
lookupName(languages) {
|
|
235
|
+
const cs = this.codeSystem.jsonObj;
|
|
236
|
+
if (!cs.title) return this.name();
|
|
237
|
+
const base = (c) => String(c || '').toLowerCase().split('-')[0];
|
|
238
|
+
const translations = (cs._title?.extension || [])
|
|
239
|
+
.filter(e => e.url === 'http://hl7.org/fhir/StructureDefinition/translation')
|
|
240
|
+
.map(e => ({
|
|
241
|
+
lang: (e.extension || []).find(x => x.url === 'lang')?.valueCode,
|
|
242
|
+
content: (e.extension || []).find(x => x.url === 'content')?.valueString,
|
|
243
|
+
}))
|
|
244
|
+
.filter(t => t.lang && t.content);
|
|
245
|
+
for (const l of languages || []) {
|
|
246
|
+
const want = base(l.language || l.code);
|
|
247
|
+
if (!want) continue;
|
|
248
|
+
if (cs.language && base(cs.language) === want) return cs.title;
|
|
249
|
+
const t = translations.find(t => base(t.lang) === want);
|
|
250
|
+
if (t) return t.content;
|
|
251
|
+
}
|
|
252
|
+
return cs.title;
|
|
253
|
+
}
|
|
254
|
+
|
|
143
255
|
/**
|
|
144
256
|
* @returns {string} URI for the code system
|
|
145
257
|
*/
|
|
@@ -1145,14 +1257,15 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1145
1257
|
|
|
1146
1258
|
|
|
1147
1259
|
const results = new FhirCodeSystemProviderFilterContext();
|
|
1148
|
-
const searchTerm = filter.filter
|
|
1260
|
+
const searchTerm = foldSearchText(filter.filter);
|
|
1261
|
+
const searchWords = searchTerm.split(/[^\p{L}\p{N}]+/u).filter(word => word !== '');
|
|
1149
1262
|
|
|
1150
1263
|
// Search through all concepts
|
|
1151
1264
|
const allConcepts = this.codeSystem.getAllConcepts();
|
|
1152
1265
|
|
|
1153
1266
|
for (const concept of allConcepts) {
|
|
1154
1267
|
this.opContext.deadCheck('cs:searchFilter');
|
|
1155
|
-
const rating = this._calculateSearchRating(concept, searchTerm);
|
|
1268
|
+
const rating = this._calculateSearchRating(concept, searchTerm, searchWords);
|
|
1156
1269
|
if (rating > 0) {
|
|
1157
1270
|
results.add(concept, rating);
|
|
1158
1271
|
}
|
|
@@ -1174,47 +1287,57 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1174
1287
|
/**
|
|
1175
1288
|
* Calculate search relevance rating for a concept
|
|
1176
1289
|
* @param {Object} concept - The concept to rate
|
|
1177
|
-
* @param {string} searchTerm - The search term (
|
|
1290
|
+
* @param {string} searchTerm - The search term (folded: lower case, no accents)
|
|
1291
|
+
* @param {string[]} searchWords - The words of the search term
|
|
1178
1292
|
* @returns {number} Rating (0 = no match, higher = better match)
|
|
1179
1293
|
* @private
|
|
1180
1294
|
*/
|
|
1181
|
-
_calculateSearchRating(concept, searchTerm) {
|
|
1295
|
+
_calculateSearchRating(concept, searchTerm, searchWords) {
|
|
1296
|
+
const texts = conceptSearchTexts(concept);
|
|
1297
|
+
const display = texts.display;
|
|
1182
1298
|
let rating = 0;
|
|
1183
1299
|
|
|
1184
1300
|
// Exact matches get highest rating
|
|
1185
|
-
if (
|
|
1301
|
+
if (texts.code === searchTerm) {
|
|
1186
1302
|
rating = 100;
|
|
1187
|
-
} else if (
|
|
1303
|
+
} else if (display && display.text === searchTerm) {
|
|
1188
1304
|
rating = 100;
|
|
1189
1305
|
}
|
|
1190
1306
|
// Code starts with search term
|
|
1191
|
-
else if (
|
|
1307
|
+
else if (texts.code.startsWith(searchTerm)) {
|
|
1192
1308
|
rating = 90;
|
|
1193
1309
|
}
|
|
1194
1310
|
// Display starts with search term
|
|
1195
|
-
else if (
|
|
1196
|
-
const lengthRatio = searchTerm.length /
|
|
1311
|
+
else if (display && display.text.startsWith(searchTerm)) {
|
|
1312
|
+
const lengthRatio = searchTerm.length / display.text.length;
|
|
1197
1313
|
rating = 80 + (10 * lengthRatio);
|
|
1198
1314
|
}
|
|
1199
1315
|
// Code contains search term
|
|
1200
|
-
else if (
|
|
1316
|
+
else if (texts.code.includes(searchTerm)) {
|
|
1201
1317
|
rating = 60;
|
|
1202
1318
|
}
|
|
1203
1319
|
// Display contains search term
|
|
1204
|
-
else if (
|
|
1320
|
+
else if (display && display.text.includes(searchTerm)) {
|
|
1205
1321
|
rating = 50;
|
|
1206
1322
|
}
|
|
1207
1323
|
// Definition contains search term
|
|
1208
|
-
else if (
|
|
1324
|
+
else if (texts.definition && texts.definition.includes(searchTerm)) {
|
|
1209
1325
|
rating = 30;
|
|
1210
1326
|
}
|
|
1211
1327
|
// Check designations
|
|
1212
|
-
else if (
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1328
|
+
else if (texts.designations.some(designation => designation.text.includes(searchTerm))) {
|
|
1329
|
+
rating = 40;
|
|
1330
|
+
}
|
|
1331
|
+
// Each word of a search of several starts a word of the display, in any order;
|
|
1332
|
+
// the shorter the display, the closer the match
|
|
1333
|
+
else if (searchWords.length > 1 && display && startsWords(display, searchWords)) {
|
|
1334
|
+
rating = 20 + (10 * wordsRatio(searchWords, display));
|
|
1335
|
+
}
|
|
1336
|
+
// Or of one designation
|
|
1337
|
+
else if (searchWords.length > 1) {
|
|
1338
|
+
const designation = texts.designations.find(designation => startsWords(designation, searchWords));
|
|
1339
|
+
if (designation) {
|
|
1340
|
+
rating = 10 + (10 * wordsRatio(searchWords, designation));
|
|
1218
1341
|
}
|
|
1219
1342
|
}
|
|
1220
1343
|
|
|
@@ -1679,7 +1802,7 @@ class FhirCodeSystemFactory extends CodeSystemFactoryProvider {
|
|
|
1679
1802
|
});
|
|
1680
1803
|
}
|
|
1681
1804
|
|
|
1682
|
-
return
|
|
1805
|
+
return FhirCodeSystemProvider.create(opContext, codeSystem, supplements);
|
|
1683
1806
|
}
|
|
1684
1807
|
|
|
1685
1808
|
// eslint-disable-next-line no-unused-vars
|