fhirsmith 0.7.5 → 0.8.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 (46) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +8 -0
  3. package/library/html.js +4 -0
  4. package/library/languages.js +10 -0
  5. package/package.json +1 -1
  6. package/packages/package-crawler.js +106 -51
  7. package/packages/packages.js +14 -0
  8. package/publisher/publisher.js +118 -28
  9. package/registry/registry.js +99 -91
  10. package/root-bare-template.html +92 -0
  11. package/security.md +32 -0
  12. package/server.js +99 -22
  13. package/stats.js +43 -10
  14. package/tx/README.md +6 -6
  15. package/tx/cs/cs-api.js +3 -0
  16. package/tx/cs/cs-api.md +285 -0
  17. package/tx/cs/cs-loinc.js +14 -2
  18. package/tx/cs/cs-rxnorm.js +14 -10
  19. package/tx/cs/cs-snomed.js +166 -5
  20. package/tx/html/dash-metrics.liquid +147 -0
  21. package/tx/importers/import-rxnorm.module.js +4 -30
  22. package/tx/importers/readme.md +3 -1
  23. package/tx/library/canonical-resource.js +8 -0
  24. package/tx/library/conceptmap.js +3 -1
  25. package/tx/library/designations.js +4 -8
  26. package/tx/library/renderer.js +9 -9
  27. package/tx/library.js +10 -4
  28. package/tx/ocl/cm-ocl.cjs +185 -65
  29. package/tx/ocl/cs-ocl.cjs +69 -50
  30. package/tx/ocl/jobs/background-queue.cjs +0 -8
  31. package/tx/ocl/mappers/concept-mapper.cjs +13 -3
  32. package/tx/ocl/shared/patches.cjs +1 -0
  33. package/tx/ocl/vs-ocl.cjs +137 -157
  34. package/tx/operation-context.js +3 -3
  35. package/tx/provider.js +4 -3
  36. package/tx/sct/structures.js +5 -0
  37. package/tx/tx-html.js +36 -9
  38. package/tx/tx.fhir.org.yml +1 -1
  39. package/tx/tx.js +34 -11
  40. package/tx/vs/vs-database.js +127 -6
  41. package/tx/vs/vs-vsac.js +98 -3
  42. package/tx/workers/search.js +2 -1
  43. package/tx/workers/translate.js +39 -14
  44. package/tx/workers/validate.js +3 -3
  45. package/utilities/dashboard.html +274 -0
  46. package/xig/xig.js +171 -9
package/tx/tx-html.js CHANGED
@@ -234,24 +234,44 @@ class TxHtmlRenderer {
234
234
  html += await this.buildSearchForm(req);
235
235
 
236
236
  // ===== Packages and Factories Section =====
237
- html += '<hr/><h3>Content Sources &amp; Code System Factories</h3>';
237
+ html += '<hr/><h3>Source Content</h3>';
238
238
 
239
- // List content sources
240
- html += '<h6>Content Sources</h6>';
241
- if (provider.contentSources && provider.contentSources.length > 0) {
242
- const sorted = [...provider.contentSources].sort();
239
+ // List Packages
240
+ html += '<h6>FHIR Packages</h6>';
241
+ if (provider.packageSources && provider.packageSources.length > 0) {
242
+ const sorted = [...provider.packageSources].sort();
243
243
  html += '<ul>';
244
244
  for (const source of sorted) {
245
245
  html += `<li>${escape(source)}</li>`;
246
246
  }
247
247
  html += '</ul>';
248
248
  } else {
249
- html += '<p><em>No content sources available</em></p>';
249
+ html += '<p><em>No FHIR Packages Loaded</em></p>';
250
250
  }
251
251
 
252
- // Code System Factories table
253
- // Code System Factories table
254
- html += '<h6 class="mt-4">External CodeSystems</h6>';
252
+ // List Packages
253
+ html += '<h6>External Sources</h6>';
254
+ if (provider.externalSources && provider.externalSources.length > 0) {
255
+ const sorted = [...provider.externalSources].sort();
256
+ html += '<ul>';
257
+ for (const source of sorted) {
258
+ let n = source.name();
259
+ if (!n) {
260
+ n = source.sourcePackage();
261
+ }
262
+ let ii = source.infoName();
263
+ if (ii) {
264
+ html += `<li>${escape(n)} (<a href="info/${source.id()}">${ii}</a>)</li>`;
265
+ } else {
266
+ html += `<li>${escape(n)}</li>`;
267
+ }
268
+ }
269
+ html += '</ul>';
270
+ } else {
271
+ html += '<p><em>No External Sources Configured</em></p>';
272
+ }
273
+
274
+ html += '<h6 class="mt-4">Special CodeSystems</h6>';
255
275
  html += '<table class="grid">';
256
276
  html += '<thead><tr><th>Name</th><th>URI</th><th>Version</th><th>Use Count</th></tr></thead>';
257
277
  html += '<tbody>';
@@ -1290,6 +1310,13 @@ class TxHtmlRenderer {
1290
1310
  });
1291
1311
  }
1292
1312
 
1313
+ async buildInfoPage(source, req) {
1314
+ let html = '';
1315
+ const infoContent = await source.info(req);
1316
+ html += infoContent;
1317
+ return html;
1318
+ }
1319
+
1293
1320
  buildSourceOptions(provider) {
1294
1321
  let result = '<option value=""></option>';
1295
1322
  result += `<option value="internal">internal</option>`;
@@ -13,7 +13,7 @@ sources:
13
13
  - ucum:tx/data/ucum-essence.xml
14
14
  - loinc:loinc-2.77-a.db
15
15
  - loinc!:loinc-2.81-b.db
16
- - rxnorm:rxnorm_02032025-a.db
16
+ - rxnorm:rxnorm_03022026.db
17
17
  - ndc:ndc-20211101.db
18
18
  - unii:unii_20240622.db
19
19
  - snomed:sct_intl_20240201.cache
package/tx/tx.js CHANGED
@@ -9,7 +9,7 @@ const express = require('express');
9
9
  const path = require('path');
10
10
  const Logger = require('../library/logger');
11
11
  const { Library } = require('./library');
12
- const { OperationContext, ResourceCache, ExpansionCache } = require('./operation-context');
12
+ const { OperationContext, ResourceCache, ExpansionCache, debugLog} = require('./operation-context');
13
13
  const { LanguageDefinitions } = require('../library/languages');
14
14
  const { I18nSupport } = require('../library/i18nsupport');
15
15
  const { CodeSystemXML } = require('./xml/codesystem-xml');
@@ -965,6 +965,29 @@ class TXModule {
965
965
  this.countRequest('home', Date.now() - start);
966
966
  }
967
967
  });
968
+
969
+ // External source info pages
970
+ router.get('/info/:id', async (req, res) => {
971
+ const start = Date.now();
972
+ try {
973
+ const source = req.txEndpoint.provider.externalSources.find(s => s.id() === req.params.id);
974
+ if (!source) {
975
+ res.status(404).send('Not found');
976
+ return;
977
+ }
978
+ let txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txEndpoint.provider), this.liquid, this.languages, this.i18n, req.txEndpoint.path);
979
+ const content = await txhtml.buildInfoPage(source, req);
980
+ const html = await txhtml.renderPage(source.name(), content, req.txEndpoint, start);
981
+ res.setHeader('Content-Type', 'text/html');
982
+ res.send(html);
983
+ } catch (error) {
984
+ debugLog(error);
985
+ this.log.error(`Error rendering info page for ${req.params.id}: ${error.message}`);
986
+ res.status(500).send('Internal server error');
987
+ } finally {
988
+ this.countRequest('info', Date.now() - start);
989
+ }
990
+ });
968
991
  }
969
992
 
970
993
  /**
@@ -1153,16 +1176,16 @@ class TXModule {
1153
1176
  ec = 0;
1154
1177
 
1155
1178
  checkProperJson() { // jsonStr) {
1156
- // const errors = [];
1157
- // if (jsonStr.includes("[]")) errors.push("Found [] in json");
1158
- // if (jsonStr.includes('""')) errors.push('Found "" in json');
1159
- //
1160
- // if (errors.length > 0) {
1161
- // this.ec++;
1162
- // const filename = `/Users/grahamegrieve/temp/tx-err-log/err${this.ec}.json`;
1163
- // writeFileSync(filename, jsonStr);
1164
- // throw new Error(errors.join('; '));
1165
- // }
1179
+ // const errors = [];
1180
+ // if (jsonStr.includes("[]")) errors.push("Found [] in json");
1181
+ // if (jsonStr.includes('""')) errors.push('Found "" in json');
1182
+ //
1183
+ // if (errors.length > 0) {
1184
+ // this.ec++;
1185
+ // const filename = `/Users/grahamegrieve/temp/tx-err-log/err${this.ec}.json`;
1186
+ // writeFileSync(filename, jsonStr);
1187
+ // throw new Error(errors.join('; '));
1188
+ // }
1166
1189
  }
1167
1190
 
1168
1191
  transformResourceForVersion(data, fhirVersion) {
@@ -22,6 +22,46 @@ class ValueSetDatabase {
22
22
  this._writeDb = null; // Write connection (opened only when needed)
23
23
  }
24
24
 
25
+ /**
26
+ * Apply any pending schema migrations
27
+ * @param {sqlite3.Database} db
28
+ * @returns {Promise<void>}
29
+ * @private
30
+ */
31
+ _migrateIfNeeded(db) {
32
+ return new Promise((resolve, reject) => {
33
+ db.all("PRAGMA table_info(valuesets)", [], (err, cols) => {
34
+ if (err) { reject(err); return; }
35
+ const hasCol = cols.some(c => c.name === 'date_first_seen');
36
+ const migrations = [];
37
+ if (!hasCol) {
38
+ migrations.push(new Promise((res, rej) => {
39
+ db.run(
40
+ "ALTER TABLE valuesets ADD COLUMN date_first_seen INTEGER DEFAULT 0",
41
+ [],
42
+ (err) => err ? rej(err) : res()
43
+ );
44
+ }));
45
+ }
46
+ // Ensure vsac_runs table exists
47
+ migrations.push(new Promise((res, rej) => {
48
+ db.run(`
49
+ CREATE TABLE IF NOT EXISTS vsac_runs (
50
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
51
+ started_at INTEGER NOT NULL,
52
+ finished_at INTEGER,
53
+ status TEXT NOT NULL DEFAULT 'running',
54
+ error_message TEXT,
55
+ total_fetched INTEGER,
56
+ total_new INTEGER
57
+ )
58
+ `, [], (err) => err ? rej(err) : res());
59
+ }));
60
+ Promise.all(migrations).then(() => resolve()).catch(reject);
61
+ });
62
+ });
63
+ }
64
+
25
65
  /**
26
66
  * Get a read-only database connection (opens lazily if needed)
27
67
  * @returns {Promise<sqlite3.Database>}
@@ -62,7 +102,7 @@ class ValueSetDatabase {
62
102
  this._writeDb = null;
63
103
  reject(new Error(`Failed to open database for writing: ${err.message}`));
64
104
  } else {
65
- resolve(this._writeDb);
105
+ this._migrateIfNeeded(this._writeDb).then(() => resolve(this._writeDb)).catch(reject);
66
106
  }
67
107
  });
68
108
  });
@@ -144,7 +184,8 @@ class ValueSetDatabase {
144
184
  status TEXT,
145
185
  title TEXT,
146
186
  content TEXT NOT NULL,
147
- last_seen INTEGER DEFAULT (strftime('%s', 'now'))
187
+ last_seen INTEGER DEFAULT (strftime('%s', 'now')),
188
+ date_first_seen INTEGER DEFAULT (strftime('%s', 'now'))
148
189
  )
149
190
  `);
150
191
 
@@ -182,6 +223,19 @@ class ValueSetDatabase {
182
223
  )
183
224
  `);
184
225
 
226
+ // Run tracking table
227
+ db.run(`
228
+ CREATE TABLE vsac_runs (
229
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
230
+ started_at INTEGER NOT NULL,
231
+ finished_at INTEGER,
232
+ status TEXT NOT NULL DEFAULT 'running',
233
+ error_message TEXT,
234
+ total_fetched INTEGER,
235
+ total_new INTEGER
236
+ )
237
+ `);
238
+
185
239
  // Create indexes for better search performance
186
240
  db.run('CREATE INDEX idx_valuesets_url ON valuesets(url, version)');
187
241
  db.run('CREATE INDEX idx_valuesets_version ON valuesets(version)');
@@ -190,6 +244,7 @@ class ValueSetDatabase {
190
244
  db.run('CREATE INDEX idx_valuesets_title ON valuesets(title)');
191
245
  db.run('CREATE INDEX idx_valuesets_publisher ON valuesets(publisher)');
192
246
  db.run('CREATE INDEX idx_valuesets_last_seen ON valuesets(last_seen)');
247
+ db.run('CREATE INDEX idx_valuesets_date_first_seen ON valuesets(date_first_seen)');
193
248
  db.run('CREATE INDEX idx_identifiers_system ON valueset_identifiers(system)');
194
249
  db.run('CREATE INDEX idx_identifiers_value ON valueset_identifiers(value)');
195
250
  db.run('CREATE INDEX idx_jurisdictions_system ON valueset_jurisdictions(system)');
@@ -208,6 +263,58 @@ class ValueSetDatabase {
208
263
  });
209
264
  }
210
265
 
266
+ /**
267
+ * Record the start of a VSAC sync run
268
+ * @returns {Promise<number>} The run ID
269
+ */
270
+ async startRun() {
271
+ const db = await this._getWriteConnection();
272
+ return new Promise((resolve, reject) => {
273
+ db.run(
274
+ `INSERT INTO vsac_runs (started_at, status) VALUES (strftime('%s','now'), 'running')`,
275
+ [],
276
+ function(err) { err ? reject(err) : resolve(this.lastID); }
277
+ );
278
+ });
279
+ }
280
+
281
+ /**
282
+ * Record the successful completion of a VSAC sync run
283
+ * @param {number} id - The run ID from startRun()
284
+ * @param {number} totalFetched - Total value sets fetched
285
+ * @param {number} totalNew - Number of new value sets found
286
+ * @returns {Promise<void>}
287
+ */
288
+ async finishRun(id, totalFetched, totalNew) {
289
+ const db = await this._getWriteConnection();
290
+ return new Promise((resolve, reject) => {
291
+ db.run(
292
+ `UPDATE vsac_runs SET finished_at = strftime('%s','now'), status = 'ok',
293
+ total_fetched = ?, total_new = ? WHERE id = ?`,
294
+ [totalFetched, totalNew, id],
295
+ err => err ? reject(err) : resolve()
296
+ );
297
+ });
298
+ }
299
+
300
+ /**
301
+ * Record a failed VSAC sync run
302
+ * @param {number} id - The run ID from startRun()
303
+ * @param {string} errorMessage - The error message
304
+ * @returns {Promise<void>}
305
+ */
306
+ async failRun(id, errorMessage) {
307
+ const db = await this._getWriteConnection();
308
+ return new Promise((resolve, reject) => {
309
+ db.run(
310
+ `UPDATE vsac_runs SET finished_at = strftime('%s','now'), status = 'error',
311
+ error_message = ? WHERE id = ?`,
312
+ [errorMessage, id],
313
+ err => err ? reject(err) : resolve()
314
+ );
315
+ });
316
+ }
317
+
211
318
  /**
212
319
  * Insert or update a single ValueSet in the database
213
320
  * @param {Object} valueSet - The ValueSet resource
@@ -246,10 +353,24 @@ class ValueSetDatabase {
246
353
  const expansionId = valueSet.expansion?.identifier || null;
247
354
 
248
355
  db.run(`
249
- INSERT OR REPLACE INTO valuesets (
250
- id, url, version, date, description, effectivePeriod_start, effectivePeriod_end,
251
- expansion_identifier, name, publisher, status, title, content, last_seen
252
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'))
356
+ INSERT INTO valuesets (
357
+ id, url, version, date, description, effectivePeriod_start, effectivePeriod_end,
358
+ expansion_identifier, name, publisher, status, title, content, last_seen, date_first_seen
359
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))
360
+ ON CONFLICT(id) DO UPDATE SET
361
+ url=excluded.url,
362
+ version=excluded.version,
363
+ date=excluded.date,
364
+ description=excluded.description,
365
+ effectivePeriod_start=excluded.effectivePeriod_start,
366
+ effectivePeriod_end=excluded.effectivePeriod_end,
367
+ expansion_identifier=excluded.expansion_identifier,
368
+ name=excluded.name,
369
+ publisher=excluded.publisher,
370
+ status=excluded.status,
371
+ title=excluded.title,
372
+ content=excluded.content,
373
+ last_seen=strftime('%s', 'now')
253
374
  `, [
254
375
  valueSet.id,
255
376
  valueSet.url,
package/tx/vs/vs-vsac.js CHANGED
@@ -70,6 +70,8 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
70
70
  if (!(await this.database.exists())) {
71
71
  await this.database.create();
72
72
  } else {
73
+ // Ensure schema is up to date (e.g. date_first_seen column added after initial deploy)
74
+ await this.database._migrateIfNeeded(await this.database._getWriteConnection());
73
75
  // Load existing data
74
76
  await this._reloadMap();
75
77
  }
@@ -96,6 +98,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
96
98
  try {
97
99
  await this.refreshValueSets();
98
100
  } catch (error) {
101
+ debugLog(error);
99
102
  this.log.error(error, 'Error during scheduled refresh:');
100
103
  }
101
104
  }, intervalMs);
@@ -124,6 +127,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
124
127
  this.queue = [];
125
128
 
126
129
  this.isRefreshing = true;
130
+ const runId = await this.database.startRun();
127
131
 
128
132
  try {
129
133
  // phase 1: list all value sets
@@ -200,10 +204,15 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
200
204
 
201
205
  // Reload map with fresh data
202
206
  await this._reloadMap();
203
- console.log(`VSAC refresh completed. Total: ${tracking.totalFetched} ValueSets, Deleted: ${tracking.deletedCount}`);
207
+ let msg = `VSAC refresh completed. Total: ${tracking.totalFetched} ValueSets, Deleted: ${tracking.deletedCount}`;
208
+ this.stats.taskDone('VSAC Sync', msg);
209
+ console.log(msg);
210
+
211
+ await this.database.finishRun(runId, tracking.totalFetched, tracking.totalNew);
204
212
  } catch (error) {
205
213
  debugLog(error, 'Error during VSAC refresh:');
206
- this.stats.task('VSAC Sync', `Error (${error.message})`);
214
+ this.stats.taskError('VSAC Sync', `Error (${error.message})`);
215
+ await this.database.failRun(runId, error.message);
207
216
  throw error;
208
217
  } finally {
209
218
  this.isRefreshing = false;
@@ -263,7 +272,6 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
263
272
  }
264
273
  }
265
274
 
266
-
267
275
  /**
268
276
  * Fetch a FHIR Bundle from the server
269
277
  * @param {string} url - Relative URL to fetch
@@ -509,7 +517,94 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
509
517
  let logMsg = `VSAC (${tracking.count} of ${length}) ${q}: ${vcount} versions`;
510
518
  console.log(logMsg);
511
519
  this.stats.task('VSAC Sync', logMsg);
520
+ }
521
+
522
+ name() {
523
+ return "VSAC";
524
+ }
525
+
526
+ infoName() {
527
+ return "history";
528
+ }
529
+
530
+ async info() {
531
+ const escape = require('escape-html');
532
+ const db = await this.database._getReadConnection();
533
+
534
+ const rows = await new Promise((resolve, reject) => {
535
+ db.all(
536
+ `SELECT 'vs' AS kind,
537
+ url,
538
+ version,
539
+ date_first_seen AS ts,
540
+ NULL AS status,
541
+ NULL AS error_message,
542
+ NULL AS finished_at,
543
+ NULL AS total_fetched,
544
+ NULL AS total_new
545
+ FROM valuesets
546
+ WHERE date_first_seen > 0
547
+ UNION ALL
548
+ SELECT 'run' AS kind,
549
+ NULL,
550
+ NULL,
551
+ started_at AS ts,
552
+ status,
553
+ error_message,
554
+ finished_at,
555
+ total_fetched,
556
+ total_new
557
+ FROM vsac_runs
558
+ ORDER BY ts DESC
559
+ LIMIT 200`,
560
+ [],
561
+ (err, rows) => err ? reject(err) : resolve(rows)
562
+ );
563
+ });
512
564
 
565
+ const fmt = ts => ts
566
+ ? new Date(ts * 1000).toISOString().replace('T', ' ').substring(0, 19) + ' UTC'
567
+ : '—';
568
+
569
+ let html = '<h3>VSAC Sync History</h3>';
570
+ html += '<table class="grid">';
571
+ html += '<thead><tr><th>Time</th><th>Event</th><th>Detail</th></tr></thead>';
572
+ html += '<tbody>';
573
+
574
+ for (const row of rows) {
575
+ if (row.kind === 'run') {
576
+ const duration = row.finished_at ? `${row.finished_at - row.ts}s` : 'in progress';
577
+ let detail, colour;
578
+ if (row.status === 'ok') {
579
+ detail = `${row.total_fetched} fetched, ${row.total_new} new, ${duration}`;
580
+ colour = 'green';
581
+ } else if (row.status === 'error') {
582
+ detail = `Failed: ${escape(row.error_message || '')} (${duration})`;
583
+ colour = 'red';
584
+ } else {
585
+ detail = `Running... (started ${fmt(row.ts)})`;
586
+ colour = 'orange';
587
+ }
588
+ html += `<tr style="background:#f0f0f0">`;
589
+ html += `<td>${escape(fmt(row.ts))}</td>`;
590
+ html += `<td><strong style="color:${colour}">Sync run</strong></td>`;
591
+ html += `<td>${detail}</td>`;
592
+ html += `</tr>`;
593
+ } else {
594
+ html += `<tr>`;
595
+ html += `<td>${escape(fmt(row.ts))}</td>`;
596
+ html += `<td>New value set</td>`;
597
+ html += `<td>${escape(row.url || '')}#${escape(row.version || '')}</td>`;
598
+ html += `</tr>`;
599
+ }
600
+ }
601
+
602
+ html += '</tbody></table>';
603
+ return html;
604
+ }
605
+
606
+ id() {
607
+ return "vsac";
513
608
  }
514
609
  }
515
610
 
@@ -35,7 +35,8 @@ class SearchWorker extends TerminologyWorker {
35
35
  '_offset', '_count', '_elements', '_sort', '_summary', '_total', '_format',
36
36
  'url', 'version', 'content-mode', 'date', 'description',
37
37
  'supplements', 'identifier', 'jurisdiction', 'name',
38
- 'publisher', 'status', 'system', 'title', 'text'
38
+ 'publisher', 'status', 'system', 'title', 'text',
39
+ 'source-system', 'target-system'
39
40
  ];
40
41
 
41
42
  // Summary elements for _summary=true (marked elements per resource type)
@@ -115,8 +115,12 @@ class TranslateWorker extends TerminologyWorker {
115
115
  let targetSystem = null;
116
116
 
117
117
  // Get the source coding
118
+ // Accept both R5 names (sourceCoding, sourceCodeableConcept, sourceCode/sourceSystem)
119
+ // and R4 names (coding, codeableConcept, code/system) as aliases
118
120
  if (params.has('sourceCoding')) {
119
121
  coding = params.get('sourceCoding');
122
+ } else if (params.has('coding')) {
123
+ coding = params.get('coding');
120
124
  } else if (params.has('sourceCodeableConcept')) {
121
125
  const cc = params.get('sourceCodeableConcept');
122
126
  if (cc.coding && cc.coding.length > 0) {
@@ -125,16 +129,23 @@ class TranslateWorker extends TerminologyWorker {
125
129
  throw new Issue('error', 'invalid', null, null,
126
130
  'sourceCodeableConcept must contain at least one coding', null, 400);
127
131
  }
128
- } else if (params.has('sourceCode')) {
129
- if (!params.has('sourceSystem')) {
132
+ } else if (params.has('codeableConcept')) {
133
+ const cc = params.get('codeableConcept');
134
+ if (cc.coding && cc.coding.length > 0) {
135
+ coding = cc.coding[0];
136
+ } else {
130
137
  throw new Issue('error', 'invalid', null, null,
131
- 'sourceSystem parameter is required when using sourceCode', null, 400);
138
+ 'codeableConcept must contain at least one coding', null, 400);
132
139
  }
133
- coding = {
134
- system: params.get('sourceSystem'),
135
- version: params.get('sourceVersion'),
136
- code: params.get('sourceCode')
137
- };
140
+ } else if (params.has('sourceCode') || params.has('code')) {
141
+ const code = params.has('sourceCode') ? params.get('sourceCode') : params.get('code');
142
+ const system = params.has('sourceSystem') ? params.get('sourceSystem') : params.get('system');
143
+ if (!system) {
144
+ throw new Issue('error', 'invalid', null, null,
145
+ 'system parameter is required when using code/sourceCode', null, 400);
146
+ }
147
+ const version = params.has('sourceVersion') ? params.get('sourceVersion') : params.get('version');
148
+ coding = { system, version, code };
138
149
  } else {
139
150
  throw new Issue('error', 'invalid', null, null,
140
151
  'Must provide sourceCode (with system), sourceCoding, or sourceCodeableConcept', null, 400);
@@ -169,7 +180,7 @@ class TranslateWorker extends TerminologyWorker {
169
180
  // If no explicit concept map, we need to find one based on source/target
170
181
  if (conceptMaps.length == 0) {
171
182
  await this.findConceptMapsInAdditionalResources(conceptMaps, coding.system, sourceScope, targetScope, targetSystem);
172
- await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, coding.system, sourceScope, targetScope, targetSystem);
183
+ await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, coding.system, sourceScope, targetScope, targetSystem, coding.code);
173
184
  if (conceptMaps.length == 0) {
174
185
  throw new Issue('error', 'not-found', null, null, 'No suitable ConceptMaps found for the specified source and target', null, 404);
175
186
  }
@@ -208,10 +219,14 @@ class TranslateWorker extends TerminologyWorker {
208
219
  txp.readParams(params.jsonObj);
209
220
 
210
221
  // Get the source coding
222
+ // Accept both R5 names (sourceCoding, sourceCodeableConcept, sourceCode)
223
+ // and R4 names (coding, codeableConcept, code) as aliases
211
224
  let coding = null;
212
225
 
213
226
  if (params.has('sourceCoding')) {
214
227
  coding = params.get('sourceCoding');
228
+ } else if (params.has('coding')) {
229
+ coding = params.get('coding');
215
230
  } else if (params.has('sourceCodeableConcept')) {
216
231
  const cc = params.get('sourceCodeableConcept');
217
232
  if (cc.coding && cc.coding.length > 0) {
@@ -220,15 +235,25 @@ class TranslateWorker extends TerminologyWorker {
220
235
  throw new Issue('error', 'invalid', null, null,
221
236
  'sourceCodeableConcept must contain at least one coding', null, 400);
222
237
  }
223
- } else if (params.has('sourceCode')) {
224
- if (!params.has('system')) {
238
+ } else if (params.has('codeableConcept')) {
239
+ const cc = params.get('codeableConcept');
240
+ if (cc.coding && cc.coding.length > 0) {
241
+ coding = cc.coding[0];
242
+ } else {
243
+ throw new Issue('error', 'invalid', null, null,
244
+ 'codeableConcept must contain at least one coding', null, 400);
245
+ }
246
+ } else if (params.has('sourceCode') || params.has('code')) {
247
+ const code = params.has('sourceCode') ? params.get('sourceCode') : params.get('code');
248
+ const system = params.has('system') ? params.get('system') : null;
249
+ if (!system) {
225
250
  throw new Issue('error', 'invalid', null, null,
226
- 'system parameter is required when using sourceCode', null, 400);
251
+ 'system parameter is required when using code/sourceCode', null, 400);
227
252
  }
228
253
  coding = {
229
- system: params.get('system'),
254
+ system,
230
255
  version: params.get('version'),
231
- code: params.get('sourceCode')
256
+ code
232
257
  };
233
258
  } else {
234
259
  throw new Issue('error', 'invalid', null, null,
@@ -1107,7 +1107,7 @@ class ValueSetChecker {
1107
1107
  codelist = !codelist ? '\'' + cc + '\'' : codelist + ', \'' + cc + '\'';
1108
1108
 
1109
1109
  if (v === false && !this.valueSet.jsonObj.internallyDefined && mode === 'codeableConcept') {
1110
- let m = this.worker.i18n.translate('None_of_the_provided_codes_are_in_the_value_set_one', this.params.HTTPLanguages, ['', this.valueSet.vurl, '\'' + cc + '\'']);
1110
+ let m = this.worker.i18n.translate('None_of_the_provided_codes_are_in_the_value_set_one', this.params.HTTPLanguages, ['', this.valueSet.vurlOrMsg, '\'' + cc + '\'']);
1111
1111
  let p = issuePath + '.coding[' + i + '].code';
1112
1112
  op.addIssue(new Issue('information', 'code-invalid', p, 'None_of_the_provided_codes_are_in_the_value_set_one', m, 'this-code-not-in-vs'));
1113
1113
  if (cause.value === 'null') {
@@ -1284,10 +1284,10 @@ class ValueSetChecker {
1284
1284
  let mid, m, p;
1285
1285
  if (mode === 'codeableConcept') {
1286
1286
  mid = 'TX_GENERAL_CC_ERROR_MESSAGE';
1287
- m = this.worker.i18n.translate('TX_GENERAL_CC_ERROR_MESSAGE', this.params.HTTPLanguages, [this.valueSet.vurl]);
1287
+ m = this.worker.i18n.translate('TX_GENERAL_CC_ERROR_MESSAGE', this.params.HTTPLanguages, [this.valueSet.vurlOrMsg]);
1288
1288
  } else {
1289
1289
  mid = 'None_of_the_provided_codes_are_in_the_value_set_one';
1290
- m = this.worker.i18n.translate('None_of_the_provided_codes_are_in_the_value_set_one', this.params.HTTPLanguages, ['', this.valueSet.vurl, codelist]);
1290
+ m = this.worker.i18n.translate('None_of_the_provided_codes_are_in_the_value_set_one', this.params.HTTPLanguages, ['', this.valueSet.vurlOrMsg, codelist]);
1291
1291
  }
1292
1292
 
1293
1293
  if (mode === 'codeableConcept') {