fhirsmith 0.18.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/ACCESSIBILITY.md +119 -0
  2. package/CHANGELOG.md +190 -9
  3. package/CONFIGURATION.md +305 -0
  4. package/LICENSE +29 -0
  5. package/README.md +29 -122
  6. package/cluster.js +1220 -0
  7. package/cluster.md +707 -0
  8. package/config-template.json +77 -8
  9. package/extension-tracker/extension-tracker-template.html +5 -8
  10. package/extension-tracker/extension-tracker.js +0 -2
  11. package/folder/folder.js +7 -6
  12. package/ips/analysis.js +560 -0
  13. package/ips/ips-template.html +5 -8
  14. package/ips/ips.js +331 -33
  15. package/ips/jurisdictions.js +65 -0
  16. package/ips/nationality.js +120 -0
  17. package/ips/pipeline.js +131 -17
  18. package/ips/presenter.js +1 -1
  19. package/ips/profile-editor.js +283 -4
  20. package/ips/readme.md +170 -20
  21. package/ips/render.js +769 -109
  22. package/ips/scripts.js +46 -0
  23. package/ips/store.js +123 -11
  24. package/ips/structure.js +247 -0
  25. package/ips/terminology.js +1 -1
  26. package/library/body-limit.js +22 -0
  27. package/library/cache-generation.js +15 -0
  28. package/library/cgroup-memory.js +133 -1
  29. package/library/cluster-counts.js +121 -0
  30. package/library/cluster-events.js +153 -0
  31. package/library/cluster-metrics.js +193 -0
  32. package/library/cluster-status.js +468 -0
  33. package/library/config-reader.js +96 -0
  34. package/library/daily-report.js +100 -2
  35. package/library/html-server.js +27 -1
  36. package/library/i18nsupport.js +23 -1
  37. package/library/keep-alive.js +18 -0
  38. package/library/languages.js +17 -3
  39. package/library/logger.js +31 -0
  40. package/library/memory-guard.js +428 -0
  41. package/library/native-memory.js +52 -0
  42. package/library/npm-audit.js +51 -25
  43. package/library/openapi-doc.js +7 -5
  44. package/library/package-manager.js +8 -0
  45. package/library/role.js +197 -0
  46. package/library/server-credentials.js +169 -0
  47. package/library/sqlite-utilities.js +59 -0
  48. package/library/ssrf-guard.js +100 -0
  49. package/library/static-response-stats.js +16 -0
  50. package/library/version-utilities.js +50 -0
  51. package/library/worker-count.js +192 -0
  52. package/nginx.md +30 -11
  53. package/npmprojector/indexer.js +18 -10
  54. package/package.json +11 -8
  55. package/packages/feed-check-page.js +117 -0
  56. package/packages/feed-check.js +279 -0
  57. package/packages/npm-check.js +57 -3
  58. package/packages/npm-signing.js +219 -0
  59. package/packages/openapi.yaml +207 -1
  60. package/packages/package-crawler.js +322 -41
  61. package/packages/packages-template.html +6 -8
  62. package/packages/packages.js +604 -57
  63. package/packages/readme.md +113 -1
  64. package/publisher/publisher-template.html +2 -5
  65. package/publisher/publisher.js +121 -110
  66. package/publisher/readme.md +75 -0
  67. package/registry/api.js +179 -1
  68. package/registry/crawler.js +86 -108
  69. package/registry/documentation/api.md +68 -2
  70. package/registry/documentation/configuration.md +34 -0
  71. package/registry/documentation/internals.md +4 -1
  72. package/registry/documentation/readme.md +3 -2
  73. package/registry/model.js +8 -0
  74. package/registry/openapi.yaml +84 -0
  75. package/registry/registry-template.html +5 -8
  76. package/registry/registry.js +58 -6
  77. package/root-bare-template.html +7 -4
  78. package/root-template.html +5 -8
  79. package/server.js +398 -198
  80. package/shl/shl.js +38 -37
  81. package/static/a11y/a11y-check.js +832 -0
  82. package/static/a11y/axe.min.js +12 -0
  83. package/static/assets/css/bootstrap-fhir.css +15 -15
  84. package/static/assets/images/flags/au.svg +18 -0
  85. package/static/assets/images/flags/unknown.svg +11 -0
  86. package/static/assets/images/flags/us.svg +63 -0
  87. package/static/assets/images/icon_audit.png +0 -0
  88. package/static/assets/images/icon_clinical.png +0 -0
  89. package/static/assets/images/icon_document.png +0 -0
  90. package/static/assets/images/icon_external.png +0 -0
  91. package/static/assets/images/icon_medication.png +0 -0
  92. package/static/assets/images/icon_organization.png +0 -0
  93. package/static/assets/images/icon_other.png +0 -0
  94. package/static/assets/images/icon_person.png +0 -0
  95. package/static/assets/images/icon_section.png +0 -0
  96. package/stats-db.js +45 -0
  97. package/stats.js +197 -10
  98. package/testing/testing-template.html +5 -8
  99. package/token/nginx_passport_setup.md +1 -1
  100. package/token/security_guide.md +1 -1
  101. package/token/token-template.html +2 -5
  102. package/token/token.js +40 -36
  103. package/translations/Messages.properties +19 -0
  104. package/tx/README.md +158 -4
  105. package/tx/closure/closure-store.js +126 -3
  106. package/tx/cm/cm-database.js +69 -57
  107. package/tx/cm/cm-package.js +50 -16
  108. package/tx/cs/cs-api.js +284 -19
  109. package/tx/cs/cs-api.md +41 -4
  110. package/tx/cs/cs-areacode.js +1 -1
  111. package/tx/cs/cs-country.js +17 -5
  112. package/tx/cs/cs-cpt.js +33 -16
  113. package/tx/cs/cs-cs.js +199 -24
  114. package/tx/cs/cs-currency.js +1 -1
  115. package/tx/cs/cs-icd11.js +21 -13
  116. package/tx/cs/cs-icf.js +22 -3
  117. package/tx/cs/cs-lang.js +2 -2
  118. package/tx/cs/cs-loinc.js +193 -67
  119. package/tx/cs/cs-mimetypes.js +12 -2
  120. package/tx/cs/cs-ndc.js +40 -25
  121. package/tx/cs/cs-omop.js +54 -29
  122. package/tx/cs/cs-rxnorm.js +300 -196
  123. package/tx/cs/cs-snomed.js +861 -95
  124. package/tx/cs/cs-ucum.js +14 -4
  125. package/tx/cs/cs-unii.js +18 -14
  126. package/tx/cs/cs-virtual.js +210 -0
  127. package/tx/ecosystem-resolver.js +413 -0
  128. package/tx/html/cluster-dash-metrics.liquid +26 -0
  129. package/tx/html/cluster-home-metrics.liquid +31 -0
  130. package/tx/html/cluster-metrics-script.liquid +251 -0
  131. package/tx/html/codesystem-operations.liquid +3 -3
  132. package/tx/html/ecl-panel.liquid +1 -1
  133. package/tx/html/external-metrics.liquid +17 -0
  134. package/tx/html/operations-form.liquid +17 -17
  135. package/tx/html/search-form.liquid +11 -11
  136. package/tx/html/status-line.liquid +58 -0
  137. package/tx/html/status-panel.liquid +140 -0
  138. package/tx/html/status-tasks.liquid +25 -0
  139. package/tx/html/tx-template.html +2 -5
  140. package/tx/importers/atc-to-fhir.js +17 -16
  141. package/tx/importers/export-icd11-codesystem.js +3 -2
  142. package/tx/importers/import-icd11.js +4 -3
  143. package/tx/importers/import-loinc.module.js +118 -56
  144. package/tx/importers/import-ndc.module.js +56 -44
  145. package/tx/importers/import-rxnorm.module.js +118 -79
  146. package/tx/importers/import-sct.module.js +125 -61
  147. package/tx/importers/import-unii.module.js +37 -36
  148. package/tx/importers/subset-loinc.module.js +49 -48
  149. package/tx/importers/subset-rxnorm.module.js +29 -28
  150. package/tx/importers/terminal.js +18 -0
  151. package/tx/importers/tx-import-base.js +13 -12
  152. package/tx/importers/tx-import-settings.js +5 -4
  153. package/tx/importers/tx-import.js +53 -52
  154. package/tx/library/canonical-resource.js +49 -0
  155. package/tx/library/codesystem.js +94 -5
  156. package/tx/library/designations.js +45 -2
  157. package/tx/library/extensions.js +13 -7
  158. package/tx/library/implicit-valuesets.js +66 -0
  159. package/tx/library/renderer.js +77 -21
  160. package/tx/library/sqlite-utilities.js +2 -0
  161. package/tx/library/tho-precedence.js +40 -0
  162. package/tx/library/ucum-parsers.js +11 -1
  163. package/tx/library/wait-for-cache.js +43 -0
  164. package/tx/library.js +321 -22
  165. package/tx/openapi-operations.js +6 -4
  166. package/tx/operation-context.js +460 -99
  167. package/tx/params.js +25 -0
  168. package/tx/problems.js +3 -5
  169. package/tx/provider.js +314 -67
  170. package/tx/resource-cache-sqlite.js +1050 -0
  171. package/tx/sct/documentation.md +45 -21
  172. package/tx/sct/ecl.js +81 -17
  173. package/tx/sct/editions.js +39 -0
  174. package/tx/sct/expressions.js +10 -1
  175. package/tx/sct/load-config.js +107 -0
  176. package/tx/sct/mmap.js +109 -0
  177. package/tx/sct/structures.js +175 -15
  178. package/tx/sct/text-index.js +5 -1
  179. package/tx/syndication/syndication-feed.js +2 -2
  180. package/tx/tx-html.js +44 -8
  181. package/tx/tx.fhir.org.yml +8 -8
  182. package/tx/tx.js +282 -80
  183. package/tx/usage-tracker.js +39 -11
  184. package/tx/vs/vs-api.js +19 -0
  185. package/tx/vs/vs-database.js +386 -62
  186. package/tx/vs/vs-package.js +44 -17
  187. package/tx/vs/vs-vsac.js +649 -101
  188. package/tx/workers/batch-validate.js +37 -10
  189. package/tx/workers/batch.js +6 -1
  190. package/tx/workers/cache-control.js +15 -4
  191. package/tx/workers/closure.js +156 -13
  192. package/tx/workers/compare.js +8 -5
  193. package/tx/workers/expand.js +1173 -289
  194. package/tx/workers/metadata.js +35 -0
  195. package/tx/workers/search.js +53 -1
  196. package/tx/workers/supplement-filters.js +145 -0
  197. package/tx/workers/translate.js +169 -4
  198. package/tx/workers/validate.js +223 -119
  199. package/tx/workers/worker.js +123 -31
  200. package/tx/xversion/xv-valueset.js +7 -4
  201. package/utilities/submit-test-report.js +3 -1
  202. package/utilities/tx-stress-test.js +482 -0
  203. package/vcl/readme.md +37 -0
  204. package/vcl/vcl-parser.js +15 -0
  205. package/xig/readme.md +52 -0
  206. package/xig/xig-template.html +5 -8
  207. package/xig/xig.js +238 -422
  208. package/changelog-template.md +0 -22
  209. package/registry/test-runner.js +0 -237
  210. package/release.sh +0 -80
  211. package/test-scripts/repro-re2-wasm-leak.js +0 -34
  212. package/tsconfig.eslint.json +0 -9
  213. package/tx/html/dash-metrics.liquid +0 -147
  214. package/tx/html/home-metrics.liquid +0 -225
@@ -73,6 +73,14 @@ class CountryCodeServices extends CodeSystemProvider {
73
73
  if (!ctxt) {
74
74
  return null;
75
75
  }
76
+ // a user-assigned code's display is a placeholder ('User-assigned'): an extension-codes
77
+ // supplement's display for it comes first
78
+ if (ctxt.userDefined) {
79
+ const ext = this._extensionCodeDisplay(ctxt.code);
80
+ if (ext) {
81
+ return ext;
82
+ }
83
+ }
76
84
  if (ctxt.display && this.opContext.langs.isEnglishOrNothing()) {
77
85
  return ctxt.display;
78
86
  }
@@ -116,11 +124,15 @@ class CountryCodeServices extends CodeSystemProvider {
116
124
  const ctxt = await this.#ensureContext(code);
117
125
  if (ctxt != null) {
118
126
  displays.userDefined = ctxt.userDefined;
119
- displays.addDesignation( true, 'active', 'en', CodeSystem.makeUseForDisplay(), ctxt.display);
127
+ // when an extension-codes supplement gives a user-assigned code a display, the placeholder
128
+ // ('User-assigned') isn't one of its displays
129
+ if (!(ctxt.userDefined && this._extensionCodeDisplay(ctxt.code))) {
130
+ displays.addDesignation( true, 'active', 'en', CodeSystem.makeUseForDisplay(), ctxt.display);
131
+ }
120
132
  if (ctxt.french) {
121
133
  displays.addDesignation(true, 'active', 'fr', CodeSystem.makeUseForDisplay(), ctxt.french);
122
134
  }
123
- this._listSupplementDesignations(ctxt.code, displays);
135
+ this._listSupplementDesignations(ctxt.code, displays, ctxt.userDefined);
124
136
  }
125
137
  }
126
138
 
@@ -199,8 +211,8 @@ class CountryCodeServices extends CodeSystemProvider {
199
211
  const result = new CountryCodeConceptFilter();
200
212
 
201
213
  try {
202
- // Create regex with anchors to match the Pascal implementation (^value$)
203
- const regex = regexUtilities.compile('^' + value + '$');
214
+ // anchored to the whole code
215
+ const regex = regexUtilities.compile('^(?:' + value + ')$'); // the whole value: a|b is ^(?:a|b)$, not ^a|b$
204
216
 
205
217
  for (const concept of this.codes) {
206
218
  if (regex.test(concept.code)) {
@@ -266,7 +278,7 @@ class CountryCodeServices extends CodeSystemProvider {
266
278
  if (concept && set.list.includes(concept)) {
267
279
  return concept;
268
280
  }
269
- return `Code '${code}' not found in filter set`;
281
+ return null;
270
282
  }
271
283
 
272
284
  async filterCheck(filterContext, set, concept) {
package/tx/cs/cs-cpt.js CHANGED
@@ -4,6 +4,7 @@ const { CodeSystem } = require('../library/codesystem');
4
4
  const { FilterExecutionContext, CodeSystemFactoryProvider } = require('./cs-api');
5
5
  const {validateArrayParameter} = require("../../library/utilities");
6
6
  const {BaseCSServices} = require("./cs-base");
7
+ const { guarded } = require('../library/sqlite-utilities');
7
8
 
8
9
  class CPTConceptDesignation {
9
10
  constructor(kind, value) {
@@ -123,10 +124,7 @@ class CPTServices extends BaseCSServices {
123
124
  }
124
125
 
125
126
  close() {
126
- if (this.db) {
127
- this.db.close();
128
- this.db = null;
129
- }
127
+ this.closeDb();
130
128
  }
131
129
 
132
130
  // Metadata methods
@@ -228,6 +226,15 @@ class CPTServices extends BaseCSServices {
228
226
  isNotClosed() {
229
227
  return true;
230
228
  }
229
+ /**
230
+ * The concept's properties for $expand: built by extendLookup, so that an expansion reports
231
+ * the same properties $lookup does (see CodeSystemProvider._propertiesFromLookup). Not the
232
+ * copyright, which extendLookup always adds: it is about the code system, not the concept
233
+ */
234
+ async properties(context, names) {
235
+ return (await this._propertiesFromLookup(context, names)).filter(p => p.code !== 'copyright');
236
+ }
237
+
231
238
  async extendLookup(ctxt, props, params) {
232
239
  validateArrayParameter(props, 'props', String);
233
240
  validateArrayParameter(params, 'params', Object);
@@ -571,7 +578,8 @@ class CPTServices extends BaseCSServices {
571
578
 
572
579
 
573
580
 
574
- // Subsumption testing - not implemented
581
+ // Subsumption: CPT codes have no subsumption relationships, so two codes that both exist are
582
+ // not-subsumed (the caller deals with equal codes: equivalent)
575
583
  async subsumesTest(codeA, codeB) {
576
584
  await this.#ensureContext(codeA);
577
585
  await this.#ensureContext(codeB);
@@ -585,9 +593,14 @@ class CPTServices extends BaseCSServices {
585
593
  }
586
594
 
587
595
  class CPTServicesFactory extends CodeSystemFactoryProvider {
588
- constructor(i18n, dbPath) {
596
+ /**
597
+ * @param {Object} [options] - {readOnly: true} in a tx-worker (cluster.md): the file is
598
+ * the master's; this process opens it read-only
599
+ */
600
+ constructor(i18n, dbPath, options = {}) {
589
601
  super(i18n);
590
602
  this.dbPath = dbPath;
603
+ this.readOnly = !!options.readOnly;
591
604
  this.uses = 0;
592
605
  this._loaded = false;
593
606
  this._sharedData = null;
@@ -613,8 +626,12 @@ class CPTServicesFactory extends CodeSystemFactoryProvider {
613
626
  }
614
627
  }
615
628
 
629
+ #open() {
630
+ return this.readOnly ? new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY) : new sqlite3.Database(this.dbPath);
631
+ }
632
+
616
633
  async load() {
617
- const db = new sqlite3.Database(this.dbPath);
634
+ const db = this.#open();
618
635
 
619
636
  try {
620
637
  this._sharedData = {
@@ -645,7 +662,7 @@ class CPTServicesFactory extends CodeSystemFactoryProvider {
645
662
 
646
663
  async #loadVersion(db) {
647
664
  return new Promise((resolve, reject) => {
648
- db.all('SELECT * FROM Information', (err, rows) => {
665
+ db.all('SELECT * FROM Information', guarded(reject, (err, rows) => {
649
666
  if (err) {
650
667
  reject(err);
651
668
  } else {
@@ -656,13 +673,13 @@ class CPTServicesFactory extends CodeSystemFactoryProvider {
656
673
  }
657
674
  resolve();
658
675
  }
659
- });
676
+ }));
660
677
  });
661
678
  }
662
679
 
663
680
  async #loadConcepts(db) {
664
681
  return new Promise((resolve, reject) => {
665
- db.all('SELECT * FROM Concepts', (err, rows) => {
682
+ db.all('SELECT * FROM Concepts', guarded(reject, (err, rows) => {
666
683
  if (err) {
667
684
  reject(err);
668
685
  } else {
@@ -680,13 +697,13 @@ class CPTServicesFactory extends CodeSystemFactoryProvider {
680
697
  }
681
698
  resolve();
682
699
  }
683
- });
700
+ }));
684
701
  });
685
702
  }
686
703
 
687
704
  async #loadProperties(db) {
688
705
  return new Promise((resolve, reject) => {
689
- db.all('SELECT * FROM Properties', (err, rows) => {
706
+ db.all('SELECT * FROM Properties', guarded(reject, (err, rows) => {
690
707
  if (err) {
691
708
  reject(err);
692
709
  } else {
@@ -698,13 +715,13 @@ class CPTServicesFactory extends CodeSystemFactoryProvider {
698
715
  }
699
716
  resolve();
700
717
  }
701
- });
718
+ }));
702
719
  });
703
720
  }
704
721
 
705
722
  async #loadDesignations(db) {
706
723
  return new Promise((resolve, reject) => {
707
- db.all('SELECT * FROM Designations', (err, rows) => {
724
+ db.all('SELECT * FROM Designations', guarded(reject, (err, rows) => {
708
725
  if (err) {
709
726
  reject(err);
710
727
  } else {
@@ -716,7 +733,7 @@ class CPTServicesFactory extends CodeSystemFactoryProvider {
716
733
  }
717
734
  resolve();
718
735
  }
719
- });
736
+ }));
720
737
  });
721
738
  }
722
739
 
@@ -729,7 +746,7 @@ class CPTServicesFactory extends CodeSystemFactoryProvider {
729
746
  this.recordUse();
730
747
 
731
748
  // Create fresh database connection for this provider instance
732
- const db = new sqlite3.Database(this.dbPath);
749
+ const db = this.#open();
733
750
 
734
751
  return new CPTServices(opContext, supplements, db, this._sharedData);
735
752
  }
package/tx/cs/cs-cs.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const { CodeSystem} = require("../library/codesystem");
2
- const { CodeSystemFactoryProvider, FilterExecutionContext } = require( "./cs-api");
2
+ const { CodeSystemFactoryProvider, FilterExecutionContext, CodeSystemProvider } = require( "./cs-api");
3
3
  const { VersionUtilities } = require("../../library/version-utilities");
4
4
  const { Language } = require ("../../library/languages");
5
5
  const { validateOptionalParameter, getValuePrimitive, validateArrayParameter} = require("../../library/utilities");
@@ -19,6 +19,9 @@ 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 standard concept properties (http://hl7.org/fhir/concept-properties)
23
+ const CONCEPT_PROPERTIES = 'http://hl7.org/fhir/concept-properties#';
24
+
22
25
  // The texts of each concept as a text filter compares them, by concept. Folding is
23
26
  // the costly part of a search over a large code system, so it is done once per
24
27
  // concept, and dropped with the code system.
@@ -53,7 +56,8 @@ function conceptSearchTexts(concept) {
53
56
  display: concept.display ? searchText(concept.display) : null,
54
57
  definition: concept.definition ? foldSearchText(concept.definition) : null,
55
58
  designations: Array.isArray(concept.designation)
56
- ? concept.designation.filter(designation => designation.value).map(designation => searchText(designation.value))
59
+ ? concept.designation.filter(designation => designation.value)
60
+ .map(designation => ({ ...searchText(designation.value), language: designation.language || null }))
57
61
  : []
58
62
  };
59
63
  SEARCH_TEXTS.set(concept, texts);
@@ -80,9 +84,16 @@ function wordsRatio(words, text) {
80
84
  * Context class for FHIR CodeSystem provider concepts
81
85
  */
82
86
  class FhirCodeSystemProviderContext {
83
- constructor(code, concept) {
87
+ /**
88
+ * @param {string} code - the code
89
+ * @param {Object} concept - its definition in the code system
90
+ * @param {boolean} reserved - true if the code system doesn't define the code, but it is in its
91
+ * reserved code space (so the concept is just the code, and supplements may define it)
92
+ */
93
+ constructor(code, concept, reserved = false) {
84
94
  this.code = code;
85
95
  this.concept = concept;
96
+ this.reserved = reserved;
86
97
  }
87
98
  }
88
99
 
@@ -187,6 +198,16 @@ function providerClassFor(url) {
187
198
  }
188
199
 
189
200
  class FhirCodeSystemProvider extends BaseCSServices {
201
+ /**
202
+ * A code system that reserves code spaces for users (codesystem-reservedCodeSpace) has codes that
203
+ * aren't listed in an expansion of all of it
204
+ * @returns {string|null}
205
+ */
206
+ openCodeSpaceReason() {
207
+ return this.codeSystem.reservedCodeSpaces().length > 0 ?
208
+ "The code system '" + this.system() + "' reserves code spaces for users to define their own codes, and those codes are not in the expansion" : null;
209
+ }
210
+
190
211
  /**
191
212
  * The provider for a CodeSystem resource: a FhirCodeSystemProvider, or the subclass
192
213
  * registered for its url. Always use this rather than the constructor.
@@ -315,6 +336,46 @@ class FhirCodeSystemProvider extends BaseCSServices {
315
336
  return this.codeSystem.jsonObj.property || null;
316
337
  }
317
338
 
339
+ /**
340
+ * The codes this code system uses for one of the standard concept properties
341
+ * (http://hl7.org/fhir/concept-properties#<name>). A property declared with the
342
+ * standard uri counts whatever local code it was given. A code that the code
343
+ * system declares with a different uri is a different property, and does not
344
+ * count. A code the code system doesn't declare (or declares with no uri) is
345
+ * recognised by the code alone - the standard code, and any legacy aliases
346
+ *
347
+ * @param {string} name - the standard property code (e.g. notSelectable)
348
+ * @param {string[]} aliases - legacy codes also recognised when not declared otherwise
349
+ * @returns {Set<string>} the local codes for the property
350
+ */
351
+ standardPropertyCodes(name, aliases = []) {
352
+ const uri = CONCEPT_PROPERTIES + name;
353
+ const defs = this.propertyDefinitions() || [];
354
+ const codes = new Set();
355
+ for (const d of defs) {
356
+ if (d.uri === uri) {
357
+ codes.add(d.code);
358
+ }
359
+ }
360
+ for (const c of [name, ...aliases]) {
361
+ if (!defs.some(d => d.code === c && d.uri && d.uri !== uri)) {
362
+ codes.add(c);
363
+ }
364
+ }
365
+ return codes;
366
+ }
367
+
368
+ /**
369
+ * Whether the code system declares the code of a standard concept property for a
370
+ * property of its own (with a different uri), so that the code is not the standard one
371
+ * @param {string} name - the standard property code
372
+ * @returns {boolean}
373
+ */
374
+ redefinesStandardProperty(name) {
375
+ const uri = CONCEPT_PROPERTIES + name;
376
+ return (this.propertyDefinitions() || []).some(d => d.code === name && d.uri && d.uri !== uri);
377
+ }
378
+
318
379
  /**
319
380
  * @param {Languages} languages - Language specification
320
381
  * @returns {boolean} Whether any displays are available for the languages
@@ -421,6 +482,16 @@ class FhirCodeSystemProvider extends BaseCSServices {
421
482
  };
422
483
  }
423
484
 
485
+ // a code the code system doesn't define, in a code space it reserves for users to define their own
486
+ // codes, is a valid code. An extension-codes supplement in scope may define it (display etc); if
487
+ // none does, it has no display
488
+ if (this.codeSystem.isInReservedCodeSpace(code)) {
489
+ return {
490
+ context: new FhirCodeSystemProviderContext(code, { code: code }, true),
491
+ message: null
492
+ };
493
+ }
494
+
424
495
  return {
425
496
  context: null,
426
497
  message: undefined
@@ -468,6 +539,11 @@ class FhirCodeSystemProvider extends BaseCSServices {
468
539
  if (supplementDisplay) {
469
540
  return supplementDisplay;
470
541
  }
542
+ // a code in the reserved code space has the display an extension-codes supplement gives it,
543
+ // whatever languages were asked for - or, if none does, the code space's default display
544
+ if (ctxt.reserved) {
545
+ return this._extensionCodeDisplay(ctxt.code) || this.codeSystem.reservedCodeSpaceFor(ctxt.code).defaultDisplay;
546
+ }
471
547
 
472
548
  // Use language-aware display logic
473
549
  if (this.opContext.langs && !this.opContext.langs.isEnglishOrNothing()) {
@@ -524,14 +600,10 @@ class FhirCodeSystemProvider extends BaseCSServices {
524
600
  return false;
525
601
  }
526
602
 
527
- // Check for abstract property
603
+ // Check for the notSelectable property, by whatever code this code system gives it
528
604
  if (ctxt.concept.property && Array.isArray(ctxt.concept.property)) {
529
- const abstractProp = ctxt.concept.property.find(p =>
530
- p.code === 'abstract' ||
531
- p.code === 'not-selectable' ||
532
- p.code === 'notSelectable' ||
533
- p.uri === 'http://hl7.org/fhir/concept-properties#notSelectable'
534
- );
605
+ const codes = this.standardPropertyCodes('notSelectable', ['abstract', 'not-selectable']);
606
+ const abstractProp = ctxt.concept.property.find(p => codes.has(p.code));
535
607
  if (abstractProp && abstractProp.valueBoolean) {
536
608
  return true;
537
609
  }
@@ -735,8 +807,17 @@ class FhirCodeSystemProvider extends BaseCSServices {
735
807
  }
736
808
  }
737
809
 
810
+ // a code in the reserved code space that no extension-codes supplement in scope gives a display has
811
+ // its code space's default display ('User-assigned'). It is a placeholder, so (as for ISO 3166's
812
+ // user-assigned codes) another display is not wrong - see validate.js checkDisplays
813
+ if (ctxt.reserved && !this._extensionCodeDisplay(ctxt.code)) {
814
+ displays.userDefined = true;
815
+ const displayLang = this.defaultLanguage ? this.defaultLanguage.toString() : null;
816
+ displays.addDesignation(true, 'active', displayLang, CodeSystem.makeUseForDisplay(), this.codeSystem.reservedCodeSpaceFor(ctxt.code).defaultDisplay);
817
+ }
818
+
738
819
  // Add supplement designations
739
- this._listSupplementDesignations(ctxt.code, displays);
820
+ this._listSupplementDesignations(ctxt.code, displays, ctxt.reserved);
740
821
  }
741
822
 
742
823
  /**
@@ -1093,9 +1174,10 @@ class FhirCodeSystemProvider extends BaseCSServices {
1093
1174
  * @param {boolean} iterate - True if results will be iterated
1094
1175
  * @returns {FilterExecutionContext} Filter context
1095
1176
  */
1096
- async getPrepContext(iterate) {
1097
-
1098
- return new FilterExecutionContext(iterate);
1177
+ async getPrepContext(iterate, languages = null) {
1178
+ const result = new FilterExecutionContext(iterate);
1179
+ result.languages = languages; // what the request asked for (a text filter searches one language)
1180
+ return result;
1099
1181
  }
1100
1182
 
1101
1183
  /**
@@ -1122,7 +1204,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1122
1204
  }
1123
1205
  // Supported hierarchy filters
1124
1206
  if ((prop === 'concept' || prop === 'code') &&
1125
- ['is-a', 'descendent-of', 'is-not-a', 'child-of', 'in', '=', 'regex'].includes(op)) {
1207
+ ['is-a', 'descendent-of', 'descendent-leaf', 'is-not-a', 'child-of', 'in', 'not-in', '=', 'regex'].includes(op)) {
1126
1208
  return true;
1127
1209
  }
1128
1210
 
@@ -1265,13 +1347,16 @@ class FhirCodeSystemProvider extends BaseCSServices {
1265
1347
  const results = new FhirCodeSystemProviderFilterContext();
1266
1348
  const searchTerm = foldSearchText(filter.filter);
1267
1349
  const searchWords = searchTerm.split(/[^\p{L}\p{N}]+/u).filter(word => word !== '');
1350
+ // the display, definition and designations in one language only (see searchLanguage),
1351
+ // with the designations that supplements in use give in that language
1352
+ const lang = this.searchLanguage(filterContext.languages);
1268
1353
 
1269
1354
  // Search through all concepts
1270
1355
  const allConcepts = this.codeSystem.getAllConcepts();
1271
1356
 
1272
1357
  for (const concept of allConcepts) {
1273
1358
  this.opContext.deadCheck('cs:searchFilter');
1274
- const rating = this._calculateSearchRating(concept, searchTerm, searchWords);
1359
+ const rating = this._calculateSearchRating(concept, searchTerm, searchWords, lang);
1275
1360
  if (rating > 0) {
1276
1361
  results.add(concept, rating);
1277
1362
  }
@@ -1298,8 +1383,20 @@ class FhirCodeSystemProvider extends BaseCSServices {
1298
1383
  * @returns {number} Rating (0 = no match, higher = better match)
1299
1384
  * @private
1300
1385
  */
1301
- _calculateSearchRating(concept, searchTerm, searchWords) {
1302
- const texts = conceptSearchTexts(concept);
1386
+ _calculateSearchRating(concept, searchTerm, searchWords, lang = null) {
1387
+ const all = conceptSearchTexts(concept);
1388
+ let texts = all;
1389
+ if (lang) {
1390
+ const csLang = this.defLang();
1391
+ const inLang = CodeSystemProvider.inSearchLanguage(csLang, lang);
1392
+ texts = {
1393
+ code: all.code,
1394
+ display: inLang ? all.display : null,
1395
+ definition: inLang ? all.definition : null,
1396
+ designations: all.designations.filter(d => CodeSystemProvider.inSearchLanguage(d.language, lang, csLang))
1397
+ .concat(this.#supplementSearchTexts(concept.code, lang))
1398
+ };
1399
+ }
1303
1400
  const display = texts.display;
1304
1401
  let rating = 0;
1305
1402
 
@@ -1350,6 +1447,55 @@ class FhirCodeSystemProvider extends BaseCSServices {
1350
1447
  return rating;
1351
1448
  }
1352
1449
 
1450
+ /**
1451
+ * As for any code system (CodeSystemProvider.searchLanguage), but whether there is text in
1452
+ * a language is decided by what the code system actually has: its own language, a designation
1453
+ * in it, or a supplement's (hasAnyDisplays says yes to English whatever the code system's
1454
+ * language is)
1455
+ */
1456
+ searchLanguage(languages) {
1457
+ for (const lang of (languages && languages.languages) || []) {
1458
+ if (lang && lang.language && lang.code !== '*' && this.#hasSearchText(lang)) {
1459
+ return lang;
1460
+ }
1461
+ }
1462
+ return new Language(this.defLang());
1463
+ }
1464
+
1465
+ #hasSearchText(lang) {
1466
+ const csLang = this.defLang();
1467
+ if (CodeSystemProvider.inSearchLanguage(csLang, lang)) {
1468
+ return true;
1469
+ }
1470
+ for (const concept of this.codeSystem.getAllConcepts()) {
1471
+ if ((concept.designation || []).some(d => d.value && CodeSystemProvider.inSearchLanguage(d.language, lang, csLang))) {
1472
+ return true;
1473
+ }
1474
+ }
1475
+ return this._hasSupplementText(lang);
1476
+ }
1477
+
1478
+ // the displays and designations that supplements in use give a code, in the search language
1479
+ #supplementSearchTexts(code, lang) {
1480
+ const result = [];
1481
+ for (const supplement of this.supplements || []) {
1482
+ const concept = supplement.getConceptByCode(code);
1483
+ if (!concept) {
1484
+ continue;
1485
+ }
1486
+ const suppLang = supplement.jsonObj.language || this.defLang();
1487
+ if (concept.display && CodeSystemProvider.inSearchLanguage(suppLang, lang)) {
1488
+ result.push(searchText(concept.display));
1489
+ }
1490
+ for (const d of concept.designation || []) {
1491
+ if (d.value && CodeSystemProvider.inSearchLanguage(d.language, lang, suppLang)) {
1492
+ result.push(searchText(d.value));
1493
+ }
1494
+ }
1495
+ }
1496
+ return result;
1497
+ }
1498
+
1353
1499
  /**
1354
1500
  * Execute a value set filter
1355
1501
  * @param {FilterExecutionContext} filterContext - Filter context
@@ -1399,9 +1545,10 @@ class FhirCodeSystemProvider extends BaseCSServices {
1399
1545
  }
1400
1546
  }
1401
1547
 
1402
- // Handle known special properties
1548
+ // Handle known special properties - unless the code system uses the code for a
1549
+ // property of its own, in which case the property filter above applies
1403
1550
  const knownProperties = ['notSelectable', 'status', 'inactive', 'deprecated'];
1404
- if (knownProperties.includes(prop)) {
1551
+ if (knownProperties.includes(prop) && !this.redefinesStandardProperty(prop)) {
1405
1552
  results = await this._handleKnownPropertyFilter(filterContext, prop, op, value);
1406
1553
  }
1407
1554
 
@@ -1439,7 +1586,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1439
1586
  */
1440
1587
  _isCacheableFilter(prop, op) {
1441
1588
  if (prop === 'concept' || prop === 'code') {
1442
- return op === 'is-a' || op === 'descendent-of' || op === 'is-not-a' || op === 'regex';
1589
+ return op === 'is-a' || op === 'descendent-of' || op === 'descendent-leaf' || op === 'is-not-a' || op === 'regex';
1443
1590
  }
1444
1591
  if (prop === 'child' && op === 'exists') {
1445
1592
  return true;
@@ -1478,6 +1625,19 @@ class FhirCodeSystemProvider extends BaseCSServices {
1478
1625
  // Find all descendants of the specified code
1479
1626
  const includeRoot = (op === 'is-a');
1480
1627
  await this._addDescendants(results, value, includeRoot);
1628
+ } else if (op === 'descendent-leaf') {
1629
+ // R5: the descendants that have no children of their own
1630
+ if (this.codeSystem.getConceptByCode(value)) {
1631
+ for (const code of this.codeSystem.getDescendants(value)) {
1632
+ this.opContext.deadCheck('cs:conceptFilter:descendent-leaf');
1633
+ if (code !== value && this.codeSystem.getChildren(code).length === 0) {
1634
+ const concept = this.codeSystem.getConceptByCode(code);
1635
+ if (concept) {
1636
+ results.add(concept, 0);
1637
+ }
1638
+ }
1639
+ }
1640
+ }
1481
1641
  } else if (op === 'child-of') {
1482
1642
  // Find all descendants of the specified code
1483
1643
  await this._addChildren(results, value);
@@ -1507,6 +1667,20 @@ class FhirCodeSystemProvider extends BaseCSServices {
1507
1667
  }
1508
1668
  }
1509
1669
  }
1670
+ else if (op === 'not-in') {
1671
+ // Every code except the listed ones (comma separated) - their descendants are not excluded
1672
+ const excludeSet = new Set(value.split(',').map(c => c.trim()));
1673
+ const allCodes = this.codeSystem.getAllCodes();
1674
+ for (const code of allCodes) {
1675
+ this.opContext.deadCheck('cs:conceptFilter:not-in');
1676
+ if (!excludeSet.has(code)) {
1677
+ const concept = this.codeSystem.getConceptByCode(code);
1678
+ if (concept) {
1679
+ results.add(concept, 0);
1680
+ }
1681
+ }
1682
+ }
1683
+ }
1510
1684
  else if (op === '=') {
1511
1685
  // Exact match
1512
1686
  const concept = this.codeSystem.getConceptByCode(value);
@@ -1517,7 +1691,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1517
1691
  else if (op === 'regex') {
1518
1692
  // Regular expression match
1519
1693
  try {
1520
- const regex = regexUtilities.compile('^' + value + '$');
1694
+ const regex = regexUtilities.compile('^(?:' + value + ')$'); // the whole value: a|b is ^(?:a|b)$, not ^a|b$
1521
1695
  const allCodes = this.codeSystem.getAllCodes();
1522
1696
  for (const code of allCodes) {
1523
1697
  this.opContext.deadCheck('cs:conceptFilter:regex');
@@ -1665,7 +1839,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1665
1839
  }
1666
1840
  else if (op === 'regex') {
1667
1841
  try {
1668
- const regex = regexUtilities.compile('^' + value + '$');
1842
+ const regex = regexUtilities.compile('^(?:' + value + ')$'); // the whole value: a|b is ^(?:a|b)$, not ^a|b$
1669
1843
  return properties.some(p => regex.test(this._getPropertyValue(p)));
1670
1844
  } catch (error) {
1671
1845
  return false;
@@ -1705,13 +1879,14 @@ class FhirCodeSystemProvider extends BaseCSServices {
1705
1879
  async _handleKnownPropertyFilter(filterContext, prop, op, value) {
1706
1880
  const results = new FhirCodeSystemProviderFilterContext();
1707
1881
  const allConcepts = this.codeSystem.getAllConcepts();
1882
+ const nsCodes = this.standardPropertyCodes('notSelectable', ['abstract', 'not-selectable']);
1708
1883
 
1709
1884
  for (const concept of allConcepts) {
1710
1885
  this.opContext.deadCheck('cs:knownPropertyFilter');
1711
1886
  let matches = false;
1712
1887
 
1713
1888
  if (prop === 'notSelectable') {
1714
- const abstractProp = (concept.property || []).find(p => p.code === 'abstract' || p.code === 'notSelectable' || p.uri === 'http://hl7.org/fhir/concept-properties#notSelectable');
1889
+ const abstractProp = (concept.property || []).find(p => nsCodes.has(p.code));
1715
1890
  let vv = abstractProp ? String(getValuePrimitive(abstractProp)) : null;
1716
1891
  if (op === '=') {
1717
1892
  matches = (vv === value);
@@ -245,7 +245,7 @@ class Iso4217Services extends CodeSystemProvider {
245
245
  return concept;
246
246
  }
247
247
  }
248
- return `Code '${code}' not found in filter set`;
248
+ return null;
249
249
  }
250
250
 
251
251
  async filterCheck(filterContext, set, concept) {