fhirsmith 0.18.1 → 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 +190 -9
- 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 +331 -33
- package/ips/jurisdictions.js +65 -0
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +131 -17
- package/ips/presenter.js +1 -1
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +170 -20
- package/ips/render.js +769 -109
- package/ips/scripts.js +46 -0
- package/ips/store.js +123 -11
- 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/api.js +179 -1
- package/registry/crawler.js +86 -108
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/configuration.md +34 -0
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +58 -6
- 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 +158 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +50 -16
- 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 +199 -24
- 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 +193 -67
- 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 +861 -95
- 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 +125 -61
- 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/tho-precedence.js +40 -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 +314 -67
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +81 -17
- 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 +175 -15
- 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 +44 -17
- package/tx/vs/vs-vsac.js +649 -101
- 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/publisher/publisher.js
CHANGED
|
@@ -14,6 +14,7 @@ const folders = require('../library/folder-setup');
|
|
|
14
14
|
const escape = require('escape-html');
|
|
15
15
|
const { requireSameOrigin } = require('../library/same-origin');
|
|
16
16
|
const {Utilities} = require("../library/utilities");
|
|
17
|
+
const { guarded } = require('../library/sqlite-utilities');
|
|
17
18
|
|
|
18
19
|
// GitHub refuses any file larger than this at the pre-receive hook, and the rejection takes down
|
|
19
20
|
// the whole push rather than just the offending file.
|
|
@@ -206,19 +207,19 @@ class PublisherModule {
|
|
|
206
207
|
|
|
207
208
|
for (const sql of tables) {
|
|
208
209
|
await new Promise((resolve, reject) => {
|
|
209
|
-
this.db.run(sql, (err) => {
|
|
210
|
+
this.db.run(sql, guarded(reject, (err) => {
|
|
210
211
|
if (err) {
|
|
211
212
|
this.logger.error('Failed to create table:', err);
|
|
212
213
|
reject(err);
|
|
213
214
|
} else {
|
|
214
215
|
resolve();
|
|
215
216
|
}
|
|
216
|
-
});
|
|
217
|
+
}));
|
|
217
218
|
});
|
|
218
219
|
}
|
|
219
220
|
|
|
220
|
-
//
|
|
221
|
-
await this.
|
|
221
|
+
// the administrator, with the configured password (or a random one)
|
|
222
|
+
await this.ensureAdmin();
|
|
222
223
|
|
|
223
224
|
// Schema migrations for existing databases
|
|
224
225
|
await this.runMigrations();
|
|
@@ -227,100 +228,110 @@ class PublisherModule {
|
|
|
227
228
|
async runMigrations() {
|
|
228
229
|
// Add announcement column if it doesn't exist (added after initial schema)
|
|
229
230
|
const columns = await new Promise((resolve, reject) => {
|
|
230
|
-
this.db.all("PRAGMA table_info(tasks)", (err, rows) => {
|
|
231
|
+
this.db.all("PRAGMA table_info(tasks)", guarded(reject, (err, rows) => {
|
|
231
232
|
if (err) reject(err);
|
|
232
233
|
else resolve(rows || []);
|
|
233
|
-
});
|
|
234
|
+
}));
|
|
234
235
|
});
|
|
235
236
|
const columnNames = columns.map(c => c.name);
|
|
236
237
|
if (!columnNames.includes('announcement')) {
|
|
237
238
|
await new Promise((resolve, reject) => {
|
|
238
|
-
this.db.run('ALTER TABLE tasks ADD COLUMN announcement TEXT', (err) => {
|
|
239
|
+
this.db.run('ALTER TABLE tasks ADD COLUMN announcement TEXT', guarded(reject, (err) => {
|
|
239
240
|
if (err) reject(err);
|
|
240
241
|
else {
|
|
241
242
|
this.logger.info('Migration: added announcement column to tasks table');
|
|
242
243
|
resolve();
|
|
243
244
|
}
|
|
244
|
-
});
|
|
245
|
+
}));
|
|
245
246
|
});
|
|
246
247
|
}
|
|
247
248
|
if (!columnNames.includes('publisher_version')) {
|
|
248
249
|
await new Promise((resolve, reject) => {
|
|
249
|
-
this.db.run('ALTER TABLE tasks ADD COLUMN publisher_version TEXT', (err) => {
|
|
250
|
+
this.db.run('ALTER TABLE tasks ADD COLUMN publisher_version TEXT', guarded(reject, (err) => {
|
|
250
251
|
if (err) reject(err);
|
|
251
252
|
else {
|
|
252
253
|
this.logger.info('Migration: added publisher_version column to tasks table');
|
|
253
254
|
resolve();
|
|
254
255
|
}
|
|
255
|
-
});
|
|
256
|
+
}));
|
|
256
257
|
});
|
|
257
258
|
}
|
|
258
259
|
if (!columnNames.includes('release_url')) {
|
|
259
260
|
await new Promise((resolve, reject) => {
|
|
260
|
-
this.db.run('ALTER TABLE tasks ADD COLUMN release_url TEXT', (err) => {
|
|
261
|
+
this.db.run('ALTER TABLE tasks ADD COLUMN release_url TEXT', guarded(reject, (err) => {
|
|
261
262
|
if (err) reject(err);
|
|
262
263
|
else {
|
|
263
264
|
this.logger.info('Migration: added release_url column to tasks table');
|
|
264
265
|
resolve();
|
|
265
266
|
}
|
|
266
|
-
});
|
|
267
|
+
}));
|
|
267
268
|
});
|
|
268
269
|
}
|
|
269
270
|
const websiteColumns = await new Promise((resolve, reject) => {
|
|
270
|
-
this.db.all("PRAGMA table_info(websites)", (err, rows) => {
|
|
271
|
+
this.db.all("PRAGMA table_info(websites)", guarded(reject, (err, rows) => {
|
|
271
272
|
if (err) reject(err);
|
|
272
273
|
else resolve(rows || []);
|
|
273
|
-
});
|
|
274
|
+
}));
|
|
274
275
|
});
|
|
275
276
|
const websiteColumnNames = websiteColumns.map(c => c.name);
|
|
276
277
|
if (!websiteColumnNames.includes('git_root')) {
|
|
277
278
|
await new Promise((resolve, reject) => {
|
|
278
|
-
this.db.run('ALTER TABLE websites ADD COLUMN git_root TEXT', (err) => {
|
|
279
|
+
this.db.run('ALTER TABLE websites ADD COLUMN git_root TEXT', guarded(reject, (err) => {
|
|
279
280
|
if (err) reject(err);
|
|
280
281
|
else {
|
|
281
282
|
this.logger.info('Migration: added git_root column to websites table');
|
|
282
283
|
resolve();
|
|
283
284
|
}
|
|
284
|
-
});
|
|
285
|
+
}));
|
|
285
286
|
});
|
|
286
287
|
}
|
|
287
288
|
}
|
|
288
289
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
290
|
+
/**
|
|
291
|
+
* The administrator, login 'admin'. Its password is the config's adminPassword, set (or reset)
|
|
292
|
+
* at every start, so changing the config changes it. With no adminPassword, an 'admin' that
|
|
293
|
+
* doesn't exist yet (and no other administrator) - or that still has the old fixed default
|
|
294
|
+
* password, admin123 - gets a random password (a UUID), which is logged: set adminPassword to
|
|
295
|
+
* choose it. There is no page to change a password
|
|
296
|
+
*/
|
|
297
|
+
async ensureAdmin() {
|
|
298
|
+
const get = (sql, params = []) => new Promise((resolve, reject) =>
|
|
299
|
+
this.db.get(sql, params, guarded(reject, (err, row) => err ? reject(err) : resolve(row))));
|
|
300
|
+
const run = (sql, params = []) => new Promise((resolve, reject) =>
|
|
301
|
+
this.db.run(sql, params, guarded(reject, (err) => err ? reject(err) : resolve())));
|
|
302
|
+
const setPassword = async (admin, password) => {
|
|
303
|
+
const hash = await bcrypt.hash(password, 10);
|
|
304
|
+
if (admin) {
|
|
305
|
+
await run('UPDATE users SET password_hash = ?, is_admin = 1 WHERE id = ?', [hash, admin.id]);
|
|
306
|
+
} else {
|
|
307
|
+
await run('INSERT INTO users (name, login, password_hash, is_admin) VALUES (?, ?, ?, ?)',
|
|
308
|
+
['Administrator', 'admin', hash, 1]);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
304
311
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
});
|
|
323
|
-
})
|
|
312
|
+
const configured = typeof this.config.adminPassword === 'string' && this.config.adminPassword.trim() !== ''
|
|
313
|
+
? this.config.adminPassword : null;
|
|
314
|
+
const admin = await get("SELECT id, password_hash FROM users WHERE login = 'admin'");
|
|
315
|
+
if (configured) {
|
|
316
|
+
if (!admin || !(await bcrypt.compare(configured, admin.password_hash))) {
|
|
317
|
+
await setPassword(admin, configured);
|
|
318
|
+
this.logger.info(`Administrator 'admin': password ${admin ? 'changed to' : 'set from'} the configured adminPassword`);
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (!admin) {
|
|
323
|
+
const others = await get('SELECT COUNT(*) as count FROM users WHERE is_admin = 1');
|
|
324
|
+
if (others.count > 0) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const password = crypto.randomUUID();
|
|
328
|
+
await setPassword(null, password);
|
|
329
|
+
this.logger.warn(`Created administrator - login: admin, password: ${password}. Set adminPassword in the publisher config to choose the password`);
|
|
330
|
+
} else if (await bcrypt.compare('admin123', admin.password_hash)) {
|
|
331
|
+
const password = crypto.randomUUID();
|
|
332
|
+
await setPassword(admin, password);
|
|
333
|
+
this.logger.warn(`Administrator 'admin' had the old default password (admin123); it is now ${password}. Set adminPassword in the publisher config to choose the password`);
|
|
334
|
+
}
|
|
324
335
|
}
|
|
325
336
|
|
|
326
337
|
setupRoutes() {
|
|
@@ -485,10 +496,10 @@ class PublisherModule {
|
|
|
485
496
|
this.db.get(
|
|
486
497
|
'SELECT * FROM tasks WHERE status = ? ORDER BY queued_at ASC LIMIT 1',
|
|
487
498
|
['queued'],
|
|
488
|
-
(err, row) => {
|
|
499
|
+
guarded(reject, (err, row) => {
|
|
489
500
|
if (err) reject(err);
|
|
490
501
|
else resolve(row);
|
|
491
|
-
}
|
|
502
|
+
})
|
|
492
503
|
);
|
|
493
504
|
});
|
|
494
505
|
}
|
|
@@ -498,10 +509,10 @@ class PublisherModule {
|
|
|
498
509
|
this.db.get(
|
|
499
510
|
'SELECT * FROM tasks WHERE status = ? ORDER BY publishing_at ASC LIMIT 1',
|
|
500
511
|
['publishing'],
|
|
501
|
-
(err, row) => {
|
|
512
|
+
guarded(reject, (err, row) => {
|
|
502
513
|
if (err) reject(err);
|
|
503
514
|
else resolve(row);
|
|
504
|
-
}
|
|
515
|
+
})
|
|
505
516
|
);
|
|
506
517
|
});
|
|
507
518
|
}
|
|
@@ -536,10 +547,10 @@ class PublisherModule {
|
|
|
536
547
|
this.db.run(
|
|
537
548
|
'UPDATE tasks SET ' + fields.join(', ') + ' WHERE id = ?',
|
|
538
549
|
values,
|
|
539
|
-
(err) => {
|
|
550
|
+
guarded(reject, (err) => {
|
|
540
551
|
if (err) reject(err);
|
|
541
552
|
else resolve();
|
|
542
|
-
}
|
|
553
|
+
})
|
|
543
554
|
);
|
|
544
555
|
});
|
|
545
556
|
}
|
|
@@ -558,20 +569,20 @@ class PublisherModule {
|
|
|
558
569
|
this.db.run(
|
|
559
570
|
'UPDATE tasks SET ' + assignments.join(', ') + ' WHERE id = ?',
|
|
560
571
|
values,
|
|
561
|
-
(err) => {
|
|
572
|
+
guarded(reject, (err) => {
|
|
562
573
|
if (err) reject(err);
|
|
563
574
|
else resolve();
|
|
564
|
-
}
|
|
575
|
+
})
|
|
565
576
|
);
|
|
566
577
|
});
|
|
567
578
|
}
|
|
568
579
|
|
|
569
580
|
async logTaskMessage(taskId, level, message) {
|
|
570
|
-
return new Promise((resolve) => {
|
|
581
|
+
return new Promise((resolve, reject) => {
|
|
571
582
|
this.db.run(
|
|
572
583
|
'INSERT INTO task_logs (task_id, level, message) VALUES (?, ?, ?)',
|
|
573
584
|
[taskId.toString(), level, message],
|
|
574
|
-
() => resolve() // Don't fail if logging fails
|
|
585
|
+
guarded(reject, () => resolve()) // Don't fail if logging fails
|
|
575
586
|
);
|
|
576
587
|
});
|
|
577
588
|
}
|
|
@@ -1629,10 +1640,10 @@ class PublisherModule {
|
|
|
1629
1640
|
this.db.get(
|
|
1630
1641
|
'SELECT * FROM users WHERE login = ?',
|
|
1631
1642
|
[login],
|
|
1632
|
-
(err, row) => {
|
|
1643
|
+
guarded(reject, (err, row) => {
|
|
1633
1644
|
if (err) reject(err);
|
|
1634
1645
|
else resolve(row);
|
|
1635
|
-
}
|
|
1646
|
+
})
|
|
1636
1647
|
);
|
|
1637
1648
|
});
|
|
1638
1649
|
|
|
@@ -1874,10 +1885,10 @@ class PublisherModule {
|
|
|
1874
1885
|
this.db.run(
|
|
1875
1886
|
'INSERT INTO tasks (user_id, website_id, github_org, github_repo, git_branch, npm_package_id, version) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
1876
1887
|
[req.session.userId, website_id, github_org, github_repo, git_branch, npm_package_id, version],
|
|
1877
|
-
function (err) {
|
|
1888
|
+
guarded(reject, function (err) {
|
|
1878
1889
|
if (err) reject(err);
|
|
1879
1890
|
else resolve(this.lastID);
|
|
1880
|
-
}
|
|
1891
|
+
})
|
|
1881
1892
|
);
|
|
1882
1893
|
});
|
|
1883
1894
|
|
|
@@ -1924,10 +1935,10 @@ class PublisherModule {
|
|
|
1924
1935
|
this.db.run(
|
|
1925
1936
|
'UPDATE tasks SET status = ?, approved_by = ?, publishing_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
1926
1937
|
['publishing', req.session.userId, taskId],
|
|
1927
|
-
(err) => {
|
|
1938
|
+
guarded(reject, (err) => {
|
|
1928
1939
|
if (err) reject(err);
|
|
1929
1940
|
else resolve();
|
|
1930
|
-
}
|
|
1941
|
+
})
|
|
1931
1942
|
);
|
|
1932
1943
|
});
|
|
1933
1944
|
|
|
@@ -1985,18 +1996,18 @@ class PublisherModule {
|
|
|
1985
1996
|
|
|
1986
1997
|
// Delete task logs
|
|
1987
1998
|
await new Promise((resolve, reject) => {
|
|
1988
|
-
this.db.run('DELETE FROM task_logs WHERE task_id = ?', [taskId.toString()], (err) => {
|
|
1999
|
+
this.db.run('DELETE FROM task_logs WHERE task_id = ?', [taskId.toString()], guarded(reject, (err) => {
|
|
1989
2000
|
if (err) reject(err);
|
|
1990
2001
|
else resolve();
|
|
1991
|
-
});
|
|
2002
|
+
}));
|
|
1992
2003
|
});
|
|
1993
2004
|
|
|
1994
2005
|
// Delete task record
|
|
1995
2006
|
await new Promise((resolve, reject) => {
|
|
1996
|
-
this.db.run('DELETE FROM tasks WHERE id = ?', [taskId], (err) => {
|
|
2007
|
+
this.db.run('DELETE FROM tasks WHERE id = ?', [taskId], guarded(reject, (err) => {
|
|
1997
2008
|
if (err) reject(err);
|
|
1998
2009
|
else resolve();
|
|
1999
|
-
});
|
|
2010
|
+
}));
|
|
2000
2011
|
});
|
|
2001
2012
|
|
|
2002
2013
|
this.logUserAction(req.session.userId, 'delete_task', taskId, req.ip);
|
|
@@ -2027,7 +2038,7 @@ class PublisherModule {
|
|
|
2027
2038
|
this.db.run(
|
|
2028
2039
|
'INSERT INTO tasks (user_id, website_id, github_org, github_repo, git_branch, npm_package_id, version) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
2029
2040
|
[req.session.userId, task.website_id, task.github_org, task.github_repo, task.git_branch, task.npm_package_id, task.version],
|
|
2030
|
-
function (err) { if (err) reject(err); else resolve(this.lastID); }
|
|
2041
|
+
guarded(reject, function (err) { if (err) reject(err); else resolve(this.lastID); })
|
|
2031
2042
|
);
|
|
2032
2043
|
});
|
|
2033
2044
|
this.logUserAction(req.session.userId, 'retry_task', newTaskId.toString(), req.ip);
|
|
@@ -2451,7 +2462,7 @@ class PublisherModule {
|
|
|
2451
2462
|
this.db.run(
|
|
2452
2463
|
'UPDATE websites SET name=?, local_folder=?, git_root = ?, history_templates=?, web_templates=?, server_update_script=?, is_active=? WHERE id=?',
|
|
2453
2464
|
[name, local_folder, git_root, history_templates, web_templates, server_update_script, is_active === '1' ? 1 : 0, websiteId],
|
|
2454
|
-
(err) => err ? reject(err) : resolve()
|
|
2465
|
+
guarded(reject, (err) => err ? reject(err) : resolve())
|
|
2455
2466
|
);
|
|
2456
2467
|
});
|
|
2457
2468
|
|
|
@@ -2572,10 +2583,10 @@ class PublisherModule {
|
|
|
2572
2583
|
this.db.run(
|
|
2573
2584
|
'INSERT INTO websites (name, local_folder, git_root, history_templates, web_templates, server_update_script) VALUES (?, ?, ?, ?, ?, ?)',
|
|
2574
2585
|
[name, local_folder, git_root, history_templates, web_templates, server_update_script],
|
|
2575
|
-
function (err) {
|
|
2586
|
+
guarded(reject, function (err) {
|
|
2576
2587
|
if (err) reject(err);
|
|
2577
2588
|
else resolve();
|
|
2578
|
-
}
|
|
2589
|
+
})
|
|
2579
2590
|
);
|
|
2580
2591
|
});
|
|
2581
2592
|
|
|
@@ -2716,10 +2727,10 @@ class PublisherModule {
|
|
|
2716
2727
|
this.db.run(
|
|
2717
2728
|
'INSERT INTO users (name, login, password_hash, is_admin) VALUES (?, ?, ?, ?)',
|
|
2718
2729
|
[name, login, passwordHash, is_admin ? 1 : 0],
|
|
2719
|
-
function (err) {
|
|
2730
|
+
guarded(reject, function (err) {
|
|
2720
2731
|
if (err) reject(err);
|
|
2721
2732
|
else resolve();
|
|
2722
|
-
}
|
|
2733
|
+
})
|
|
2723
2734
|
);
|
|
2724
2735
|
});
|
|
2725
2736
|
|
|
@@ -2750,10 +2761,10 @@ class PublisherModule {
|
|
|
2750
2761
|
|
|
2751
2762
|
// Clear existing permissions
|
|
2752
2763
|
await new Promise((resolve, reject) => {
|
|
2753
|
-
this.db.run('DELETE FROM user_website_permissions WHERE user_id = ?', [user_id], (err) => {
|
|
2764
|
+
this.db.run('DELETE FROM user_website_permissions WHERE user_id = ?', [user_id], guarded(reject, (err) => {
|
|
2754
2765
|
if (err) reject(err);
|
|
2755
2766
|
else resolve();
|
|
2756
|
-
});
|
|
2767
|
+
}));
|
|
2757
2768
|
});
|
|
2758
2769
|
|
|
2759
2770
|
// Add new permissions
|
|
@@ -2766,10 +2777,10 @@ class PublisherModule {
|
|
|
2766
2777
|
this.db.run(
|
|
2767
2778
|
'INSERT INTO user_website_permissions (user_id, website_id, can_queue, can_approve) VALUES (?, ?, ?, ?)',
|
|
2768
2779
|
[user_id, website.id, canQueue ? 1 : 0, canApprove ? 1 : 0],
|
|
2769
|
-
(err) => {
|
|
2780
|
+
guarded(reject, (err) => {
|
|
2770
2781
|
if (err) reject(err);
|
|
2771
2782
|
else resolve();
|
|
2772
|
-
}
|
|
2783
|
+
})
|
|
2773
2784
|
);
|
|
2774
2785
|
});
|
|
2775
2786
|
}
|
|
@@ -2795,10 +2806,10 @@ class PublisherModule {
|
|
|
2795
2806
|
sql += ' LIMIT ' + limit;
|
|
2796
2807
|
}
|
|
2797
2808
|
|
|
2798
|
-
this.db.all(sql, (err, rows) => {
|
|
2809
|
+
this.db.all(sql, guarded(reject, (err, rows) => {
|
|
2799
2810
|
if (err) reject(err);
|
|
2800
2811
|
else resolve(rows || []);
|
|
2801
|
-
});
|
|
2812
|
+
}));
|
|
2802
2813
|
});
|
|
2803
2814
|
}
|
|
2804
2815
|
|
|
@@ -2807,10 +2818,10 @@ class PublisherModule {
|
|
|
2807
2818
|
this.db.get(
|
|
2808
2819
|
'SELECT t.*, u.name as user_name, u.login as user_login, w.name as website_name, approver.name as approved_by_name FROM tasks t JOIN users u ON t.user_id = u.id JOIN websites w ON t.website_id = w.id LEFT JOIN users approver ON t.approved_by = approver.id WHERE t.id = ?',
|
|
2809
2820
|
[taskId],
|
|
2810
|
-
(err, row) => {
|
|
2821
|
+
guarded(reject, (err, row) => {
|
|
2811
2822
|
if (err) reject(err);
|
|
2812
2823
|
else resolve(row);
|
|
2813
|
-
}
|
|
2824
|
+
})
|
|
2814
2825
|
);
|
|
2815
2826
|
});
|
|
2816
2827
|
}
|
|
@@ -2820,10 +2831,10 @@ class PublisherModule {
|
|
|
2820
2831
|
this.db.all(
|
|
2821
2832
|
'SELECT * FROM task_logs WHERE task_id = ? ORDER BY timestamp ASC',
|
|
2822
2833
|
[taskId.toString()],
|
|
2823
|
-
(err, rows) => {
|
|
2834
|
+
guarded(reject, (err, rows) => {
|
|
2824
2835
|
if (err) reject(err);
|
|
2825
2836
|
else resolve(rows || []);
|
|
2826
|
-
}
|
|
2837
|
+
})
|
|
2827
2838
|
);
|
|
2828
2839
|
});
|
|
2829
2840
|
}
|
|
@@ -2833,10 +2844,10 @@ class PublisherModule {
|
|
|
2833
2844
|
this.db.all(
|
|
2834
2845
|
'SELECT ua.*, u.name as user_name, u.login as user_login FROM user_actions ua LEFT JOIN users u ON ua.user_id = u.id WHERE ua.target_id = ? ORDER BY ua.timestamp ASC',
|
|
2835
2846
|
[taskId.toString()],
|
|
2836
|
-
(err, rows) => {
|
|
2847
|
+
guarded(reject, (err, rows) => {
|
|
2837
2848
|
if (err) reject(err);
|
|
2838
2849
|
else resolve(rows || []);
|
|
2839
|
-
}
|
|
2850
|
+
})
|
|
2840
2851
|
);
|
|
2841
2852
|
});
|
|
2842
2853
|
}
|
|
@@ -2846,10 +2857,10 @@ class PublisherModule {
|
|
|
2846
2857
|
this.db.all(
|
|
2847
2858
|
'SELECT w.* FROM websites w JOIN user_website_permissions p ON w.id = p.website_id WHERE p.user_id = ? AND p.can_queue = 1 AND w.is_active = 1',
|
|
2848
2859
|
[userId],
|
|
2849
|
-
(err, rows) => {
|
|
2860
|
+
guarded(reject, (err, rows) => {
|
|
2850
2861
|
if (err) reject(err);
|
|
2851
2862
|
else resolve(rows || []);
|
|
2852
|
-
}
|
|
2863
|
+
})
|
|
2853
2864
|
);
|
|
2854
2865
|
});
|
|
2855
2866
|
}
|
|
@@ -2859,10 +2870,10 @@ class PublisherModule {
|
|
|
2859
2870
|
this.db.get(
|
|
2860
2871
|
'SELECT can_queue FROM user_website_permissions WHERE user_id = ? AND website_id = ?',
|
|
2861
2872
|
[userId, websiteId],
|
|
2862
|
-
(err, row) => {
|
|
2873
|
+
guarded(reject, (err, row) => {
|
|
2863
2874
|
if (err) reject(err);
|
|
2864
2875
|
else resolve(row && row.can_queue);
|
|
2865
|
-
}
|
|
2876
|
+
})
|
|
2866
2877
|
);
|
|
2867
2878
|
});
|
|
2868
2879
|
}
|
|
@@ -2872,10 +2883,10 @@ class PublisherModule {
|
|
|
2872
2883
|
this.db.get(
|
|
2873
2884
|
'SELECT can_approve FROM user_website_permissions WHERE user_id = ? AND website_id = ?',
|
|
2874
2885
|
[userId, websiteId],
|
|
2875
|
-
(err, row) => {
|
|
2886
|
+
guarded(reject, (err, row) => {
|
|
2876
2887
|
if (err) reject(err);
|
|
2877
2888
|
else resolve(row && row.can_approve);
|
|
2878
|
-
}
|
|
2889
|
+
})
|
|
2879
2890
|
);
|
|
2880
2891
|
});
|
|
2881
2892
|
}
|
|
@@ -2885,10 +2896,10 @@ class PublisherModule {
|
|
|
2885
2896
|
this.db.get(
|
|
2886
2897
|
'SELECT * FROM tasks WHERE npm_package_id = ? AND version = ? AND status NOT IN (?, ?) ORDER BY queued_at DESC LIMIT 1',
|
|
2887
2898
|
[packageId, version, 'complete', 'failed'],
|
|
2888
|
-
(err, row) => {
|
|
2899
|
+
guarded(reject, (err, row) => {
|
|
2889
2900
|
if (err) reject(err);
|
|
2890
2901
|
else resolve(row);
|
|
2891
|
-
}
|
|
2902
|
+
})
|
|
2892
2903
|
);
|
|
2893
2904
|
});
|
|
2894
2905
|
}
|
|
@@ -2898,29 +2909,29 @@ class PublisherModule {
|
|
|
2898
2909
|
this.db.get(
|
|
2899
2910
|
'SELECT * FROM tasks WHERE npm_package_id = ? AND version = ? ORDER BY queued_at DESC LIMIT 1',
|
|
2900
2911
|
[packageId, version],
|
|
2901
|
-
(err, row) => {
|
|
2912
|
+
guarded(reject, (err, row) => {
|
|
2902
2913
|
if (err) reject(err);
|
|
2903
2914
|
else resolve(row);
|
|
2904
|
-
}
|
|
2915
|
+
})
|
|
2905
2916
|
);
|
|
2906
2917
|
});
|
|
2907
2918
|
}
|
|
2908
2919
|
|
|
2909
2920
|
async getWebsites() {
|
|
2910
2921
|
return new Promise((resolve, reject) => {
|
|
2911
|
-
this.db.all('SELECT * FROM websites ORDER BY name', (err, rows) => {
|
|
2922
|
+
this.db.all('SELECT * FROM websites ORDER BY name', guarded(reject, (err, rows) => {
|
|
2912
2923
|
if (err) reject(err);
|
|
2913
2924
|
else resolve(rows || []);
|
|
2914
|
-
});
|
|
2925
|
+
}));
|
|
2915
2926
|
});
|
|
2916
2927
|
}
|
|
2917
2928
|
|
|
2918
2929
|
async getWebsite(websiteId) {
|
|
2919
2930
|
return new Promise((resolve, reject) => {
|
|
2920
|
-
this.db.get('SELECT * FROM websites WHERE id = ?', [websiteId], (err, row) => {
|
|
2931
|
+
this.db.get('SELECT * FROM websites WHERE id = ?', [websiteId], guarded(reject, (err, row) => {
|
|
2921
2932
|
if (err) reject(err);
|
|
2922
2933
|
else resolve(row);
|
|
2923
|
-
});
|
|
2934
|
+
}));
|
|
2924
2935
|
});
|
|
2925
2936
|
}
|
|
2926
2937
|
|
|
@@ -2967,10 +2978,10 @@ class PublisherModule {
|
|
|
2967
2978
|
|
|
2968
2979
|
async getUsers() {
|
|
2969
2980
|
return new Promise((resolve, reject) => {
|
|
2970
|
-
this.db.all('SELECT id, name, login, is_admin, created_at FROM users ORDER BY name', (err, rows) => {
|
|
2981
|
+
this.db.all('SELECT id, name, login, is_admin, created_at FROM users ORDER BY name', guarded(reject, (err, rows) => {
|
|
2971
2982
|
if (err) reject(err);
|
|
2972
2983
|
else resolve(rows || []);
|
|
2973
|
-
});
|
|
2984
|
+
}));
|
|
2974
2985
|
});
|
|
2975
2986
|
}
|
|
2976
2987
|
|
|
@@ -2979,10 +2990,10 @@ class PublisherModule {
|
|
|
2979
2990
|
this.db.all(
|
|
2980
2991
|
'SELECT * FROM user_website_permissions WHERE user_id = ?',
|
|
2981
2992
|
[userId],
|
|
2982
|
-
(err, rows) => {
|
|
2993
|
+
guarded(reject, (err, rows) => {
|
|
2983
2994
|
if (err) reject(err);
|
|
2984
2995
|
else resolve(rows || []);
|
|
2985
|
-
}
|
|
2996
|
+
})
|
|
2986
2997
|
);
|
|
2987
2998
|
});
|
|
2988
2999
|
}
|
|
@@ -2997,7 +3008,7 @@ class PublisherModule {
|
|
|
2997
3008
|
return 'The database is not open.';
|
|
2998
3009
|
}
|
|
2999
3010
|
const get = (sql, params) => new Promise((resolve, reject) =>
|
|
3000
|
-
this.db.get(sql, params, (err, row) => err ? reject(err) : resolve(row)));
|
|
3011
|
+
this.db.get(sql, params, guarded(reject, (err, row) => err ? reject(err) : resolve(row))));
|
|
3001
3012
|
// the timestamps are CURRENT_TIMESTAMP: UTC, 'YYYY-MM-DD HH:MM:SS'
|
|
3002
3013
|
const utc = ms => new Date(ms).toISOString().replace('T', ' ').substring(0, 19);
|
|
3003
3014
|
try {
|
|
@@ -3039,15 +3050,15 @@ class PublisherModule {
|
|
|
3039
3050
|
|
|
3040
3051
|
// Close database
|
|
3041
3052
|
if (this.db) {
|
|
3042
|
-
return new Promise((resolve) => {
|
|
3043
|
-
this.db.close((err) => {
|
|
3053
|
+
return new Promise((resolve, reject) => {
|
|
3054
|
+
this.db.close(guarded(reject, (err) => {
|
|
3044
3055
|
if (err) {
|
|
3045
3056
|
this.logger.error('Error closing database:', err);
|
|
3046
3057
|
} else {
|
|
3047
3058
|
this.logger.info('Database closed');
|
|
3048
3059
|
}
|
|
3049
3060
|
resolve();
|
|
3050
|
-
});
|
|
3061
|
+
}));
|
|
3051
3062
|
});
|
|
3052
3063
|
}
|
|
3053
3064
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Publisher - publishing FHIR implementation guides
|
|
2
|
+
|
|
3
|
+
The `publisher` module runs IG publications for the websites it is set up for: a user asks for
|
|
4
|
+
a publication of an IG (a GitHub repository, branch, package id and version), the module builds
|
|
5
|
+
it with the IG Publisher, and once an approver has looked at the draft, it publishes it into the
|
|
6
|
+
website's web folder and pushes it.
|
|
7
|
+
|
|
8
|
+
This module exists to run the publication service at healthintersections.com.au/publisher; it
|
|
9
|
+
isn't designed for anyone else to run, though nothing stops you. It needs Java, git, SUSHI (npm)
|
|
10
|
+
and a checkout of each website's web folder on the server, and push access to them.
|
|
11
|
+
|
|
12
|
+
## How a publication runs
|
|
13
|
+
|
|
14
|
+
1. **Request**: a logged-in user who may publish to the website creates a task: the website,
|
|
15
|
+
the GitHub org, repository and branch, and the package id and version it should produce.
|
|
16
|
+
The task is `queued`.
|
|
17
|
+
2. **Draft build** (`building`): the module clones the repository into a task folder under
|
|
18
|
+
`workspaceRoot`, downloads the latest IG Publisher, updates SUSHI, runs the IG Publisher,
|
|
19
|
+
and checks that the package id and version are the ones asked for. Then the task is
|
|
20
|
+
`waiting for approval`, with its output, history and QA report on the task's pages.
|
|
21
|
+
3. **Approval**: a user who may approve for the website approves it (or deletes it).
|
|
22
|
+
4. **Publication** (`publishing`): the module clones the supporting repositories (the history
|
|
23
|
+
and web templates, and the IG registry), pulls the web folder, runs the IG Publisher in
|
|
24
|
+
go-publish mode, checks the result is a publication build, moves files too big for GitHub
|
|
25
|
+
out of the web folder (`large-file-archive`), commits and pushes the web folder, then the
|
|
26
|
+
IG registry. With a `github-app`, it then tags the IG source repository and makes a GitHub
|
|
27
|
+
release with a zip of the source - a failure there is only a warning.
|
|
28
|
+
|
|
29
|
+
A task that fails can be retried. Only one task runs at a time.
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
In `config.json`, under `modules`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
"publisher": {
|
|
37
|
+
"enabled": true,
|
|
38
|
+
"database": "publisher.db",
|
|
39
|
+
"sessionSecret": "a-long-random-string",
|
|
40
|
+
"adminPassword": "a-good-password",
|
|
41
|
+
"workspaceRoot": "workspaces"
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Setting | Default | Description |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `enabled` | false | Run the module, at `/publisher` |
|
|
48
|
+
| `database` | - | The SQLite database (users, websites, permissions, tasks). A relative path is in the data directory's `publisher` folder |
|
|
49
|
+
| `sessionSecret` | random | Signs the login session cookie. Set it: without it, a random one is made at startup (with a warning), and logins don't survive a restart |
|
|
50
|
+
| `adminPassword` | random | The password of the administrator, `admin` (see [Administration](#administration)) |
|
|
51
|
+
| `cookieSecure` | true | The session cookie is only sent over https. Set `false` only to run over plain http. Behind a TLS proxy, the proxy must set `X-Forwarded-Proto`, or logins seem to do nothing |
|
|
52
|
+
| `workspaceRoot` | - | Where task folders (`task-{id}`) are made. A relative path is in the data directory's `publisher` folder |
|
|
53
|
+
| `igPublisherTimeoutMinutes` | 60 | An IG Publisher run that takes longer is stopped, and the task fails |
|
|
54
|
+
| `pollInterval` | 5000 | How often (ms) the module looks for a task to run |
|
|
55
|
+
| `loginAttemptsPerWindow` | 20 | Failed logins allowed per IP address per 15 minutes |
|
|
56
|
+
| `taskActionsPerMinute` | 30 | Task actions (create, approve, delete, retry) per IP address per minute |
|
|
57
|
+
| `taskReadsPerMinute` | 300 | Reads of task output, history and QA pages per IP address per minute |
|
|
58
|
+
| `large-file-archive` | none | Where files over GitHub's 100 MB limit are moved from the web folder, for websites whose publish setup says they are on GitHub. A relative path is in the data directory's `publisher` folder. Without it they are left in place, and the push fails |
|
|
59
|
+
| `ig-registry-url` | `git@github.com:FHIR/ig-registry.git` | Where the IG registry is cloned from and pushed to. Set it to an ssh_config Host alias to push with a repository deploy key |
|
|
60
|
+
| `github-app` | none | `app-id`, `private-key` (a .pem file) and `installations` (GitHub org -> installation id) for the GitHub App that tags source repositories and makes releases. Without it, that step is skipped |
|
|
61
|
+
|
|
62
|
+
`config-template.json` has the same settings, with more on each.
|
|
63
|
+
|
|
64
|
+
## Administration
|
|
65
|
+
|
|
66
|
+
The administrator's login is `admin`, and its password is `adminPassword` from the config,
|
|
67
|
+
set at every start - so to change it, change the config and restart. Without `adminPassword`,
|
|
68
|
+
the first start gives the administrator a random password (a UUID) and logs it; set
|
|
69
|
+
`adminPassword` to choose your own. There is no page to change a password.
|
|
70
|
+
|
|
71
|
+
Administrators manage (at `/publisher/admin/...`):
|
|
72
|
+
|
|
73
|
+
- **Websites**: each has a name, its local web folder (a git checkout), the history and web
|
|
74
|
+
templates, and a server update script.
|
|
75
|
+
- **Users**, and what each may do for each website: publish (create tasks) and approve.
|