fhirsmith 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +113 -0
- package/README.md +80 -13
- package/config-template.json +106 -1
- package/ips/ai-client.js +263 -0
- package/ips/composer.js +805 -0
- package/ips/editor-schema.js +289 -0
- package/ips/editor.js +290 -0
- package/ips/fhirpath-ips.js +397 -0
- package/ips/ips-codeable-concepts.md +1742 -0
- package/ips/ips-template.html +125 -0
- package/ips/ips-valuesets.js +159 -0
- package/ips/ips.js +1736 -0
- package/ips/membership.js +179 -0
- package/ips/pipeline.js +1012 -0
- package/ips/presenter.js +974 -0
- package/ips/profile-editor.js +578 -0
- package/ips/profiles.js +506 -0
- package/ips/readme.md +460 -0
- package/ips/render.js +1482 -0
- package/ips/search-params.js +83 -0
- package/ips/search.js +667 -0
- package/ips/share.js +178 -0
- package/ips/store.js +650 -0
- package/ips/terminology.js +395 -0
- package/ips/tx-client.js +202 -0
- package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
- package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
- package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
- package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
- package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
- package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
- package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
- package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
- package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
- package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
- package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
- package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
- package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-medication.json +50 -0
- package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
- package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
- package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
- package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
- package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
- package/ips/valuesets/ValueSet-ips-route.json +46 -0
- package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
- package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
- package/ips/valuesets/elements.json +44 -0
- package/library/daily-report.js +432 -0
- package/library/fhir-search-values.js +138 -0
- package/library/html-server.js +32 -23
- package/library/mailer.js +77 -0
- package/library/narrative-check.js +21 -2
- package/library/npm-audit.js +25 -0
- package/library/package-manager.js +4 -2
- package/library/sqlite-session-store.js +29 -1
- package/package.json +8 -3
- package/packages/packages.js +34 -3
- package/publisher/publisher.js +29 -0
- package/registry/auth-store.js +366 -0
- package/registry/auth.js +872 -0
- package/registry/crawler.js +41 -1
- package/registry/documentation/api.md +162 -0
- package/registry/documentation/configuration.md +93 -0
- package/registry/documentation/documents.md +74 -0
- package/registry/documentation/internals.md +151 -0
- package/registry/documentation/readme.md +100 -0
- package/registry/documentation/security.md +199 -0
- package/registry/documents.js +309 -0
- package/registry/openapi.yaml +30 -0
- package/registry/registry-template.html +1 -1
- package/registry/registry.js +230 -44
- package/registry/uptime.js +154 -0
- package/server.js +105 -44
- package/shl/shl.js +64 -9
- package/stats-db.js +93 -0
- package/stats.js +158 -1
- package/testing/search.js +2 -108
- package/testing/store.js +5 -0
- package/testing/testing.js +20 -0
- package/token/token-template.html +5 -4
- package/token/token.js +17 -10
- package/translations/Messages.properties +10 -0
- package/tx/README.md +52 -0
- package/tx/closure/closure-store.js +14 -0
- package/tx/cs/cs-api.js +92 -14
- package/tx/cs/cs-cs.js +64 -3
- package/tx/cs/cs-icd11.js +366 -112
- package/tx/cs/cs-icf.js +431 -0
- package/tx/cs/cs-loinc.js +296 -27
- package/tx/cs/cs-omop.js +10 -3
- package/tx/cs/cs-snomed.js +1210 -80
- package/tx/cs/cs-ucum.js +6 -0
- package/tx/cs/icd11-expressions.js +191 -78
- package/tx/importers/csv-records.js +35 -0
- package/tx/importers/export-icd11-codesystem.js +58 -12
- package/tx/importers/icd11-schema.md +9 -5
- package/tx/importers/import-loinc.module.js +177 -41
- package/tx/importers/import-sct.module.js +66 -1
- package/tx/importers/subset-loinc.module.js +25 -6
- package/tx/library/codesystem.js +4 -1
- package/tx/library/designations.js +23 -30
- package/tx/library.js +98 -4
- package/tx/openapi-operations.js +6 -1
- package/tx/params.js +8 -1
- package/tx/provider.js +24 -6
- package/tx/sct/documentation.md +251 -0
- package/tx/sct/ecl.js +758 -46
- package/tx/sct/expressions.js +113 -52
- package/tx/sct/structures.js +8 -2
- package/tx/syndication/syndication-feed.js +218 -0
- package/tx/syndication/syndication-store.js +415 -0
- package/tx/tx-html.js +15 -1
- package/tx/tx.fhir.org.yml +3 -4
- package/tx/tx.js +144 -2
- package/tx/vs/vs-vsac.js +25 -6
- package/tx/workers/closure.js +20 -0
- package/tx/workers/expand.js +35 -8
- package/tx/workers/lookup.js +61 -6
- package/tx/workers/subsumes.js +1 -1
- package/tx/workers/translate.js +166 -15
- package/tx/workers/validate.js +138 -6
- package/utilities/make-sct-loinc-subset.js +250 -0
- package/utilities/make-sct-test-extension.js +264 -0
- package/xig/xig.js +32 -0
- package/registry/readme.md +0 -124
|
@@ -0,0 +1,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 };
|