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.
- package/CHANGELOG.md +50 -0
- package/README.md +8 -0
- package/library/html.js +4 -0
- package/library/languages.js +10 -0
- package/package.json +1 -1
- package/packages/package-crawler.js +106 -51
- package/packages/packages.js +14 -0
- package/publisher/publisher.js +118 -28
- package/registry/registry.js +99 -91
- package/root-bare-template.html +92 -0
- package/security.md +32 -0
- package/server.js +99 -22
- package/stats.js +43 -10
- package/tx/README.md +6 -6
- package/tx/cs/cs-api.js +3 -0
- package/tx/cs/cs-api.md +285 -0
- package/tx/cs/cs-loinc.js +14 -2
- package/tx/cs/cs-rxnorm.js +14 -10
- package/tx/cs/cs-snomed.js +166 -5
- package/tx/html/dash-metrics.liquid +147 -0
- package/tx/importers/import-rxnorm.module.js +4 -30
- package/tx/importers/readme.md +3 -1
- package/tx/library/canonical-resource.js +8 -0
- package/tx/library/conceptmap.js +3 -1
- package/tx/library/designations.js +4 -8
- package/tx/library/renderer.js +9 -9
- package/tx/library.js +10 -4
- package/tx/ocl/cm-ocl.cjs +185 -65
- package/tx/ocl/cs-ocl.cjs +69 -50
- package/tx/ocl/jobs/background-queue.cjs +0 -8
- package/tx/ocl/mappers/concept-mapper.cjs +13 -3
- package/tx/ocl/shared/patches.cjs +1 -0
- package/tx/ocl/vs-ocl.cjs +137 -157
- package/tx/operation-context.js +3 -3
- package/tx/provider.js +4 -3
- package/tx/sct/structures.js +5 -0
- package/tx/tx-html.js +36 -9
- package/tx/tx.fhir.org.yml +1 -1
- package/tx/tx.js +34 -11
- package/tx/vs/vs-database.js +127 -6
- package/tx/vs/vs-vsac.js +98 -3
- package/tx/workers/search.js +2 -1
- package/tx/workers/translate.js +39 -14
- package/tx/workers/validate.js +3 -3
- package/utilities/dashboard.html +274 -0
- 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
|
|
237
|
+
html += '<hr/><h3>Source Content</h3>';
|
|
238
238
|
|
|
239
|
-
// List
|
|
240
|
-
html += '<h6>
|
|
241
|
-
if (provider.
|
|
242
|
-
const sorted = [...provider.
|
|
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
|
|
249
|
+
html += '<p><em>No FHIR Packages Loaded</em></p>';
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
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>`;
|
package/tx/tx.fhir.org.yml
CHANGED
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
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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) {
|
package/tx/vs/vs-database.js
CHANGED
|
@@ -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
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
package/tx/workers/search.js
CHANGED
|
@@ -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)
|
package/tx/workers/translate.js
CHANGED
|
@@ -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('
|
|
129
|
-
|
|
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
|
-
'
|
|
138
|
+
'codeableConcept must contain at least one coding', null, 400);
|
|
132
139
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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('
|
|
224
|
-
|
|
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
|
|
254
|
+
system,
|
|
230
255
|
version: params.get('version'),
|
|
231
|
-
code
|
|
256
|
+
code
|
|
232
257
|
};
|
|
233
258
|
} else {
|
|
234
259
|
throw new Issue('error', 'invalid', null, null,
|
package/tx/workers/validate.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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') {
|