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.
- package/CHANGELOG.md +83 -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 +422 -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 +7 -2
- 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 +9 -0
- package/tx/README.md +52 -0
- package/tx/cs/cs-api.js +90 -14
- package/tx/cs/cs-cs.js +56 -1
- package/tx/cs/cs-icd11.js +366 -112
- package/tx/cs/cs-icf.js +418 -0
- package/tx/cs/cs-loinc.js +47 -25
- package/tx/cs/cs-omop.js +10 -3
- package/tx/cs/cs-snomed.js +918 -55
- package/tx/cs/cs-ucum.js +6 -0
- package/tx/cs/icd11-expressions.js +191 -78
- package/tx/importers/export-icd11-codesystem.js +58 -12
- package/tx/importers/icd11-schema.md +9 -5
- package/tx/library.js +98 -4
- package/tx/openapi-operations.js +4 -0
- package/tx/params.js +8 -1
- package/tx/provider.js +16 -4
- package/tx/sct/documentation.md +251 -0
- package/tx/sct/ecl.js +758 -46
- package/tx/sct/expressions.js +113 -52
- package/tx/syndication/syndication-feed.js +218 -0
- package/tx/syndication/syndication-store.js +415 -0
- package/tx/tx-html.js +15 -1
- package/tx/tx.fhir.org.yml +2 -3
- package/tx/tx.js +144 -2
- package/tx/vs/vs-vsac.js +25 -6
- package/tx/workers/expand.js +33 -6
- 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 +128 -6
- package/utilities/make-sct-test-extension.js +264 -0
- package/xig/xig.js +32 -0
- package/registry/readme.md +0 -124
|
@@ -0,0 +1,397 @@
|
|
|
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
|
+
* Two FHIRPath functions for IPS documents, for fhirpath.js (a user invocation table):
|
|
9
|
+
*
|
|
10
|
+
* section(system, code [, min, max])
|
|
11
|
+
* The input: documents (Bundles) or Compositions. On a document, section() moves into
|
|
12
|
+
* its Composition (the Bundle's first Composition entry) by itself, so the rules of a
|
|
13
|
+
* clinical profile, which are on the document, can say Bundle.section(...). For each
|
|
14
|
+
* input, the Composition's sections, nested ones too, whose
|
|
15
|
+
* code has a coding with that system and code. When the number found is within
|
|
16
|
+
* min..max, they are all in the output; when it is not, none of that item's are.
|
|
17
|
+
*
|
|
18
|
+
* entry(system, code [, min, max])
|
|
19
|
+
* The input: sections (usually from section(), but any section will do). For each,
|
|
20
|
+
* its entries, resolved, whose code (code, medicationCodeableConcept or vaccineCode)
|
|
21
|
+
* has a coding with that system and code. Within min..max, they are all in the
|
|
22
|
+
* output; otherwise none of that section's are.
|
|
23
|
+
*
|
|
24
|
+
* resources(system, code [, type])
|
|
25
|
+
* The input: documents (Bundles), or anything in one (a Composition, say). Every
|
|
26
|
+
* resource in the Bundle whose code (as entry() matches it) has a coding with that
|
|
27
|
+
* system and code - and, when a type is given, that is of that type (Condition...).
|
|
28
|
+
*
|
|
29
|
+
* entryIn(valueSet [, min, max])
|
|
30
|
+
* resourcesIn(valueSet [, type])
|
|
31
|
+
* As entry() and resources(), with the code in a value set (a canonical, with |version
|
|
32
|
+
* or not) rather than a given code: a resource is in when any coding of its code is.
|
|
33
|
+
* Whether a code is in a value set is the caller's membership() (ipsFunctions), which
|
|
34
|
+
* usually asks a terminology server - asynchronously, so the evaluation must be
|
|
35
|
+
* (evaluateAsync); answers are best cached (membership.js).
|
|
36
|
+
*
|
|
37
|
+
* min and max are optional (together): with neither, there is no constraint; max may
|
|
38
|
+
* be '*'. Being outside the bounds is not an error - the item adds nothing.
|
|
39
|
+
*
|
|
40
|
+
* References are resolved in the Bundle the section is in: found by walking up from the
|
|
41
|
+
* section, or else the Bundle the expression was evaluated on (the evaluation's
|
|
42
|
+
* context). A reference is matched to an entry by fullUrl - absolute (urn:uuid:, http..)
|
|
43
|
+
* as is, relative (Type/id) against the base of the Composition's fullUrl, or by type
|
|
44
|
+
* and id when the entries have no usable fullUrl - and '#id' is a contained resource of
|
|
45
|
+
* the Composition. When there is no Bundle, or it doesn't have the resource, the
|
|
46
|
+
* caller's resolve(reference, composition) is asked, when there is one. A medication
|
|
47
|
+
* given by reference (medicationReference) is not followed.
|
|
48
|
+
*
|
|
49
|
+
* @module ips/fhirpath-ips
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
const fhirpath = require('fhirpath');
|
|
53
|
+
const r4Model = require('fhirpath/fhir-context/r4');
|
|
54
|
+
const { ResourceNode } = require('fhirpath/src/types.js');
|
|
55
|
+
|
|
56
|
+
const valData = fhirpath.util.valData;
|
|
57
|
+
// the elements of a resource that hold the code entry() matches
|
|
58
|
+
const CODE_ELEMENTS = ['code', 'medicationCodeableConcept', 'vaccineCode'];
|
|
59
|
+
|
|
60
|
+
function isObject(v) {
|
|
61
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A number as fhirpath.js gives it (an integer literal is one of its decimals), as a JS number. */
|
|
65
|
+
function num(v) {
|
|
66
|
+
return v && typeof v === 'object' && typeof v.toNumber === 'function' ? v.toNumber() : v;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** min and max from the arguments: min an Integer, max an Integer or '*'. */
|
|
70
|
+
function bounds(fnName, min, max) {
|
|
71
|
+
if (min === undefined) {
|
|
72
|
+
return { min: 0, max: Infinity };
|
|
73
|
+
}
|
|
74
|
+
min = num(min);
|
|
75
|
+
if (!Number.isInteger(min) || min < 0) {
|
|
76
|
+
throw new Error(`${fnName}(): min must be a non-negative integer`);
|
|
77
|
+
}
|
|
78
|
+
const m = Array.isArray(max) ? max.map(valData) : [max];
|
|
79
|
+
if (m.length !== 1) {
|
|
80
|
+
throw new Error(`${fnName}(): max must be an integer or '*'`);
|
|
81
|
+
}
|
|
82
|
+
const v = num(m[0]);
|
|
83
|
+
if (v === '*') {
|
|
84
|
+
return { min, max: Infinity };
|
|
85
|
+
}
|
|
86
|
+
if (!Number.isInteger(v)) {
|
|
87
|
+
throw new Error(`${fnName}(): max must be an integer or '*'`);
|
|
88
|
+
}
|
|
89
|
+
if (v < min) {
|
|
90
|
+
throw new Error(`${fnName}(): max must be an integer no less than min, or '*'`);
|
|
91
|
+
}
|
|
92
|
+
return { min, max: v };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whether a CodeableConcept has a coding with the system and code. */
|
|
96
|
+
function hasCoding(concept, system, code) {
|
|
97
|
+
return isObject(concept) && Array.isArray(concept.coding) &&
|
|
98
|
+
concept.coding.some(c => isObject(c) && c.system === system && c.code === code);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The nearest node above (or at) a node whose data is a resource of the type. */
|
|
102
|
+
function ancestor(node, resourceType) {
|
|
103
|
+
for (let n = node; n; n = n.parentResNode) {
|
|
104
|
+
if (n instanceof ResourceNode && valData(n)?.resourceType === resourceType) {
|
|
105
|
+
return n;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The Bundle node the evaluation was started on, if it was. */
|
|
112
|
+
function rootBundle(ctx) {
|
|
113
|
+
const root = Array.isArray(ctx.dataRoot) ? ctx.dataRoot : [ctx.dataRoot];
|
|
114
|
+
return root.find(n => valData(n)?.resourceType === 'Bundle') || null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The Composition for an input item, as a node: the item itself, or a document's first
|
|
119
|
+
* Composition entry (a node under the Bundle's, so the Bundle can be found from it).
|
|
120
|
+
*/
|
|
121
|
+
function compositionNode(ctx, item) {
|
|
122
|
+
const data = valData(item);
|
|
123
|
+
if (!isObject(data)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const node = item instanceof ResourceNode ? item : ResourceNode.makeResNode(ctx, data, null, null, null, null);
|
|
127
|
+
if (data.resourceType === 'Composition') {
|
|
128
|
+
return node;
|
|
129
|
+
}
|
|
130
|
+
if (data.resourceType !== 'Bundle' || !Array.isArray(data.entry)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const i = data.entry.findIndex(e => e?.resource?.resourceType === 'Composition');
|
|
134
|
+
if (i < 0) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const entry = ResourceNode.makeResNode(ctx, data.entry[i], node, 'Bundle.entry', null, 'BackboneElement', 'entry', i);
|
|
138
|
+
return ResourceNode.makeResNode(ctx, data.entry[i].resource, entry, 'Bundle.entry.resource', null, null, 'resource', null);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** All of a node's sections, nested ones after their parent, as nodes. */
|
|
142
|
+
function allSections(ctx, parent, out = []) {
|
|
143
|
+
const list = valData(parent)?.section;
|
|
144
|
+
if (!Array.isArray(list)) {
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
list.forEach((s, i) => {
|
|
148
|
+
if (isObject(s)) {
|
|
149
|
+
const node = ResourceNode.makeResNode(ctx, s, parent, 'Composition.section', null, 'BackboneElement', 'section', i);
|
|
150
|
+
out.push(node);
|
|
151
|
+
allSections(ctx, node, out);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The base of an absolute RESTful fullUrl ([base]/Type/id), or null. */
|
|
158
|
+
function baseOf(fullUrl) {
|
|
159
|
+
const m = typeof fullUrl === 'string' ? /^(https?:\/\/.+?\/)[A-Z][A-Za-z]+\/[A-Za-z0-9\-.]{1,64}(\/_history\/[A-Za-z0-9\-.]{1,64})?$/.exec(fullUrl) : null;
|
|
160
|
+
return m ? m[1] : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* A reference resolved in a Bundle: the entry index and resource, or null.
|
|
165
|
+
* @param {Object} bundle
|
|
166
|
+
* @param {string} reference
|
|
167
|
+
* @param {string|undefined} fromFullUrl - the fullUrl of the referring resource's entry
|
|
168
|
+
*/
|
|
169
|
+
function inBundle(bundle, reference, fromFullUrl) {
|
|
170
|
+
const entries = Array.isArray(bundle?.entry) ? bundle.entry : [];
|
|
171
|
+
const ref = reference.replace(/\/_history\/[^/]+$/, '');
|
|
172
|
+
const at = (pred) => {
|
|
173
|
+
const i = entries.findIndex(e => isObject(e) && isObject(e.resource) && pred(e));
|
|
174
|
+
return i < 0 ? null : { index: i, resource: entries[i].resource };
|
|
175
|
+
};
|
|
176
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(ref)) {
|
|
177
|
+
return at(e => e.fullUrl === ref);
|
|
178
|
+
}
|
|
179
|
+
const m = /^([A-Z][A-Za-z]+)\/([A-Za-z0-9\-.]{1,64})$/.exec(ref);
|
|
180
|
+
if (!m) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const base = baseOf(fromFullUrl);
|
|
184
|
+
if (base) {
|
|
185
|
+
const found = at(e => e.fullUrl === base + ref);
|
|
186
|
+
if (found) {
|
|
187
|
+
return found;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// no usable base: an entry with that type and id, whose fullUrl (if any) ends with it
|
|
191
|
+
return at(e => e.resource.resourceType === m[1] && e.resource.id === m[2] &&
|
|
192
|
+
(typeof e.fullUrl !== 'string' || e.fullUrl.endsWith('/' + ref) || !baseOf(e.fullUrl)));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* A section's entries, resolved, as nodes: in the Bundle the section is in (or the one the
|
|
197
|
+
* evaluation was started on), a resource contained in the Composition, or what the
|
|
198
|
+
* caller's resolve() gives. Unresolvable references are left out.
|
|
199
|
+
*/
|
|
200
|
+
function sectionEntryNodes(ctx, item, resolve) {
|
|
201
|
+
const s = valData(item);
|
|
202
|
+
if (!isObject(s) || !Array.isArray(s.entry)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const sectionNode = item instanceof ResourceNode ? item : null;
|
|
206
|
+
const compNode = ancestor(sectionNode, 'Composition');
|
|
207
|
+
const comp = compNode ? valData(compNode) : null;
|
|
208
|
+
const bundleNode = ancestor(sectionNode, 'Bundle') || rootBundle(ctx);
|
|
209
|
+
const bundle = bundleNode ? valData(bundleNode) : null;
|
|
210
|
+
const compEntry = compNode?.parentResNode && valData(compNode.parentResNode);
|
|
211
|
+
const fromFullUrl = isObject(compEntry) && typeof compEntry.fullUrl === 'string' ? compEntry.fullUrl
|
|
212
|
+
: (bundle && comp ? bundle.entry?.find(e => e?.resource === comp)?.fullUrl : undefined);
|
|
213
|
+
const nodes = [];
|
|
214
|
+
for (const ref of s.entry) {
|
|
215
|
+
const reference = isObject(ref) && typeof ref.reference === 'string' ? ref.reference : null;
|
|
216
|
+
if (!reference) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
let node = null;
|
|
220
|
+
if (reference.startsWith('#')) {
|
|
221
|
+
const c = Array.isArray(comp?.contained) ? comp.contained.findIndex(r => isObject(r) && r.id === reference.substring(1)) : -1;
|
|
222
|
+
if (c >= 0) {
|
|
223
|
+
node = ResourceNode.makeResNode(ctx, comp.contained[c], compNode, 'Composition.contained', null, null, 'contained', c);
|
|
224
|
+
}
|
|
225
|
+
} else if (bundle) {
|
|
226
|
+
const r = inBundle(bundle, reference, fromFullUrl);
|
|
227
|
+
if (r) {
|
|
228
|
+
const e = ResourceNode.makeResNode(ctx, bundle.entry[r.index], bundleNode, 'Bundle.entry', null, 'BackboneElement', 'entry', r.index);
|
|
229
|
+
node = ResourceNode.makeResNode(ctx, r.resource, e, 'Bundle.entry.resource', null, null, 'resource', null);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (!node && resolve) {
|
|
233
|
+
const r = resolve(reference, comp);
|
|
234
|
+
if (isObject(r) && r.resourceType) {
|
|
235
|
+
node = ResourceNode.makeResNode(ctx, r, null, null, null, null);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (node) {
|
|
239
|
+
nodes.push(node);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return nodes;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The resources of the documents the inputs are (or are in), as nodes; each document once. */
|
|
246
|
+
function documentResourceNodes(ctx, inputs, fnName, type) {
|
|
247
|
+
if (type !== undefined && (typeof type !== 'string' || !/^[A-Z][A-Za-z]{1,63}$/.test(type))) {
|
|
248
|
+
throw new Error(`${fnName}(): type must be a resource type`);
|
|
249
|
+
}
|
|
250
|
+
const out = [];
|
|
251
|
+
const seen = new Set();
|
|
252
|
+
for (const item of inputs) {
|
|
253
|
+
const data = valData(item);
|
|
254
|
+
const node = item instanceof ResourceNode ? item : (isObject(data) ? ResourceNode.makeResNode(ctx, data, null, null, null, null) : null);
|
|
255
|
+
const bundleNode = data?.resourceType === 'Bundle' ? node : (ancestor(node, 'Bundle') || rootBundle(ctx));
|
|
256
|
+
const bundle = bundleNode ? valData(bundleNode) : null;
|
|
257
|
+
if (!bundle || seen.has(bundle) || !Array.isArray(bundle.entry)) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
seen.add(bundle);
|
|
261
|
+
bundle.entry.forEach((e, i) => {
|
|
262
|
+
const r = isObject(e) ? e.resource : null;
|
|
263
|
+
if (!isObject(r) || (type && r.resourceType !== type)) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const en = ResourceNode.makeResNode(ctx, e, bundleNode, 'Bundle.entry', null, 'BackboneElement', 'entry', i);
|
|
267
|
+
out.push(ResourceNode.makeResNode(ctx, r, en, 'Bundle.entry.resource', null, null, 'resource', null));
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** A value set's canonical, as entryIn() and resourcesIn() take it: a URL, with |version or not. */
|
|
274
|
+
function checkCanonical(fnName, url) {
|
|
275
|
+
if (typeof url !== 'string' || !/^[a-z][a-z0-9+.-]*:[^\s|]{1,250}(\|[^\s|]{1,64})?$/i.test(url)) {
|
|
276
|
+
throw new Error(`${fnName}(): the value set must be a canonical URL (optionally with |version)`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Of the nodes, those with a code (code, medicationCodeableConcept or vaccineCode) in the
|
|
282
|
+
* value set, as membership() says. membership(url, concept) answers true or false, or null
|
|
283
|
+
* when it can't tell (which is not a member), at once or as a Promise. Synchronous answers
|
|
284
|
+
* give a synchronous result; any Promise makes the result one, which fhirpath.js accepts
|
|
285
|
+
* only when the evaluation is asynchronous.
|
|
286
|
+
*/
|
|
287
|
+
function membersOf(ctx, fnName, nodes, url, membership) {
|
|
288
|
+
const asks = nodes.map(n => {
|
|
289
|
+
const r = valData(n);
|
|
290
|
+
const concepts = CODE_ELEMENTS.map(el => r[el]).filter(c => isObject(c) && Array.isArray(c.coding) && c.coding.length);
|
|
291
|
+
return concepts.map(c => membership(url, c));
|
|
292
|
+
});
|
|
293
|
+
const isMember = (answers) => answers.some(a => a === true);
|
|
294
|
+
if (!asks.some(list => list.some(a => a && typeof a.then === 'function'))) {
|
|
295
|
+
return nodes.filter((n, i) => isMember(asks[i]));
|
|
296
|
+
}
|
|
297
|
+
fhirpath.util.checkAllowAsync(ctx, fnName);
|
|
298
|
+
return Promise.all(asks.map(list => Promise.all(list))).then(answers => nodes.filter((n, i) => isMember(answers[i])));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The fhirpath.js user invocation table with section(), entry(), resources(), entryIn()
|
|
303
|
+
* and resourcesIn().
|
|
304
|
+
*
|
|
305
|
+
* @param {{resolve?: function(string, Object): (Object|null), membership?: function(string, Object): (boolean|null|Promise<boolean|null>)}} [options]
|
|
306
|
+
* resolve: for references that aren't in a Bundle; given the reference and the
|
|
307
|
+
* Composition, it returns the resource or null. membership: whether a CodeableConcept is
|
|
308
|
+
* in a value set (a canonical) - without it, entryIn() and resourcesIn() find nothing
|
|
309
|
+
*/
|
|
310
|
+
function ipsFunctions(options = {}) {
|
|
311
|
+
const resolve = typeof options.resolve === 'function' ? options.resolve : null;
|
|
312
|
+
const membership = typeof options.membership === 'function' ? options.membership : () => false;
|
|
313
|
+
|
|
314
|
+
function section(inputs, system, code, min, max) {
|
|
315
|
+
const ctx = this;
|
|
316
|
+
const b = bounds('section', min, max);
|
|
317
|
+
const out = [];
|
|
318
|
+
for (const item of inputs) {
|
|
319
|
+
const comp = compositionNode(ctx, item);
|
|
320
|
+
if (!comp) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const found = allSections(ctx, comp).filter(s => hasCoding(valData(s).code, system, code));
|
|
324
|
+
if (found.length >= b.min && found.length <= b.max) {
|
|
325
|
+
out.push(...found);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const hasCode = (system, code) => (node) => CODE_ELEMENTS.some(el => hasCoding(valData(node)[el], system, code));
|
|
332
|
+
const within = (b, found) => (found.length >= b.min && found.length <= b.max ? found : []);
|
|
333
|
+
|
|
334
|
+
function entry(inputs, system, code, min, max) {
|
|
335
|
+
const ctx = this;
|
|
336
|
+
const b = bounds('entry', min, max);
|
|
337
|
+
const out = [];
|
|
338
|
+
for (const item of inputs) {
|
|
339
|
+
const nodes = sectionEntryNodes(ctx, item, resolve);
|
|
340
|
+
if (nodes) {
|
|
341
|
+
out.push(...within(b, nodes.filter(hasCode(system, code))));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function entryIn(inputs, valueSet, min, max) {
|
|
348
|
+
const ctx = this;
|
|
349
|
+
checkCanonical('entryIn', valueSet);
|
|
350
|
+
const b = bounds('entryIn', min, max);
|
|
351
|
+
const found = inputs.map(item => sectionEntryNodes(ctx, item, resolve)).filter(Boolean).map(nodes => membersOf(ctx, 'entryIn', nodes, valueSet, membership));
|
|
352
|
+
if (!found.some(f => typeof f.then === 'function')) {
|
|
353
|
+
return found.flatMap(f => within(b, f));
|
|
354
|
+
}
|
|
355
|
+
return Promise.all(found).then(all => all.flatMap(f => within(b, f)));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function resources(inputs, system, code, type) {
|
|
359
|
+
return documentResourceNodes(this, inputs, 'resources', type).filter(hasCode(system, code));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function resourcesIn(inputs, valueSet, type) {
|
|
363
|
+
checkCanonical('resourcesIn', valueSet);
|
|
364
|
+
return membersOf(this, 'resourcesIn', documentResourceNodes(this, inputs, 'resourcesIn', type), valueSet, membership);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const arity = { 2: ['String', 'String'], 4: ['String', 'String', 'Integer', 'Any'] };
|
|
368
|
+
// internalStructures: the input and output are nodes, so the paths (and the Bundle a
|
|
369
|
+
// section is in) are known; and fhirpath.js calls the function with the context as this
|
|
370
|
+
return {
|
|
371
|
+
section: { fn: section, arity, internalStructures: true },
|
|
372
|
+
entry: { fn: entry, arity, internalStructures: true },
|
|
373
|
+
resources: { fn: resources, arity: { 2: ['String', 'String'], 3: ['String', 'String', 'String'] }, internalStructures: true },
|
|
374
|
+
entryIn: { fn: entryIn, arity: { 1: ['String'], 3: ['String', 'Integer', 'Any'] }, internalStructures: true },
|
|
375
|
+
resourcesIn: { fn: resourcesIn, arity: { 1: ['String'], 2: ['String', 'String'] }, internalStructures: true }
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Evaluate an expression on a document (or anything else) with the module's functions.
|
|
381
|
+
* Synchronous: a membership() that answers with Promises needs evaluateAsync().
|
|
382
|
+
*
|
|
383
|
+
* @param {Object} resource
|
|
384
|
+
* @param {string} expression
|
|
385
|
+
* @param {{vars?: Object, resolve?: Function, membership?: Function}} [options]
|
|
386
|
+
* @returns {Array} the result, as plain values
|
|
387
|
+
*/
|
|
388
|
+
function evaluate(resource, expression, options = {}) {
|
|
389
|
+
return fhirpath.evaluate(resource, expression, options.vars || null, r4Model, { userInvocationTable: ipsFunctions(options) });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** As evaluate(), always asynchronous: what a membership() that asks a server needs. */
|
|
393
|
+
function evaluateAsync(resource, expression, options = {}) {
|
|
394
|
+
return fhirpath.evaluate(resource, expression, options.vars || null, r4Model, { userInvocationTable: ipsFunctions(options), async: 'always' });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
module.exports = { ipsFunctions, evaluate, evaluateAsync, hasCoding, inBundle, CODE_ELEMENTS };
|