fhirsmith 0.16.1 → 0.18.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/CHANGELOG.md +130 -0
- package/README.md +105 -30
- package/config-template.json +130 -4
- package/extension-tracker/extension-tracker.js +111 -3
- package/extension-tracker/readme.md +3 -0
- 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 +422 -0
- package/library/fhir-search-values.js +138 -0
- package/library/html-server.js +32 -23
- package/library/html.js +9 -0
- package/library/mailer.js +77 -0
- package/library/narrative-check.js +222 -0
- 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 -2
- package/packages/package-crawler.js +189 -82
- package/packages/packages.js +36 -63
- package/packages/utilities/dedupe-versions.js +22 -8
- package/publisher/publisher.js +29 -0
- package/registry/auth-store.js +366 -0
- package/registry/auth.js +872 -0
- package/registry/crawler.js +48 -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 +247 -54
- package/registry/uptime.js +154 -0
- package/release.sh +80 -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/readme.md +1 -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 +9 -0
- package/tx/README.md +52 -0
- package/tx/cm/cm-database.js +62 -0
- package/tx/cm/cm-package.js +20 -3
- package/tx/cs/cs-api.js +90 -14
- package/tx/cs/cs-cs.js +142 -19
- package/tx/cs/cs-icd11.js +366 -112
- package/tx/cs/cs-icf.js +418 -0
- package/tx/cs/cs-loinc.js +79 -39
- package/tx/cs/cs-omop.js +10 -3
- package/tx/cs/cs-snomed.js +918 -55
- package/tx/cs/cs-ucum.js +6 -0
- package/tx/cs/icd11-expressions.js +191 -78
- package/tx/importers/export-icd11-codesystem.js +58 -12
- package/tx/importers/icd11-schema.md +9 -5
- package/tx/library.js +107 -6
- package/tx/ocl/README.md +62 -6
- package/tx/ocl/cm-ocl.cjs +166 -44
- package/tx/ocl/cs-ocl.cjs +163 -13
- package/tx/ocl/resolve/public-access.js +143 -0
- package/tx/ocl/resolve/reference-resolver.js +458 -0
- package/tx/ocl/vs-ocl.cjs +167 -4
- package/tx/openapi-operations.js +4 -0
- package/tx/params.js +8 -1
- package/tx/problems.js +7 -5
- package/tx/provider.js +21 -5
- package/tx/sct/documentation.md +251 -0
- package/tx/sct/ecl.js +758 -46
- package/tx/sct/expressions.js +113 -52
- package/tx/syndication/syndication-feed.js +218 -0
- package/tx/syndication/syndication-store.js +415 -0
- package/tx/tx-html.js +37 -8
- package/tx/tx.fhir.org.yml +2 -3
- package/tx/tx.js +148 -5
- package/tx/vs/vs-database.js +26 -0
- package/tx/vs/vs-package.js +42 -10
- package/tx/vs/vs-vsac.js +25 -6
- package/tx/workers/expand.js +34 -7
- package/tx/workers/lookup.js +61 -6
- package/tx/workers/metadata.js +6 -1
- package/tx/workers/subsumes.js +1 -1
- package/tx/workers/translate.js +166 -15
- package/tx/workers/validate.js +128 -6
- package/utilities/make-sct-test-extension.js +264 -0
- package/utilities/submit-test-report.js +1 -1
- package/xig/xig.js +120 -12
- package/gr.sh +0 -3
- package/registry/readme.md +0 -124
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// registry/uptime.js
|
|
2
|
+
// Uptime history for the registered servers: one record per endpoint per scan, saying
|
|
3
|
+
// whether the endpoint answered. The registry's own scan is the probe - nothing else is
|
|
4
|
+
// pinged - so a scan that didn't happen (the registry was down, or the master list could
|
|
5
|
+
// not be read) is not held against any server.
|
|
6
|
+
|
|
7
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
class UptimeHistory {
|
|
10
|
+
/**
|
|
11
|
+
* @param {object} [options]
|
|
12
|
+
* @param {number} [options.retentionDays] how long records are kept (default 90)
|
|
13
|
+
* @param {number} [options.windowDays] the period uptime is reported over (default 30)
|
|
14
|
+
*/
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.retentionMs = (options.retentionDays || 90) * DAY;
|
|
17
|
+
this.windowMs = (options.windowDays || 30) * DAY;
|
|
18
|
+
this.records = new Map(); // address -> [[timeMs, ok(1|0)], ...] in time order
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Record the outcome of one probe of an endpoint
|
|
23
|
+
* @param {string} address the endpoint URL
|
|
24
|
+
* @param {boolean} ok whether the scan of it succeeded
|
|
25
|
+
* @param {number} [time] when the scan ran (ms)
|
|
26
|
+
*/
|
|
27
|
+
record(address, ok, time = Date.now()) {
|
|
28
|
+
if (!address) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
let list = this.records.get(address);
|
|
32
|
+
if (!list) {
|
|
33
|
+
list = [];
|
|
34
|
+
this.records.set(address, list);
|
|
35
|
+
}
|
|
36
|
+
list.push([time, ok ? 1 : 0]);
|
|
37
|
+
this.prune(address, time);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
prune(address, now = Date.now()) {
|
|
41
|
+
const list = this.records.get(address);
|
|
42
|
+
if (!list) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const cutoff = now - this.retentionMs;
|
|
46
|
+
let drop = 0;
|
|
47
|
+
while (drop < list.length && list[drop][0] < cutoff) {
|
|
48
|
+
drop++;
|
|
49
|
+
}
|
|
50
|
+
if (drop > 0) {
|
|
51
|
+
list.splice(0, drop);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Uptime over the reporting window for a set of endpoints, as the fraction of their
|
|
57
|
+
* probes that succeeded: a server with two endpoints of which one is down for a scan
|
|
58
|
+
* is half down for that scan.
|
|
59
|
+
* @param {string|string[]} addresses one endpoint, or the endpoints of one server
|
|
60
|
+
* @param {number} [now]
|
|
61
|
+
* @param {number} [from] the start of the period; the reporting window by default
|
|
62
|
+
* @returns {{percent: number, probes: number, failures: number, since: number}|null}
|
|
63
|
+
* null when there have been no probes in the window
|
|
64
|
+
*/
|
|
65
|
+
uptime(addresses, now = Date.now(), from = now - this.windowMs) {
|
|
66
|
+
const list = Array.isArray(addresses) ? addresses : [addresses];
|
|
67
|
+
let probes = 0;
|
|
68
|
+
let failures = 0;
|
|
69
|
+
let since = Infinity;
|
|
70
|
+
for (const address of list) {
|
|
71
|
+
for (const [time, ok] of this.records.get(address) || []) {
|
|
72
|
+
if (time < from || time > now) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
probes++;
|
|
76
|
+
if (!ok) {
|
|
77
|
+
failures++;
|
|
78
|
+
}
|
|
79
|
+
if (time < since) {
|
|
80
|
+
since = time;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (probes === 0) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return { percent: 100 * (probes - failures) / probes, probes, failures, since };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Forget what is past retention. Only age decides: an endpoint missing from one crawl (a
|
|
92
|
+
* sub-registry that failed to fetch, a crawl cut short at shutdown) keeps its history,
|
|
93
|
+
* and one that has really gone ages out with its last probes
|
|
94
|
+
* @param {Iterable<string>} [addresses] the endpoints of the last crawl (no longer used:
|
|
95
|
+
* a crawl can miss endpoints that still exist)
|
|
96
|
+
*/
|
|
97
|
+
retain(addresses, now = Date.now()) {
|
|
98
|
+
for (const address of [...this.records.keys()]) {
|
|
99
|
+
this.prune(address, now);
|
|
100
|
+
if (this.records.get(address).length === 0) {
|
|
101
|
+
this.records.delete(address);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
toJSON() {
|
|
107
|
+
const endpoints = {};
|
|
108
|
+
for (const [address, list] of this.records) {
|
|
109
|
+
if (list.length > 0) {
|
|
110
|
+
endpoints[address] = list.map(([t, ok]) => [t, ok]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { version: '1', endpoints };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static fromJSON(json, options) {
|
|
117
|
+
const history = new UptimeHistory(options);
|
|
118
|
+
if (!json || json.version !== '1' || typeof json.endpoints !== 'object') {
|
|
119
|
+
return history;
|
|
120
|
+
}
|
|
121
|
+
for (const [address, list] of Object.entries(json.endpoints || {})) {
|
|
122
|
+
if (!Array.isArray(list)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const clean = list
|
|
126
|
+
.filter(r => Array.isArray(r) && Number.isFinite(r[0]))
|
|
127
|
+
.map(r => [r[0], r[1] ? 1 : 0])
|
|
128
|
+
.sort((a, b) => a[0] - b[0]);
|
|
129
|
+
if (clean.length > 0) {
|
|
130
|
+
history.records.set(address, clean);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return history;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Format an uptime for display: 100%, 99.9%, 97%, ... - the precision that is honest
|
|
139
|
+
* for the number of probes behind it
|
|
140
|
+
*/
|
|
141
|
+
function formatUptime(u) {
|
|
142
|
+
if (!u) {
|
|
143
|
+
return '';
|
|
144
|
+
}
|
|
145
|
+
if (u.failures === 0) {
|
|
146
|
+
return '100%';
|
|
147
|
+
}
|
|
148
|
+
if (u.percent >= 99) {
|
|
149
|
+
return `${(Math.floor(u.percent * 10) / 10).toFixed(1)}%`;
|
|
150
|
+
}
|
|
151
|
+
return `${Math.floor(u.percent)}%`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = { UptimeHistory, formatUptime };
|
package/release.sh
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Release FHIRsmith: ./release.sh 0.17.0
|
|
4
|
+
#
|
|
5
|
+
# Checks that the release is ready, submits the tx test report to the /testing server, then
|
|
6
|
+
# tags the release and pushes the tag (which starts .github/workflows/release.yml).
|
|
7
|
+
# Stops at the first problem, before anything is tagged.
|
|
8
|
+
#
|
|
9
|
+
# The test report: run the tx tests first (npx jest tests/tx/test-cases.test.js). If the
|
|
10
|
+
# /testing server needs a token, set FHIRSMITH_TESTING_TOKEN.
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
fail() {
|
|
15
|
+
echo "gr.sh: $*" >&2
|
|
16
|
+
exit 1
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
[ $# -eq 1 ] || fail "usage: ./gr.sh <version>, e.g. ./gr.sh 0.17.0"
|
|
20
|
+
VERSION="${1#v}"
|
|
21
|
+
TAG="v$VERSION"
|
|
22
|
+
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "'$1' is not a release version (X.Y.Z)"
|
|
23
|
+
|
|
24
|
+
cd "$(dirname "$0")"
|
|
25
|
+
|
|
26
|
+
# --- git: on main, nothing uncommitted, and exactly what is on GitHub -------------------
|
|
27
|
+
[ "$(git rev-parse --abbrev-ref HEAD)" = "main" ] || fail "not on main"
|
|
28
|
+
[ -z "$(git status --porcelain --untracked-files=no)" ] || fail "there are uncommitted changes"
|
|
29
|
+
git fetch --quiet origin main --tags
|
|
30
|
+
[ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ] \
|
|
31
|
+
|| fail "main is not the same as origin/main - push (or pull) first, so the tag is on a commit GitHub has"
|
|
32
|
+
! git rev-parse -q --verify "refs/tags/$TAG" >/dev/null || fail "tag $TAG already exists"
|
|
33
|
+
|
|
34
|
+
# --- versions: what release.yml's verify-version job checks ----------------------------
|
|
35
|
+
check_version() {
|
|
36
|
+
local what="$1" found="$2"
|
|
37
|
+
[ "$found" = "$VERSION" ] || fail "$what is '$found', not $VERSION (npm version --no-git-tag-version $VERSION && npm install --package-lock-only)"
|
|
38
|
+
}
|
|
39
|
+
check_version "package.json version" "$(node -p "require('./package.json').version")"
|
|
40
|
+
check_version "package-lock.json version" "$(node -p "require('./package-lock.json').version")"
|
|
41
|
+
check_version 'package-lock.json packages[""]' "$(node -p "require('./package-lock.json').packages[''].version")"
|
|
42
|
+
|
|
43
|
+
# --- CHANGELOG: a dated section for this version, with the conformance statement in -----
|
|
44
|
+
HEADING=$(grep -E "^## \[v?${VERSION//./\\.}\]" CHANGELOG.md || true)
|
|
45
|
+
[ -n "$HEADING" ] || fail "CHANGELOG.md has no section for $VERSION"
|
|
46
|
+
[[ "$HEADING" =~ [0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || fail "the CHANGELOG.md section for $VERSION has no date: $HEADING"
|
|
47
|
+
SECTION=$(sed -n "/^## \[v\{0,1\}${VERSION//./\\.}\]/,/^## \[/p" CHANGELOG.md | sed '$d')
|
|
48
|
+
! grep -qx '(paste)' <<<"$SECTION" || fail "the Tx Conformance Statement for $VERSION hasn't been pasted into CHANGELOG.md"
|
|
49
|
+
|
|
50
|
+
# --- the test report: this version, passed, and run on what is being released ----------
|
|
51
|
+
REPORT=test-cases-report.json
|
|
52
|
+
[ -f "$REPORT" ] || fail "no $REPORT - run the tx tests first (npx jest tests/tx/test-cases.test.js)"
|
|
53
|
+
# the last commit that changed what the tests run - not the notes, nor the version bump
|
|
54
|
+
CODE_COMMITTED=$(git log -1 --format=%cI -- . ':(exclude)*.md' ':(exclude)release.sh' ':(exclude)package.json' ':(exclude)package-lock.json')
|
|
55
|
+
node - "$REPORT" "$VERSION" "$CODE_COMMITTED" <<'NODE' || exit 1
|
|
56
|
+
const [file, version, committed] = process.argv.slice(2);
|
|
57
|
+
const report = JSON.parse(require('fs').readFileSync(file, 'utf8'));
|
|
58
|
+
const fhirsmith = (report.participant || []).find(p => p.uri === 'https://github.com/HealthIntersections/FHIRsmith');
|
|
59
|
+
const problems = [];
|
|
60
|
+
if (!fhirsmith || fhirsmith.version !== version) {
|
|
61
|
+
problems.push(`it is from FHIRsmith ${fhirsmith ? fhirsmith.version : '(unknown)'}, not ${version}`);
|
|
62
|
+
}
|
|
63
|
+
if (report.result !== 'pass') {
|
|
64
|
+
problems.push(`its result is '${report.result}', not 'pass'`);
|
|
65
|
+
}
|
|
66
|
+
if (!report.issued || new Date(report.issued) < new Date(committed)) {
|
|
67
|
+
problems.push(`it was run (${report.issued}) before the last change to the code (${committed})`);
|
|
68
|
+
}
|
|
69
|
+
if (problems.length > 0) {
|
|
70
|
+
console.error(`gr.sh: ${file} can't be submitted for ${version}: ${problems.join('; ')} - run the tx tests again`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
NODE
|
|
74
|
+
|
|
75
|
+
# --- submit the report, then tag ---------------------------------------------------------
|
|
76
|
+
node utilities/submit-test-report.js -file "$REPORT" || fail "the test report was not accepted, so $TAG has not been tagged"
|
|
77
|
+
|
|
78
|
+
git tag -a "$TAG" -m "FHIRsmith $VERSION"
|
|
79
|
+
git push origin "$TAG"
|
|
80
|
+
echo "Tagged and pushed $TAG - the release workflow is now running"
|
package/server.js
CHANGED
|
@@ -18,6 +18,8 @@ const escape = require('escape-html');
|
|
|
18
18
|
const { resolveWithin } = require('./library/path-safety');
|
|
19
19
|
const { readCgroupMemoryLimit } = require('./library/cgroup-memory');
|
|
20
20
|
const { NpmAudit } = require('./library/npm-audit');
|
|
21
|
+
const Mailer = require('./library/mailer');
|
|
22
|
+
const DailyReport = require('./library/daily-report');
|
|
21
23
|
|
|
22
24
|
// Load configuration BEFORE logger
|
|
23
25
|
let config;
|
|
@@ -51,7 +53,7 @@ serverLog.info(`Data directory: ${folders.dataDir()}`);
|
|
|
51
53
|
serverLog.info(`========================================`);
|
|
52
54
|
|
|
53
55
|
const activeModules = config.modules ? Object.keys(config.modules)
|
|
54
|
-
.filter(mod => config.modules[mod]
|
|
56
|
+
.filter(mod => config.modules[mod]?.enabled)
|
|
55
57
|
.join(', ') : [];
|
|
56
58
|
serverLog.info(`Loaded Configuration. Active modules = ${activeModules}`);
|
|
57
59
|
|
|
@@ -66,6 +68,7 @@ const TokenModule = require('./token/token.js');
|
|
|
66
68
|
const NpmProjectorModule = require('./npmprojector/npmprojector.js');
|
|
67
69
|
const TXModule = require('./tx/tx.js');
|
|
68
70
|
const TestingModule = require('./testing/testing.js');
|
|
71
|
+
const IpsModule = require('./ips/ips.js');
|
|
69
72
|
|
|
70
73
|
const htmlServer = require('./library/html-server');
|
|
71
74
|
const ServerStats = require("./stats");
|
|
@@ -90,9 +93,15 @@ app.set('trust proxy', config.server.trustProxy ?? DEFAULT_TRUST_PROXY);
|
|
|
90
93
|
const PORT = process.env.PORT || config.server.port || 3000;
|
|
91
94
|
|
|
92
95
|
// Middleware
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
// The body parsers, for every module but those that read their own bodies with their own
|
|
97
|
+
// limits and checks (the ips module: a document is read only once its submitter's token has
|
|
98
|
+
// been checked, and no bigger than the module's maxSize)
|
|
99
|
+
const OWN_BODIES = ['/ips'];
|
|
100
|
+
const ownsBody = (req) => OWN_BODIES.some(p => req.path === p || req.path.startsWith(p + '/'));
|
|
101
|
+
const unlessOwnBody = (parser) => (req, res, next) => (ownsBody(req) ? next() : parser(req, res, next));
|
|
102
|
+
app.use(unlessOwnBody(express.raw({ type: 'application/fhir+json', limit: '50mb' })));
|
|
103
|
+
app.use(unlessOwnBody(express.raw({ type: 'application/fhir+xml', limit: '50mb' })));
|
|
104
|
+
app.use(unlessOwnBody(express.json({ limit: '50mb' })));
|
|
96
105
|
app.use((req, res, next) => {
|
|
97
106
|
const requestId = req.headers['x-request-id'];
|
|
98
107
|
if (requestId) {
|
|
@@ -113,6 +122,8 @@ const modules = {};
|
|
|
113
122
|
let stats = null;
|
|
114
123
|
// periodic npm advisory check of the installed packages - see library/npm-audit.js
|
|
115
124
|
let npmAudit = null;
|
|
125
|
+
// the daily report, emailed at midnight when "email" and "dailyReport.to" are configured - see library/daily-report.js
|
|
126
|
+
let dailyReport = null;
|
|
116
127
|
|
|
117
128
|
// Initialize modules based on configuration
|
|
118
129
|
async function initializeModules() {
|
|
@@ -247,6 +258,22 @@ async function initializeModules() {
|
|
|
247
258
|
throw error;
|
|
248
259
|
}
|
|
249
260
|
}
|
|
261
|
+
if (config.modules?.ips?.enabled) {
|
|
262
|
+
try {
|
|
263
|
+
serverLog.info('Initializing module: ips...');
|
|
264
|
+
// sharing through this server's own shl module: its password, unless the ips
|
|
265
|
+
// configuration names an SHL server of its own
|
|
266
|
+
if (config.modules.ips.shl && !config.modules.ips.shl.password && config.modules.shl?.password) {
|
|
267
|
+
config.modules.ips.shl = { server: '/shl', ...config.modules.ips.shl, password: config.modules.shl.password };
|
|
268
|
+
}
|
|
269
|
+
modules.ips = new IpsModule(stats.forModule('ips'));
|
|
270
|
+
await modules.ips.initialize(config.modules.ips, app);
|
|
271
|
+
app.use('/ips', modules.ips.router);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
serverLog.error('Failed to initialize ips module:', error);
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
250
277
|
|
|
251
278
|
// Initialize TX module
|
|
252
279
|
// Note: TX module registers its own endpoints directly on the app
|
|
@@ -255,7 +282,7 @@ async function initializeModules() {
|
|
|
255
282
|
try {
|
|
256
283
|
serverLog.info('Initializing module: tx...');
|
|
257
284
|
modules.tx = new TXModule(stats.forModule('tx'));
|
|
258
|
-
await modules.tx.initialize(config.modules.tx, app);
|
|
285
|
+
await modules.tx.initialize(config.modules.tx, app, { hostName: config.hostName });
|
|
259
286
|
} catch (error) {
|
|
260
287
|
serverLog.error('Failed to initialize TX module:', error);
|
|
261
288
|
throw error;
|
|
@@ -317,52 +344,52 @@ async function buildRootPageContent() {
|
|
|
317
344
|
content += '<ul class="list-group">';
|
|
318
345
|
|
|
319
346
|
// Check which modules are enabled and add them to the list
|
|
320
|
-
if (config.modules
|
|
347
|
+
if (config.modules?.packages?.enabled) {
|
|
321
348
|
content += '<li class="list-group-item">';
|
|
322
349
|
content += '<a href="/packages" class="text-decoration-none">Package Server</a>: Browse and download FHIR Implementation Guide packages (<a href="/packages/openapi">API</a>)';
|
|
323
350
|
content += '</li>';
|
|
324
351
|
}
|
|
325
352
|
|
|
326
|
-
if (config.modules
|
|
353
|
+
if (config.modules?.xig?.enabled) {
|
|
327
354
|
content += '<li class="list-group-item">';
|
|
328
355
|
content += '<a href="/xig" class="text-decoration-none">FHIR IG Statistics</a>: Statistics and analysis of FHIR Implementation Guides';
|
|
329
356
|
content += '</li>';
|
|
330
357
|
}
|
|
331
358
|
|
|
332
|
-
if (config.modules
|
|
359
|
+
if (config.modules?.shl?.enabled) {
|
|
333
360
|
content += '<li class="list-group-item">';
|
|
334
361
|
content += '<a href="/shl" class="text-decoration-none">SHL Server</a>: SMART Health Links management and validation';
|
|
335
362
|
content += '</li>';
|
|
336
363
|
}
|
|
337
364
|
|
|
338
|
-
if (config.modules
|
|
365
|
+
if (config.modules?.vcl?.enabled) {
|
|
339
366
|
content += '<li class="list-group-item">';
|
|
340
367
|
content += '<a href="/VCL" class="text-decoration-none">VCL Server</a>: ValueSet Compose Language expression parsing';
|
|
341
368
|
content += '</li>';
|
|
342
369
|
}
|
|
343
370
|
|
|
344
|
-
if (config.modules
|
|
371
|
+
if (config.modules?.registry?.enabled) {
|
|
345
372
|
content += '<li class="list-group-item">';
|
|
346
373
|
content += '<a href="/tx-reg" class="text-decoration-none">Terminology Server Registry</a>: ';
|
|
347
374
|
content += 'Discover and query FHIR terminology servers for code system and value set support (<a href="/tx-reg/openapi">API</a>)';
|
|
348
375
|
content += '</li>';
|
|
349
376
|
}
|
|
350
377
|
|
|
351
|
-
if (config.modules
|
|
378
|
+
if (config.modules?.publisher?.enabled) {
|
|
352
379
|
content += '<li class="list-group-item">';
|
|
353
380
|
content += '<a href="/publisher" class="text-decoration-none">FHIR Publisher</a>: ';
|
|
354
381
|
content += 'Manage FHIR Implementation Guide publication tasks and approvals';
|
|
355
382
|
content += '</li>';
|
|
356
383
|
}
|
|
357
384
|
|
|
358
|
-
if (config.modules
|
|
385
|
+
if (config.modules?.token?.enabled) {
|
|
359
386
|
content += '<li class="list-group-item">';
|
|
360
387
|
content += '<a href="/token" class="text-decoration-none">Token Server</a>: ';
|
|
361
388
|
content += 'OAuth authentication and API key management for FHIR services';
|
|
362
389
|
content += '</li>';
|
|
363
390
|
}
|
|
364
391
|
|
|
365
|
-
if (config.modules
|
|
392
|
+
if (config.modules?.npmprojector?.enabled) {
|
|
366
393
|
content += '<li class="list-group-item">';
|
|
367
394
|
content += '<a href="/npmprojector" class="text-decoration-none">NpmProjector</a>: ';
|
|
368
395
|
content += 'Hot-reloading FHIR server with FHIRPath-based search indexes';
|
|
@@ -383,7 +410,14 @@ async function buildRootPageContent() {
|
|
|
383
410
|
content += '</li>';
|
|
384
411
|
}
|
|
385
412
|
|
|
386
|
-
if (config.modules
|
|
413
|
+
if (config.modules?.ips?.enabled) {
|
|
414
|
+
content += '<li class="list-group-item">';
|
|
415
|
+
content += '<a href="/ips" class="text-decoration-none">IPS</a>: ';
|
|
416
|
+
content += 'Collections of International Patient Summary documents, each with its own FHIR endpoint';
|
|
417
|
+
content += '</li>';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (config.modules?.folder?.enabled) {
|
|
387
421
|
content += '<li class="list-group-item">';
|
|
388
422
|
content += '<strong>Cache Folder</strong>: ';
|
|
389
423
|
content += 'Cache Folder for Kindling';
|
|
@@ -400,7 +434,7 @@ async function buildRootPageContent() {
|
|
|
400
434
|
}
|
|
401
435
|
|
|
402
436
|
|
|
403
|
-
if (config.modules
|
|
437
|
+
if (config.modules?.tx?.enabled) {
|
|
404
438
|
content += '<li class="list-group-item">';
|
|
405
439
|
content += '<strong>TX Terminology Server</strong>: ';
|
|
406
440
|
content += 'FHIR terminology services (CodeSystem, ValueSet, ConceptMap)';
|
|
@@ -422,16 +456,7 @@ async function buildRootPageContent() {
|
|
|
422
456
|
|
|
423
457
|
// Calculate uptime
|
|
424
458
|
const uptimeMs = Date.now() - stats.startTime;
|
|
425
|
-
const
|
|
426
|
-
const uptimeDays = Math.floor(uptimeSeconds / 86400);
|
|
427
|
-
const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600);
|
|
428
|
-
const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60);
|
|
429
|
-
const uptimeSecs = uptimeSeconds % 60;
|
|
430
|
-
let uptimeStr = '';
|
|
431
|
-
if (uptimeDays > 0) uptimeStr += `${uptimeDays}d `;
|
|
432
|
-
if (uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeHours}h `;
|
|
433
|
-
if (uptimeMinutes > 0 || uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeMinutes}m `;
|
|
434
|
-
uptimeStr += `${uptimeSecs}s`;
|
|
459
|
+
const uptimeStr = uptimeText(uptimeMs);
|
|
435
460
|
|
|
436
461
|
// Memory usage
|
|
437
462
|
const memUsage = process.memoryUsage();
|
|
@@ -550,7 +575,7 @@ app.get('/', async (req, res) => {
|
|
|
550
575
|
// Build basic stats for root page
|
|
551
576
|
const stats = {
|
|
552
577
|
version: packageJson.version,
|
|
553
|
-
enabledModules: Object.keys(config.modules).filter(m => config.modules[m]
|
|
578
|
+
enabledModules: Object.keys(config.modules).filter(m => config.modules[m]?.enabled).length,
|
|
554
579
|
processingTime: Date.now() - startTime,
|
|
555
580
|
about
|
|
556
581
|
};
|
|
@@ -616,16 +641,7 @@ app.get('/dashboard', async (req, res) => {
|
|
|
616
641
|
const startTime = Date.now();
|
|
617
642
|
// Calculate uptime
|
|
618
643
|
const uptimeMs = Date.now() - stats.startTime;
|
|
619
|
-
const
|
|
620
|
-
const uptimeDays = Math.floor(uptimeSeconds / 86400);
|
|
621
|
-
const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600);
|
|
622
|
-
const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60);
|
|
623
|
-
const uptimeSecs = uptimeSeconds % 60;
|
|
624
|
-
let uptimeStr = '';
|
|
625
|
-
if (uptimeDays > 0) uptimeStr += `${uptimeDays}d `;
|
|
626
|
-
if (uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeHours}h `;
|
|
627
|
-
if (uptimeMinutes > 0 || uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeMinutes}m `;
|
|
628
|
-
uptimeStr += `${uptimeSecs}s`;
|
|
644
|
+
const uptimeStr = uptimeText(uptimeMs);
|
|
629
645
|
|
|
630
646
|
// Memory usage
|
|
631
647
|
const memUsage = process.memoryUsage();
|
|
@@ -674,7 +690,7 @@ app.get('/dashboard', async (req, res) => {
|
|
|
674
690
|
|
|
675
691
|
const pageStats = {
|
|
676
692
|
version: packageJson.version,
|
|
677
|
-
enabledModules: Object.keys(config.modules).filter(m => config.modules[m]
|
|
693
|
+
enabledModules: Object.keys(config.modules).filter(m => config.modules[m]?.enabled).length,
|
|
678
694
|
processingTime: Date.now() - startTime
|
|
679
695
|
};
|
|
680
696
|
|
|
@@ -701,6 +717,30 @@ function formatBytes(bytes) {
|
|
|
701
717
|
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
702
718
|
}
|
|
703
719
|
|
|
720
|
+
/**
|
|
721
|
+
* "3d 4h 5m 6s" for this run, followed by the proportion of time the server has
|
|
722
|
+
* been up over all its recorded runs, when the statistics database is keeping that.
|
|
723
|
+
*/
|
|
724
|
+
function uptimeText(uptimeMs) {
|
|
725
|
+
const uptimeSeconds = Math.floor(uptimeMs / 1000);
|
|
726
|
+
const uptimeDays = Math.floor(uptimeSeconds / 86400);
|
|
727
|
+
const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600);
|
|
728
|
+
const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60);
|
|
729
|
+
const uptimeSecs = uptimeSeconds % 60;
|
|
730
|
+
let uptimeStr = '';
|
|
731
|
+
if (uptimeDays > 0) uptimeStr += `${uptimeDays}d `;
|
|
732
|
+
if (uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeHours}h `;
|
|
733
|
+
if (uptimeMinutes > 0 || uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeMinutes}m `;
|
|
734
|
+
uptimeStr += `${uptimeSecs}s`;
|
|
735
|
+
const overall = stats.overallUptime();
|
|
736
|
+
if (overall) {
|
|
737
|
+
// floor rather than round, so 99.96% doesn't claim to be 100%
|
|
738
|
+
const pct = overall.percent >= 100 ? '100' : (Math.floor(overall.percent * 10) / 10).toFixed(1);
|
|
739
|
+
uptimeStr += ` (${pct}% overall)`;
|
|
740
|
+
}
|
|
741
|
+
return uptimeStr;
|
|
742
|
+
}
|
|
743
|
+
|
|
704
744
|
function pctColor(pct) {
|
|
705
745
|
// Gradient from green (#deffe0) at 0% to red (#ffd3d1) at 100%
|
|
706
746
|
const t = Math.max(0, Math.min(100, pct)) / 100;
|
|
@@ -797,11 +837,21 @@ async function startServer() {
|
|
|
797
837
|
|
|
798
838
|
// Start server
|
|
799
839
|
app.listen(PORT, () => {
|
|
800
|
-
stats.markStarted();
|
|
840
|
+
stats.markStarted(packageJson.version);
|
|
801
841
|
serverLog.info(`=== Server running on http://localhost:${PORT} ===`);
|
|
802
842
|
});
|
|
803
843
|
npmAudit.start();
|
|
804
|
-
|
|
844
|
+
dailyReport = new DailyReport(config.dailyReport, {
|
|
845
|
+
log: Logger.getInstance().child({ module: 'daily-report' }),
|
|
846
|
+
stats: stats,
|
|
847
|
+
modules: modules,
|
|
848
|
+
mailer: new Mailer(config.email, Logger.getInstance().child({ module: 'email' })),
|
|
849
|
+
npmAudit: npmAudit,
|
|
850
|
+
version: packageJson.version,
|
|
851
|
+
serverId: config.hostName
|
|
852
|
+
});
|
|
853
|
+
dailyReport.start();
|
|
854
|
+
if (modules.packages && config.modules?.packages?.enabled) {
|
|
805
855
|
modules.packages.startInitialCrawler();
|
|
806
856
|
}
|
|
807
857
|
// Run usage tracker in background after startup
|
|
@@ -825,12 +875,21 @@ async function startServer() {
|
|
|
825
875
|
}
|
|
826
876
|
}
|
|
827
877
|
|
|
828
|
-
// Graceful shutdown
|
|
829
|
-
|
|
878
|
+
// Graceful shutdown: on Ctrl-C (SIGINT), and on SIGTERM - what systemd, docker stop and
|
|
879
|
+
// kill send - so the modules can save what they hold (usage counts, crawl data) either way
|
|
880
|
+
let shuttingDown = false;
|
|
881
|
+
async function shutdown() {
|
|
882
|
+
if (shuttingDown) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
shuttingDown = true;
|
|
830
886
|
serverLog.info('\nShutting down server...');
|
|
831
887
|
if (npmAudit) {
|
|
832
888
|
npmAudit.stop();
|
|
833
889
|
}
|
|
890
|
+
if (dailyReport) {
|
|
891
|
+
dailyReport.stop();
|
|
892
|
+
}
|
|
834
893
|
|
|
835
894
|
// Shutdown all modules
|
|
836
895
|
for (const [moduleName, moduleInstance] of Object.entries(modules)) {
|
|
@@ -847,7 +906,9 @@ process.on('SIGINT', async () => {
|
|
|
847
906
|
stats.finishStats();
|
|
848
907
|
serverLog.info('Server shutdown complete');
|
|
849
908
|
process.exit(0);
|
|
850
|
-
}
|
|
909
|
+
}
|
|
910
|
+
process.on('SIGINT', () => void shutdown());
|
|
911
|
+
process.on('SIGTERM', () => void shutdown());
|
|
851
912
|
|
|
852
913
|
async function serveFhirsmithHome(req, res) {
|
|
853
914
|
// Check if client wants HTML response
|
|
@@ -868,7 +929,7 @@ async function serveFhirsmithHome(req, res) {
|
|
|
868
929
|
// Build basic stats for root page
|
|
869
930
|
const stats = {
|
|
870
931
|
version: packageJson.version,
|
|
871
|
-
enabledModules: Object.keys(config.modules).filter(m => config.modules[m]
|
|
932
|
+
enabledModules: Object.keys(config.modules).filter(m => config.modules[m]?.enabled).length,
|
|
872
933
|
processingTime: Date.now() - startTime
|
|
873
934
|
};
|
|
874
935
|
|
|
@@ -885,7 +946,7 @@ async function serveFhirsmithHome(req, res) {
|
|
|
885
946
|
// Return JSON response for API clients
|
|
886
947
|
const enabledModules = {};
|
|
887
948
|
Object.keys(config.modules).forEach(moduleName => {
|
|
888
|
-
if (config.modules[moduleName]
|
|
949
|
+
if (config.modules[moduleName]?.enabled) {
|
|
889
950
|
if (moduleName === 'tx') {
|
|
890
951
|
// TX module has multiple endpoints
|
|
891
952
|
enabledModules[moduleName] = {
|