fhirsmith 0.17.0 → 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.
Files changed (121) hide show
  1. package/CHANGELOG.md +83 -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 +422 -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 +7 -2
  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 +9 -0
  89. package/tx/README.md +52 -0
  90. package/tx/cs/cs-api.js +90 -14
  91. package/tx/cs/cs-cs.js +56 -1
  92. package/tx/cs/cs-icd11.js +366 -112
  93. package/tx/cs/cs-icf.js +418 -0
  94. package/tx/cs/cs-loinc.js +47 -25
  95. package/tx/cs/cs-omop.js +10 -3
  96. package/tx/cs/cs-snomed.js +918 -55
  97. package/tx/cs/cs-ucum.js +6 -0
  98. package/tx/cs/icd11-expressions.js +191 -78
  99. package/tx/importers/export-icd11-codesystem.js +58 -12
  100. package/tx/importers/icd11-schema.md +9 -5
  101. package/tx/library.js +98 -4
  102. package/tx/openapi-operations.js +4 -0
  103. package/tx/params.js +8 -1
  104. package/tx/provider.js +16 -4
  105. package/tx/sct/documentation.md +251 -0
  106. package/tx/sct/ecl.js +758 -46
  107. package/tx/sct/expressions.js +113 -52
  108. package/tx/syndication/syndication-feed.js +218 -0
  109. package/tx/syndication/syndication-store.js +415 -0
  110. package/tx/tx-html.js +15 -1
  111. package/tx/tx.fhir.org.yml +2 -3
  112. package/tx/tx.js +144 -2
  113. package/tx/vs/vs-vsac.js +25 -6
  114. package/tx/workers/expand.js +33 -6
  115. package/tx/workers/lookup.js +61 -6
  116. package/tx/workers/subsumes.js +1 -1
  117. package/tx/workers/translate.js +166 -15
  118. package/tx/workers/validate.js +128 -6
  119. package/utilities/make-sct-test-extension.js +264 -0
  120. package/xig/xig.js +32 -0
  121. package/registry/readme.md +0 -124
@@ -0,0 +1,179 @@
1
+ //
2
+ // Copyright 2026, Health Intersections Pty Ltd (http://www.healthintersections.com.au)
3
+ //
4
+ // Licensed under BSD-3: https://opensource.org/license/bsd-3-clause
5
+ //
6
+
7
+ /**
8
+ * Value set membership for entryIn() and resourcesIn() (fhirpath-ips.js): is a
9
+ * CodeableConcept in a value set? Asked of the terminology server ($validate-code, with
10
+ * the concept), as the rules are evaluated - only what a rule actually reaches is asked.
11
+ *
12
+ * A session is one run of rules over one document. It asks each question once (questions
13
+ * in flight are shared), keeps to a budget, and remembers what it couldn't find out, so a
14
+ * rule that failed because the server couldn't answer can be told from one that plainly
15
+ * failed. Definite answers (true, false) are also kept in a cache that outlives the session
16
+ * (the module's), so the same concept in the next document isn't asked again; an answer
17
+ * the server couldn't give is not kept beyond the session.
18
+ *
19
+ * The module's own value sets (ips/valuesets/) go with the question, with what they draw on,
20
+ * so any server can answer for them; any other is asked by its canonical, and must be one
21
+ * the server knows.
22
+ *
23
+ * @module ips/membership
24
+ */
25
+
26
+ const DEFAULT_MAX_QUESTIONS = 500;
27
+ const MAX_CACHED = 20000;
28
+
29
+ function isObject(v) {
30
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
31
+ }
32
+
33
+ /** The codings that matter, in a stable order: the concept's identity for the cache. */
34
+ function plainConcept(concept) {
35
+ const coding = (Array.isArray(concept?.coding) ? concept.coding : [])
36
+ .filter(c => isObject(c) && typeof c.system === 'string' && typeof c.code === 'string')
37
+ .map(c => {
38
+ const p = { system: c.system, code: c.code };
39
+ if (typeof c.version === 'string') {
40
+ p.version = c.version;
41
+ }
42
+ return p;
43
+ });
44
+ return { coding };
45
+ }
46
+
47
+ function keyOf(url, concept) {
48
+ return `member|${url}|` + concept.coding.map(c => `${c.system}|${c.version || ''}|${c.code}`).sort().join(',');
49
+ }
50
+
51
+ /** What a $validate-code response says: true, false, or null with why it said neither. */
52
+ function answerOf(r, internal) {
53
+ if (!r) {
54
+ return { result: null, message: 'The terminology server could not be reached' };
55
+ }
56
+ const res = r.resource;
57
+ const issueText = (oo) => (Array.isArray(oo?.issue) ? oo.issue : []).map(i => i?.details?.text || i?.diagnostics).filter(Boolean).join('; ');
58
+ if (res?.resourceType === 'Parameters') {
59
+ const result = (res.parameter || []).find(p => p?.name === 'result');
60
+ if (result && typeof result.valueBoolean === 'boolean') {
61
+ return { result: result.valueBoolean, message: null };
62
+ }
63
+ const issues = (res.parameter || []).find(p => p?.name === 'issues');
64
+ return { result: null, message: issueText(issues?.resource) || 'The terminology server gave no result' };
65
+ }
66
+ if (res?.resourceType === 'OperationOutcome') {
67
+ return { result: null, message: issueText(res) || `The terminology server answered HTTP ${r.status}` };
68
+ }
69
+ if (r.status === 404 && internal) {
70
+ return { result: null, message: 'The terminology server is not configured correctly (see the server log)' };
71
+ }
72
+ return { result: null, message: `The terminology server answered HTTP ${r.status}` };
73
+ }
74
+
75
+ class MembershipSession {
76
+ /**
77
+ * @param {Object|null} tx - the module's TxClient (none: nothing can be answered)
78
+ * @param {Object|null} valueSets - the module's IpsValueSets
79
+ * @param {{cache?: Map, maxQuestions?: number, log?: Object}} [options] - cache: kept
80
+ * beyond the session (the module's), for definite answers
81
+ */
82
+ constructor(tx, valueSets, options = {}) {
83
+ this.tx = tx;
84
+ this.valueSets = valueSets;
85
+ this.cache = options.cache || null;
86
+ this.maxQuestions = Number.isInteger(options.maxQuestions) && options.maxQuestions >= 0 ? options.maxQuestions : DEFAULT_MAX_QUESTIONS;
87
+ this.log = options.log || null;
88
+ this.questions = 0;
89
+ this.answers = new Map();
90
+ // what couldn't be found out: value set -> message
91
+ this.problems = new Map();
92
+ // what fhirpath-ips.js's membership option wants
93
+ this.membership = (url, concept) => this.check(url, concept);
94
+ }
95
+
96
+ /**
97
+ * Is the concept in the value set?
98
+ * @returns {boolean|Promise<boolean|null>} at once, when it is known (or can't be in it);
99
+ * null when it couldn't be found out
100
+ */
101
+ check(url, concept) {
102
+ const plain = plainConcept(concept);
103
+ if (!plain.coding.length) {
104
+ return false;
105
+ }
106
+ const key = keyOf(url, plain);
107
+ if (this.answers.has(key)) {
108
+ return this.answers.get(key);
109
+ }
110
+ if (this.cache && this.cache.has(key)) {
111
+ const known = this.cache.get(key);
112
+ this.answers.set(key, known);
113
+ return known;
114
+ }
115
+ const pending = this.ask(url, plain).then(a => {
116
+ this.answers.set(key, a.result);
117
+ if (a.result === null) {
118
+ this.problem(url, a.message);
119
+ } else if (this.cache) {
120
+ if (this.cache.size >= MAX_CACHED) {
121
+ this.cache.delete(this.cache.keys().next().value);
122
+ }
123
+ this.cache.set(key, a.result);
124
+ }
125
+ return a.result;
126
+ });
127
+ this.answers.set(key, pending);
128
+ return pending;
129
+ }
130
+
131
+ problem(url, message) {
132
+ if (!this.problems.has(url)) {
133
+ this.problems.set(url, message);
134
+ }
135
+ }
136
+
137
+ async ask(url, concept) {
138
+ if (!this.tx) {
139
+ return { result: null, message: 'No terminology server is configured' };
140
+ }
141
+ if (this.questions >= this.maxQuestions) {
142
+ return { result: null, message: `More than ${this.maxQuestions} value set questions for one document` };
143
+ }
144
+ this.questions++;
145
+ const bar = url.indexOf('|');
146
+ const canonical = bar < 0 ? url : url.substring(0, bar);
147
+ const version = bar < 0 ? null : url.substring(bar + 1);
148
+ const params = { resourceType: 'Parameters', parameter: [] };
149
+ const own = this.valueSets ? this.valueSets.valueSet(canonical) : null;
150
+ if (own && (!version || own.version === version)) {
151
+ params.parameter.push({ name: 'valueSet', resource: own });
152
+ for (const r of this.valueSets.referencedResources(canonical)) {
153
+ params.parameter.push({ name: 'tx-resource', resource: r });
154
+ }
155
+ } else {
156
+ params.parameter.push({ name: 'url', valueUri: canonical });
157
+ if (version) {
158
+ params.parameter.push({ name: 'valueSetVersion', valueString: version });
159
+ }
160
+ }
161
+ params.parameter.push({ name: 'codeableConcept', valueCodeableConcept: concept });
162
+ let r = null;
163
+ try {
164
+ r = await this.tx.post('/ValueSet/$validate-code', params);
165
+ } catch (e) {
166
+ if (this.log) {
167
+ this.log.error(`IPS module: the terminology server could not be reached: ${e.message}`);
168
+ }
169
+ }
170
+ return answerOf(r, this.tx.internal);
171
+ }
172
+
173
+ /** What couldn't be found out, by value set: [{valueSet, message}]. */
174
+ unresolved() {
175
+ return [...this.problems].map(([valueSet, message]) => ({ valueSet, message }));
176
+ }
177
+ }
178
+
179
+ module.exports = { MembershipSession, plainConcept, DEFAULT_MAX_QUESTIONS };