fhirsmith 0.18.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +149 -0
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +132 -59
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +74 -21
- package/ips/presenter.js +1 -1
- package/ips/readme.md +132 -25
- package/ips/render.js +466 -77
- package/ips/store.js +79 -22
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/crawler.js +84 -108
- package/registry/documentation/configuration.md +34 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +5 -4
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +154 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +29 -1
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +148 -14
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +185 -66
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +608 -64
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +57 -58
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +278 -42
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/ecl.js +47 -6
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +166 -13
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +27 -4
- package/tx/vs/vs-vsac.js +648 -100
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- package/tx/html/home-metrics.liquid +0 -225
|
@@ -128,8 +128,6 @@
|
|
|
128
128
|
<div id="stripe"> </div>
|
|
129
129
|
<div class="container"> <!-- container -->
|
|
130
130
|
<div style="background-color: #ad1f2f; padding: 6px; color: white;"> <!-- container -->
|
|
131
|
-
<a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> © HL7.org |
|
|
132
|
-
<a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] |
|
|
133
131
|
<a href="/" style="color: gold"> Server Home</a> |
|
|
134
132
|
<a href="/token" style="color: gold">Token Home</a>
|
|
135
133
|
</div> <!-- /container -->
|
|
@@ -144,7 +142,7 @@
|
|
|
144
142
|
<div class="inner-wrapper">
|
|
145
143
|
<div class="col-9">
|
|
146
144
|
|
|
147
|
-
<h2><img border="0" src="
|
|
145
|
+
<h2><a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" title="FHIRsmith [%ver%]"><img border="0" src="/FHIRsmith32.png" alt="FHIRsmith Logo" style="vertical-align: text-bottom"/></a> Token Server: [%title%] </h2>
|
|
148
146
|
|
|
149
147
|
[%content%]
|
|
150
148
|
|
|
@@ -183,8 +181,7 @@
|
|
|
183
181
|
<div class="container"> <!-- container -->
|
|
184
182
|
<div class="inner-wrapper">
|
|
185
183
|
<p>
|
|
186
|
-
<a href="
|
|
187
|
-
<a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] © HealthIntersections.com.au 2023+ |
|
|
184
|
+
<a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" alt="FHIRsmith Logo" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] © HealthIntersections.com.au 2023+ |
|
|
188
185
|
([%ms%] ms)
|
|
189
186
|
|
|
190
187
|
</p>
|
package/token/token.js
CHANGED
|
@@ -20,6 +20,7 @@ const lusca = require('lusca');
|
|
|
20
20
|
const folders = require('../library/folder-setup');
|
|
21
21
|
const { SqliteSessionStore } = require('../library/sqlite-session-store');
|
|
22
22
|
const escape = require('escape-html');
|
|
23
|
+
const { guarded } = require('../library/sqlite-utilities');
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
const Logger = require('../library/logger');
|
|
@@ -74,7 +75,7 @@ class TokenModule {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
// Create tables
|
|
77
|
-
this.db.serialize(() => {
|
|
78
|
+
this.db.serialize(guarded(reject, () => {
|
|
78
79
|
// Users table - enhanced with OAuth data
|
|
79
80
|
this.db.run(`
|
|
80
81
|
CREATE TABLE IF NOT EXISTS users (
|
|
@@ -146,7 +147,7 @@ class TokenModule {
|
|
|
146
147
|
this.db.run(`CREATE INDEX IF NOT EXISTS idx_usage_date ON usage_stats(date)`);
|
|
147
148
|
this.db.run(`CREATE INDEX IF NOT EXISTS idx_security_log_user ON security_log(user_id)`);
|
|
148
149
|
this.db.run(`CREATE INDEX IF NOT EXISTS idx_security_log_event ON security_log(event_type)`);
|
|
149
|
-
});
|
|
150
|
+
}));
|
|
150
151
|
|
|
151
152
|
this.log.info(`Token database initialized: ${dbPath}`);
|
|
152
153
|
resolve();
|
|
@@ -166,7 +167,10 @@ class TokenModule {
|
|
|
166
167
|
saveUninitialized: false,
|
|
167
168
|
rolling: true, // Reset expiration on each request
|
|
168
169
|
cookie: {
|
|
169
|
-
|
|
170
|
+
// Secure when the request came over https: 'auto' asks req.secure, which (with
|
|
171
|
+
// server.trustProxy) follows nginx's X-Forwarded-Proto. It used to depend on NODE_ENV,
|
|
172
|
+
// which nothing sets, so the cookie was never Secure. config.cookieSecure overrides
|
|
173
|
+
secure: this.config.cookieSecure ?? 'auto',
|
|
170
174
|
httpOnly: true, // Prevent XSS
|
|
171
175
|
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
172
176
|
sameSite: 'lax' // CSRF protection
|
|
@@ -780,7 +784,7 @@ class TokenModule {
|
|
|
780
784
|
this.db.get(
|
|
781
785
|
'SELECT id FROM users WHERE (email = ?) OR (provider = ? AND provider_id = ?)',
|
|
782
786
|
[userData.email, userData.provider, userData.provider_id],
|
|
783
|
-
(err, row) => {
|
|
787
|
+
guarded(reject, (err, row) => {
|
|
784
788
|
if (err) {
|
|
785
789
|
reject(err);
|
|
786
790
|
return;
|
|
@@ -791,29 +795,29 @@ class TokenModule {
|
|
|
791
795
|
this.db.run(
|
|
792
796
|
'UPDATE users SET email = ?, name = ?, profile_data = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
793
797
|
[userData.email, userData.name, userData.profile_data, row.id],
|
|
794
|
-
(err) => {
|
|
798
|
+
guarded(reject, (err) => {
|
|
795
799
|
if (err) {
|
|
796
800
|
reject(err);
|
|
797
801
|
} else {
|
|
798
802
|
resolve(row.id);
|
|
799
803
|
}
|
|
800
|
-
}
|
|
804
|
+
})
|
|
801
805
|
);
|
|
802
806
|
} else {
|
|
803
807
|
// Create new user
|
|
804
808
|
this.db.run(
|
|
805
809
|
'INSERT INTO users (email, name, provider, provider_id, profile_data) VALUES (?, ?, ?, ?, ?)',
|
|
806
810
|
[userData.email, userData.name, userData.provider, userData.provider_id, userData.profile_data],
|
|
807
|
-
function(err) {
|
|
811
|
+
guarded(reject, function(err) {
|
|
808
812
|
if (err) {
|
|
809
813
|
reject(err);
|
|
810
814
|
} else {
|
|
811
815
|
resolve(this.lastID);
|
|
812
816
|
}
|
|
813
|
-
}
|
|
817
|
+
})
|
|
814
818
|
);
|
|
815
819
|
}
|
|
816
|
-
}
|
|
820
|
+
})
|
|
817
821
|
);
|
|
818
822
|
});
|
|
819
823
|
}
|
|
@@ -823,10 +827,10 @@ class TokenModule {
|
|
|
823
827
|
this.db.get(
|
|
824
828
|
'SELECT * FROM users WHERE id = ? AND is_active = 1',
|
|
825
829
|
[userId],
|
|
826
|
-
(err, row) => {
|
|
830
|
+
guarded(reject, (err, row) => {
|
|
827
831
|
if (err) reject(err);
|
|
828
832
|
else resolve(row);
|
|
829
|
-
}
|
|
833
|
+
})
|
|
830
834
|
);
|
|
831
835
|
});
|
|
832
836
|
}
|
|
@@ -836,10 +840,10 @@ class TokenModule {
|
|
|
836
840
|
this.db.run(
|
|
837
841
|
'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?',
|
|
838
842
|
[userId],
|
|
839
|
-
(err) => {
|
|
843
|
+
guarded(reject, (err) => {
|
|
840
844
|
if (err) reject(err);
|
|
841
845
|
else resolve();
|
|
842
|
-
}
|
|
846
|
+
})
|
|
843
847
|
);
|
|
844
848
|
});
|
|
845
849
|
}
|
|
@@ -852,10 +856,10 @@ class TokenModule {
|
|
|
852
856
|
WHERE user_id = ?
|
|
853
857
|
ORDER BY created_at DESC`,
|
|
854
858
|
[userId],
|
|
855
|
-
(err, rows) => {
|
|
859
|
+
guarded(reject, (err, rows) => {
|
|
856
860
|
if (err) reject(err);
|
|
857
861
|
else resolve(rows || []);
|
|
858
|
-
}
|
|
862
|
+
})
|
|
859
863
|
);
|
|
860
864
|
});
|
|
861
865
|
}
|
|
@@ -870,10 +874,10 @@ class TokenModule {
|
|
|
870
874
|
`INSERT INTO api_keys (user_id, key_hash, key_prefix, name, scopes, expires_at, created_ip)
|
|
871
875
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
872
876
|
[userId, keyHash, keyPrefix, name, scopes, expiresAt, ip],
|
|
873
|
-
function(err) {
|
|
877
|
+
guarded(reject, function(err) {
|
|
874
878
|
if (err) reject(err);
|
|
875
879
|
else resolve(this.lastID);
|
|
876
|
-
}
|
|
880
|
+
})
|
|
877
881
|
);
|
|
878
882
|
});
|
|
879
883
|
}
|
|
@@ -887,7 +891,7 @@ class TokenModule {
|
|
|
887
891
|
FROM api_keys ak
|
|
888
892
|
JOIN users u ON ak.user_id = u.id
|
|
889
893
|
WHERE ak.key_prefix = ? AND ak.is_active = 1 AND u.is_active = 1
|
|
890
|
-
`, [keyPrefix], async (err, rows) => {
|
|
894
|
+
`, [keyPrefix], guarded(reject, async (err, rows) => {
|
|
891
895
|
if (err) {
|
|
892
896
|
reject(err);
|
|
893
897
|
return;
|
|
@@ -911,7 +915,7 @@ class TokenModule {
|
|
|
911
915
|
} catch (error) {
|
|
912
916
|
reject(error);
|
|
913
917
|
}
|
|
914
|
-
});
|
|
918
|
+
}));
|
|
915
919
|
});
|
|
916
920
|
}
|
|
917
921
|
|
|
@@ -920,13 +924,13 @@ class TokenModule {
|
|
|
920
924
|
this.db.run(
|
|
921
925
|
'UPDATE api_keys SET is_active = 0 WHERE id = ? AND user_id = ?',
|
|
922
926
|
[keyId, userId],
|
|
923
|
-
function(err) {
|
|
927
|
+
guarded(reject, function(err) {
|
|
924
928
|
if (err) {
|
|
925
929
|
reject(err);
|
|
926
930
|
} else {
|
|
927
931
|
resolve(this.changes > 0);
|
|
928
932
|
}
|
|
929
|
-
}
|
|
933
|
+
})
|
|
930
934
|
);
|
|
931
935
|
});
|
|
932
936
|
}
|
|
@@ -937,10 +941,10 @@ class TokenModule {
|
|
|
937
941
|
this.db.run(
|
|
938
942
|
'UPDATE api_keys SET last_used = CURRENT_TIMESTAMP WHERE id = ?',
|
|
939
943
|
[keyId],
|
|
940
|
-
(err) => {
|
|
944
|
+
guarded(reject, (err) => {
|
|
941
945
|
if (err) reject(err);
|
|
942
946
|
else resolve();
|
|
943
|
-
}
|
|
947
|
+
})
|
|
944
948
|
);
|
|
945
949
|
});
|
|
946
950
|
}
|
|
@@ -956,10 +960,10 @@ class TokenModule {
|
|
|
956
960
|
request_count = request_count + ?,
|
|
957
961
|
last_request_ip = ?,
|
|
958
962
|
updated_at = CURRENT_TIMESTAMP
|
|
959
|
-
`, [keyId, today, count, ip, count, ip], function(err) {
|
|
963
|
+
`, [keyId, today, count, ip, count, ip], guarded(reject, function(err) {
|
|
960
964
|
if (err) reject(err);
|
|
961
965
|
else resolve();
|
|
962
|
-
});
|
|
966
|
+
}));
|
|
963
967
|
});
|
|
964
968
|
}
|
|
965
969
|
|
|
@@ -970,10 +974,10 @@ class TokenModule {
|
|
|
970
974
|
this.db.get(
|
|
971
975
|
'SELECT request_count FROM usage_stats WHERE api_key_id = ? AND date = ?',
|
|
972
976
|
[keyId, today],
|
|
973
|
-
(err, row) => {
|
|
977
|
+
guarded(reject, (err, row) => {
|
|
974
978
|
if (err) reject(err);
|
|
975
979
|
else resolve(row ? row.request_count : 0);
|
|
976
|
-
}
|
|
980
|
+
})
|
|
977
981
|
);
|
|
978
982
|
});
|
|
979
983
|
}
|
|
@@ -986,10 +990,10 @@ class TokenModule {
|
|
|
986
990
|
JOIN api_keys ak ON us.api_key_id = ak.id
|
|
987
991
|
WHERE ak.user_id = ? AND us.date >= date('now', '-30 days')
|
|
988
992
|
ORDER BY us.date DESC
|
|
989
|
-
`, [userId], (err, rows) => {
|
|
993
|
+
`, [userId], guarded(reject, (err, rows) => {
|
|
990
994
|
if (err) reject(err);
|
|
991
995
|
else resolve(rows || []);
|
|
992
|
-
});
|
|
996
|
+
}));
|
|
993
997
|
});
|
|
994
998
|
}
|
|
995
999
|
|
|
@@ -1000,10 +1004,10 @@ class TokenModule {
|
|
|
1000
1004
|
FROM usage_stats
|
|
1001
1005
|
WHERE api_key_id = ? AND date >= date('now', '-${days} days')
|
|
1002
1006
|
ORDER BY date DESC
|
|
1003
|
-
`, [keyId], (err, rows) => {
|
|
1007
|
+
`, [keyId], guarded(reject, (err, rows) => {
|
|
1004
1008
|
if (err) reject(err);
|
|
1005
1009
|
else resolve(rows || []);
|
|
1006
|
-
});
|
|
1010
|
+
}));
|
|
1007
1011
|
});
|
|
1008
1012
|
}
|
|
1009
1013
|
|
|
@@ -1013,13 +1017,13 @@ class TokenModule {
|
|
|
1013
1017
|
this.db.run(
|
|
1014
1018
|
'INSERT INTO security_log (user_id, event_type, ip_address, user_agent, details) VALUES (?, ?, ?, ?, ?)',
|
|
1015
1019
|
[userId, eventType, ip, userAgent, JSON.stringify(details)],
|
|
1016
|
-
function(err) {
|
|
1020
|
+
guarded(reject, function(err) {
|
|
1017
1021
|
if (err) {
|
|
1018
1022
|
// Don't fail the main operation if logging fails
|
|
1019
1023
|
console.error('Failed to log security event:', err);
|
|
1020
1024
|
}
|
|
1021
1025
|
resolve();
|
|
1022
|
-
}
|
|
1026
|
+
})
|
|
1023
1027
|
);
|
|
1024
1028
|
});
|
|
1025
1029
|
}
|
|
@@ -1292,15 +1296,15 @@ class TokenModule {
|
|
|
1292
1296
|
this.sessionStore.close();
|
|
1293
1297
|
}
|
|
1294
1298
|
if (this.db) {
|
|
1295
|
-
await new Promise((resolve) => {
|
|
1296
|
-
this.db.close((err) => {
|
|
1299
|
+
await new Promise((resolve, reject) => {
|
|
1300
|
+
this.db.close(guarded(reject, (err) => {
|
|
1297
1301
|
if (err) {
|
|
1298
1302
|
this.log.error('Error closing database:', err);
|
|
1299
1303
|
} else {
|
|
1300
1304
|
this.log.info('Database connection closed');
|
|
1301
1305
|
}
|
|
1302
1306
|
resolve();
|
|
1303
|
-
});
|
|
1307
|
+
}));
|
|
1304
1308
|
});
|
|
1305
1309
|
}
|
|
1306
1310
|
}
|
|
@@ -159,6 +159,7 @@ CODESYSTEM_SHAREABLE_MISSING_HL7 = Code systems published by HL7 SHALL conform t
|
|
|
159
159
|
CODESYSTEM_SUPP_NO_DISPLAY = This display (''{0}'') differs from that defined by the base code system (''{1}''). Both displays claim to be the ''primary designation'' for the same language (''{2}''), and the correct interpretation of this is undefined
|
|
160
160
|
CODESYSTEM_THO_CHECK = Most code systems defined in HL7 IGs will need to move to THO later during the process. Consider giving this code system a THO URL now (See https://confluence.hl7.org/display/TSMG/Terminology+Play+Book, and/or talk to TSMG)
|
|
161
161
|
CODESYSTEM_TOO_COSTLY_TIME = The operation ''{1}'' took too long to process (>{0}sec) (at {2})
|
|
162
|
+
CODESYSTEM_VERSION_NEEDED = The code system ''{0}'' says that a version must be specified when it is referenced (versionNeeded = true), but this coding has no version
|
|
162
163
|
CODESYSTEM_NOT_ENUMERABLE = The code system ''{0}'' cannot be expanded because its codes cannot be iterated or enumerated in any meaningful sense
|
|
163
164
|
Coding_has_no_system__cannot_validate = Coding has no system. A code with no system has no defined meaning, and it cannot be validated. A system should be provided
|
|
164
165
|
Coding_has_no_system_or_code__cannot_validate = Coding has neither a system nor a code, so it cannot be validated. A system and a code should be provided
|
|
@@ -1094,6 +1095,8 @@ Unable_to_handle_system__filter_with_property__ = Unable to handle system {0} fi
|
|
|
1094
1095
|
UNABLE_TO_HANDLE_SYSTEM__PROPERTY_FILTER_WITH_OP__ = Unable to handle system {0} property filter with op = {1}
|
|
1095
1096
|
UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE = The system {0} filter with property = {1}, op = {2} has no value
|
|
1096
1097
|
UNABLE_TO_INFER_CODESYSTEM = The System URI could not be determined for the code ''{0}'' in the ValueSet ''{1}''
|
|
1098
|
+
UNABLE_TO_INFER_CODESYSTEM_UNKNOWN_SYSTEM = The System URI could not be determined for the code ''{0}'' in the ValueSet ''{1}'': a definition for the CodeSystem ''{2}'' it includes could not be found
|
|
1099
|
+
VALUESET_MODIFIER_EXTENSION_UNKNOWN = The value set {0} can''t be processed: {1} has a modifier extension that isn''t understood ({2})
|
|
1097
1100
|
Unable_to_locate_the_profile__in_order_to_validate_against_it = Unable to locate the profile ''{0}'' in order to validate against it
|
|
1098
1101
|
Unable_to_process_request_for_resource_for___ = Unable to process request for resource for {0} / {1}
|
|
1099
1102
|
Unable_to_resolve_ = Unable to resolve the reference {0}
|
|
@@ -1116,9 +1119,20 @@ Unable_to_resolve_system__value_set_has_no_includes_or_expansion = The System UR
|
|
|
1116
1119
|
UNABLE_TO_RESOLVE_SYSTEM_SYSTEM_IS_INDETERMINATE = The code system {1} referred to from value set {0} has a grammar, and the code might be valid in it
|
|
1117
1120
|
Unable_to_resolve_value_Set_ = A definition for the value Set ''{0}'' could not be found
|
|
1118
1121
|
CACHE_ID_UNKNOWN = The cache ''{0}'' was never issued by this server, and this server has no record of it ever existing. Caches are created with $cache-control?mode=start, and a cache-id is only valid on the server instance and endpoint that issued it - either it was issued somewhere else, or this server has restarted since it was issued
|
|
1122
|
+
CACHE_ID_UNKNOWN_SHARED = The cache ''{0}'' was never issued by this server for this endpoint: this server has no record of it. Caches are created with $cache-control?mode=start, and a cache-id is only valid on the server and endpoint that issued it
|
|
1123
|
+
CACHE_ID_NEVER_ISSUED = The cache ''{0}'' was never issued by this server: it carries this server''s instance code and the start of this run, which began {1} ago, but this server has no record of it (it remembers the fate of the last {2} caches that ended). Caches are created with $cache-control?mode=start
|
|
1124
|
+
CACHE_ID_GONE_DURING_REQUEST = The cache ''{0}'' ended ({1}) while this request was using it, so {2} is no longer available. This operation has not been performed. Start a new cache with $cache-control?mode=start, and send the request again
|
|
1125
|
+
CACHE_ID_CHANGED_DURING_REQUEST = The cache ''{0}'' changed while this request was using it: another request replaced {1} after this one had read the list of what the cache holds. This operation has not been performed. Send the request again, to read the new contents together
|
|
1126
|
+
CACHE_ID_LOST_TO_RESTART = The cache ''{0}'' was lost to a server restart {1} ago. Caches are not carried over a restart, because neither the client nor the server can be sure a cache is intact across operations that were tried while the server was down. Start a new cache with $cache-control?mode=start and send the resources again
|
|
1119
1127
|
CACHE_ID_CLOSED = The cache ''{0}'' was closed {1} ago by the client, using $cache-control?mode=end (it had been open for {2}). It did not expire and it was not idle: something in the client released this cache while this request was still using it
|
|
1120
1128
|
CACHE_ID_EXPIRED = The cache ''{0}'' expired: it went {1} without being used, and this server closes caches that have been idle for more than {2} (this one was dropped {3} ago). Every request that uses a cache resets its idle clock, so a client that keeps using a cache will not see this
|
|
1121
1129
|
CACHE_ID_CLEARED = The cache ''{0}'' was dropped {1} ago, when this server cleared all of its caches
|
|
1130
|
+
CACHE_ID_EVICTED = The cache ''{0}'' was dropped {1} ago to make room: the caches held more concepts than this server allows, and this was the one used least recently
|
|
1131
|
+
CACHE_ID_DISCARDED = The cache ''{0}'' was dropped {1} ago because it grew too large: a request would have taken it over {2} concepts, the most this server allows in one cache (cacheMaxConcepts). This operation has not been performed. Start a new cache, and send the request again
|
|
1132
|
+
CACHE_ID_DISCARDED_TOO_BIG = The cache ''{0}'' grew too large: this request would have taken it over {1} concepts, the most this server allows in one cache (cacheMaxConcepts), so the server has dropped the cache. This operation has not been performed. Start a new cache, and send the request again
|
|
1133
|
+
CACHE_ID_EVICTED_SIZE = The cache ''{0}'' was dropped {1} ago to make room: this server holds at most {2} of client resources across all caches, and when it is full it drops the caches that have gone longest without being used (this one had been idle for {3}). Start a new cache and send the resources again
|
|
1134
|
+
CACHE_REQUEST_TOO_BIG = This request''s resources hold {0} concepts, more than this server allows in one cache ({1}, cacheMaxConcepts), so no cache can hold them
|
|
1135
|
+
CACHE_START_TOO_BIG = The cache ''{0}'' would hold {1} concepts, more than this server allows in one cache ({2}, cacheMaxConcepts)
|
|
1122
1136
|
CACHE_ID_OTHER_INSTANCE = The cache ''{0}'' was issued by the server instance ''{1}'', but this is instance ''{2}''. A cache only exists on the instance that issued it, so this request reached the wrong server: either the proxy in front of these servers is not routing on the cache-id, or instance ''{1}'' is unavailable
|
|
1123
1137
|
CACHE_ID_OTHER_INSTANCE_UNNAMED = The cache ''{0}'' was issued by the server instance ''{1}'', but this server has no instance code, so it cannot have issued it. A cache only exists on the instance that issued it, so this request reached the wrong server: either the proxy in front of these servers is not routing on the cache-id, or instance ''{1}'' is unavailable
|
|
1124
1138
|
CLOSURE_NOT_SUPPORTED = This server does not support $closure (it has to be turned on by the administrator)
|
|
@@ -1141,6 +1155,7 @@ CLOSURE_TOO_MANY_REQUEST_CONCEPTS = A single $closure request can add at most {0
|
|
|
1141
1155
|
CLOSURE_TOO_MANY_ENTRIES = Adding these concepts would put more entries into the closure table ''{0}'' than this server allows, in one request or in total
|
|
1142
1156
|
CLOSURE_CONFIG_TOO_LARGE = The configuration supplied for this closure table (mostly the tx-resource parameters) is larger than this server will keep, which is {0} bytes
|
|
1143
1157
|
CLOSURE_TABLE_BUSY = The closure table ''{0}'' is busy with other requests. Try again shortly
|
|
1158
|
+
CLOSURE_TABLE_CHANGED = The closure table ''{0}'' was reset while this request was reading it. Retry the request against the new table
|
|
1144
1159
|
SUBSUMPTION_CS_HIERARCHY_MEANING = The CodeSystem ''{0}'' has hierarchyMeaning = {1}, so its hierarchy does not describe subsumption, and subsumption cannot be tested
|
|
1145
1160
|
SUBSUMPTION_CS_NOT_COMPLETE = The CodeSystem ''{0}'' has content = {1}, so subsumption cannot be tested (subsumption can only be tested against a code system with content = complete)
|
|
1146
1161
|
SUBSUMPTION_CS_NOT_FOUND = A definition for the CodeSystem ''{0}'' could not be found, so subsumption cannot be tested
|
|
@@ -1283,6 +1298,7 @@ VALUESET_CONCEPT_DISPLAY_PRESENCE_MIXED = This include has some concepts with di
|
|
|
1283
1298
|
VALUESET_CONCEPT_DISPLAY_SCT_TAG_MIXED = This SNOMED-CT based include has some concepts with semantic tags (FSN terms) and some without (preferred terms) - check that this is what is intended (examples for FSN: {0} and examples for no FSN: {1})
|
|
1284
1299
|
VALUESET_EXAMPLE_SYSTEM_ERROR = Example System ''{0}'' specified, which is illegal. Concepts and Filters can''t be checked
|
|
1285
1300
|
VALUESET_EXAMPLE_SYSTEM_HINT = Example System ''{0}'' specified, so Concepts and Filters can''t be checked
|
|
1301
|
+
VALUESET_EXPANSION_CODESYSTEM_LIMITED = Only {1} codes from the code system ''{0}'' can be listed in an expansion (by agreement with its publisher), so this expansion doesn''t list all the codes asked for
|
|
1286
1302
|
VALUESET_IMPORT_UNION_INTERSECTION = This value set has a single include with multiple imported value sets. Per issue https://jira.hl7.org/browse/FHIR-25179, there has been confusion in the past whether these value sets are unioned or intersectioned. If this value set is contained in a package published prior to March 31 2022, it will be treated as a union, otherwise it will be treated as an intersection. If want a union, split the value set imports across multiple includes
|
|
1287
1303
|
VALUESET_INC_TOO_MANY_CODES = The value set include has too many concepts to validate ({0}), so each individual code has not been checked
|
|
1288
1304
|
VALUESET_INC_NOT_VALIDATING = The concepts in the value set include have not been validated because the validator is configured not to check them
|
|
@@ -1327,6 +1343,9 @@ VALUESET_SHAREABLE_MISSING_HL7 = Value sets published by HL7 SHALL conform to th
|
|
|
1327
1343
|
VALUESET_SUPPLEMENT_MISSING_one = Required supplement not found: {1}
|
|
1328
1344
|
VALUESET_SUPPLEMENT_MISSING_other = Required supplements not found: {1}
|
|
1329
1345
|
VALUESET_TOO_COSTLY = The value set ''{0}'' expansion has too many codes to produce ({1})
|
|
1346
|
+
VALUESET_TOO_COSTLY_REQUEST = The expansion of the value set in the request has too many codes to produce ({0})
|
|
1347
|
+
VALUESET_TOO_COSTLY_PAGE = The value set ''{0}'' can''t be paged to offset {1} with count {2}: the server walks at most {3} codes to reach a page
|
|
1348
|
+
VALUESET_TOO_COSTLY_PAGE_REQUEST = The value set in the request can''t be paged to offset {0} with count {1}: the server walks at most {2} codes to reach a page
|
|
1330
1349
|
VALUESET_TOO_COSTLY_COUNT = The value set ''{0}'' expansion has {2} codes, which is too many to display ({1})
|
|
1331
1350
|
VALUESET_TOO_COSTLY_TIME = The value set ''{0}'' {2} took too long to process (>{1}sec)
|
|
1332
1351
|
VALUESET_UNC_SYSTEM_WARNING = Unknown System ''{0}'' specified, so Concepts and Filters can''t be checked (Details: {1})
|
package/tx/README.md
CHANGED
|
@@ -27,8 +27,8 @@ Add the `tx` section to your `config.json`:
|
|
|
27
27
|
"librarySource": "/path/to/library.yml",
|
|
28
28
|
"publishLibrarySource": false,
|
|
29
29
|
"cacheTimeout": 30,
|
|
30
|
-
"internalLimit" :
|
|
31
|
-
"externalLimit" :
|
|
30
|
+
"internalLimit" : 100000,
|
|
31
|
+
"externalLimit" : 10000,
|
|
32
32
|
"expansionCacheSize": 1000,
|
|
33
33
|
"expansionCacheMemoryThreshold": 0,
|
|
34
34
|
"endpoints": [
|
|
@@ -64,13 +64,96 @@ Add the `tx` section to your `config.json`:
|
|
|
64
64
|
|---------------------------------|---------|----------|---------------------------------------------------------------------------------------------------------|
|
|
65
65
|
| `enabled` | boolean | Yes | Whether the module is enabled |
|
|
66
66
|
| `cacheTimeout` | integer | No | How many minutes to keep client side caches (for cache-id parameter). Default: 30 |
|
|
67
|
+
| `cacheMaxConcepts` | integer | No | Concepts one client side cache (cache-id) may hold. Default: 250000, about 10x the largest IG Publisher session measured on build.fhir.org (24,048). A request that would take a cache over it is not performed: the server drops the cache and answers 404, an OperationOutcome with one issue coded `tx-issue-type#cache-id-unknown` (message id `CACHE_ID_DISCARDED_TOO_BIG`, saying the cache grew too large), as for any cache that is gone. The client starts a new cache and sends the request again; later requests with the old id get the same 404 `cache-id-unknown`, saying why it was dropped (`CACHE_ID_DISCARDED`). A request whose own resources hold more than this, so that no cache could take them, is refused instead (422 too-costly) and the cache is kept, as is a new cache too big to start. 0 = no limit |
|
|
68
|
+
| `cacheMaxTotalConcepts` | integer | No | Concepts all the client side caches of an endpoint may hold between them (about 250 bytes of heap each). Default: 1000000, 4 full caches, about 40x the largest session measured (24,048). The least recently used caches are dropped to stay under it; a request with a dropped cache-id gets a 404 `cache-id-unknown`, saying it was dropped to make room. 0 = no limit |
|
|
69
|
+
| `usageTrackerMaxCodes` | integer | No | Distinct codes the usage tracker (`usage-count` expansion property) keeps. Default: 1000000, 2x the about 500,000 codes the value sets of a full library list. Codes beyond it aren't counted (no error); codes already tracked still are. 0 = no limit |
|
|
70
|
+
| `usageTrackerMaxCodeLength` | integer | No | Longest code, in characters, the usage tracker keeps. Default: 100, 2.5x the longest code those value sets list (40). Longer codes aren't counted (no error). 0 = no limit |
|
|
71
|
+
| `languageTagCacheSize` | integer | No | Parsed language tags kept (least recently used dropped). Default: 1000; an 8 KB private use tag takes about 46 KB parsed, so at most about 46 MB. Clients see nothing but a re-parse. 0 = no limit (unique tags in requests then grow the heap forever) |
|
|
67
72
|
| `expansionCacheSize` | integer | No | Maximum number of expanded ValueSets to cache. Default: 1000 |
|
|
68
73
|
| `expansionCacheMemoryThreshold` | integer | No | Heap memory usage in MB that triggers evicting oldest half of expansion cache. 0 = disabled. Default: 0 |
|
|
74
|
+
| `expansionCacheMaxCodes` | integer | No | Most codes the expansion caches of all the endpoints hold between them; the least recently used expansions are dropped to stay under it. Default: a quarter of the heap limit at 1 KB a code (about 1 million codes with a 4 GB heap) |
|
|
69
75
|
| `librarySource` | string | Yes | Path to the YAML file that defines the terminology sources to load |
|
|
70
76
|
| `publishLibrarySource` | boolean | No | Publish that YAML file at `/{path}/library`. Default: false |
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
77
|
+
| `pruneTerminologyCache` | boolean | No | Once the library and every endpoint's core packages have loaded, delete what in `terminology-cache` wasn't used: the `.db` and `.cache` files (the downloaded databases and SNOMED CT caches of sources no longer in the library, and the fallback of an `a\|b` source whose first file is there), with their SQLite side files; and the package folders (`id#version`: packages no longer in the library, older versions of ones that are, the core packages of FHIR versions no endpoint has, and what failed downloads left), with the value set and concept map caches in them. Other files and folders (`vsac`, `ocl`, `syndication`, `folder-source-*`) are left alone, and so is anything changed in the last day (it may be being copied in for a library that doesn't name it yet). In a cluster, only the master deletes. Default: false: everything run from the same data directory (a development server and the tests, say) shares `terminology-cache`, and what one doesn't use another may |
|
|
78
|
+
| `internalLimit` | integer | No | Most codes in the expansion of a value set that an `$expand` imports (`compose.include.valueSet`). Default: 100000. Over it, the `$expand` fails with 422 too-costly. Also the most the `x-too-costly-threshold` header can ask for. Other internal expansions (in `$validate-code`, for one) use 10000 and don't read the setting |
|
|
79
|
+
| `externalLimit` | integer | No | Most codes an `$expand` returns. Default: 10000 (or `internalLimit`, if that is less). Over it, an expansion asked for without `count` fails with 422 too-costly (`VALUESET_TOO_COSTLY`, "... has too many codes to produce (>10000)"); a `count` bigger than it is cut down to it. A request can change it with the `x-too-costly-threshold` header (see below): the tx-ecosystem tests that depend on a limit set it |
|
|
80
|
+
| `walkLimit` | integer | No | How far a paged `$expand` (`count` and `offset`) may walk to reach its page: the most `offset + count`. Default: 10 x `internalLimit` (1000000). Only the page is built - the codes before it are passed keeping just their keys - so a page can lie well past `externalLimit`, which still caps `count`. Over it, the `$expand` fails with 422 too-costly (`VALUESET_TOO_COSTLY_PAGE`, "... can't be paged to offset ... with count ...: the server walks at most 1000000 codes to reach a page"); a value set that ends before the offset is an empty page, not a refusal. The walk is also bounded by `operationTimeLimit`. Any page, the first too, builds only its own codes; the codes after it are counted by key for the total, which is reported only when the whole expansion is within `externalLimit`, as for an unpaged one. Not for a value set with an exclude, or a request with a text filter: there, `offset + count` must still be within `externalLimit` |
|
|
81
|
+
| `operationTimeLimit` | integer | No | Seconds of compute one operation may use (sub-operations share it). Default: 20. Time spent waiting - for the event loop, or in a queue before the operation starts - doesn't count. Over it, the operation fails with 422 too-costly ("Operation exceeded time limit of 20 seconds at ..."). The default sits under common client timeouts (okhttp gives up after 10-30 s): with a limit at or over the client's, the client hangs up first and never sees the OperationOutcome |
|
|
82
|
+
| `ecosystem` | object | No | Answering for code systems that aren't held here by asking other servers in the terminology ecosystem - see [Ecosystem Resolution](#ecosystem-resolution) below. Default: not used |
|
|
73
83
|
| `endpoints` | array | Yes | List of endpoint configurations (at least one required) |
|
|
84
|
+
| `vsacCfg` | object | No | Settings for the VSAC value set provider; required if the library loads VSAC (see below) |
|
|
85
|
+
|
|
86
|
+
### Ecosystem Resolution
|
|
87
|
+
|
|
88
|
+
When a code system (or a version of one) isn't held here, the server can ask the terminology
|
|
89
|
+
ecosystem registry which server has it, and ask that server (`cs/cs-virtual.js`). Only
|
|
90
|
+
`$validate-code` against a value set is supported this way, and slowly: a `$lookup` per code, and a
|
|
91
|
+
`$validate-code` per filter.
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
"ecosystem" : {
|
|
95
|
+
"use": true,
|
|
96
|
+
"excludedServers": [ "tx.fhir.org" ],
|
|
97
|
+
"excludedPatterns": [ "http://hl7.org/*", "http://terminology.hl7.org/*" ],
|
|
98
|
+
"apiKeys": { "hl7-ca" : "xx" },
|
|
99
|
+
"oauth" : {
|
|
100
|
+
"nzhts": {
|
|
101
|
+
"tokenEndpoint" : "https://authenticate.nzhts.digital.health.nz/auth/realms/nzhts/protocol/openid-connect/token",
|
|
102
|
+
"clientId" : "hl7",
|
|
103
|
+
"clientSecret" : "xx"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| Setting | Type | Description |
|
|
110
|
+
|---------|------|-------------|
|
|
111
|
+
| `use` | boolean | Turns it on. Default: false |
|
|
112
|
+
| `registry` | string | The ecosystem registry. Default: `https://ecosystem.hl7.org/tx-reg` |
|
|
113
|
+
| `excludedServers` | array | Servers never used, each a host (`tx.fhir.org`) or an address that the server's address starts with. tx.fhir.org (and tx-dev.fhir.org, which serves the same content) sets this to `["tx.fhir.org"]`: the registry names tx.fhir.org for what `tx.fhir.org.yml` loads, so that's this server, logically, whatever its address is, and asking it again can only give the answer this server already gave |
|
|
114
|
+
| `excludedPatterns` | array | Code systems never looked for in the ecosystem, as the registry writes them: `*` matches anything, and a pattern is matched against the url, and the url\|version. These aren't even asked about, so they cost nothing: a server that knows the registry will name itself (or a server in `excludedServers`) for them should list them |
|
|
115
|
+
| `apiKeys` | object | Server code (as in the registry) -> API key, sent as `Api-Key`. As for the registry module |
|
|
116
|
+
| `oauth` | object | Server code -> `{ tokenEndpoint, clientId, clientSecret, scope }`: an OAuth2 client_credentials grant, the token sent as `Authorization: Bearer`. As for the registry module |
|
|
117
|
+
| `allowPrivateAddresses` | boolean | Lets requests go to private, loopback and other non-public addresses. Default: false - the registry, the servers it names, and the OAuth token endpoints must all be at public addresses, because the addresses come from the registry, which may not be the public one (as for the registry module's setting of the same name). For a local registry or test servers |
|
|
118
|
+
|
|
119
|
+
A server is used if it's open, or if there are credentials for it. The server must also say, in its
|
|
120
|
+
TerminologyCapabilities, that it has the code system and version asked for (or, for a version that
|
|
121
|
+
is a url, a release of it - a SNOMED CT edition asked for without a release date). With no version
|
|
122
|
+
asked for, it's the server's only version, or the one it marks `isDefault`. Requests sent on to
|
|
123
|
+
another server carry the header `X-FHIRsmith-Ecosystem-Forwarded`, and a request that arrives with
|
|
124
|
+
it is never resolved through the ecosystem again.
|
|
125
|
+
|
|
126
|
+
What the registry says, and what each server says it has, is kept for an hour, and then refreshed
|
|
127
|
+
in the background: a request never waits for a refresh, only the first request for something does
|
|
128
|
+
(if a refresh fails, the old answer is used, and the refresh tried again in five minutes). The
|
|
129
|
+
other servers' answers to `$lookup` and `$validate-code` are kept for an hour too (the most recent
|
|
130
|
+
10,000), so the same question is only asked once. Requests that had to ask another server are
|
|
131
|
+
counted, and graphed, separately on the home page (External requests).
|
|
132
|
+
|
|
133
|
+
### VSAC Configuration
|
|
134
|
+
|
|
135
|
+
`vsacCfg` configures the VSAC provider (a `vsac` source in the library). The value sets are
|
|
136
|
+
synced from VSAC into a SQLite database in the terminology cache folder, and read from it when
|
|
137
|
+
they are asked for; only the recently used ones are kept parsed in memory.
|
|
138
|
+
|
|
139
|
+
| Option | Type | Required | Description |
|
|
140
|
+
|------------------------|---------|----------|---------------------------------------------------------------------------------------------------|
|
|
141
|
+
| `apiKey` | string | Yes | UMLS API key for VSAC |
|
|
142
|
+
| `baseUrl` | string | No | VSAC FHIR server. Default: `https://cts.nlm.nih.gov/fhir` |
|
|
143
|
+
| `refreshIntervalHours` | number | No | Hours between syncs. A failed sync is retried after 1, 5, then every 30 minutes. Default: 24 |
|
|
144
|
+
| `timeoutMs` | integer | No | Timeout for requests to VSAC, in milliseconds. Default: 120000 |
|
|
145
|
+
| `resyncPassword` | string | No | If set, the /info page offers a form (gated by this password) to resync one value set now |
|
|
146
|
+
| `valueSetCacheMB` | number | No | Size of the recently used value sets kept parsed, measured by their stored JSON; 0 keeps none. Default: 64 |
|
|
147
|
+
|
|
148
|
+
`valueSetCacheMB` is measured in stored JSON, not memory: parsed value sets take several
|
|
149
|
+
times as much heap as their JSON (so expect a few hundred MB of heap at the default), and in
|
|
150
|
+
cluster mode every worker keeps its own.
|
|
151
|
+
|
|
152
|
+
`externalLimit`, `internalLimit` and `walkLimit` are checked at startup: each, when set, must be a whole number above 0, and `externalLimit` <= `internalLimit` <= `walkLimit`; anything else stops the server with an error saying which. The limits in use are logged at startup. Every client gets the same limits: the tx-ecosystem test runner (User-Agent `Tools/Java`) used to be held to the old defaults (3000 and 10000) whatever the configuration; now the tests that depend on a limit ask for it with the header.
|
|
153
|
+
|
|
154
|
+
The `x-too-costly-threshold` request header sets the `externalLimit` for that request, for any client. It is there so test cases can ask for a lower limit: it can't be more than the `internalLimit` (a bigger value is cut down to it), and a value that isn't a positive whole number is ignored, so there is always a limit.
|
|
155
|
+
|
|
156
|
+
The largest request body the server accepts is a server setting, `server.maxBodyMB` (see the main README); a tx endpoint answers a bigger one with a 413 OperationOutcome (too-costly).
|
|
74
157
|
|
|
75
158
|
### Endpoint Configuration
|
|
76
159
|
|
|
@@ -84,6 +167,24 @@ Each endpoint in the `endpoints` array has:
|
|
|
84
167
|
|
|
85
168
|
**Note:** Endpoint paths must be unique. The module will throw an error if duplicate paths are configured or if a path conflicts with another module.
|
|
86
169
|
|
|
170
|
+
### Fixed limits
|
|
171
|
+
|
|
172
|
+
These are constants in the code, not settings; they are listed so their effects can be recognised.
|
|
173
|
+
|
|
174
|
+
| Limit | Value | What it bounds |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| Memory per code, for the default of `expansionCacheMaxCodes` | 1 KB | only the derived default (`operation-context.js`) |
|
|
177
|
+
| Share of the heap, for the default of `expansionCacheMaxCodes` | 0.25 | a quarter of the V8 heap limit, per process |
|
|
178
|
+
| Shortest expansion that is cached | 250 ms | expansions that took less are not kept in the expansion cache |
|
|
179
|
+
| Longest compute before yielding the event loop | 10 ms | how long one operation can hold up the others (`operation-context.js`, `YIELD_INTERVAL`) |
|
|
180
|
+
| ECL nesting depth | 100 | bracketed expressions and refinements; deeper is a parse error |
|
|
181
|
+
| ECL sub-expressions | 500 | focus concepts, refinement values, ... (a long `A OR B OR ...` reaches it first); more is a parse error |
|
|
182
|
+
| Resource self-nesting depth | 100 | `CodeSystem.concept` and `ValueSet.expansion.contains` (SNOMED CT is about 30 deep); deeper is a 400 |
|
|
183
|
+
| XML element depth | 1000 | a deeper XML body is an error |
|
|
184
|
+
| LOINC SQLite `cache_size` | 10000 pages (about 40 MB) | page cache, per database connection |
|
|
185
|
+
| LOINC SQLite `mmap_size` | 256 MB | memory-mapped reads, per database connection; file-backed pages, shared between connections |
|
|
186
|
+
| Forced GC back-off | 60 s at most | while the memory guard is refusing requests, the wait between forced GCs doubles up to this (server wide, `library/memory-guard.js`) |
|
|
187
|
+
|
|
87
188
|
## Supported Operations
|
|
88
189
|
|
|
89
190
|
Each endpoint provides the following FHIR terminology operations:
|
|
@@ -258,6 +359,52 @@ actually returns (metadata, read and search, `$expand`, `$validate-code`, `$look
|
|
|
258
359
|
`$subsumes`, `$batch-validate-code`, `$versions`, `$cache-control`, an error) against the
|
|
259
360
|
schemas, using ajv. So a new parameter needs adding to openapi-operations.js.
|
|
260
361
|
|
|
362
|
+
## Loading SNOMED CT
|
|
363
|
+
|
|
364
|
+
By default each SNOMED CT cache file is read into the server process, at about its file size
|
|
365
|
+
(0.6–1.2 GB an edition). A file can instead be memory-mapped read-only:
|
|
366
|
+
|
|
367
|
+
- the edition loads in milliseconds;
|
|
368
|
+
- only the pages that requests touch become resident, as page cache that the kernel can drop
|
|
369
|
+
and read again;
|
|
370
|
+
- every server process on the machine that maps the same file shares those pages.
|
|
371
|
+
|
|
372
|
+
The first request to touch a part of a mapped edition waits for the disk, unless that edition
|
|
373
|
+
is *prewarmed*: mapped, then read through once in the background at startup, so its pages are
|
|
374
|
+
already in the page cache when requests arrive. Startup doesn't wait for it.
|
|
375
|
+
|
|
376
|
+
This is set per server, in the tx module's `snomed` block in `config.json`:
|
|
377
|
+
|
|
378
|
+
```json
|
|
379
|
+
"tx": {
|
|
380
|
+
"snomed": {
|
|
381
|
+
"load": "mmap",
|
|
382
|
+
"prewarm": ["http://snomed.info/sct/900000000000207008/version/20250201",
|
|
383
|
+
"sct_us_20250901b.cache"],
|
|
384
|
+
"read": []
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
- **`load`** is `"read"` (the default) or `"mmap"`, for every edition not named below.
|
|
390
|
+
- **`prewarm`** lists editions to prewarm (map, then read through once in the background at
|
|
391
|
+
startup), even when `load` is `"read"`.
|
|
392
|
+
- **`read`** lists editions to read into the process even when `load` is `"mmap"`.
|
|
393
|
+
- An entry is any of:
|
|
394
|
+
- a cache file name (`sct_us_20250901b.cache`)
|
|
395
|
+
- an edition version URI (`http://snomed.info/sct/731000124108/version/20250901`)
|
|
396
|
+
- an edition URI (`http://snomed.info/sct/731000124108`), which covers every loaded version
|
|
397
|
+
of that edition
|
|
398
|
+
- `read` wins over `prewarm`.
|
|
399
|
+
- Entries that match no loaded edition are warned about at startup.
|
|
400
|
+
- With no `snomed` block, everything is read, as before.
|
|
401
|
+
|
|
402
|
+
Mapping uses the optional `koffi` dependency. Where it is not available (Windows, 32-bit
|
|
403
|
+
platforms, or koffi not installed), a mapped edition is read instead, with a warning in the
|
|
404
|
+
server log. The log says, for each edition that was to be mapped, whether it really was. When
|
|
405
|
+
any edition is mapped, the memory guard leaves file-backed pages out of the memory it measures
|
|
406
|
+
(Linux).
|
|
407
|
+
|
|
261
408
|
## Library Configuration
|
|
262
409
|
|
|
263
410
|
The library - the source to load - is configured using a YAML file.
|
|
@@ -408,6 +555,9 @@ Common edition identifiers:
|
|
|
408
555
|
|
|
409
556
|
The file is built by importing SNOMED CT (see [documentation](importers/readme.md))
|
|
410
557
|
|
|
558
|
+
How the cache files are loaded is a deployment choice, so it is set in `config.json` (the
|
|
559
|
+
tx module's `snomed` block), not here - see [Loading SNOMED CT](#loading-snomed-ct).
|
|
560
|
+
|
|
411
561
|
|
|
412
562
|
#### `cpt` - CPT (Current Procedural Terminology)
|
|
413
563
|
|