fhirsmith 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +113 -0
- package/README.md +80 -13
- package/config-template.json +106 -1
- package/ips/ai-client.js +263 -0
- package/ips/composer.js +805 -0
- package/ips/editor-schema.js +289 -0
- package/ips/editor.js +290 -0
- package/ips/fhirpath-ips.js +397 -0
- package/ips/ips-codeable-concepts.md +1742 -0
- package/ips/ips-template.html +125 -0
- package/ips/ips-valuesets.js +159 -0
- package/ips/ips.js +1736 -0
- package/ips/membership.js +179 -0
- package/ips/pipeline.js +1012 -0
- package/ips/presenter.js +974 -0
- package/ips/profile-editor.js +578 -0
- package/ips/profiles.js +506 -0
- package/ips/readme.md +460 -0
- package/ips/render.js +1482 -0
- package/ips/search-params.js +83 -0
- package/ips/search.js +667 -0
- package/ips/share.js +178 -0
- package/ips/store.js +650 -0
- package/ips/terminology.js +395 -0
- package/ips/tx-client.js +202 -0
- package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
- package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
- package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
- package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
- package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
- package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
- package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
- package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
- package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
- package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
- package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
- package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
- package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-medication.json +50 -0
- package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
- package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
- package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
- package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
- package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
- package/ips/valuesets/ValueSet-ips-route.json +46 -0
- package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
- package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
- package/ips/valuesets/elements.json +44 -0
- package/library/daily-report.js +432 -0
- package/library/fhir-search-values.js +138 -0
- package/library/html-server.js +32 -23
- package/library/mailer.js +77 -0
- package/library/narrative-check.js +21 -2
- package/library/npm-audit.js +25 -0
- package/library/package-manager.js +4 -2
- package/library/sqlite-session-store.js +29 -1
- package/package.json +8 -3
- package/packages/packages.js +34 -3
- package/publisher/publisher.js +29 -0
- package/registry/auth-store.js +366 -0
- package/registry/auth.js +872 -0
- package/registry/crawler.js +41 -1
- package/registry/documentation/api.md +162 -0
- package/registry/documentation/configuration.md +93 -0
- package/registry/documentation/documents.md +74 -0
- package/registry/documentation/internals.md +151 -0
- package/registry/documentation/readme.md +100 -0
- package/registry/documentation/security.md +199 -0
- package/registry/documents.js +309 -0
- package/registry/openapi.yaml +30 -0
- package/registry/registry-template.html +1 -1
- package/registry/registry.js +230 -44
- package/registry/uptime.js +154 -0
- package/server.js +105 -44
- package/shl/shl.js +64 -9
- package/stats-db.js +93 -0
- package/stats.js +158 -1
- package/testing/search.js +2 -108
- package/testing/store.js +5 -0
- package/testing/testing.js +20 -0
- package/token/token-template.html +5 -4
- package/token/token.js +17 -10
- package/translations/Messages.properties +10 -0
- package/tx/README.md +52 -0
- package/tx/closure/closure-store.js +14 -0
- package/tx/cs/cs-api.js +92 -14
- package/tx/cs/cs-cs.js +64 -3
- package/tx/cs/cs-icd11.js +366 -112
- package/tx/cs/cs-icf.js +431 -0
- package/tx/cs/cs-loinc.js +296 -27
- package/tx/cs/cs-omop.js +10 -3
- package/tx/cs/cs-snomed.js +1210 -80
- package/tx/cs/cs-ucum.js +6 -0
- package/tx/cs/icd11-expressions.js +191 -78
- package/tx/importers/csv-records.js +35 -0
- package/tx/importers/export-icd11-codesystem.js +58 -12
- package/tx/importers/icd11-schema.md +9 -5
- package/tx/importers/import-loinc.module.js +177 -41
- package/tx/importers/import-sct.module.js +66 -1
- package/tx/importers/subset-loinc.module.js +25 -6
- package/tx/library/codesystem.js +4 -1
- package/tx/library/designations.js +23 -30
- package/tx/library.js +98 -4
- package/tx/openapi-operations.js +6 -1
- package/tx/params.js +8 -1
- package/tx/provider.js +24 -6
- package/tx/sct/documentation.md +251 -0
- package/tx/sct/ecl.js +758 -46
- package/tx/sct/expressions.js +113 -52
- package/tx/sct/structures.js +8 -2
- package/tx/syndication/syndication-feed.js +218 -0
- package/tx/syndication/syndication-store.js +415 -0
- package/tx/tx-html.js +15 -1
- package/tx/tx.fhir.org.yml +3 -4
- package/tx/tx.js +144 -2
- package/tx/vs/vs-vsac.js +25 -6
- package/tx/workers/closure.js +20 -0
- package/tx/workers/expand.js +35 -8
- package/tx/workers/lookup.js +61 -6
- package/tx/workers/subsumes.js +1 -1
- package/tx/workers/translate.js +166 -15
- package/tx/workers/validate.js +138 -6
- package/utilities/make-sct-loinc-subset.js +250 -0
- package/utilities/make-sct-test-extension.js +264 -0
- package/xig/xig.js +32 -0
- package/registry/readme.md +0 -124
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// library/mailer.js
|
|
2
|
+
// Outbound email for the server, over SMTP (nodemailer). One of these is made from the
|
|
3
|
+
// top-level "email" block of config.json, and anything that wants to send mail asks it
|
|
4
|
+
// whether it's configured first - a server with no "email" block simply doesn't send.
|
|
5
|
+
//
|
|
6
|
+
// "email": {
|
|
7
|
+
// "host": "smtp.example.org",
|
|
8
|
+
// "port": 587, // default 587; 465 implies secure
|
|
9
|
+
// "secure": false, // TLS from the start (true for 465), else STARTTLS
|
|
10
|
+
// "user": "...", "password": "...", // optional - leave out for an open relay
|
|
11
|
+
// "from": "FHIRsmith <fhirsmith@example.org>"
|
|
12
|
+
// }
|
|
13
|
+
//
|
|
14
|
+
// nodemailer is only loaded when a message is actually sent, so a server that doesn't
|
|
15
|
+
// send mail doesn't need it installed.
|
|
16
|
+
|
|
17
|
+
class Mailer {
|
|
18
|
+
|
|
19
|
+
constructor(config, log) {
|
|
20
|
+
this.config = config || {};
|
|
21
|
+
this.log = log || console;
|
|
22
|
+
this.transport = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether enough has been configured to send: a host to send through and an
|
|
27
|
+
* address to send from.
|
|
28
|
+
*/
|
|
29
|
+
configured() {
|
|
30
|
+
return Boolean(this.config.host && this.config.from);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_transport() {
|
|
34
|
+
if (!this.transport) {
|
|
35
|
+
const nodemailer = require('nodemailer');
|
|
36
|
+
const port = this.config.port || 587;
|
|
37
|
+
const options = {
|
|
38
|
+
host: this.config.host,
|
|
39
|
+
port: port,
|
|
40
|
+
secure: this.config.secure ?? (port === 465)
|
|
41
|
+
};
|
|
42
|
+
if (this.config.user) {
|
|
43
|
+
options.auth = {user: this.config.user, pass: this.config.password};
|
|
44
|
+
}
|
|
45
|
+
this.transport = nodemailer.createTransport(options);
|
|
46
|
+
}
|
|
47
|
+
return this.transport;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Send one message. `to` is an address or a list of them; text is the body.
|
|
52
|
+
* Resolves with nodemailer's result, or throws - callers decide what a
|
|
53
|
+
* failure means to them.
|
|
54
|
+
*/
|
|
55
|
+
async send({to, subject, text, html}) {
|
|
56
|
+
if (!this.configured()) {
|
|
57
|
+
throw new Error('Email is not configured (email.host and email.from are needed)');
|
|
58
|
+
}
|
|
59
|
+
if (!to || (Array.isArray(to) && to.length === 0)) {
|
|
60
|
+
throw new Error('No recipient given');
|
|
61
|
+
}
|
|
62
|
+
const message = {
|
|
63
|
+
from: this.config.from,
|
|
64
|
+
to: Array.isArray(to) ? to.join(', ') : to,
|
|
65
|
+
subject: subject,
|
|
66
|
+
text: text
|
|
67
|
+
};
|
|
68
|
+
if (html) {
|
|
69
|
+
message.html = html;
|
|
70
|
+
}
|
|
71
|
+
const result = await this._transport().sendMail(message);
|
|
72
|
+
this.log.info(`Email sent to ${message.to}: ${subject}`);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = Mailer;
|
|
@@ -61,10 +61,29 @@ function urlsIn(name, value) {
|
|
|
61
61
|
return [value];
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// CSS comments removed by a single pass: the obvious regex, /\/\*[\s\S]*?\*\//g, rescans to
|
|
65
|
+
// the end of the string for every unterminated '/*', which is quadratic - a style attribute
|
|
66
|
+
// of a few hundred KB of '/* ' held the event loop for seconds
|
|
67
|
+
function stripCssComments(css) {
|
|
68
|
+
let out = '';
|
|
69
|
+
let i = 0;
|
|
70
|
+
for (;;) {
|
|
71
|
+
const start = css.indexOf('/*', i);
|
|
72
|
+
if (start < 0) {
|
|
73
|
+
return out + css.substring(i);
|
|
74
|
+
}
|
|
75
|
+
out += css.substring(i, start);
|
|
76
|
+
const end = css.indexOf('*/', start + 2);
|
|
77
|
+
if (end < 0) {
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
i = end + 2;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
64
84
|
// CSS with comments removed and escapes decoded, so \65 xpression( is seen as expression(
|
|
65
85
|
function normaliseCss(css) {
|
|
66
|
-
return String(css)
|
|
67
|
-
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
86
|
+
return stripCssComments(String(css))
|
|
68
87
|
.replace(/\\([0-9a-fA-F]{1,6})\s?/g, (_, hex) => {
|
|
69
88
|
const cp = parseInt(hex, 16);
|
|
70
89
|
return cp > 0 && cp <= 0x10ffff ? String.fromCodePoint(cp) : '';
|
package/library/npm-audit.js
CHANGED
|
@@ -371,6 +371,31 @@ class NpmAudit {
|
|
|
371
371
|
return this.findings.some(f => f.severity === 'critical' || f.severity === 'high');
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
/**
|
|
375
|
+
* For the daily report: plain text, and only when there's something to act
|
|
376
|
+
* on - packages with known vulnerabilities (and what to do about them), a
|
|
377
|
+
* check that couldn't be run, or a newer FHIRsmith release. '' otherwise.
|
|
378
|
+
*/
|
|
379
|
+
reportText() {
|
|
380
|
+
if (!this.enabled) {
|
|
381
|
+
return '';
|
|
382
|
+
}
|
|
383
|
+
const sentences = [];
|
|
384
|
+
if (this.findings.length > 0) {
|
|
385
|
+
const list = this.findings.map(f => `${f.name} ${f.installed.join('/')} (${f.severity}: ${f.range})`).join(', ');
|
|
386
|
+
sentences.push(`npm audit found ${this.summary()} in the packages this server uses: ${list}. ${this.advice()}`);
|
|
387
|
+
}
|
|
388
|
+
if (this.lastError) {
|
|
389
|
+
sentences.push(this.checkedAt
|
|
390
|
+
? `The latest npm audit check failed: ${this.lastError}.`
|
|
391
|
+
: `The npm audit check could not be run: ${this.lastError}.`);
|
|
392
|
+
}
|
|
393
|
+
if (this.findings.length === 0 && this.installedVersion && compareVersions(this.installedVersion, this.latestVersion) < 0) {
|
|
394
|
+
sentences.push(`FHIRsmith ${this.latestVersion} is available (this server is running ${this.installedVersion}).`);
|
|
395
|
+
}
|
|
396
|
+
return sentences.join(' ');
|
|
397
|
+
}
|
|
398
|
+
|
|
374
399
|
/**
|
|
375
400
|
* Home page: nothing unless there is something to report
|
|
376
401
|
*/
|
|
@@ -520,8 +520,10 @@ class PackageManager {
|
|
|
520
520
|
const packagePath = path.join(this.cacheFolder, packageName);
|
|
521
521
|
|
|
522
522
|
try {
|
|
523
|
-
|
|
524
|
-
|
|
523
|
+
// the folder alone is not enough: a download that failed after the folder was made
|
|
524
|
+
// leaves an empty one, which must not count as the package
|
|
525
|
+
const stats = await fs.stat(path.join(packagePath, 'package', 'package.json'));
|
|
526
|
+
if (stats.isFile()) {
|
|
525
527
|
return packageName;
|
|
526
528
|
}
|
|
527
529
|
} catch (error) {
|
|
@@ -130,4 +130,32 @@ class SqliteSessionStore extends session.Store {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
/**
|
|
134
|
+
* The callback API this store uses (run, get), over a better-sqlite3 database - for the
|
|
135
|
+
* modules that keep their data with better-sqlite3 rather than sqlite3.
|
|
136
|
+
* @param {object} db - an open better-sqlite3 Database
|
|
137
|
+
*/
|
|
138
|
+
function betterSqlite(db) {
|
|
139
|
+
return {
|
|
140
|
+
run(sql, params, cb) {
|
|
141
|
+
let info;
|
|
142
|
+
try {
|
|
143
|
+
info = db.prepare(sql).run(...params);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
return cb(e);
|
|
146
|
+
}
|
|
147
|
+
cb.call(info, null);
|
|
148
|
+
},
|
|
149
|
+
get(sql, params, cb) {
|
|
150
|
+
let row;
|
|
151
|
+
try {
|
|
152
|
+
row = db.prepare(sql).get(...params);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
return cb(e);
|
|
155
|
+
}
|
|
156
|
+
cb(null, row);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { SqliteSessionStore, betterSqlite };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fhirsmith",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"txVersion": "1.
|
|
3
|
+
"version": "0.18.1",
|
|
4
|
+
"txVersion": "1.10.0",
|
|
5
5
|
"description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem",
|
|
6
6
|
"main": "server.js",
|
|
7
7
|
"engines": {
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"test:xig": "jest --testPathPatterns=xig",
|
|
20
20
|
"test:packages": "jest --testPathPatterns=packages",
|
|
21
21
|
"test:token": "jest --testPathPatterns=token",
|
|
22
|
+
"test:testing": "jest --testPathPatterns=tests/testing",
|
|
23
|
+
"test:ips": "jest --testPathPatterns=tests/ips",
|
|
22
24
|
"test:integration": "jest --testPathPatterns=integration",
|
|
23
25
|
"test:unit": "jest --testPathPatterns=unit",
|
|
24
26
|
"lint": "eslint . --ext .js --ignore-path .gitignore",
|
|
@@ -46,15 +48,17 @@
|
|
|
46
48
|
"express-rate-limit": "^7.4.1",
|
|
47
49
|
"express-session": "^1.19.0",
|
|
48
50
|
"fast-xml-parser": "^5.3.4",
|
|
49
|
-
"fhir-validator-wrapper": "1.
|
|
51
|
+
"fhir-validator-wrapper": "1.5.0",
|
|
50
52
|
"fhirpath": "^5.2.1",
|
|
51
53
|
"fs-extra": "^11.3.3",
|
|
52
54
|
"ini": "^6.0.0",
|
|
53
55
|
"inquirer": "^8.2.5",
|
|
56
|
+
"jose": "^5.10.0",
|
|
54
57
|
"liquidjs": "^10.25.5",
|
|
55
58
|
"lusca": "^1.7.0",
|
|
56
59
|
"natural": "^8.1.1",
|
|
57
60
|
"node-cron": "^4.6.0",
|
|
61
|
+
"nodemailer": "^10.2.0",
|
|
58
62
|
"pako": "^2.1.0",
|
|
59
63
|
"parse5": "^7.3.0",
|
|
60
64
|
"passport": "^0.7.0",
|
|
@@ -62,6 +66,7 @@
|
|
|
62
66
|
"passport-github2": "^0.1.12",
|
|
63
67
|
"passport-google-oauth20": "^2.0.0",
|
|
64
68
|
"properties-file": "^3.6.4",
|
|
69
|
+
"qrcode-generator": "^2.0.4",
|
|
65
70
|
"re2js": "^2.8.0",
|
|
66
71
|
"rimraf": "^5.0.10",
|
|
67
72
|
"sqlite3": "^6.0.1",
|
package/packages/packages.js
CHANGED
|
@@ -1282,11 +1282,15 @@ class PackagesModule {
|
|
|
1282
1282
|
|
|
1283
1283
|
next();
|
|
1284
1284
|
pckLog.info(`/download/${id}/${version}`);
|
|
1285
|
-
}
|
|
1285
|
+
} catch (error) {
|
|
1286
1286
|
this.stats.countRequest('version', Date.now() - start);
|
|
1287
|
+
throw error;
|
|
1287
1288
|
}
|
|
1288
1289
|
}, async (req, res) => {
|
|
1289
1290
|
const start = Date.now();
|
|
1291
|
+
// counted as 'download' (the tarball) or 'version' (its JSON document) - the
|
|
1292
|
+
// daily report counts the downloads as packages served
|
|
1293
|
+
let name = 'version';
|
|
1290
1294
|
try {
|
|
1291
1295
|
try {
|
|
1292
1296
|
const {id, version} = req.params;
|
|
@@ -1297,6 +1301,7 @@ class PackagesModule {
|
|
|
1297
1301
|
await this.serveVersionDocument(req.secure, id, version, req, res);
|
|
1298
1302
|
pckLog.info(`/version/${id}/${version}`);
|
|
1299
1303
|
} else {
|
|
1304
|
+
name = 'download';
|
|
1300
1305
|
await this.serveDownload(req.secure, id, version, res);
|
|
1301
1306
|
pckLog.info(`/download/${id}/${version}`);
|
|
1302
1307
|
}
|
|
@@ -1305,7 +1310,7 @@ class PackagesModule {
|
|
|
1305
1310
|
res.status(500).json({error: 'Internal server error'});
|
|
1306
1311
|
}
|
|
1307
1312
|
} finally {
|
|
1308
|
-
this.stats.countRequest(
|
|
1313
|
+
this.stats.countRequest(name, Date.now() - start);
|
|
1309
1314
|
}
|
|
1310
1315
|
});
|
|
1311
1316
|
|
|
@@ -1322,8 +1327,9 @@ class PackagesModule {
|
|
|
1322
1327
|
|
|
1323
1328
|
next();
|
|
1324
1329
|
pckLog.info(`/page/${page}`);
|
|
1325
|
-
}
|
|
1330
|
+
} catch (error) {
|
|
1326
1331
|
this.stats.countRequest('page', Date.now() - start);
|
|
1332
|
+
throw error;
|
|
1327
1333
|
}
|
|
1328
1334
|
}, async (req, res) => {
|
|
1329
1335
|
const start = Date.now();
|
|
@@ -2805,6 +2811,31 @@ class PackagesModule {
|
|
|
2805
2811
|
return va.pre.localeCompare(vb.pre, undefined, { numeric: true });
|
|
2806
2812
|
}
|
|
2807
2813
|
|
|
2814
|
+
/**
|
|
2815
|
+
* The paragraph for the server's daily report: packages served (tarball
|
|
2816
|
+
* downloads) and package versions newly indexed by the crawler in the period.
|
|
2817
|
+
*/
|
|
2818
|
+
async dailyReport({since, now}) {
|
|
2819
|
+
const {plural} = require('../library/daily-report');
|
|
2820
|
+
const counts = this.stats.countsSince(since, now);
|
|
2821
|
+
const downloads = counts.operations.download || 0;
|
|
2822
|
+
const sentences = [`${plural(downloads, 'package')} ${downloads === 1 ? 'was' : 'were'} served (${plural(counts.total, 'request')} in all).`];
|
|
2823
|
+
if (this.db) {
|
|
2824
|
+
try {
|
|
2825
|
+
const row = await new Promise((resolve, reject) => this.db.get(
|
|
2826
|
+
'SELECT COUNT(*) AS n, COUNT(DISTINCT Id) AS ids FROM PackageVersions WHERE Indexed > ?',
|
|
2827
|
+
[new Date(since).toISOString().replace('T', ' ').substring(0, 19)],
|
|
2828
|
+
(err, row) => err ? reject(err) : resolve(row)));
|
|
2829
|
+
sentences.push(row.n === 0
|
|
2830
|
+
? 'No new package versions were found.'
|
|
2831
|
+
: `${plural(row.n, 'new package version')} ${row.n === 1 ? 'was' : 'were'} found, of ${plural(row.ids, 'package')}.`);
|
|
2832
|
+
} catch (e) {
|
|
2833
|
+
sentences.push(`The new packages could not be counted: ${e.message}.`);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
return sentences.join(' ');
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2808
2839
|
async shutdown() {
|
|
2809
2840
|
pckLog.info('Shutting down Packages module...');
|
|
2810
2841
|
|
package/publisher/publisher.js
CHANGED
|
@@ -2987,6 +2987,35 @@ class PublisherModule {
|
|
|
2987
2987
|
});
|
|
2988
2988
|
}
|
|
2989
2989
|
|
|
2990
|
+
/**
|
|
2991
|
+
* The paragraph for the server's daily report: publication runs started in
|
|
2992
|
+
* the period, and how many finished successfully.
|
|
2993
|
+
*/
|
|
2994
|
+
async dailyReport({since, now}) {
|
|
2995
|
+
const {plural} = require('../library/daily-report');
|
|
2996
|
+
if (!this.db) {
|
|
2997
|
+
return 'The database is not open.';
|
|
2998
|
+
}
|
|
2999
|
+
const get = (sql, params) => new Promise((resolve, reject) =>
|
|
3000
|
+
this.db.get(sql, params, (err, row) => err ? reject(err) : resolve(row)));
|
|
3001
|
+
// the timestamps are CURRENT_TIMESTAMP: UTC, 'YYYY-MM-DD HH:MM:SS'
|
|
3002
|
+
const utc = ms => new Date(ms).toISOString().replace('T', ' ').substring(0, 19);
|
|
3003
|
+
try {
|
|
3004
|
+
const started = await get('SELECT COUNT(*) AS n FROM tasks WHERE building_at > ? AND building_at <= ?', [utc(since), utc(now)]);
|
|
3005
|
+
const finished = await get('SELECT COUNT(*) AS n FROM tasks WHERE completed_at > ? AND completed_at <= ?', [utc(since), utc(now)]);
|
|
3006
|
+
const failed = await get('SELECT COUNT(*) AS n FROM tasks WHERE failed_at > ? AND failed_at <= ?', [utc(since), utc(now)]);
|
|
3007
|
+
const waiting = await get("SELECT COUNT(*) AS n FROM tasks WHERE status IN ('queued', 'waiting for approval')", []);
|
|
3008
|
+
let text = `${plural(started.n, 'publication run')} ${started.n === 1 ? 'was' : 'were'} started, and ${finished.n} finished successfully`;
|
|
3009
|
+
text += failed.n > 0 ? ` (${failed.n} failed).` : '.';
|
|
3010
|
+
if (waiting.n > 0) {
|
|
3011
|
+
text += ` ${plural(waiting.n, 'task')} ${waiting.n === 1 ? 'is' : 'are'} queued or waiting for approval.`;
|
|
3012
|
+
}
|
|
3013
|
+
return text;
|
|
3014
|
+
} catch (e) {
|
|
3015
|
+
return `The database could not be read: ${e.message}.`;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
|
|
2990
3019
|
async shutdown() {
|
|
2991
3020
|
this.logger.info('Shutting down publisher module...');
|
|
2992
3021
|
|