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.
Files changed (47) hide show
  1. package/CHANGELOG.md +72 -10
  2. package/ips/ips.js +251 -26
  3. package/ips/jurisdictions.js +65 -0
  4. package/ips/pipeline.js +67 -6
  5. package/ips/profile-editor.js +283 -4
  6. package/ips/readme.md +48 -5
  7. package/ips/render.js +317 -46
  8. package/ips/scripts.js +46 -0
  9. package/ips/store.js +55 -0
  10. package/library/daily-report.js +25 -15
  11. package/package.json +3 -3
  12. package/registry/api.js +179 -1
  13. package/registry/crawler.js +2 -0
  14. package/registry/documentation/api.md +68 -2
  15. package/registry/documentation/internals.md +4 -1
  16. package/registry/documentation/readme.md +3 -2
  17. package/registry/model.js +8 -0
  18. package/registry/openapi.yaml +84 -0
  19. package/registry/registry.js +53 -2
  20. package/translations/Messages.properties +1 -0
  21. package/tx/README.md +4 -0
  22. package/tx/closure/closure-store.js +14 -0
  23. package/tx/cm/cm-package.js +21 -15
  24. package/tx/cs/cs-api.js +3 -1
  25. package/tx/cs/cs-cs.js +59 -12
  26. package/tx/cs/cs-icf.js +47 -34
  27. package/tx/cs/cs-loinc.js +257 -3
  28. package/tx/cs/cs-snomed.js +544 -55
  29. package/tx/importers/csv-records.js +35 -0
  30. package/tx/importers/import-loinc.module.js +177 -41
  31. package/tx/importers/import-sct.module.js +131 -1
  32. package/tx/importers/subset-loinc.module.js +25 -6
  33. package/tx/library/codesystem.js +4 -1
  34. package/tx/library/designations.js +23 -30
  35. package/tx/library/tho-precedence.js +40 -0
  36. package/tx/openapi-operations.js +2 -1
  37. package/tx/provider.js +46 -29
  38. package/tx/sct/documentation.md +45 -21
  39. package/tx/sct/ecl.js +34 -11
  40. package/tx/sct/structures.js +15 -2
  41. package/tx/tx.fhir.org.yml +2 -2
  42. package/tx/vs/vs-package.js +17 -13
  43. package/tx/vs/vs-vsac.js +1 -1
  44. package/tx/workers/closure.js +20 -0
  45. package/tx/workers/expand.js +2 -2
  46. package/tx/workers/validate.js +10 -0
  47. package/utilities/make-sct-loinc-subset.js +250 -0
@@ -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
- html += `<p>${lastUpdatedText}. <a href="https://github.com/FHIR/ig-registry/blob/master/tx-registry-doco.md">Register your own server</a>`+
811
- ` - see <a href="https://build.fhir.org/ig/HL7/fhir-tx-ecosystem-ig/ecosystem.html">Documentation</a></p>`;
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
  */
@@ -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 left out by excludeUrls, by their own (unprefixed) id
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
- * Leave out the concept maps with any of these urls: they can't be fetched, they aren't
338
- * found by search, and they aren't used for translation. The FHIR core package's concept
339
- * maps are left out where hl7.terminology provides the same url (see
340
- * Provider.applyTHOPrecedence). Must be called once the provider is initialized - the
341
- * database is shared, so it isn't changed.
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>} urls
346
+ * @param {Map<string, Set<string>>} thoVersions - url -> the versions hl7.terminology provides
344
347
  */
345
- excludeUrls(urls) {
346
- const excluded = new Set();
348
+ keepPinnedOnly(thoVersions) {
349
+ const affected = new Set();
347
350
  for (const cm of this.conceptMapMap.values()) {
348
- if (cm && urls.has(cm.url)) {
349
- excluded.add(cm);
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 (excluded.has(cm)) {
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 excluded) {
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
- if (cm.providesTranslation(sourceSystem, sourceScope, targetScope, targetSystem)) {
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 abstract property
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 abstractProp = ctxt.concept.property.find(p =>
530
- p.code === 'abstract' ||
531
- p.code === 'not-selectable' ||
532
- p.code === 'notSelectable' ||
533
- p.uri === 'http://hl7.org/fhir/concept-properties#notSelectable'
534
- );
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 requested (or by default)
1031
- if (this._hasProp(props, 'property', true)) {
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 => p.code === 'abstract' || p.code === 'notSelectable' || p.uri === 'http://hl7.org/fhir/concept-properties#notSelectable');
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 not supported yet (barriers '.', facilitators '+')
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) and q4-n (extent of
18
- * problem), under the group concepts q1..q4. A position cannot be omitted; 8 (not specified) is
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: the property that reports the qualifier, the
41
- * scale (group concept) its values come from, and the label that introduces its value in the
42
- * display (none for b and s, whose qualifiers say what they are).
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
- { property: 'extent', scale: 'q1' },
50
- { property: 'nature', scale: 'q2' },
51
- { property: 'localization', scale: 'q3' }
52
- ],
53
- d: [
54
- { property: 'performance', scale: 'q4', label: 'performance' },
55
- { property: 'capacity', scale: 'q4', label: 'capacity' },
56
- { property: 'capacity-with-assistance', scale: 'q4', label: 'capacity with assistance' },
57
- { property: 'performance-without-assistance', scale: 'q4', label: 'performance without assistance' }
58
- ],
59
- e: null // barriers and facilitators: not supported yet
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).filter(q => q).flatMap(q => q.map(p => p.property)));
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 '.' and one to three
114
- * qualifier digits. An unknown category is a plain miss (no message); a known category
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 dot = code.indexOf('.');
123
- if (dot <= 0) {
132
+ const sep = code.search(/[.+]/);
133
+ if (sep <= 0) {
124
134
  return base;
125
135
  }
126
- const stemCode = code.substring(0, dot);
127
- const digits = code.substring(dot + 1);
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: `The ICF code '${code}' is not valid: qualifiers on ${COMPONENT_NAMES[component] || 'these'} codes are not supported` };
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 '.' (${names})`
144
- : `The ICF code '${code}' is not valid: ${COMPONENT_CODES[component]} takes 1 to ${positions.length} qualifier digits after the '.' (${names})` };
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++) {