Skip to content

Commit

Permalink
Merge branch 'feature/LF-2100/supplementary-weight-function' into 'ma…
Browse files Browse the repository at this point in the history
…ster'

Added supplementary function weight()

See merge request lfor/fhirpath.js!14
  • Loading branch information
yuriy-sedinkin committed Jul 2, 2024
2 parents cbdcdbc + ad7e71f commit 3cd1c87
Show file tree
Hide file tree
Showing 15 changed files with 1,937 additions and 48 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
This log documents significant changes for each release. This project follows
[Semantic Versioning](http://semver.org/).

## [3.14.1] - 2024-07-02
### Fixed
- impossibility to use attribute name that starts with a capital letter.

## [3.14.0] - 2024-07-02
### Added
- supplementary function `weight()` with alternative name `ordinal()`.

## [3.13.4] - 2024-06-13
### Fixed
- a bug that could cause the context input parameter containing environment
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fhirpath",
"version": "3.13.4",
"version": "3.14.1",
"description": "A FHIRPath engine",
"main": "src/fhirpath.js",
"types": "src/fhirpath.d.ts",
Expand Down
70 changes: 44 additions & 26 deletions src/fhirpath.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ let engine = {}; // the object with all FHIRPath functions and operations
let existence = require("./existence");
let filtering = require("./filtering");
let aggregate = require("./aggregate");
let supplements = require("./sdc-ig-supplements");
let combining = require("./combining");
let misc = require("./misc");
let equality = require("./equality");
Expand Down Expand Up @@ -83,6 +84,8 @@ engine.invocationTable = {
min: {fn: aggregate.minFn},
max: {fn: aggregate.maxFn},
avg: {fn: aggregate.avgFn},
weight: {fn: supplements.weight},
ordinal: {fn: supplements.weight},
single: {fn: filtering.singleFn},
first: {fn: filtering.firstFn},
last: {fn: filtering.lastFn},
Expand Down Expand Up @@ -194,7 +197,7 @@ engine.TermExpression = function(ctx, parentData, node) {
if (parentData) {
parentData = parentData.map((x) => {
if (x instanceof Object && x.resourceType) {
return makeResNode(x, x.resourceType, null, x.resourceType);
return makeResNode(x, null, x.resourceType, null, x.resourceType);
}
return x;
});
Expand Down Expand Up @@ -263,17 +266,17 @@ engine.ExternalConstantTerm = function(ctx, parentData, node) {
if (Array.isArray(value)) {
value = value.map(
i => i?.__path__
? makeResNode(i, i.__path__.path || null, null,
i.__path__.fhirNodeDataType || null)
? makeResNode(i, i.__path__.parentResNode, i.__path__.path, null,
i.__path__.fhirNodeDataType)
: i?.resourceType
? makeResNode(i, null, null)
? makeResNode(i, null, null, null)
: i );
} else {
value = value?.__path__
? makeResNode(value, value.__path__.path || null, null,
value.__path__.fhirNodeDataType || null)
? makeResNode(value, value.__path__.parentResNode, value.__path__.path, null,
value.__path__.fhirNodeDataType)
: value?.resourceType
? makeResNode(value, null, null)
? makeResNode(value, null, null, null)
: value;
}
ctx.processedVars[varName] = value;
Expand Down Expand Up @@ -377,17 +380,16 @@ engine.MemberInvocation = function(ctx, parentData, node ) {
const model = ctx.model;

if (parentData) {
if(util.isCapitalized(key)) {
return parentData
.filter((x) => x instanceof ResourceNode && x.path === key);
} else {
return parentData.reduce(function(acc, res) {
res = makeResNode(res, res.__path__?.path || null, null,
res.__path__?.fhirNodeDataType || null);
return parentData.reduce(function(acc, res) {
res = makeResNode(res, null, res.__path__?.path, null,
res.__path__?.fhirNodeDataType);
if (res.data?.resourceType === key) {
acc.push(res);
} else {
util.pushFn(acc, util.makeChildResNodes(res, key, model));
return acc;
}, []);
}
}
return acc;
}, []);
} else {
return [];
}
Expand Down Expand Up @@ -678,21 +680,35 @@ function applyParsedPath(resource, parsedPath, context, model, options) {
constants.reset();
let dataRoot = util.arraify(resource).map(
i => i?.__path__
? makeResNode(i, i.__path__.path, null,
i.__path__.fhirNodeDataType || null)
? makeResNode(i, i.__path__.parentResNode, i.__path__.path, null,
i.__path__.fhirNodeDataType)
: i );
// doEval takes a "ctx" object, and we store things in that as we parse, so we
// need to put user-provided variable data in a sub-object, ctx.vars.
// Set up default standard variables, and allow override from the variables.
// However, we'll keep our own copy of dataRoot for internal processing.
let vars = {context: dataRoot, ucum: 'http://unitsofmeasure.org'};
let ctx = {dataRoot, processedVars: vars, vars: {...context}, model};
let ctx = {
dataRoot,
processedVars: {
ucum: 'http://unitsofmeasure.org'
},
vars: {
context: dataRoot,
...context
},
model
};
if (options.traceFn) {
ctx.customTraceFn = options.traceFn;
}
if (options.userInvocationTable) {
ctx.userInvocationTable = options.userInvocationTable;
}
ctx.defaultScoreExts = [
'http://hl7.org/fhir/StructureDefinition/ordinalValue',
'http://hl7.org/fhir/StructureDefinition/itemWeight',
'http://hl7.org/fhir/StructureDefinition/questionnaire-ordinalValue'
];
return engine.doEval(ctx, dataRoot, parsedPath.children[0])
// engine.doEval returns array of "ResourceNode" and/or "FP_Type" instances.
// "ResourceNode" or "FP_Type" instances are not created for sub-items.
Expand All @@ -703,9 +719,11 @@ function applyParsedPath(resource, parsedPath, context, model, options) {
// Path for the data extracted from the resource.
let path;
let fhirNodeDataType;
let parentResNode;
if (n instanceof ResourceNode) {
path = n.path;
fhirNodeDataType = n.fhirNodeDataType;
parentResNode = n.parentResNode;
}
n = util.valData(n);
if (n instanceof FP_Type) {
Expand All @@ -718,7 +736,7 @@ function applyParsedPath(resource, parsedPath, context, model, options) {
// Add a hidden (non-enumerable) property with the path to the data extracted
// from the resource.
if (path && typeof n === 'object' && !n.__path__) {
Object.defineProperty(n, '__path__', { value: {path, fhirNodeDataType} });
Object.defineProperty(n, '__path__', { value: {path, fhirNodeDataType, parentResNode} });
}
acc.push(n);
}
Expand Down Expand Up @@ -827,10 +845,10 @@ function compile(path, model, options) {
return function (fhirData, context) {
if (path.base) {
let basePath = model.pathsDefinedElsewhere[path.base] || path.base;
const baseFhirNodeDataType = model && model.path2Type[basePath] || null;
const baseFhirNodeDataType = model && model.path2Type[basePath];
basePath = baseFhirNodeDataType === 'BackboneElement' || baseFhirNodeDataType === 'Element' ? basePath : baseFhirNodeDataType || basePath;

fhirData = makeResNode(fhirData, basePath, null, baseFhirNodeDataType);
fhirData = makeResNode(fhirData, null, basePath, null, baseFhirNodeDataType);
}
// Globally set model before applying parsed FHIRPath expression
TypeInfo.model = model;
Expand All @@ -856,8 +874,8 @@ function typesFn(fhirpathResult) {
return util.arraify(fhirpathResult).map(value => {
const ti = TypeInfo.fromValue(
value?.__path__
? new ResourceNode(value, value.__path__?.path, null,
value.__path__?.fhirNodeDataType)
? new ResourceNode(value, value.__path__?.parentResNode,
value.__path__?.path, null, value.__path__?.fhirNodeDataType)
: value );
return `${ti.namespace}.${ti.name}`;
});
Expand Down
6 changes: 3 additions & 3 deletions src/filtering.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ engine.extension = function(parentData, url) {
if (extensions) {
return extensions
.filter(extension => extension.url === url)
.map(x => ResourceNode.makeResNode(x, 'Extension', null, 'Extension'));
.map(e => ResourceNode.makeResNode(e, x, 'Extension', null, 'Extension'));
}
return [];
}));
Expand Down Expand Up @@ -67,9 +67,9 @@ engine.repeatMacro = function(parentData, expr) {

//TODO: behavior on object?
engine.singleFn = function(x) {
if(x.length == 1){
if(x.length === 1){
return x;
} else if (x.length == 0) {
} else if (x.length === 0) {
return [];
} else {
throw new Error("Expected single");
Expand Down
116 changes: 116 additions & 0 deletions src/sdc-ig-supplements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Contains the supplementary FHIRPath functions defined in the Structured Data
// Capture IG, https://hl7.org/fhir/uv/sdc/expressions.html#fhirpath-supplements.

let engine = {};

/**
* Returns numeric values from the score extension associated with the input
* collection of Questionnaire items. See the description of the ordinal()
* function here:
* https://hl7.org/fhir/uv/sdc/expressions.html#fhirpath-supplements
* @param {Array} coll - questionnaire items
* @return {number[]}
*/
engine.weight = function (coll) {
if(coll !== false && ! coll) { return []; }

const userScoreExtUrl = this.vars.scoreExt || this.processedVars.scoreExt;
const checkExtUrl = userScoreExtUrl
? (e) => e.url === userScoreExtUrl
: (e) => this.defaultScoreExts.includes(e.url);
const res = [];

const questionnaire = this.vars.questionnaire || this.processedVars.questionnaire?.data;
coll.forEach((item) => {
if (item?.data) {
const valueCoding = item.data.valueCoding;
let value = valueCoding;
if (!value) {
const prop = Object.keys(item.data).find(p => p.length > 5 && p.startsWith('value'));
// if we found a child value[x] property
value = prop
// we use it to get a score extension
? item.data[prop]
// otherwise, if the source item has a simple data type
: item._data?.extension
// we get the extension from the adjacent property starting with
// an underscore
? item._data
// otherwise we get the extension from the source item
// (e.g. 'item' is a Coding)
: item.data;
}
const score = value?.extension?.find(checkExtUrl)?.valueDecimal;
if (score !== undefined) {
// if we have a score extension in the source item, use it.
res.push(score);
} else if (valueCoding) {
const linkIds = getLinkIds(item.parentResNode);
if (linkIds.length) {
if (questionnaire) {
const qItem = getQItemByLinkIds(questionnaire, linkIds);
const answerOption = qItem?.answerOption?.find(o =>
o.valueCoding.code === valueCoding.code
&& o.valueCoding.system === valueCoding.system
);
if (answerOption) {
const score = answerOption.extension?.find(checkExtUrl)?.valueDecimal;
if (score !== undefined) {
// if we have a score extension for the answerOption, use it.
res.push(score);
}
} else {
throw new Error(
'Questionnaire answerOption with this linkId was not found: ' +
item.parentResNode.data.linkId + '.');
}
} else {
throw new Error('%questionnaire is needed but not specified.');
}
}
}
}
});

return res;
};

/**
* Returns array of linkIds of ancestor ResourceNodes and source ResourceNode
* starting with the linkId of the given node and ending with the topmost item's
* linkId.
* @param {ResourceNode} node - source ResourceNode.
* @return {String[]}
*/
function getLinkIds(node) {
const res = [];

while (node.data?.linkId) {
res.push(node.data.linkId);
node = node.parentResNode;
}

return res;
}

/**
* Returns a questionnaire item based on the linkIds array of the ancestor
* ResourceNodes and the target ResourceNode. If the questionnaire item is not
* found, it returns null.
* @param {Object} questionnaire - object with a Questionnaire resource.
* @param {string[]} linkIds - array of linkIds starting with the linkId of the
* target node and ending with the topmost item's linkId.
* @return {Object | null}
*/
function getQItemByLinkIds(questionnaire, linkIds) {
let currentNode = questionnaire;
for(let i = linkIds.length-1; i >= 0; --i) {
currentNode = currentNode.item?.find(o => o.linkId === linkIds[i]);
if (!currentNode) {
return null;
}
}
return currentNode;
}

module.exports = engine;
20 changes: 11 additions & 9 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -1287,27 +1287,29 @@ class ResourceNode {
* Constructs a instance for the given node ("data") of a resource. If the
* data is the top-level node of a resouce, the path and type parameters will
* be ignored in favor of the resource's resourceType field.
* @param {*} data the node's data or value (which might be an object with
* @param {*} data - the node's data or value (which might be an object with
* sub-nodes, an array, or FHIR data type)
* @param {string} path the node's path in the resource (e.g. Patient.name).
* @param {ResourceNode} parentResNode - parent ResourceNode.
* @param {string} path - the node's path in the resource (e.g. Patient.name).
* If the data's type can be determined from data, that will take precedence
* over this parameter.
* @param {*} _data additional data stored in a property named with "_"
* @param {*} _data - additional data stored in a property named with "_"
* prepended, see https://www.hl7.org/fhir/element.html#json for details.
* @param {string} fhirNodeDataType FHIR node data type, if the resource node
* @param {string} fhirNodeDataType - FHIR node data type, if the resource node
* is described in the FHIR model.
*/
constructor(data, path, _data, fhirNodeDataType) {
constructor(data, parentResNode, path, _data, fhirNodeDataType) {
// If data is a resource (maybe a contained resource) reset the path
// information to the resource type.
if (data?.resourceType) {
path = data.resourceType;
fhirNodeDataType = data.resourceType;
}
this.path = path;
this.parentResNode = parentResNode || null;
this.path = path || null;
this.data = data;
this._data = _data || {};
this.fhirNodeDataType = fhirNodeDataType;
this.fhirNodeDataType = fhirNodeDataType || null;
}

/**
Expand Down Expand Up @@ -1384,8 +1386,8 @@ class ResourceNode {
* given node is already a ResourceNode. Takes the same arguments as the
* constructor for ResourceNode.
*/
ResourceNode.makeResNode = function(data, path, _data, fhirNodeDataType = null) {
return (data instanceof ResourceNode) ? data : new ResourceNode(data, path, _data, fhirNodeDataType);
ResourceNode.makeResNode = function(data, parentResNode, path, _data, fhirNodeDataType = null) {
return (data instanceof ResourceNode) ? data : new ResourceNode(data, parentResNode, path, _data, fhirNodeDataType);
};

// The set of available data types in the System namespace
Expand Down
Loading

0 comments on commit 3cd1c87

Please sign in to comment.