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.
Files changed (92) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/library/cron-utilities.js +136 -0
  4. package/library/html-server.js +13 -29
  5. package/library/html.js +3 -8
  6. package/library/languages.js +160 -37
  7. package/library/package-manager.js +48 -1
  8. package/library/utilities.js +100 -19
  9. package/package.json +2 -2
  10. package/packages/package-crawler.js +6 -1
  11. package/packages/packages.js +38 -54
  12. package/publisher/publisher.js +19 -27
  13. package/registry/api.js +11 -10
  14. package/registry/crawler.js +31 -29
  15. package/registry/model.js +5 -26
  16. package/registry/registry.js +32 -41
  17. package/server.js +53 -5
  18. package/shl/shl.js +0 -18
  19. package/static/assets/js/statuspage.js +1 -9
  20. package/stats.js +39 -1
  21. package/token/token.js +14 -9
  22. package/translations/Messages.properties +2 -1
  23. package/tx/README.md +17 -6
  24. package/tx/cs/cs-api.js +19 -1
  25. package/tx/cs/cs-base.js +77 -0
  26. package/tx/cs/cs-country.js +46 -0
  27. package/tx/cs/cs-cpt.js +9 -5
  28. package/tx/cs/cs-cs.js +27 -13
  29. package/tx/cs/cs-lang.js +60 -22
  30. package/tx/cs/cs-loinc.js +69 -98
  31. package/tx/cs/cs-mimetypes.js +4 -0
  32. package/tx/cs/cs-ndc.js +6 -0
  33. package/tx/cs/cs-omop.js +16 -15
  34. package/tx/cs/cs-rxnorm.js +23 -1
  35. package/tx/cs/cs-snomed.js +283 -40
  36. package/tx/cs/cs-ucum.js +90 -70
  37. package/tx/importers/import-sct.module.js +371 -35
  38. package/tx/importers/readme.md +117 -7
  39. package/tx/library/bundle.js +5 -0
  40. package/tx/library/capabilitystatement.js +3 -142
  41. package/tx/library/codesystem.js +19 -173
  42. package/tx/library/conceptmap.js +4 -218
  43. package/tx/library/designations.js +14 -1
  44. package/tx/library/extensions.js +7 -0
  45. package/tx/library/namingsystem.js +3 -89
  46. package/tx/library/operation-outcome.js +8 -3
  47. package/tx/library/parameters.js +3 -2
  48. package/tx/library/renderer.js +10 -6
  49. package/tx/library/terminologycapabilities.js +3 -243
  50. package/tx/library/valueset.js +3 -235
  51. package/tx/library.js +100 -13
  52. package/tx/operation-context.js +23 -4
  53. package/tx/params.js +35 -38
  54. package/tx/provider.js +6 -5
  55. package/tx/sct/expressions.js +12 -3
  56. package/tx/tx-html.js +80 -89
  57. package/tx/tx.fhir.org.yml +6 -5
  58. package/tx/tx.js +163 -13
  59. package/tx/vs/vs-database.js +56 -39
  60. package/tx/vs/vs-package.js +21 -2
  61. package/tx/vs/vs-vsac.js +175 -39
  62. package/tx/workers/batch-validate.js +2 -0
  63. package/tx/workers/batch.js +2 -0
  64. package/tx/workers/expand.js +132 -112
  65. package/tx/workers/lookup.js +33 -14
  66. package/tx/workers/metadata.js +2 -2
  67. package/tx/workers/read.js +3 -2
  68. package/tx/workers/related.js +574 -0
  69. package/tx/workers/search.js +46 -9
  70. package/tx/workers/subsumes.js +13 -3
  71. package/tx/workers/translate.js +7 -3
  72. package/tx/workers/validate.js +258 -285
  73. package/tx/workers/worker.js +43 -39
  74. package/tx/xml/bundle-xml.js +237 -0
  75. package/tx/xml/xml-base.js +215 -64
  76. package/tx/xversion/xv-bundle.js +71 -0
  77. package/tx/xversion/xv-capabiliityStatement.js +137 -0
  78. package/tx/xversion/xv-codesystem.js +169 -0
  79. package/tx/xversion/xv-conceptmap.js +224 -0
  80. package/tx/xversion/xv-namingsystem.js +88 -0
  81. package/tx/xversion/xv-operationoutcome.js +27 -0
  82. package/tx/xversion/xv-parameters.js +87 -0
  83. package/tx/xversion/xv-resource.js +45 -0
  84. package/tx/xversion/xv-terminologyCapabilities.js +214 -0
  85. package/tx/xversion/xv-valueset.js +234 -0
  86. package/utilities/dev-proxy-server.js +126 -0
  87. package/utilities/explode-results.js +58 -0
  88. package/utilities/split-by-system.js +198 -0
  89. package/utilities/vsac-cs-fetcher.js +0 -0
  90. package/{windows-install.js → utilities/windows-install.js} +2 -0
  91. package/vcl/vcl.js +0 -18
  92. 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.`);