diff --git a/CHANGELOG.md b/CHANGELOG.md index 9738da8..11e6d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ This log documents significant changes for each release. This project follows [Semantic Versioning](http://semver.org/). +## [3.14.1] - 2024-07-02 +### Fixed +- impossibility to use attribute name that starts with a capital letter. + +## [3.14.0] - 2024-07-02 +### Added +- supplementary function `weight()` with alternative name `ordinal()`. + ## [3.13.4] - 2024-06-13 ### Fixed - a bug that could cause the context input parameter containing environment diff --git a/package-lock.json b/package-lock.json index 021d344..ffd3cbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fhirpath", - "version": "3.13.4", + "version": "3.14.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "fhirpath", - "version": "3.13.4", + "version": "3.14.1", "hasInstallScript": true, "license": "SEE LICENSE in LICENSE.md", "dependencies": { diff --git a/package.json b/package.json index f847d04..ec10f4d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fhirpath", - "version": "3.13.4", + "version": "3.14.1", "description": "A FHIRPath engine", "main": "src/fhirpath.js", "types": "src/fhirpath.d.ts", diff --git a/src/fhirpath.js b/src/fhirpath.js index d919d83..7fa3991 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("./sdc-ig-supplements"); let combining = require("./combining"); let misc = require("./misc"); let equality = require("./equality"); @@ -83,6 +84,8 @@ engine.invocationTable = { min: {fn: aggregate.minFn}, max: {fn: aggregate.maxFn}, avg: {fn: aggregate.avgFn}, + weight: {fn: supplements.weight}, + ordinal: {fn: supplements.weight}, single: {fn: filtering.singleFn}, first: {fn: filtering.firstFn}, last: {fn: filtering.lastFn}, @@ -194,7 +197,7 @@ engine.TermExpression = function(ctx, parentData, node) { if (parentData) { parentData = parentData.map((x) => { if (x instanceof Object && x.resourceType) { - return makeResNode(x, x.resourceType, null, x.resourceType); + return makeResNode(x, null, x.resourceType, null, x.resourceType); } return x; }); @@ -263,17 +266,17 @@ engine.ExternalConstantTerm = function(ctx, parentData, node) { if (Array.isArray(value)) { value = value.map( i => i?.__path__ - ? makeResNode(i, i.__path__.path || null, null, - i.__path__.fhirNodeDataType || null) + ? makeResNode(i, i.__path__.parentResNode, i.__path__.path, null, + i.__path__.fhirNodeDataType) : i?.resourceType - ? makeResNode(i, null, null) + ? makeResNode(i, null, null, null) : i ); } else { value = value?.__path__ - ? makeResNode(value, value.__path__.path || null, null, - value.__path__.fhirNodeDataType || null) + ? makeResNode(value, value.__path__.parentResNode, value.__path__.path, null, + value.__path__.fhirNodeDataType) : value?.resourceType - ? makeResNode(value, null, null) + ? makeResNode(value, null, null, null) : value; } ctx.processedVars[varName] = value; @@ -377,17 +380,16 @@ engine.MemberInvocation = function(ctx, parentData, node ) { const model = ctx.model; if (parentData) { - if(util.isCapitalized(key)) { - return parentData - .filter((x) => x instanceof ResourceNode && x.path === key); - } else { - return parentData.reduce(function(acc, res) { - res = makeResNode(res, res.__path__?.path || null, null, - res.__path__?.fhirNodeDataType || null); + return parentData.reduce(function(acc, res) { + res = makeResNode(res, null, res.__path__?.path, null, + res.__path__?.fhirNodeDataType); + if (res.data?.resourceType === key) { + acc.push(res); + } else { util.pushFn(acc, util.makeChildResNodes(res, key, model)); - return acc; - }, []); - } + } + return acc; + }, []); } else { return []; } @@ -678,21 +680,35 @@ function applyParsedPath(resource, parsedPath, context, model, options) { constants.reset(); let dataRoot = util.arraify(resource).map( i => i?.__path__ - ? makeResNode(i, i.__path__.path, null, - i.__path__.fhirNodeDataType || null) + ? makeResNode(i, i.__path__.parentResNode, i.__path__.path, null, + i.__path__.fhirNodeDataType) : i ); // doEval takes a "ctx" object, and we store things in that as we parse, so we // need to put user-provided variable data in a sub-object, ctx.vars. // Set up default standard variables, and allow override from the variables. // However, we'll keep our own copy of dataRoot for internal processing. - let vars = {context: dataRoot, ucum: 'http://unitsofmeasure.org'}; - let ctx = {dataRoot, processedVars: vars, vars: {...context}, model}; + let ctx = { + dataRoot, + processedVars: { + ucum: 'http://unitsofmeasure.org' + }, + vars: { + context: dataRoot, + ...context + }, + model + }; if (options.traceFn) { ctx.customTraceFn = options.traceFn; } if (options.userInvocationTable) { ctx.userInvocationTable = options.userInvocationTable; } + ctx.defaultScoreExts = [ + 'http://hl7.org/fhir/StructureDefinition/ordinalValue', + 'http://hl7.org/fhir/StructureDefinition/itemWeight', + 'http://hl7.org/fhir/StructureDefinition/questionnaire-ordinalValue' + ]; return engine.doEval(ctx, dataRoot, parsedPath.children[0]) // engine.doEval returns array of "ResourceNode" and/or "FP_Type" instances. // "ResourceNode" or "FP_Type" instances are not created for sub-items. @@ -703,9 +719,11 @@ function applyParsedPath(resource, parsedPath, context, model, options) { // Path for the data extracted from the resource. let path; let fhirNodeDataType; + let parentResNode; if (n instanceof ResourceNode) { path = n.path; fhirNodeDataType = n.fhirNodeDataType; + parentResNode = n.parentResNode; } n = util.valData(n); if (n instanceof FP_Type) { @@ -718,7 +736,7 @@ function applyParsedPath(resource, parsedPath, context, model, options) { // Add a hidden (non-enumerable) property with the path to the data extracted // from the resource. if (path && typeof n === 'object' && !n.__path__) { - Object.defineProperty(n, '__path__', { value: {path, fhirNodeDataType} }); + Object.defineProperty(n, '__path__', { value: {path, fhirNodeDataType, parentResNode} }); } acc.push(n); } @@ -827,10 +845,10 @@ function compile(path, model, options) { return function (fhirData, context) { if (path.base) { let basePath = model.pathsDefinedElsewhere[path.base] || path.base; - const baseFhirNodeDataType = model && model.path2Type[basePath] || null; + const baseFhirNodeDataType = model && model.path2Type[basePath]; basePath = baseFhirNodeDataType === 'BackboneElement' || baseFhirNodeDataType === 'Element' ? basePath : baseFhirNodeDataType || basePath; - fhirData = makeResNode(fhirData, basePath, null, baseFhirNodeDataType); + fhirData = makeResNode(fhirData, null, basePath, null, baseFhirNodeDataType); } // Globally set model before applying parsed FHIRPath expression TypeInfo.model = model; @@ -856,8 +874,8 @@ function typesFn(fhirpathResult) { return util.arraify(fhirpathResult).map(value => { const ti = TypeInfo.fromValue( value?.__path__ - ? new ResourceNode(value, value.__path__?.path, null, - value.__path__?.fhirNodeDataType) + ? new ResourceNode(value, value.__path__?.parentResNode, + value.__path__?.path, null, value.__path__?.fhirNodeDataType) : value ); return `${ti.namespace}.${ti.name}`; }); 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/sdc-ig-supplements.js b/src/sdc-ig-supplements.js new file mode 100644 index 0000000..bf7e86a --- /dev/null +++ b/src/sdc-ig-supplements.js @@ -0,0 +1,116 @@ +// Contains the supplementary FHIRPath functions defined in the Structured Data +// Capture IG, https://hl7.org/fhir/uv/sdc/expressions.html#fhirpath-supplements. + +let engine = {}; + +/** + * Returns numeric values from the score extension associated with the input + * collection of Questionnaire items. See the description of the ordinal() + * function here: + * https://hl7.org/fhir/uv/sdc/expressions.html#fhirpath-supplements + * @param {Array} coll - questionnaire items + * @return {number[]} + */ +engine.weight = function (coll) { + if(coll !== false && ! coll) { return []; } + + const userScoreExtUrl = this.vars.scoreExt || this.processedVars.scoreExt; + const checkExtUrl = userScoreExtUrl + ? (e) => e.url === userScoreExtUrl + : (e) => this.defaultScoreExts.includes(e.url); + const res = []; + + const questionnaire = this.vars.questionnaire || this.processedVars.questionnaire?.data; + coll.forEach((item) => { + if (item?.data) { + const valueCoding = item.data.valueCoding; + let value = valueCoding; + if (!value) { + const prop = Object.keys(item.data).find(p => p.length > 5 && p.startsWith('value')); + // if we found a child value[x] property + value = prop + // we use it to get a score extension + ? item.data[prop] + // otherwise, if the source item has a simple data type + : item._data?.extension + // we get the extension from the adjacent property starting with + // an underscore + ? item._data + // otherwise we get the extension from the source item + // (e.g. 'item' is a Coding) + : item.data; + } + const score = value?.extension?.find(checkExtUrl)?.valueDecimal; + if (score !== undefined) { + // if we have a score extension in the source item, use it. + res.push(score); + } else if (valueCoding) { + const linkIds = getLinkIds(item.parentResNode); + if (linkIds.length) { + if (questionnaire) { + const qItem = getQItemByLinkIds(questionnaire, linkIds); + const answerOption = qItem?.answerOption?.find(o => + o.valueCoding.code === valueCoding.code + && o.valueCoding.system === valueCoding.system + ); + if (answerOption) { + const score = answerOption.extension?.find(checkExtUrl)?.valueDecimal; + if (score !== undefined) { + // if we have a score extension for the answerOption, use it. + res.push(score); + } + } else { + throw new Error( + 'Questionnaire answerOption with this linkId was not found: ' + + item.parentResNode.data.linkId + '.'); + } + } else { + throw new Error('%questionnaire is needed but not specified.'); + } + } + } + } + }); + + return res; +}; + +/** + * Returns array of linkIds of ancestor ResourceNodes and source ResourceNode + * starting with the linkId of the given node and ending with the topmost item's + * linkId. + * @param {ResourceNode} node - source ResourceNode. + * @return {String[]} + */ +function getLinkIds(node) { + const res = []; + + while (node.data?.linkId) { + res.push(node.data.linkId); + node = node.parentResNode; + } + + return res; +} + +/** + * Returns a questionnaire item based on the linkIds array of the ancestor + * ResourceNodes and the target ResourceNode. If the questionnaire item is not + * found, it returns null. + * @param {Object} questionnaire - object with a Questionnaire resource. + * @param {string[]} linkIds - array of linkIds starting with the linkId of the + * target node and ending with the topmost item's linkId. + * @return {Object | null} + */ +function getQItemByLinkIds(questionnaire, linkIds) { + let currentNode = questionnaire; + for(let i = linkIds.length-1; i >= 0; --i) { + currentNode = currentNode.item?.find(o => o.linkId === linkIds[i]); + if (!currentNode) { + return null; + } + } + return currentNode; +} + +module.exports = engine; diff --git a/src/types.js b/src/types.js index 1419c5d..6ae0189 100644 --- a/src/types.js +++ b/src/types.js @@ -1287,27 +1287,29 @@ class ResourceNode { * Constructs a instance for the given node ("data") of a resource. If the * data is the top-level node of a resouce, the path and type parameters will * be ignored in favor of the resource's resourceType field. - * @param {*} data the node's data or value (which might be an object with + * @param {*} data - the node's data or value (which might be an object with * sub-nodes, an array, or FHIR data type) - * @param {string} path the node's path in the resource (e.g. Patient.name). + * @param {ResourceNode} parentResNode - parent ResourceNode. + * @param {string} path - the node's path in the resource (e.g. Patient.name). * If the data's type can be determined from data, that will take precedence * over this parameter. - * @param {*} _data additional data stored in a property named with "_" + * @param {*} _data - additional data stored in a property named with "_" * prepended, see https://www.hl7.org/fhir/element.html#json for details. - * @param {string} fhirNodeDataType FHIR node data type, if the resource node + * @param {string} fhirNodeDataType - FHIR node data type, if the resource node * is described in the FHIR model. */ - constructor(data, path, _data, fhirNodeDataType) { + constructor(data, parentResNode, path, _data, fhirNodeDataType) { // If data is a resource (maybe a contained resource) reset the path // information to the resource type. if (data?.resourceType) { path = data.resourceType; fhirNodeDataType = data.resourceType; } - this.path = path; + this.parentResNode = parentResNode || null; + this.path = path || null; this.data = data; this._data = _data || {}; - this.fhirNodeDataType = fhirNodeDataType; + this.fhirNodeDataType = fhirNodeDataType || null; } /** @@ -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..16fec36 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){ @@ -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; } @@ -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/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 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 new file mode 100644 index 0000000..bd732b5 --- /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": [ + { + "valueCoding": { + "code": "LA6569-3", + "display": "Several days", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 2 + } + ] + } + } + ], + "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", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", + "valueDecimal": 3 + } + ] + } + } + ], + "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-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..128b953 --- /dev/null +++ b/test/resources/phq9.json @@ -0,0 +1,1184 @@ +{ + "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-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/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..695d7c1 --- /dev/null +++ b/test/supplements.test.js @@ -0,0 +1,92 @@ +const fhirpath = require("../src/fhirpath"); +const r4_model = require("../fhir-context/r4"); +const _ = require("lodash"); + +const input = { + 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")); + }, + 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")); + }, + 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", () => { + + ['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 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 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.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]); + }); + }); + }); + +});