From 1bd05548a98387a4a17a4a1a94a88a9a54c84c4c Mon Sep 17 00:00:00 2001 From: sedinkinya Date: Fri, 10 May 2024 10:26:09 -0400 Subject: [PATCH 1/9] Added supplementary function weight() LF-2100 --- CHANGELOG.md | 4 + package-lock.json | 6 +- package.json | 2 +- src/fhirpath.js | 32 +- src/filtering.js | 6 +- src/supplements.js | 78 ++ src/types.js | 16 +- src/utilities.js | 12 +- test/resources/phq9-response.json | 146 ++++ test/resources/phq9.json | 1206 +++++++++++++++++++++++++++++ test/supplements.test.js | 30 + 11 files changed, 1506 insertions(+), 32 deletions(-) create mode 100644 src/supplements.js create mode 100644 test/resources/phq9-response.json create mode 100644 test/resources/phq9.json create mode 100644 test/supplements.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 697d7bc..aab9299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ This log documents significant changes for each release. This project follows [Semantic Versioning](http://semver.org/). +## [3.14.0] - 2024-05-09 +### Added +- supplementary function `weight()`. + ## [3.13.1] - 2024-04-24 ### Fixed - an issue with evaluating an expression for a resource passed through an diff --git a/package-lock.json b/package-lock.json index 8c3a29b..75150fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fhirpath", - "version": "3.13.1", + "version": "3.14.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "fhirpath", - "version": "3.13.1", + "version": "3.14.0", "hasInstallScript": true, "license": "SEE LICENSE in LICENSE.md", "dependencies": { @@ -16870,7 +16870,7 @@ "version": "git+ssh://git@github.com/caderek/benny.git#0ad058d3c7ef0b488a8fe9ae3519159fc7f36bb6", "integrity": "sha512-E6Hn7OIXBdxjgl6yKBTL7fEp143s2+5tQyks9DMzsoT9dMuN+jQTsmlaE9QbjuyUH2N+NrxslWdcGlN8B/xrcw==", "dev": true, - "from": "benny@git+ssh://git@github.com:caderek/benny.git#0ad058d3c7ef0b488a8fe9ae3519159fc7f36bb6", + "from": "benny@github:caderek/benny#0ad058d3c7ef0b488a8fe9ae3519159fc7f36bb6", "requires": { "@arrows/composition": "^1.0.0", "@arrows/dispatch": "^1.0.2", diff --git a/package.json b/package.json index 204447e..3690e2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fhirpath", - "version": "3.13.1", + "version": "3.14.0", "description": "A FHIRPath engine", "main": "src/fhirpath.js", "dependencies": { diff --git a/src/fhirpath.js b/src/fhirpath.js index b7c4407..848f3e9 100644 --- a/src/fhirpath.js +++ b/src/fhirpath.js @@ -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("./supplements"); let combining = require("./combining"); let misc = require("./misc"); let equality = require("./equality"); @@ -83,6 +84,7 @@ engine.invocationTable = { min: {fn: aggregate.minFn}, max: {fn: aggregate.maxFn}, avg: {fn: aggregate.avgFn}, + weight: {fn: supplements.weight}, single: {fn: filtering.singleFn}, first: {fn: filtering.firstFn}, last: {fn: filtering.lastFn}, @@ -194,7 +196,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; }); @@ -263,17 +265,17 @@ engine.ExternalConstantTerm = function(ctx, parentData, node) { if (Array.isArray(value)) { value = value.map( i => i?.__path__ - ? makeResNode(i, i.__path__.path || null, null, + ? makeResNode(i, i.__path__.parentResNode, i.__path__.path || null, null, i.__path__.fhirNodeDataType || null) : i?.resourceType - ? makeResNode(i, null, null) + ? makeResNode(i, null, null, null) : i ); } else { value = value?.__path__ - ? makeResNode(value, value.__path__.path || null, null, + ? makeResNode(value, value.__path__.parentResNode, value.__path__.path || null, null, value.__path__.fhirNodeDataType || null) : value?.resourceType - ? makeResNode(value, null, null) + ? makeResNode(value, null, null, null) : value; } ctx.processedVars[varName] = value; @@ -380,7 +382,7 @@ engine.MemberInvocation = function(ctx, parentData, node ) { .filter((x) => x instanceof ResourceNode && x.path === key); } else { return parentData.reduce(function(acc, res) { - res = makeResNode(res, res.__path__?.path || null, null, + res = makeResNode(res, null, res.__path__?.path || null, null, res.__path__?.fhirNodeDataType || null); util.pushFn(acc, util.makeChildResNodes(res, key, model)); return acc; @@ -676,14 +678,18 @@ function applyParsedPath(resource, parsedPath, context, model, options) { constants.reset(); let dataRoot = util.arraify(resource).map( i => i?.__path__ - ? makeResNode(i, i.__path__.path, null, + ? makeResNode(i, i.__path__.parentResNode, i.__path__.path, null, i.__path__.fhirNodeDataType || null) : 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 vars = { + context: dataRoot, + ucum: 'http://unitsofmeasure.org', + scoreExt: 'http://hl7.org/fhir/StructureDefinition/ordinalValue' + }; let ctx = {dataRoot, processedVars: vars, vars: context || {}, model}; if (options.traceFn) { ctx.customTraceFn = options.traceFn; @@ -701,9 +707,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) { @@ -716,7 +724,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); } @@ -828,7 +836,7 @@ function compile(path, model, options) { const baseFhirNodeDataType = model && model.path2Type[basePath] || null; 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; @@ -854,8 +862,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 || null, + value.__path__?.path || null, null, value.__path__?.fhirNodeDataType || null) : value ); return `${ti.namespace}.${ti.name}`; }); diff --git a/src/filtering.js b/src/filtering.js index 34a4795..6dc1e68 100644 --- a/src/filtering.js +++ b/src/filtering.js @@ -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 []; })); @@ -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"); diff --git a/src/supplements.js b/src/supplements.js new file mode 100644 index 0000000..f814f70 --- /dev/null +++ b/src/supplements.js @@ -0,0 +1,78 @@ +// Contains the supplementary FHIRPath functions. + +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 scoreExtUrl = this.vars.scoreExt || this.processedVars.scoreExt; + const res = []; + const linkId2Code = {}; + + coll.forEach((answer) => { + if (answer.data.valueCoding) { + const score = answer.data.extension?.find(e => e.url === scoreExtUrl)?.valueDecimal; + if (score !== undefined) { + // if we have a score extension in the source item, use it. + res.push(score); + } else { + // otherwise we will try to find the score in the %questionnaire. + linkId2Code[answer.parentResNode.data.linkId] = answer.data.valueCoding.code; + } + } + }); + + const questionnaire = this.vars.questionnaire || this.processedVars.questionnaire?.data; + if (questionnaire) { + forEachQItem(questionnaire, (qItem) => { + const code = linkId2Code[qItem.linkId]; + if (code) { + const answerOption = qItem.answerOption?.find(o => o.valueCoding.code === code); + if (answerOption) { + delete linkId2Code[qItem.linkId]; + const score = answerOption.extension?.find(e => e.url === scoreExtUrl)?.valueDecimal; + if (score !== undefined) { + // if we have a score extension for the answerOption, use it. + res.push(score); + } + } + } + }); + } + + // Check for errors. + const unfoundLinkIds = Object.keys(linkId2Code); + if (unfoundLinkIds.length) { + if (questionnaire) { + throw new Error('Questionnaire answerOptions with these linkIds were not found: ' + unfoundLinkIds.join(',') + '.'); + } else { + throw new Error('%questionnaire is needed but not specified.'); + } + } + + return res; +}; + +/** + * Runs a function for each questionnaire item. + * @param {Object} questionnaire - Questionnaire resource. + * @param {(item) => void} fn - function. + */ +function forEachQItem(questionnaire, fn) { + if(questionnaire.item) { + questionnaire.item.forEach((item) => { + fn(item); + forEachQItem(item, fn); + }); + } +} + +module.exports = engine; diff --git a/src/types.js b/src/types.js index 1419c5d..6b405c7 100644 --- a/src/types.js +++ b/src/types.js @@ -1287,23 +1287,25 @@ 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.parentResNode = parentResNode; this.path = path; this.data = data; this._data = _data || {}; @@ -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 diff --git a/src/utilities.js b/src/utilities.js index d8861c0..fa66a64 100644 --- a/src/utilities.js +++ b/src/utilities.js @@ -47,7 +47,7 @@ util.assertType = function(data, types, errorMsgPrefix) { }; util.isEmpty = function(x){ - return Array.isArray(x) && x.length == 0; + return Array.isArray(x) && x.length === 0; }; util.isSome = function(x){ @@ -56,7 +56,7 @@ util.isSome = function(x){ util.isTrue = function(x){ // We use util.valData because we can use a boolean node as a criterion - return x !== null && x !== undefined && (x === true || (x.length == 1 && util.valData(x[0]) === true)); + return x !== null && x !== undefined && (x === true || (x.length === 1 && util.valData(x[0]) === true)); }; util.isCapitalized = function(x){ @@ -172,20 +172,20 @@ util.makeChildResNodes = function(parentResNode, childProperty, model) { if (util.isSome(toAdd) || util.isSome(_toAdd)) { if(Array.isArray(toAdd)) { result = toAdd.map((x, i)=> - ResourceNode.makeResNode(x, childPath, _toAdd && _toAdd[i], fhirNodeDataType)); + ResourceNode.makeResNode(x, parentResNode, childPath, _toAdd && _toAdd[i], fhirNodeDataType)); // Add items to the end of the ResourceNode list that have no value // but have associated data, such as extensions or ids. const _toAddLength = _toAdd?.length || 0; for (let i = toAdd.length; i < _toAddLength; ++i) { - result.push(ResourceNode.makeResNode(null, childPath, _toAdd[i], fhirNodeDataType)); + result.push(ResourceNode.makeResNode(null, parentResNode, childPath, _toAdd[i], fhirNodeDataType)); } } else if (toAdd == null && Array.isArray(_toAdd)) { // Add items to the end of the ResourceNode list when there are no // values at all, but there is a list of associated data, such as // extensions or ids. - result = _toAdd.map((x) => ResourceNode.makeResNode(null, childPath, x, fhirNodeDataType)); + result = _toAdd.map((x) => ResourceNode.makeResNode(null, parentResNode, childPath, x, fhirNodeDataType)); } else { - result = [ResourceNode.makeResNode(toAdd, childPath, _toAdd, fhirNodeDataType)]; + result = [ResourceNode.makeResNode(toAdd, parentResNode, childPath, _toAdd, fhirNodeDataType)]; } } else { result = []; diff --git a/test/resources/phq9-response.json b/test/resources/phq9-response.json new file mode 100644 index 0000000..2179d62 --- /dev/null +++ b/test/resources/phq9-response.json @@ -0,0 +1,146 @@ +{ + "resourceType": "QuestionnaireResponse", + "meta": { + "profile": [ + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaireresponse|3.0" + ], + "tag": [ + { + "code": "lformsVersion: 35.0.4" + } + ] + }, + "status": "completed", + "authored": "2024-04-04T18:41:13.730Z", + "item": [ + { + "answer": [ + { + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + } + ], + "linkId": "/44250-9", + "text": "Little interest or pleasure in doing things?" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + } + ], + "linkId": "/44255-8", + "text": "Feeling down, depressed, or hopeless?" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6571-9", + "display": "Nearly every day" + } + } + ], + "linkId": "/44259-0", + "text": "Trouble falling or staying asleep, or sleeping too much" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + } + ], + "linkId": "/44254-1", + "text": "Feeling tired or having little energy" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + } + ], + "linkId": "/44251-7", + "text": "Poor appetite or overeating" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6571-9", + "display": "Nearly every day" + } + } + ], + "linkId": "/44258-2", + "text": "Feeling bad about yourself-or that you are a failure or have let yourself or your family down" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6568-5", + "display": "Not at all" + } + } + ], + "linkId": "/44252-5", + "text": "Trouble concentrating on things, such as reading the newspaper or watching television" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + } + ], + "linkId": "/44253-3", + "text": "Moving or speaking so slowly that other people could have noticed. Or the opposite-being so fidgety or restless that you have been moving around a lot more than usual" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + } + ], + "linkId": "/44260-8", + "text": "Thoughts that you would be better off dead, or of hurting yourself in some way" + }, + { + "answer": [ + { + "valueDecimal": 15 + } + ], + "linkId": "/44261-6", + "text": "Patient health questionnaire 9 item total score" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6572-7", + "display": "Not difficult at all" + } + } + ], + "linkId": "/69722-7", + "text": "How difficult have these problems made it for you to do your work, take care of things at home, or get along with other people?" + } + ] +} diff --git a/test/resources/phq9.json b/test/resources/phq9.json new file mode 100644 index 0000000..cda64c8 --- /dev/null +++ b/test/resources/phq9.json @@ -0,0 +1,1206 @@ +{ + "resourceType": "Questionnaire", + "id": "44249-1-x", + "status": "draft", + "meta": { + "profile": [ + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire|2.7" + ], + "tag": [ + { + "code": "lformsVersion: 25.0.0" + } + ] + }, + "title": "PHQ-9 quick depression assessment panel", + "name": "PHQ-9 quick depression assessment panel", + "identifier": [ + { + "system": "http://loinc.org", + "value": "44249-1" + } + ], + "code": [ + { + "system": "http://loinc.org", + "code": "44249-1", + "display": "PHQ-9 quick depression assessment panel" + } + ], + "subjectType": [ + "Patient", + "Person" + ], + "item": [ + { + "type": "choice", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-minOccurs", + "valueInteger": 1 + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "drop-down", + "display": "Drop down" + } + ], + "text": "Drop down" + } + }, + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod", + "valueDuration": { + "value": 100, + "unit": "year", + "system": "http://unitsofmeasure.org", + "code": "a" + } + } + ], + "required": true, + "linkId": "/44250-9", + "code": [ + { + "system": "http://loinc.org", + "code": "44250-9", + "display": "Little interest or pleasure in doing things?" + } + ], + "text": "Little interest or pleasure in doing things?", + "answerOption": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "0" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 0 + } + ], + "valueCoding": { + "code": "LA6568-5", + "display": "Not at all" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 1 + } + ], + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "2" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 2 + } + ], + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "3" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 3 + } + ], + "valueCoding": { + "code": "LA6571-9", + "display": "Nearly every day" + } + } + ] + }, + { + "type": "choice", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-minOccurs", + "valueInteger": 1 + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "drop-down", + "display": "Drop down" + } + ], + "text": "Drop down" + } + }, + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod", + "valueDuration": { + "value": 100, + "unit": "year", + "system": "http://unitsofmeasure.org", + "code": "a" + } + } + ], + "required": true, + "linkId": "/44255-8", + "code": [ + { + "system": "http://loinc.org", + "code": "44255-8", + "display": "Feeling down, depressed, or hopeless?" + } + ], + "text": "Feeling down, depressed, or hopeless?", + "answerOption": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "0" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 0 + } + ], + "valueCoding": { + "code": "LA6568-5", + "display": "Not at all" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 1 + } + ], + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "2" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 2 + } + ], + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "3" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 3 + } + ], + "valueCoding": { + "code": "LA6571-9", + "display": "Nearly every day" + } + } + ] + }, + { + "type": "choice", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-minOccurs", + "valueInteger": 1 + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "drop-down", + "display": "Drop down" + } + ], + "text": "Drop down" + } + }, + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod", + "valueDuration": { + "value": 100, + "unit": "year", + "system": "http://unitsofmeasure.org", + "code": "a" + } + } + ], + "required": true, + "linkId": "/44259-0", + "code": [ + { + "system": "http://loinc.org", + "code": "44259-0", + "display": "Trouble falling or staying asleep, or sleeping too much" + } + ], + "text": "Trouble falling or staying asleep, or sleeping too much", + "answerOption": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "0" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 0 + } + ], + "valueCoding": { + "code": "LA6568-5", + "display": "Not at all" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": 1 + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 1 + } + ], + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "2" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 2 + } + ], + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "3" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 3 + } + ], + "valueCoding": { + "code": "LA6571-9", + "display": "Nearly every day" + } + } + ] + }, + { + "type": "choice", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-minOccurs", + "valueInteger": 1 + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "drop-down", + "display": "Drop down" + } + ], + "text": "Drop down" + } + }, + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod", + "valueDuration": { + "value": 100, + "unit": "year", + "system": "http://unitsofmeasure.org", + "code": "a" + } + } + ], + "required": true, + "linkId": "/44254-1", + "code": [ + { + "system": "http://loinc.org", + "code": "44254-1", + "display": "Feeling tired or having little energy" + } + ], + "text": "Feeling tired or having little energy", + "answerOption": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "0" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 0 + } + ], + "valueCoding": { + "code": "LA6568-5", + "display": "Not at all" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 1 + } + ], + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "2" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 2 + } + ], + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "3" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 3 + } + ], + "valueCoding": { + "code": "LA6571-9", + "display": "Nearly every day" + } + } + ] + }, + { + "type": "choice", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-minOccurs", + "valueInteger": 1 + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "drop-down", + "display": "Drop down" + } + ], + "text": "Drop down" + } + }, + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod", + "valueDuration": { + "value": 100, + "unit": "year", + "system": "http://unitsofmeasure.org", + "code": "a" + } + } + ], + "required": true, + "linkId": "/44251-7", + "code": [ + { + "system": "http://loinc.org", + "code": "44251-7", + "display": "Poor appetite or overeating" + } + ], + "text": "Poor appetite or overeating", + "answerOption": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "0" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 0 + } + ], + "valueCoding": { + "code": "LA6568-5", + "display": "Not at all" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 1 + } + ], + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "2" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 2 + } + ], + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "3" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 3 + } + ], + "valueCoding": { + "code": "LA6571-9", + "display": "Nearly every day" + } + } + ] + }, + { + "type": "choice", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-minOccurs", + "valueInteger": 1 + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "drop-down", + "display": "Drop down" + } + ], + "text": "Drop down" + } + }, + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod", + "valueDuration": { + "value": 100, + "unit": "year", + "system": "http://unitsofmeasure.org", + "code": "a" + } + } + ], + "required": true, + "linkId": "/44258-2", + "code": [ + { + "system": "http://loinc.org", + "code": "44258-2", + "display": "Feeling bad about yourself-or that you are a failure or have let yourself or your family down" + } + ], + "text": "Feeling bad about yourself-or that you are a failure or have let yourself or your family down", + "answerOption": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "0" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 0 + } + ], + "valueCoding": { + "code": "LA6568-5", + "display": "Not at all" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 1 + } + ], + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "2" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 2 + } + ], + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "3" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 3 + } + ], + "valueCoding": { + "code": "LA6571-9", + "display": "Nearly every day" + } + } + ] + }, + { + "type": "choice", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-minOccurs", + "valueInteger": 1 + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "drop-down", + "display": "Drop down" + } + ], + "text": "Drop down" + } + }, + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod", + "valueDuration": { + "value": 100, + "unit": "year", + "system": "http://unitsofmeasure.org", + "code": "a" + } + } + ], + "required": true, + "linkId": "/44252-5", + "code": [ + { + "system": "http://loinc.org", + "code": "44252-5", + "display": "Trouble concentrating on things, such as reading the newspaper or watching television" + } + ], + "text": "Trouble concentrating on things, such as reading the newspaper or watching television", + "answerOption": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "0" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 0 + } + ], + "valueCoding": { + "code": "LA6568-5", + "display": "Not at all" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 1 + } + ], + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "2" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 2 + } + ], + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "3" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 3 + } + ], + "valueCoding": { + "code": "LA6571-9", + "display": "Nearly every day" + } + } + ] + }, + { + "type": "choice", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-minOccurs", + "valueInteger": 1 + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "drop-down", + "display": "Drop down" + } + ], + "text": "Drop down" + } + }, + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod", + "valueDuration": { + "value": 100, + "unit": "year", + "system": "http://unitsofmeasure.org", + "code": "a" + } + } + ], + "required": true, + "linkId": "/44253-3", + "code": [ + { + "system": "http://loinc.org", + "code": "44253-3", + "display": "Moving or speaking so slowly that other people could have noticed. Or the opposite-being so fidgety or restless that you have been moving around a lot more than usual" + } + ], + "text": "Moving or speaking so slowly that other people could have noticed. Or the opposite-being so fidgety or restless that you have been moving around a lot more than usual", + "answerOption": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "0" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 0 + } + ], + "valueCoding": { + "code": "LA6568-5", + "display": "Not at all" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 1 + } + ], + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "2" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 2 + } + ], + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "3" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 3 + } + ], + "valueCoding": { + "code": "LA6571-9", + "display": "Nearly every day" + } + } + ] + }, + { + "type": "choice", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "drop-down", + "display": "Drop down" + } + ], + "text": "Drop down" + } + }, + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod", + "valueDuration": { + "value": 100, + "unit": "year", + "system": "http://unitsofmeasure.org", + "code": "a" + } + } + ], + "required": false, + "linkId": "/44260-8", + "code": [ + { + "system": "http://loinc.org", + "code": "44260-8", + "display": "Thoughts that you would be better off dead, or of hurting yourself in some way" + } + ], + "text": "Thoughts that you would be better off dead, or of hurting yourself in some way", + "answerOption": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "0" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 0 + } + ], + "valueCoding": { + "code": "LA6568-5", + "display": "Not at all" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 1 + } + ], + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "2" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 2 + } + ], + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", + "valueString": "3" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 3 + } + ], + "valueCoding": { + "code": "LA6571-9", + "display": "Nearly every day" + } + } + ] + }, + { + "type": "decimal", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory", + "valueCodeableConcept": { + "text": "The PHQ-9 is the standard (and most commonly used) depression measure, and it ranges from 0-27 Scoring: Add up all checked boxes on PHQ-9. For every check: Not at all = 0; Several days = 1; More than half the days = 2; Nearly every day = 3 (the scores are the codes that appear in the answer list for each of the PHQ-9 problem panel terms). Interpretation: 1-4 = Minimal depression; 5-9 = Mild depression; 10-14 = Moderate depression; 15-19 = Moderately severe depression; 20-27 = Severed depression.", + "coding": [ + { + "display": "The PHQ-9 is the standard (and most commonly used) depression measure, and it ranges from 0-27 Scoring: Add up all checked boxes on PHQ-9. For every check: Not at all = 0; Several days = 1; More than half the days = 2; Nearly every day = 3 (the scores are the codes that appear in the answer list for each of the PHQ-9 problem panel terms). Interpretation: 1-4 = Minimal depression; 5-9 = Mild depression; 10-14 = Moderate depression; 15-19 = Moderately severe depression; 20-27 = Severed depression." + } + ] + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-unit", + "valueCoding": { + "display": "{score}" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/variable", + "valueExpression": { + "name": "scoreExt", + "language": "text/fhirpath", + "expression": "'http://hl7.org/fhir/StructureDefinition/ordinalValue'" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/variable", + "valueExpression": { + "name": "q1_value", + "language": "text/fhirpath", + "expression": "%questionnaire.item.where(linkId = '/44250-9').answerOption.where(valueCoding.code=%resource.item.where(linkId = '/44250-9').answer.valueCoding.code).extension.where(url=%scoreExt).valueDecimal" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/variable", + "valueExpression": { + "name": "q2_value", + "language": "text/fhirpath", + "expression": "%questionnaire.item.where(linkId = '/44255-8').answerOption.where(valueCoding.code=%resource.item.where(linkId = '/44255-8').answer.valueCoding.code).extension.where(url=%scoreExt).valueDecimal" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/variable", + "valueExpression": { + "name": "q3_value", + "language": "text/fhirpath", + "expression": "%questionnaire.item.where(linkId = '/44259-0').answerOption.where(valueCoding.code=%resource.item.where(linkId = '/44259-0').answer.valueCoding.code).extension.where(url=%scoreExt).valueDecimal" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/variable", + "valueExpression": { + "name": "q4_value", + "language": "text/fhirpath", + "expression": "%questionnaire.item.where(linkId = '/44254-1').answerOption.where(valueCoding.code=%resource.item.where(linkId = '/44254-1').answer.valueCoding.code).extension.where(url=%scoreExt).valueDecimal" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/variable", + "valueExpression": { + "name": "q5_value", + "language": "text/fhirpath", + "expression": "%questionnaire.item.where(linkId = '/44251-7').answerOption.where(valueCoding.code=%resource.item.where(linkId = '/44251-7').answer.valueCoding.code).extension.where(url=%scoreExt).valueDecimal" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/variable", + "valueExpression": { + "name": "q6_value", + "language": "text/fhirpath", + "expression": "%questionnaire.item.where(linkId = '/44258-2').answerOption.where(valueCoding.code=%resource.item.where(linkId = '/44258-2').answer.valueCoding.code).extension.where(url=%scoreExt).valueDecimal" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/variable", + "valueExpression": { + "name": "q7_value", + "language": "text/fhirpath", + "expression": "%questionnaire.item.where(linkId = '/44252-5').answerOption.where(valueCoding.code=%resource.item.where(linkId = '/44252-5').answer.valueCoding.code).extension.where(url=%scoreExt).valueDecimal" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/variable", + "valueExpression": { + "name": "q8_value", + "language": "text/fhirpath", + "expression": "%questionnaire.item.where(linkId = '/44253-3').answerOption.where(valueCoding.code=%resource.item.where(linkId = '/44253-3').answer.valueCoding.code).extension.where(url=%scoreExt).valueDecimal" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/variable", + "valueExpression": { + "name": "q9_value", + "language": "text/fhirpath", + "expression": "%questionnaire.item.where(linkId = '/44260-8').answerOption.where(valueCoding.code=%resource.item.where(linkId = '/44260-8').answer.valueCoding.code).extension.where(url=%scoreExt).valueDecimal" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/variable", + "valueExpression": { + "name": "any_questions_answered", + "language": "text/fhirpath", + "expression": "%q1_value.exists() or %q2_value.exists() or %q3_value.exists() or %q4_value.exists() or %q5_value.exists() or %q6_value.exists() or %q7_value.exists() or %q8_value.exists() or %q9_value.exists()" + } + }, + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression", + "valueExpression": { + "description": "Total score calculation", + "language": "text/fhirpath", + "expression": "iif(%any_questions_answered, iif(%q1_value.exists(), %q1_value, 0) + iif(%q2_value.exists(), %q2_value, 0) + iif(%q3_value.exists(), %q3_value, 0) + iif(%q4_value.exists(), %q4_value, 0) + iif(%q5_value.exists(), %q5_value, 0) + iif(%q6_value.exists(), %q6_value, 0) + iif(%q7_value.exists(), %q7_value, 0) + iif(%q8_value.exists(), %q8_value, 0) + iif(%q9_value.exists(), %q9_value, 0), {})" + } + }, + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod", + "valueDuration": { + "value": 100, + "unit": "year", + "system": "http://unitsofmeasure.org", + "code": "a" + } + } + ], + "required": false, + "linkId": "/44261-6", + "code": [ + { + "system": "http://loinc.org", + "code": "44261-6", + "display": "Patient health questionnaire 9 item total score" + } + ], + "text": "Patient health questionnaire 9 item total score" + }, + { + "type": "choice", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "drop-down", + "display": "Drop down" + } + ], + "text": "Drop down" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory", + "valueCodeableConcept": { + "text": "If you checked off any problems on this questionnaire", + "coding": [ + { + "display": "If you checked off any problems on this questionnaire" + } + ] + } + }, + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod", + "valueDuration": { + "value": 100, + "unit": "year", + "system": "http://unitsofmeasure.org", + "code": "a" + } + } + ], + "required": false, + "linkId": "/69722-7", + "code": [ + { + "system": "http://loinc.org", + "code": "69722-7", + "display": "How difficult have these problems made it for you to do your work, take care of things at home, or get along with other people?" + } + ], + "text": "How difficult have these problems made it for you to do your work, take care of things at home, or get along with other people?", + "answerOption": [ + { + "valueCoding": { + "code": "LA6572-7", + "display": "Not difficult at all" + } + }, + { + "valueCoding": { + "code": "LA6573-5", + "display": "Somewhat difficult" + } + }, + { + "valueCoding": { + "code": "LA6575-0", + "display": "Very difficult" + } + }, + { + "valueCoding": { + "code": "LA6574-3", + "display": "Extremely difficult" + } + } + ] + } + ] +} diff --git a/test/supplements.test.js b/test/supplements.test.js new file mode 100644 index 0000000..0e775b3 --- /dev/null +++ b/test/supplements.test.js @@ -0,0 +1,30 @@ +const fhirpath = require("../src/fhirpath"); +const r4_model = require("../fhir-context/r4"); +const _ = require("lodash"); + +const input = { + get questionnaire() { + // Clone input file contents to avoid one test affecting another + return _.cloneDeep(require("../test/resources/phq9.json")); + }, + get questionnaireResponse() { + // Clone input file contents to avoid one test affecting another + return _.cloneDeep(require("../test/resources/phq9-response.json")); + }, +}; + +describe("supplements", () => { + + describe('weight()', () => { + it("should return correct results when getting scores from the Questionnaire resource", () => { + const res = fhirpath.evaluate( + input.questionnaireResponse, + '%context.repeat(item).where(linkId!=\'total\').answer.weight().sum()', + { + questionnaire: input.questionnaire + }, r4_model); + expect(res).toStrictEqual([15]); + }); + }); + +}); From 842367bdd405382004e0eb113e6d605353fe6074 Mon Sep 17 00:00:00 2001 From: sedinkinya Date: Fri, 17 May 2024 15:44:23 -0400 Subject: [PATCH 2/9] Changes as per review LF-2100 --- .../phq9-response-with-embedded-scores.json | 158 ++++++++++++++++++ .../phq9-response-with-unlinked-answers.json | 146 ++++++++++++++++ test/resources/phq9.json | 22 --- test/supplements.test.js | 28 ++++ 4 files changed, 332 insertions(+), 22 deletions(-) create mode 100644 test/resources/phq9-response-with-embedded-scores.json create mode 100644 test/resources/phq9-response-with-unlinked-answers.json diff --git a/test/resources/phq9-response-with-embedded-scores.json b/test/resources/phq9-response-with-embedded-scores.json new file mode 100644 index 0000000..2c57d5d --- /dev/null +++ b/test/resources/phq9-response-with-embedded-scores.json @@ -0,0 +1,158 @@ +{ + "resourceType": "QuestionnaireResponse", + "meta": { + "profile": [ + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaireresponse|3.0" + ], + "tag": [ + { + "code": "lformsVersion: 35.0.4" + } + ] + }, + "status": "completed", + "authored": "2024-04-04T18:41:13.730Z", + "item": [ + { + "answer": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 2 + } + ], + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + } + ], + "linkId": "/44250-9", + "text": "Little interest or pleasure in doing things?" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + } + ], + "linkId": "/44255-8", + "text": "Feeling down, depressed, or hopeless?" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6571-9", + "display": "Nearly every day" + } + } + ], + "linkId": "/44259-0", + "text": "Trouble falling or staying asleep, or sleeping too much" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + } + ], + "linkId": "/44254-1", + "text": "Feeling tired or having little energy" + }, + { + "answer": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 3 + } + ], + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + } + ], + "linkId": "/44251-7", + "text": "Poor appetite or overeating" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6571-9", + "display": "Nearly every day" + } + } + ], + "linkId": "/44258-2", + "text": "Feeling bad about yourself-or that you are a failure or have let yourself or your family down" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6568-5", + "display": "Not at all" + } + } + ], + "linkId": "/44252-5", + "text": "Trouble concentrating on things, such as reading the newspaper or watching television" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + } + ], + "linkId": "/44253-3", + "text": "Moving or speaking so slowly that other people could have noticed. Or the opposite-being so fidgety or restless that you have been moving around a lot more than usual" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + } + ], + "linkId": "/44260-8", + "text": "Thoughts that you would be better off dead, or of hurting yourself in some way" + }, + { + "answer": [ + { + "valueDecimal": 15 + } + ], + "linkId": "/44261-6", + "text": "Patient health questionnaire 9 item total score" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6572-7", + "display": "Not difficult at all" + } + } + ], + "linkId": "/69722-7", + "text": "How difficult have these problems made it for you to do your work, take care of things at home, or get along with other people?" + } + ] +} diff --git a/test/resources/phq9-response-with-unlinked-answers.json b/test/resources/phq9-response-with-unlinked-answers.json new file mode 100644 index 0000000..435c375 --- /dev/null +++ b/test/resources/phq9-response-with-unlinked-answers.json @@ -0,0 +1,146 @@ +{ + "resourceType": "QuestionnaireResponse", + "meta": { + "profile": [ + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaireresponse|3.0" + ], + "tag": [ + { + "code": "lformsVersion: 35.0.4" + } + ] + }, + "status": "completed", + "authored": "2024-04-04T18:41:13.730Z", + "item": [ + { + "answer": [ + { + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + } + ], + "linkId": "/44250-9-unlinked-item", + "text": "Little interest or pleasure in doing things?" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + } + ], + "linkId": "/44255-8", + "text": "Feeling down, depressed, or hopeless?" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6571-9-unlinked-answer", + "display": "Nearly every day" + } + } + ], + "linkId": "/44259-0", + "text": "Trouble falling or staying asleep, or sleeping too much" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + } + ], + "linkId": "/44254-1", + "text": "Feeling tired or having little energy" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + } + ], + "linkId": "/44251-7", + "text": "Poor appetite or overeating" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6571-9", + "display": "Nearly every day" + } + } + ], + "linkId": "/44258-2", + "text": "Feeling bad about yourself-or that you are a failure or have let yourself or your family down" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6568-5", + "display": "Not at all" + } + } + ], + "linkId": "/44252-5", + "text": "Trouble concentrating on things, such as reading the newspaper or watching television" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6569-3", + "display": "Several days" + } + } + ], + "linkId": "/44253-3", + "text": "Moving or speaking so slowly that other people could have noticed. Or the opposite-being so fidgety or restless that you have been moving around a lot more than usual" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6570-1", + "display": "More than half the days" + } + } + ], + "linkId": "/44260-8", + "text": "Thoughts that you would be better off dead, or of hurting yourself in some way" + }, + { + "answer": [ + { + "valueDecimal": 15 + } + ], + "linkId": "/44261-6", + "text": "Patient health questionnaire 9 item total score" + }, + { + "answer": [ + { + "valueCoding": { + "code": "LA6572-7", + "display": "Not difficult at all" + } + } + ], + "linkId": "/69722-7", + "text": "How difficult have these problems made it for you to do your work, take care of things at home, or get along with other people?" + } + ] +} diff --git a/test/resources/phq9.json b/test/resources/phq9.json index cda64c8..128b953 100644 --- a/test/resources/phq9.json +++ b/test/resources/phq9.json @@ -994,17 +994,6 @@ { "type": "decimal", "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory", - "valueCodeableConcept": { - "text": "The PHQ-9 is the standard (and most commonly used) depression measure, and it ranges from 0-27 Scoring: Add up all checked boxes on PHQ-9. For every check: Not at all = 0; Several days = 1; More than half the days = 2; Nearly every day = 3 (the scores are the codes that appear in the answer list for each of the PHQ-9 problem panel terms). Interpretation: 1-4 = Minimal depression; 5-9 = Mild depression; 10-14 = Moderate depression; 15-19 = Moderately severe depression; 20-27 = Severed depression.", - "coding": [ - { - "display": "The PHQ-9 is the standard (and most commonly used) depression measure, and it ranges from 0-27 Scoring: Add up all checked boxes on PHQ-9. For every check: Not at all = 0; Several days = 1; More than half the days = 2; Nearly every day = 3 (the scores are the codes that appear in the answer list for each of the PHQ-9 problem panel terms). Interpretation: 1-4 = Minimal depression; 5-9 = Mild depression; 10-14 = Moderate depression; 15-19 = Moderately severe depression; 20-27 = Severed depression." - } - ] - } - }, { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-unit", "valueCoding": { @@ -1144,17 +1133,6 @@ "text": "Drop down" } }, - { - "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory", - "valueCodeableConcept": { - "text": "If you checked off any problems on this questionnaire", - "coding": [ - { - "display": "If you checked off any problems on this questionnaire" - } - ] - } - }, { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod", "valueDuration": { diff --git a/test/supplements.test.js b/test/supplements.test.js index 0e775b3..3180939 100644 --- a/test/supplements.test.js +++ b/test/supplements.test.js @@ -11,6 +11,14 @@ const input = { // Clone input file contents to avoid one test affecting another return _.cloneDeep(require("../test/resources/phq9-response.json")); }, + get questionnaireResponseWithEmbeddedScores() { + // Clone input file contents to avoid one test affecting another + return _.cloneDeep(require("./resources/phq9-response-with-embedded-scores.json")); + }, + get questionnaireResponseWithUnlinkedAnswers() { + // Clone input file contents to avoid one test affecting another + return _.cloneDeep(require("./resources/phq9-response-with-unlinked-answers.json")); + }, }; describe("supplements", () => { @@ -25,6 +33,26 @@ describe("supplements", () => { }, r4_model); expect(res).toStrictEqual([15]); }); + + it("should return correct results when getting some scores from the QuestionnaireResponse resource", () => { + const res = fhirpath.evaluate( + input.questionnaireResponseWithEmbeddedScores, + '%context.repeat(item).where(linkId!=\'total\').answer.weight().sum()', + { + questionnaire: input.questionnaire + }, r4_model); + expect(res).toStrictEqual([17]); + }); + + it("should throw an error when getting scores for answers that doesn't exists in the Questionnaire", () => { + const res = () => fhirpath.evaluate( + input.questionnaireResponseWithUnlinkedAnswers, + '%context.repeat(item).where(linkId!=\'total\').answer.weight().sum()', + { + questionnaire: input.questionnaire + }, r4_model); + expect(res).toThrow('Questionnaire answerOptions with these linkIds were not found: /44250-9-unlinked-item,/44259-0.'); + }); }); }); From e58a1b6b789ba43f9d423105bf6dbc3a258f0949 Mon Sep 17 00:00:00 2001 From: sedinkinya Date: Tue, 21 May 2024 19:30:04 -0400 Subject: [PATCH 3/9] Fix https://github.com/HL7/fhirpath.js/issues/136 LF-2591 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- src/fhirpath.js | 19 +++++++++---------- test/cases/3.2_paths.yaml | 9 +++++++++ 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aab9299..d894963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ This log documents significant changes for each release. This project follows [Semantic Versioning](http://semver.org/). +## [3.14.1] - 2024-05-21 +### Fixed +- impossibility to use attribute name that starts with a capital letter. + ## [3.14.0] - 2024-05-09 ### Added - supplementary function `weight()`. diff --git a/package-lock.json b/package-lock.json index 75150fb..b4e31ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fhirpath", - "version": "3.14.0", + "version": "3.14.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "fhirpath", - "version": "3.14.0", + "version": "3.14.1", "hasInstallScript": true, "license": "SEE LICENSE in LICENSE.md", "dependencies": { diff --git a/package.json b/package.json index 3690e2e..3ee13e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fhirpath", - "version": "3.14.0", + "version": "3.14.1", "description": "A FHIRPath engine", "main": "src/fhirpath.js", "dependencies": { diff --git a/src/fhirpath.js b/src/fhirpath.js index 848f3e9..c0c9662 100644 --- a/src/fhirpath.js +++ b/src/fhirpath.js @@ -377,17 +377,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, null, res.__path__?.path || null, null, - res.__path__?.fhirNodeDataType || null); + return parentData.reduce(function(acc, res) { + res = makeResNode(res, null, res.__path__?.path || null, null, + res.__path__?.fhirNodeDataType || null); + if (res.data?.resourceType === key) { + acc.push(res); + } else { util.pushFn(acc, util.makeChildResNodes(res, key, model)); - return acc; - }, []); - } + } + return acc; + }, []); } else { return []; } diff --git a/test/cases/3.2_paths.yaml b/test/cases/3.2_paths.yaml index 7dd1caa..eef75b2 100644 --- a/test/cases/3.2_paths.yaml +++ b/test/cases/3.2_paths.yaml @@ -99,9 +99,18 @@ tests: model: 'stu3' result: ['Green'] + - desc: "Access a custom field starting with a capital letter (1)" + expression: CustomField = 'test' + result: [true] + + - desc: "Access a custom field starting with a capital letter (2)" + expression: Observation.CustomField = 'test' + result: [true] + subject: resourceType: Observation + CustomField: "test" valueString: "high" contained: - resourceType: Observation From 749fc98162752707c9ed4a9532c7991ae70035e8 Mon Sep 17 00:00:00 2001 From: sedinkinya Date: Tue, 4 Jun 2024 11:48:45 -0400 Subject: [PATCH 4/9] Revert "Fix https://github.com/HL7/fhirpath.js/issues/136" This reverts commit e58a1b6b789ba43f9d423105bf6dbc3a258f0949. --- CHANGELOG.md | 4 ---- package-lock.json | 4 ++-- package.json | 2 +- src/fhirpath.js | 19 ++++++++++--------- test/cases/3.2_paths.yaml | 9 --------- 5 files changed, 13 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d894963..aab9299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,6 @@ This log documents significant changes for each release. This project follows [Semantic Versioning](http://semver.org/). -## [3.14.1] - 2024-05-21 -### Fixed -- impossibility to use attribute name that starts with a capital letter. - ## [3.14.0] - 2024-05-09 ### Added - supplementary function `weight()`. diff --git a/package-lock.json b/package-lock.json index b4e31ea..75150fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fhirpath", - "version": "3.14.1", + "version": "3.14.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "fhirpath", - "version": "3.14.1", + "version": "3.14.0", "hasInstallScript": true, "license": "SEE LICENSE in LICENSE.md", "dependencies": { diff --git a/package.json b/package.json index 3ee13e8..3690e2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fhirpath", - "version": "3.14.1", + "version": "3.14.0", "description": "A FHIRPath engine", "main": "src/fhirpath.js", "dependencies": { diff --git a/src/fhirpath.js b/src/fhirpath.js index c0c9662..848f3e9 100644 --- a/src/fhirpath.js +++ b/src/fhirpath.js @@ -377,16 +377,17 @@ engine.MemberInvocation = function(ctx, parentData, node ) { const model = ctx.model; if (parentData) { - return parentData.reduce(function(acc, res) { - res = makeResNode(res, null, res.__path__?.path || null, null, - res.__path__?.fhirNodeDataType || null); - if (res.data?.resourceType === key) { - acc.push(res); - } else { + if(util.isCapitalized(key)) { + return parentData + .filter((x) => x instanceof ResourceNode && x.path === key); + } else { + return parentData.reduce(function(acc, res) { + res = makeResNode(res, null, res.__path__?.path || null, null, + res.__path__?.fhirNodeDataType || null); util.pushFn(acc, util.makeChildResNodes(res, key, model)); - } - return acc; - }, []); + return acc; + }, []); + } } else { return []; } diff --git a/test/cases/3.2_paths.yaml b/test/cases/3.2_paths.yaml index eef75b2..7dd1caa 100644 --- a/test/cases/3.2_paths.yaml +++ b/test/cases/3.2_paths.yaml @@ -99,18 +99,9 @@ tests: model: 'stu3' result: ['Green'] - - desc: "Access a custom field starting with a capital letter (1)" - expression: CustomField = 'test' - result: [true] - - - desc: "Access a custom field starting with a capital letter (2)" - expression: Observation.CustomField = 'test' - result: [true] - subject: resourceType: Observation - CustomField: "test" valueString: "high" contained: - resourceType: Observation From 5d1c9a040fc0d425b299d93968ba7149e6bda383 Mon Sep 17 00:00:00 2001 From: sedinkinya Date: Wed, 12 Jun 2024 13:45:56 -0400 Subject: [PATCH 5/9] Changes as per review LF-2100 --- src/fhirpath.js | 6 +-- src/sdc-ig-supplements.js | 86 +++++++++++++++++++++++++++++++++++++++ src/supplements.js | 78 ----------------------------------- src/types.js | 6 +-- test/supplements.test.js | 2 +- 5 files changed, 93 insertions(+), 85 deletions(-) create mode 100644 src/sdc-ig-supplements.js delete mode 100644 src/supplements.js diff --git a/src/fhirpath.js b/src/fhirpath.js index 848f3e9..4a9d9ed 100644 --- a/src/fhirpath.js +++ b/src/fhirpath.js @@ -38,7 +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("./supplements"); +let supplements = require("./sdc-ig-supplements"); let combining = require("./combining"); let misc = require("./misc"); let equality = require("./equality"); @@ -862,8 +862,8 @@ function typesFn(fhirpathResult) { return util.arraify(fhirpathResult).map(value => { const ti = TypeInfo.fromValue( value?.__path__ - ? new ResourceNode(value, value.__path__?.parentResNode || null, - value.__path__?.path || null, null, value.__path__?.fhirNodeDataType || null) + ? new ResourceNode(value, value.__path__?.parentResNode, + value.__path__?.path, null, value.__path__?.fhirNodeDataType) : value ); return `${ti.namespace}.${ti.name}`; }); diff --git a/src/sdc-ig-supplements.js b/src/sdc-ig-supplements.js new file mode 100644 index 0000000..653a879 --- /dev/null +++ b/src/sdc-ig-supplements.js @@ -0,0 +1,86 @@ +// 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 scoreExtUrl = this.vars.scoreExt || this.processedVars.scoreExt; + const res = []; + + const questionnaire = this.vars.questionnaire || this.processedVars.questionnaire?.data; + coll.forEach((answer) => { + if (answer.data.valueCoding) { + const score = answer.data.extension?.find(e => e.url === scoreExtUrl)?.valueDecimal; + if (score !== undefined) { + // if we have a score extension in the source item, use it. + res.push(score); + } else if (questionnaire) { + const qItem = getQItemByLinkIds(questionnaire, getLinkIds(answer.parentResNode)); + const valueCoding = answer.data.valueCoding; + const answerOption = qItem?.answerOption?.find(o => + o.valueCoding.code === valueCoding.code + && o.valueCoding.system === valueCoding.system + ); + if (answerOption) { + const score = answerOption.extension?.find(e => e.url === scoreExtUrl)?.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 were not found: ' + answer.parentResNode.data.linkId + '.'); + } + } else { + throw new Error('%questionnaire is needed but not specified.'); + } + } + }); + + return res; +}; + +/** + * Returns array of linkIds of parent ResourceNodes and source ResourceNode. + * @param {ResourceNode} node - source ResourceNode. + * @return {String[]} + */ +function getLinkIds(node) { + const res = []; + + while (node.data?.linkId) { + res.unshift(node.data.linkId); + node = node.parentResNode; + } + + return res; +} + +/** + * Returns a questionnaire item based on the linkIds array of the parent + * ResourceNodes and the target ResourceNode. + * @param {Object} questionnaire - object with a Questionnaire resource. + * @param {string[]} linkIds - array of linkIds. + * @return {Object} + */ +function getQItemByLinkIds(questionnaire, linkIds) { + let currentNode = questionnaire; + for(let i = 0; i < linkIds.length; ++i) { + currentNode = currentNode.item?.find(o => o.linkId === linkIds[i]); + if (!currentNode) { + return null; + } + } + return currentNode; +} + +module.exports = engine; diff --git a/src/supplements.js b/src/supplements.js deleted file mode 100644 index f814f70..0000000 --- a/src/supplements.js +++ /dev/null @@ -1,78 +0,0 @@ -// Contains the supplementary FHIRPath functions. - -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 scoreExtUrl = this.vars.scoreExt || this.processedVars.scoreExt; - const res = []; - const linkId2Code = {}; - - coll.forEach((answer) => { - if (answer.data.valueCoding) { - const score = answer.data.extension?.find(e => e.url === scoreExtUrl)?.valueDecimal; - if (score !== undefined) { - // if we have a score extension in the source item, use it. - res.push(score); - } else { - // otherwise we will try to find the score in the %questionnaire. - linkId2Code[answer.parentResNode.data.linkId] = answer.data.valueCoding.code; - } - } - }); - - const questionnaire = this.vars.questionnaire || this.processedVars.questionnaire?.data; - if (questionnaire) { - forEachQItem(questionnaire, (qItem) => { - const code = linkId2Code[qItem.linkId]; - if (code) { - const answerOption = qItem.answerOption?.find(o => o.valueCoding.code === code); - if (answerOption) { - delete linkId2Code[qItem.linkId]; - const score = answerOption.extension?.find(e => e.url === scoreExtUrl)?.valueDecimal; - if (score !== undefined) { - // if we have a score extension for the answerOption, use it. - res.push(score); - } - } - } - }); - } - - // Check for errors. - const unfoundLinkIds = Object.keys(linkId2Code); - if (unfoundLinkIds.length) { - if (questionnaire) { - throw new Error('Questionnaire answerOptions with these linkIds were not found: ' + unfoundLinkIds.join(',') + '.'); - } else { - throw new Error('%questionnaire is needed but not specified.'); - } - } - - return res; -}; - -/** - * Runs a function for each questionnaire item. - * @param {Object} questionnaire - Questionnaire resource. - * @param {(item) => void} fn - function. - */ -function forEachQItem(questionnaire, fn) { - if(questionnaire.item) { - questionnaire.item.forEach((item) => { - fn(item); - forEachQItem(item, fn); - }); - } -} - -module.exports = engine; diff --git a/src/types.js b/src/types.js index 6b405c7..6ae0189 100644 --- a/src/types.js +++ b/src/types.js @@ -1305,11 +1305,11 @@ class ResourceNode { path = data.resourceType; fhirNodeDataType = data.resourceType; } - this.parentResNode = parentResNode; - this.path = path; + this.parentResNode = parentResNode || null; + this.path = path || null; this.data = data; this._data = _data || {}; - this.fhirNodeDataType = fhirNodeDataType; + this.fhirNodeDataType = fhirNodeDataType || null; } /** diff --git a/test/supplements.test.js b/test/supplements.test.js index 3180939..26e3c65 100644 --- a/test/supplements.test.js +++ b/test/supplements.test.js @@ -51,7 +51,7 @@ describe("supplements", () => { { questionnaire: input.questionnaire }, r4_model); - expect(res).toThrow('Questionnaire answerOptions with these linkIds were not found: /44250-9-unlinked-item,/44259-0.'); + expect(res).toThrow('Questionnaire answerOption with this linkId were not found: /44250-9-unlinked-item.'); }); }); From 187acf035503e90b5aaebfdb030b51776b3ef062 Mon Sep 17 00:00:00 2001 From: sedinkinya Date: Wed, 26 Jun 2024 17:33:29 -0400 Subject: [PATCH 6/9] Changes as per review LF-2100 --- CHANGELOG.md | 2 +- src/fhirpath.js | 37 ++++++---- src/sdc-ig-supplements.js | 69 +++++++++++++------ src/utilities.js | 2 +- test/resources/observation-example-2.json | 10 +++ .../phq9-response-with-embedded-scores.json | 28 ++++---- test/supplements.test.js | 68 ++++++++++-------- 7 files changed, 138 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aab9299..e03d347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ This log documents significant changes for each release. This project follows ## [3.14.0] - 2024-05-09 ### Added -- supplementary function `weight()`. +- supplementary function `weight()` with alternative name `ordinal()`. ## [3.13.1] - 2024-04-24 ### Fixed diff --git a/src/fhirpath.js b/src/fhirpath.js index 4a9d9ed..ffb386e 100644 --- a/src/fhirpath.js +++ b/src/fhirpath.js @@ -85,6 +85,7 @@ engine.invocationTable = { 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}, @@ -265,15 +266,15 @@ engine.ExternalConstantTerm = function(ctx, parentData, node) { if (Array.isArray(value)) { value = value.map( i => i?.__path__ - ? makeResNode(i, i.__path__.parentResNode, 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, null) : i ); } else { value = value?.__path__ - ? makeResNode(value, value.__path__.parentResNode, 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, null) : value; @@ -382,8 +383,8 @@ engine.MemberInvocation = function(ctx, parentData, node ) { .filter((x) => x instanceof ResourceNode && x.path === key); } else { return parentData.reduce(function(acc, res) { - res = makeResNode(res, null, res.__path__?.path || null, null, - res.__path__?.fhirNodeDataType || null); + res = makeResNode(res, null, res.__path__?.path, null, + res.__path__?.fhirNodeDataType); util.pushFn(acc, util.makeChildResNodes(res, key, model)); return acc; }, []); @@ -679,24 +680,34 @@ function applyParsedPath(resource, parsedPath, context, model, options) { let dataRoot = util.arraify(resource).map( i => i?.__path__ ? makeResNode(i, i.__path__.parentResNode, i.__path__.path, null, - i.__path__.fhirNodeDataType || 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', - scoreExt: 'http://hl7.org/fhir/StructureDefinition/ordinalValue' + let ctx = { + dataRoot, + processedVars: { + ucum: 'http://unitsofmeasure.org' + }, + vars: { + context: dataRoot, + ...context + }, + model }; - let ctx = {dataRoot, processedVars: vars, vars: 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. @@ -833,7 +844,7 @@ 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, null, basePath, null, baseFhirNodeDataType); diff --git a/src/sdc-ig-supplements.js b/src/sdc-ig-supplements.js index 653a879..bcd6816 100644 --- a/src/sdc-ig-supplements.js +++ b/src/sdc-ig-supplements.js @@ -14,31 +14,52 @@ let engine = {}; engine.weight = function (coll) { if(coll !== false && ! coll) { return []; } - const scoreExtUrl = this.vars.scoreExt || this.processedVars.scoreExt; + 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((answer) => { - if (answer.data.valueCoding) { - const score = answer.data.extension?.find(e => e.url === scoreExtUrl)?.valueDecimal; + if (answer?.data) { + let value = answer.data.valueCoding; + if (!value) { + const prop = Object.keys(answer.data).find(p => p.startsWith('value')); + // if we found a child value[x] property + value = prop + // we use it to get a score extension + ? answer.data[prop] + // otherwise, if the source item has a simple data type + : answer._data?.extension + // we get the extension from the adjacent property starting with an underscore + ? answer._data + // otherwise we get the extension from the source item + : answer.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 (questionnaire) { - const qItem = getQItemByLinkIds(questionnaire, getLinkIds(answer.parentResNode)); const valueCoding = answer.data.valueCoding; - const answerOption = qItem?.answerOption?.find(o => - o.valueCoding.code === valueCoding.code - && o.valueCoding.system === valueCoding.system - ); - if (answerOption) { - const score = answerOption.extension?.find(e => e.url === scoreExtUrl)?.valueDecimal; - if (score !== undefined) { - // if we have a score extension for the answerOption, use it. - res.push(score); + if (valueCoding) { + const qItem = getQItemByLinkIds( + questionnaire, getLinkIds(answer.parentResNode) + ); + 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: ' + answer.parentResNode.data.linkId + '.'); } - } else { - throw new Error('Questionnaire answerOption with this linkId were not found: ' + answer.parentResNode.data.linkId + '.'); } } else { throw new Error('%questionnaire is needed but not specified.'); @@ -50,7 +71,9 @@ engine.weight = function (coll) { }; /** - * Returns array of linkIds of parent ResourceNodes and source ResourceNode. + * 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[]} */ @@ -58,7 +81,7 @@ function getLinkIds(node) { const res = []; while (node.data?.linkId) { - res.unshift(node.data.linkId); + res.push(node.data.linkId); node = node.parentResNode; } @@ -66,15 +89,17 @@ function getLinkIds(node) { } /** - * Returns a questionnaire item based on the linkIds array of the parent - * ResourceNodes and the target ResourceNode. + * 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. - * @return {Object} + * @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 = 0; i < linkIds.length; ++i) { + for(let i = linkIds.length-1; i >= 0; --i) { currentNode = currentNode.item?.find(o => o.linkId === linkIds[i]); if (!currentNode) { return null; diff --git a/src/utilities.js b/src/utilities.js index fa66a64..16fec36 100644 --- a/src/utilities.js +++ b/src/utilities.js @@ -164,7 +164,7 @@ util.makeChildResNodes = function(parentResNode, childProperty, model) { let fhirNodeDataType = null; if (model) { - fhirNodeDataType = model.path2Type[childPath] || null; + fhirNodeDataType = model.path2Type[childPath]; childPath = model.path2TypeWithoutElements[childPath] || childPath; } diff --git a/test/resources/observation-example-2.json b/test/resources/observation-example-2.json index ce85bce..3a3e2e7 100644 --- a/test/resources/observation-example-2.json +++ b/test/resources/observation-example-2.json @@ -2,6 +2,16 @@ "resourceType": "Observation", "id": "example", "valueQuantity": { + "extension": [ + { + "url": "http://someScoreExtension", + "valueDecimal": 3 + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 4 + } + ], "value": 185, "unit": "months", "system": "http://unitsofmeasure.org", diff --git a/test/resources/phq9-response-with-embedded-scores.json b/test/resources/phq9-response-with-embedded-scores.json index 2c57d5d..bd732b5 100644 --- a/test/resources/phq9-response-with-embedded-scores.json +++ b/test/resources/phq9-response-with-embedded-scores.json @@ -16,15 +16,15 @@ { "answer": [ { - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", - "valueDecimal": 2 - } - ], "valueCoding": { "code": "LA6569-3", - "display": "Several days" + "display": "Several days", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 2 + } + ] } } ], @@ -70,15 +70,15 @@ { "answer": [ { - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", - "valueDecimal": 3 - } - ], "valueCoding": { "code": "LA6570-1", - "display": "More than half the days" + "display": "More than half the days", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 3 + } + ] } } ], diff --git a/test/supplements.test.js b/test/supplements.test.js index 26e3c65..0bec96c 100644 --- a/test/supplements.test.js +++ b/test/supplements.test.js @@ -3,6 +3,10 @@ const r4_model = require("../fhir-context/r4"); const _ = require("lodash"); const input = { + get observation() { + // Clone input file contents to avoid one test affecting another + return _.cloneDeep(require("../test/resources/observation-example-2.json")); + }, get questionnaire() { // Clone input file contents to avoid one test affecting another return _.cloneDeep(require("../test/resources/phq9.json")); @@ -23,35 +27,45 @@ const input = { describe("supplements", () => { - describe('weight()', () => { - it("should return correct results when getting scores from the Questionnaire resource", () => { - const res = fhirpath.evaluate( - input.questionnaireResponse, - '%context.repeat(item).where(linkId!=\'total\').answer.weight().sum()', - { - questionnaire: input.questionnaire - }, r4_model); - expect(res).toStrictEqual([15]); - }); + ['weight', 'ordinal'].forEach(fnName => { + describe(fnName+'()', () => { + it("should return the correct result when getting scores from the Questionnaire resource", () => { + const res = fhirpath.evaluate( + input.questionnaireResponse, + `%context.repeat(item).answer.${fnName}().sum()`, + { + questionnaire: input.questionnaire + }, r4_model); + expect(res).toStrictEqual([15]); + }); - it("should return correct results when getting some scores from the QuestionnaireResponse resource", () => { - const res = fhirpath.evaluate( - input.questionnaireResponseWithEmbeddedScores, - '%context.repeat(item).where(linkId!=\'total\').answer.weight().sum()', - { - questionnaire: input.questionnaire - }, r4_model); - expect(res).toStrictEqual([17]); - }); + it("should return the correct result when getting some scores from the QuestionnaireResponse resource", () => { + const res = fhirpath.evaluate( + input.questionnaireResponseWithEmbeddedScores, + `%context.repeat(item).answer.${fnName}().sum()`, + { + questionnaire: input.questionnaire + }, r4_model); + expect(res).toStrictEqual([17]); + }); + + it("should throw an error when getting scores for answers that doesn't exists in the Questionnaire", () => { + const res = () => fhirpath.evaluate( + input.questionnaireResponseWithUnlinkedAnswers, + `%context.repeat(item).answer.${fnName}().sum()`, + { + questionnaire: input.questionnaire + }, r4_model); + expect(res).toThrow('Questionnaire answerOption with this linkId was not found: /44250-9-unlinked-item.'); + }); - it("should throw an error when getting scores for answers that doesn't exists in the Questionnaire", () => { - const res = () => fhirpath.evaluate( - input.questionnaireResponseWithUnlinkedAnswers, - '%context.repeat(item).where(linkId!=\'total\').answer.weight().sum()', - { - questionnaire: input.questionnaire - }, r4_model); - expect(res).toThrow('Questionnaire answerOption with this linkId were not found: /44250-9-unlinked-item.'); + it("should return the correct result when getting a score from the Observation resource", () => { + const res = fhirpath.evaluate( + input.observation, `%context.${fnName}()`, + { scoreExt: 'http://someScoreExtension'}, r4_model + ); + expect(res).toStrictEqual([3]); + }); }); }); From 0d129993f49c027ab6ae27859240720d882c2ae7 Mon Sep 17 00:00:00 2001 From: sedinkinya Date: Thu, 27 Jun 2024 16:33:42 -0400 Subject: [PATCH 7/9] Changes as per review LF-2100 --- src/sdc-ig-supplements.js | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/sdc-ig-supplements.js b/src/sdc-ig-supplements.js index bcd6816..ebf8618 100644 --- a/src/sdc-ig-supplements.js +++ b/src/sdc-ig-supplements.js @@ -23,26 +23,17 @@ engine.weight = function (coll) { const questionnaire = this.vars.questionnaire || this.processedVars.questionnaire?.data; coll.forEach((answer) => { if (answer?.data) { - let value = answer.data.valueCoding; + const valueCoding = answer.data.valueCoding; + let value = valueCoding; if (!value) { const prop = Object.keys(answer.data).find(p => p.startsWith('value')); - // if we found a child value[x] property - value = prop - // we use it to get a score extension - ? answer.data[prop] - // otherwise, if the source item has a simple data type - : answer._data?.extension - // we get the extension from the adjacent property starting with an underscore - ? answer._data - // otherwise we get the extension from the source item - : answer.data; + value = prop ? answer.data[prop] : null; } 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 (questionnaire) { - const valueCoding = answer.data.valueCoding; if (valueCoding) { const qItem = getQItemByLinkIds( questionnaire, getLinkIds(answer.parentResNode) @@ -58,7 +49,10 @@ engine.weight = function (coll) { res.push(score); } } else { - throw new Error('Questionnaire answerOption with this linkId was not found: ' + answer.parentResNode.data.linkId + '.'); + throw new Error( + 'Questionnaire answerOption with this linkId was not found: ' + + answer.parentResNode.data.linkId + + '. Looking upon the underlying CodeSystem is not supported yet.'); } } } else { From b09f545e4061f2b302655f162b13df752090543d Mon Sep 17 00:00:00 2001 From: sedinkinya Date: Mon, 1 Jul 2024 15:49:14 -0400 Subject: [PATCH 8/9] Changes as per review LF-2100 --- src/sdc-ig-supplements.js | 61 +++++++++++++++++++++++---------------- test/supplements.test.js | 24 +++++++++++++-- 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/src/sdc-ig-supplements.js b/src/sdc-ig-supplements.js index ebf8618..bf7e86a 100644 --- a/src/sdc-ig-supplements.js +++ b/src/sdc-ig-supplements.js @@ -21,42 +21,53 @@ engine.weight = function (coll) { const res = []; const questionnaire = this.vars.questionnaire || this.processedVars.questionnaire?.data; - coll.forEach((answer) => { - if (answer?.data) { - const valueCoding = answer.data.valueCoding; + coll.forEach((item) => { + if (item?.data) { + const valueCoding = item.data.valueCoding; let value = valueCoding; if (!value) { - const prop = Object.keys(answer.data).find(p => p.startsWith('value')); - value = prop ? answer.data[prop] : null; + 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 (questionnaire) { - if (valueCoding) { - const qItem = getQItemByLinkIds( - questionnaire, getLinkIds(answer.parentResNode) - ); - 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 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 answerOption with this linkId was not found: ' + - answer.parentResNode.data.linkId + - '. Looking upon the underlying CodeSystem is not supported yet.'); + throw new Error('%questionnaire is needed but not specified.'); } } - } else { - throw new Error('%questionnaire is needed but not specified.'); } } }); diff --git a/test/supplements.test.js b/test/supplements.test.js index 0bec96c..695d7c1 100644 --- a/test/supplements.test.js +++ b/test/supplements.test.js @@ -3,7 +3,11 @@ const r4_model = require("../fhir-context/r4"); const _ = require("lodash"); const input = { - get observation() { + get observationExample1() { + // Clone input file contents to avoid one test affecting another + return _.cloneDeep(require("../test/resources/observation-example.json")); + }, + get observationExample2() { // Clone input file contents to avoid one test affecting another return _.cloneDeep(require("../test/resources/observation-example-2.json")); }, @@ -59,13 +63,29 @@ describe("supplements", () => { expect(res).toThrow('Questionnaire answerOption with this linkId was not found: /44250-9-unlinked-item.'); }); + it("should return an empty array when the Observation resource doesn't have a score", () => { + const res = fhirpath.evaluate( + input.observationExample1, `%context.${fnName}()`, + { scoreExt: 'http://someScoreExtension'}, r4_model + ); + expect(res).toStrictEqual([]); + }); + it("should return the correct result when getting a score from the Observation resource", () => { const res = fhirpath.evaluate( - input.observation, `%context.${fnName}()`, + input.observationExample2, `%context.${fnName}()`, { scoreExt: 'http://someScoreExtension'}, r4_model ); expect(res).toStrictEqual([3]); }); + + it("should return the correct result when the source item has a score", () => { + const res = fhirpath.evaluate( + input.observationExample2, `%context.value.${fnName}()`, + {}, r4_model + ); + expect(res).toStrictEqual([4]); + }); }); }); From 16ac94af38a34aadd0b1a4f33db8c8028826316c Mon Sep 17 00:00:00 2001 From: sedinkinya Date: Tue, 2 Jul 2024 18:21:43 -0400 Subject: [PATCH 9/9] Revert "Revert "Fix https://github.com/HL7/fhirpath.js/issues/136"" This reverts commit 749fc981 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- src/fhirpath.js | 19 +++++++++---------- test/cases/3.2_paths.yaml | 9 +++++++++ 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e03d347..ae0bb16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ This log documents significant changes for each release. This project follows [Semantic Versioning](http://semver.org/). +## [3.14.1] - 2024-05-21 +### Fixed +- impossibility to use attribute name that starts with a capital letter. + ## [3.14.0] - 2024-05-09 ### Added - supplementary function `weight()` with alternative name `ordinal()`. diff --git a/package-lock.json b/package-lock.json index 75150fb..b4e31ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fhirpath", - "version": "3.14.0", + "version": "3.14.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "fhirpath", - "version": "3.14.0", + "version": "3.14.1", "hasInstallScript": true, "license": "SEE LICENSE in LICENSE.md", "dependencies": { diff --git a/package.json b/package.json index 3690e2e..3ee13e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fhirpath", - "version": "3.14.0", + "version": "3.14.1", "description": "A FHIRPath engine", "main": "src/fhirpath.js", "dependencies": { diff --git a/src/fhirpath.js b/src/fhirpath.js index ffb386e..24bdc80 100644 --- a/src/fhirpath.js +++ b/src/fhirpath.js @@ -378,17 +378,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, null, res.__path__?.path, null, - res.__path__?.fhirNodeDataType); + 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 []; } diff --git a/test/cases/3.2_paths.yaml b/test/cases/3.2_paths.yaml index 7dd1caa..eef75b2 100644 --- a/test/cases/3.2_paths.yaml +++ b/test/cases/3.2_paths.yaml @@ -99,9 +99,18 @@ tests: model: 'stu3' result: ['Green'] + - desc: "Access a custom field starting with a capital letter (1)" + expression: CustomField = 'test' + result: [true] + + - desc: "Access a custom field starting with a capital letter (2)" + expression: Observation.CustomField = 'test' + result: [true] + subject: resourceType: Observation + CustomField: "test" valueString: "high" contained: - resourceType: Observation