fhirsmith 0.7.0 → 0.7.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 +16 -0
- package/README.md +13 -4
- package/configurations/readme.md +1 -0
- package/package.json +1 -1
- package/tx/html/conceptmap-operations.liquid +19 -0
- package/tx/library/renderer.js +571 -2
- package/tx/tx-html.js +48 -3
- package/tx/workers/read.js +23 -8
- package/tx/workers/search.js +36 -1
- package/tx/workers/validate.js +11 -6
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to the Health Intersections Node Server will be documented i
|
|
|
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
|
+
## [v0.7.1] - 2026-03-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Add web interface for ConceptMap
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Change status out parameter on $validate-code from string -> code
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Fix handling of markdown in release process
|
|
18
|
+
- OCL cache fixes
|
|
19
|
+
-
|
|
20
|
+
### Tx Conformance Statement
|
|
21
|
+
|
|
22
|
+
FHIRsmith passed all 1452 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.1-SNAPSHOT, runner v6.8.2)
|
|
23
|
+
|
|
8
24
|
## [v0.7.0] - 2026-03-13
|
|
9
25
|
|
|
10
26
|
### Added
|
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#  FHIRsmith - FHIR Server toolkit
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
3
|
This server provides a set of server-side services that are useful for the FHIR Community. The set of are two kinds of services:
|
|
6
4
|
|
|
7
5
|
## Modules useful to anyone in the community
|
|
@@ -31,17 +29,28 @@ in-build support for SSL, rate limiting etc.
|
|
|
31
29
|
|
|
32
30
|
There are 4 executable programs:
|
|
33
31
|
* the server (`node server`)
|
|
34
|
-
* the test cases (`npm test`)
|
|
35
32
|
* the terminology importer (`node --max-old-space-size=8192 tx/importers/tx-import XXX`) - see [Doco](tx/importers/readme.md)
|
|
33
|
+
* the test cases (`npm test`)
|
|
36
34
|
* the test cases generater (`node tx/tests/testcases-generator.js`)
|
|
37
35
|
|
|
36
|
+
Unless you're developing, you only need the first two
|
|
37
|
+
|
|
38
|
+
### Quick Start
|
|
39
|
+
|
|
40
|
+
* Install FHIRSmith (using docker, or an NPM release, or just get the code by git)
|
|
41
|
+
* Figure out the data directory
|
|
42
|
+
* Provide a configuration to tell the server what to run (see documentation below, or use a [prebuilt configuration]/configurations/readme.md)
|
|
43
|
+
* Run the server
|
|
44
|
+
|
|
45
|
+
For further details of these steps, read on
|
|
46
|
+
|
|
38
47
|
### Data Directory
|
|
39
48
|
|
|
40
49
|
The server separates code from runtime data. All databases, caches, logs, and downloaded
|
|
41
50
|
files are stored in a single data directory. The location is determined by:
|
|
42
51
|
|
|
43
52
|
1. The `FHIRSMITH_DATA_DIR` environment variable (if set)
|
|
44
|
-
2. Otherwise, defaults to `./data` relative to the working directory
|
|
53
|
+
2. Otherwise, defaults to `./data` relative to the working directory (development set up)
|
|
45
54
|
|
|
46
55
|
The data directory contains (depending on which modules are in use):
|
|
47
56
|
* `config.json` — server and module configuration
|
package/configurations/readme.md
CHANGED
|
@@ -3,3 +3,4 @@ This folder contains some basic starter configurations:
|
|
|
3
3
|
* Terminology server: see tx-config.json for a vanilla server that doesn't contain any licensed content
|
|
4
4
|
* NPM web server: see projector.json for a basic configuration to make a package available online
|
|
5
5
|
|
|
6
|
+
To use these, copy the relevant file to your local data directory, and rename to config.json
|
package/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
<div class="operation-form" style="margin-bottom: 15px;">
|
|
3
|
+
<strong>Translate</strong>
|
|
4
|
+
<form method="get" action="$translate" style="margin-left: 10px; margin-top: 5px;">
|
|
5
|
+
<input type="hidden" name="url" value="{{ url }}"/>
|
|
6
|
+
<table class="grid" cellpadding="0" cellspacing="0">
|
|
7
|
+
<tr>
|
|
8
|
+
<td>System:</td><td><select name="sourceSystem">{{ sources }}</select></td>
|
|
9
|
+
</tr>
|
|
10
|
+
<tr>
|
|
11
|
+
<td>Code:</td><td><select name="sourceCode">{{ codes }}</select></td>
|
|
12
|
+
</tr>
|
|
13
|
+
<tr>
|
|
14
|
+
<td>Target:</td><td><select name="targetSystem">{{ targets }}</select></td>
|
|
15
|
+
</tr>
|
|
16
|
+
</table>
|
|
17
|
+
<button type="submit" class="btn btn-sm btn-primary">Translate</button>
|
|
18
|
+
</form>
|
|
19
|
+
</div>
|
package/tx/library/renderer.js
CHANGED
|
@@ -317,8 +317,8 @@ class Renderer {
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
translate(msgId) {
|
|
321
|
-
return this.opContext.i18n.formatPhrase(msgId, this.opContext.langs,
|
|
320
|
+
translate(msgId, params = []) {
|
|
321
|
+
return this.opContext.i18n.formatPhrase(msgId, this.opContext.langs, params);
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
translatePlural(num, msgId) {
|
|
@@ -1640,6 +1640,575 @@ class Renderer {
|
|
|
1640
1640
|
}
|
|
1641
1641
|
}
|
|
1642
1642
|
}
|
|
1643
|
+
|
|
1644
|
+
// Methods to add to the Renderer class in renderer.js for ConceptMap rendering.
|
|
1645
|
+
// These follow the Java ConceptMapRenderer logic and use the same translated strings.
|
|
1646
|
+
|
|
1647
|
+
// ---- Add these methods to the Renderer class ----
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* Render a ConceptMap resource to HTML.
|
|
1651
|
+
* Follows the same pattern as renderValueSet/renderCodeSystem:
|
|
1652
|
+
* metadata table (reusing renderMetadataTable), then group-by-group rendering.
|
|
1653
|
+
*/
|
|
1654
|
+
async renderConceptMap(cm) {
|
|
1655
|
+
if (cm.json) {
|
|
1656
|
+
cm = cm.json;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
let div_ = div();
|
|
1660
|
+
|
|
1661
|
+
// Metadata table
|
|
1662
|
+
div_.h3().tx("Properties");
|
|
1663
|
+
await this.renderMetadataTable(cm, div_.table("grid"));
|
|
1664
|
+
|
|
1665
|
+
div_.h3("Mapping Details");
|
|
1666
|
+
// Source/Target scope line (mirrors Java: CONC_MAP_FROM / CONC_MAP_TO)
|
|
1667
|
+
const p = div_.para();
|
|
1668
|
+
p.tx(this.translate('CONC_MAP_FROM') + " ");
|
|
1669
|
+
const sourceScope = cm.sourceScope || cm.sourceCanonical || cm.sourceUri;
|
|
1670
|
+
if (sourceScope) {
|
|
1671
|
+
await this.renderLink(p, sourceScope);
|
|
1672
|
+
} else {
|
|
1673
|
+
p.tx(this.translate('CONC_MAP_NOT_SPEC'));
|
|
1674
|
+
}
|
|
1675
|
+
p.tx(" " + this.translate('CONC_MAP_TO') + " ");
|
|
1676
|
+
const targetScope = cm.targetScope || cm.targetCanonical || cm.targetUri;
|
|
1677
|
+
if (targetScope) {
|
|
1678
|
+
await this.renderLink(p, targetScope);
|
|
1679
|
+
} else {
|
|
1680
|
+
p.tx(this.translate('CONC_MAP_NOT_SPEC'));
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
div_.br();
|
|
1684
|
+
|
|
1685
|
+
// Render each group
|
|
1686
|
+
let gc = 0;
|
|
1687
|
+
for (const grp of cm.group || []) {
|
|
1688
|
+
gc++;
|
|
1689
|
+
if (gc > 1) {
|
|
1690
|
+
div_.hr();
|
|
1691
|
+
}
|
|
1692
|
+
await this.renderConceptMapGroup(div_, cm, grp, gc);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
return div_.toString();
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* Render a single ConceptMap group.
|
|
1700
|
+
* Determines whether this is a "simple" group (1:1 mappings, no dependsOn/product)
|
|
1701
|
+
* or a "complex" group, and delegates accordingly.
|
|
1702
|
+
*/
|
|
1703
|
+
async renderConceptMapGroup(x, cm, grp, gc) {
|
|
1704
|
+
// Analyze the group to determine rendering mode
|
|
1705
|
+
let hasComment = false;
|
|
1706
|
+
let hasProperties = false;
|
|
1707
|
+
let ok = true; // true = simple rendering
|
|
1708
|
+
|
|
1709
|
+
const props = {}; // property code -> Set of systems
|
|
1710
|
+
const sources = { code: new Set() };
|
|
1711
|
+
const targets = { code: new Set() };
|
|
1712
|
+
|
|
1713
|
+
if (grp.source) sources.code.add(grp.source);
|
|
1714
|
+
if (grp.target) targets.code.add(grp.target);
|
|
1715
|
+
|
|
1716
|
+
for (const elem of grp.element || []) {
|
|
1717
|
+
const isSimple = elem.noMap ||
|
|
1718
|
+
(elem.target && elem.target.length === 1 &&
|
|
1719
|
+
(!elem.target[0].dependsOn || elem.target[0].dependsOn.length === 0) &&
|
|
1720
|
+
(!elem.target[0].product || elem.target[0].product.length === 0));
|
|
1721
|
+
ok = ok && isSimple;
|
|
1722
|
+
|
|
1723
|
+
if (Extensions.readString(elem, 'http://hl7.org/fhir/StructureDefinition/conceptmap-nomap-comment')) {
|
|
1724
|
+
hasComment = true;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
for (const tgt of elem.target || []) {
|
|
1728
|
+
if (tgt.comment) {
|
|
1729
|
+
hasComment = true;
|
|
1730
|
+
}
|
|
1731
|
+
for (const pp of tgt.property || []) {
|
|
1732
|
+
if (!props[pp.code]) {
|
|
1733
|
+
props[pp.code] = new Set();
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
for (const d of tgt.dependsOn || []) {
|
|
1737
|
+
if (!sources[d.attribute]) {
|
|
1738
|
+
sources[d.attribute] = new Set();
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
for (const d of tgt.product || []) {
|
|
1742
|
+
if (!targets[d.attribute]) {
|
|
1743
|
+
targets[d.attribute] = new Set();
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
if (Object.keys(props).length > 0) {
|
|
1750
|
+
hasProperties = true;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// Group header
|
|
1754
|
+
const pp = x.para();
|
|
1755
|
+
pp.b().tx(this.translate('CONC_MAP_GRP', [gc])+ " ");
|
|
1756
|
+
pp.tx(this.translate('CONC_MAP_FROM') + " ");
|
|
1757
|
+
if (grp.source) {
|
|
1758
|
+
await this.renderLink(pp, grp.source);
|
|
1759
|
+
} else {
|
|
1760
|
+
pp.code().tx(this.translate('CONC_MAP_CODE_SYS_UNSPEC'));
|
|
1761
|
+
}
|
|
1762
|
+
pp.tx(" to ");
|
|
1763
|
+
if (grp.target) {
|
|
1764
|
+
await this.renderLink(pp, grp.target);
|
|
1765
|
+
} else {
|
|
1766
|
+
pp.code().tx(this.translate('CONC_MAP_CODE_SYS_UNSPEC'));
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
if (ok) {
|
|
1770
|
+
await this.renderSimpleConceptMapGroup(x, grp, hasComment);
|
|
1771
|
+
} else {
|
|
1772
|
+
await this.renderComplexConceptMapGroup(x, grp, hasComment, hasProperties, props, sources, targets);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
/**
|
|
1777
|
+
* Render a simple ConceptMap group: Source | Relationship | Target | Comment
|
|
1778
|
+
* This is the "ok" path from the Java code where all elements have at most
|
|
1779
|
+
* one target and no dependsOn/product.
|
|
1780
|
+
*/
|
|
1781
|
+
async renderSimpleConceptMapGroup(x, grp, hasComment) {
|
|
1782
|
+
const tbl = x.table("grid");
|
|
1783
|
+
let tr = tbl.tr();
|
|
1784
|
+
tr.td().b().tx(this.translate('CONC_MAP_SOURCE'));
|
|
1785
|
+
tr.td().b().tx(this.translate('CONC_MAP_REL'));
|
|
1786
|
+
tr.td().b().tx(this.translate('CONC_MAP_TRGT'));
|
|
1787
|
+
if (hasComment) {
|
|
1788
|
+
tr.td().b().tx(this.translate('GENERAL_COMMENT'));
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
for (const elem of grp.element || []) {
|
|
1792
|
+
tr = tbl.tr();
|
|
1793
|
+
const td = tr.td();
|
|
1794
|
+
td.tx(elem.code);
|
|
1795
|
+
const display = elem.display || await this.getDisplayForConcept(grp.source, elem.code);
|
|
1796
|
+
if (display && !this.isSameCodeAndDisplay(elem.code, display)) {
|
|
1797
|
+
td.tx(" (" + display + ")");
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
if (elem.noMap) {
|
|
1801
|
+
const nomapComment = Extensions.readString(elem, 'http://hl7.org/fhir/StructureDefinition/conceptmap-nomap-comment');
|
|
1802
|
+
if (!hasComment) {
|
|
1803
|
+
tr.td().colspan("2").style("background-color: #efefef").tx("(not mapped)");
|
|
1804
|
+
} else if (nomapComment) {
|
|
1805
|
+
tr.td().colspan("2").style("background-color: #efefef").tx("(not mapped)");
|
|
1806
|
+
tr.td().style("background-color: #efefef").tx(nomapComment);
|
|
1807
|
+
} else {
|
|
1808
|
+
tr.td().colspan("3").style("background-color: #efefef").tx("(not mapped)");
|
|
1809
|
+
}
|
|
1810
|
+
} else {
|
|
1811
|
+
let first = true;
|
|
1812
|
+
for (const tgt of elem.target || []) {
|
|
1813
|
+
if (first) {
|
|
1814
|
+
first = false;
|
|
1815
|
+
} else {
|
|
1816
|
+
tr = tbl.tr();
|
|
1817
|
+
tr.td().style("opacity: 0.5").tx('"');
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// Relationship cell
|
|
1821
|
+
this.renderConceptMapRelationship(tr, tgt);
|
|
1822
|
+
|
|
1823
|
+
// Target code cell
|
|
1824
|
+
const tgtTd = tr.td();
|
|
1825
|
+
tgtTd.tx(tgt.code || '');
|
|
1826
|
+
const tgtDisplay = tgt.display || await this.getDisplayForConcept(grp.target, tgt.code);
|
|
1827
|
+
if (tgtDisplay && !this.isSameCodeAndDisplay(tgt.code, tgtDisplay)) {
|
|
1828
|
+
tgtTd.tx(" (" + tgtDisplay + ")");
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
if (hasComment) {
|
|
1832
|
+
tr.td().tx(tgt.comment || '');
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
this.addUnmapped(tbl, grp);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
/**
|
|
1841
|
+
* Render a complex ConceptMap group with dependsOn, product, and/or property columns.
|
|
1842
|
+
* This is the "!ok" path from the Java code.
|
|
1843
|
+
*/
|
|
1844
|
+
async renderComplexConceptMapGroup(x, grp, hasComment, hasProperties, props, sources, targets) {
|
|
1845
|
+
// Check if any targets have relationships
|
|
1846
|
+
let hasRelationships = false;
|
|
1847
|
+
for (const elem of grp.element || []) {
|
|
1848
|
+
for (const tgt of elem.target || []) {
|
|
1849
|
+
if (tgt.relationship) {
|
|
1850
|
+
hasRelationships = true;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
const tbl = x.table("grid");
|
|
1856
|
+
|
|
1857
|
+
// First header row: Source Details | Relationship | Target Details | Comment | Properties
|
|
1858
|
+
let tr = tbl.tr();
|
|
1859
|
+
const sourceColCount = 1 + Object.keys(sources).length - 1; // code + dependsOn attributes
|
|
1860
|
+
const targetColCount = 1 + Object.keys(targets).length - 1; // code + product attributes
|
|
1861
|
+
tr.td().colspan(String(sourceColCount + 1)).b().tx(this.translate('CONC_MAP_SRC_DET'));
|
|
1862
|
+
if (hasRelationships) {
|
|
1863
|
+
tr.td().b().tx(this.translate('CONC_MAP_REL'));
|
|
1864
|
+
}
|
|
1865
|
+
tr.td().colspan(String(targetColCount + 1)).b().tx(this.translate('CONC_MAP_TRGT_DET'));
|
|
1866
|
+
if (hasComment) {
|
|
1867
|
+
tr.td().b().tx(this.translate('GENERAL_COMMENT'));
|
|
1868
|
+
}
|
|
1869
|
+
if (hasProperties) {
|
|
1870
|
+
tr.td().colspan(String(Object.keys(props).length)).b().tx(this.translate('GENERAL_PROPS'));
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// Second header row: actual column headers
|
|
1874
|
+
tr = tbl.tr();
|
|
1875
|
+
|
|
1876
|
+
// Source code column
|
|
1877
|
+
if (sources.code.size === 1) {
|
|
1878
|
+
const url = [...sources.code][0];
|
|
1879
|
+
await this.renderCSDetailsLink(tr, url, true);
|
|
1880
|
+
} else {
|
|
1881
|
+
tr.td().b().tx(this.translate('GENERAL_CODE'));
|
|
1882
|
+
}
|
|
1883
|
+
// Source dependsOn attribute columns
|
|
1884
|
+
for (const s of Object.keys(sources)) {
|
|
1885
|
+
if (s !== 'code') {
|
|
1886
|
+
if (sources[s].size === 1) {
|
|
1887
|
+
const url = [...sources[s]][0];
|
|
1888
|
+
await this.renderCSDetailsLink(tr, url, false);
|
|
1889
|
+
} else {
|
|
1890
|
+
tr.td().b().tx(this.getDescForConcept(s));
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
// Relationship column
|
|
1895
|
+
if (hasRelationships) {
|
|
1896
|
+
tr.td();
|
|
1897
|
+
}
|
|
1898
|
+
// Target code column
|
|
1899
|
+
if (targets.code.size === 1) {
|
|
1900
|
+
const url = [...targets.code][0];
|
|
1901
|
+
await this.renderCSDetailsLink(tr, url, true);
|
|
1902
|
+
} else {
|
|
1903
|
+
tr.td().b().tx(this.translate('GENERAL_CODE'));
|
|
1904
|
+
}
|
|
1905
|
+
// Target product attribute columns
|
|
1906
|
+
for (const s of Object.keys(targets)) {
|
|
1907
|
+
if (s !== 'code') {
|
|
1908
|
+
if (targets[s].size === 1) {
|
|
1909
|
+
const url = [...targets[s]][0];
|
|
1910
|
+
await this.renderCSDetailsLink(tr, url, false);
|
|
1911
|
+
} else {
|
|
1912
|
+
tr.td().b().tx(this.getDescForConcept(s));
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
// Comment column header
|
|
1917
|
+
if (hasComment) {
|
|
1918
|
+
tr.td();
|
|
1919
|
+
}
|
|
1920
|
+
// Property column headers
|
|
1921
|
+
if (hasProperties) {
|
|
1922
|
+
for (const s of Object.keys(props)) {
|
|
1923
|
+
if (props[s].size === 1) {
|
|
1924
|
+
const url = [...props[s]][0];
|
|
1925
|
+
await this.renderCSDetailsLink(tr, url, false);
|
|
1926
|
+
} else {
|
|
1927
|
+
tr.td().b().tx(this.getDescForConcept(s));
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// Data rows
|
|
1933
|
+
for (const elem of grp.element || []) {
|
|
1934
|
+
if (elem.noMap) {
|
|
1935
|
+
tr = tbl.tr();
|
|
1936
|
+
const td = tr.td().style("border-right-width: 0px");
|
|
1937
|
+
if (sources.code.size === 1) {
|
|
1938
|
+
td.tx(elem.code);
|
|
1939
|
+
} else {
|
|
1940
|
+
td.tx(grp.source + " / " + elem.code);
|
|
1941
|
+
}
|
|
1942
|
+
const display = elem.display || await this.getDisplayForConcept(grp.source, elem.code);
|
|
1943
|
+
tr.td().style("border-left-width: 0px").tx(display || '');
|
|
1944
|
+
|
|
1945
|
+
const nomapComment = Extensions.readString(elem, 'http://hl7.org/fhir/StructureDefinition/conceptmap-nomap-comment');
|
|
1946
|
+
if (nomapComment) {
|
|
1947
|
+
tr.td().colspan("3").style("background-color: #efefef").tx("(not mapped)");
|
|
1948
|
+
tr.td().style("background-color: #efefef").tx(nomapComment);
|
|
1949
|
+
} else {
|
|
1950
|
+
tr.td().colspan("4").style("background-color: #efefef").tx("(not mapped)");
|
|
1951
|
+
}
|
|
1952
|
+
} else {
|
|
1953
|
+
let first = true;
|
|
1954
|
+
for (let ti = 0; ti < (elem.target || []).length; ti++) {
|
|
1955
|
+
const tgt = elem.target[ti];
|
|
1956
|
+
const last = ti === elem.target.length - 1;
|
|
1957
|
+
tr = tbl.tr();
|
|
1958
|
+
|
|
1959
|
+
// Source code cell
|
|
1960
|
+
const td = tr.td().style("border-right-width: 0px");
|
|
1961
|
+
if (!first && !last) {
|
|
1962
|
+
td.style("border-top-style: none; border-bottom-style: none");
|
|
1963
|
+
} else if (!first) {
|
|
1964
|
+
td.style("border-top-style: none");
|
|
1965
|
+
} else if (!last) {
|
|
1966
|
+
td.style("border-bottom-style: none");
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
if (first) {
|
|
1970
|
+
if (sources.code.size === 1) {
|
|
1971
|
+
td.tx(elem.code);
|
|
1972
|
+
} else {
|
|
1973
|
+
td.tx(grp.source + " / " + elem.code);
|
|
1974
|
+
}
|
|
1975
|
+
const display = elem.display || await this.getDisplayForConcept(grp.source, elem.code);
|
|
1976
|
+
const dispTd = tr.td();
|
|
1977
|
+
if (!last) {
|
|
1978
|
+
dispTd.style("border-left-width: 0px; border-bottom-style: none");
|
|
1979
|
+
} else {
|
|
1980
|
+
dispTd.style("border-left-width: 0px");
|
|
1981
|
+
}
|
|
1982
|
+
dispTd.tx(display || '');
|
|
1983
|
+
} else {
|
|
1984
|
+
// Empty display cell for subsequent targets
|
|
1985
|
+
const dispTd = tr.td();
|
|
1986
|
+
if (!last) {
|
|
1987
|
+
dispTd.style("border-left-width: 0px; border-top-style: none; border-bottom-style: none");
|
|
1988
|
+
} else {
|
|
1989
|
+
dispTd.style("border-top-style: none; border-left-width: 0px");
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// Source dependsOn columns
|
|
1994
|
+
for (const s of Object.keys(sources)) {
|
|
1995
|
+
if (s !== 'code') {
|
|
1996
|
+
const depTd = tr.td();
|
|
1997
|
+
const val = this.getDependsOnValue(tgt.dependsOn, s, sources[s].size !== 1);
|
|
1998
|
+
depTd.tx(val || '');
|
|
1999
|
+
const depDisplay = this.getDependsOnDisplay(tgt.dependsOn, s);
|
|
2000
|
+
if (depDisplay) {
|
|
2001
|
+
depTd.tx(" (" + depDisplay + ")");
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
first = false;
|
|
2007
|
+
|
|
2008
|
+
// Relationship cell
|
|
2009
|
+
if (hasRelationships) {
|
|
2010
|
+
this.renderConceptMapRelationship(tr, tgt);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// Target code cell
|
|
2014
|
+
const tgtTd = tr.td().style("border-right-width: 0px");
|
|
2015
|
+
if (targets.code.size === 1) {
|
|
2016
|
+
tgtTd.tx(tgt.code || '');
|
|
2017
|
+
} else {
|
|
2018
|
+
tgtTd.tx((grp.target || '') + " / " + (tgt.code || ''));
|
|
2019
|
+
}
|
|
2020
|
+
const tgtDisplay = tgt.display || await this.getDisplayForConcept(grp.target, tgt.code);
|
|
2021
|
+
tr.td().style("border-left-width: 0px").tx(tgtDisplay || '');
|
|
2022
|
+
|
|
2023
|
+
// Target product columns
|
|
2024
|
+
for (const s of Object.keys(targets)) {
|
|
2025
|
+
if (s !== 'code') {
|
|
2026
|
+
const prodTd = tr.td();
|
|
2027
|
+
const val = this.getDependsOnValue(tgt.product, s, targets[s].size !== 1);
|
|
2028
|
+
prodTd.tx(val || '');
|
|
2029
|
+
const prodDisplay = this.getDependsOnDisplay(tgt.product, s);
|
|
2030
|
+
if (prodDisplay) {
|
|
2031
|
+
prodTd.tx(" (" + prodDisplay + ")");
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// Comment cell
|
|
2037
|
+
if (hasComment) {
|
|
2038
|
+
tr.td().tx(tgt.comment || '');
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// Property cells
|
|
2042
|
+
if (hasProperties) {
|
|
2043
|
+
for (const s of Object.keys(props)) {
|
|
2044
|
+
const propTd = tr.td();
|
|
2045
|
+
propTd.tx(this.getPropertyValueFromList(tgt.property, s));
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
this.addUnmapped(tbl, grp);
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* Render the relationship cell for a target element.
|
|
2056
|
+
* Handles both R5 relationship codes and legacy R4 equivalence codes via extension.
|
|
2057
|
+
*/
|
|
2058
|
+
renderConceptMapRelationship(tr, tgt) {
|
|
2059
|
+
if (tgt.relationship) {
|
|
2060
|
+
tr.td().tx(this.presentRelationshipCode(tgt.relationship));
|
|
2061
|
+
} else if (tgt.equivalence) {
|
|
2062
|
+
tr.td().tx(this.presentEquivalenceCode(tgt.equivalence));
|
|
2063
|
+
} else {
|
|
2064
|
+
tr.td().tx("(" + "equivalent" + ")");
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
/**
|
|
2069
|
+
* Render a code system details link in a header cell.
|
|
2070
|
+
* Mirrors Java renderCSDetailsLink.
|
|
2071
|
+
*/
|
|
2072
|
+
async renderCSDetailsLink(tr, url, span2) {
|
|
2073
|
+
const td = tr.td();
|
|
2074
|
+
if (span2) {
|
|
2075
|
+
td.colspan("2");
|
|
2076
|
+
}
|
|
2077
|
+
td.b().tx(this.translate('CONC_MAP_CODES'));
|
|
2078
|
+
td.tx(" " + this.translate('CONC_MAP_FRM') + " ");
|
|
2079
|
+
const linkinfo = this.linkResolver ? await this.linkResolver.resolveURL(this.opContext, url) : null;
|
|
2080
|
+
if (linkinfo) {
|
|
2081
|
+
td.ah(linkinfo.link).tx(linkinfo.description);
|
|
2082
|
+
} else {
|
|
2083
|
+
td.tx(url);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
/**
|
|
2088
|
+
* Translate a FHIR R5 ConceptMap relationship code to a human-readable string.
|
|
2089
|
+
* Uses the same strings as the Java renderer.
|
|
2090
|
+
*/
|
|
2091
|
+
presentRelationshipCode(code) {
|
|
2092
|
+
switch (code) {
|
|
2093
|
+
case 'related-to': return 'is related to';
|
|
2094
|
+
case 'equivalent': return 'is equivalent to';
|
|
2095
|
+
case 'source-is-narrower-than-target': return 'is narrower than';
|
|
2096
|
+
case 'source-is-broader-than-target': return 'is broader than';
|
|
2097
|
+
case 'not-related-to': return 'is not related to';
|
|
2098
|
+
default: return code;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
/**
|
|
2103
|
+
* Translate a legacy (R2/R3/R4) ConceptMap equivalence code to a human-readable string.
|
|
2104
|
+
* Uses the same strings as the Java renderer.
|
|
2105
|
+
*/
|
|
2106
|
+
presentEquivalenceCode(code) {
|
|
2107
|
+
switch (code) {
|
|
2108
|
+
case 'relatedto': return 'is related to';
|
|
2109
|
+
case 'equivalent': return 'is equivalent to';
|
|
2110
|
+
case 'equal': return 'is equal to';
|
|
2111
|
+
case 'wider': return 'maps to wider concept';
|
|
2112
|
+
case 'subsumes': return 'is subsumed by';
|
|
2113
|
+
case 'narrower': return 'maps to narrower concept';
|
|
2114
|
+
case 'specializes': return 'has specialization';
|
|
2115
|
+
case 'inexact': return 'maps loosely to';
|
|
2116
|
+
case 'unmatched': return 'has no match';
|
|
2117
|
+
case 'disjoint': return 'is not related to';
|
|
2118
|
+
default: return code;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
/**
|
|
2123
|
+
* Check if a code and its display text are essentially the same
|
|
2124
|
+
* (ignoring spaces, hyphens, and case).
|
|
2125
|
+
*/
|
|
2126
|
+
isSameCodeAndDisplay(code, display) {
|
|
2127
|
+
if (!code || !display) return false;
|
|
2128
|
+
const c = code.replace(/[ -]/g, '').toLowerCase();
|
|
2129
|
+
const d = display.replace(/[ -]/g, '').toLowerCase();
|
|
2130
|
+
return c === d;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
/**
|
|
2134
|
+
* Look up a display string for a concept. Delegates to the linkResolver if available.
|
|
2135
|
+
*/
|
|
2136
|
+
async getDisplayForConcept(system, code) {
|
|
2137
|
+
if (!system || !code) return null;
|
|
2138
|
+
if (!this.linkResolver) return null;
|
|
2139
|
+
const result = await this.linkResolver.resolveCode(this.opContext, system, null, code);
|
|
2140
|
+
return result ? result.description : null;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
/**
|
|
2144
|
+
* Get a description for a concept attribute code (used in complex table headers).
|
|
2145
|
+
* Mirrors Java getDescForConcept.
|
|
2146
|
+
*/
|
|
2147
|
+
getDescForConcept(s) {
|
|
2148
|
+
if (s.startsWith('http://hl7.org/fhir/v2/element/')) {
|
|
2149
|
+
return 'v2 ' + s.substring('http://hl7.org/fhir/v2/element/'.length);
|
|
2150
|
+
}
|
|
2151
|
+
return s;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
/**
|
|
2155
|
+
* Extract a value from a dependsOn or product list by attribute name.
|
|
2156
|
+
*/
|
|
2157
|
+
getDependsOnValue(list, attribute) {
|
|
2158
|
+
if (!list) return null;
|
|
2159
|
+
for (const item of list) {
|
|
2160
|
+
if (item.attribute === attribute) {
|
|
2161
|
+
// R5 uses value[x], try common types
|
|
2162
|
+
if (item.valueCode) return item.valueCode;
|
|
2163
|
+
if (item.valueString) return item.valueString;
|
|
2164
|
+
if (item.valueCoding) return item.valueCoding.code || '';
|
|
2165
|
+
if (item.value) return String(item.value);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
return null;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
/**
|
|
2172
|
+
* Extract a display from a dependsOn or product list by attribute name.
|
|
2173
|
+
*/
|
|
2174
|
+
// eslint-disable-next-line no-unused-vars
|
|
2175
|
+
getDependsOnDisplay(list, attribute) {
|
|
2176
|
+
// In current FHIR, dependsOn display is not directly available;
|
|
2177
|
+
// would require a lookup. Return null for now (matches Java which also returns null).
|
|
2178
|
+
return null;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
/**
|
|
2182
|
+
* Extract a property value from a target's property list by code.
|
|
2183
|
+
*/
|
|
2184
|
+
getPropertyValueFromList(list, code) {
|
|
2185
|
+
if (!list) return '';
|
|
2186
|
+
const results = [];
|
|
2187
|
+
for (const item of list) {
|
|
2188
|
+
if (item.code === code) {
|
|
2189
|
+
// R5 MappingPropertyComponent uses value[x]
|
|
2190
|
+
if (item.valueCode !== undefined) results.push(item.valueCode);
|
|
2191
|
+
else if (item.valueString !== undefined) results.push(item.valueString);
|
|
2192
|
+
else if (item.valueCoding !== undefined) results.push(item.valueCoding.code || '');
|
|
2193
|
+
else if (item.valueBoolean !== undefined) results.push(String(item.valueBoolean));
|
|
2194
|
+
else if (item.valueInteger !== undefined) results.push(String(item.valueInteger));
|
|
2195
|
+
else if (item.valueDecimal !== undefined) results.push(String(item.valueDecimal));
|
|
2196
|
+
else if (item.valueDateTime !== undefined) results.push(item.valueDateTime);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
return results.join(', ');
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
/**
|
|
2203
|
+
* Render the unmapped section for a group, if present.
|
|
2204
|
+
* Currently a stub matching the Java implementation.
|
|
2205
|
+
*/
|
|
2206
|
+
addUnmapped(tbl, grp) {
|
|
2207
|
+
if (grp.unmapped) {
|
|
2208
|
+
// TODO: render unmapped mode/code/url when needed
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
|
|
1643
2212
|
}
|
|
1644
2213
|
|
|
1645
2214
|
module.exports = { Renderer };
|
package/tx/tx-html.js
CHANGED
|
@@ -18,6 +18,7 @@ const {CapabilityStatementXML} = require("./xml/capabilitystatement-xml");
|
|
|
18
18
|
const {TerminologyCapabilitiesXML} = require("./xml/terminologycapabilities-xml");
|
|
19
19
|
const {ParametersXML} = require("./xml/parameters-xml");
|
|
20
20
|
const {OperationOutcomeXML} = require("./xml/operationoutcome-xml");
|
|
21
|
+
const {debugLog} = require("./operation-context");
|
|
21
22
|
|
|
22
23
|
const txHtmlLog = Logger.getInstance().child({ module: 'tx-html' });
|
|
23
24
|
|
|
@@ -312,7 +313,7 @@ class TxHtmlRenderer {
|
|
|
312
313
|
return await this.renderValueSet(json, inBundle, _fmt, op, exp);
|
|
313
314
|
}
|
|
314
315
|
case 'ConceptMap':
|
|
315
|
-
return await this.renderConceptMap(json, inBundle);
|
|
316
|
+
return await this.renderConceptMap(json, inBundle, _fmt, op);
|
|
316
317
|
case 'CapabilityStatement':
|
|
317
318
|
return await this.renderCapabilityStatement(json, inBundle);
|
|
318
319
|
case 'TerminologyCapabilities':
|
|
@@ -327,6 +328,7 @@ class TxHtmlRenderer {
|
|
|
327
328
|
return await this.renderGeneric(json, inBundle);
|
|
328
329
|
}
|
|
329
330
|
} catch (error) {
|
|
331
|
+
debugLog(error);
|
|
330
332
|
console.error(error);
|
|
331
333
|
throw error;
|
|
332
334
|
}
|
|
@@ -698,8 +700,51 @@ class TxHtmlRenderer {
|
|
|
698
700
|
* Render ConceptMap resource
|
|
699
701
|
*/
|
|
700
702
|
// eslint-disable-next-line no-unused-vars
|
|
701
|
-
async renderConceptMap(json, inBundle) {
|
|
702
|
-
|
|
703
|
+
async renderConceptMap(json, inBundle, _fmt, op) {
|
|
704
|
+
if (inBundle || op) {
|
|
705
|
+
return await this.renderResourceWithNarrative(json, await this.renderer.renderConceptMap(json));
|
|
706
|
+
} else {
|
|
707
|
+
let html = `<ul class="nav nav-tabs">`;
|
|
708
|
+
html += this.tab(!_fmt || _fmt == 'html', json.resourceType, json.resourceType, 'html', json.id);
|
|
709
|
+
html += this.tab(_fmt && _fmt == 'html/json', 'JSON', json.resourceType, 'html/json', json.id);
|
|
710
|
+
html += this.tab(_fmt && _fmt == 'html/narrative', 'Original Narrative', json.resourceType, 'html/narrative', json.id);
|
|
711
|
+
html += this.tab(_fmt && _fmt == 'html/ops', 'Translate', json.resourceType, 'html/ops', json.id);
|
|
712
|
+
html += `</ul>`;
|
|
713
|
+
|
|
714
|
+
if (!_fmt || _fmt == 'html') {
|
|
715
|
+
html += await this.renderResourceWithNarrative(json, await this.renderer.renderConceptMap(json));
|
|
716
|
+
} else if (_fmt == "html/json") {
|
|
717
|
+
html += await this.renderResourceJson(json);
|
|
718
|
+
} else if (_fmt == "html/narrative") {
|
|
719
|
+
html += await this.renderResourceWithNarrative(json, json.text?.div);
|
|
720
|
+
} else if (_fmt == "html/ops") {
|
|
721
|
+
const sourceSet = new Set();
|
|
722
|
+
const codeSet = new Set();
|
|
723
|
+
const targetSet = new Set();
|
|
724
|
+
|
|
725
|
+
for (const grp of json.group || []) {
|
|
726
|
+
if (grp.source) sourceSet.add(`<option value="${escape(grp.source)}">${escape(grp.source)}</option>`);
|
|
727
|
+
if (grp.target) targetSet.add(`<option value="${escape(grp.target)}">${escape(grp.target)}</option>`);
|
|
728
|
+
for (const elem of grp.element || []) {
|
|
729
|
+
if (elem.code) codeSet.add(`<option value="${escape(elem.code)}">${escape(elem.code)}</option>`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const sources = [...sourceSet];
|
|
733
|
+
const codes = [...codeSet];
|
|
734
|
+
const targets = [...targetSet];
|
|
735
|
+
|
|
736
|
+
html += await this.liquid.renderFile('conceptmap-operations', {
|
|
737
|
+
opsId: this.generateResourceId(),
|
|
738
|
+
cmSystemId: this.generateResourceId(),
|
|
739
|
+
inferSystemId: this.generateResourceId(),
|
|
740
|
+
sources: sources,
|
|
741
|
+
codes: codes,
|
|
742
|
+
targets: targets,
|
|
743
|
+
url: escape(json.url || '')
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
return html;
|
|
747
|
+
}
|
|
703
748
|
}
|
|
704
749
|
|
|
705
750
|
/**
|
package/tx/workers/read.js
CHANGED
|
@@ -47,14 +47,7 @@ class ReadWorker extends TerminologyWorker {
|
|
|
47
47
|
return await this.handleValueSet(req, res, id);
|
|
48
48
|
|
|
49
49
|
case 'ConceptMap':
|
|
50
|
-
return
|
|
51
|
-
resourceType: 'OperationOutcome',
|
|
52
|
-
issue: [{
|
|
53
|
-
severity: 'error',
|
|
54
|
-
code: 'not-supported',
|
|
55
|
-
diagnostics: 'ConceptMap read not yet implemented'
|
|
56
|
-
}]
|
|
57
|
-
});
|
|
50
|
+
return await this.handleConceptMap(req, res, id);
|
|
58
51
|
|
|
59
52
|
default:
|
|
60
53
|
return res.status(404).json({
|
|
@@ -152,6 +145,28 @@ class ReadWorker extends TerminologyWorker {
|
|
|
152
145
|
}
|
|
153
146
|
}
|
|
154
147
|
|
|
148
|
+
return res.status(404).json({
|
|
149
|
+
resourceType: 'OperationOutcome',
|
|
150
|
+
issue: [{
|
|
151
|
+
severity: 'error',
|
|
152
|
+
code: 'not-found',
|
|
153
|
+
diagnostics: `ValueSet/${id} not found`
|
|
154
|
+
}]
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Handle ConceptMap read
|
|
159
|
+
*/
|
|
160
|
+
async handleConceptMap(req, res, id) {
|
|
161
|
+
// Iterate through valueSetProviders in order
|
|
162
|
+
for (const cmsp of this.provider.conceptMapProviders) {
|
|
163
|
+
this.deadCheck('handleConceptMap-loop');
|
|
164
|
+
const vs = await cmsp.fetchConceptMapById(id);
|
|
165
|
+
if (vs) {
|
|
166
|
+
return res.json(vs.jsonObj);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
155
170
|
return res.status(404).json({
|
|
156
171
|
resourceType: 'OperationOutcome',
|
|
157
172
|
issue: [{
|
package/tx/workers/search.js
CHANGED
|
@@ -96,7 +96,7 @@ class SearchWorker extends TerminologyWorker {
|
|
|
96
96
|
|
|
97
97
|
case 'ConceptMap':
|
|
98
98
|
// Not implemented yet - return empty set
|
|
99
|
-
matches =
|
|
99
|
+
matches = await this.searchConceptMaps(params, elements);
|
|
100
100
|
break;
|
|
101
101
|
|
|
102
102
|
default:
|
|
@@ -235,6 +235,41 @@ class SearchWorker extends TerminologyWorker {
|
|
|
235
235
|
return allMatches;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Search ConceptMaps by delegating to providers
|
|
240
|
+
*/
|
|
241
|
+
async searchConceptMaps(params, elements) {
|
|
242
|
+
const allMatches = [];
|
|
243
|
+
|
|
244
|
+
// Convert params object to array format expected by ValueSet providers
|
|
245
|
+
// Exclude control params (_offset, _count, _elements, _sort)
|
|
246
|
+
const searchParams = [];
|
|
247
|
+
let source = null;
|
|
248
|
+
for (const [key, value] of Object.entries(params)) {
|
|
249
|
+
if (!key.startsWith('_') && value && SearchWorker.ALLOWED_PARAMS.includes(key)) {
|
|
250
|
+
searchParams.push({ name: key, value: value });
|
|
251
|
+
}
|
|
252
|
+
if (key == 'source') {
|
|
253
|
+
source = value;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const cmsp of this.provider.conceptMapProviders) {
|
|
258
|
+
if (!source || source == cmsp.sourcePackage()) {
|
|
259
|
+
this.deadCheck('searchConceptMaps-providers');
|
|
260
|
+
const results = await cmsp.searchConceptMaps(searchParams, elements);
|
|
261
|
+
if (results && Array.isArray(results)) {
|
|
262
|
+
for (const vs of results) {
|
|
263
|
+
this.deadCheck('searchConceptMaps-results');
|
|
264
|
+
allMatches.push(vs.jsonObj || vs);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return allMatches;
|
|
271
|
+
}
|
|
272
|
+
|
|
238
273
|
/**
|
|
239
274
|
* Check if a value matches the search term (partial, case-insensitive)
|
|
240
275
|
*/
|
package/tx/workers/validate.js
CHANGED
|
@@ -944,13 +944,13 @@ class ValueSetChecker {
|
|
|
944
944
|
if (inactive.value) {
|
|
945
945
|
result.AddParamBool('inactive', inactive.value);
|
|
946
946
|
if (vstatus.value && vstatus.value !== 'inactive') {
|
|
947
|
-
result.
|
|
947
|
+
result.addParamCode('status', vstatus.value);
|
|
948
948
|
}
|
|
949
949
|
let msg = this.worker.i18n.translate('INACTIVE_CONCEPT_FOUND', this.params.HTTPLanguages, [vstatus.value, coding.code]);
|
|
950
950
|
messages.push(msg);
|
|
951
951
|
op.addIssue(new Issue('warning', 'business-rule', path, 'INACTIVE_CONCEPT_FOUND', msg, 'code-comment'));
|
|
952
952
|
} else if (vstatus.value.toLowerCase() === 'deprecated') {
|
|
953
|
-
result.
|
|
953
|
+
result.addParamCode('status', vstatus.value);
|
|
954
954
|
let msg = this.worker.i18n.translate('DEPRECATED_CONCEPT_FOUND', this.params.HTTPLanguages, [vstatus.value, coding.code]);
|
|
955
955
|
messages.push(msg);
|
|
956
956
|
op.addIssue(new Issue('warning', 'business-rule', path, 'DEPRECATED_CONCEPT_FOUND', msg, 'code-comment'));
|
|
@@ -1348,13 +1348,18 @@ class ValueSetChecker {
|
|
|
1348
1348
|
if (inactive.value) {
|
|
1349
1349
|
result.addParamBool('inactive', inactive.value);
|
|
1350
1350
|
if (vstatus.value && vstatus.value !== 'inactive') {
|
|
1351
|
-
result.
|
|
1351
|
+
result.addParamCode('status', vstatus.value);
|
|
1352
|
+
}
|
|
1353
|
+
if (!['inactive', 'DISCOURAGED'].includes(vstatus.value)) {
|
|
1354
|
+
let m = this.worker.i18n.translate('INACTIVE_CONCEPT_FOUND', this.params.HTTPLanguages, ['inactive', tcode]);
|
|
1355
|
+
msg(m);
|
|
1356
|
+
op.addIssue(new Issue('warning', 'business-rule', inactive.path, 'INACTIVE_CONCEPT_FOUND', m, 'code-comment'));
|
|
1352
1357
|
}
|
|
1353
1358
|
let m = this.worker.i18n.translate('INACTIVE_CONCEPT_FOUND', this.params.HTTPLanguages, [vstatus.value, tcode]);
|
|
1354
1359
|
msg(m);
|
|
1355
1360
|
op.addIssue(new Issue('warning', 'business-rule', inactive.path, 'INACTIVE_CONCEPT_FOUND', m, 'code-comment'));
|
|
1356
1361
|
} else if (vstatus.value && vstatus.value.toLowerCase() === 'deprecated') {
|
|
1357
|
-
result.
|
|
1362
|
+
result.addParamCode('status', 'deprecated');
|
|
1358
1363
|
let m = this.worker.i18n.translate('DEPRECATED_CONCEPT_FOUND', this.params.HTTPLanguages, [vstatus.value, tcode]);
|
|
1359
1364
|
msg(m);
|
|
1360
1365
|
op.addIssue(new Issue('warning', 'business-rule', issuePath, 'DEPRECATED_CONCEPT_FOUND', m, 'code-comment'));
|
|
@@ -1494,13 +1499,13 @@ class ValueSetChecker {
|
|
|
1494
1499
|
if (inactive.value) {
|
|
1495
1500
|
result.addParamBool('inactive', inactive.value);
|
|
1496
1501
|
if (vstatus.value && vstatus.value !== 'inactive') {
|
|
1497
|
-
result.
|
|
1502
|
+
result.addParamCode('status', vstatus.value);
|
|
1498
1503
|
}
|
|
1499
1504
|
let msg = this.worker.i18n.translate('INACTIVE_CONCEPT_FOUND', this.params.HTTPLanguages, [vstatus.value, code]);
|
|
1500
1505
|
messages.push(msg);
|
|
1501
1506
|
op.addIssue(new Issue('warning', 'business-rule', 'code', 'INACTIVE_CONCEPT_FOUND', msg, 'code-comment'));
|
|
1502
1507
|
} else if (vstatus.value.toLowerCase() === 'deprecated') {
|
|
1503
|
-
result.
|
|
1508
|
+
result.addParamCode('status', vstatus.value);
|
|
1504
1509
|
let msg = this.worker.i18n.translate('DEPRECATED_CONCEPT_FOUND', this.params.HTTPLanguages, [vstatus.value, code]);
|
|
1505
1510
|
messages.push(msg);
|
|
1506
1511
|
op.addIssue(new Issue('warning', 'business-rule', 'code', 'DEPRECATED_CONCEPT_FOUND', msg, 'code-comment'));
|