fhirsmith 0.9.0 → 0.9.2
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 +31 -0
- package/package.json +1 -1
- package/root-bare-template.html +58 -9775
- package/translations/Messages.properties +2 -2
- package/tx/cs/cs-cs.js +32 -5
- package/tx/importers/import-sct.module.js +167 -79
- package/tx/library/extensions.js +7 -1
- package/tx/library/renderer.js +11 -2
- package/tx/library.js +3 -0
- package/tx/vs/vs-database.js +213 -92
- package/tx/vs/vs-vsac.js +118 -50
- package/tx/workers/validate.js +1 -1
- package/tx/xversion/xv-valueset.js +28 -8
package/tx/vs/vs-database.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require('fs').promises;
|
|
2
|
+
const crypto = require('crypto');
|
|
2
3
|
const sqlite3 = require('sqlite3').verbose();
|
|
3
4
|
const { VersionUtilities } = require('../../library/version-utilities');
|
|
4
5
|
const ValueSet = require("../library/valueset");
|
|
@@ -18,8 +19,13 @@ class ValueSetDatabase {
|
|
|
18
19
|
*/
|
|
19
20
|
constructor(dbPath) {
|
|
20
21
|
this.dbPath = dbPath;
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
// Single read-write connection used for everything. Using a separate
|
|
23
|
+
// OPEN_READONLY connection for reads can miss WAL-based schema changes
|
|
24
|
+
// made through the write connection (because read-only opens can't fully
|
|
25
|
+
// participate in the shared-memory protocol), so queries issued right
|
|
26
|
+
// after a migration ALTER TABLE can fail with a stale schema cache.
|
|
27
|
+
this._writeDb = null;
|
|
28
|
+
this._migrationPromise = null;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
/**
|
|
@@ -29,46 +35,104 @@ class ValueSetDatabase {
|
|
|
29
35
|
* @private
|
|
30
36
|
*/
|
|
31
37
|
_migrateIfNeeded(db) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
// Run migrations SEQUENTIALLY. node-sqlite3 does not guarantee that
|
|
39
|
+
// separately-submitted statements run in submission order on the same
|
|
40
|
+
// connection — `db.serialize()` is opt-in. Without sequencing, a
|
|
41
|
+
// `CREATE INDEX` can race ahead of its `CREATE TABLE`, or a `PRAGMA
|
|
42
|
+
// table_info` can race ahead of a `CREATE TABLE IF NOT EXISTS`, and
|
|
43
|
+
// you get "no such table" errors on DDL that should have been fine.
|
|
44
|
+
const run = (sql) => new Promise((res, rej) => {
|
|
45
|
+
db.run(sql, [], (err) => err ? rej(err) : res());
|
|
46
|
+
});
|
|
47
|
+
const all = (sql) => new Promise((res, rej) => {
|
|
48
|
+
db.all(sql, [], (err, rows) => err ? rej(err) : res(rows));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return (async () => {
|
|
52
|
+
const cols = await all("PRAGMA table_info(valuesets)");
|
|
53
|
+
const hasDateFirstSeen = cols.some(c => c.name === 'date_first_seen');
|
|
54
|
+
const hasContentHash = cols.some(c => c.name === 'content_hash');
|
|
55
|
+
|
|
56
|
+
if (!hasDateFirstSeen) {
|
|
57
|
+
await run("ALTER TABLE valuesets ADD COLUMN date_first_seen INTEGER DEFAULT 0");
|
|
58
|
+
}
|
|
59
|
+
if (!hasContentHash) {
|
|
60
|
+
await run("ALTER TABLE valuesets ADD COLUMN content_hash TEXT");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Ensure vsac_runs table exists (with total_updated for fresh installs)
|
|
64
|
+
await run(`
|
|
65
|
+
CREATE TABLE IF NOT EXISTS vsac_runs (
|
|
66
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
|
+
started_at INTEGER NOT NULL,
|
|
68
|
+
finished_at INTEGER,
|
|
69
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
70
|
+
error_message TEXT,
|
|
71
|
+
total_fetched INTEGER,
|
|
72
|
+
total_new INTEGER,
|
|
73
|
+
total_updated INTEGER
|
|
74
|
+
)
|
|
75
|
+
`);
|
|
76
|
+
|
|
77
|
+
// If vsac_runs already existed (older schema), add total_updated column
|
|
78
|
+
const runCols = await all("PRAGMA table_info(vsac_runs)");
|
|
79
|
+
const hasTotalUpdated = runCols.some(c => c.name === 'total_updated');
|
|
80
|
+
if (!hasTotalUpdated && runCols.length > 0) {
|
|
81
|
+
await run("ALTER TABLE vsac_runs ADD COLUMN total_updated INTEGER");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Ensure vsac_settings table exists (for _lastUpdated tracking etc.)
|
|
85
|
+
await run(`
|
|
86
|
+
CREATE TABLE IF NOT EXISTS vsac_settings (
|
|
87
|
+
key TEXT PRIMARY KEY,
|
|
88
|
+
value TEXT
|
|
89
|
+
)
|
|
90
|
+
`);
|
|
91
|
+
|
|
92
|
+
// Ensure vsac_events table exists (audit log of new/updated/deleted value sets)
|
|
93
|
+
await run(`
|
|
94
|
+
CREATE TABLE IF NOT EXISTS vsac_events (
|
|
95
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
96
|
+
timestamp INTEGER NOT NULL,
|
|
97
|
+
event_type TEXT NOT NULL,
|
|
98
|
+
url TEXT NOT NULL,
|
|
99
|
+
version TEXT,
|
|
100
|
+
detail TEXT
|
|
101
|
+
)
|
|
102
|
+
`);
|
|
103
|
+
await run("CREATE INDEX IF NOT EXISTS idx_events_timestamp ON vsac_events(timestamp)");
|
|
104
|
+
|
|
105
|
+
// Backfill content_hash for any existing rows that don't have one.
|
|
106
|
+
// This establishes a baseline so the NEXT sync can detect real content
|
|
107
|
+
// changes immediately (otherwise the first sync just silently populates
|
|
108
|
+
// hashes and can never flag anything as 'updated').
|
|
109
|
+
const needHash = await all(
|
|
110
|
+
"SELECT COUNT(*) AS n FROM valuesets WHERE content_hash IS NULL"
|
|
111
|
+
);
|
|
112
|
+
const missing = (needHash[0] && needHash[0].n) || 0;
|
|
113
|
+
if (missing > 0) {
|
|
114
|
+
console.log(`Backfilling content_hash for ${missing} existing value sets...`);
|
|
115
|
+
const rows = await all(
|
|
116
|
+
"SELECT id, content FROM valuesets WHERE content_hash IS NULL"
|
|
117
|
+
);
|
|
118
|
+
let done = 0;
|
|
119
|
+
for (const row of rows) {
|
|
120
|
+
const hash = crypto.createHash('sha256').update(row.content).digest('hex');
|
|
121
|
+
await new Promise((res, rej) => {
|
|
39
122
|
db.run(
|
|
40
|
-
|
|
41
|
-
[],
|
|
123
|
+
'UPDATE valuesets SET content_hash = ? WHERE id = ?',
|
|
124
|
+
[hash, row.id],
|
|
42
125
|
(err) => err ? rej(err) : res()
|
|
43
126
|
);
|
|
44
|
-
})
|
|
127
|
+
});
|
|
128
|
+
done++;
|
|
129
|
+
if (done % 1000 === 0) {
|
|
130
|
+
console.log(` ...${done}/${missing}`);
|
|
131
|
+
}
|
|
45
132
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
// Ensure vsac_settings table exists (for _lastUpdated tracking etc.)
|
|
61
|
-
migrations.push(new Promise((res, rej) => {
|
|
62
|
-
db.run(`
|
|
63
|
-
CREATE TABLE IF NOT EXISTS vsac_settings (
|
|
64
|
-
key TEXT PRIMARY KEY,
|
|
65
|
-
value TEXT
|
|
66
|
-
)
|
|
67
|
-
`, [], (err) => err ? rej(err) : res());
|
|
68
|
-
}));
|
|
69
|
-
Promise.all(migrations).then(() => resolve()).catch(reject);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
133
|
+
console.log(`Backfilled ${done} hashes.`);
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
72
136
|
}
|
|
73
137
|
|
|
74
138
|
/**
|
|
@@ -77,21 +141,9 @@ class ValueSetDatabase {
|
|
|
77
141
|
* @private
|
|
78
142
|
*/
|
|
79
143
|
_getReadConnection() {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
this._db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY, (err) => {
|
|
87
|
-
if (err) {
|
|
88
|
-
this._db = null;
|
|
89
|
-
reject(new Error(`Failed to open database ${this.dbPath}: ${err.message}`));
|
|
90
|
-
} else {
|
|
91
|
-
resolve(this._db);
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
});
|
|
144
|
+
// Reads go through the same connection as writes. See the constructor
|
|
145
|
+
// comment for why we don't use a separate OPEN_READONLY connection.
|
|
146
|
+
return this._ensureMigrated().then(() => this._writeDb);
|
|
95
147
|
}
|
|
96
148
|
|
|
97
149
|
/**
|
|
@@ -100,21 +152,38 @@ class ValueSetDatabase {
|
|
|
100
152
|
* @private
|
|
101
153
|
*/
|
|
102
154
|
_getWriteConnection() {
|
|
103
|
-
return
|
|
155
|
+
return this._ensureMigrated().then(() => this._writeDb);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Ensure the database schema is migrated. Idempotent: subsequent calls
|
|
160
|
+
* return the cached promise. Opens a write connection (which is required
|
|
161
|
+
* for ALTER TABLE) if one is not already open. The write connection is
|
|
162
|
+
* kept open for reuse by later _getWriteConnection calls.
|
|
163
|
+
* @returns {Promise<void>}
|
|
164
|
+
* @private
|
|
165
|
+
*/
|
|
166
|
+
_ensureMigrated() {
|
|
167
|
+
if (this._migrationPromise) {
|
|
168
|
+
return this._migrationPromise;
|
|
169
|
+
}
|
|
170
|
+
this._migrationPromise = new Promise((resolve, reject) => {
|
|
104
171
|
if (this._writeDb) {
|
|
105
|
-
|
|
172
|
+
this._migrateIfNeeded(this._writeDb).then(resolve).catch(reject);
|
|
106
173
|
return;
|
|
107
174
|
}
|
|
108
|
-
|
|
109
175
|
this._writeDb = new sqlite3.Database(this.dbPath, (err) => {
|
|
110
176
|
if (err) {
|
|
111
177
|
this._writeDb = null;
|
|
112
178
|
reject(new Error(`Failed to open database for writing: ${err.message}`));
|
|
113
|
-
|
|
114
|
-
this._migrateIfNeeded(this._writeDb).then(() => resolve(this._writeDb)).catch(reject);
|
|
179
|
+
return;
|
|
115
180
|
}
|
|
181
|
+
this._migrateIfNeeded(this._writeDb).then(resolve).catch(reject);
|
|
116
182
|
});
|
|
117
183
|
});
|
|
184
|
+
// If migration fails, clear the cached promise so a retry can attempt again
|
|
185
|
+
this._migrationPromise.catch(() => { this._migrationPromise = null; });
|
|
186
|
+
return this._migrationPromise;
|
|
118
187
|
}
|
|
119
188
|
|
|
120
189
|
/**
|
|
@@ -122,29 +191,20 @@ class ValueSetDatabase {
|
|
|
122
191
|
* @returns {Promise<void>}
|
|
123
192
|
*/
|
|
124
193
|
async close() {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (this._db) {
|
|
128
|
-
closePromises.push(new Promise((resolve) => {
|
|
129
|
-
this._db.close((err) => {
|
|
130
|
-
if (err) console.warn(`Warning closing read connection: ${err.message}`);
|
|
131
|
-
this._db = null;
|
|
132
|
-
resolve();
|
|
133
|
-
});
|
|
134
|
-
}));
|
|
135
|
-
}
|
|
194
|
+
// Clear the cached migration promise so a subsequent open re-migrates
|
|
195
|
+
this._migrationPromise = null;
|
|
136
196
|
|
|
137
|
-
if (this._writeDb) {
|
|
138
|
-
|
|
139
|
-
this._writeDb.close((err) => {
|
|
140
|
-
if (err) console.warn(`Warning closing write connection: ${err.message}`);
|
|
141
|
-
this._writeDb = null;
|
|
142
|
-
resolve();
|
|
143
|
-
});
|
|
144
|
-
}));
|
|
197
|
+
if (!this._writeDb) {
|
|
198
|
+
return;
|
|
145
199
|
}
|
|
146
200
|
|
|
147
|
-
await Promise
|
|
201
|
+
await new Promise((resolve) => {
|
|
202
|
+
this._writeDb.close((err) => {
|
|
203
|
+
if (err) console.warn(`Warning closing database connection: ${err.message}`);
|
|
204
|
+
this._writeDb = null;
|
|
205
|
+
resolve();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
148
208
|
}
|
|
149
209
|
|
|
150
210
|
/**
|
|
@@ -193,6 +253,7 @@ class ValueSetDatabase {
|
|
|
193
253
|
status TEXT,
|
|
194
254
|
title TEXT,
|
|
195
255
|
content TEXT NOT NULL,
|
|
256
|
+
content_hash TEXT,
|
|
196
257
|
last_seen INTEGER DEFAULT (strftime('%s', 'now')),
|
|
197
258
|
date_first_seen INTEGER DEFAULT (strftime('%s', 'now'))
|
|
198
259
|
)
|
|
@@ -241,17 +302,31 @@ class ValueSetDatabase {
|
|
|
241
302
|
status TEXT NOT NULL DEFAULT 'running',
|
|
242
303
|
error_message TEXT,
|
|
243
304
|
total_fetched INTEGER,
|
|
244
|
-
total_new INTEGER
|
|
305
|
+
total_new INTEGER,
|
|
306
|
+
total_updated INTEGER
|
|
245
307
|
)
|
|
246
308
|
`);
|
|
247
309
|
|
|
248
310
|
// Settings table (key-value store for _lastUpdated tracking etc.)
|
|
249
311
|
db.run(`
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
312
|
+
CREATE TABLE IF NOT EXISTS vsac_settings (
|
|
313
|
+
key TEXT PRIMARY KEY,
|
|
314
|
+
value TEXT
|
|
315
|
+
)
|
|
316
|
+
`);
|
|
317
|
+
|
|
318
|
+
// Event log table (new/updated/deleted value sets)
|
|
319
|
+
db.run(`
|
|
320
|
+
CREATE TABLE IF NOT EXISTS vsac_events (
|
|
321
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
322
|
+
timestamp INTEGER NOT NULL,
|
|
323
|
+
event_type TEXT NOT NULL,
|
|
324
|
+
url TEXT NOT NULL,
|
|
325
|
+
version TEXT,
|
|
326
|
+
detail TEXT
|
|
327
|
+
)
|
|
254
328
|
`);
|
|
329
|
+
db.run('CREATE INDEX idx_events_timestamp ON vsac_events(timestamp)');
|
|
255
330
|
|
|
256
331
|
// Create indexes for better search performance
|
|
257
332
|
db.run('CREATE INDEX idx_valuesets_url ON valuesets(url, version)');
|
|
@@ -300,15 +375,36 @@ class ValueSetDatabase {
|
|
|
300
375
|
* @param {number} id - The run ID from startRun()
|
|
301
376
|
* @param {number} totalFetched - Total value sets fetched
|
|
302
377
|
* @param {number} totalNew - Number of new value sets found
|
|
378
|
+
* @param {number} [totalUpdated=0] - Number of existing value sets whose content changed
|
|
303
379
|
* @returns {Promise<void>}
|
|
304
380
|
*/
|
|
305
|
-
async finishRun(id, totalFetched, totalNew) {
|
|
381
|
+
async finishRun(id, totalFetched, totalNew, totalUpdated = 0) {
|
|
306
382
|
const db = await this._getWriteConnection();
|
|
307
383
|
return new Promise((resolve, reject) => {
|
|
308
384
|
db.run(
|
|
309
385
|
`UPDATE vsac_runs SET finished_at = strftime('%s','now'), status = 'ok',
|
|
310
|
-
total_fetched = ?, total_new = ? WHERE id = ?`,
|
|
311
|
-
[totalFetched, totalNew, id],
|
|
386
|
+
total_fetched = ?, total_new = ?, total_updated = ? WHERE id = ?`,
|
|
387
|
+
[totalFetched, totalNew, totalUpdated, id],
|
|
388
|
+
err => err ? reject(err) : resolve()
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Record a VSAC event in the audit log
|
|
395
|
+
* @param {string} eventType - 'new', 'updated', or 'deleted'
|
|
396
|
+
* @param {string} url - The value set URL
|
|
397
|
+
* @param {string|null} version - The version, or null
|
|
398
|
+
* @param {string|null} [detail] - Optional detail string
|
|
399
|
+
* @returns {Promise<void>}
|
|
400
|
+
*/
|
|
401
|
+
async recordEvent(eventType, url, version, detail = null) {
|
|
402
|
+
const db = await this._getWriteConnection();
|
|
403
|
+
return new Promise((resolve, reject) => {
|
|
404
|
+
db.run(
|
|
405
|
+
`INSERT INTO vsac_events (timestamp, event_type, url, version, detail)
|
|
406
|
+
VALUES (strftime('%s','now'), ?, ?, ?, ?)`,
|
|
407
|
+
[eventType, url, version || null, detail],
|
|
312
408
|
err => err ? reject(err) : resolve()
|
|
313
409
|
);
|
|
314
410
|
});
|
|
@@ -358,7 +454,7 @@ class ValueSetDatabase {
|
|
|
358
454
|
return new Promise((resolve, reject) => {
|
|
359
455
|
db.run(
|
|
360
456
|
`INSERT INTO vsac_settings (key, value) VALUES (?, ?)
|
|
361
|
-
|
|
457
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
|
362
458
|
[key, value],
|
|
363
459
|
err => err ? reject(err) : resolve()
|
|
364
460
|
);
|
|
@@ -368,9 +464,10 @@ class ValueSetDatabase {
|
|
|
368
464
|
/**
|
|
369
465
|
* Insert or update a single ValueSet in the database
|
|
370
466
|
* @param {Object} valueSet - The ValueSet resource
|
|
467
|
+
* @param {string} [contentHash] - Optional pre-computed content hash to store
|
|
371
468
|
* @returns {Promise<void>}
|
|
372
469
|
*/
|
|
373
|
-
async upsertValueSet(valueSet) {
|
|
470
|
+
async upsertValueSet(valueSet, contentHash = null) {
|
|
374
471
|
if (!valueSet.url) {
|
|
375
472
|
throw new Error('ValueSet must have a url property');
|
|
376
473
|
}
|
|
@@ -405,8 +502,9 @@ class ValueSetDatabase {
|
|
|
405
502
|
db.run(`
|
|
406
503
|
INSERT INTO valuesets (
|
|
407
504
|
id, url, version, date, description, effectivePeriod_start, effectivePeriod_end,
|
|
408
|
-
expansion_identifier, name, publisher, status, title, content,
|
|
409
|
-
|
|
505
|
+
expansion_identifier, name, publisher, status, title, content, content_hash,
|
|
506
|
+
last_seen, date_first_seen
|
|
507
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))
|
|
410
508
|
ON CONFLICT(id) DO UPDATE SET
|
|
411
509
|
url=excluded.url,
|
|
412
510
|
version=excluded.version,
|
|
@@ -420,6 +518,7 @@ class ValueSetDatabase {
|
|
|
420
518
|
status=excluded.status,
|
|
421
519
|
title=excluded.title,
|
|
422
520
|
content=excluded.content,
|
|
521
|
+
content_hash=excluded.content_hash,
|
|
423
522
|
last_seen=strftime('%s', 'now')
|
|
424
523
|
`, [
|
|
425
524
|
valueSet.id,
|
|
@@ -434,7 +533,8 @@ class ValueSetDatabase {
|
|
|
434
533
|
valueSet.publisher || null,
|
|
435
534
|
valueSet.status || null,
|
|
436
535
|
valueSet.title || null,
|
|
437
|
-
JSON.stringify(valueSet)
|
|
536
|
+
JSON.stringify(valueSet),
|
|
537
|
+
contentHash
|
|
438
538
|
], (err) => {
|
|
439
539
|
if (err) {
|
|
440
540
|
reject(new Error(`Failed to insert main record: ${err.message}`));
|
|
@@ -450,6 +550,24 @@ class ValueSetDatabase {
|
|
|
450
550
|
});
|
|
451
551
|
}
|
|
452
552
|
|
|
553
|
+
/**
|
|
554
|
+
* Backfill the content_hash column for a row without rewriting content or
|
|
555
|
+
* emitting an event. Used for legacy rows from before content_hash existed.
|
|
556
|
+
* @param {string} id - The ValueSet id
|
|
557
|
+
* @param {string} hash - The SHA-256 hex hash to store
|
|
558
|
+
* @returns {Promise<void>}
|
|
559
|
+
*/
|
|
560
|
+
async setContentHash(id, hash) {
|
|
561
|
+
const db = await this._getWriteConnection();
|
|
562
|
+
return new Promise((resolve, reject) => {
|
|
563
|
+
db.run(
|
|
564
|
+
'UPDATE valuesets SET content_hash = ? WHERE id = ?',
|
|
565
|
+
[hash, id],
|
|
566
|
+
err => err ? reject(err) : resolve()
|
|
567
|
+
);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
453
571
|
/**
|
|
454
572
|
* Just update the timestamp on the valueset
|
|
455
573
|
* @param {Object} valueSet - The ValueSet resource
|
|
@@ -590,7 +708,7 @@ class ValueSetDatabase {
|
|
|
590
708
|
const db = await this._getReadConnection();
|
|
591
709
|
|
|
592
710
|
return new Promise((resolve, reject) => {
|
|
593
|
-
db.all('SELECT id, url, version, content FROM valuesets', [], (err, rows) => {
|
|
711
|
+
db.all('SELECT id, url, version, content, content_hash FROM valuesets', [], (err, rows) => {
|
|
594
712
|
if (err) {
|
|
595
713
|
reject(new Error(`Failed to load value sets: ${err.message}`));
|
|
596
714
|
return;
|
|
@@ -603,6 +721,9 @@ class ValueSetDatabase {
|
|
|
603
721
|
for (const row of rows) {
|
|
604
722
|
const valueSet = new ValueSet(JSON.parse(row.content));
|
|
605
723
|
valueSet.sourcePackage = source;
|
|
724
|
+
// Attach the stored content hash so callers can detect changes
|
|
725
|
+
// without recomputing over the full JSON.
|
|
726
|
+
valueSet.contentHash = row.content_hash || null;
|
|
606
727
|
// Store by URL and id alone
|
|
607
728
|
this.addToMap(valueSetMap, row.id, row.url, row.version, valueSet);
|
|
608
729
|
}
|