fhirsmith 0.18.0 → 0.18.1
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 +31 -1
- package/library/daily-report.js +25 -15
- package/package.json +2 -2
- package/translations/Messages.properties +1 -0
- package/tx/closure/closure-store.js +14 -0
- package/tx/cs/cs-api.js +3 -1
- package/tx/cs/cs-cs.js +8 -2
- package/tx/cs/cs-icf.js +47 -34
- package/tx/cs/cs-loinc.js +249 -2
- package/tx/cs/cs-snomed.js +292 -25
- package/tx/importers/csv-records.js +35 -0
- package/tx/importers/import-loinc.module.js +177 -41
- package/tx/importers/import-sct.module.js +66 -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/openapi-operations.js +2 -1
- package/tx/provider.js +10 -4
- package/tx/sct/structures.js +8 -2
- package/tx/tx.fhir.org.yml +2 -2
- 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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ All notable changes to Health Intersections FHIRsmith will be documented in this
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.18.1] - 2026-10-25
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- ICF (`http://hl7.org/fhir/sid/icf`): qualified codes now cover environmental factors (`e`) - a barrier after `.`, or a facilitator after `+` in place of the `.` (no `+9`), on the new `q5` (barrier) and `q6` (facilitator) scales. As a whole: A category may be followed by `.` and one qualifier digit per position, the positions depending on the component: extent for body functions (`b`); extent, nature and localization for body structures (`s`); performance, capacity, and optionally capacity with assistance and performance without assistance for activities and participation (`d`); one for environmental factors (`e`), a barrier after `.` or a facilitator after `+` in place of the `.`. Each digit is checked against its scale in the code system (`q1`-`q6`, added to the ICF code systems in fhir.tx.support with a `kind` property). A qualified code gets a generated display (8, not specified, is left out of it; `d` qualifiers are labelled `performance`, `capacity`, ...), its qualifiers as properties in `$lookup` (`extent`, `nature`, `localization`, `performance`, `capacity`, `capacity-with-assistance`, `performance-without-assistance`, `barrier`, `facilitator`), and sits under its category for subsumption and `is-a` filters. Value sets can control qualifiers with an `exists` `true`/`false` filter on any of those properties (8 counts as absent) and `qualified` `=` `false`. ICF is now marked as not closed. The behaviour is in `tx/cs/cs-icf.js`, a subclass of the CodeSystem provider that `FhirCodeSystemProvider.create()` picks by url - the way to give any code system loaded from a package behaviour of its own
|
|
14
|
+
- SNOMED CT alternate identifiers (`sct2_Identifier`, e.g. the LOINC Extension's LOINC codes): the importer reads them into the cache (cache format 18, written only when there are identifiers, so other caches stay 17). `$lookup` returns them as Coding properties named for the scheme (`loinc` = `http://loinc.org#...`), by default and when asked for. `$expand`: the filter `mapped:<scheme>` `exists` `true`/`false` (e.g. `mapped:30051010000102`) for concepts that do or don't have one. `$translate`: each module with identifiers defines an implicit map `<edition>/version/<date>?fhir_cm=<module>` (e.g. `?fhir_cm=11010000107`), `equivalent` in both directions, nominated by url or found from the systems (`targetSystem` `http://loinc.org` from a SNOMED CT coding, or `http://snomed.info/sct|<edition>` from a LOINC one)
|
|
15
|
+
- SNOMED CT test edition: a subset of the LOINC Extension (LOINC ontology, module `11010000107`) - twelve LOINC-coded concepts with their LOINC identifiers (`sct2_Identifier`), plus the module and identifier scheme concepts - generated from the LOINC Extension release by `utilities/make-sct-loinc-subset.js`. The test cache is now `sct_test_20250909d.cache` (2280 concepts)
|
|
16
|
+
- LOINC importer: mappings from LOINC codes, parts and answers to other code systems, in two new tables - `ExternalCodes` (system, version, code, display; one row per target) and `ExternalMappings` (LOINC code to target, with an equivalence). Read from `PartFile/PartRelatedCodeMapping.csv` (the file's own equivalence; the RadLex rows are left out for now), `LoincIeeeMedicalDeviceCodeMappingTable` (MDC, `urn:iso:std:iso:11073:10101`, the CF_CODE10 as the code and the REFID as display; IEEE's `broader`/`narrower` are from the LOINC side, so they become `narrower`/`wider`) and the `ExtCode*` columns of `AnswerList.csv` (`equivalent`; LOINC's `CMS/HCPCS` and `https://www.cdc.gov/phin` systems become `http://www.cms.gov/Medicare/Coding/HCPCSReleaseCodeSets` and `urn:oid:2.16.840.1.113883.6.238`). Both mapping files are optional. LOINC caches need rebuilding to get them; nothing uses them yet
|
|
17
|
+
- LOINC: the mappings to other code systems are used. `$lookup` returns them as Coding properties named for the map (`sct`, `chebi`, `mdc`, `race`...), with the equivalence as a subproperty, by default and when asked for. `$translate`: each target system has an implicit map `http://loinc.org/cm/<name>` - `loinc`, `ncbi` (PubChem), `sct`, `hcpcs`, `gene-names`, `rxnorm`, `chebi`, `ncbi-clinvar`, `ncbi-gene`, `ncbi-taxo`, `mdc`, `race` - that works in both directions (LOINC to the system, and the system to LOINC), found from the target system or nominated by url. Found from the systems, a map is only offered for a code it has a mapping for, so translations that a SNOMED CT edition answers (the LOINC Extension) are unchanged. Targets in other systems (`CMS LCDS`, `Unicode`) are not used yet. Code system providers' `findImplicitConceptMaps` now also gets the code being translated
|
|
18
|
+
- `$closure`: `dispose` - a request with `name` and `dispose` = `true` says the client has finished with the closure table, and the server deletes it (not in the specification; disposing of a table that doesn't exist is fine - it may already have been pruned). It can't be combined with concepts, a version, `reset` or configuration (`CLOSURE_DISPOSE_NOT_ALONE`, 400). Documented in the OpenAPI
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Global language packs: the `codesystem-supplement-type` code is now `global-lang-pack` (`lang-pack` is still accepted)
|
|
23
|
+
- `$lookup` on SNOMED CT: when the request names properties, only those are returned - effectiveTime, module, parent, child and the defining relationships are no longer added regardless
|
|
24
|
+
- Daily report: the title is now "{server} daily report for {date} covering the last {24 hours | n days}", and the summary is one line per item - `Versions:` (FHIRsmith and Node), `Uptime:` (this run, and the percentage over all recorded runs), `Space:` (the log files and the disk) and `Audit:`
|
|
25
|
+
- Tx test generator (`tx/tests/testcases-generator.js`): disabled suites and tests are left out, and `version` gates (including negated ones, `!4.0`) and `modes` lists are read as the Java runner reads them, so tests gated `!4.0` now run at R5. The rules are in `tx/tests/test-case-gates.js`
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Implicit ConceptMaps nominated by url are looked for in every version of a code system, not just the first loaded (a SNOMED CT edition's maps are named by its edition URI)
|
|
30
|
+
- LOINC importer (and subsetter): CSV files are read by record, not by line, so a quoted field with a line break in it is one record. LOINC 2.83's `ComponentHierarchyBySystem.csv` has one (the text of `LP409416-7`), and the import failed with `UNIQUE constraint failed: Config.ConfigKey` - the second half of the record looked like a second root
|
|
31
|
+
- `$expand`: a supplement that only supplied the chosen display or a designation - a global language pack, typically - was never reported as a `used-supplement`. Designations from a supplement carry it as `.supplement`, but the reporting looked for `.source`, which is never set
|
|
32
|
+
- `CodeSystem/$validate-code` with `useSupplement`: the supplement was always reported as missing (`VALUESET_SUPPLEMENT_MISSING`, 422), even when it was passed in `tx-resource`, because the supplements were only checked off while preparing a value set, and validating against a code system doesn't prepare one
|
|
33
|
+
- `$lookup` on a CodeSystem resource: a property asked for by its own code (`property=prop`) was not returned - only `property=property`, `*` or no `property` parameter returned the code system's properties. A property the server doesn't know is still ignored
|
|
34
|
+
|
|
35
|
+
### Tx Conformance Statement
|
|
36
|
+
|
|
37
|
+
FHIRsmith v0.18.1 passed all 4337 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed+mimetypes+icd-11+icf+closure, tests v1.10.0, runner v7.0.3)
|
|
38
|
+
|
|
8
39
|
## [0.18.0] - 2026-10-23
|
|
9
40
|
|
|
10
41
|
A big release: a new IPS module (prototype), a secured and documented registry with uptime reporting, a syndication feed for the terminology server, a daily email report, ICF qualified codes, SNOMED CT edition to edition translation and much more ECL, and the ICD-11 representation agreed at the October 2026 test-case review meetings. Plus the outcome of security reviews of the registry, IPS and token modules. The README now says what state each module is in.
|
|
@@ -43,7 +74,6 @@ Note: We are heading towards a 1.0.0 release.
|
|
|
43
74
|
- Home page and dashboard: the uptime box now also reports the server's overall uptime - `Uptime: 38m 16s (99.3% overall)` - the proportion of the time since the first recorded run that the server has been running. Each run is recorded in the statistics database (`server_runs` in `stats.db`) with a heartbeat every minute, so a crash loses at most a minute. Not shown when statistics are turned off
|
|
44
75
|
- `library/fhir-search-values.js`: the date range, prefix and comma-splitting parts of FHIR search, moved out of the testing module for sharing
|
|
45
76
|
- `library/package-manager.js`: a cached package folder with no `package/package.json` (a download that failed after the folder was made) is fetched again instead of being used
|
|
46
|
-
- ICF (`http://hl7.org/fhir/sid/icf`): qualified codes. A category may be followed by `.` and one qualifier digit per position, the positions depending on the component: extent for body functions (`b`); extent, nature and localization for body structures (`s`); performance, capacity, and optionally capacity with assistance and performance without assistance for activities and participation (`d`). Qualifiers on environmental factors (`e`) are not supported yet. Each digit is checked against its scale in the code system (`q1`-`q4`, added to the ICF code systems in fhir.tx.support with a `kind` property). A qualified code gets a generated display (8, not specified, is left out of it; `d` qualifiers are labelled `performance`, `capacity`, ...), its qualifiers as properties in `$lookup` (`extent`, `nature`, `localization`, `performance`, `capacity`, `capacity-with-assistance`, `performance-without-assistance`), and sits under its category for subsumption and `is-a` filters. Value sets can control qualifiers with an `exists` `true`/`false` filter on any of those properties (8 counts as absent) and `qualified` `=` `false`. ICF is now marked as not closed. The behaviour is in `tx/cs/cs-icf.js`, a subclass of the CodeSystem provider that `FhirCodeSystemProvider.create()` picks by url - the way to give any code system loaded from a package behaviour of its own
|
|
47
77
|
- `$translate`: SNOMED CT edition to edition translation with no map - from an edition to one it depends on (per the module dependency reference set), using the implicit map `<edition>/version/<date>?fhir_cm=edition/<target module>` (the url agreed with the SNOMED committee), found from `targetSystem` or nominated by url. A concept in the target edition's modules is its own translation; an inactive concept translates through its historical associations; a fully defined extension concept translates to its normal form (or the target concept with that normal form), when that uses only target edition content; anything else translates to its proximal target-edition ancestors as `source-is-narrower-than-target`, unless those are only top-level hierarchies, which is reported as the reason there is no translation. The source concept in a match carries its version, as given
|
|
48
78
|
- `$translate`: when the target code a map directs to is not in its code system's normal form, the match has a `normalized-concept`. A source code that has a normal form is looked up both as given and in its normal form (a map entry written in the normal form matches any code with that normal form; the map's own entries are not normalised). A target in a code system the server doesn't know gets a `note` saying it was not validated, and a target code that is not valid gives an `error`
|
|
49
79
|
- `$lookup`: a warning when the `displayLanguage` asked for has no display for the code, and the display is in the default language (`LOOKUP_DISPLAY_LANGUAGE_FALLBACK`)
|
package/library/daily-report.js
CHANGED
|
@@ -85,6 +85,15 @@ function formatSpan(ms) {
|
|
|
85
85
|
return parts.join(' ');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/** "24 hours", "3 days" - for the report title; rounded, since it's a description of the period */
|
|
89
|
+
function describeSpan(ms) {
|
|
90
|
+
const hours = Math.round(ms / 3600000);
|
|
91
|
+
if (hours < 48) {
|
|
92
|
+
return plural(Math.max(1, hours), 'hour');
|
|
93
|
+
}
|
|
94
|
+
return plural(Math.round(hours / 24), 'day');
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
function plural(n, word) {
|
|
89
98
|
return `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
90
99
|
}
|
|
@@ -243,8 +252,7 @@ class DailyReport {
|
|
|
243
252
|
*/
|
|
244
253
|
async build(since, now = Date.now()) {
|
|
245
254
|
const parts = [];
|
|
246
|
-
parts.push(`# ${this.serverId} daily report`);
|
|
247
|
-
parts.push(`${localDateTime(now)}, covering the ${formatSpan(now - since)} since ${localDateTime(since)}.`);
|
|
255
|
+
parts.push(`# ${this.serverId} daily report for ${localDateTime(now)} covering the last ${describeSpan(now - since)}`);
|
|
248
256
|
parts.push('## Stability');
|
|
249
257
|
parts.push(this.stabilityParagraph(since, now));
|
|
250
258
|
for (const [name, module] of Object.entries(this.modules)) {
|
|
@@ -347,25 +355,26 @@ class DailyReport {
|
|
|
347
355
|
* the disk they're on.
|
|
348
356
|
*/
|
|
349
357
|
systemParagraph(now) {
|
|
350
|
-
|
|
351
|
-
|
|
358
|
+
// one line each; the two trailing spaces are markdown's line break
|
|
359
|
+
const lines = [];
|
|
360
|
+
lines.push(`Versions: FHIRsmith = v${this.version}. Node = ${process.version} (${os.type()} ${os.release()}, ${os.arch()})`);
|
|
352
361
|
if (this.stats && typeof this.stats.startTime === 'number') {
|
|
353
|
-
let up = `This run
|
|
362
|
+
let up = `Uptime: This run = ${formatSpan(now - this.stats.startTime)}`;
|
|
354
363
|
const overall = typeof this.stats.overallUptime === 'function' ? this.stats.overallUptime(now) : null;
|
|
355
364
|
if (overall) {
|
|
356
365
|
const pct = overall.percent >= 100 ? '100' : (Math.floor(overall.percent * 10) / 10).toFixed(1);
|
|
357
|
-
up +=
|
|
366
|
+
up += `. All runs, ${pct}%`;
|
|
358
367
|
}
|
|
359
|
-
|
|
368
|
+
lines.push(up);
|
|
360
369
|
}
|
|
361
|
-
|
|
370
|
+
lines.push(`Space: ${this.logsText()}`);
|
|
362
371
|
if (this.npmAudit && typeof this.npmAudit.reportText === 'function') {
|
|
363
372
|
const audit = this.npmAudit.reportText();
|
|
364
373
|
if (audit) {
|
|
365
|
-
|
|
374
|
+
lines.push(`Audit: ${audit}`);
|
|
366
375
|
}
|
|
367
376
|
}
|
|
368
|
-
return
|
|
377
|
+
return lines.join(' \n');
|
|
369
378
|
}
|
|
370
379
|
|
|
371
380
|
/**
|
|
@@ -388,7 +397,7 @@ class DailyReport {
|
|
|
388
397
|
return `${plural(failed.length, 'background task')} ${failed.length === 1 ? 'is' : 'are'} in a failed state:\n\n${lines.join('\n')}`;
|
|
389
398
|
}
|
|
390
399
|
|
|
391
|
-
|
|
400
|
+
logsText() {
|
|
392
401
|
const logDir = folders.logsDir();
|
|
393
402
|
try {
|
|
394
403
|
const files = fs.readdirSync(logDir).filter(f => f.endsWith('.log'));
|
|
@@ -402,13 +411,13 @@ class DailyReport {
|
|
|
402
411
|
const free = fstats.bavail * fstats.bsize;
|
|
403
412
|
const size = fstats.blocks * fstats.bsize;
|
|
404
413
|
const freePct = size > 0 ? Math.round((free * 100) / size) : 0;
|
|
405
|
-
disk =
|
|
414
|
+
disk = ` on a disk with ${formatBytes(free)} free of ${formatBytes(size)} (${freePct}% free)`;
|
|
406
415
|
} catch {
|
|
407
|
-
disk = '
|
|
416
|
+
disk = ' on a disk whose free space could not be read';
|
|
408
417
|
}
|
|
409
|
-
return
|
|
418
|
+
return `${plural(files.length, 'log file')} totalling ${formatBytes(total)}${disk}.`;
|
|
410
419
|
} catch (e) {
|
|
411
|
-
return `
|
|
420
|
+
return `the logs folder (${logDir}) could not be read: ${e.message}.`;
|
|
412
421
|
}
|
|
413
422
|
}
|
|
414
423
|
}
|
|
@@ -417,6 +426,7 @@ module.exports = DailyReport;
|
|
|
417
426
|
module.exports.nextMidnight = nextMidnight;
|
|
418
427
|
module.exports.localDate = localDate;
|
|
419
428
|
module.exports.formatSpan = formatSpan;
|
|
429
|
+
module.exports.describeSpan = describeSpan;
|
|
420
430
|
// for modules writing their paragraph: "3 packages", "1 package"
|
|
421
431
|
module.exports.plural = plural;
|
|
422
432
|
module.exports.localDateTime = localDateTime;
|
package/package.json
CHANGED
|
@@ -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)
|
|
@@ -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/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
|
@@ -1027,11 +1027,17 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1027
1027
|
return;
|
|
1028
1028
|
}
|
|
1029
1029
|
|
|
1030
|
-
// Add properties if
|
|
1031
|
-
|
|
1030
|
+
// Add the concept's properties: each one if it was asked for by its code (or '*', or nothing
|
|
1031
|
+
// was asked for). 'property' asks for all of them - it used to be the only thing that did, so
|
|
1032
|
+
// asking for a property by its own code returned nothing. A code the server doesn't know is
|
|
1033
|
+
// simply not matched
|
|
1034
|
+
{
|
|
1032
1035
|
const properties = await this.properties(ctxt);
|
|
1033
1036
|
if (properties) {
|
|
1034
1037
|
for (const property of properties) {
|
|
1038
|
+
if (!this._hasProp(props, property.code, true) && !this._hasProp(props, 'property', true)) {
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1035
1041
|
let parts = [];
|
|
1036
1042
|
parts.push({ name: 'code', valueCode: property.code });
|
|
1037
1043
|
|
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++) {
|