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.
Files changed (131) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README.md +80 -13
  3. package/config-template.json +106 -1
  4. package/ips/ai-client.js +263 -0
  5. package/ips/composer.js +805 -0
  6. package/ips/editor-schema.js +289 -0
  7. package/ips/editor.js +290 -0
  8. package/ips/fhirpath-ips.js +397 -0
  9. package/ips/ips-codeable-concepts.md +1742 -0
  10. package/ips/ips-template.html +125 -0
  11. package/ips/ips-valuesets.js +159 -0
  12. package/ips/ips.js +1736 -0
  13. package/ips/membership.js +179 -0
  14. package/ips/pipeline.js +1012 -0
  15. package/ips/presenter.js +974 -0
  16. package/ips/profile-editor.js +578 -0
  17. package/ips/profiles.js +506 -0
  18. package/ips/readme.md +460 -0
  19. package/ips/render.js +1482 -0
  20. package/ips/search-params.js +83 -0
  21. package/ips/search.js +667 -0
  22. package/ips/share.js +178 -0
  23. package/ips/store.js +650 -0
  24. package/ips/terminology.js +395 -0
  25. package/ips/tx-client.js +202 -0
  26. package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
  27. package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
  28. package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
  29. package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
  30. package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
  31. package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
  32. package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
  33. package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
  34. package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
  35. package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
  36. package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
  37. package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
  38. package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
  39. package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
  40. package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
  41. package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
  42. package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
  43. package/ips/valuesets/ValueSet-ips-medication.json +50 -0
  44. package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
  45. package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
  46. package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
  47. package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
  48. package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
  49. package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
  50. package/ips/valuesets/ValueSet-ips-route.json +46 -0
  51. package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
  52. package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
  53. package/ips/valuesets/elements.json +44 -0
  54. package/library/daily-report.js +432 -0
  55. package/library/fhir-search-values.js +138 -0
  56. package/library/html-server.js +32 -23
  57. package/library/mailer.js +77 -0
  58. package/library/narrative-check.js +21 -2
  59. package/library/npm-audit.js +25 -0
  60. package/library/package-manager.js +4 -2
  61. package/library/sqlite-session-store.js +29 -1
  62. package/package.json +8 -3
  63. package/packages/packages.js +34 -3
  64. package/publisher/publisher.js +29 -0
  65. package/registry/auth-store.js +366 -0
  66. package/registry/auth.js +872 -0
  67. package/registry/crawler.js +41 -1
  68. package/registry/documentation/api.md +162 -0
  69. package/registry/documentation/configuration.md +93 -0
  70. package/registry/documentation/documents.md +74 -0
  71. package/registry/documentation/internals.md +151 -0
  72. package/registry/documentation/readme.md +100 -0
  73. package/registry/documentation/security.md +199 -0
  74. package/registry/documents.js +309 -0
  75. package/registry/openapi.yaml +30 -0
  76. package/registry/registry-template.html +1 -1
  77. package/registry/registry.js +230 -44
  78. package/registry/uptime.js +154 -0
  79. package/server.js +105 -44
  80. package/shl/shl.js +64 -9
  81. package/stats-db.js +93 -0
  82. package/stats.js +158 -1
  83. package/testing/search.js +2 -108
  84. package/testing/store.js +5 -0
  85. package/testing/testing.js +20 -0
  86. package/token/token-template.html +5 -4
  87. package/token/token.js +17 -10
  88. package/translations/Messages.properties +10 -0
  89. package/tx/README.md +52 -0
  90. package/tx/closure/closure-store.js +14 -0
  91. package/tx/cs/cs-api.js +92 -14
  92. package/tx/cs/cs-cs.js +64 -3
  93. package/tx/cs/cs-icd11.js +366 -112
  94. package/tx/cs/cs-icf.js +431 -0
  95. package/tx/cs/cs-loinc.js +296 -27
  96. package/tx/cs/cs-omop.js +10 -3
  97. package/tx/cs/cs-snomed.js +1210 -80
  98. package/tx/cs/cs-ucum.js +6 -0
  99. package/tx/cs/icd11-expressions.js +191 -78
  100. package/tx/importers/csv-records.js +35 -0
  101. package/tx/importers/export-icd11-codesystem.js +58 -12
  102. package/tx/importers/icd11-schema.md +9 -5
  103. package/tx/importers/import-loinc.module.js +177 -41
  104. package/tx/importers/import-sct.module.js +66 -1
  105. package/tx/importers/subset-loinc.module.js +25 -6
  106. package/tx/library/codesystem.js +4 -1
  107. package/tx/library/designations.js +23 -30
  108. package/tx/library.js +98 -4
  109. package/tx/openapi-operations.js +6 -1
  110. package/tx/params.js +8 -1
  111. package/tx/provider.js +24 -6
  112. package/tx/sct/documentation.md +251 -0
  113. package/tx/sct/ecl.js +758 -46
  114. package/tx/sct/expressions.js +113 -52
  115. package/tx/sct/structures.js +8 -2
  116. package/tx/syndication/syndication-feed.js +218 -0
  117. package/tx/syndication/syndication-store.js +415 -0
  118. package/tx/tx-html.js +15 -1
  119. package/tx/tx.fhir.org.yml +3 -4
  120. package/tx/tx.js +144 -2
  121. package/tx/vs/vs-vsac.js +25 -6
  122. package/tx/workers/closure.js +20 -0
  123. package/tx/workers/expand.js +35 -8
  124. package/tx/workers/lookup.js +61 -6
  125. package/tx/workers/subsumes.js +1 -1
  126. package/tx/workers/translate.js +166 -15
  127. package/tx/workers/validate.js +138 -6
  128. package/utilities/make-sct-loinc-subset.js +250 -0
  129. package/utilities/make-sct-test-extension.js +264 -0
  130. package/xig/xig.js +32 -0
  131. package/registry/readme.md +0 -124
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generates the "national extension" content for the SNOMED CT test edition
4
+ * (module 31000003106, namespace 1000003) that the tx-ecosystem tests use.
5
+ *
6
+ * The test edition at tx/data/snomed is a subontology extract of the
7
+ * International Edition; everything in it except the module concept itself
8
+ * is International content. This script adds a handful of extension concepts
9
+ * whose definitions are built entirely from International concepts that are
10
+ * in the subset, so that national -> international translation can be tested:
11
+ *
12
+ * - one fully defined concept with a single ungrouped attribute (laterality)
13
+ * - one fully defined concept with role-grouped attributes, not equivalent
14
+ * to any International concept
15
+ * - one fully defined concept that is exactly equivalent to an existing
16
+ * International concept (same defining relationships)
17
+ * - one primitive concept with only an IS A (closest ancestor only)
18
+ * - one primitive concept directly under a top-level hierarchy (no useful ancestor)
19
+ * - one inactive concept with no historical association (nothing to translate to)
20
+ * - one inactive concept REPLACED BY an International concept
21
+ *
22
+ * Output is a set of RF2 snapshot files alongside the INT ones, with the
23
+ * TEST1000003 namespace tag in the filename. Rows are deterministic, so the
24
+ * script can be re-run safely; ids are allocated from item numbers >= 100001
25
+ * so they never collide with the extractor-generated ids.
26
+ *
27
+ * Usage: node utilities/make-sct-test-extension.js [tx/data/snomed]
28
+ */
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+ const crypto = require('crypto');
33
+
34
+ const root = process.argv[2] || path.join(__dirname, '..', 'tx', 'data', 'snomed');
35
+ const TAG = 'TEST1000003';
36
+ const ET = '20250909';
37
+ const MODULE = '31000003106';
38
+ const NAMESPACE = '1000003';
39
+ const EOL = '\r\n';
40
+
41
+ // --- SCTID construction -------------------------------------------------
42
+
43
+ // Verhoeff check digit
44
+ const d = [
45
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 2, 3, 4, 0, 6, 7, 8, 9, 5], [2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
46
+ [3, 4, 0, 1, 2, 8, 9, 5, 6, 7], [4, 0, 1, 2, 3, 9, 5, 6, 7, 8], [5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
47
+ [6, 5, 9, 8, 7, 1, 0, 4, 3, 2], [7, 6, 5, 9, 8, 2, 1, 0, 4, 3], [8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
48
+ [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]];
49
+ const p = [
50
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 5, 7, 6, 2, 8, 3, 0, 9, 4], [5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
51
+ [8, 9, 1, 6, 0, 4, 3, 5, 2, 7], [9, 4, 5, 3, 1, 2, 6, 8, 7, 0], [4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
52
+ [2, 7, 9, 3, 8, 0, 6, 4, 1, 5], [7, 0, 4, 6, 8, 1, 5, 3, 9, 2]];
53
+ const inv = [0, 4, 3, 2, 1, 5, 6, 7, 8, 9];
54
+
55
+ function verhoeff(numStr) {
56
+ let c = 0;
57
+ const digits = numStr.split('').reverse().map(Number);
58
+ for (let i = 0; i < digits.length; i++) {
59
+ c = d[c][p[(i + 1) % 8][digits[i]]];
60
+ }
61
+ return inv[c];
62
+ }
63
+
64
+ function sctid(item, partition) {
65
+ const body = `${item}${NAMESPACE}${partition}`;
66
+ return body + verhoeff(body);
67
+ }
68
+
69
+ let nextItem = 100001;
70
+ const conceptId = () => sctid(nextItem++, '10');
71
+ const descriptionId = () => sctid(nextItem++, '11');
72
+ const relationshipId = () => sctid(nextItem++, '12');
73
+
74
+ // Deterministic UUID for refset members (name-based, so re-runs are stable)
75
+ function memberId(...parts) {
76
+ const h = crypto.createHash('sha1').update(['sct-test-ext', ...parts].join('|')).digest('hex');
77
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-5${h.slice(13, 16)}-a${h.slice(17, 20)}-${h.slice(20, 32)}`;
78
+ }
79
+
80
+ // --- RF2 constants ------------------------------------------------------
81
+
82
+ const FULLY_DEFINED = '900000000000073002';
83
+ const PRIMITIVE = '900000000000074008';
84
+ const FSN = '900000000000003001';
85
+ const SYNONYM = '900000000000013009';
86
+ const CASE_INSENSITIVE = '900000000000448009';
87
+ const IS_A = '116680003';
88
+ const INFERRED = '900000000000011006';
89
+ const EXISTENTIAL = '900000000000451002';
90
+ const EN_GB = '900000000000508004';
91
+ const EN_US = '900000000000509007';
92
+ const PREFERRED = '900000000000548007';
93
+ const ACCEPTABLE = '900000000000549004';
94
+ const OWL_AXIOM_REFSET = '733073007';
95
+ const ROLE_GROUP = '609096000';
96
+ const REPLACED_BY = '900000000000526001'; // the SAME AS refset concept is not in the subset
97
+
98
+ // --- The extension content ---------------------------------------------
99
+
100
+ // All destination/attribute ids below are International concepts present in
101
+ // the test subset. Each concept: fsn, synonyms (first is preferred),
102
+ // parents, groups (array of arrays of [typeId, destinationId]; group 0 =
103
+ // ungrouped), definition status, and the stated OWL axiom.
104
+ const concepts = [
105
+ {
106
+ key: 'right-kidney',
107
+ // shape 1: fully defined, one ungrouped attribute (Laterality is never grouped)
108
+ fsn: 'Structure of kidney adjacent to liver (body structure)',
109
+ synonyms: ['Right kidney (the one under the liver)', 'Liver-side kidney'],
110
+ parents: ['64033007'], // Kidney structure
111
+ ungrouped: [['272741003', '24028007']], // Laterality = Right
112
+ groups: [],
113
+ status: FULLY_DEFINED,
114
+ owl: (id) => `EquivalentClasses(:${id} ObjectIntersectionOf(:64033007 ObjectSomeValuesFrom(:272741003 :24028007)))`
115
+ },
116
+ {
117
+ key: 'pate-hepatitis',
118
+ // shape 2: fully defined, role-grouped, no International equivalent
119
+ fsn: 'Acute toxic hepatitis caused by liver pate (disorder)',
120
+ synonyms: ['Pate-induced hepatitis', 'Revenge of the liver'],
121
+ parents: ['4896000'], // Acute toxic hepatitis
122
+ ungrouped: [],
123
+ groups: [
124
+ [['116676008', '4532008'], // Associated morphology = Acute inflammation
125
+ ['246075003', '227014008'], // Causative agent = Liver pate
126
+ ['363698007', '10200004']], // Finding site = Liver structure
127
+ [['263502005', '424124008']] // Clinical course = Sudden onset AND/OR short duration
128
+ ],
129
+ status: FULLY_DEFINED,
130
+ // Same shape as the International axiom for 4896000, with the causative agent refined
131
+ owl: (id) => `EquivalentClasses(:${id} ObjectIntersectionOf(:75478009 ObjectSomeValuesFrom(:${ROLE_GROUP} ObjectIntersectionOf(ObjectSomeValuesFrom(:116676008 :4532008) ObjectSomeValuesFrom(:246075003 :227014008) ObjectSomeValuesFrom(:363698007 :10200004))) ObjectSomeValuesFrom(:${ROLE_GROUP} ObjectSomeValuesFrom(:263502005 :424124008))))`
132
+ },
133
+ {
134
+ key: 'us-liver-biopsy',
135
+ // shape 3: fully defined, exactly equivalent to 438300009 |Biopsy of liver using ultrasound guidance|
136
+ fsn: 'Liver biopsy using the ultrasound thingy (procedure)',
137
+ synonyms: ['Ultrasound-guided liver biopsy (local dialect)'],
138
+ parents: ['105377009', '86259008'], // Ultrasonography of liver, Biopsy of liver
139
+ ungrouped: [],
140
+ groups: [
141
+ [['260686004', '129314006'], // Method = Biopsy - action
142
+ ['405813007', '10200004']], // Procedure site - Direct = Liver structure
143
+ [['260686004', '278292003'], // Method = Ultrasound imaging - action
144
+ ['363703001', '429892002'], // Has intent = Guidance intent
145
+ ['405813007', '10200004']] // Procedure site - Direct = Liver structure
146
+ ],
147
+ status: FULLY_DEFINED,
148
+ owl: (id) => `EquivalentClasses(:${id} :438300009)`
149
+ },
150
+ {
151
+ key: 'liverish',
152
+ // shape 4: primitive, IS A only
153
+ fsn: 'Feeling liverish (finding)',
154
+ synonyms: ['Liverish', 'Bit liverish this morning'],
155
+ parents: ['300338006'], // Liver problem
156
+ ungrouped: [],
157
+ groups: [],
158
+ status: PRIMITIVE,
159
+ owl: (id) => `SubClassOf(:${id} :300338006)`
160
+ },
161
+ {
162
+ key: 'vaguely-unwell',
163
+ // shape 5: primitive, directly under a top-level hierarchy - the only International
164
+ // ancestor carries no information, so there is no useful translation
165
+ fsn: 'Feeling vaguely unwell about the liver (finding)',
166
+ synonyms: ['Vaguely unwell about the liver'],
167
+ parents: ['404684003'], // Clinical finding
168
+ ungrouped: [],
169
+ groups: [],
170
+ status: PRIMITIVE,
171
+ owl: (id) => `SubClassOf(:${id} :404684003)`
172
+ },
173
+ {
174
+ key: 'dropsy',
175
+ // shape 6: inactive, no relationships, no historical association - nothing at all
176
+ fsn: 'Dropsy of the liver (disorder)',
177
+ synonyms: ['Hepatic dropsy'],
178
+ parents: [], ungrouped: [], groups: [],
179
+ status: PRIMITIVE,
180
+ active: false
181
+ },
182
+ {
183
+ key: 'hepatic-revenge',
184
+ // shape 7: inactive, REPLACED BY an International concept
185
+ fsn: 'Hepatic revenge syndrome (disorder)',
186
+ synonyms: ['Hepatic revenge'],
187
+ parents: [], ungrouped: [], groups: [],
188
+ status: PRIMITIVE,
189
+ active: false,
190
+ replacedBy: '4896000' // Acute toxic hepatitis
191
+ }
192
+ ];
193
+
194
+ // --- Row builders -------------------------------------------------------
195
+
196
+ const conceptRows = [];
197
+ const descriptionRows = [];
198
+ const relationshipRows = [];
199
+ const languageRows = [];
200
+ const owlRows = [];
201
+ const associationRows = [];
202
+ const summary = [];
203
+
204
+ for (const c of concepts) {
205
+ const id = conceptId();
206
+ const active = c.active === false ? '0' : '1';
207
+ conceptRows.push([id, ET, active, MODULE, c.status]);
208
+
209
+ const terms = [[FSN, c.fsn, PREFERRED], ...c.synonyms.map((t, i) => [SYNONYM, t, i === 0 ? PREFERRED : ACCEPTABLE])];
210
+ for (const [typeId, term, acceptability] of terms) {
211
+ const did = descriptionId();
212
+ descriptionRows.push([did, ET, '1', MODULE, id, 'en', typeId, term, CASE_INSENSITIVE]);
213
+ for (const refset of [EN_GB, EN_US]) {
214
+ languageRows.push([memberId('lang', refset, did), ET, '1', MODULE, refset, did, acceptability]);
215
+ }
216
+ }
217
+
218
+ for (const parent of c.parents) {
219
+ relationshipRows.push([relationshipId(), ET, '1', MODULE, id, parent, '0', IS_A, INFERRED, EXISTENTIAL]);
220
+ }
221
+ for (const [typeId, dest] of c.ungrouped) {
222
+ relationshipRows.push([relationshipId(), ET, '1', MODULE, id, dest, '0', typeId, INFERRED, EXISTENTIAL]);
223
+ }
224
+ c.groups.forEach((group, gi) => {
225
+ for (const [typeId, dest] of group) {
226
+ relationshipRows.push([relationshipId(), ET, '1', MODULE, id, dest, String(gi + 1), typeId, INFERRED, EXISTENTIAL]);
227
+ }
228
+ });
229
+
230
+ if (c.owl) {
231
+ owlRows.push([memberId('owl', id), ET, '1', MODULE, OWL_AXIOM_REFSET, id, c.owl(id)]);
232
+ }
233
+ if (c.replacedBy) {
234
+ associationRows.push([memberId('assoc', REPLACED_BY, id), ET, '1', MODULE, REPLACED_BY, id, c.replacedBy]);
235
+ }
236
+ summary.push(`${id} ${c.fsn}${c.active === false ? ' (inactive)' : ''}`);
237
+ }
238
+
239
+ // --- Write files --------------------------------------------------------
240
+
241
+ function write(rel, header, rows) {
242
+ const file = path.join(root, rel);
243
+ fs.mkdirSync(path.dirname(file), { recursive: true });
244
+ fs.writeFileSync(file, [header, ...rows.map(r => r.join('\t'))].join(EOL) + EOL, 'utf8');
245
+ console.log(`${rows.length.toString().padStart(3)} rows ${rel}`);
246
+ }
247
+
248
+ write(`Terminology/sct2_Concept_Snapshot_${TAG}_${ET}.txt`,
249
+ 'id\teffectiveTime\tactive\tmoduleId\tdefinitionStatusId', conceptRows);
250
+ write(`Terminology/sct2_Description_Snapshot-en_${TAG}_${ET}.txt`,
251
+ 'id\teffectiveTime\tactive\tmoduleId\tconceptId\tlanguageCode\ttypeId\tterm\tcaseSignificanceId', descriptionRows);
252
+ write(`Terminology/sct2_Relationship_Snapshot_${TAG}_${ET}.txt`,
253
+ 'id\teffectiveTime\tactive\tmoduleId\tsourceId\tdestinationId\trelationshipGroup\ttypeId\tcharacteristicTypeId\tmodifierId', relationshipRows);
254
+ write(`Terminology/sct2_sRefset_OWLExpressionSnapshot_${TAG}_${ET}.txt`,
255
+ 'id\teffectiveTime\tactive\tmoduleId\trefsetId\treferencedComponentId\towlExpression', owlRows);
256
+ // the IG master keeps the content refsets under Refset/Content; FHIRsmith's copy has them in Refset/
257
+ const contentDir = fs.existsSync(path.join(root, 'Refset', 'Content')) ? 'Refset/Content' : 'Refset';
258
+ write(`${contentDir}/der2_cRefset_AssociationSnapshot_${TAG}_${ET}.txt`,
259
+ 'id\teffectiveTime\tactive\tmoduleId\trefsetId\treferencedComponentId\ttargetComponentId', associationRows);
260
+ write(`Refset/Language/der2_cRefset_LanguageSnapshot-en_${TAG}_${ET}.txt`,
261
+ 'id\teffectiveTime\tactive\tmoduleId\trefsetId\treferencedComponentId\tacceptabilityId', languageRows);
262
+
263
+ console.log('\nConcepts:');
264
+ for (const s of summary) console.log(' ' + s);
package/xig/xig.js CHANGED
@@ -3251,6 +3251,37 @@ async function initializeXigModule(stats, xigConfig) {
3251
3251
  }
3252
3252
 
3253
3253
  // Graceful shutdown
3254
+ /**
3255
+ * The paragraph for the server's daily report: pages served, and whether the
3256
+ * database can be read (it's downloaded afresh each day, and a failed download
3257
+ * or an unreadable file is what this is for).
3258
+ */
3259
+ async function dailyReport({since, now}) {
3260
+ const {plural} = require('../library/daily-report');
3261
+ const sentences = [];
3262
+ if (globalStats) {
3263
+ const counts = globalStats.countsSince(since, now);
3264
+ sentences.push(`${plural(counts.total, 'page')} ${counts.total === 1 ? 'was' : 'were'} served.`);
3265
+ }
3266
+ if (!xigDb) {
3267
+ sentences.push('The database is not open: it could not be accessed.');
3268
+ } else {
3269
+ try {
3270
+ const row = await new Promise((resolve, reject) =>
3271
+ xigDb.get('SELECT COUNT(*) AS n FROM Resources', [], (err, row) => err ? reject(err) : resolve(row)));
3272
+ sentences.push(`The database holds ${plural(row.n, 'resource')}.`);
3273
+ } catch (e) {
3274
+ sentences.push(`The database could not be accessed: ${e.message}.`);
3275
+ }
3276
+ }
3277
+ const last = getLastUpdateAttempt();
3278
+ if (last && last.status === 'failed') {
3279
+ const when = last.timestamp instanceof Date ? last.timestamp.toISOString().substring(0, 16).replace('T', ' ') : String(last.timestamp);
3280
+ sentences.push(`The last database update (${when}) failed${last.error ? ': ' + last.error : ''}.`);
3281
+ }
3282
+ return sentences.join(' ');
3283
+ }
3284
+
3254
3285
  function shutdown() {
3255
3286
  return new Promise((resolve) => {
3256
3287
  if (xigDb) {
@@ -3275,6 +3306,7 @@ module.exports = {
3275
3306
  updateXigDatabase,
3276
3307
  getDatabaseInfo,
3277
3308
  shutdown,
3309
+ dailyReport,
3278
3310
  initializeXigModule,
3279
3311
 
3280
3312
  // Cache functions
@@ -1,124 +0,0 @@
1
- # Terminology Server Registry
2
-
3
- The coordination server of a terminology server ecosystem, as described by the
4
- [terminology ecosystem IG](https://build.fhir.org/ig/HL7/fhir-tx-ecosystem-ig/ecosystem.html).
5
- It regularly scans the servers listed in the ecosystem's master registration file, and tells
6
- clients which terminology server to use for a code system or value set. It runs at
7
- http://tx.fhir.org/tx-reg for the HL7 ecosystem.
8
-
9
- Its main client is the HL7 Java tooling (`TerminologyClientManager` in org.hl7.fhir.r5),
10
- which calls `/resolve` to route each terminology operation to the right server.
11
-
12
- ## API
13
-
14
- The public API is described by an OpenAPI 3.1 spec:
15
-
16
- | URL | |
17
- |---|---|
18
- | `/tx-reg/openapi` | Browsable reference (HTML), with a "try it" form for each GET operation |
19
- | `/tx-reg/openapi.json` | The spec as JSON |
20
- | `/tx-reg/openapi.yaml` | The spec as YAML (the source file, [openapi.yaml](openapi.yaml)) |
21
-
22
- Every response carries a `Link: </tx-reg/openapi.json>; rel="service-desc"` header
23
- (RFC 8631), and the HTML pages carry the matching `<link>` element.
24
-
25
- | Endpoint | Purpose |
26
- |---|---|
27
- | `GET /tx-reg/` | **Discovery**: the server endpoints in the ecosystem. Filters: `registry`, `server`, `fhirVersion`, `url`, `authoritativeOnly`, `language` |
28
- | `GET /tx-reg/resolve` | **Resolution**: which endpoints to use for a code system (`url`) or value set (`valueSet`) at a `fhirVersion`. Also `authoritativeOnly`, `language`, `usage`, `version` |
29
-
30
- Both return an HTML page instead of JSON when the request's `Accept` header contains
31
- `text/html`. `/tx-reg/resolve` without parameters is a form for trying it out.
32
- `/tx-reg/log` (the crawler log) is operational, and not in the spec.
33
-
34
- Parameters are read leniently: unknown parameters are ignored, and a repeated parameter
35
- takes its first value.
36
-
37
- ### How resolution decides
38
-
39
- * An endpoint is only returned for content it actually hosts, as reported by its
40
- TerminologyCapabilities (code systems) and ValueSet search (value sets).
41
- * A SNOMED CT edition (`http://snomed.info/sct|http://snomed.info/sct/{edition}`) is hosted
42
- by any endpoint that hosts a version of that edition. Otherwise SNOMED CT versions match
43
- exactly; for other code systems, a server that hosts the code system hosts all its
44
- versions.
45
- * Endpoints whose server claims authority (the `authoritative` masks in its registration)
46
- are listed as `authoritative`; the others as `candidates`.
47
- * With `language`, servers with a matching language specific claim (`languages` in the
48
- registration) come first, most specific tag first; candidates are marked
49
- `language-support: unknown`.
50
- * A server with a `usage` list is only returned when the request's `usage` is in it.
51
- * A server's `exclusions` hide the matching content from it entirely.
52
- * FHIR versions can be given as release codes (`R4`, `R4B`, `R5`, `R6` ...) or numbers
53
- (`4.0.1`, `4.0`).
54
-
55
- ### Differences from the ecosystem IG
56
-
57
- * Discovery: without `url`, a row's `authoritative`/`authoritative-valuesets` are the
58
- server's claim masks, and there are no candidate lists. With `url`, `candidate` is
59
- `[url]` when the endpoint hosts it without claiming authority.
60
- * Entries carry the IG's security flags (`open`, `token`...) and also a `security` string
61
- (`open` or `api-key`). Note that this records how the *registry* reaches the server: an
62
- endpoint is `api-key` when the registry is configured with a key for it (`apiKeys`), and
63
- `open` otherwise.
64
- * Resolve takes a `version` parameter, as an alternative to `url|version`.
65
- * Resolve omits `authoritative` and `candidates` when they are empty.
66
-
67
- ### Keeping the spec honest
68
-
69
- [openapi.yaml](openapi.yaml) is maintained by hand. `tests/registry/openapi.test.js` fails
70
- if a route is added without being described (or listed in the test's `EXCLUDED` table), if
71
- the spec describes a route the router doesn't have, or if the documented query parameters
72
- differ from `DISCOVERY_PARAMS` and `RESOLVE_PARAMS` in `registry.js`. So when you change a
73
- route or a parameter, change `openapi.yaml` with it.
74
-
75
- ## Configuration
76
-
77
- In the server config, under `modules.registry`:
78
-
79
- ```json
80
- "registry": {
81
- "enabled": true,
82
- "masterUrl": "https://fhir.github.io/ig-registry/tx-servers.json",
83
- "crawlInterval": 30,
84
- "timeout": 30000,
85
- "userAgent": "YourServer/1.0",
86
- "apiKeys": {}
87
- }
88
- ```
89
-
90
- | Setting | |
91
- |---|---|
92
- | `masterUrl` | The ecosystem's master registration file. Defaults to the HL7 one. |
93
- | `crawlInterval` | Minutes between scans. 0 or absent: no scanning. |
94
- | `timeout` | Per-request timeout for scanning, in milliseconds. |
95
- | `userAgent` | The User-Agent the scanner sends. |
96
- | `apiKeys` | API keys for servers that need one, by server code. |
97
-
98
- ## Scanning
99
-
100
- Each scan reads the master registration file, each registry it lists, and then, for each
101
- server endpoint:
102
-
103
- * `/metadata` (the CapabilityStatement, for the software name and version)
104
- * `/metadata?mode=terminology` (the TerminologyCapabilities, for the code systems and
105
- versions it hosts)
106
- * `/ValueSet?_elements=url,version` (the value sets it hosts)
107
-
108
- An endpoint that fails keeps the content found by its last successful scan, and reports the
109
- error. The scanner refuses to fetch from private or loopback addresses (SSRF protection).
110
-
111
- The results are saved to `[data]/registry/registry-data.json` after each scan, and loaded at
112
- startup, so the registry can answer immediately after a restart.
113
-
114
- ## Code
115
-
116
- | File | |
117
- |---|---|
118
- | `registry.js` | The module: routes, HTML pages, scan scheduling |
119
- | `api.js` | Discovery and resolution |
120
- | `crawler.js` | Scanning |
121
- | `model.js` | The data model, and mask and version matching |
122
- | `openapi.yaml`, `openapi.js` | The API description |
123
-
124
- Tests are in `tests/registry`.