fhirsmith 0.18.0 → 0.18.2
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 +72 -10
- package/ips/ips.js +251 -26
- package/ips/jurisdictions.js +65 -0
- package/ips/pipeline.js +67 -6
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +48 -5
- package/ips/render.js +317 -46
- package/ips/scripts.js +46 -0
- package/ips/store.js +55 -0
- package/library/daily-report.js +25 -15
- package/package.json +3 -3
- package/registry/api.js +179 -1
- package/registry/crawler.js +2 -0
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry.js +53 -2
- package/translations/Messages.properties +1 -0
- package/tx/README.md +4 -0
- package/tx/closure/closure-store.js +14 -0
- package/tx/cm/cm-package.js +21 -15
- package/tx/cs/cs-api.js +3 -1
- package/tx/cs/cs-cs.js +59 -12
- package/tx/cs/cs-icf.js +47 -34
- package/tx/cs/cs-loinc.js +257 -3
- package/tx/cs/cs-snomed.js +544 -55
- package/tx/importers/csv-records.js +35 -0
- package/tx/importers/import-loinc.module.js +177 -41
- package/tx/importers/import-sct.module.js +131 -1
- package/tx/importers/subset-loinc.module.js +25 -6
- package/tx/library/codesystem.js +4 -1
- package/tx/library/designations.js +23 -30
- package/tx/library/tho-precedence.js +40 -0
- package/tx/openapi-operations.js +2 -1
- package/tx/provider.js +46 -29
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +34 -11
- package/tx/sct/structures.js +15 -2
- package/tx/tx.fhir.org.yml +2 -2
- package/tx/vs/vs-package.js +17 -13
- package/tx/vs/vs-vsac.js +1 -1
- package/tx/workers/closure.js +20 -0
- package/tx/workers/expand.js +2 -2
- package/tx/workers/validate.js +10 -0
- package/utilities/make-sct-loinc-subset.js +250 -0
package/registry/registry.js
CHANGED
|
@@ -20,6 +20,7 @@ const { RegistryAuth } = require('./auth');
|
|
|
20
20
|
// parameters are ignored) because existing ecosystem clients rely on that.
|
|
21
21
|
const DISCOVERY_PARAMS = ['registry', 'server', 'fhirVersion', 'url', 'authoritativeOnly', 'language'];
|
|
22
22
|
const RESOLVE_PARAMS = ['fhirVersion', 'url', 'version', 'valueSet', 'authoritativeOnly', 'language', 'usage'];
|
|
23
|
+
const CHOOSE_SERVER_PARAMS = ['fhirVersion', 'system', 'default', 'valueSet', 'usage', 'language'];
|
|
23
24
|
|
|
24
25
|
// A link to a URL from a registry list or a crawled server: those are other people's
|
|
25
26
|
// content, so only an http(s) URL is linked, and it is escaped
|
|
@@ -325,6 +326,8 @@ class RegistryModule {
|
|
|
325
326
|
// Main registry page
|
|
326
327
|
this.router.get('/', apiLimiter, apiGuard, session, this.handleMainPage.bind(this));
|
|
327
328
|
this.router.get('/resolve', apiLimiter, apiGuard, session, this.handleResolveEndpoint.bind(this));
|
|
329
|
+
// JSON only - there's no page for a browser, so it's always guarded
|
|
330
|
+
this.router.get('/choose-server', apiLimiter, guard, this.handleChooseServerEndpoint.bind(this));
|
|
328
331
|
this.router.get('/log', session, this.handleLogEndpoint.bind(this));
|
|
329
332
|
this.router.get('/software', session, this.handleSoftwarePage.bind(this));
|
|
330
333
|
|
|
@@ -807,8 +810,15 @@ class RegistryModule {
|
|
|
807
810
|
const lastRunDate = new Date(stats.lastRun);
|
|
808
811
|
lastUpdatedText = `Last Updated: ${lastRunDate.toLocaleString()}`;
|
|
809
812
|
}
|
|
810
|
-
|
|
811
|
-
|
|
813
|
+
// what goes here depends on the ecosystem, so it comes from the master registration file
|
|
814
|
+
html += `<p>${lastUpdatedText}.`;
|
|
815
|
+
if (data.registration) {
|
|
816
|
+
html += ` <a href="${safeHref(data.registration)}">Register your own server</a>`;
|
|
817
|
+
}
|
|
818
|
+
if (data.ecosystemDoco) {
|
|
819
|
+
html += `${data.registration ? ' -' : ''} see <a href="${safeHref(data.ecosystemDoco)}">Documentation</a>`;
|
|
820
|
+
}
|
|
821
|
+
html += '</p>';
|
|
812
822
|
|
|
813
823
|
html += '<table class="grid">';
|
|
814
824
|
html += '<thead><tr>';
|
|
@@ -1503,6 +1513,46 @@ class RegistryModule {
|
|
|
1503
1513
|
}
|
|
1504
1514
|
}
|
|
1505
1515
|
|
|
1516
|
+
/**
|
|
1517
|
+
* GET /choose-server: which one server to use for a request involving a set of code
|
|
1518
|
+
* systems (see RegistryAPI.chooseServer). system repeats; the other parameters take their
|
|
1519
|
+
* first value
|
|
1520
|
+
*/
|
|
1521
|
+
handleChooseServerEndpoint(req, res) {
|
|
1522
|
+
const start = Date.now();
|
|
1523
|
+
try {
|
|
1524
|
+
const params = this._normalizeQueryParams(req.query);
|
|
1525
|
+
const {fhirVersion, valueSet, usage, language} = params;
|
|
1526
|
+
const defaultServer = params.default;
|
|
1527
|
+
const raw = req.query.system;
|
|
1528
|
+
const systems = (raw == null ? [] : Array.isArray(raw) ? raw : [raw]).map(String).filter(s => s);
|
|
1529
|
+
|
|
1530
|
+
if (!fhirVersion) {
|
|
1531
|
+
return res.status(400).json({error: 'A FHIR version is required'});
|
|
1532
|
+
}
|
|
1533
|
+
if (!defaultServer || !this._isValidUrl(defaultServer, ['http:', 'https:'])) {
|
|
1534
|
+
return res.status(400).json({error: 'A default server (an http or https URL) is required'});
|
|
1535
|
+
}
|
|
1536
|
+
for (const system of systems) {
|
|
1537
|
+
if (!this._isValidUrl(system.split('|')[0])) {
|
|
1538
|
+
return res.status(400).json({error: 'Invalid code system URL format'});
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
if (valueSet && !this._isValidUrl(valueSet.split('|')[0])) {
|
|
1542
|
+
return res.status(400).json({error: 'Invalid value set URL format'});
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const result = this.api.chooseServer(fhirVersion, systems, defaultServer, valueSet, usage, language);
|
|
1546
|
+
this.logger.info(`Chose ${result.server} for ${systems.join(', ') || '(no systems)'} (FHIR ${fhirVersion}, usage=${usage}, language=${language || 'none'})`);
|
|
1547
|
+
res.json(result);
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
this.logger.error('Error in choose-server endpoint:', error);
|
|
1550
|
+
res.status(400).json({error: error.message});
|
|
1551
|
+
} finally {
|
|
1552
|
+
this.stats.countRequest('choose-server', Date.now() - start);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1506
1556
|
buildResolveResultContent(result, fhirVersion, resourceUrl, usage, language) {
|
|
1507
1557
|
let html = '';
|
|
1508
1558
|
|
|
@@ -1833,6 +1883,7 @@ class RegistryModule {
|
|
|
1833
1883
|
|
|
1834
1884
|
RegistryModule.DISCOVERY_PARAMS = DISCOVERY_PARAMS;
|
|
1835
1885
|
RegistryModule.RESOLVE_PARAMS = RESOLVE_PARAMS;
|
|
1886
|
+
RegistryModule.CHOOSE_SERVER_PARAMS = CHOOSE_SERVER_PARAMS;
|
|
1836
1887
|
|
|
1837
1888
|
module.exports = RegistryModule;
|
|
1838
1889
|
module.exports.commonBaseUrl = commonBaseUrl;
|
|
@@ -1129,6 +1129,7 @@ CLOSURE_NAME_NOT_UUID = The closure table name ''{0}'' is not valid: this server
|
|
|
1129
1129
|
CLOSURE_PARAM_UNKNOWN = The parameter ''{0}'' is not supported by $closure on this server
|
|
1130
1130
|
CLOSURE_CONCEPT_AND_VERSION = A $closure request can add concepts or resynchronise from a version, but not both at once
|
|
1131
1131
|
CLOSURE_RESET_AND_VERSION = A $closure request can reset the closure table or resynchronise from a version, but not both at once
|
|
1132
|
+
CLOSURE_DISPOSE_NOT_ALONE = A $closure request that disposes of the closure table can only name the table: it cannot also add concepts, resynchronise, reset or configure it
|
|
1132
1133
|
CLOSURE_TABLE_EXISTS = The closure table ''{0}'' already exists. To discard its content and start again, include the parameter reset = true
|
|
1133
1134
|
CLOSURE_TABLE_UNKNOWN = The closure table ''{0}'' does not exist on this server. Initialise it first, with a $closure request that names it and has no concepts
|
|
1134
1135
|
CLOSURE_CONFIG_NOT_ALLOWED = The parameter ''{0}'' configures a closure table, so it can only be supplied when the table is initialised (or reset)
|
package/tx/README.md
CHANGED
|
@@ -312,6 +312,10 @@ THO's v3-ActCode code system is `CodeSystem/tho-v3-ActCode`. No resource is serv
|
|
|
312
312
|
- `core` is the FHIR core package each endpoint loads for its FHIR version (`CodeSystem/core-administrative-gender`), and `x` is the code systems the server implements natively (`CodeSystem/x-...`); neither can be given as a prefix
|
|
313
313
|
- Sources that only provide code systems natively (`internal`, `snomed`, `loinc` etc.) don't serve resources by id, so a prefix does nothing for them
|
|
314
314
|
|
|
315
|
+
#### hl7.terminology and the FHIR core packages
|
|
316
|
+
|
|
317
|
+
Much of the content of the FHIR core packages has moved to terminology.hl7.org (`hl7.terminology`), and its versions were reset when it moved (`coverage-selfpay` is 4.0.1 in `hl7.fhir.r4.core`, and 1.0.1 in `hl7.terminology`), so version order can't decide between the two. Where `hl7.terminology` provides a code system, value set or concept map with the same url as the endpoint's core package, THO's is always the one a reference with no version gets. The core package's versions are still available to a reference that names them (`http://hl7.org/fhir/concept-property-type|5.0.0`), unless THO provides that same version itself. Core value sets and concept maps that THO also provides are not read by id, found by search, or used for `$translate` unless asked for by version.
|
|
318
|
+
|
|
315
319
|
### Source Types
|
|
316
320
|
|
|
317
321
|
#### `internal` - Built-in Code Systems
|
|
@@ -286,6 +286,20 @@ class ClosureStore {
|
|
|
286
286
|
return tx();
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Delete a table, and everything in it. The caller holds the table's lock.
|
|
291
|
+
* @param {string} name
|
|
292
|
+
* @returns {boolean} whether there was a table to delete
|
|
293
|
+
*/
|
|
294
|
+
dropTable(name) {
|
|
295
|
+
const existing = this.stmt.getTable.get(name);
|
|
296
|
+
if (!existing) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
this.stmt.deleteTable.run(existing.id);
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
289
303
|
/**
|
|
290
304
|
* The parameters the table was initialised with, as Parameters.parameter entries.
|
|
291
305
|
*/
|
package/tx/cm/cm-package.js
CHANGED
|
@@ -8,6 +8,7 @@ const { VersionUtilities } = require('../../library/version-utilities');
|
|
|
8
8
|
const {validateParameter} = require("../../library/utilities");
|
|
9
9
|
const {ConceptMap} = require("../library/conceptmap");
|
|
10
10
|
const { applyIdPrefix, stripIdPrefix } = require('../library/resource-ids');
|
|
11
|
+
const { keepForPinnedReference } = require('../library/tho-precedence');
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Package-based ConceptMap provider using shared database layer
|
|
@@ -169,7 +170,7 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
169
170
|
|
|
170
171
|
// Only process each ConceptMap once (use URL to deduplicate)
|
|
171
172
|
const vsUrl = json.url;
|
|
172
|
-
if (seen.has(vsUrl)) {
|
|
173
|
+
if (seen.has(vsUrl) || this.#pinnedOnly.has(vs)) {
|
|
173
174
|
continue;
|
|
174
175
|
}
|
|
175
176
|
|
|
@@ -330,35 +331,39 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
330
331
|
return this.packageLoader.id();
|
|
331
332
|
}
|
|
332
333
|
|
|
333
|
-
// the concept maps
|
|
334
|
+
// the concept maps that keepPinnedOnly restricted, by their own (unprefixed) id, and themselves
|
|
334
335
|
#excludedIds = new Set();
|
|
336
|
+
#pinnedOnly = new Set();
|
|
335
337
|
|
|
336
338
|
/**
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
339
|
+
* The concept maps with these urls are hl7.terminology's to provide (see
|
|
340
|
+
* Provider.applyTHOPrecedence): from this (core) package they are only available to a
|
|
341
|
+
* reference that names their version, where THO doesn't provide that version itself. They
|
|
342
|
+
* aren't the default for their url, can't be read by id, aren't found by search, and aren't
|
|
343
|
+
* used for translation unless asked for. Must be called once the provider is initialized -
|
|
344
|
+
* the database is shared, so it isn't changed.
|
|
342
345
|
*
|
|
343
|
-
* @param {Set<string
|
|
346
|
+
* @param {Map<string, Set<string>>} thoVersions - url -> the versions hl7.terminology provides
|
|
344
347
|
*/
|
|
345
|
-
|
|
346
|
-
const
|
|
348
|
+
keepPinnedOnly(thoVersions) {
|
|
349
|
+
const affected = new Set();
|
|
347
350
|
for (const cm of this.conceptMapMap.values()) {
|
|
348
|
-
if (cm &&
|
|
349
|
-
|
|
351
|
+
if (cm && thoVersions.has(cm.url)) {
|
|
352
|
+
affected.add(cm);
|
|
350
353
|
}
|
|
351
354
|
}
|
|
352
355
|
for (const [key, cm] of [...this.conceptMapMap]) {
|
|
353
|
-
if (
|
|
356
|
+
if (affected.has(cm) && !keepForPinnedReference(key, cm, thoVersions.get(cm.url))) {
|
|
354
357
|
this.conceptMapMap.delete(key);
|
|
355
358
|
}
|
|
356
359
|
}
|
|
357
|
-
for (const cm of
|
|
360
|
+
for (const cm of affected) {
|
|
361
|
+
this.#pinnedOnly.add(cm);
|
|
358
362
|
this.#excludedIds.add(stripIdPrefix(cm.id, this.spaceId) ?? cm.id);
|
|
359
363
|
}
|
|
360
364
|
}
|
|
361
365
|
|
|
366
|
+
|
|
362
367
|
prefixIds() {
|
|
363
368
|
this.#applyIdPrefix();
|
|
364
369
|
}
|
|
@@ -371,7 +376,8 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
371
376
|
|
|
372
377
|
async findConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem) {
|
|
373
378
|
for (let cm of this.conceptMapMap.values()) {
|
|
374
|
-
|
|
379
|
+
// a concept map that THO also provides is only used when it's asked for by version
|
|
380
|
+
if (!this.#pinnedOnly.has(cm) && cm.providesTranslation(sourceSystem, sourceScope, targetScope, targetSystem)) {
|
|
375
381
|
conceptMaps.push(cm);
|
|
376
382
|
}
|
|
377
383
|
}
|
package/tx/cs/cs-api.js
CHANGED
|
@@ -1036,9 +1036,11 @@ class CodeSystemFactoryProvider {
|
|
|
1036
1036
|
* @param source - the source system URI
|
|
1037
1037
|
* @param sourceVersion - the version of the source coding (may be null)
|
|
1038
1038
|
* @param dest - the target system URI, possibly with |version
|
|
1039
|
+
* @param code - the code being translated, if known (under reverse, the target concept's
|
|
1040
|
+
* code), so a provider can leave out a map that has nothing to say about it
|
|
1039
1041
|
*/
|
|
1040
1042
|
// eslint-disable-next-line no-unused-vars
|
|
1041
|
-
async findImplicitConceptMaps(conceptMaps, source, sourceVersion, dest) {
|
|
1043
|
+
async findImplicitConceptMaps(conceptMaps, source, sourceVersion, dest, code = null) {
|
|
1042
1044
|
return null;
|
|
1043
1045
|
}
|
|
1044
1046
|
|
package/tx/cs/cs-cs.js
CHANGED
|
@@ -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.
|
|
@@ -315,6 +318,46 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
315
318
|
return this.codeSystem.jsonObj.property || null;
|
|
316
319
|
}
|
|
317
320
|
|
|
321
|
+
/**
|
|
322
|
+
* The codes this code system uses for one of the standard concept properties
|
|
323
|
+
* (http://hl7.org/fhir/concept-properties#<name>). A property declared with the
|
|
324
|
+
* standard uri counts whatever local code it was given. A code that the code
|
|
325
|
+
* system declares with a different uri is a different property, and does not
|
|
326
|
+
* count. A code the code system doesn't declare (or declares with no uri) is
|
|
327
|
+
* recognised by the code alone - the standard code, and any legacy aliases
|
|
328
|
+
*
|
|
329
|
+
* @param {string} name - the standard property code (e.g. notSelectable)
|
|
330
|
+
* @param {string[]} aliases - legacy codes also recognised when not declared otherwise
|
|
331
|
+
* @returns {Set<string>} the local codes for the property
|
|
332
|
+
*/
|
|
333
|
+
standardPropertyCodes(name, aliases = []) {
|
|
334
|
+
const uri = CONCEPT_PROPERTIES + name;
|
|
335
|
+
const defs = this.propertyDefinitions() || [];
|
|
336
|
+
const codes = new Set();
|
|
337
|
+
for (const d of defs) {
|
|
338
|
+
if (d.uri === uri) {
|
|
339
|
+
codes.add(d.code);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
for (const c of [name, ...aliases]) {
|
|
343
|
+
if (!defs.some(d => d.code === c && d.uri && d.uri !== uri)) {
|
|
344
|
+
codes.add(c);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return codes;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Whether the code system declares the code of a standard concept property for a
|
|
352
|
+
* property of its own (with a different uri), so that the code is not the standard one
|
|
353
|
+
* @param {string} name - the standard property code
|
|
354
|
+
* @returns {boolean}
|
|
355
|
+
*/
|
|
356
|
+
redefinesStandardProperty(name) {
|
|
357
|
+
const uri = CONCEPT_PROPERTIES + name;
|
|
358
|
+
return (this.propertyDefinitions() || []).some(d => d.code === name && d.uri && d.uri !== uri);
|
|
359
|
+
}
|
|
360
|
+
|
|
318
361
|
/**
|
|
319
362
|
* @param {Languages} languages - Language specification
|
|
320
363
|
* @returns {boolean} Whether any displays are available for the languages
|
|
@@ -524,14 +567,10 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
524
567
|
return false;
|
|
525
568
|
}
|
|
526
569
|
|
|
527
|
-
// Check for
|
|
570
|
+
// Check for the notSelectable property, by whatever code this code system gives it
|
|
528
571
|
if (ctxt.concept.property && Array.isArray(ctxt.concept.property)) {
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
p.code === 'not-selectable' ||
|
|
532
|
-
p.code === 'notSelectable' ||
|
|
533
|
-
p.uri === 'http://hl7.org/fhir/concept-properties#notSelectable'
|
|
534
|
-
);
|
|
572
|
+
const codes = this.standardPropertyCodes('notSelectable', ['abstract', 'not-selectable']);
|
|
573
|
+
const abstractProp = ctxt.concept.property.find(p => codes.has(p.code));
|
|
535
574
|
if (abstractProp && abstractProp.valueBoolean) {
|
|
536
575
|
return true;
|
|
537
576
|
}
|
|
@@ -1027,11 +1066,17 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1027
1066
|
return;
|
|
1028
1067
|
}
|
|
1029
1068
|
|
|
1030
|
-
// Add properties if
|
|
1031
|
-
|
|
1069
|
+
// Add the concept's properties: each one if it was asked for by its code (or '*', or nothing
|
|
1070
|
+
// was asked for). 'property' asks for all of them - it used to be the only thing that did, so
|
|
1071
|
+
// asking for a property by its own code returned nothing. A code the server doesn't know is
|
|
1072
|
+
// simply not matched
|
|
1073
|
+
{
|
|
1032
1074
|
const properties = await this.properties(ctxt);
|
|
1033
1075
|
if (properties) {
|
|
1034
1076
|
for (const property of properties) {
|
|
1077
|
+
if (!this._hasProp(props, property.code, true) && !this._hasProp(props, 'property', true)) {
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1035
1080
|
let parts = [];
|
|
1036
1081
|
parts.push({ name: 'code', valueCode: property.code });
|
|
1037
1082
|
|
|
@@ -1393,9 +1438,10 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1393
1438
|
}
|
|
1394
1439
|
}
|
|
1395
1440
|
|
|
1396
|
-
// Handle known special properties
|
|
1441
|
+
// Handle known special properties - unless the code system uses the code for a
|
|
1442
|
+
// property of its own, in which case the property filter above applies
|
|
1397
1443
|
const knownProperties = ['notSelectable', 'status', 'inactive', 'deprecated'];
|
|
1398
|
-
if (knownProperties.includes(prop)) {
|
|
1444
|
+
if (knownProperties.includes(prop) && !this.redefinesStandardProperty(prop)) {
|
|
1399
1445
|
results = await this._handleKnownPropertyFilter(filterContext, prop, op, value);
|
|
1400
1446
|
}
|
|
1401
1447
|
|
|
@@ -1699,13 +1745,14 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1699
1745
|
async _handleKnownPropertyFilter(filterContext, prop, op, value) {
|
|
1700
1746
|
const results = new FhirCodeSystemProviderFilterContext();
|
|
1701
1747
|
const allConcepts = this.codeSystem.getAllConcepts();
|
|
1748
|
+
const nsCodes = this.standardPropertyCodes('notSelectable', ['abstract', 'not-selectable']);
|
|
1702
1749
|
|
|
1703
1750
|
for (const concept of allConcepts) {
|
|
1704
1751
|
this.opContext.deadCheck('cs:knownPropertyFilter');
|
|
1705
1752
|
let matches = false;
|
|
1706
1753
|
|
|
1707
1754
|
if (prop === 'notSelectable') {
|
|
1708
|
-
const abstractProp = (concept.property || []).find(p =>
|
|
1755
|
+
const abstractProp = (concept.property || []).find(p => nsCodes.has(p.code));
|
|
1709
1756
|
let vv = abstractProp ? String(getValuePrimitive(abstractProp)) : null;
|
|
1710
1757
|
if (op === '=') {
|
|
1711
1758
|
matches = (vv === value);
|
package/tx/cs/cs-icf.js
CHANGED
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
* s body structures extent, nature, localization
|
|
13
13
|
* d activities/participation performance, capacity, then optionally capacity with
|
|
14
14
|
* assistance and performance without assistance
|
|
15
|
-
* e environmental factors
|
|
15
|
+
* e environmental factors one: a barrier after '.', or a facilitator after '+' (in
|
|
16
|
+
* place of the '.') - e310.2, e310+3
|
|
16
17
|
* Each digit must be a value of its position's scale, which the code system carries as the
|
|
17
|
-
* concepts q1-n (extent of impairment), q2-n (nature), q3-n (localization)
|
|
18
|
-
* problem),
|
|
18
|
+
* concepts q1-n (extent of impairment), q2-n (nature), q3-n (localization), q4-n (extent of
|
|
19
|
+
* problem), q5-n (barrier) and q6-n (facilitator, which has no 9), under the group concepts
|
|
20
|
+
* q1..q6. A position cannot be omitted; 8 (not specified) is
|
|
19
21
|
* what fills it, and 8 says nothing in the display. Each position is reported by $lookup as the
|
|
20
22
|
* property named in QUALIFIERS.
|
|
21
23
|
*
|
|
@@ -37,32 +39,40 @@ const { Issue } = require('../library/operation-outcome');
|
|
|
37
39
|
const { FhirCodeSystemProvider, FhirCodeSystemProviderContext, FhirCodeSystemProviderFilterContext } = require('./cs-cs');
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
|
-
* The qualifier positions of each component
|
|
41
|
-
* scale (group concept) its values come from, and the
|
|
42
|
-
* display (none
|
|
42
|
+
* The qualifier positions of each component, by the separator that introduces them: the
|
|
43
|
+
* property that reports the qualifier, the scale (group concept) its values come from, and the
|
|
44
|
+
* label that introduces its value in the display (none where the value says what it is).
|
|
45
|
+
* Only environmental factors take '+', which makes the qualifier a facilitator.
|
|
43
46
|
*/
|
|
44
47
|
const QUALIFIERS = {
|
|
45
|
-
b:
|
|
46
|
-
{ property: 'extent', scale: 'q1' }
|
|
47
|
-
|
|
48
|
-
s:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
48
|
+
b: {
|
|
49
|
+
'.': [{ property: 'extent', scale: 'q1' }]
|
|
50
|
+
},
|
|
51
|
+
s: {
|
|
52
|
+
'.': [
|
|
53
|
+
{ property: 'extent', scale: 'q1' },
|
|
54
|
+
{ property: 'nature', scale: 'q2' },
|
|
55
|
+
{ property: 'localization', scale: 'q3' }
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
d: {
|
|
59
|
+
'.': [
|
|
60
|
+
{ property: 'performance', scale: 'q4', label: 'performance' },
|
|
61
|
+
{ property: 'capacity', scale: 'q4', label: 'capacity' },
|
|
62
|
+
{ property: 'capacity-with-assistance', scale: 'q4', label: 'capacity with assistance' },
|
|
63
|
+
{ property: 'performance-without-assistance', scale: 'q4', label: 'performance without assistance' }
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
e: {
|
|
67
|
+
'.': [{ property: 'barrier', scale: 'q5' }],
|
|
68
|
+
'+': [{ property: 'facilitator', scale: 'q6' }]
|
|
69
|
+
}
|
|
60
70
|
};
|
|
61
71
|
|
|
62
72
|
const COMPONENT_NAMES = { b: 'body functions', s: 'body structures', d: 'activities and participation', e: 'environmental factors' };
|
|
63
|
-
const COMPONENT_CODES = { b: 'a body functions (b) code', s: 'a body structures (s) code', d: 'an activities and participation (d) code' };
|
|
73
|
+
const COMPONENT_CODES = { b: 'a body functions (b) code', s: 'a body structures (s) code', d: 'an activities and participation (d) code', e: 'an environmental factors (e) code' };
|
|
64
74
|
|
|
65
|
-
const QUALIFIER_PROPERTIES = new Set(Object.values(QUALIFIERS).
|
|
75
|
+
const QUALIFIER_PROPERTIES = new Set(Object.values(QUALIFIERS).flatMap(c => Object.values(c)).flatMap(q => q.map(p => p.property)));
|
|
66
76
|
|
|
67
77
|
/**
|
|
68
78
|
* A category with qualifiers. code and concept are the category's, so whatever is inherited
|
|
@@ -110,21 +120,22 @@ class IcfCodeSystemProvider extends FhirCodeSystemProvider {
|
|
|
110
120
|
// -- locating -------------------------------------------------------------------------
|
|
111
121
|
|
|
112
122
|
/**
|
|
113
|
-
* A code in the code system, or a selectable category followed by '.'
|
|
114
|
-
* qualifier digits. An unknown category is a plain miss
|
|
115
|
-
* with bad qualifiers says what is wrong with them.
|
|
123
|
+
* A code in the code system, or a selectable category followed by '.' (or, for an
|
|
124
|
+
* environmental factor, '+') and its qualifier digits. An unknown category is a plain miss
|
|
125
|
+
* (no message); a known category with bad qualifiers says what is wrong with them.
|
|
116
126
|
*/
|
|
117
127
|
async locate(code) {
|
|
118
128
|
const base = await super.locate(code);
|
|
119
129
|
if (base.context || typeof code !== 'string') {
|
|
120
130
|
return base;
|
|
121
131
|
}
|
|
122
|
-
const
|
|
123
|
-
if (
|
|
132
|
+
const sep = code.search(/[.+]/);
|
|
133
|
+
if (sep <= 0) {
|
|
124
134
|
return base;
|
|
125
135
|
}
|
|
126
|
-
const
|
|
127
|
-
const
|
|
136
|
+
const separator = code[sep];
|
|
137
|
+
const stemCode = code.substring(0, sep);
|
|
138
|
+
const digits = code.substring(sep + 1);
|
|
128
139
|
const stem = this.codeSystem.getConceptByCode(stemCode);
|
|
129
140
|
if (!stem) {
|
|
130
141
|
return { context: null, message: undefined };
|
|
@@ -133,15 +144,17 @@ class IcfCodeSystemProvider extends FhirCodeSystemProvider {
|
|
|
133
144
|
return { context: null, message: `The ICF code '${code}' is not valid: qualifiers can only follow a selectable ICF category, and '${stemCode}' is not one` };
|
|
134
145
|
}
|
|
135
146
|
const component = stemCode[0];
|
|
136
|
-
const positions = QUALIFIERS[component];
|
|
147
|
+
const positions = (QUALIFIERS[component] || {})[separator];
|
|
137
148
|
if (!positions) {
|
|
138
|
-
return { context: null, message:
|
|
149
|
+
return { context: null, message: separator === '+'
|
|
150
|
+
? `The ICF code '${code}' is not valid: only an environmental factor (e) can be a facilitator, written with '+'`
|
|
151
|
+
: `The ICF code '${code}' is not valid: qualifiers on ${COMPONENT_NAMES[component] || 'these'} codes are not supported` };
|
|
139
152
|
}
|
|
140
153
|
const names = positions.map(q => q.property).join(', ');
|
|
141
154
|
if (!new RegExp(`^[0-9]{1,${positions.length}}$`).test(digits)) {
|
|
142
155
|
return { context: null, message: positions.length === 1
|
|
143
|
-
? `The ICF code '${code}' is not valid: ${COMPONENT_CODES[component]} takes one qualifier digit after the '
|
|
144
|
-
: `The ICF code '${code}' is not valid: ${COMPONENT_CODES[component]} takes 1 to ${positions.length} qualifier digits after the '
|
|
156
|
+
? `The ICF code '${code}' is not valid: ${COMPONENT_CODES[component]} takes one qualifier digit after the '${separator}' (${names})`
|
|
157
|
+
: `The ICF code '${code}' is not valid: ${COMPONENT_CODES[component]} takes 1 to ${positions.length} qualifier digits after the '${separator}' (${names})` };
|
|
145
158
|
}
|
|
146
159
|
const qualifiers = [];
|
|
147
160
|
for (let i = 0; i < digits.length; i++) {
|