Skip to content

Commit 1bd0554

Browse files
Added supplementary function weight()
LF-2100
1 parent 35ac9c9 commit 1bd0554

11 files changed

+1506
-32
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
This log documents significant changes for each release. This project follows
44
[Semantic Versioning](http://semver.org/).
55

6+
## [3.14.0] - 2024-05-09
7+
### Added
8+
- supplementary function `weight()`.
9+
610
## [3.13.1] - 2024-04-24
711
### Fixed
812
- an issue with evaluating an expression for a resource passed through an

package-lock.json

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fhirpath",
3-
"version": "3.13.1",
3+
"version": "3.14.0",
44
"description": "A FHIRPath engine",
55
"main": "src/fhirpath.js",
66
"dependencies": {

src/fhirpath.js

+20-12
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ let engine = {}; // the object with all FHIRPath functions and operations
3838
let existence = require("./existence");
3939
let filtering = require("./filtering");
4040
let aggregate = require("./aggregate");
41+
let supplements = require("./supplements");
4142
let combining = require("./combining");
4243
let misc = require("./misc");
4344
let equality = require("./equality");
@@ -83,6 +84,7 @@ engine.invocationTable = {
8384
min: {fn: aggregate.minFn},
8485
max: {fn: aggregate.maxFn},
8586
avg: {fn: aggregate.avgFn},
87+
weight: {fn: supplements.weight},
8688
single: {fn: filtering.singleFn},
8789
first: {fn: filtering.firstFn},
8890
last: {fn: filtering.lastFn},
@@ -194,7 +196,7 @@ engine.TermExpression = function(ctx, parentData, node) {
194196
if (parentData) {
195197
parentData = parentData.map((x) => {
196198
if (x instanceof Object && x.resourceType) {
197-
return makeResNode(x, x.resourceType, null, x.resourceType);
199+
return makeResNode(x, null, x.resourceType, null, x.resourceType);
198200
}
199201
return x;
200202
});
@@ -263,17 +265,17 @@ engine.ExternalConstantTerm = function(ctx, parentData, node) {
263265
if (Array.isArray(value)) {
264266
value = value.map(
265267
i => i?.__path__
266-
? makeResNode(i, i.__path__.path || null, null,
268+
? makeResNode(i, i.__path__.parentResNode, i.__path__.path || null, null,
267269
i.__path__.fhirNodeDataType || null)
268270
: i?.resourceType
269-
? makeResNode(i, null, null)
271+
? makeResNode(i, null, null, null)
270272
: i );
271273
} else {
272274
value = value?.__path__
273-
? makeResNode(value, value.__path__.path || null, null,
275+
? makeResNode(value, value.__path__.parentResNode, value.__path__.path || null, null,
274276
value.__path__.fhirNodeDataType || null)
275277
: value?.resourceType
276-
? makeResNode(value, null, null)
278+
? makeResNode(value, null, null, null)
277279
: value;
278280
}
279281
ctx.processedVars[varName] = value;
@@ -380,7 +382,7 @@ engine.MemberInvocation = function(ctx, parentData, node ) {
380382
.filter((x) => x instanceof ResourceNode && x.path === key);
381383
} else {
382384
return parentData.reduce(function(acc, res) {
383-
res = makeResNode(res, res.__path__?.path || null, null,
385+
res = makeResNode(res, null, res.__path__?.path || null, null,
384386
res.__path__?.fhirNodeDataType || null);
385387
util.pushFn(acc, util.makeChildResNodes(res, key, model));
386388
return acc;
@@ -676,14 +678,18 @@ function applyParsedPath(resource, parsedPath, context, model, options) {
676678
constants.reset();
677679
let dataRoot = util.arraify(resource).map(
678680
i => i?.__path__
679-
? makeResNode(i, i.__path__.path, null,
681+
? makeResNode(i, i.__path__.parentResNode, i.__path__.path, null,
680682
i.__path__.fhirNodeDataType || null)
681683
: i );
682684
// doEval takes a "ctx" object, and we store things in that as we parse, so we
683685
// need to put user-provided variable data in a sub-object, ctx.vars.
684686
// Set up default standard variables, and allow override from the variables.
685687
// However, we'll keep our own copy of dataRoot for internal processing.
686-
let vars = {context: dataRoot, ucum: 'http://unitsofmeasure.org'};
688+
let vars = {
689+
context: dataRoot,
690+
ucum: 'http://unitsofmeasure.org',
691+
scoreExt: 'http://hl7.org/fhir/StructureDefinition/ordinalValue'
692+
};
687693
let ctx = {dataRoot, processedVars: vars, vars: context || {}, model};
688694
if (options.traceFn) {
689695
ctx.customTraceFn = options.traceFn;
@@ -701,9 +707,11 @@ function applyParsedPath(resource, parsedPath, context, model, options) {
701707
// Path for the data extracted from the resource.
702708
let path;
703709
let fhirNodeDataType;
710+
let parentResNode;
704711
if (n instanceof ResourceNode) {
705712
path = n.path;
706713
fhirNodeDataType = n.fhirNodeDataType;
714+
parentResNode = n.parentResNode;
707715
}
708716
n = util.valData(n);
709717
if (n instanceof FP_Type) {
@@ -716,7 +724,7 @@ function applyParsedPath(resource, parsedPath, context, model, options) {
716724
// Add a hidden (non-enumerable) property with the path to the data extracted
717725
// from the resource.
718726
if (path && typeof n === 'object' && !n.__path__) {
719-
Object.defineProperty(n, '__path__', { value: {path, fhirNodeDataType} });
727+
Object.defineProperty(n, '__path__', { value: {path, fhirNodeDataType, parentResNode} });
720728
}
721729
acc.push(n);
722730
}
@@ -828,7 +836,7 @@ function compile(path, model, options) {
828836
const baseFhirNodeDataType = model && model.path2Type[basePath] || null;
829837
basePath = baseFhirNodeDataType === 'BackboneElement' || baseFhirNodeDataType === 'Element' ? basePath : baseFhirNodeDataType || basePath;
830838

831-
fhirData = makeResNode(fhirData, basePath, null, baseFhirNodeDataType);
839+
fhirData = makeResNode(fhirData, null, basePath, null, baseFhirNodeDataType);
832840
}
833841
// Globally set model before applying parsed FHIRPath expression
834842
TypeInfo.model = model;
@@ -854,8 +862,8 @@ function typesFn(fhirpathResult) {
854862
return util.arraify(fhirpathResult).map(value => {
855863
const ti = TypeInfo.fromValue(
856864
value?.__path__
857-
? new ResourceNode(value, value.__path__?.path, null,
858-
value.__path__?.fhirNodeDataType)
865+
? new ResourceNode(value, value.__path__?.parentResNode || null,
866+
value.__path__?.path || null, null, value.__path__?.fhirNodeDataType || null)
859867
: value );
860868
return `${ti.namespace}.${ti.name}`;
861869
});

src/filtering.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ engine.extension = function(parentData, url) {
2828
if (extensions) {
2929
return extensions
3030
.filter(extension => extension.url === url)
31-
.map(x => ResourceNode.makeResNode(x, 'Extension', null, 'Extension'));
31+
.map(e => ResourceNode.makeResNode(e, x, 'Extension', null, 'Extension'));
3232
}
3333
return [];
3434
}));
@@ -67,9 +67,9 @@ engine.repeatMacro = function(parentData, expr) {
6767

6868
//TODO: behavior on object?
6969
engine.singleFn = function(x) {
70-
if(x.length == 1){
70+
if(x.length === 1){
7171
return x;
72-
} else if (x.length == 0) {
72+
} else if (x.length === 0) {
7373
return [];
7474
} else {
7575
throw new Error("Expected single");

src/supplements.js

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Contains the supplementary FHIRPath functions.
2+
3+
let engine = {};
4+
5+
/**
6+
* Returns numeric values from the score extension associated with the input
7+
* collection of Questionnaire items. See the description of the ordinal()
8+
* function here:
9+
* https://hl7.org/fhir/uv/sdc/expressions.html#fhirpath-supplements
10+
* @param {Array} coll - questionnaire items
11+
* @return {number[]}
12+
*/
13+
engine.weight = function (coll) {
14+
if(coll !== false && ! coll) { return []; }
15+
16+
const scoreExtUrl = this.vars.scoreExt || this.processedVars.scoreExt;
17+
const res = [];
18+
const linkId2Code = {};
19+
20+
coll.forEach((answer) => {
21+
if (answer.data.valueCoding) {
22+
const score = answer.data.extension?.find(e => e.url === scoreExtUrl)?.valueDecimal;
23+
if (score !== undefined) {
24+
// if we have a score extension in the source item, use it.
25+
res.push(score);
26+
} else {
27+
// otherwise we will try to find the score in the %questionnaire.
28+
linkId2Code[answer.parentResNode.data.linkId] = answer.data.valueCoding.code;
29+
}
30+
}
31+
});
32+
33+
const questionnaire = this.vars.questionnaire || this.processedVars.questionnaire?.data;
34+
if (questionnaire) {
35+
forEachQItem(questionnaire, (qItem) => {
36+
const code = linkId2Code[qItem.linkId];
37+
if (code) {
38+
const answerOption = qItem.answerOption?.find(o => o.valueCoding.code === code);
39+
if (answerOption) {
40+
delete linkId2Code[qItem.linkId];
41+
const score = answerOption.extension?.find(e => e.url === scoreExtUrl)?.valueDecimal;
42+
if (score !== undefined) {
43+
// if we have a score extension for the answerOption, use it.
44+
res.push(score);
45+
}
46+
}
47+
}
48+
});
49+
}
50+
51+
// Check for errors.
52+
const unfoundLinkIds = Object.keys(linkId2Code);
53+
if (unfoundLinkIds.length) {
54+
if (questionnaire) {
55+
throw new Error('Questionnaire answerOptions with these linkIds were not found: ' + unfoundLinkIds.join(',') + '.');
56+
} else {
57+
throw new Error('%questionnaire is needed but not specified.');
58+
}
59+
}
60+
61+
return res;
62+
};
63+
64+
/**
65+
* Runs a function for each questionnaire item.
66+
* @param {Object} questionnaire - Questionnaire resource.
67+
* @param {(item) => void} fn - function.
68+
*/
69+
function forEachQItem(questionnaire, fn) {
70+
if(questionnaire.item) {
71+
questionnaire.item.forEach((item) => {
72+
fn(item);
73+
forEachQItem(item, fn);
74+
});
75+
}
76+
}
77+
78+
module.exports = engine;

src/types.js

+9-7
Original file line numberDiff line numberDiff line change
@@ -1287,23 +1287,25 @@ class ResourceNode {
12871287
* Constructs a instance for the given node ("data") of a resource. If the
12881288
* data is the top-level node of a resouce, the path and type parameters will
12891289
* be ignored in favor of the resource's resourceType field.
1290-
* @param {*} data the node's data or value (which might be an object with
1290+
* @param {*} data - the node's data or value (which might be an object with
12911291
* sub-nodes, an array, or FHIR data type)
1292-
* @param {string} path the node's path in the resource (e.g. Patient.name).
1292+
* @param {ResourceNode} parentResNode - parent ResourceNode.
1293+
* @param {string} path - the node's path in the resource (e.g. Patient.name).
12931294
* If the data's type can be determined from data, that will take precedence
12941295
* over this parameter.
1295-
* @param {*} _data additional data stored in a property named with "_"
1296+
* @param {*} _data - additional data stored in a property named with "_"
12961297
* prepended, see https://www.hl7.org/fhir/element.html#json for details.
1297-
* @param {string} fhirNodeDataType FHIR node data type, if the resource node
1298+
* @param {string} fhirNodeDataType - FHIR node data type, if the resource node
12981299
* is described in the FHIR model.
12991300
*/
1300-
constructor(data, path, _data, fhirNodeDataType) {
1301+
constructor(data, parentResNode, path, _data, fhirNodeDataType) {
13011302
// If data is a resource (maybe a contained resource) reset the path
13021303
// information to the resource type.
13031304
if (data?.resourceType) {
13041305
path = data.resourceType;
13051306
fhirNodeDataType = data.resourceType;
13061307
}
1308+
this.parentResNode = parentResNode;
13071309
this.path = path;
13081310
this.data = data;
13091311
this._data = _data || {};
@@ -1384,8 +1386,8 @@ class ResourceNode {
13841386
* given node is already a ResourceNode. Takes the same arguments as the
13851387
* constructor for ResourceNode.
13861388
*/
1387-
ResourceNode.makeResNode = function(data, path, _data, fhirNodeDataType = null) {
1388-
return (data instanceof ResourceNode) ? data : new ResourceNode(data, path, _data, fhirNodeDataType);
1389+
ResourceNode.makeResNode = function(data, parentResNode, path, _data, fhirNodeDataType = null) {
1390+
return (data instanceof ResourceNode) ? data : new ResourceNode(data, parentResNode, path, _data, fhirNodeDataType);
13891391
};
13901392

13911393
// The set of available data types in the System namespace

src/utilities.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ util.assertType = function(data, types, errorMsgPrefix) {
4747
};
4848

4949
util.isEmpty = function(x){
50-
return Array.isArray(x) && x.length == 0;
50+
return Array.isArray(x) && x.length === 0;
5151
};
5252

5353
util.isSome = function(x){
@@ -56,7 +56,7 @@ util.isSome = function(x){
5656

5757
util.isTrue = function(x){
5858
// We use util.valData because we can use a boolean node as a criterion
59-
return x !== null && x !== undefined && (x === true || (x.length == 1 && util.valData(x[0]) === true));
59+
return x !== null && x !== undefined && (x === true || (x.length === 1 && util.valData(x[0]) === true));
6060
};
6161

6262
util.isCapitalized = function(x){
@@ -172,20 +172,20 @@ util.makeChildResNodes = function(parentResNode, childProperty, model) {
172172
if (util.isSome(toAdd) || util.isSome(_toAdd)) {
173173
if(Array.isArray(toAdd)) {
174174
result = toAdd.map((x, i)=>
175-
ResourceNode.makeResNode(x, childPath, _toAdd && _toAdd[i], fhirNodeDataType));
175+
ResourceNode.makeResNode(x, parentResNode, childPath, _toAdd && _toAdd[i], fhirNodeDataType));
176176
// Add items to the end of the ResourceNode list that have no value
177177
// but have associated data, such as extensions or ids.
178178
const _toAddLength = _toAdd?.length || 0;
179179
for (let i = toAdd.length; i < _toAddLength; ++i) {
180-
result.push(ResourceNode.makeResNode(null, childPath, _toAdd[i], fhirNodeDataType));
180+
result.push(ResourceNode.makeResNode(null, parentResNode, childPath, _toAdd[i], fhirNodeDataType));
181181
}
182182
} else if (toAdd == null && Array.isArray(_toAdd)) {
183183
// Add items to the end of the ResourceNode list when there are no
184184
// values at all, but there is a list of associated data, such as
185185
// extensions or ids.
186-
result = _toAdd.map((x) => ResourceNode.makeResNode(null, childPath, x, fhirNodeDataType));
186+
result = _toAdd.map((x) => ResourceNode.makeResNode(null, parentResNode, childPath, x, fhirNodeDataType));
187187
} else {
188-
result = [ResourceNode.makeResNode(toAdd, childPath, _toAdd, fhirNodeDataType)];
188+
result = [ResourceNode.makeResNode(toAdd, parentResNode, childPath, _toAdd, fhirNodeDataType)];
189189
}
190190
} else {
191191
result = [];

0 commit comments

Comments
 (0)