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.
Files changed (142) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +105 -30
  3. package/config-template.json +130 -4
  4. package/extension-tracker/extension-tracker.js +111 -3
  5. package/extension-tracker/readme.md +3 -0
  6. package/ips/ai-client.js +263 -0
  7. package/ips/composer.js +805 -0
  8. package/ips/editor-schema.js +289 -0
  9. package/ips/editor.js +290 -0
  10. package/ips/fhirpath-ips.js +397 -0
  11. package/ips/ips-codeable-concepts.md +1742 -0
  12. package/ips/ips-template.html +125 -0
  13. package/ips/ips-valuesets.js +159 -0
  14. package/ips/ips.js +1736 -0
  15. package/ips/membership.js +179 -0
  16. package/ips/pipeline.js +1012 -0
  17. package/ips/presenter.js +974 -0
  18. package/ips/profile-editor.js +578 -0
  19. package/ips/profiles.js +506 -0
  20. package/ips/readme.md +460 -0
  21. package/ips/render.js +1482 -0
  22. package/ips/search-params.js +83 -0
  23. package/ips/search.js +667 -0
  24. package/ips/share.js +178 -0
  25. package/ips/store.js +650 -0
  26. package/ips/terminology.js +395 -0
  27. package/ips/tx-client.js +202 -0
  28. package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
  29. package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
  30. package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
  31. package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
  32. package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
  33. package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
  34. package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
  35. package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
  36. package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
  37. package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
  38. package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
  39. package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
  40. package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
  41. package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
  42. package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
  43. package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
  44. package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
  45. package/ips/valuesets/ValueSet-ips-medication.json +50 -0
  46. package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
  47. package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
  48. package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
  49. package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
  50. package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
  51. package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
  52. package/ips/valuesets/ValueSet-ips-route.json +46 -0
  53. package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
  54. package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
  55. package/ips/valuesets/elements.json +44 -0
  56. package/library/daily-report.js +422 -0
  57. package/library/fhir-search-values.js +138 -0
  58. package/library/html-server.js +32 -23
  59. package/library/html.js +9 -0
  60. package/library/mailer.js +77 -0
  61. package/library/narrative-check.js +222 -0
  62. package/library/npm-audit.js +25 -0
  63. package/library/package-manager.js +4 -2
  64. package/library/sqlite-session-store.js +29 -1
  65. package/package.json +8 -2
  66. package/packages/package-crawler.js +189 -82
  67. package/packages/packages.js +36 -63
  68. package/packages/utilities/dedupe-versions.js +22 -8
  69. package/publisher/publisher.js +29 -0
  70. package/registry/auth-store.js +366 -0
  71. package/registry/auth.js +872 -0
  72. package/registry/crawler.js +48 -1
  73. package/registry/documentation/api.md +162 -0
  74. package/registry/documentation/configuration.md +93 -0
  75. package/registry/documentation/documents.md +74 -0
  76. package/registry/documentation/internals.md +151 -0
  77. package/registry/documentation/readme.md +100 -0
  78. package/registry/documentation/security.md +199 -0
  79. package/registry/documents.js +309 -0
  80. package/registry/openapi.yaml +30 -0
  81. package/registry/registry-template.html +1 -1
  82. package/registry/registry.js +247 -54
  83. package/registry/uptime.js +154 -0
  84. package/release.sh +80 -0
  85. package/server.js +105 -44
  86. package/shl/shl.js +64 -9
  87. package/stats-db.js +93 -0
  88. package/stats.js +158 -1
  89. package/testing/readme.md +1 -1
  90. package/testing/search.js +2 -108
  91. package/testing/store.js +5 -0
  92. package/testing/testing.js +20 -0
  93. package/token/token-template.html +5 -4
  94. package/token/token.js +17 -10
  95. package/translations/Messages.properties +9 -0
  96. package/tx/README.md +52 -0
  97. package/tx/cm/cm-database.js +62 -0
  98. package/tx/cm/cm-package.js +20 -3
  99. package/tx/cs/cs-api.js +90 -14
  100. package/tx/cs/cs-cs.js +142 -19
  101. package/tx/cs/cs-icd11.js +366 -112
  102. package/tx/cs/cs-icf.js +418 -0
  103. package/tx/cs/cs-loinc.js +79 -39
  104. package/tx/cs/cs-omop.js +10 -3
  105. package/tx/cs/cs-snomed.js +918 -55
  106. package/tx/cs/cs-ucum.js +6 -0
  107. package/tx/cs/icd11-expressions.js +191 -78
  108. package/tx/importers/export-icd11-codesystem.js +58 -12
  109. package/tx/importers/icd11-schema.md +9 -5
  110. package/tx/library.js +107 -6
  111. package/tx/ocl/README.md +62 -6
  112. package/tx/ocl/cm-ocl.cjs +166 -44
  113. package/tx/ocl/cs-ocl.cjs +163 -13
  114. package/tx/ocl/resolve/public-access.js +143 -0
  115. package/tx/ocl/resolve/reference-resolver.js +458 -0
  116. package/tx/ocl/vs-ocl.cjs +167 -4
  117. package/tx/openapi-operations.js +4 -0
  118. package/tx/params.js +8 -1
  119. package/tx/problems.js +7 -5
  120. package/tx/provider.js +21 -5
  121. package/tx/sct/documentation.md +251 -0
  122. package/tx/sct/ecl.js +758 -46
  123. package/tx/sct/expressions.js +113 -52
  124. package/tx/syndication/syndication-feed.js +218 -0
  125. package/tx/syndication/syndication-store.js +415 -0
  126. package/tx/tx-html.js +37 -8
  127. package/tx/tx.fhir.org.yml +2 -3
  128. package/tx/tx.js +148 -5
  129. package/tx/vs/vs-database.js +26 -0
  130. package/tx/vs/vs-package.js +42 -10
  131. package/tx/vs/vs-vsac.js +25 -6
  132. package/tx/workers/expand.js +34 -7
  133. package/tx/workers/lookup.js +61 -6
  134. package/tx/workers/metadata.js +6 -1
  135. package/tx/workers/subsumes.js +1 -1
  136. package/tx/workers/translate.js +166 -15
  137. package/tx/workers/validate.js +128 -6
  138. package/utilities/make-sct-test-extension.js +264 -0
  139. package/utilities/submit-test-report.js +1 -1
  140. package/xig/xig.js +120 -12
  141. package/gr.sh +0 -3
  142. 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.toLowerCase();
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 (lowercase)
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 (concept.code.toLowerCase() === searchTerm) {
1301
+ if (texts.code === searchTerm) {
1186
1302
  rating = 100;
1187
- } else if (concept.display && concept.display.toLowerCase() === searchTerm) {
1303
+ } else if (display && display.text === searchTerm) {
1188
1304
  rating = 100;
1189
1305
  }
1190
1306
  // Code starts with search term
1191
- else if (concept.code.toLowerCase().startsWith(searchTerm)) {
1307
+ else if (texts.code.startsWith(searchTerm)) {
1192
1308
  rating = 90;
1193
1309
  }
1194
1310
  // Display starts with search term
1195
- else if (concept.display && concept.display.toLowerCase().startsWith(searchTerm)) {
1196
- const lengthRatio = searchTerm.length / concept.display.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 (concept.code.toLowerCase().includes(searchTerm)) {
1316
+ else if (texts.code.includes(searchTerm)) {
1201
1317
  rating = 60;
1202
1318
  }
1203
1319
  // Display contains search term
1204
- else if (concept.display && concept.display.toLowerCase().includes(searchTerm)) {
1320
+ else if (display && display.text.includes(searchTerm)) {
1205
1321
  rating = 50;
1206
1322
  }
1207
1323
  // Definition contains search term
1208
- else if (concept.definition && concept.definition.toLowerCase().includes(searchTerm)) {
1324
+ else if (texts.definition && texts.definition.includes(searchTerm)) {
1209
1325
  rating = 30;
1210
1326
  }
1211
1327
  // Check designations
1212
- else if (concept.designation && Array.isArray(concept.designation)) {
1213
- for (const designation of concept.designation) {
1214
- if (designation.value && designation.value.toLowerCase().includes(searchTerm)) {
1215
- rating = 40;
1216
- break;
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 new FhirCodeSystemProvider(opContext, codeSystem, supplements);
1805
+ return FhirCodeSystemProvider.create(opContext, codeSystem, supplements);
1683
1806
  }
1684
1807
 
1685
1808
  // eslint-disable-next-line no-unused-vars