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/xig/xig.js
CHANGED
|
@@ -16,6 +16,7 @@ const zlib = require('zlib');
|
|
|
16
16
|
const htmlServer = require('../library/html-server');
|
|
17
17
|
const folders = require('../library/folder-setup');
|
|
18
18
|
const escape = require('escape-html');
|
|
19
|
+
const { guarded } = require('../library/sqlite-utilities');
|
|
19
20
|
|
|
20
21
|
const Logger = require('../library/logger');
|
|
21
22
|
const {describeCron} = require("../library/cron-utilities");
|
|
@@ -55,6 +56,9 @@ let cacheLoadInProgress = false;
|
|
|
55
56
|
|
|
56
57
|
// Update history - tracks every download attempt for diagnostics
|
|
57
58
|
const MAX_UPDATE_HISTORY = 20;
|
|
59
|
+
// The daily download runs at 02:00 local time
|
|
60
|
+
const UPDATE_CRON = '0 2 * * *';
|
|
61
|
+
const UPDATE_HOUR = 2;
|
|
58
62
|
let updateHistory = [];
|
|
59
63
|
let updateInProgress = false;
|
|
60
64
|
|
|
@@ -99,124 +103,112 @@ function validateExternalUrl(url) {
|
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
const { realm, auth, ver, type, rt, text, pkg, onlyUsed } = queryParams;
|
|
106
|
+
// The columns that say which FHIR versions a resource is for (also the values of ver)
|
|
107
|
+
const VERSION_COLUMNS = ['R2', 'R2B', 'R3', 'R4', 'R4B', 'R5', 'R6'];
|
|
105
108
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
/**
|
|
110
|
+
* One request parameter as a string. Express makes a repeated parameter (?realm=a&realm=b) an
|
|
111
|
+
* array, and can make a bracketed one (?realm[x]=a) an object: the first value is used, and
|
|
112
|
+
* anything else is treated as not given.
|
|
113
|
+
*/
|
|
114
|
+
function queryValue(v) {
|
|
115
|
+
if (Array.isArray(v)) {
|
|
116
|
+
v = v[0];
|
|
117
|
+
}
|
|
118
|
+
return typeof v === 'string' ? v : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The WHERE clause for the resource list filters (realm, auth, ver, type, rt, text, pkg,
|
|
123
|
+
* onlyUsed), with a ? for every value that comes from the request, and those values in order.
|
|
124
|
+
* Nothing from the request is put in the SQL itself: only fixed SQL chosen by it (ver, type).
|
|
125
|
+
* The list, its count and the summary statistics all use this, so they always agree.
|
|
126
|
+
*
|
|
127
|
+
* @param {Object} queryParams
|
|
128
|
+
* @returns {{where: string, params: Array}} where starts with WHERE (WHERE 1=1 when nothing is filtered)
|
|
129
|
+
*/
|
|
130
|
+
function buildResourceFilter(queryParams) {
|
|
131
|
+
const realm = queryValue(queryParams.realm);
|
|
132
|
+
const auth = queryValue(queryParams.auth);
|
|
133
|
+
const ver = queryValue(queryParams.ver);
|
|
134
|
+
const type = queryValue(queryParams.type);
|
|
135
|
+
const rt = queryValue(queryParams.rt);
|
|
136
|
+
const text = queryValue(queryParams.text);
|
|
137
|
+
const pkg = queryValue(queryParams.pkg);
|
|
138
|
+
const onlyUsed = queryValue(queryParams.onlyUsed);
|
|
116
139
|
|
|
117
140
|
const conditions = [];
|
|
118
141
|
const params = [];
|
|
119
142
|
|
|
120
|
-
|
|
121
|
-
if (realm && realm !== '') {
|
|
143
|
+
if (realm) {
|
|
122
144
|
conditions.push('AND realm = ?');
|
|
123
145
|
params.push(realm);
|
|
124
146
|
}
|
|
125
147
|
|
|
126
|
-
|
|
127
|
-
if (auth && auth !== '') {
|
|
148
|
+
if (auth) {
|
|
128
149
|
conditions.push('AND authority = ?');
|
|
129
150
|
params.push(auth);
|
|
130
151
|
}
|
|
131
152
|
|
|
132
|
-
//
|
|
133
|
-
if (ver) {
|
|
134
|
-
|
|
135
|
-
case 'R2':
|
|
136
|
-
conditions.push('AND R2 = 1');
|
|
137
|
-
break;
|
|
138
|
-
case 'R2B':
|
|
139
|
-
conditions.push('AND R2B = 1');
|
|
140
|
-
break;
|
|
141
|
-
case 'R3':
|
|
142
|
-
conditions.push('AND R3 = 1');
|
|
143
|
-
break;
|
|
144
|
-
case 'R4':
|
|
145
|
-
conditions.push('AND R4 = 1');
|
|
146
|
-
break;
|
|
147
|
-
case 'R4B':
|
|
148
|
-
conditions.push('AND R4B = 1');
|
|
149
|
-
break;
|
|
150
|
-
case 'R5':
|
|
151
|
-
conditions.push('AND R5 = 1');
|
|
152
|
-
break;
|
|
153
|
-
case 'R6':
|
|
154
|
-
conditions.push('AND R6 = 1');
|
|
155
|
-
break;
|
|
156
|
-
}
|
|
153
|
+
// a column name, so only one of the known ones
|
|
154
|
+
if (VERSION_COLUMNS.includes(ver)) {
|
|
155
|
+
conditions.push(`AND ${ver} = 1`);
|
|
157
156
|
}
|
|
158
157
|
|
|
159
|
-
// Type-specific filters
|
|
160
158
|
switch (type) {
|
|
161
159
|
case 'cs': // CodeSystem
|
|
162
160
|
conditions.push("AND ResourceType = 'CodeSystem'");
|
|
163
161
|
break;
|
|
164
|
-
|
|
165
162
|
case 'rp': // Resource Profiles
|
|
166
163
|
conditions.push("AND ResourceType = 'StructureDefinition' AND kind = 'resource'");
|
|
167
|
-
if (rt &&
|
|
164
|
+
if (rt && hasCachedValue('profileResources', rt)) {
|
|
168
165
|
conditions.push('AND Type = ?');
|
|
169
166
|
params.push(rt);
|
|
170
167
|
}
|
|
171
168
|
break;
|
|
172
|
-
|
|
173
169
|
case 'dp': // Datatype Profiles
|
|
174
170
|
conditions.push("AND ResourceType = 'StructureDefinition' AND (kind = 'complex-type' OR kind = 'primitive-type')");
|
|
175
|
-
if (rt &&
|
|
171
|
+
if (rt && hasCachedValue('profileTypes', rt)) {
|
|
176
172
|
conditions.push('AND Type = ?');
|
|
177
173
|
params.push(rt);
|
|
178
174
|
}
|
|
179
175
|
break;
|
|
180
|
-
|
|
181
176
|
case 'lm': // Logical Models
|
|
182
177
|
conditions.push("AND ResourceType = 'StructureDefinition' AND kind = 'logical'");
|
|
183
178
|
break;
|
|
184
|
-
|
|
185
179
|
case 'ext': // Extensions
|
|
186
180
|
conditions.push("AND ResourceType = 'StructureDefinition' AND Type = 'Extension'");
|
|
187
|
-
if (rt &&
|
|
181
|
+
if (rt && hasCachedValue('extensionContexts', rt)) {
|
|
188
182
|
conditions.push('AND ResourceKey IN (SELECT ResourceKey FROM Categories WHERE Mode = 2 AND Code = ?)');
|
|
189
183
|
params.push(rt);
|
|
190
184
|
}
|
|
191
185
|
break;
|
|
192
|
-
|
|
193
186
|
case 'vs': // ValueSets
|
|
194
187
|
conditions.push("AND ResourceType = 'ValueSet'");
|
|
195
|
-
if (rt &&
|
|
188
|
+
if (rt && hasTerminologySource(rt)) {
|
|
196
189
|
conditions.push('AND ResourceKey IN (SELECT ResourceKey FROM Categories WHERE Mode = 1 AND Code = ?)');
|
|
197
190
|
params.push(rt);
|
|
198
191
|
}
|
|
199
192
|
break;
|
|
200
|
-
|
|
201
193
|
case 'cm': // ConceptMaps
|
|
202
194
|
conditions.push("AND ResourceType = 'ConceptMap'");
|
|
203
|
-
if (rt &&
|
|
195
|
+
if (rt && hasTerminologySource(rt)) {
|
|
204
196
|
conditions.push('AND ResourceKey IN (SELECT ResourceKey FROM Categories WHERE Mode = 1 AND Code = ?)');
|
|
205
197
|
params.push(rt);
|
|
206
198
|
}
|
|
207
199
|
break;
|
|
208
|
-
|
|
209
200
|
default:
|
|
210
201
|
// No specific type selected
|
|
211
|
-
if (rt &&
|
|
202
|
+
if (rt && hasCachedValue('resourceTypes', rt)) {
|
|
212
203
|
conditions.push('AND ResourceType = ?');
|
|
213
204
|
params.push(rt);
|
|
214
205
|
}
|
|
215
206
|
break;
|
|
216
207
|
}
|
|
217
208
|
|
|
218
|
-
// Text search
|
|
219
|
-
|
|
209
|
+
// Text search: one FTS phrase, so the text is searched for as written (a " in it is doubled,
|
|
210
|
+
// which is how a phrase holds one, so it can't end the phrase and add query syntax)
|
|
211
|
+
if (text) {
|
|
220
212
|
const ftsText = text.replace(/"/g, '""');
|
|
221
213
|
if (type === 'cs') {
|
|
222
214
|
conditions.push(`AND (ResourceKey IN (SELECT ResourceKey FROM ResourceFTS WHERE ResourceFTS MATCH ?)
|
|
@@ -228,129 +220,37 @@ function buildSecureResourceQuery(queryParams, offset = 0, limit = 50) {
|
|
|
228
220
|
}
|
|
229
221
|
}
|
|
230
222
|
|
|
231
|
-
// Package filter -
|
|
232
|
-
if (pkg
|
|
233
|
-
conditions.push(
|
|
234
|
-
params.push(`%${pkg}%`);
|
|
223
|
+
// Package filter - packageId#version containing the text, as written (% and _ are not wildcards)
|
|
224
|
+
if (pkg) {
|
|
225
|
+
conditions.push("AND PackageKey IN (SELECT PackageKey FROM Packages WHERE (Id || '#' || Version) LIKE ? ESCAPE '\\')");
|
|
226
|
+
params.push(`%${pkg.replace(/[\\%_]/g, c => '\\' + c)}%`);
|
|
235
227
|
}
|
|
236
228
|
|
|
237
|
-
// Only used filter
|
|
238
229
|
if (onlyUsed === 'true') {
|
|
239
230
|
conditions.push('AND EXISTS (SELECT 1 FROM DependencyList WHERE TargetKey = Resources.ResourceKey)');
|
|
240
231
|
}
|
|
241
232
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
params.push(limit, offset);
|
|
233
|
+
return { where: ['WHERE 1=1', ...conditions].join(' '), params };
|
|
234
|
+
}
|
|
245
235
|
|
|
246
|
-
|
|
236
|
+
function buildSecureResourceQuery(queryParams, offset = 0, limit = 50) {
|
|
237
|
+
const { where, params } = buildResourceFilter(queryParams);
|
|
238
|
+
const query = `
|
|
239
|
+
SELECT
|
|
240
|
+
ResourceKey, ResourceType, Type, Kind, Description, PackageKey,
|
|
241
|
+
Realm, Authority, R2, R2B, R3, R4, R4B, R5, R6,
|
|
242
|
+
Id, Url, Version, Status, Date, Name, Title, Content,
|
|
243
|
+
Supplements, Details, FMM, WG, StandardsStatus, Web,
|
|
244
|
+
(SELECT COUNT(*) FROM DependencyList WHERE TargetKey = Resources.ResourceKey) AS UsageCount
|
|
245
|
+
FROM Resources
|
|
246
|
+
${where}
|
|
247
|
+
ORDER BY ResourceType, Type, Description LIMIT ? OFFSET ?`;
|
|
248
|
+
return { query, params: [...params, limit, offset] };
|
|
247
249
|
}
|
|
248
250
|
|
|
249
251
|
function buildSecureResourceCountQuery(queryParams) {
|
|
250
|
-
const {
|
|
251
|
-
|
|
252
|
-
let baseQuery = 'SELECT COUNT(*) as total FROM Resources WHERE 1=1';
|
|
253
|
-
const conditions = [];
|
|
254
|
-
const params = [];
|
|
255
|
-
|
|
256
|
-
// Same conditions as main query but for counting
|
|
257
|
-
if (realm && realm !== '') {
|
|
258
|
-
conditions.push('AND realm = ?');
|
|
259
|
-
params.push(realm);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (auth && auth !== '') {
|
|
263
|
-
conditions.push('AND authority = ?');
|
|
264
|
-
params.push(auth);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (ver) {
|
|
268
|
-
switch (ver) {
|
|
269
|
-
case 'R2': conditions.push('AND R2 = 1'); break;
|
|
270
|
-
case 'R2B': conditions.push('AND R2B = 1'); break;
|
|
271
|
-
case 'R3': conditions.push('AND R3 = 1'); break;
|
|
272
|
-
case 'R4': conditions.push('AND R4 = 1'); break;
|
|
273
|
-
case 'R4B': conditions.push('AND R4B = 1'); break;
|
|
274
|
-
case 'R5': conditions.push('AND R5 = 1'); break;
|
|
275
|
-
case 'R6': conditions.push('AND R6 = 1'); break;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
switch (type) {
|
|
280
|
-
case 'cs':
|
|
281
|
-
conditions.push("AND ResourceType = 'CodeSystem'");
|
|
282
|
-
break;
|
|
283
|
-
case 'rp':
|
|
284
|
-
conditions.push("AND ResourceType = 'StructureDefinition' AND kind = 'resource'");
|
|
285
|
-
if (rt && rt !== '' && hasCachedValue('profileResources', rt)) {
|
|
286
|
-
conditions.push('AND Type = ?');
|
|
287
|
-
params.push(rt);
|
|
288
|
-
}
|
|
289
|
-
break;
|
|
290
|
-
case 'dp':
|
|
291
|
-
conditions.push("AND ResourceType = 'StructureDefinition' AND (kind = 'complex-type' OR kind = 'primitive-type')");
|
|
292
|
-
if (rt && rt !== '' && hasCachedValue('profileTypes', rt)) {
|
|
293
|
-
conditions.push('AND Type = ?');
|
|
294
|
-
params.push(rt);
|
|
295
|
-
}
|
|
296
|
-
break;
|
|
297
|
-
case 'lm':
|
|
298
|
-
conditions.push("AND ResourceType = 'StructureDefinition' AND kind = 'logical'");
|
|
299
|
-
break;
|
|
300
|
-
case 'ext':
|
|
301
|
-
conditions.push("AND ResourceType = 'StructureDefinition' AND Type = 'Extension'");
|
|
302
|
-
if (rt && rt !== '' && hasCachedValue('extensionContexts', rt)) {
|
|
303
|
-
conditions.push('AND ResourceKey IN (SELECT ResourceKey FROM Categories WHERE Mode = 2 AND Code = ?)');
|
|
304
|
-
params.push(rt);
|
|
305
|
-
}
|
|
306
|
-
break;
|
|
307
|
-
case 'vs':
|
|
308
|
-
conditions.push("AND ResourceType = 'ValueSet'");
|
|
309
|
-
if (rt && rt !== '' && hasTerminologySource(rt)) {
|
|
310
|
-
conditions.push('AND ResourceKey IN (SELECT ResourceKey FROM Categories WHERE Mode = 1 AND Code = ?)');
|
|
311
|
-
params.push(rt);
|
|
312
|
-
}
|
|
313
|
-
break;
|
|
314
|
-
case 'cm':
|
|
315
|
-
conditions.push("AND ResourceType = 'ConceptMap'");
|
|
316
|
-
if (rt && rt !== '' && hasTerminologySource(rt)) {
|
|
317
|
-
conditions.push('AND ResourceKey IN (SELECT ResourceKey FROM Categories WHERE Mode = 1 AND Code = ?)');
|
|
318
|
-
params.push(rt);
|
|
319
|
-
}
|
|
320
|
-
break;
|
|
321
|
-
default:
|
|
322
|
-
if (rt && rt !== '' && hasCachedValue('resourceTypes', rt)) {
|
|
323
|
-
conditions.push('AND ResourceType = ?');
|
|
324
|
-
params.push(rt);
|
|
325
|
-
}
|
|
326
|
-
break;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (text && text !== '') {
|
|
330
|
-
const ftsText = text.replace(/"/g, '""');
|
|
331
|
-
if (type === 'cs') {
|
|
332
|
-
conditions.push(`AND (ResourceKey IN (SELECT ResourceKey FROM ResourceFTS WHERE ResourceFTS MATCH ?)
|
|
333
|
-
OR ResourceKey IN (SELECT ResourceKey FROM CodeSystemFTS WHERE CodeSystemFTS MATCH ?))`);
|
|
334
|
-
params.push(`{Description Narrative} : "${ftsText}"`, `{Display Definition} : "${ftsText}"`);
|
|
335
|
-
} else {
|
|
336
|
-
conditions.push('AND ResourceKey IN (SELECT ResourceKey FROM ResourceFTS WHERE ResourceFTS MATCH ?)');
|
|
337
|
-
params.push(`{Description Narrative} : "${ftsText}"`);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Package filter - matches packageId#version containing the search string
|
|
342
|
-
if (pkg && pkg !== '') {
|
|
343
|
-
conditions.push('AND PackageKey IN (SELECT PackageKey FROM Packages WHERE (Id || \'#\' || Version) LIKE ?)');
|
|
344
|
-
params.push(`%${pkg}%`);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Only used filter
|
|
348
|
-
if (onlyUsed === 'true') {
|
|
349
|
-
conditions.push('AND EXISTS (SELECT 1 FROM DependencyList WHERE TargetKey = Resources.ResourceKey)');
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const fullQuery = baseQuery + ' ' + conditions.join(' ');
|
|
353
|
-
return { query: fullQuery, params };
|
|
252
|
+
const { where, params } = buildResourceFilter(queryParams);
|
|
253
|
+
return { query: `SELECT COUNT(*) as total FROM Resources ${where}`, params };
|
|
354
254
|
}
|
|
355
255
|
|
|
356
256
|
// Template Functions
|
|
@@ -463,141 +363,6 @@ function buildContentHtml(contentData) {
|
|
|
463
363
|
return html;
|
|
464
364
|
}
|
|
465
365
|
|
|
466
|
-
// SQL Filter Building Functions
|
|
467
|
-
|
|
468
|
-
function sqlEscapeString(str) {
|
|
469
|
-
if (!str) return '';
|
|
470
|
-
// Escape single quotes for SQL
|
|
471
|
-
return str.replace(/'/g, "''");
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function buildSqlFilter(queryParams) {
|
|
475
|
-
const { realm, auth, ver, type, rt, text, pkg, onlyUsed } = queryParams;
|
|
476
|
-
let filter = '';
|
|
477
|
-
|
|
478
|
-
// Realm filter
|
|
479
|
-
if (realm && realm !== '') {
|
|
480
|
-
filter += ` and realm = '${sqlEscapeString(realm)}'`;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Authority filter
|
|
484
|
-
if (auth && auth !== '') {
|
|
485
|
-
filter += ` and authority = '${sqlEscapeString(auth)}'`;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Version filter - check specific version columns
|
|
489
|
-
if (ver) {
|
|
490
|
-
switch (ver) {
|
|
491
|
-
case 'R2':
|
|
492
|
-
filter += ' and R2 = 1';
|
|
493
|
-
break;
|
|
494
|
-
case 'R2B':
|
|
495
|
-
filter += ' and R2B = 1';
|
|
496
|
-
break;
|
|
497
|
-
case 'R3':
|
|
498
|
-
filter += ' and R3 = 1';
|
|
499
|
-
break;
|
|
500
|
-
case 'R4':
|
|
501
|
-
filter += ' and R4 = 1';
|
|
502
|
-
break;
|
|
503
|
-
case 'R4B':
|
|
504
|
-
filter += ' and R4B = 1';
|
|
505
|
-
break;
|
|
506
|
-
case 'R5':
|
|
507
|
-
filter += ' and R5 = 1';
|
|
508
|
-
break;
|
|
509
|
-
case 'R6':
|
|
510
|
-
filter += ' and R6 = 1';
|
|
511
|
-
break;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// Type-specific filters
|
|
516
|
-
switch (type) {
|
|
517
|
-
case 'cs': // CodeSystem
|
|
518
|
-
filter += " and ResourceType = 'CodeSystem'";
|
|
519
|
-
break;
|
|
520
|
-
|
|
521
|
-
case 'rp': // Resource Profiles
|
|
522
|
-
filter += " and ResourceType = 'StructureDefinition' and kind = 'resource'";
|
|
523
|
-
if (rt && rt !== '' && hasCachedValue('profileResources', rt)) {
|
|
524
|
-
filter += ` and Type = '${sqlEscapeString(rt)}'`;
|
|
525
|
-
}
|
|
526
|
-
break;
|
|
527
|
-
|
|
528
|
-
case 'dp': // Datatype Profiles
|
|
529
|
-
filter += " and ResourceType = 'StructureDefinition' and (kind = 'complex-type' or kind = 'primitive-type')";
|
|
530
|
-
if (rt && rt !== '' && hasCachedValue('profileTypes', rt)) {
|
|
531
|
-
filter += ` and Type = '${sqlEscapeString(rt)}'`;
|
|
532
|
-
}
|
|
533
|
-
break;
|
|
534
|
-
|
|
535
|
-
case 'lm': // Logical Models
|
|
536
|
-
filter += " and ResourceType = 'StructureDefinition' and kind = 'logical'";
|
|
537
|
-
break;
|
|
538
|
-
|
|
539
|
-
case 'ext': // Extensions
|
|
540
|
-
filter += " and ResourceType = 'StructureDefinition' and (Type = 'Extension')";
|
|
541
|
-
if (rt && rt !== '' && hasCachedValue('extensionContexts', rt)) {
|
|
542
|
-
filter += ` and ResourceKey in (Select ResourceKey from Categories where Mode = 2 and Code = '${sqlEscapeString(rt)}')`;
|
|
543
|
-
}
|
|
544
|
-
break;
|
|
545
|
-
|
|
546
|
-
case 'vs': // ValueSets
|
|
547
|
-
filter += " and ResourceType = 'ValueSet'";
|
|
548
|
-
if (rt && rt !== '' && hasTerminologySource(rt)) {
|
|
549
|
-
filter += ` and ResourceKey in (Select ResourceKey from Categories where Mode = 1 and Code = '${sqlEscapeString(rt)}')`;
|
|
550
|
-
}
|
|
551
|
-
break;
|
|
552
|
-
|
|
553
|
-
case 'cm': // ConceptMaps
|
|
554
|
-
filter += " and ResourceType = 'ConceptMap'";
|
|
555
|
-
if (rt && rt !== '' && hasTerminologySource(rt)) {
|
|
556
|
-
filter += ` and ResourceKey in (Select ResourceKey from Categories where Mode = 1 and Code = '${sqlEscapeString(rt)}')`;
|
|
557
|
-
}
|
|
558
|
-
break;
|
|
559
|
-
|
|
560
|
-
default:
|
|
561
|
-
// No specific type selected - handle rt parameter for general resource filtering
|
|
562
|
-
if (rt && rt !== '' && hasCachedValue('resourceTypes', rt)) {
|
|
563
|
-
filter += ` and ResourceType = '${sqlEscapeString(rt)}'`;
|
|
564
|
-
}
|
|
565
|
-
break;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Text search filter
|
|
569
|
-
if (text && text !== '') {
|
|
570
|
-
const escapedText = sqlEscapeString(text);
|
|
571
|
-
if (type === 'cs') {
|
|
572
|
-
// Special handling for CodeSystems - search both resource and CodeSystem-specific fields
|
|
573
|
-
filter += ` and (ResourceKey in (select ResourceKey from ResourceFTS where Description match '"${escapedText}"' or Narrative match '"${escapedText}"') ` +
|
|
574
|
-
`or ResourceKey in (select ResourceKey from CodeSystemFTS where Display match '"${escapedText}"' or Definition match '"${escapedText}"'))`;
|
|
575
|
-
} else {
|
|
576
|
-
// Standard resource text search
|
|
577
|
-
filter += ` and ResourceKey in (select ResourceKey from ResourceFTS where Description match '"${escapedText}"' or Narrative match '"${escapedText}"')`;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// Package filter - matches packageId#version containing the search string
|
|
582
|
-
if (pkg && pkg !== '') {
|
|
583
|
-
const escapedPkg = sqlEscapeString(pkg);
|
|
584
|
-
filter += ` and PackageKey in (select PackageKey from Packages where (Id || '#' || Version) like '%${escapedPkg}%')`;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Only used filter
|
|
588
|
-
if (onlyUsed === 'true') {
|
|
589
|
-
filter += ` and exists (select 1 from DependencyList where TargetKey = Resources.ResourceKey)`;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Convert to proper WHERE clause
|
|
593
|
-
if (filter !== '') {
|
|
594
|
-
// Remove the first " and " and prepend "WHERE "
|
|
595
|
-
filter = 'WHERE ' + filter.substring(4);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
return filter;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
366
|
// Helper function to check if a terminology source exists
|
|
602
367
|
// This is a placeholder - you might need to implement this based on your data
|
|
603
368
|
function hasTerminologySource(sourceCode) {
|
|
@@ -606,45 +371,6 @@ function hasTerminologySource(sourceCode) {
|
|
|
606
371
|
return hasCachedValue('txSources', sourceCode);
|
|
607
372
|
}
|
|
608
373
|
|
|
609
|
-
function buildResourceListQuery(queryParams, offset = 0, limit = 50) {
|
|
610
|
-
const whereClause = buildSqlFilter(queryParams);
|
|
611
|
-
|
|
612
|
-
// Build the complete SQL query
|
|
613
|
-
let sql = `
|
|
614
|
-
SELECT
|
|
615
|
-
ResourceKey,
|
|
616
|
-
ResourceType,
|
|
617
|
-
Type,
|
|
618
|
-
Kind,
|
|
619
|
-
Description,
|
|
620
|
-
PackageKey,
|
|
621
|
-
Realm,
|
|
622
|
-
Authority,
|
|
623
|
-
R2, R2B, R3, R4, R4B, R5, R6,
|
|
624
|
-
Id,
|
|
625
|
-
Url,
|
|
626
|
-
Version,
|
|
627
|
-
Status,
|
|
628
|
-
Date,
|
|
629
|
-
Name,
|
|
630
|
-
Title,
|
|
631
|
-
Content,
|
|
632
|
-
Supplements,
|
|
633
|
-
Details,
|
|
634
|
-
FMM,
|
|
635
|
-
WG,
|
|
636
|
-
StandardsStatus,
|
|
637
|
-
Web,
|
|
638
|
-
(SELECT COUNT(*) FROM DependencyList WHERE TargetKey = Resources.ResourceKey) AS UsageCount
|
|
639
|
-
FROM Resources
|
|
640
|
-
${whereClause}
|
|
641
|
-
ORDER BY ResourceType, Type, Description
|
|
642
|
-
LIMIT ${limit} OFFSET ${offset}
|
|
643
|
-
`;
|
|
644
|
-
|
|
645
|
-
return sql.trim();
|
|
646
|
-
}
|
|
647
|
-
|
|
648
374
|
// Resource List Table Functions
|
|
649
375
|
|
|
650
376
|
function buildPaginationControls(count, offset, baseUrl, queryParams) {
|
|
@@ -823,10 +549,10 @@ async function buildResourceTable(queryParams, resourceCount, offset = 0) {
|
|
|
823
549
|
parts.push('</tr>');
|
|
824
550
|
|
|
825
551
|
const resourceRows = await new Promise((resolve, reject) => {
|
|
826
|
-
xigDb.all(resourceQuery, qp, (err, rows) => {
|
|
552
|
+
xigDb.all(resourceQuery, qp, guarded(reject, (err, rows) => {
|
|
827
553
|
if (err) reject(err);
|
|
828
554
|
else resolve(rows || []);
|
|
829
|
-
});
|
|
555
|
+
}));
|
|
830
556
|
});
|
|
831
557
|
|
|
832
558
|
// Determine resource type prefix for links
|
|
@@ -969,10 +695,10 @@ async function buildResourceTable(queryParams, resourceCount, offset = 0) {
|
|
|
969
695
|
async function fetchResourceRows(queryParams, offset = 0, limit = 200) {
|
|
970
696
|
const { query: resourceQuery, params: qp } = buildSecureResourceQuery(queryParams, offset, limit);
|
|
971
697
|
return new Promise((resolve, reject) => {
|
|
972
|
-
xigDb.all(resourceQuery, qp, (err, rows) => {
|
|
698
|
+
xigDb.all(resourceQuery, qp, guarded(reject, (err, rows) => {
|
|
973
699
|
if (err) reject(err);
|
|
974
700
|
else resolve(rows || []);
|
|
975
|
-
});
|
|
701
|
+
}));
|
|
976
702
|
});
|
|
977
703
|
}
|
|
978
704
|
|
|
@@ -1093,7 +819,7 @@ async function buildResourceCsv(queryParams) {
|
|
|
1093
819
|
|
|
1094
820
|
async function buildSummaryStats(queryParams, baseUrl) {
|
|
1095
821
|
const { ver, auth, realm } = queryParams;
|
|
1096
|
-
const
|
|
822
|
+
const { where, params } = buildResourceFilter(queryParams);
|
|
1097
823
|
let html = '';
|
|
1098
824
|
|
|
1099
825
|
if (!xigDb) {
|
|
@@ -1110,18 +836,16 @@ async function buildSummaryStats(queryParams, baseUrl) {
|
|
|
1110
836
|
const versions = getCachedSet('versions');
|
|
1111
837
|
for (const version of versions) {
|
|
1112
838
|
try {
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
sql = `SELECT COUNT(*) as count FROM Resources WHERE ${version} = 1`;
|
|
1116
|
-
} else {
|
|
1117
|
-
sql = `SELECT COUNT(*) as count FROM Resources ${currentFilter} AND ${version} = 1`;
|
|
839
|
+
if (!VERSION_COLUMNS.includes(version)) {
|
|
840
|
+
continue; // a column name, so only one of the known ones
|
|
1118
841
|
}
|
|
842
|
+
const sql = `SELECT COUNT(*) as count FROM Resources ${where} AND ${version} = 1`;
|
|
1119
843
|
|
|
1120
844
|
const count = await new Promise((resolve, reject) => {
|
|
1121
|
-
xigDb.get(sql,
|
|
845
|
+
xigDb.get(sql, params, guarded(reject, (err, row) => {
|
|
1122
846
|
if (err) reject(err);
|
|
1123
847
|
else resolve(row ? row.count : 0);
|
|
1124
|
-
});
|
|
848
|
+
}));
|
|
1125
849
|
});
|
|
1126
850
|
|
|
1127
851
|
const linkUrl = buildVersionLinkUrl(baseUrl, queryParams, version);
|
|
@@ -1138,18 +862,13 @@ async function buildSummaryStats(queryParams, baseUrl) {
|
|
|
1138
862
|
html += '<p><strong>By Authority</strong></p>';
|
|
1139
863
|
html += '<ul style="columns: 4; -webkit-columns: 4; -moz-columns: 4">';
|
|
1140
864
|
|
|
1141
|
-
|
|
1142
|
-
if (currentFilter === '') {
|
|
1143
|
-
sql = 'SELECT Authority, COUNT(*) as count FROM Resources GROUP BY Authority ORDER BY Authority';
|
|
1144
|
-
} else {
|
|
1145
|
-
sql = `SELECT Authority, COUNT(*) as count FROM Resources ${currentFilter} GROUP BY Authority ORDER BY Authority`;
|
|
1146
|
-
}
|
|
865
|
+
const sql = `SELECT Authority, COUNT(*) as count FROM Resources ${where} GROUP BY Authority ORDER BY Authority`;
|
|
1147
866
|
|
|
1148
867
|
const authorityResults = await new Promise((resolve, reject) => {
|
|
1149
|
-
xigDb.all(sql,
|
|
868
|
+
xigDb.all(sql, params, guarded(reject, (err, rows) => {
|
|
1150
869
|
if (err) reject(err);
|
|
1151
870
|
else resolve(rows || []);
|
|
1152
|
-
});
|
|
871
|
+
}));
|
|
1153
872
|
});
|
|
1154
873
|
|
|
1155
874
|
authorityResults.forEach(row => {
|
|
@@ -1171,18 +890,13 @@ async function buildSummaryStats(queryParams, baseUrl) {
|
|
|
1171
890
|
html += '<p><strong>By Realm</strong></p>';
|
|
1172
891
|
html += '<ul style="columns: 4; -webkit-columns: 4; -moz-columns: 4">';
|
|
1173
892
|
|
|
1174
|
-
|
|
1175
|
-
if (currentFilter === '') {
|
|
1176
|
-
sql = 'SELECT Realm, COUNT(*) as count FROM Resources GROUP BY Realm ORDER BY Realm';
|
|
1177
|
-
} else {
|
|
1178
|
-
sql = `SELECT Realm, COUNT(*) as count FROM Resources ${currentFilter} GROUP BY Realm ORDER BY Realm`;
|
|
1179
|
-
}
|
|
893
|
+
const sql = `SELECT Realm, COUNT(*) as count FROM Resources ${where} GROUP BY Realm ORDER BY Realm`;
|
|
1180
894
|
|
|
1181
895
|
const realmResults = await new Promise((resolve, reject) => {
|
|
1182
|
-
xigDb.all(sql,
|
|
896
|
+
xigDb.all(sql, params, guarded(reject, (err, rows) => {
|
|
1183
897
|
if (err) reject(err);
|
|
1184
898
|
else resolve(rows || []);
|
|
1185
|
-
});
|
|
899
|
+
}));
|
|
1186
900
|
});
|
|
1187
901
|
|
|
1188
902
|
var c = 0;
|
|
@@ -1825,7 +1539,7 @@ function validateDatabaseFile(filePath) {
|
|
|
1825
1539
|
}
|
|
1826
1540
|
|
|
1827
1541
|
// Try a simple query to ensure the database is accessible
|
|
1828
|
-
testDb.get("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1", (err) => {
|
|
1542
|
+
testDb.get("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1", guarded(reject, (err) => {
|
|
1829
1543
|
testDb.close();
|
|
1830
1544
|
|
|
1831
1545
|
if (err) {
|
|
@@ -1833,7 +1547,7 @@ function validateDatabaseFile(filePath) {
|
|
|
1833
1547
|
} else {
|
|
1834
1548
|
resolve();
|
|
1835
1549
|
}
|
|
1836
|
-
});
|
|
1550
|
+
}));
|
|
1837
1551
|
});
|
|
1838
1552
|
});
|
|
1839
1553
|
}
|
|
@@ -1861,13 +1575,13 @@ async function loadConfigCache() {
|
|
|
1861
1575
|
// Helper function for simple queries
|
|
1862
1576
|
const executeQuery = (sql, params = []) => {
|
|
1863
1577
|
return new Promise((resolve, reject) => {
|
|
1864
|
-
xigDb.all(sql, params, (err, rows) => {
|
|
1578
|
+
xigDb.all(sql, params, guarded(reject, (err, rows) => {
|
|
1865
1579
|
if (err) {
|
|
1866
1580
|
reject(err);
|
|
1867
1581
|
} else {
|
|
1868
1582
|
resolve(rows);
|
|
1869
1583
|
}
|
|
1870
|
-
});
|
|
1584
|
+
}));
|
|
1871
1585
|
});
|
|
1872
1586
|
};
|
|
1873
1587
|
|
|
@@ -2031,7 +1745,11 @@ function initializeDatabase() {
|
|
|
2031
1745
|
});
|
|
2032
1746
|
}
|
|
2033
1747
|
|
|
2034
|
-
|
|
1748
|
+
/**
|
|
1749
|
+
* Download the database afresh and swap it in.
|
|
1750
|
+
* @param trigger - 'cron' (the daily schedule), 'startup' (no database yet) or 'manual'
|
|
1751
|
+
*/
|
|
1752
|
+
async function updateXigDatabase(trigger = 'manual') {
|
|
2035
1753
|
if (updateInProgress) {
|
|
2036
1754
|
xigLog.warn('Update already in progress, skipping');
|
|
2037
1755
|
return;
|
|
@@ -2040,7 +1758,7 @@ async function updateXigDatabase() {
|
|
|
2040
1758
|
updateInProgress = true;
|
|
2041
1759
|
const entry = {
|
|
2042
1760
|
timestamp: new Date(),
|
|
2043
|
-
trigger:
|
|
1761
|
+
trigger: trigger,
|
|
2044
1762
|
status: 'started',
|
|
2045
1763
|
sourceUrl: XIG_DB_URL,
|
|
2046
1764
|
error: null,
|
|
@@ -2067,14 +1785,14 @@ async function updateXigDatabase() {
|
|
|
2067
1785
|
entry.status = 'validated';
|
|
2068
1786
|
|
|
2069
1787
|
if (xigDb) {
|
|
2070
|
-
await new Promise((resolve) => {
|
|
2071
|
-
xigDb.close((err) => {
|
|
1788
|
+
await new Promise((resolve, reject) => {
|
|
1789
|
+
xigDb.close(guarded(reject, (err) => {
|
|
2072
1790
|
if (err) {
|
|
2073
1791
|
xigLog.warn(`Warning: Error closing existing database: ${err.message}`);
|
|
2074
1792
|
}
|
|
2075
1793
|
xigDb = null;
|
|
2076
1794
|
resolve();
|
|
2077
|
-
});
|
|
1795
|
+
}));
|
|
2078
1796
|
});
|
|
2079
1797
|
}
|
|
2080
1798
|
|
|
@@ -2135,7 +1853,7 @@ router.use(trackRequest);
|
|
|
2135
1853
|
|
|
2136
1854
|
// Statistics functions
|
|
2137
1855
|
function getDatabaseTableCounts() {
|
|
2138
|
-
return new Promise((resolve) => {
|
|
1856
|
+
return new Promise((resolve, reject) => {
|
|
2139
1857
|
if (!xigDb) {
|
|
2140
1858
|
resolve({ packages: 0, resources: 0 });
|
|
2141
1859
|
return;
|
|
@@ -2145,7 +1863,7 @@ function getDatabaseTableCounts() {
|
|
|
2145
1863
|
let completedQueries = 0;
|
|
2146
1864
|
const totalQueries = 2;
|
|
2147
1865
|
|
|
2148
|
-
xigDb.get('SELECT COUNT(*) as count FROM Packages', [], (err, row) => {
|
|
1866
|
+
xigDb.get('SELECT COUNT(*) as count FROM Packages', [], guarded(reject, (err, row) => {
|
|
2149
1867
|
if (err) {
|
|
2150
1868
|
counts.packages = 0;
|
|
2151
1869
|
} else {
|
|
@@ -2156,9 +1874,9 @@ function getDatabaseTableCounts() {
|
|
|
2156
1874
|
if (completedQueries === totalQueries) {
|
|
2157
1875
|
resolve(counts);
|
|
2158
1876
|
}
|
|
2159
|
-
});
|
|
1877
|
+
}));
|
|
2160
1878
|
|
|
2161
|
-
xigDb.get('SELECT COUNT(*) as count FROM Resources', [], (err, row) => {
|
|
1879
|
+
xigDb.get('SELECT COUNT(*) as count FROM Resources', [], guarded(reject, (err, row) => {
|
|
2162
1880
|
if (err) {
|
|
2163
1881
|
counts.resources = 0;
|
|
2164
1882
|
} else {
|
|
@@ -2169,7 +1887,7 @@ function getDatabaseTableCounts() {
|
|
|
2169
1887
|
if (completedQueries === totalQueries) {
|
|
2170
1888
|
resolve(counts);
|
|
2171
1889
|
}
|
|
2172
|
-
});
|
|
1890
|
+
}));
|
|
2173
1891
|
});
|
|
2174
1892
|
}
|
|
2175
1893
|
|
|
@@ -2391,7 +2109,7 @@ function getDatabaseInfo() {
|
|
|
2391
2109
|
return;
|
|
2392
2110
|
}
|
|
2393
2111
|
|
|
2394
|
-
xigDb.get("SELECT COUNT(*) as tableCount FROM sqlite_master WHERE type='table'", (err, row) => {
|
|
2112
|
+
xigDb.get("SELECT COUNT(*) as tableCount FROM sqlite_master WHERE type='table'", guarded(reject, (err, row) => {
|
|
2395
2113
|
if (err) {
|
|
2396
2114
|
reject(err);
|
|
2397
2115
|
} else {
|
|
@@ -2402,7 +2120,7 @@ function getDatabaseInfo() {
|
|
|
2402
2120
|
fileSize: fs.existsSync(XIG_DB_PATH) ? fs.statSync(XIG_DB_PATH).size : 0
|
|
2403
2121
|
});
|
|
2404
2122
|
}
|
|
2405
|
-
});
|
|
2123
|
+
}));
|
|
2406
2124
|
});
|
|
2407
2125
|
}
|
|
2408
2126
|
|
|
@@ -2507,13 +2225,13 @@ router.get('/', async (req, res) => {
|
|
|
2507
2225
|
if (xigDb) {
|
|
2508
2226
|
const {query: countQuery, params: countParams} = buildSecureResourceCountQuery(queryParams);
|
|
2509
2227
|
resourceCount = await new Promise((resolve, reject) => {
|
|
2510
|
-
xigDb.get(countQuery, countParams, (err, row) => {
|
|
2228
|
+
xigDb.get(countQuery, countParams, guarded(reject, (err, row) => {
|
|
2511
2229
|
if (err) {
|
|
2512
2230
|
reject(err);
|
|
2513
2231
|
} else {
|
|
2514
2232
|
resolve(row ? row.total : 0);
|
|
2515
2233
|
}
|
|
2516
|
-
});
|
|
2234
|
+
}));
|
|
2517
2235
|
});
|
|
2518
2236
|
}
|
|
2519
2237
|
} catch (error) {
|
|
@@ -2680,10 +2398,10 @@ router.get('/resource/:packagePid/:resourceType/:resourceId', async (req, res) =
|
|
|
2680
2398
|
`;
|
|
2681
2399
|
|
|
2682
2400
|
const resourceData = await new Promise((resolve, reject) => {
|
|
2683
|
-
xigDb.get(resourceQuery, [packageObj.PackageKey, resourceType, resourceId], (err, row) => {
|
|
2401
|
+
xigDb.get(resourceQuery, [packageObj.PackageKey, resourceType, resourceId], guarded(reject, (err, row) => {
|
|
2684
2402
|
if (err) reject(err);
|
|
2685
2403
|
else resolve(row);
|
|
2686
|
-
});
|
|
2404
|
+
}));
|
|
2687
2405
|
});
|
|
2688
2406
|
|
|
2689
2407
|
if (!resourceData) {
|
|
@@ -2830,10 +2548,10 @@ async function buildResourceDependencies(resourceData, secure = false) {
|
|
|
2830
2548
|
`;
|
|
2831
2549
|
|
|
2832
2550
|
const dependencies = await new Promise((resolve, reject) => {
|
|
2833
|
-
xigDb.all(dependenciesQuery, [resourceData.ResourceKey], (err, rows) => {
|
|
2551
|
+
xigDb.all(dependenciesQuery, [resourceData.ResourceKey], guarded(reject, (err, rows) => {
|
|
2834
2552
|
if (err) reject(err);
|
|
2835
2553
|
else resolve(rows || []);
|
|
2836
|
-
});
|
|
2554
|
+
}));
|
|
2837
2555
|
});
|
|
2838
2556
|
|
|
2839
2557
|
if (dependencies.length === 0) {
|
|
@@ -2855,10 +2573,10 @@ async function buildResourceDependencies(resourceData, secure = false) {
|
|
|
2855
2573
|
`;
|
|
2856
2574
|
|
|
2857
2575
|
const uses = await new Promise((resolve, reject) => {
|
|
2858
|
-
xigDb.all(usesQuery, [resourceData.ResourceKey], (err, rows) => {
|
|
2576
|
+
xigDb.all(usesQuery, [resourceData.ResourceKey], guarded(reject, (err, rows) => {
|
|
2859
2577
|
if (err) reject(err);
|
|
2860
2578
|
else resolve(rows || []);
|
|
2861
|
-
});
|
|
2579
|
+
}));
|
|
2862
2580
|
});
|
|
2863
2581
|
|
|
2864
2582
|
if (uses.length === 0) {
|
|
@@ -2895,10 +2613,10 @@ async function buildExtensionExamplesSection(resourceUrl) {
|
|
|
2895
2613
|
`;
|
|
2896
2614
|
|
|
2897
2615
|
const extensionExamples = await new Promise((resolve, reject) => {
|
|
2898
|
-
xigDb.all(extensionExamplesQuery, [resourceUrl], (err, rows) => {
|
|
2616
|
+
xigDb.all(extensionExamplesQuery, [resourceUrl], guarded(reject, (err, rows) => {
|
|
2899
2617
|
if (err) reject(err);
|
|
2900
2618
|
else resolve(rows || []);
|
|
2901
|
-
});
|
|
2619
|
+
}));
|
|
2902
2620
|
});
|
|
2903
2621
|
|
|
2904
2622
|
if (extensionExamples.length === 0) {
|
|
@@ -2998,10 +2716,10 @@ async function buildResourceNarrative(resourceKey, packageObj) {
|
|
|
2998
2716
|
const contentsQuery = 'SELECT Json FROM Contents WHERE ResourceKey = ?';
|
|
2999
2717
|
|
|
3000
2718
|
const blobData = await new Promise((resolve, reject) => {
|
|
3001
|
-
xigDb.get(contentsQuery, [resourceKey], (err, row) => {
|
|
2719
|
+
xigDb.get(contentsQuery, [resourceKey], guarded(reject, (err, row) => {
|
|
3002
2720
|
if (err) reject(err);
|
|
3003
2721
|
else resolve(row);
|
|
3004
|
-
});
|
|
2722
|
+
}));
|
|
3005
2723
|
});
|
|
3006
2724
|
|
|
3007
2725
|
if (!blobData || !blobData.Json) {
|
|
@@ -3060,10 +2778,10 @@ async function buildResourceSource(resourceKey) {
|
|
|
3060
2778
|
const contentsQuery = 'SELECT Json FROM Contents WHERE ResourceKey = ?';
|
|
3061
2779
|
|
|
3062
2780
|
const blobData = await new Promise((resolve, reject) => {
|
|
3063
|
-
xigDb.get(contentsQuery, [resourceKey], (err, row) => {
|
|
2781
|
+
xigDb.get(contentsQuery, [resourceKey], guarded(reject, (err, row) => {
|
|
3064
2782
|
if (err) reject(err);
|
|
3065
2783
|
else resolve(row);
|
|
3066
|
-
});
|
|
2784
|
+
}));
|
|
3067
2785
|
});
|
|
3068
2786
|
|
|
3069
2787
|
if (!blobData || !blobData.Json) {
|
|
@@ -3229,18 +2947,18 @@ async function initializeXigModule(stats, xigConfig) {
|
|
|
3229
2947
|
if (!fs.existsSync(XIG_DB_PATH)) {
|
|
3230
2948
|
xigLog.info('No existing XIG database found, triggering initial download');
|
|
3231
2949
|
setTimeout(() => {
|
|
3232
|
-
updateXigDatabase().catch((err) => xigLog.error('XIG database update failed: ' + err.message));
|
|
2950
|
+
updateXigDatabase('startup').catch((err) => xigLog.error('XIG database update failed: ' + err.message));
|
|
3233
2951
|
}, 5000);
|
|
3234
2952
|
}
|
|
3235
2953
|
|
|
3236
2954
|
if (globalStats) {
|
|
3237
|
-
globalStats.addTask('XIG Download', describeCron(
|
|
2955
|
+
globalStats.addTask('XIG Download', describeCron(UPDATE_CRON));
|
|
3238
2956
|
}
|
|
3239
2957
|
// Check if auto-update is enabled
|
|
3240
2958
|
// Note: This assumes we're called only when XIG is enabled
|
|
3241
2959
|
if (xigConfig?.autoUpdate !== false) {
|
|
3242
|
-
cron.schedule(
|
|
3243
|
-
updateXigDatabase().catch((err) => xigLog.error('XIG database update failed: ' + err.message));
|
|
2960
|
+
cron.schedule(UPDATE_CRON, () => {
|
|
2961
|
+
updateXigDatabase('cron').catch((err) => xigLog.error('XIG database update failed: ' + err.message));
|
|
3244
2962
|
});
|
|
3245
2963
|
}
|
|
3246
2964
|
|
|
@@ -3252,9 +2970,105 @@ async function initializeXigModule(stats, xigConfig) {
|
|
|
3252
2970
|
|
|
3253
2971
|
// Graceful shutdown
|
|
3254
2972
|
/**
|
|
3255
|
-
* The
|
|
3256
|
-
|
|
3257
|
-
|
|
2973
|
+
* The times (ms, local 02:00) the scheduled update was due in (since, now]
|
|
2974
|
+
*/
|
|
2975
|
+
function scheduledUpdatesBetween(since, now) {
|
|
2976
|
+
const result = [];
|
|
2977
|
+
const d = new Date(since);
|
|
2978
|
+
d.setHours(UPDATE_HOUR, 0, 0, 0);
|
|
2979
|
+
if (d.getTime() <= since) {
|
|
2980
|
+
d.setDate(d.getDate() + 1);
|
|
2981
|
+
}
|
|
2982
|
+
while (d.getTime() <= now) {
|
|
2983
|
+
result.push(d.getTime());
|
|
2984
|
+
d.setDate(d.getDate() + 1);
|
|
2985
|
+
}
|
|
2986
|
+
return result;
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
/**
|
|
2990
|
+
* The sentences about database updates in the period (since, now] for the daily report:
|
|
2991
|
+
* affirms the scheduled update happened, or says that it failed or did not run.
|
|
2992
|
+
*
|
|
2993
|
+
* @param history - update attempts, newest first (getUpdateHistory())
|
|
2994
|
+
* @param autoUpdate - false if scheduled updates are turned off
|
|
2995
|
+
* @param dbModified - when the database file in use was written (ms), or null if there isn't one
|
|
2996
|
+
*/
|
|
2997
|
+
function describeUpdates(history, {since, now, autoUpdate = true, dbModified = null}) {
|
|
2998
|
+
const {localDateTime, formatSpan, formatBytes} = require('../library/daily-report');
|
|
2999
|
+
// just the time of day, unless it's more than a day before the report
|
|
3000
|
+
const time = (t) => now - t <= 24 * 60 * 60 * 1000 ? localDateTime(t).substring(11) : localDateTime(t);
|
|
3001
|
+
const at = (e) => (e.timestamp instanceof Date ? e.timestamp : new Date(e.timestamp)).getTime();
|
|
3002
|
+
const detail = (e) => {
|
|
3003
|
+
const bits = [];
|
|
3004
|
+
if (e.downloadMeta && e.downloadMeta.downloadedBytes) {
|
|
3005
|
+
bits.push(formatBytes(e.downloadMeta.downloadedBytes));
|
|
3006
|
+
}
|
|
3007
|
+
if (e.durationMs != null) {
|
|
3008
|
+
bits.push(`took ${formatSpan(e.durationMs)}`);
|
|
3009
|
+
}
|
|
3010
|
+
return bits.length ? ` (${bits.join(', ')})` : '';
|
|
3011
|
+
};
|
|
3012
|
+
|
|
3013
|
+
const sentences = [];
|
|
3014
|
+
const attempts = (history || []).filter(e => at(e) > since && at(e) <= now).sort((a, b) => at(a) - at(b));
|
|
3015
|
+
const scheduled = attempts.filter(e => e.trigger === 'cron');
|
|
3016
|
+
const others = attempts.filter(e => e.trigger !== 'cron');
|
|
3017
|
+
let updated = attempts.some(e => e.status === 'success');
|
|
3018
|
+
|
|
3019
|
+
if (autoUpdate === false) {
|
|
3020
|
+
sentences.push('Scheduled database updates are turned off.');
|
|
3021
|
+
} else {
|
|
3022
|
+
const due = scheduledUpdatesBetween(since, now);
|
|
3023
|
+
for (const e of scheduled) {
|
|
3024
|
+
if (e.status === 'success') {
|
|
3025
|
+
sentences.push(`The database was updated on schedule at ${time(at(e))}${detail(e)}.`);
|
|
3026
|
+
} else if (e.status === 'failed') {
|
|
3027
|
+
sentences.push(`The scheduled database update at ${time(at(e))} failed${e.error ? ': ' + e.error : ''}.`);
|
|
3028
|
+
} else {
|
|
3029
|
+
sentences.push(`The scheduled database update that started at ${time(at(e))} has not finished.`);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
if (scheduled.length === 0 && due.length > 0) {
|
|
3033
|
+
const last = due[due.length - 1];
|
|
3034
|
+
if (dbModified != null && dbModified >= due[0] && dbModified <= now) {
|
|
3035
|
+
// the record went with a restart, but the file shows it happened
|
|
3036
|
+
sentences.push(`The database was updated at ${time(dbModified)}.`);
|
|
3037
|
+
updated = true;
|
|
3038
|
+
} else {
|
|
3039
|
+
sentences.push(`The database was not updated on schedule: no update ran at ${time(last)} (was the server running then?).`);
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
if (scheduled.length > 0 && scheduled.length < due.length) {
|
|
3043
|
+
sentences.push(`${due.length - scheduled.length} of the ${due.length} scheduled updates in this period did not run.`);
|
|
3044
|
+
}
|
|
3045
|
+
if (scheduled.length === 0 && due.length === 0 && attempts.length === 0) {
|
|
3046
|
+
sentences.push('No database update was due in this period.');
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
for (const e of others) {
|
|
3050
|
+
const how = e.trigger === 'startup' ? 'initial download' : 'manual update';
|
|
3051
|
+
if (e.status === 'success') {
|
|
3052
|
+
sentences.push(`A ${how} at ${time(at(e))} succeeded${detail(e)}.`);
|
|
3053
|
+
} else if (e.status === 'failed') {
|
|
3054
|
+
sentences.push(`A ${how} at ${time(at(e))} failed${e.error ? ': ' + e.error : ''}.`);
|
|
3055
|
+
} else {
|
|
3056
|
+
sentences.push(`A ${how} started at ${time(at(e))} has not finished.`);
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
if (!updated) {
|
|
3060
|
+
if (dbModified == null) {
|
|
3061
|
+
sentences.push('There is no database file.');
|
|
3062
|
+
} else {
|
|
3063
|
+
sentences.push(`The database in use was downloaded ${localDateTime(dbModified)} (${formatSpan(now - dbModified)} ago).`);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
return sentences;
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
/**
|
|
3070
|
+
* The paragraph for the server's daily report: pages served, whether the database
|
|
3071
|
+
* was updated on schedule (it's downloaded afresh each day), and whether it can be read.
|
|
3258
3072
|
*/
|
|
3259
3073
|
async function dailyReport({since, now}) {
|
|
3260
3074
|
const {plural} = require('../library/daily-report');
|
|
@@ -3263,36 +3077,37 @@ async function dailyReport({since, now}) {
|
|
|
3263
3077
|
const counts = globalStats.countsSince(since, now);
|
|
3264
3078
|
sentences.push(`${plural(counts.total, 'page')} ${counts.total === 1 ? 'was' : 'were'} served.`);
|
|
3265
3079
|
}
|
|
3080
|
+
const age = getDatabaseAgeInfo();
|
|
3081
|
+
sentences.push(...describeUpdates(getUpdateHistory(), {
|
|
3082
|
+
since, now,
|
|
3083
|
+
autoUpdate: xigSettings?.autoUpdate !== false,
|
|
3084
|
+
dbModified: age.lastDownloaded ? age.lastDownloaded.getTime() : null
|
|
3085
|
+
}));
|
|
3266
3086
|
if (!xigDb) {
|
|
3267
3087
|
sentences.push('The database is not open: it could not be accessed.');
|
|
3268
3088
|
} else {
|
|
3269
3089
|
try {
|
|
3270
3090
|
const row = await new Promise((resolve, reject) =>
|
|
3271
|
-
xigDb.get('SELECT COUNT(*) AS n FROM Resources', [], (err, row) => err ? reject(err) : resolve(row)));
|
|
3091
|
+
xigDb.get('SELECT COUNT(*) AS n FROM Resources', [], guarded(reject, (err, row) => err ? reject(err) : resolve(row))));
|
|
3272
3092
|
sentences.push(`The database holds ${plural(row.n, 'resource')}.`);
|
|
3273
3093
|
} catch (e) {
|
|
3274
3094
|
sentences.push(`The database could not be accessed: ${e.message}.`);
|
|
3275
3095
|
}
|
|
3276
3096
|
}
|
|
3277
|
-
const last = getLastUpdateAttempt();
|
|
3278
|
-
if (last && last.status === 'failed') {
|
|
3279
|
-
const when = last.timestamp instanceof Date ? last.timestamp.toISOString().substring(0, 16).replace('T', ' ') : String(last.timestamp);
|
|
3280
|
-
sentences.push(`The last database update (${when}) failed${last.error ? ': ' + last.error : ''}.`);
|
|
3281
|
-
}
|
|
3282
3097
|
return sentences.join(' ');
|
|
3283
3098
|
}
|
|
3284
3099
|
|
|
3285
3100
|
function shutdown() {
|
|
3286
|
-
return new Promise((resolve) => {
|
|
3101
|
+
return new Promise((resolve, reject) => {
|
|
3287
3102
|
if (xigDb) {
|
|
3288
|
-
xigDb.close((err) => {
|
|
3103
|
+
xigDb.close(guarded(reject, (err) => {
|
|
3289
3104
|
if (err) {
|
|
3290
3105
|
xigLog.error(`Error closing XIG database: ${err.message}`);
|
|
3291
3106
|
} else {
|
|
3292
3107
|
xigLog.error('XIG database connection closed');
|
|
3293
3108
|
}
|
|
3294
3109
|
resolve();
|
|
3295
|
-
});
|
|
3110
|
+
}));
|
|
3296
3111
|
} else {
|
|
3297
3112
|
resolve();
|
|
3298
3113
|
}
|
|
@@ -3363,10 +3178,9 @@ module.exports = {
|
|
|
3363
3178
|
buildRealmLinkUrl,
|
|
3364
3179
|
|
|
3365
3180
|
// SQL filter functions
|
|
3366
|
-
|
|
3367
|
-
|
|
3181
|
+
buildResourceFilter,
|
|
3182
|
+
buildSecureResourceQuery,
|
|
3368
3183
|
buildSecureResourceCountQuery,
|
|
3369
|
-
sqlEscapeString,
|
|
3370
3184
|
hasTerminologySource,
|
|
3371
3185
|
gatherPageStatistics,
|
|
3372
3186
|
|
|
@@ -3377,6 +3191,8 @@ module.exports = {
|
|
|
3377
3191
|
buildStatsTable,
|
|
3378
3192
|
getUpdateHistory,
|
|
3379
3193
|
getLastUpdateAttempt,
|
|
3194
|
+
describeUpdates,
|
|
3195
|
+
scheduledUpdatesBetween,
|
|
3380
3196
|
|
|
3381
3197
|
// Event emitter
|
|
3382
3198
|
cacheEmitter
|