fhirsmith 0.18.2 → 1.0.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/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +149 -0
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +132 -59
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +74 -21
- package/ips/presenter.js +1 -1
- package/ips/readme.md +132 -25
- package/ips/render.js +466 -77
- package/ips/store.js +79 -22
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/crawler.js +84 -108
- package/registry/documentation/configuration.md +34 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +5 -4
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +154 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +29 -1
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +148 -14
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +185 -66
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +608 -64
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +57 -58
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +278 -42
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/ecl.js +47 -6
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +166 -13
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +27 -4
- package/tx/vs/vs-vsac.js +648 -100
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- package/tx/html/home-metrics.liquid +0 -225
package/tx/vs/vs-database.js
CHANGED
|
@@ -3,9 +3,15 @@ const crypto = require('crypto');
|
|
|
3
3
|
const sqlite3 = require('sqlite3').verbose();
|
|
4
4
|
const { VersionUtilities } = require('../../library/version-utilities');
|
|
5
5
|
const ValueSet = require("../library/valueset");
|
|
6
|
+
const { guarded } = require('../library/sqlite-utilities');
|
|
7
|
+
// the server log ({tx}), looked up when used (the database is opened by importers and tests too)
|
|
8
|
+
const txLog = () => require('../../library/logger').getInstance().child({ module: 'tx' });
|
|
6
9
|
|
|
7
10
|
// Settings key written once a package cache has been fully populated (see isComplete)
|
|
8
11
|
const CACHE_COMPLETE_KEY = 'cache-complete';
|
|
12
|
+
// A counter in vsac_settings that the VSAC sync bumps when it has written (cluster.md):
|
|
13
|
+
// a process that loaded the value set list at generation n is up to date while it says n
|
|
14
|
+
const GENERATION_KEY = 'generation';
|
|
9
15
|
|
|
10
16
|
// Columns that can be returned directly without parsing JSON
|
|
11
17
|
const INDEXED_COLUMNS = ['id', 'url', 'version', 'date', 'description', 'name', 'publisher', 'status', 'title'];
|
|
@@ -41,11 +47,22 @@ function isNewerVersion(candidate, current) {
|
|
|
41
47
|
class ValueSetDatabase {
|
|
42
48
|
vsCount;
|
|
43
49
|
|
|
50
|
+
// search() can build results with these elements from the table's columns
|
|
51
|
+
static INDEXED_COLUMNS = INDEXED_COLUMNS;
|
|
52
|
+
|
|
44
53
|
/**
|
|
45
54
|
* @param {string} dbPath - Path to the SQLite database file
|
|
55
|
+
* @param {Object} [options] - {shared: true} when several processes use the file (one
|
|
56
|
+
* writing, the others reading - cluster.md): WAL, so the readers don't block the writer,
|
|
57
|
+
* and a wait of up to 5 s for another process's lock instead of failing at once.
|
|
58
|
+
* {readOnly: true} for a cache another process builds (a tx-worker's package caches,
|
|
59
|
+
* cluster.md): opened read-only, never created or migrated - a file that still needs a
|
|
60
|
+
* migration fails to open (see _checkFinal), to be tried again once its builder is done
|
|
46
61
|
*/
|
|
47
|
-
constructor(dbPath) {
|
|
62
|
+
constructor(dbPath, options = {}) {
|
|
48
63
|
this.dbPath = dbPath;
|
|
64
|
+
this.shared = !!options.shared;
|
|
65
|
+
this.readOnly = !!options.readOnly;
|
|
49
66
|
// Single read-write connection used for everything. Using a separate
|
|
50
67
|
// OPEN_READONLY connection for reads can miss WAL-based schema changes
|
|
51
68
|
// made through the write connection (because read-only opens can't fully
|
|
@@ -69,13 +86,17 @@ class ValueSetDatabase {
|
|
|
69
86
|
// table_info` can race ahead of a `CREATE TABLE IF NOT EXISTS`, and
|
|
70
87
|
// you get "no such table" errors on DDL that should have been fine.
|
|
71
88
|
const run = (sql) => new Promise((res, rej) => {
|
|
72
|
-
db.run(sql, [], (err) => err ? rej(err) : res());
|
|
89
|
+
db.run(sql, [], guarded(rej, (err) => err ? rej(err) : res()));
|
|
73
90
|
});
|
|
74
91
|
const all = (sql) => new Promise((res, rej) => {
|
|
75
|
-
db.all(sql, [], (err, rows) => err ? rej(err) : res(rows));
|
|
92
|
+
db.all(sql, [], guarded(rej, (err, rows) => err ? rej(err) : res(rows)));
|
|
76
93
|
});
|
|
77
94
|
|
|
78
95
|
return (async () => {
|
|
96
|
+
if (this.shared) {
|
|
97
|
+
db.configure('busyTimeout', 5000);
|
|
98
|
+
await all('PRAGMA journal_mode = WAL');
|
|
99
|
+
}
|
|
79
100
|
const cols = await all("PRAGMA table_info(valuesets)");
|
|
80
101
|
const hasDateFirstSeen = cols.some(c => c.name === 'date_first_seen');
|
|
81
102
|
const hasContentHash = cols.some(c => c.name === 'content_hash');
|
|
@@ -138,7 +159,7 @@ class ValueSetDatabase {
|
|
|
138
159
|
);
|
|
139
160
|
const missing = (needHash[0] && needHash[0].n) || 0;
|
|
140
161
|
if (missing > 0) {
|
|
141
|
-
|
|
162
|
+
txLog().info(`Backfilling content_hash for ${missing} existing value sets...`);
|
|
142
163
|
const rows = await all(
|
|
143
164
|
"SELECT id, content FROM valuesets WHERE content_hash IS NULL"
|
|
144
165
|
);
|
|
@@ -149,15 +170,15 @@ class ValueSetDatabase {
|
|
|
149
170
|
db.run(
|
|
150
171
|
'UPDATE valuesets SET content_hash = ? WHERE id = ?',
|
|
151
172
|
[hash, row.id],
|
|
152
|
-
(err) => err ? rej(err) : res()
|
|
173
|
+
guarded(rej, (err) => err ? rej(err) : res())
|
|
153
174
|
);
|
|
154
175
|
});
|
|
155
176
|
done++;
|
|
156
177
|
if (done % 1000 === 0) {
|
|
157
|
-
|
|
178
|
+
txLog().info(` ...${done}/${missing}`);
|
|
158
179
|
}
|
|
159
180
|
}
|
|
160
|
-
|
|
181
|
+
txLog().info(`Backfilled ${done} hashes.`);
|
|
161
182
|
}
|
|
162
183
|
})();
|
|
163
184
|
}
|
|
@@ -194,25 +215,92 @@ class ValueSetDatabase {
|
|
|
194
215
|
if (this._migrationPromise) {
|
|
195
216
|
return this._migrationPromise;
|
|
196
217
|
}
|
|
218
|
+
if (this.readOnly) {
|
|
219
|
+
return this._openReadOnly();
|
|
220
|
+
}
|
|
197
221
|
this._migrationPromise = new Promise((resolve, reject) => {
|
|
198
222
|
if (this._writeDb) {
|
|
199
223
|
this._migrateIfNeeded(this._writeDb).then(resolve).catch(reject);
|
|
200
224
|
return;
|
|
201
225
|
}
|
|
202
|
-
this._writeDb = new sqlite3.Database(this.dbPath, (err) => {
|
|
226
|
+
this._writeDb = new sqlite3.Database(this.dbPath, guarded(reject, (err) => {
|
|
203
227
|
if (err) {
|
|
204
228
|
this._writeDb = null;
|
|
205
229
|
reject(new Error(`Failed to open database for writing: ${err.message}`));
|
|
206
230
|
return;
|
|
207
231
|
}
|
|
208
232
|
this._migrateIfNeeded(this._writeDb).then(resolve).catch(reject);
|
|
209
|
-
});
|
|
233
|
+
}));
|
|
210
234
|
});
|
|
211
235
|
// If migration fails, clear the cached promise so a retry can attempt again
|
|
212
236
|
this._migrationPromise.catch(() => { this._migrationPromise = null; });
|
|
213
237
|
return this._migrationPromise;
|
|
214
238
|
}
|
|
215
239
|
|
|
240
|
+
/**
|
|
241
|
+
* readOnly: open the file read-only, and check it needs no migration. On failure the
|
|
242
|
+
* connection is closed (the next use opens it again: the builder may have replaced it)
|
|
243
|
+
* @returns {Promise<void>}
|
|
244
|
+
* @private
|
|
245
|
+
*/
|
|
246
|
+
_openReadOnly() {
|
|
247
|
+
this._migrationPromise = new Promise((resolve, reject) => {
|
|
248
|
+
const db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY, (err) => {
|
|
249
|
+
if (err) {
|
|
250
|
+
reject(new Error(`Failed to open database ${this.dbPath} read-only: ${err.message}`));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
db.configure('busyTimeout', 5000);
|
|
254
|
+
this._checkFinal(db).then(() => {
|
|
255
|
+
this._writeDb = db;
|
|
256
|
+
resolve();
|
|
257
|
+
}, (error) => {
|
|
258
|
+
db.close(() => reject(error));
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
this._migrationPromise.catch(() => { this._migrationPromise = null; });
|
|
263
|
+
return this._migrationPromise;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Reject unless the file is as _migrateIfNeeded leaves it: every column and table there,
|
|
268
|
+
* and no row without its content_hash
|
|
269
|
+
* @param {sqlite3.Database} db
|
|
270
|
+
* @returns {Promise<void>}
|
|
271
|
+
* @private
|
|
272
|
+
*/
|
|
273
|
+
async _checkFinal(db) {
|
|
274
|
+
const all = (sql) => new Promise((res, rej) => {
|
|
275
|
+
db.all(sql, [], (err, rows) => err ? rej(err) : res(rows));
|
|
276
|
+
});
|
|
277
|
+
const columns = async (table) => (await all(`PRAGMA table_info(${table})`)).map(c => c.name);
|
|
278
|
+
const vsCols = await columns('valuesets');
|
|
279
|
+
const pending = [];
|
|
280
|
+
for (const col of ['date_first_seen', 'content_hash']) {
|
|
281
|
+
if (!vsCols.includes(col)) {
|
|
282
|
+
pending.push(`valuesets.${col}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (!(await columns('vsac_runs')).includes('total_updated')) {
|
|
286
|
+
pending.push('vsac_runs.total_updated');
|
|
287
|
+
}
|
|
288
|
+
for (const table of ['vsac_settings', 'vsac_events']) {
|
|
289
|
+
if ((await columns(table)).length === 0) {
|
|
290
|
+
pending.push(table);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (pending.length === 0) {
|
|
294
|
+
const rows = await all('SELECT COUNT(*) AS n FROM valuesets WHERE content_hash IS NULL');
|
|
295
|
+
if (rows[0] && rows[0].n > 0) {
|
|
296
|
+
pending.push(`content_hash of ${rows[0].n} value sets`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (pending.length > 0) {
|
|
300
|
+
throw new Error(`Database ${this.dbPath} is not migrated yet (${pending.join(', ')}), and is opened read-only`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
216
304
|
/**
|
|
217
305
|
* Close all database connections
|
|
218
306
|
* @returns {Promise<void>}
|
|
@@ -227,7 +315,7 @@ class ValueSetDatabase {
|
|
|
227
315
|
|
|
228
316
|
await new Promise((resolve) => {
|
|
229
317
|
this._writeDb.close((err) => {
|
|
230
|
-
if (err)
|
|
318
|
+
if (err) txLog().warn(`Warning closing database connection: ${err.message}`);
|
|
231
319
|
this._writeDb = null;
|
|
232
320
|
resolve();
|
|
233
321
|
});
|
|
@@ -256,7 +344,7 @@ class ValueSetDatabase {
|
|
|
256
344
|
await this.close();
|
|
257
345
|
|
|
258
346
|
return new Promise((resolve, reject) => {
|
|
259
|
-
const db = new sqlite3.Database(this.dbPath, (err) => {
|
|
347
|
+
const db = new sqlite3.Database(this.dbPath, guarded(reject, (err) => {
|
|
260
348
|
if (err) {
|
|
261
349
|
reject(new Error(`Failed to create database ${this.dbPath}: ${err.message}`));
|
|
262
350
|
return;
|
|
@@ -378,7 +466,7 @@ class ValueSetDatabase {
|
|
|
378
466
|
}
|
|
379
467
|
});
|
|
380
468
|
});
|
|
381
|
-
});
|
|
469
|
+
}));
|
|
382
470
|
});
|
|
383
471
|
}
|
|
384
472
|
|
|
@@ -455,6 +543,36 @@ class ValueSetDatabase {
|
|
|
455
543
|
});
|
|
456
544
|
}
|
|
457
545
|
|
|
546
|
+
/**
|
|
547
|
+
* The most recent VSAC sync run, or null if there has never been one
|
|
548
|
+
* @returns {Promise<{id: number, status: string, started_at: number, finished_at: number|null}|null>}
|
|
549
|
+
*/
|
|
550
|
+
async getLastRun() {
|
|
551
|
+
const db = await this._getReadConnection();
|
|
552
|
+
return new Promise((resolve, reject) => {
|
|
553
|
+
db.get('SELECT id, status, started_at, finished_at FROM vsac_runs ORDER BY id DESC LIMIT 1', [],
|
|
554
|
+
(err, row) => err ? reject(err) : resolve(row || null));
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Close off runs still marked 'running'. Only one process syncs a cache, so at
|
|
560
|
+
* startup any such run was cut short when the server stopped.
|
|
561
|
+
* @returns {Promise<number>} the number of runs marked as interrupted
|
|
562
|
+
*/
|
|
563
|
+
async markInterruptedRuns() {
|
|
564
|
+
const db = await this._getWriteConnection();
|
|
565
|
+
return new Promise((resolve, reject) => {
|
|
566
|
+
db.run(
|
|
567
|
+
`UPDATE vsac_runs SET finished_at = strftime('%s','now'), status = 'error',
|
|
568
|
+
error_message = 'interrupted (server stopped during the sync)'
|
|
569
|
+
WHERE status = 'running'`,
|
|
570
|
+
[],
|
|
571
|
+
function(err) { err ? reject(err) : resolve(this.changes); }
|
|
572
|
+
);
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
458
576
|
/**
|
|
459
577
|
* Get a setting value from the vsac_settings table
|
|
460
578
|
* @param {string} key - The setting key
|
|
@@ -463,10 +581,32 @@ class ValueSetDatabase {
|
|
|
463
581
|
async getSetting(key) {
|
|
464
582
|
const db = await this._getReadConnection();
|
|
465
583
|
return new Promise((resolve, reject) => {
|
|
466
|
-
db.get('SELECT value FROM vsac_settings WHERE key = ?', [key], (err, row) => {
|
|
584
|
+
db.get('SELECT value FROM vsac_settings WHERE key = ?', [key], guarded(reject, (err, row) => {
|
|
467
585
|
if (err) reject(err);
|
|
468
586
|
else resolve(row ? row.value : null);
|
|
469
|
-
});
|
|
587
|
+
}));
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* The generation (see GENERATION_KEY): 0 until a sync has bumped it. One read by key.
|
|
593
|
+
* @returns {Promise<number>}
|
|
594
|
+
*/
|
|
595
|
+
async getGeneration() {
|
|
596
|
+
const value = await this.getSetting(GENERATION_KEY);
|
|
597
|
+
return value === null ? 0 : Number(value);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Add one to the generation, in one statement.
|
|
602
|
+
* @returns {Promise<void>}
|
|
603
|
+
*/
|
|
604
|
+
async bumpGeneration() {
|
|
605
|
+
const db = await this._getWriteConnection();
|
|
606
|
+
return new Promise((resolve, reject) => {
|
|
607
|
+
db.run(`INSERT INTO vsac_settings (key, value) VALUES (?, '1')
|
|
608
|
+
ON CONFLICT(key) DO UPDATE SET value = CAST(value AS INTEGER) + 1`,
|
|
609
|
+
[GENERATION_KEY], err => err ? reject(err) : resolve());
|
|
470
610
|
});
|
|
471
611
|
}
|
|
472
612
|
|
|
@@ -488,6 +628,18 @@ class ValueSetDatabase {
|
|
|
488
628
|
});
|
|
489
629
|
}
|
|
490
630
|
|
|
631
|
+
/**
|
|
632
|
+
* Remove a setting from the vsac_settings table
|
|
633
|
+
* @param {string} key - The setting key
|
|
634
|
+
* @returns {Promise<void>}
|
|
635
|
+
*/
|
|
636
|
+
async deleteSetting(key) {
|
|
637
|
+
const db = await this._getWriteConnection();
|
|
638
|
+
return new Promise((resolve, reject) => {
|
|
639
|
+
db.run('DELETE FROM vsac_settings WHERE key = ?', [key], err => err ? reject(err) : resolve());
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
491
643
|
/**
|
|
492
644
|
* True if this cache was fully populated: markComplete() was reached. A cache
|
|
493
645
|
* whose population was cut short (the process killed part way through) has no
|
|
@@ -514,31 +666,35 @@ class ValueSetDatabase {
|
|
|
514
666
|
/**
|
|
515
667
|
* Insert or update a single ValueSet in the database
|
|
516
668
|
* @param {Object} valueSet - The ValueSet resource
|
|
517
|
-
* @param {string} [contentHash] - Optional pre-computed content hash to store
|
|
669
|
+
* @param {string} [contentHash] - Optional pre-computed content hash to store; by default the
|
|
670
|
+
* hash of the content stored, as the backfill in _migrateIfNeeded computes it, so no row is
|
|
671
|
+
* written without one (a row without one is backfilled by the next process to open the file)
|
|
518
672
|
* @returns {Promise<void>}
|
|
519
673
|
*/
|
|
520
674
|
async upsertValueSet(valueSet, contentHash = null) {
|
|
521
675
|
if (!valueSet.url) {
|
|
522
676
|
throw new Error('ValueSet must have a url property');
|
|
523
677
|
}
|
|
678
|
+
const content = JSON.stringify(valueSet);
|
|
679
|
+
contentHash = contentHash || crypto.createHash('sha256').update(content).digest('hex');
|
|
524
680
|
|
|
525
681
|
const db = await this._getWriteConnection();
|
|
526
682
|
|
|
527
683
|
return new Promise((resolve, reject) => {
|
|
528
684
|
// Step 1: Delete existing related records
|
|
529
|
-
db.run('DELETE FROM valueset_identifiers WHERE valueset_id = ?', [valueSet.id], (err) => {
|
|
685
|
+
db.run('DELETE FROM valueset_identifiers WHERE valueset_id = ?', [valueSet.id], guarded(reject, (err) => {
|
|
530
686
|
if (err) {
|
|
531
687
|
reject(new Error(`Failed to delete identifiers: ${err.message}`));
|
|
532
688
|
return;
|
|
533
689
|
}
|
|
534
690
|
|
|
535
|
-
db.run('DELETE FROM valueset_jurisdictions WHERE valueset_id = ?', [valueSet.id], (err) => {
|
|
691
|
+
db.run('DELETE FROM valueset_jurisdictions WHERE valueset_id = ?', [valueSet.id], guarded(reject, (err) => {
|
|
536
692
|
if (err) {
|
|
537
693
|
reject(new Error(`Failed to delete jurisdictions: ${err.message}`));
|
|
538
694
|
return;
|
|
539
695
|
}
|
|
540
696
|
|
|
541
|
-
db.run('DELETE FROM valueset_systems WHERE valueset_id = ?', [valueSet.id], (err) => {
|
|
697
|
+
db.run('DELETE FROM valueset_systems WHERE valueset_id = ?', [valueSet.id], guarded(reject, (err) => {
|
|
542
698
|
if (err) {
|
|
543
699
|
reject(new Error(`Failed to delete systems: ${err.message}`));
|
|
544
700
|
return;
|
|
@@ -583,9 +739,9 @@ class ValueSetDatabase {
|
|
|
583
739
|
valueSet.publisher || null,
|
|
584
740
|
valueSet.status || null,
|
|
585
741
|
valueSet.title || null,
|
|
586
|
-
|
|
742
|
+
content,
|
|
587
743
|
contentHash
|
|
588
|
-
], (err) => {
|
|
744
|
+
], guarded(reject, (err) => {
|
|
589
745
|
if (err) {
|
|
590
746
|
reject(new Error(`Failed to insert main record: ${err.message}`));
|
|
591
747
|
return;
|
|
@@ -593,10 +749,10 @@ class ValueSetDatabase {
|
|
|
593
749
|
|
|
594
750
|
// Step 3: Insert related records
|
|
595
751
|
this._insertRelatedRecords(db, valueSet, resolve, reject);
|
|
596
|
-
});
|
|
597
|
-
});
|
|
598
|
-
});
|
|
599
|
-
});
|
|
752
|
+
}));
|
|
753
|
+
}));
|
|
754
|
+
}));
|
|
755
|
+
}));
|
|
600
756
|
});
|
|
601
757
|
}
|
|
602
758
|
|
|
@@ -758,7 +914,7 @@ class ValueSetDatabase {
|
|
|
758
914
|
const db = await this._getReadConnection();
|
|
759
915
|
|
|
760
916
|
return new Promise((resolve, reject) => {
|
|
761
|
-
db.all('SELECT id, url, version, content, content_hash FROM valuesets', [], (err, rows) => {
|
|
917
|
+
db.all('SELECT id, url, version, content, content_hash FROM valuesets', [], guarded(reject, (err, rows) => {
|
|
762
918
|
if (err) {
|
|
763
919
|
reject(new Error(`Failed to load value sets: ${err.message}`));
|
|
764
920
|
return;
|
|
@@ -769,21 +925,11 @@ class ValueSetDatabase {
|
|
|
769
925
|
const valueSetMap = new Map();
|
|
770
926
|
|
|
771
927
|
for (const row of rows) {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
valueSet = new ValueSet(JSON.parse(row.content));
|
|
775
|
-
} catch (e) {
|
|
776
|
-
// one unacceptable value set (e.g. one containing a CodeSystem, which the
|
|
777
|
-
// server doesn't support) must not take the whole package's value sets with it
|
|
778
|
-
require('../../library/logger').getInstance().child({ module: 'tx' })
|
|
779
|
-
.warn(`${source || 'ValueSet database'}: skipping ValueSet ${row.url || row.id}${row.version ? '|' + row.version : ''}: ${e.message}`);
|
|
928
|
+
const valueSet = this._rowToValueSet(row, source);
|
|
929
|
+
if (!valueSet) {
|
|
780
930
|
this.vsCount--;
|
|
781
931
|
continue;
|
|
782
932
|
}
|
|
783
|
-
valueSet.sourcePackage = source;
|
|
784
|
-
// Attach the stored content hash so callers can detect changes
|
|
785
|
-
// without recomputing over the full JSON.
|
|
786
|
-
valueSet.contentHash = row.content_hash || null;
|
|
787
933
|
// Store by URL and id alone
|
|
788
934
|
this.addToMap(valueSetMap, row.id, row.url, row.version, valueSet);
|
|
789
935
|
}
|
|
@@ -792,10 +938,135 @@ class ValueSetDatabase {
|
|
|
792
938
|
} catch (error) {
|
|
793
939
|
reject(new Error(`Failed to parse value set content: ${error.message}`));
|
|
794
940
|
}
|
|
795
|
-
});
|
|
941
|
+
}));
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Build the ValueSet for a row (id, url, version, content, content_hash) - or null if the
|
|
947
|
+
* content isn't an acceptable value set, which is logged and then treated as absent
|
|
948
|
+
* @param {Object} row
|
|
949
|
+
* @param {string} source
|
|
950
|
+
* @returns {ValueSet|null}
|
|
951
|
+
* @private
|
|
952
|
+
*/
|
|
953
|
+
_rowToValueSet(row, source) {
|
|
954
|
+
let valueSet;
|
|
955
|
+
try {
|
|
956
|
+
valueSet = new ValueSet(JSON.parse(row.content));
|
|
957
|
+
} catch (e) {
|
|
958
|
+
// one unacceptable value set (e.g. one containing a CodeSystem, which the
|
|
959
|
+
// server doesn't support) must not take the whole package's value sets with it
|
|
960
|
+
require('../../library/logger').getInstance().child({ module: 'tx' })
|
|
961
|
+
.warn(`${source || 'ValueSet database'}: skipping ValueSet ${row.url || row.id}${row.version ? '|' + row.version : ''}: ${e.message}`);
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
valueSet.sourcePackage = source;
|
|
965
|
+
// Attach the stored content hash so callers can detect changes
|
|
966
|
+
// without recomputing over the full JSON.
|
|
967
|
+
valueSet.contentHash = row.content_hash || null;
|
|
968
|
+
return valueSet;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Read one ValueSet by id, for providers that read value sets on demand instead of
|
|
973
|
+
* holding them all in memory (see loadAllValueSets)
|
|
974
|
+
* @param {string} id
|
|
975
|
+
* @param {string} source
|
|
976
|
+
* @returns {Promise<ValueSet|null>} null if there's no such row, or it was rejected
|
|
977
|
+
*/
|
|
978
|
+
async loadValueSet(id, source) {
|
|
979
|
+
const db = await this._getReadConnection();
|
|
980
|
+
const row = await new Promise((resolve, reject) => {
|
|
981
|
+
db.get('SELECT id, url, version, content, content_hash FROM valuesets WHERE id = ?', [id],
|
|
982
|
+
(err, row) => err ? reject(new Error(`Failed to load value set ${id}: ${err.message}`)) : resolve(row));
|
|
983
|
+
});
|
|
984
|
+
const vs = row ? this._rowToValueSet(row, source) : null;
|
|
985
|
+
if (vs) {
|
|
986
|
+
// the size of what it was read from, for a provider that keeps some (see VSAC RecentValueSets)
|
|
987
|
+
vs.storedLength = row.content.length;
|
|
988
|
+
}
|
|
989
|
+
return vs;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* The id and version of every row for a url (or every row, for null), in the order loadAllValueSets sees them -
|
|
994
|
+
* enough to work out which one a url (and version) refers to (see addToMap) without
|
|
995
|
+
* reading any content
|
|
996
|
+
* @param {string} url
|
|
997
|
+
* @returns {Promise<Array<{id: string, url: string, version: string}>>}
|
|
998
|
+
*/
|
|
999
|
+
async listVersions(url) {
|
|
1000
|
+
const db = await this._getReadConnection();
|
|
1001
|
+
return new Promise((resolve, reject) => {
|
|
1002
|
+
db.all(url == null ? 'SELECT id, url, version FROM valuesets ORDER BY rowid' : 'SELECT id, url, version FROM valuesets WHERE url = ? ORDER BY rowid', url == null ? [] : [url],
|
|
1003
|
+
(err, rows) => err ? reject(new Error(`Failed to list versions of ${url}: ${err.message}`)) : resolve(rows));
|
|
796
1004
|
});
|
|
797
1005
|
}
|
|
798
1006
|
|
|
1007
|
+
/**
|
|
1008
|
+
* Read several ValueSets by id
|
|
1009
|
+
* @param {string[]} ids
|
|
1010
|
+
* @param {string} source
|
|
1011
|
+
* @returns {Promise<Map<string, ValueSet>>} id -> ValueSet, for the ids there are acceptable rows for
|
|
1012
|
+
*/
|
|
1013
|
+
async loadValueSets(ids, source) {
|
|
1014
|
+
const db = await this._getReadConnection();
|
|
1015
|
+
const result = new Map();
|
|
1016
|
+
for (const row of (await this._readRows(db, ids)).values()) {
|
|
1017
|
+
const vs = this._rowToValueSet(row, source);
|
|
1018
|
+
if (vs) {
|
|
1019
|
+
result.set(row.id, vs);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return result;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* The stored content hash for url|version, or undefined if there's no such row
|
|
1027
|
+
* @param {string} url
|
|
1028
|
+
* @param {string} version
|
|
1029
|
+
* @returns {Promise<{id: string, content_hash: string}|undefined>}
|
|
1030
|
+
*/
|
|
1031
|
+
async getVersion(url, version) {
|
|
1032
|
+
const db = await this._getReadConnection();
|
|
1033
|
+
return new Promise((resolve, reject) => {
|
|
1034
|
+
db.get('SELECT id, content_hash FROM valuesets WHERE url = ? AND version = ?', [url, version],
|
|
1035
|
+
(err, row) => err ? reject(new Error(`Failed to read ${url}|${version}: ${err.message}`)) : resolve(row));
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Every url|version in the database (as a Set of 'url|version' strings)
|
|
1041
|
+
* @returns {Promise<Set<string>>}
|
|
1042
|
+
*/
|
|
1043
|
+
async listVersionKeys() {
|
|
1044
|
+
const db = await this._getReadConnection();
|
|
1045
|
+
return new Promise((resolve, reject) => {
|
|
1046
|
+
db.all('SELECT url, version FROM valuesets WHERE version IS NOT NULL', [], guarded(reject, (err, rows) => {
|
|
1047
|
+
if (err) {
|
|
1048
|
+
reject(new Error(`Failed to list value set versions: ${err.message}`));
|
|
1049
|
+
} else {
|
|
1050
|
+
resolve(new Set(rows.map(row => `${row.url}|${row.version}`)));
|
|
1051
|
+
}
|
|
1052
|
+
}));
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Count the value sets (sets vsCount), and the distinct urls among them
|
|
1058
|
+
* @returns {Promise<{total: number, urls: number}>}
|
|
1059
|
+
*/
|
|
1060
|
+
async countValueSets() {
|
|
1061
|
+
const db = await this._getReadConnection();
|
|
1062
|
+
const row = await new Promise((resolve, reject) => {
|
|
1063
|
+
db.get('SELECT COUNT(*) AS total, COUNT(DISTINCT url) AS urls FROM valuesets', [],
|
|
1064
|
+
(err, row) => err ? reject(new Error(`Failed to count value sets: ${err.message}`)) : resolve(row));
|
|
1065
|
+
});
|
|
1066
|
+
this.vsCount = row.total;
|
|
1067
|
+
return row;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
799
1070
|
addToMap(valueSetMap, id, url, version, valueSet) {
|
|
800
1071
|
// The bare-url key must resolve to the *latest* version, but rows arrive in no
|
|
801
1072
|
// particular order (and several versions of the same url coexist - e.g. VSAC, where
|
|
@@ -840,17 +1111,22 @@ class ValueSetDatabase {
|
|
|
840
1111
|
const canOptimize = elements && elements.length > 0 &&
|
|
841
1112
|
elements.every(e => INDEXED_COLUMNS.includes(e));
|
|
842
1113
|
|
|
843
|
-
// Always include 'id' in the columns to select when optimizing
|
|
844
|
-
|
|
1114
|
+
// Always include 'id' in the columns to select when optimizing (and the version, when the
|
|
1115
|
+
// results are matched on it - see versionSearchMatches)
|
|
1116
|
+
const versionAsked = searchParams.filter(p => p.name.toLowerCase() === 'version').map(p => p.value);
|
|
1117
|
+
let columnsToSelect = canOptimize
|
|
845
1118
|
? (elements.includes('id') ? elements : ['id', ...elements])
|
|
846
1119
|
: null;
|
|
1120
|
+
if (columnsToSelect && versionAsked.length > 0 && !columnsToSelect.includes('version')) {
|
|
1121
|
+
columnsToSelect = [...columnsToSelect, 'version'];
|
|
1122
|
+
}
|
|
847
1123
|
|
|
848
1124
|
const db = await this._getReadConnection();
|
|
849
1125
|
|
|
850
1126
|
return new Promise((resolve, reject) => {
|
|
851
1127
|
const { query, params } = this._buildSearchQuery(searchParams, columnsToSelect);
|
|
852
1128
|
|
|
853
|
-
db.all(query, params, (err, rows) => {
|
|
1129
|
+
db.all(query, params, guarded(reject, (err, rows) => {
|
|
854
1130
|
if (err) {
|
|
855
1131
|
reject(new Error(`Search query failed: ${err.message}`));
|
|
856
1132
|
return;
|
|
@@ -879,12 +1155,57 @@ class ValueSetDatabase {
|
|
|
879
1155
|
// (see loadAllValueSets), so it isn't a result
|
|
880
1156
|
results = rows.map(row => map.get(row.id)).filter(vs => vs);
|
|
881
1157
|
}
|
|
1158
|
+
for (const criteria of versionAsked) {
|
|
1159
|
+
results = results.filter(r => {
|
|
1160
|
+
const json = r.jsonObj || r;
|
|
1161
|
+
return VersionUtilities.versionSearchMatches(criteria, json.version, VersionUtilities.versionAlgorithmOf(json));
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
882
1164
|
|
|
883
1165
|
resolve(results);
|
|
884
1166
|
} catch (error) {
|
|
885
1167
|
reject(new Error(`Failed to parse search results: ${error.message}`));
|
|
886
1168
|
}
|
|
887
|
-
});
|
|
1169
|
+
}));
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* The rows that match a search, in the order search() finds them, with only the indexed
|
|
1175
|
+
* columns a search is sorted by - for a provider that reads the matching value sets on demand,
|
|
1176
|
+
* and only reads the ones it has to return
|
|
1177
|
+
* @param {Array<{name: string, value: string}>} searchParams
|
|
1178
|
+
* @returns {Promise<Array<{id: string, url: string, version: string, date: string, name: string}>>}
|
|
1179
|
+
*/
|
|
1180
|
+
async searchRows(searchParams) {
|
|
1181
|
+
const db = await this._getReadConnection();
|
|
1182
|
+
const { query, params } = this._buildSearchQuery(searchParams, ['id', 'url', 'version', 'date', 'name']);
|
|
1183
|
+
const rows = await new Promise((resolve, reject) => {
|
|
1184
|
+
db.all(query, params, (err, rows) => err ? reject(new Error(`Search query failed: ${err.message}`)) : resolve(rows));
|
|
1185
|
+
});
|
|
1186
|
+
// the query only narrows a version search to the versions that could match: they are matched
|
|
1187
|
+
// here, as search() matches them (on the version alone: the rows have no versionAlgorithm)
|
|
1188
|
+
const versionAsked = searchParams.filter(p => p.name.toLowerCase() === 'version').map(p => p.value);
|
|
1189
|
+
return rows.filter(row => versionAsked.every(criteria => VersionUtilities.versionSearchMatches(criteria, row.version)));
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* Read the rows for a list of ids
|
|
1194
|
+
* @param {sqlite3.Database} db
|
|
1195
|
+
* @param {string[]} ids
|
|
1196
|
+
* @returns {Promise<Map<string, Object>>} id -> row (id, url, version, content, content_hash)
|
|
1197
|
+
* @private
|
|
1198
|
+
*/
|
|
1199
|
+
_readRows(db, ids) {
|
|
1200
|
+
return new Promise((resolve, reject) => {
|
|
1201
|
+
db.all('SELECT id, url, version, content, content_hash FROM valuesets WHERE id IN (SELECT value FROM json_each(?))',
|
|
1202
|
+
[JSON.stringify(ids)], guarded(reject, (err, rows) => {
|
|
1203
|
+
if (err) {
|
|
1204
|
+
reject(new Error(`Failed to read value sets: ${err.message}`));
|
|
1205
|
+
} else {
|
|
1206
|
+
resolve(new Map(rows.map(row => [row.id, row])));
|
|
1207
|
+
}
|
|
1208
|
+
}));
|
|
888
1209
|
});
|
|
889
1210
|
}
|
|
890
1211
|
|
|
@@ -898,7 +1219,7 @@ class ValueSetDatabase {
|
|
|
898
1219
|
|
|
899
1220
|
return new Promise((resolve, reject) => {
|
|
900
1221
|
// Get URLs to delete first
|
|
901
|
-
db.all('SELECT url FROM valuesets WHERE last_seen < ?', [cutoffTimestamp], (err, rows) => {
|
|
1222
|
+
db.all('SELECT url FROM valuesets WHERE last_seen < ?', [cutoffTimestamp], guarded(reject, (err, rows) => {
|
|
902
1223
|
if (err) {
|
|
903
1224
|
reject(new Error(`Failed to find old records: ${err.message}`));
|
|
904
1225
|
return;
|
|
@@ -941,21 +1262,21 @@ class ValueSetDatabase {
|
|
|
941
1262
|
|
|
942
1263
|
pendingDeletes = 3; // identifiers, jurisdictions, systems
|
|
943
1264
|
|
|
944
|
-
db.run(`DELETE FROM valueset_identifiers WHERE valueset_id IN (${placeholders})`, idsToDelete, (err) => {
|
|
1265
|
+
db.run(`DELETE FROM valueset_identifiers WHERE valueset_id IN (${placeholders})`, idsToDelete, guarded(reject, (err) => {
|
|
945
1266
|
if (err) deleteError(new Error(`Failed to delete identifier records: ${err.message}`));
|
|
946
1267
|
else deleteComplete();
|
|
947
|
-
});
|
|
1268
|
+
}));
|
|
948
1269
|
|
|
949
|
-
db.run(`DELETE FROM valueset_jurisdictions WHERE valueset_id IN (${placeholders})`, idsToDelete, (err) => {
|
|
1270
|
+
db.run(`DELETE FROM valueset_jurisdictions WHERE valueset_id IN (${placeholders})`, idsToDelete, guarded(reject, (err) => {
|
|
950
1271
|
if (err) deleteError(new Error(`Failed to delete jurisdiction records: ${err.message}`));
|
|
951
1272
|
else deleteComplete();
|
|
952
|
-
});
|
|
1273
|
+
}));
|
|
953
1274
|
|
|
954
|
-
db.run(`DELETE FROM valueset_systems WHERE valueset_id IN (${placeholders})`, idsToDelete, (err) => {
|
|
1275
|
+
db.run(`DELETE FROM valueset_systems WHERE valueset_id IN (${placeholders})`, idsToDelete, guarded(reject, (err) => {
|
|
955
1276
|
if (err) deleteError(new Error(`Failed to delete system records: ${err.message}`));
|
|
956
1277
|
else deleteComplete();
|
|
957
|
-
});
|
|
958
|
-
});
|
|
1278
|
+
}));
|
|
1279
|
+
}));
|
|
959
1280
|
});
|
|
960
1281
|
}
|
|
961
1282
|
|
|
@@ -992,17 +1313,17 @@ class ValueSetDatabase {
|
|
|
992
1313
|
};
|
|
993
1314
|
|
|
994
1315
|
// Total count
|
|
995
|
-
db.get(queries[0], [], (err, row) => {
|
|
1316
|
+
db.get(queries[0], [], guarded(reject, (err, row) => {
|
|
996
1317
|
if (err) {
|
|
997
1318
|
handleError(err);
|
|
998
1319
|
return;
|
|
999
1320
|
}
|
|
1000
1321
|
results.totalValueSets = row.total;
|
|
1001
1322
|
checkComplete();
|
|
1002
|
-
});
|
|
1323
|
+
}));
|
|
1003
1324
|
|
|
1004
1325
|
// Status breakdown
|
|
1005
|
-
db.all(queries[1], [], (err, rows) => {
|
|
1326
|
+
db.all(queries[1], [], guarded(reject, (err, rows) => {
|
|
1006
1327
|
if (err) {
|
|
1007
1328
|
handleError(err);
|
|
1008
1329
|
return;
|
|
@@ -1012,17 +1333,17 @@ class ValueSetDatabase {
|
|
|
1012
1333
|
results.byStatus[row.status || 'null'] = row.count;
|
|
1013
1334
|
}
|
|
1014
1335
|
checkComplete();
|
|
1015
|
-
});
|
|
1336
|
+
}));
|
|
1016
1337
|
|
|
1017
1338
|
// System count
|
|
1018
|
-
db.get(queries[2], [], (err, row) => {
|
|
1339
|
+
db.get(queries[2], [], guarded(reject, (err, row) => {
|
|
1019
1340
|
if (err) {
|
|
1020
1341
|
handleError(err);
|
|
1021
1342
|
return;
|
|
1022
1343
|
}
|
|
1023
1344
|
results.totalSystems = row.systems;
|
|
1024
1345
|
checkComplete();
|
|
1025
|
-
});
|
|
1346
|
+
}));
|
|
1026
1347
|
});
|
|
1027
1348
|
}
|
|
1028
1349
|
|
|
@@ -1043,13 +1364,16 @@ class ValueSetDatabase {
|
|
|
1043
1364
|
|
|
1044
1365
|
switch (name.toLowerCase()) {
|
|
1045
1366
|
case 'url':
|
|
1046
|
-
|
|
1047
|
-
|
|
1367
|
+
// a left match (case-sensitive): the url starts with the value
|
|
1368
|
+
conditions.push('substr(v.url, 1, length(?)) = ?');
|
|
1369
|
+
params.push(value, value);
|
|
1048
1370
|
break;
|
|
1049
1371
|
|
|
1050
1372
|
case 'version':
|
|
1051
|
-
|
|
1052
|
-
|
|
1373
|
+
// version-aware matching (see VersionUtilities.versionSearchMatches): narrowed here to
|
|
1374
|
+
// the versions that could match, and matched on the results (see search)
|
|
1375
|
+
conditions.push("v.version LIKE ? ESCAPE '\\'");
|
|
1376
|
+
params.push(VersionUtilities.versionSearchPrefix(value).replace(/[\\%_]/g, c => '\\' + c) + '%');
|
|
1053
1377
|
break;
|
|
1054
1378
|
|
|
1055
1379
|
case 'name':
|
|
@@ -1149,7 +1473,7 @@ class ValueSetDatabase {
|
|
|
1149
1473
|
const db = await this._getReadConnection();
|
|
1150
1474
|
|
|
1151
1475
|
return new Promise((resolve, reject) => {
|
|
1152
|
-
db.all('SELECT url FROM valuesets ORDER BY url', [], (err, rows) => {
|
|
1476
|
+
db.all('SELECT url FROM valuesets ORDER BY url', [], guarded(reject, (err, rows) => {
|
|
1153
1477
|
if (err) {
|
|
1154
1478
|
reject(new Error(`Failed to list value sets: ${err.message}`));
|
|
1155
1479
|
return;
|
|
@@ -1157,7 +1481,7 @@ class ValueSetDatabase {
|
|
|
1157
1481
|
|
|
1158
1482
|
const urls = rows.map(row => row.url);
|
|
1159
1483
|
resolve(urls);
|
|
1160
|
-
});
|
|
1484
|
+
}));
|
|
1161
1485
|
});
|
|
1162
1486
|
}
|
|
1163
1487
|
}
|