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/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
|
-
|
|
440
|
-
this.router.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 © 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® is a registered United States trademark of Regenstrief Institute, Inc.
|
|
378
381
|
HTA_SCT_DESC = SNOMED Clinical Terms® (SNOMED CT®)
|
|
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
|
package/tx/cm/cm-database.js
CHANGED
|
@@ -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
|
package/tx/cm/cm-package.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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
|
-
*
|
|
79
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
962
|
-
* @param
|
|
963
|
-
* @
|
|
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
|
-
|
|
1040
|
+
// eslint-disable-next-line no-unused-vars
|
|
1041
|
+
async findImplicitConceptMaps(conceptMaps, source, sourceVersion, dest) {
|
|
966
1042
|
return null;
|
|
967
1043
|
}
|
|
968
1044
|
|