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/token/token.js CHANGED
@@ -435,12 +435,19 @@ class TokenModule {
435
435
  this.router.post('/keys', this.requireAuth.bind(this), this.createApiKey.bind(this));
436
436
  this.router.delete('/keys/:id', this.requireAuth.bind(this), this.deleteApiKey.bind(this));
437
437
 
438
- // JSON API routes for other servers
439
- this.router.get('/api/validate/:key', this.validateApiKey.bind(this));
440
- this.router.post('/api/usage/:key', this.recordUsage.bind(this));
438
+ // JSON API routes for other servers. The key goes in a header (Authorization: Bearer, or
439
+ // X-API-Key), not the path, so that it doesn't end up in access logs
440
+ this.router.get('/api/validate', this.validateApiKey.bind(this));
441
+ this.router.post('/api/usage', this.recordUsage.bind(this));
441
442
 
442
443
  // Statistics
443
- this.router.get('/api/stats/:key', this.getUsageStats.bind(this));
444
+ this.router.get('/api/stats', this.getUsageStats.bind(this));
445
+ }
446
+
447
+ // The API key a request carries: Authorization: Bearer <key>, or X-API-Key: <key>
448
+ apiKeyFrom(req) {
449
+ const auth = /^Bearer\s+(\S+)$/i.exec(req.get('Authorization') || '');
450
+ return auth ? auth[1] : (req.get('X-API-Key') || null);
444
451
  }
445
452
 
446
453
  // Middleware
@@ -627,7 +634,7 @@ class TokenModule {
627
634
  try {
628
635
 
629
636
  try {
630
- const apiKey = req.params.key;
637
+ const apiKey = this.apiKeyFrom(req);
631
638
 
632
639
  if (!apiKey || !apiKey.startsWith('tk_')) {
633
640
  return res.status(400).json({
@@ -700,7 +707,7 @@ class TokenModule {
700
707
  try {
701
708
 
702
709
  try {
703
- const apiKey = req.params.key;
710
+ const apiKey = this.apiKeyFrom(req);
704
711
  const {count = 1} = req.body;
705
712
 
706
713
  if (!apiKey || !apiKey.startsWith('tk_')) {
@@ -737,8 +744,8 @@ class TokenModule {
737
744
  try {
738
745
 
739
746
  try {
740
- const apiKey = req.params.key;
741
- const days = Math.min(parseInt(req.query.days) || 30, 365); // Max 1 year
747
+ const apiKey = this.apiKeyFrom(req);
748
+ const days = Math.max(1, Math.min(parseInt(req.query.days) || 30, 365)); // Max 1 year
742
749
 
743
750
  if (!apiKey || !apiKey.startsWith('tk_')) {
744
751
  return res.status(400).json({error: 'Invalid key format'});
@@ -989,7 +996,7 @@ class TokenModule {
989
996
  async getApiKeyUsageStats(keyId, days = 30) {
990
997
  return new Promise((resolve, reject) => {
991
998
  this.db.all(`
992
- SELECT date, request_count, last_request_ip
999
+ SELECT date, request_count
993
1000
  FROM usage_stats
994
1001
  WHERE api_key_id = ? AND date >= date('now', '-${days} days')
995
1002
  ORDER BY date DESC
@@ -1134,7 +1141,7 @@ class TokenModule {
1134
1141
  <td><span class="${statusClass}">${status}</span></td>
1135
1142
  <td>
1136
1143
  ${key.is_active ?
1137
- `<button class="btn btn-sm btn-danger" onclick="deleteKey(${key.id}, '${escape(key.name)}')">Delete</button>` :
1144
+ `<button class="btn btn-sm btn-danger" onclick="deleteKey(${Number(key.id)})">Delete</button>` :
1138
1145
  '<span class="text-muted">-</span>'
1139
1146
  }
1140
1147
  </td>
@@ -181,6 +181,9 @@ CONCEPTMAP_GROUP_TARGET_PROPERTY_TYPE_NO_SYSTEM = Since no system has been provi
181
181
  CONCEPTMAP_GROUP_TARGET_PROPERTY_TYPE_UNKNOWN_SYSTEM = The code system {0} could not be found, so code values can''t be checked
182
182
  CONCEPTMAP_GROUP_TARGET_SERVER_SIDE = Target Code System {0} is only supported on the terminology server, so the target codes are not validated for performance reasons
183
183
  CONCEPTMAP_GROUP_TARGET_UNKNOWN = The Target Code System {0} is not fully defined and populated, and no targetScope is specified, so the target code checking will not be performed
184
+ TRANSLATE_TARGET_CS_UNKNOWN = The code system {0} is not known, and the code ''{1}'' has not been validated
185
+ TRANSLATE_SCT_EDITION_TOP_LEVEL_ONE = No translation: the only {0} ancestor of {1} is the top-level hierarchy concept {2}
186
+ TRANSLATE_SCT_EDITION_TOP_LEVEL_MANY = No translation: the only {0} ancestors of {1} are the top-level hierarchy concepts {2}
184
187
  CONCEPTMAP_SHAREABLE_EXTRA_MISSING = Published concept maps SHOULD conform to the ShareableConceptMap profile, which says that the element ConceptMap.{0} should be present, but it is not
185
188
  CONCEPTMAP_SHAREABLE_EXTRA_MISSING_HL7 = Concept maps published by HL7 SHALL conform to the ShareableConceptMap profile, which says that the element ConceptMap.{0} should be present, but it is not
186
189
  CONCEPTMAP_SHAREABLE_MISSING = Published concept maps SHOULD conform to the ShareableConceptMap profile, which says that the element ConceptMap.{0} is mandatory, but it is not present
@@ -377,6 +380,11 @@ HTA_LOINC_DESC = LOINC
377
380
  HTA_LOINC_MESSAGE = This material contains content from <a href="http://loinc.org">LOINC</a>. LOINC is copyright &copy; 1995-2020, Regenstrief Institute, Inc. and the Logical Observation Identifiers Names and Codes (LOINC) Committee and is available at no cost under the <a href="http://loinc.org/license">license</a>. LOINC&reg; is a registered United States trademark of Regenstrief Institute, Inc.
378
381
  HTA_SCT_DESC = SNOMED Clinical Terms&reg; (SNOMED CT&reg;)
379
382
  HTA_SCT_MESSAGE = This material contains content that is copyright of SNOMED International. Implementers of these specifications must have the appropriate SNOMED CT Affiliate license - for more information contact <a href="https://www.snomed.org/get-snomed">https://www.snomed.org/get-snomed</a> or <a href="mailto:info@snomed.org">info@snomed.org</a>.
383
+ ICD11_CLASSIFICATION_CODE_NONE = The classificationCode extension says ''{0}'', but ''{1}'' has no classification code
384
+ ICD11_CLASSIFICATION_CODE_NOT_ID = The classificationCode extension is only used when the code is an entity id, and ''{0}'' is not one (the entity id is ''{1}'')
385
+ ICD11_CLASSIFICATION_CODE_SYSTEM = The classificationCode extension is only defined for ICD-11 MMS and ICF codings, not {0}
386
+ ICD11_CLASSIFICATION_CODE_WRONG = The classificationCode extension says ''{0}'', but the classification code of ''{1}'' is ''{2}''
387
+ ICD11_PC_AXIS_NOT_MULTIPLE = ''{0}'' takes one value on its {1} axis, but has several: {2}
380
388
  ICD11_PC_NO_AXES = ''{0}'' cannot be postcoordinated onto ''{1}'': that concept declares no postcoordination axes
381
389
  ICD11_PC_VALUE_NOT_ON_AXIS = ''{0}'' is not a permitted value on any postcoordination axis of ''{1}''. That stem declares: {2}
382
390
  IG_DEPENDENCY_CAN_VERSION_ALONE = The canonical URL specifies the version ''{0}'', while the dependency does not. If a version is specified, it must be in the explicit version property
@@ -412,6 +420,7 @@ Internal_INT_Bad_Type = Unhandled fixed value type {0}
412
420
  INV_FAILED = Constraint failed: {0}: ''{1}''
413
421
  INV_FAILED_SOURCE = Constraint failed: {0}: ''{1}'' (defined in {2})
414
422
  INVALID_DISPLAY_NAME = Invalid displayLanguage: ''{0}''
423
+ LOOKUP_DISPLAY_LANGUAGE_FALLBACK = There is no display in the language ''{0}'' for the code ''{1}'' in {2}, so the display is in the default language ''{3}''
415
424
  Invalid_slicing__there_is_more_than_one_type_slice_at__but_one_of_them__has_min__1_so_the_other_slices_cannot_exist=Invalid slicing: there is more than one type slice at {0}, but one of them ({1}) has min = 1, so the other slices cannot exist
416
425
  JSON_COMMA_EXTRA = There is an extra comma at the end of the {0} in the JSON
417
426
  JSON_COMMA_MISSING = A Comma is missing in the JSON
package/tx/README.md CHANGED
@@ -148,6 +148,58 @@ The file is read from disk per request, so editing it is reflected immediately -
148
148
  this means the published file can be ahead of what the running server actually loaded, if it
149
149
  has been edited since startup.
150
150
 
151
+ ### Syndication feed
152
+
153
+ `syndication.enabled` publishes the content the library loads as a
154
+ [Terminology Syndication Feed](https://build.fhir.org/ig/FHIR/fhir-syndication-ig/en/)
155
+ (Atom, NCTS ASF profile), so that Ontoserver, other FHIRsmith servers and people can follow
156
+ what the server loads. It is off by default.
157
+
158
+ ```json
159
+ "syndication": { "enabled": true }
160
+ ```
161
+
162
+ **The store.** As the library loads its sources, each one is recorded in the syndication store,
163
+ `<data>/terminology-cache/syndication` (`syndication.json` plus `files/`). The store is defined
164
+ from the moment syndication is turned on: content loaded before then is not back-filled, but
165
+ since induction happens as content loads, everything the server loads after that is recorded
166
+ the first time the store sees it, with the time it was recorded. The store only grows -
167
+ entries are never changed or removed, there are no retract entries, and a later update to
168
+ the same content is a new entry (so `contentItemVersion` is not unique - a deliberate departure
169
+ from the IG). One store serves all the endpoints.
170
+
171
+ | Source | Entry | Link | Category |
172
+ |--------|-------|------|----------|
173
+ | `npm:` / `url:` packages, and each endpoint's core package | the package | `https://npm.fhir.org/web/{id}-{version}.tgz` (a `url:` source: its url) | `FHIR_Package` |
174
+ | `file:` / `folder:` | each resource that has a version | a copy in the store | `FHIR_CodeSystem` / `FHIR_ValueSet` / `FHIR_ConceptMap` |
175
+ | `snomed`, `loinc`, `rxnorm`, `ndc`, `unii`, `omop`, `icd11`, `cpt` (fragment only) | the file | `base.url` + file name | `SNOMED_CACHE`, `LOINC_DB` ... in `http://healthintersections.com.au/syndication/fhirsmith/1.0.0` |
176
+
177
+ Not syndicated: `internal:` sources (including VSAC), OCL, `#current`/build packages, resources
178
+ without a version, the full CPT database, and binaries that are not published at `base.url`
179
+ (checked with a HEAD request - a binary that is only local is a development build). Binary
180
+ entries carry `<rights>`. Every link carries `ncts:sha256Hash`: a package is downloaded once to
181
+ hash it, then discarded. Only the core packages are FHIR version specific; everything else
182
+ appears in every endpoint's feed, with that endpoint's `ncts:fhirVersion`.
183
+
184
+ Induction runs in the background, one item at a time, so it never holds up startup; a failure
185
+ is logged and tried again the next time the content loads.
186
+
187
+ **The feed** uses RFC 5005 archived feeds, 30 entries a page:
188
+
189
+ | URL | Content |
190
+ |-----|---------|
191
+ | `/{path}/syndication` | the newest entries (fewer than 30), linked to the newest archive |
192
+ | `/{path}/syndication/archive/{n}` | archive *n* (1 = oldest), linked back to *n*-1 - never changes once it exists |
193
+ | `/{path}/syndication/files/{name}` | a resource file copied into the store |
194
+
195
+ The links in the feed are built from the server's `hostName` (top level of config.json) as
196
+ `https://{hostName}{path}`, never from the request's Host header; only archives built that way
197
+ are served as cacheable forever. Without a `hostName` the links follow the request, and
198
+ archives are `no-cache`.
199
+
200
+ A browser (`Accept: text/html`) gets the feed as a page; `?_format=atom` forces Atom. The IG's
201
+ query-string filters are not supported. A **Syndication** item appears in the tx navigation bar.
202
+
151
203
  ## OpenAPI description
152
204
 
153
205
  Each R5 endpoint is described by an OpenAPI 3.1 spec (the R4 and R3 endpoints work the same
@@ -2,6 +2,9 @@ const fs = require('fs').promises;
2
2
  const sqlite3 = require('sqlite3').verbose();
3
3
  const { VersionUtilities } = require('../../library/version-utilities');
4
4
  const { ConceptMap } = require('../library/conceptmap');
5
+ // Key of the row written once the cache has been fully populated (see isComplete)
6
+ const CACHE_COMPLETE_KEY = 'cache-complete';
7
+
5
8
  // Columns that can be returned directly without parsing JSON
6
9
  const INDEXED_COLUMNS = ['id', 'url', 'version', 'date', 'description', 'name', 'publisher', 'status', 'title'];
7
10
 
@@ -113,6 +116,14 @@ class ConceptMapDatabase {
113
116
  db.run('CREATE INDEX idx_jurisdictions_code ON conceptmap_jurisdictions(code)');
114
117
  db.run('CREATE INDEX idx_systems_system ON conceptmap_systems(system)');
115
118
 
119
+ // Cache status (key-value) - holds the marker that population finished
120
+ db.run(`
121
+ CREATE TABLE cache_status (
122
+ key TEXT PRIMARY KEY,
123
+ value TEXT
124
+ )
125
+ `);
126
+
116
127
  db.close((err) => {
117
128
  if (err) {
118
129
  reject(new Error(`Failed to close database after creation: ${err.message}`));
@@ -125,6 +136,57 @@ class ConceptMapDatabase {
125
136
  });
126
137
  }
127
138
 
139
+ /**
140
+ * True if this cache was fully populated: markComplete() was reached. A cache
141
+ * whose population was cut short (the process killed part way through) has no
142
+ * marker - nor does one built before the marker existed, or a file too damaged
143
+ * to read - so it reports false.
144
+ * @returns {Promise<boolean>}
145
+ */
146
+ async isComplete() {
147
+ return new Promise((resolve) => {
148
+ const db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY, (err) => {
149
+ if (err) {
150
+ resolve(false);
151
+ return;
152
+ }
153
+ db.get('SELECT value FROM cache_status WHERE key = ?', [CACHE_COMPLETE_KEY], (err, row) => {
154
+ db.close(() => resolve(!err && row != null));
155
+ });
156
+ });
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Record that this cache has been fully populated. Called once, after the last
162
+ * concept map has been written
163
+ * @returns {Promise<void>}
164
+ */
165
+ async markComplete() {
166
+ return new Promise((resolve, reject) => {
167
+ const db = new sqlite3.Database(this.dbPath, (err) => {
168
+ if (err) {
169
+ reject(new Error(`Failed to open database: ${err.message}`));
170
+ return;
171
+ }
172
+ db.serialize(() => {
173
+ db.run('CREATE TABLE IF NOT EXISTS cache_status (key TEXT PRIMARY KEY, value TEXT)');
174
+ db.run(`INSERT INTO cache_status (key, value) VALUES (?, ?)
175
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
176
+ [CACHE_COMPLETE_KEY, new Date().toISOString()], (err) => {
177
+ db.close((closeErr) => {
178
+ if (err || closeErr) {
179
+ reject(new Error(`Failed to mark cache complete: ${(err || closeErr).message}`));
180
+ } else {
181
+ resolve();
182
+ }
183
+ });
184
+ });
185
+ });
186
+ });
187
+ });
188
+ }
189
+
128
190
  /**
129
191
  * Insert or update a single ConceptMap in the database
130
192
  * @param {Object} conceptMap - The ConceptMap resource
@@ -1,4 +1,6 @@
1
1
  const path = require('path');
2
+ const fs = require('fs').promises;
3
+ const { assertNoActiveContent } = require('../../library/narrative-check');
2
4
  const { AbstractConceptMapProvider } = require('./cm-api');
3
5
  const { PackageContentLoader } = require('../../library/package-manager');
4
6
  const { ConceptMapDatabase } = require('./cm-database');
@@ -20,7 +22,9 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
20
22
  super();
21
23
  validateParameter(packageLoader, "packageLoader", PackageContentLoader);
22
24
  this.packageLoader = packageLoader;
23
- this.dbPath = path.join(packageLoader.packageFolder, '.conceptmaps.db');
25
+ // -2: caches from before concept maps were checked for active content in their
26
+ // narrative are rebuilt (see initialize)
27
+ this.dbPath = path.join(packageLoader.packageFolder, '.conceptmaps-2.db');
24
28
  this.database = new ConceptMapDatabase(this.dbPath);
25
29
  this.conceptMapMap = new Map();
26
30
  this.initialized = false;
@@ -36,12 +40,23 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
36
40
  return;
37
41
  }
38
42
  await this.packageLoader.initialize();
39
-
40
- const dbExists = await this.database.exists();
43
+ await fs.rm(path.join(this.packageLoader.packageFolder, '.conceptmaps.db'), { force: true }); // the old cache
44
+
45
+ // A cache is only used once its population finished. One that was cut short
46
+ // (the process killed part way through) would otherwise be trusted forever,
47
+ // silently missing whatever hadn't been written yet
48
+ let dbExists = await this.database.exists();
49
+ if (dbExists && !(await this.database.isComplete())) {
50
+ require('../../library/logger').getInstance().child({ module: 'tx' })
51
+ .warn(`Package ${this.packageLoader.pid ? this.packageLoader.pid() : ''}: concept map cache ${this.dbPath} is incomplete - rebuilding it`);
52
+ await fs.rm(this.dbPath, { force: true });
53
+ dbExists = false;
54
+ }
41
55
 
42
56
  if (!dbExists) {
43
57
  await this.database.create();
44
58
  await this._populateDatabase();
59
+ await this.database.markComplete();
45
60
  }
46
61
 
47
62
  this.conceptMapMap = await this.database.loadAllConceptMaps();
@@ -68,6 +83,8 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
68
83
  const conceptMap = await this.packageLoader.loadFile(entry);
69
84
  if (conceptMap.url) {
70
85
  try {
86
+ // its narrative is shown as-is in a page, so it must not carry script
87
+ assertNoActiveContent(conceptMap);
71
88
  conceptMaps.push(new ConceptMap(conceptMap, this.packageLoader.fhirVersion())); // get in converted to R5 format
72
89
  } catch (e) {
73
90
  // one unacceptable concept map (e.g. one with contained resources, which the
package/tx/cs/cs-api.js CHANGED
@@ -67,19 +67,89 @@ class CodeSystemProvider {
67
67
  */
68
68
 
69
69
  /**
70
- * The display name of the code system - what $lookup reports as its name parameter, and
71
- * what a person reads. Every provider should override this; the fallback is the
72
- * versioned uri, which is not a name, and a provider that forgets ends up reporting
70
+ * The name of the code system (CodeSystem.name, or the provider's equivalent). Every
71
+ * provider should override this; the fallback is the versioned uri, which is not a name,
72
+ * and a provider that forgets ends up reporting
73
73
  * "http://snomed.info/sct|http://snomed.info/sct/731000124108/version/20230301" where
74
74
  * the others report "LOINC".
75
+ * @returns {string} name of the code system
76
+ */
77
+ name() { return this.system() + (this.version() ? "|"+this.version() : "") }
78
+
79
+ /**
80
+ * What $lookup reports as its `name` parameter - "a display name for the code system".
81
+ * That is the title, translated into the request's language where the provider can,
82
+ * falling back to name(). Only $lookup uses this; everything else keeps using name().
83
+ * The default has no title to offer, so it is name().
84
+ * @param {Languages} [languages] - the request's working languages (displayLanguage, else
85
+ * Accept-Language)
75
86
  * @returns {string} display name for the code system
76
87
  */
88
+ // eslint-disable-next-line no-unused-vars
89
+ lookupName(languages) { return this.name(); }
90
+
91
+ /**
92
+ * A second, shorter form of the code that some code systems also carry - ICD-11's short
93
+ * classification code (1A00) beside its code, the entity uri. $validate-code reports it as
94
+ * the classificationCode output parameter when the request used the code itself, and
95
+ * $expand always reports it as a contains property. Null (the default) when there is none.
96
+ * @param {CodeSystemProviderContext} context
97
+ * @returns {Promise<string|null>}
98
+ */
99
+ // eslint-disable-next-line no-unused-vars
100
+ async classificationCode(context) { return null; }
101
+
77
102
  /**
78
- * @param {Languages} [languages] - the request's working languages, for a provider whose name
79
- * is translated; the default ignores them
103
+ * The classificationCode $validate-code reports for a code as it was given: by default the
104
+ * concept's classificationCode, whatever spelling was used.
105
+ * @param {CodeSystemProviderContext} context the concept the code located
106
+ * @param {string} code the code as given
107
+ * @returns {Promise<string|null>}
80
108
  */
81
109
  // eslint-disable-next-line no-unused-vars
82
- name(languages) { return this.system() + (this.version() ? "|"+this.version() : "") }
110
+ async reportedClassificationCode(context, code) { return this.classificationCode(context); }
111
+
112
+ /**
113
+ * The code to put in an expansion for a concept the value set enumerated as `requested`.
114
+ * The default keeps the value set's own spelling; a code system whose normal form differs
115
+ * from what a value set may write (ICD-11: short code vs entity uri) returns code().
116
+ * @param {CodeSystemProviderContext} context
117
+ * @param {string} requested - the code as the value set wrote it
118
+ * @returns {Promise<string>}
119
+ */
120
+ // eslint-disable-next-line no-unused-vars
121
+ async expansionCode(context, requested) { return requested; }
122
+
123
+ /**
124
+ * Properties an expansion carries on every contains entry whatever the request asked
125
+ * for (each must also come back from properties()). None by default.
126
+ * @returns {string[]}
127
+ */
128
+ mandatoryExpansionProperties() { return []; }
129
+
130
+ /**
131
+ * Check the extensions on a Coding that this code system defines - ICD-11's
132
+ * classificationCode, say. Called by $validate-code once the coding's code has validated.
133
+ * Extensions the code system does not define are not its business, and are ignored.
134
+ *
135
+ * @param {Object} coding the Coding as given
136
+ * @param {CodeSystemProviderContext} context the concept the code located
137
+ * @returns {Promise<{index: number, msgId: string, params: string[]}[]>} one entry per problem,
138
+ * naming the extension (its index in coding.extension) and the message; none by default
139
+ */
140
+ // eslint-disable-next-line no-unused-vars
141
+ async checkCodingExtensions(coding, context) { return []; }
142
+
143
+ /**
144
+ * Things $validate-code should say about a code that is valid - for a postcoordinated
145
+ * expression, say, a qualifier the grammar allows but a rule of the code system does not
146
+ * (ICD-11: two values on an axis that takes one).
147
+ *
148
+ * @param {CodeSystemProviderContext} context the concept the code located
149
+ * @returns {Promise<{severity: string, msgId: string, params: string[]}[]>} none by default
150
+ */
151
+ // eslint-disable-next-line no-unused-vars
152
+ async codeIssues(context) { return []; }
83
153
 
84
154
  /**
85
155
  * @returns {string} uri for the code system
@@ -540,9 +610,11 @@ class CodeSystemProvider {
540
610
  * @param {CodeSystemProviderContext} ctxt the context to add properties for
541
611
  * @param {string[]} props the properties requested
542
612
  * @param {Parameters} params the parameters response to add to
613
+ * @param {TxParameters} [txParams] the parsed request parameters, for providers whose output
614
+ * depends on more than the property list (ICD-11's post-coordination-metadata)
543
615
  */
544
-
545
- async extendLookup(ctxt, props, params) { }
616
+ // eslint-disable-next-line no-unused-vars
617
+ async extendLookup(ctxt, props, params, txParams) { }
546
618
 
547
619
  // procedure getCDSInfo(card : TCDSHookCard; langList : THTTPLanguageList; baseURL, code, display : String); virtual;
548
620
 
@@ -954,15 +1026,19 @@ class CodeSystemFactoryProvider {
954
1026
  }
955
1027
 
956
1028
  /**
957
- * build and return a known concept map from the URL, if there is one.
1029
+ * Add any concept maps this code system implies for translating from source
1030
+ * (a system URI, with the version of the source coding, which may be empty)
1031
+ * to dest (a system URI, possibly with |version).
958
1032
  *
959
- * the conceptmap is never visible to a user; if it has an implicitSource, then
1033
+ * The conceptmap is never visible to a user; if it has an internalSource, then
960
1034
  * provider.getTranslations will be called when it's actually used
961
- * @param url
962
- * @param version
963
- * @returns {ConceptMap}
1035
+ * @param conceptMaps - the list to add to
1036
+ * @param source - the source system URI
1037
+ * @param sourceVersion - the version of the source coding (may be null)
1038
+ * @param dest - the target system URI, possibly with |version
964
1039
  */
965
- async findImplicitConceptMaps(conceptMaps, source, dest) {
1040
+ // eslint-disable-next-line no-unused-vars
1041
+ async findImplicitConceptMaps(conceptMaps, source, sourceVersion, dest) {
966
1042
  return null;
967
1043
  }
968
1044