fhirsmith 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/library/cron-utilities.js +136 -0
- package/library/html-server.js +13 -29
- package/library/html.js +3 -8
- package/library/languages.js +160 -37
- package/library/package-manager.js +48 -1
- package/library/utilities.js +100 -19
- package/package.json +2 -2
- package/packages/package-crawler.js +6 -1
- package/packages/packages.js +38 -54
- package/publisher/publisher.js +19 -27
- package/registry/api.js +11 -10
- package/registry/crawler.js +31 -29
- package/registry/model.js +5 -26
- package/registry/registry.js +32 -41
- package/server.js +53 -5
- package/shl/shl.js +0 -18
- package/static/assets/js/statuspage.js +1 -9
- package/stats.js +39 -1
- package/token/token.js +14 -9
- package/translations/Messages.properties +2 -1
- package/tx/README.md +17 -6
- package/tx/cs/cs-api.js +19 -1
- package/tx/cs/cs-base.js +77 -0
- package/tx/cs/cs-country.js +46 -0
- package/tx/cs/cs-cpt.js +9 -5
- package/tx/cs/cs-cs.js +27 -13
- package/tx/cs/cs-lang.js +60 -22
- package/tx/cs/cs-loinc.js +69 -98
- package/tx/cs/cs-mimetypes.js +4 -0
- package/tx/cs/cs-ndc.js +6 -0
- package/tx/cs/cs-omop.js +16 -15
- package/tx/cs/cs-rxnorm.js +23 -1
- package/tx/cs/cs-snomed.js +283 -40
- package/tx/cs/cs-ucum.js +90 -70
- package/tx/importers/import-sct.module.js +371 -35
- package/tx/importers/readme.md +117 -7
- package/tx/library/bundle.js +5 -0
- package/tx/library/capabilitystatement.js +3 -142
- package/tx/library/codesystem.js +19 -173
- package/tx/library/conceptmap.js +4 -218
- package/tx/library/designations.js +14 -1
- package/tx/library/extensions.js +7 -0
- package/tx/library/namingsystem.js +3 -89
- package/tx/library/operation-outcome.js +8 -3
- package/tx/library/parameters.js +3 -2
- package/tx/library/renderer.js +10 -6
- package/tx/library/terminologycapabilities.js +3 -243
- package/tx/library/valueset.js +3 -235
- package/tx/library.js +100 -13
- package/tx/operation-context.js +23 -4
- package/tx/params.js +35 -38
- package/tx/provider.js +6 -5
- package/tx/sct/expressions.js +12 -3
- package/tx/tx-html.js +80 -89
- package/tx/tx.fhir.org.yml +6 -5
- package/tx/tx.js +163 -13
- package/tx/vs/vs-database.js +56 -39
- package/tx/vs/vs-package.js +21 -2
- package/tx/vs/vs-vsac.js +175 -39
- package/tx/workers/batch-validate.js +2 -0
- package/tx/workers/batch.js +2 -0
- package/tx/workers/expand.js +132 -112
- package/tx/workers/lookup.js +33 -14
- package/tx/workers/metadata.js +2 -2
- package/tx/workers/read.js +3 -2
- package/tx/workers/related.js +574 -0
- package/tx/workers/search.js +46 -9
- package/tx/workers/subsumes.js +13 -3
- package/tx/workers/translate.js +7 -3
- package/tx/workers/validate.js +258 -285
- package/tx/workers/worker.js +43 -39
- package/tx/xml/bundle-xml.js +237 -0
- package/tx/xml/xml-base.js +215 -64
- package/tx/xversion/xv-bundle.js +71 -0
- package/tx/xversion/xv-capabiliityStatement.js +137 -0
- package/tx/xversion/xv-codesystem.js +169 -0
- package/tx/xversion/xv-conceptmap.js +224 -0
- package/tx/xversion/xv-namingsystem.js +88 -0
- package/tx/xversion/xv-operationoutcome.js +27 -0
- package/tx/xversion/xv-parameters.js +87 -0
- package/tx/xversion/xv-resource.js +45 -0
- package/tx/xversion/xv-terminologyCapabilities.js +214 -0
- package/tx/xversion/xv-valueset.js +234 -0
- package/utilities/dev-proxy-server.js +126 -0
- package/utilities/explode-results.js +58 -0
- package/utilities/split-by-system.js +198 -0
- package/utilities/vsac-cs-fetcher.js +0 -0
- package/{windows-install.js → utilities/windows-install.js} +2 -0
- package/vcl/vcl.js +0 -18
- package/xig/xig.js +108 -99
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
const {VersionUtilities} = require("../../library/version-utilities");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts input TerminologyCapabilities to R5 format (modifies input object for performance)
|
|
5
|
+
* @param {Object} jsonObj - The input TerminologyCapabilities object
|
|
6
|
+
* @param {string} version - Source FHIR version
|
|
7
|
+
* @returns {Object} The same object, potentially modified to R5 format
|
|
8
|
+
* @private
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
function terminologyCapabilitiesToR5(jsonObj, sourceVersion) {
|
|
12
|
+
if (VersionUtilities.isR5Ver(sourceVersion)) {
|
|
13
|
+
return jsonObj; // No conversion needed
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (VersionUtilities.isR4Ver(sourceVersion)) {
|
|
17
|
+
// R4 to R5: No major structural changes needed for TerminologyCapabilities
|
|
18
|
+
return jsonObj;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (VersionUtilities.isR3Ver(sourceVersion)) {
|
|
22
|
+
// R3: TerminologyCapabilities doesn't exist - it's a Parameters resource
|
|
23
|
+
// Convert from Parameters format to TerminologyCapabilities
|
|
24
|
+
return convertParametersToR5(jsonObj);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return jsonObj;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Converts R3 Parameters format to R5 TerminologyCapabilities
|
|
33
|
+
* @param {Object} params - The Parameters resource
|
|
34
|
+
* @returns {Object} TerminologyCapabilities in R5 format
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
function convertParametersToR5(params) {
|
|
38
|
+
if (params.resourceType !== 'Parameters') {
|
|
39
|
+
throw new Error('R3 TerminologyCapabilities must be a Parameters resource');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = {
|
|
43
|
+
resourceType: 'TerminologyCapabilities',
|
|
44
|
+
id: params.id,
|
|
45
|
+
status: 'active', // Default, as Parameters doesn't carry this
|
|
46
|
+
kind: 'instance', // Default for terminology server capabilities
|
|
47
|
+
codeSystem: []
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const parameters = params.parameter || [];
|
|
51
|
+
let currentSystem = null;
|
|
52
|
+
|
|
53
|
+
for (const param of parameters) {
|
|
54
|
+
switch (param.name) {
|
|
55
|
+
case 'url':
|
|
56
|
+
result.url = param.valueUri;
|
|
57
|
+
break;
|
|
58
|
+
case 'version':
|
|
59
|
+
if (currentSystem) {
|
|
60
|
+
// This is a code system version
|
|
61
|
+
if (param.valueCode) {
|
|
62
|
+
currentSystem.version = currentSystem.version || [];
|
|
63
|
+
currentSystem.version.push({ code: param.valueCode });
|
|
64
|
+
}
|
|
65
|
+
// Empty version parameter means no specific version
|
|
66
|
+
} else {
|
|
67
|
+
// This is the TerminologyCapabilities version
|
|
68
|
+
result.version = param.valueCode || param.valueString;
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
case 'date':
|
|
72
|
+
result.date = param.valueDateTime;
|
|
73
|
+
break;
|
|
74
|
+
case 'system':
|
|
75
|
+
// Start a new code system
|
|
76
|
+
currentSystem = { uri: param.valueUri };
|
|
77
|
+
result.codeSystem.push(currentSystem);
|
|
78
|
+
break;
|
|
79
|
+
case 'expansion.parameter':
|
|
80
|
+
result.expansion = result.expansion || { parameter: [] };
|
|
81
|
+
result.expansion.parameter.push({ name: param.valueCode });
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Converts R5 TerminologyCapabilities to target version format (clones object first)
|
|
92
|
+
* @param {Object} r5Obj - The R5 format TerminologyCapabilities object
|
|
93
|
+
* @param {string} targetVersion - Target FHIR version
|
|
94
|
+
* @returns {Object} New object in target version format
|
|
95
|
+
* @private
|
|
96
|
+
*/
|
|
97
|
+
function terminologyCapabilitiesFromR5(r5Obj, targetVersion) {
|
|
98
|
+
if (VersionUtilities.isR5Ver(targetVersion)) {
|
|
99
|
+
return r5Obj; // No conversion needed
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Clone the object to avoid modifying the original
|
|
103
|
+
const cloned = JSON.parse(JSON.stringify(r5Obj));
|
|
104
|
+
|
|
105
|
+
if (VersionUtilities.isR4Ver(targetVersion)) {
|
|
106
|
+
return terminologyCapabilitiesR5ToR4(cloned);
|
|
107
|
+
} else if (VersionUtilities.isR3Ver(targetVersion)) {
|
|
108
|
+
return terminologyCapabilitiesR5ToR3(cloned);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw new Error(`Unsupported target FHIR version: ${targetVersion}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Converts R5 TerminologyCapabilities to R4 format
|
|
116
|
+
* @param {Object} r5Obj - Cloned R5 TerminologyCapabilities object
|
|
117
|
+
* @returns {Object} R4 format TerminologyCapabilities
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
function terminologyCapabilitiesR5ToR4(r5Obj) {
|
|
121
|
+
|
|
122
|
+
if (r5Obj.versionAlgorithmString) {
|
|
123
|
+
delete r5Obj.versionAlgorithmString;
|
|
124
|
+
}
|
|
125
|
+
if (r5Obj.versionAlgorithmCoding) {
|
|
126
|
+
delete r5Obj.versionAlgorithmCoding;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return r5Obj;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Converts R5 TerminologyCapabilities to R3 format
|
|
134
|
+
* @param {Object} r5Obj - Cloned R5 TerminologyCapabilities object
|
|
135
|
+
* @returns {Object} R3 format TerminologyCapabilities
|
|
136
|
+
* @private
|
|
137
|
+
*/
|
|
138
|
+
function terminologyCapabilitiesR5ToR3(r5Obj) {
|
|
139
|
+
// In R3, TerminologyCapabilities didn't exist - we represent it as a Parameters resource
|
|
140
|
+
const params = {
|
|
141
|
+
resourceType: 'Parameters',
|
|
142
|
+
id: r5Obj.id,
|
|
143
|
+
parameter: []
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Add url parameter
|
|
147
|
+
if (r5Obj.url) {
|
|
148
|
+
params.parameter.push({
|
|
149
|
+
name: 'url',
|
|
150
|
+
valueUri: r5Obj.url
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Add version parameter
|
|
155
|
+
if (r5Obj.version) {
|
|
156
|
+
params.parameter.push({
|
|
157
|
+
name: 'version',
|
|
158
|
+
valueCode: r5Obj.version
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Add date parameter
|
|
163
|
+
if (r5Obj.date) {
|
|
164
|
+
params.parameter.push({
|
|
165
|
+
name: 'date',
|
|
166
|
+
valueDateTime: r5Obj.date
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Add code systems with their versions
|
|
171
|
+
for (const codeSystem of r5Obj.codeSystem || []) {
|
|
172
|
+
// Add system parameter
|
|
173
|
+
params.parameter.push({
|
|
174
|
+
name: 'system',
|
|
175
|
+
valueUri: codeSystem.uri
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Add version parameter(s) for this code system
|
|
179
|
+
if (codeSystem.version && codeSystem.version.length > 0) {
|
|
180
|
+
for (const ver of codeSystem.version) {
|
|
181
|
+
if (ver.code) {
|
|
182
|
+
params.parameter.push({
|
|
183
|
+
name: 'version',
|
|
184
|
+
valueCode: ver.code
|
|
185
|
+
});
|
|
186
|
+
} else {
|
|
187
|
+
// Empty version parameter when no specific version
|
|
188
|
+
params.parameter.push({
|
|
189
|
+
name: 'version'
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
// No version specified for this code system
|
|
195
|
+
params.parameter.push({
|
|
196
|
+
name: 'version'
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Add expansion parameters
|
|
202
|
+
if (r5Obj.expansion && r5Obj.expansion.parameter) {
|
|
203
|
+
for (const expParam of r5Obj.expansion.parameter) {
|
|
204
|
+
params.parameter.push({
|
|
205
|
+
name: 'expansion.parameter',
|
|
206
|
+
valueCode: expParam.name
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return params;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = { terminologyCapabilitiesToR5, terminologyCapabilitiesFromR5 };
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
const {VersionUtilities} = require("../../library/version-utilities");
|
|
2
|
+
const {getValueName} = require("../../library/utilities");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts input ValueSet to R5 format (modifies input object for performance)
|
|
6
|
+
* @param {Object} jsonObj - The input ValueSet object
|
|
7
|
+
* @param {string} version - Source FHIR version
|
|
8
|
+
* @returns {Object} The same object, potentially modified to R5 format
|
|
9
|
+
* @private
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function valueSetToR5(jsonObj, sourceVersion) {
|
|
13
|
+
if (VersionUtilities.isR5Ver(sourceVersion)) {
|
|
14
|
+
return jsonObj; // No conversion needed
|
|
15
|
+
}
|
|
16
|
+
if (VersionUtilities.isR4Ver(sourceVersion)) {
|
|
17
|
+
return jsonObj; // No conversion needed
|
|
18
|
+
}
|
|
19
|
+
if (VersionUtilities.isR3Ver(sourceVersion)) {
|
|
20
|
+
// R3 to R5: Remove extensible field (we ignore it completely)
|
|
21
|
+
if (jsonObj.extensible !== undefined) {
|
|
22
|
+
delete jsonObj.extensible;
|
|
23
|
+
}
|
|
24
|
+
return jsonObj; // No conversion needed
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Unsupported FHIR version: ${sourceVersion}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Converts R5 ValueSet to target version format (clones object first)
|
|
31
|
+
* @param {Object} r5Obj - The R5 format ValueSet object
|
|
32
|
+
* @param {string} targetVersion - Target FHIR version
|
|
33
|
+
* @returns {Object} New object in target version format
|
|
34
|
+
* @private
|
|
35
|
+
*/
|
|
36
|
+
function valueSetFromR5(r5Obj, targetVersion) {
|
|
37
|
+
if (VersionUtilities.isR5Ver(targetVersion)) {
|
|
38
|
+
return r5Obj; // No conversion needed
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Clone the object to avoid modifying the original
|
|
42
|
+
const cloned = JSON.parse(JSON.stringify(r5Obj));
|
|
43
|
+
|
|
44
|
+
if (VersionUtilities.isR4Ver(targetVersion)) {
|
|
45
|
+
return valueSetR5ToR4(cloned);
|
|
46
|
+
} else if (VersionUtilities.isR3Ver(targetVersion)) {
|
|
47
|
+
return valueSetR5ToR3(cloned);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw new Error(`Unsupported target FHIR version: ${targetVersion}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Converts R5 ValueSet to R4 format
|
|
55
|
+
* @param {Object} r5Obj - Cloned R5 ValueSet object
|
|
56
|
+
* @returns {Object} R4 format ValueSet
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
function valueSetR5ToR4(r5Obj) {
|
|
60
|
+
if (r5Obj.versionAlgorithmString) {
|
|
61
|
+
delete r5Obj.versionAlgorithmString;
|
|
62
|
+
}
|
|
63
|
+
if (r5Obj.versionAlgorithmCoding) {
|
|
64
|
+
delete r5Obj.versionAlgorithmCoding;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Filter out R5-only filter operators in compose
|
|
68
|
+
if (r5Obj.compose && r5Obj.compose.include) {
|
|
69
|
+
r5Obj.compose.include = r5Obj.compose.include.map(include => {
|
|
70
|
+
if (include.filter && Array.isArray(include.filter)) {
|
|
71
|
+
include.filter = include.filter.map(filter => {
|
|
72
|
+
if (filter.op && isR5OnlyFilterOperator(filter.op)) {
|
|
73
|
+
// Remove R5-only operators
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return filter;
|
|
77
|
+
}).filter(filter => filter !== null);
|
|
78
|
+
}
|
|
79
|
+
return include;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (r5Obj.compose && r5Obj.compose.exclude) {
|
|
84
|
+
r5Obj.compose.exclude = r5Obj.compose.exclude.map(exclude => {
|
|
85
|
+
if (exclude.filter && Array.isArray(exclude.filter)) {
|
|
86
|
+
exclude.filter = exclude.filter.map(filter => {
|
|
87
|
+
if (filter.op && isR5OnlyFilterOperator(filter.op)) {
|
|
88
|
+
// Remove R5-only operators
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return filter;
|
|
92
|
+
}).filter(filter => filter !== null);
|
|
93
|
+
}
|
|
94
|
+
return exclude;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (r5Obj.expansion) {
|
|
99
|
+
let exp = r5Obj.expansion;
|
|
100
|
+
|
|
101
|
+
// Convert ValueSet.expansion.property to extensions
|
|
102
|
+
if (exp.property && exp.property.length > 0) {
|
|
103
|
+
exp.extension = exp.extension || [];
|
|
104
|
+
for (let prop of exp.property) {
|
|
105
|
+
exp.extension.push({
|
|
106
|
+
url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.property",
|
|
107
|
+
extension: [
|
|
108
|
+
{ url: "code", valueCode: prop.code },
|
|
109
|
+
{ url: "uri", valueUri: prop.uri }
|
|
110
|
+
]
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
delete exp.property;
|
|
114
|
+
convertContainsPropertyR5ToR4(exp.contains);
|
|
115
|
+
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return r5Obj;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Converts R5 ValueSet to R3 format
|
|
124
|
+
* @param {Object} r5Obj - Cloned R5 ValueSet object
|
|
125
|
+
* @returns {Object} R3 format ValueSet
|
|
126
|
+
* @private
|
|
127
|
+
*/
|
|
128
|
+
function valueSetR5ToR3(r5Obj) {
|
|
129
|
+
// First apply R4 conversions
|
|
130
|
+
const r4Obj = valueSetR5ToR4(r5Obj);
|
|
131
|
+
|
|
132
|
+
// R3 has more limited filter operator support
|
|
133
|
+
if (r4Obj.compose && r4Obj.compose.include) {
|
|
134
|
+
r4Obj.compose.include = r4Obj.compose.include.map(include => {
|
|
135
|
+
if (include.filter && Array.isArray(include.filter)) {
|
|
136
|
+
include.filter = include.filter.map(filter => {
|
|
137
|
+
if (filter.op && !isR3CompatibleFilterOperator(filter.op)) {
|
|
138
|
+
// Remove non-R3-compatible operators
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return filter;
|
|
142
|
+
}).filter(filter => filter !== null);
|
|
143
|
+
}
|
|
144
|
+
return include;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (r4Obj.compose && r4Obj.compose.exclude) {
|
|
149
|
+
r4Obj.compose.exclude = r4Obj.compose.exclude.map(exclude => {
|
|
150
|
+
if (exclude.filter && Array.isArray(exclude.filter)) {
|
|
151
|
+
exclude.filter = exclude.filter.map(filter => {
|
|
152
|
+
if (filter.op && !isR3CompatibleFilterOperator(filter.op)) {
|
|
153
|
+
// Remove non-R3-compatible operators
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return filter;
|
|
157
|
+
}).filter(filter => filter !== null);
|
|
158
|
+
}
|
|
159
|
+
return exclude;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return r4Obj;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
// Recursive function to convert contains.property
|
|
168
|
+
function convertContainsPropertyR5ToR4(containsList) {
|
|
169
|
+
if (!containsList) return;
|
|
170
|
+
|
|
171
|
+
for (let item of containsList) {
|
|
172
|
+
if (item.property && item.property.length > 0) {
|
|
173
|
+
item.extension = item.extension || [];
|
|
174
|
+
for (let prop of item.property) {
|
|
175
|
+
let ext = {
|
|
176
|
+
url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.contains.property",
|
|
177
|
+
extension: [
|
|
178
|
+
{ url: "code", valueCode: prop.code }
|
|
179
|
+
]
|
|
180
|
+
};
|
|
181
|
+
let pn = getValueName(prop);
|
|
182
|
+
let subExt = { url: "value" };
|
|
183
|
+
subExt[pn] = prop[pn];
|
|
184
|
+
ext.extension.push(subExt);
|
|
185
|
+
item.extension.push(ext);
|
|
186
|
+
}
|
|
187
|
+
delete item.property;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Recurse into nested contains
|
|
191
|
+
if (item.contains) {
|
|
192
|
+
convertContainsPropertyR5ToR4(item.contains);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Checks if a filter operator is R5-only
|
|
200
|
+
* @param {string} operator - Filter operator code
|
|
201
|
+
* @returns {boolean} True if operator is R5-only
|
|
202
|
+
* @private
|
|
203
|
+
*/
|
|
204
|
+
function isR5OnlyFilterOperator(operator) {
|
|
205
|
+
const r5OnlyOperators = [
|
|
206
|
+
'generalizes', // Added in R5
|
|
207
|
+
// Add other R5-only operators as they're identified
|
|
208
|
+
];
|
|
209
|
+
return r5OnlyOperators.includes(operator);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Checks if a filter operator is compatible with R3
|
|
214
|
+
* @param {string} operator - Filter operator code
|
|
215
|
+
* @returns {boolean} True if operator is R3-compatible
|
|
216
|
+
* @private
|
|
217
|
+
*/
|
|
218
|
+
function isR3CompatibleFilterOperator(operator) {
|
|
219
|
+
const r3CompatibleOperators = [
|
|
220
|
+
'=', // Equal
|
|
221
|
+
'is-a', // Is-A relationship
|
|
222
|
+
'descendent-of', // Descendant of (note: R3 spelling)
|
|
223
|
+
'is-not-a', // Is-Not-A relationship
|
|
224
|
+
'regex', // Regular expression
|
|
225
|
+
'in', // In set
|
|
226
|
+
'not-in', // Not in set
|
|
227
|
+
'exists', // Property exists
|
|
228
|
+
];
|
|
229
|
+
return r3CompatibleOperators.includes(operator);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
module.exports = { valueSetToR5, valueSetFromR5 };
|
|
234
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import https from 'https';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
|
|
6
|
+
const LISTEN_PORT = 3002;
|
|
7
|
+
const PROD_HOST = '127.0.0.1';
|
|
8
|
+
const PROD_PORT = 3001;
|
|
9
|
+
const DEV_HOST = 'tx-dev.fhir.org';
|
|
10
|
+
const DEV_PORT = 443;
|
|
11
|
+
const DEV_HTTPS = true;
|
|
12
|
+
const LOG_LOCATION = '/Users/grahamegrieve/temp/tx-comp-log/log.ndjson'; // 'T:\\logs\\comparison.ndjson';
|
|
13
|
+
|
|
14
|
+
const logStream = fs.createWriteStream(LOG_LOCATION, { flags: 'a' });
|
|
15
|
+
|
|
16
|
+
function collectBody(req) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const chunks = [];
|
|
19
|
+
req.on('data', c => chunks.push(c));
|
|
20
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
21
|
+
req.on('error', reject);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function forward(method, url, headers, body, host, port, useHttps = false) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const mod = useHttps ? https : http;
|
|
28
|
+
const req = mod.request({
|
|
29
|
+
hostname: host,
|
|
30
|
+
port,
|
|
31
|
+
path: url,
|
|
32
|
+
method,
|
|
33
|
+
headers: { ...headers, host: 'tx.fhir.org' },
|
|
34
|
+
timeout: 600000,
|
|
35
|
+
}, (res) => {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
res.on('data', c => chunks.push(c));
|
|
38
|
+
res.on('end', () => {
|
|
39
|
+
const body = Buffer.concat(chunks);
|
|
40
|
+
resolve({
|
|
41
|
+
status: res.statusCode,
|
|
42
|
+
headers: res.headers,
|
|
43
|
+
body,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
req.on('error', reject);
|
|
48
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
49
|
+
req.write(body);
|
|
50
|
+
req.end();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isJsonRequest(req) {
|
|
55
|
+
const accept = (req.headers['accept'] || '').toLowerCase();
|
|
56
|
+
const ct = (req.headers['content-type'] || '').toLowerCase();
|
|
57
|
+
return accept.includes('json') || accept.includes('fhir') ||
|
|
58
|
+
ct.includes('json') || ct.includes('fhir');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function summarise(result) {
|
|
62
|
+
if (result.status === 'rejected') {
|
|
63
|
+
return { error: result.reason?.message || 'unknown' };
|
|
64
|
+
}
|
|
65
|
+
const r = result.value;
|
|
66
|
+
return {
|
|
67
|
+
status: r.status,
|
|
68
|
+
contentType: r.headers['content-type'] || '',
|
|
69
|
+
size: r.body.length,
|
|
70
|
+
hash: crypto.createHash('md5').update(r.body).digest('hex'),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
http.createServer(async (req, res) => {
|
|
75
|
+
const body = await collectBody(req);
|
|
76
|
+
|
|
77
|
+
if (!isJsonRequest(req)) {
|
|
78
|
+
try {
|
|
79
|
+
const prod = await forward(req.method, req.url, req.headers, body, PROD_HOST, PROD_PORT);
|
|
80
|
+
res.writeHead(prod.status, prod.headers);
|
|
81
|
+
res.end(prod.body);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
res.writeHead(502);
|
|
84
|
+
res.end('Bad Gateway');
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const [prodResult, devResult] = await Promise.allSettled([
|
|
90
|
+
forward(req.method, req.url, req.headers, body, PROD_HOST, PROD_PORT),
|
|
91
|
+
forward(req.method, req.url, req.headers, body, DEV_HOST, DEV_PORT, DEV_HTTPS),
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
const prodSummary = summarise(prodResult);
|
|
95
|
+
const devSummary = summarise(devResult);
|
|
96
|
+
const match = prodSummary.hash === devSummary.hash && prodSummary.status === devSummary.status;
|
|
97
|
+
|
|
98
|
+
const logEntry = {
|
|
99
|
+
ts: new Date().toISOString(),
|
|
100
|
+
id: crypto.randomUUID(),
|
|
101
|
+
method: req.method,
|
|
102
|
+
url: req.url,
|
|
103
|
+
match,
|
|
104
|
+
prod: prodSummary,
|
|
105
|
+
dev: devSummary,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (!match) {
|
|
109
|
+
if (prodResult.status === 'fulfilled') logEntry.prodBody = prodResult.value.body.toString('utf8').substring(0, 50000);
|
|
110
|
+
if (devResult.status === 'fulfilled') logEntry.devBody = devResult.value.body.toString('utf8').substring(0, 50000);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
logStream.write(JSON.stringify(logEntry) + '\n');
|
|
114
|
+
|
|
115
|
+
if (prodResult.status === 'fulfilled') {
|
|
116
|
+
const p = prodResult.value;
|
|
117
|
+
res.writeHead(p.status, p.headers);
|
|
118
|
+
res.end(p.body);
|
|
119
|
+
} else {
|
|
120
|
+
res.writeHead(502);
|
|
121
|
+
res.end('Bad Gateway');
|
|
122
|
+
}
|
|
123
|
+
}).listen(LISTEN_PORT);
|
|
124
|
+
|
|
125
|
+
console.log(`Comparison proxy listening on port ${LISTEN_PORT}`);
|
|
126
|
+
console.log(`Production: ${PROD_HOST}:${PROD_PORT} | Dev: ${DEV_HOST}:${DEV_PORT}${DEV_HTTPS ? ' (HTTPS)' : ''}`);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
const dir = '/Users/grahamegrieve/temp/tx-comp/';
|
|
5
|
+
|
|
6
|
+
function stripDiagnostics(jsonStr) {
|
|
7
|
+
let obj;
|
|
8
|
+
try { obj = JSON.parse(jsonStr); } catch { return null; }
|
|
9
|
+
if (obj.resourceType === 'Parameters' && Array.isArray(obj.parameter)) {
|
|
10
|
+
obj.parameter = obj.parameter.filter(p => p.name !== 'diagnostics');
|
|
11
|
+
}
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const files = readdirSync(dir).filter(f => f.startsWith('system-') && f.endsWith('.ndjson'));
|
|
16
|
+
|
|
17
|
+
let totalWritten = 0;
|
|
18
|
+
let totalSkipped = 0;
|
|
19
|
+
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
const subdir = join(dir, file.replace('.ndjson', ''));
|
|
22
|
+
let prodDirCreated = false;
|
|
23
|
+
let devDirCreated = false;
|
|
24
|
+
const prodDir = join(subdir, 'prod');
|
|
25
|
+
const devDir = join(subdir, 'dev');
|
|
26
|
+
|
|
27
|
+
const lines = readFileSync(join(dir, file), 'utf8').split('\n').filter(l => l.trim());
|
|
28
|
+
let written = 0;
|
|
29
|
+
let skipped = 0;
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
let obj;
|
|
32
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
33
|
+
const id = obj.id || `unknown-${written + skipped}`;
|
|
34
|
+
|
|
35
|
+
const prod = obj.prodBody ? stripDiagnostics(obj.prodBody) : null;
|
|
36
|
+
const dev = obj.devBody ? stripDiagnostics(obj.devBody) : null;
|
|
37
|
+
|
|
38
|
+
// Compare after stripping diagnostics
|
|
39
|
+
const prodStr = prod ? JSON.stringify(prod) : '';
|
|
40
|
+
const devStr = dev ? JSON.stringify(dev) : '';
|
|
41
|
+
if (prodStr === devStr) {
|
|
42
|
+
skipped++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// They differ - write them out
|
|
47
|
+
if (!prodDirCreated) { mkdirSync(prodDir, { recursive: true }); prodDirCreated = true; }
|
|
48
|
+
if (!devDirCreated) { mkdirSync(devDir, { recursive: true }); devDirCreated = true; }
|
|
49
|
+
|
|
50
|
+
if (prod) writeFileSync(join(prodDir, `${id}.json`), JSON.stringify(prod, null, 2) + '\n');
|
|
51
|
+
if (dev) writeFileSync(join(devDir, `${id}.json`), JSON.stringify(dev, null, 2) + '\n');
|
|
52
|
+
written++;
|
|
53
|
+
}
|
|
54
|
+
totalWritten += written;
|
|
55
|
+
totalSkipped += skipped;
|
|
56
|
+
console.log(`${file}: ${written} differ, ${skipped} match (after removing diagnostics)`);
|
|
57
|
+
}
|
|
58
|
+
console.log(`\nDone. ${totalWritten} written, ${totalSkipped} skipped.`);
|