Skip to content

Commit 2cf8494

Browse files
authored
Merge pull request #54 from beda-software/questionnaire-variable
support questionnaire-level fhirpath variables
2 parents e1070f2 + 6be364d commit 2cf8494

7 files changed

Lines changed: 177 additions & 1 deletion

File tree

src/converter/__tests__/fce.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import fce_practitioner_create_structure_map from './resources/questionnaire_fce
3737
import fce_practitioner_edit from './resources/questionnaire_fce/practitioner_edit.json';
3838
import fce_practitioner_role_create from './resources/questionnaire_fce/practitioner_role_create.json';
3939
import fce_public_appointment from './resources/questionnaire_fce/public_appointment.json';
40+
import fce_questionnaire_variable from './resources/questionnaire_fce/questionnaire_variable.json';
4041
import fce_review_of_systems from './resources/questionnaire_fce/review_of_systems.json';
4142
import fce_source_queries from './resources/questionnaire_fce/source_queries.json';
4243
import fce_sub_questionnaire from './resources/questionnaire_fce/sub-questionnaire.json';
@@ -81,6 +82,7 @@ import fhir_practitioner_create_structure_map from './resources/questionnaire_fh
8182
import fhir_practitioner_edit from './resources/questionnaire_fhir/practitioner_edit.json';
8283
import fhir_practitioner_role_create from './resources/questionnaire_fhir/practitioner_role_create.json';
8384
import fhir_public_appointment from './resources/questionnaire_fhir/public_appointment.json';
85+
import fhir_questionnaire_variable from './resources/questionnaire_fhir/questionnaire_variable.json';
8486
import fhir_review_of_systems from './resources/questionnaire_fhir/review_of_systems.json';
8587
import fhir_source_queries from './resources/questionnaire_fhir/source_queries.json';
8688
import fhir_sub_questionnaire from './resources/questionnaire_fhir/sub-questionnaire.json';
@@ -128,6 +130,7 @@ describe('Questionanire and QuestionnaireResponses transformation', () => {
128130
['answerOptionsToggleExpression', fhir_answer_options_toggle_expression, fce_answer_options_toggle_expression],
129131
['unit-option', fhir_unit_option, fce_unit_option],
130132
['variable', fhir_variable, fce_variable],
133+
['questionnaire-variable', fhir_questionnaire_variable, fce_questionnaire_variable],
131134
['sub-questionnaire', fhir_sub_questionnaire, fce_sub_questionnaire],
132135
['with-attachment-questionnaire', fhir_with_attachment_questionnaire, fce_with_attachment_questionnaire],
133136
// NOTE: this following is not included here because it's example of mixed FCE + extensions, it is only for FCE -> FHIR
@@ -180,6 +183,7 @@ describe('Questionanire and QuestionnaireResponses transformation', () => {
180183
['answerOptionsToggleExpression', fce_answer_options_toggle_expression, fhir_answer_options_toggle_expression],
181184
['unit-option', fce_unit_option, fhir_unit_option],
182185
['variable', fce_variable, fhir_variable],
186+
['questionnaire-variable', fce_questionnaire_variable, fhir_questionnaire_variable],
183187
['sub-questionnaire', fce_sub_questionnaire, fhir_sub_questionnaire],
184188
['with-attachment-questionnaire', fce_with_attachment_questionnaire, fhir_with_attachment_questionnaire],
185189
['mixed-fce-with-extensions', fce_mixed_fce_with_extensions, fhir_mixed_fce_with_extensions],
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"resourceType": "Questionnaire",
3+
"id": "questionnaire-variable",
4+
"meta": {
5+
"profile": [
6+
"https://emr-core.beda.software/StructureDefinition/fhir-emr-questionnaire"
7+
]
8+
},
9+
"status": "active",
10+
"variable": [
11+
{
12+
"name": "First",
13+
"language": "text/fhirpath",
14+
"expression": "'First'"
15+
},
16+
{
17+
"name": "Second",
18+
"language": "text/fhirpath",
19+
"expression": "%First + 'Second'"
20+
}
21+
],
22+
"item": [
23+
{
24+
"type": "string",
25+
"linkId": "test"
26+
}
27+
]
28+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"item": [
3+
{
4+
"type": "string",
5+
"linkId": "test"
6+
}
7+
],
8+
"meta": {
9+
"profile": ["https://emr-core.beda.software/StructureDefinition/fhir-emr-questionnaire"]
10+
},
11+
"extension": [
12+
{
13+
"url": "http://hl7.org/fhir/StructureDefinition/variable",
14+
"valueExpression": {
15+
"name": "First",
16+
"language": "text/fhirpath",
17+
"expression": "'First'"
18+
}
19+
},
20+
{
21+
"url": "http://hl7.org/fhir/StructureDefinition/variable",
22+
"valueExpression": {
23+
"name": "Second",
24+
"language": "text/fhirpath",
25+
"expression": "%First + 'Second'"
26+
}
27+
}
28+
],
29+
"status": "active",
30+
"id": "questionnaire-variable",
31+
"resourceType": "Questionnaire"
32+
}

src/converter/fceToFhir/questionnaire/processExtensions.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export function processExtensions(questionnaire: FCEQuestionnaire): FHIRQuestion
1010
assembledFrom,
1111
assembleContext,
1212
itemPopulationContext,
13+
variable,
1314
...fhirQuestionnaire
1415
} = questionnaire;
1516

@@ -107,6 +108,15 @@ export function processExtensions(questionnaire: FCEQuestionnaire): FHIRQuestion
107108
});
108109
}
109110

111+
if (variable) {
112+
extensions = extensions.concat(
113+
variable.map((expr) => ({
114+
url: 'http://hl7.org/fhir/StructureDefinition/variable',
115+
valueExpression: expr,
116+
})),
117+
);
118+
}
119+
110120
if (extensions.length) {
111121
fhirQuestionnaire.extension = (fhirQuestionnaire.extension ?? []).concat(extensions);
112122
}

src/converter/fhirToFce/questionnaire/processExtensions.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export function processExtensions(fhirQuestionnaire: FHIRQuestionnaire): {
88
assembledFrom?: any;
99
assembleContext?: string[];
1010
itemPopulationContext?: FHIRExpression;
11+
variable?: FHIRExpression[];
1112
} {
1213
const launchContext = processLaunchContext(fhirQuestionnaire);
1314
const mapping = processMapping(fhirQuestionnaire);
@@ -16,6 +17,7 @@ export function processExtensions(fhirQuestionnaire: FHIRQuestionnaire): {
1617
const assembledFrom = processAssembledFrom(fhirQuestionnaire);
1718
const assembleContext = processAssembleContext(fhirQuestionnaire);
1819
const itemPopulationContext = processItemPopulationContext(fhirQuestionnaire);
20+
const variable = processVariable(fhirQuestionnaire);
1921

2022
return {
2123
launchContext: launchContext?.length ? launchContext : undefined,
@@ -25,6 +27,7 @@ export function processExtensions(fhirQuestionnaire: FHIRQuestionnaire): {
2527
assembledFrom: assembledFrom ? assembledFrom : undefined,
2628
assembleContext: assembleContext.length ? assembleContext : undefined,
2729
itemPopulationContext: itemPopulationContext ? itemPopulationContext : undefined,
30+
variable: variable?.length ? variable : undefined,
2831
};
2932
}
3033

@@ -141,3 +144,15 @@ function processItemPopulationContext(fhirQuestionnaire: FHIRQuestionnaire): FHI
141144

142145
return extension.valueExpression;
143146
}
147+
148+
function processVariable(fhirQuestionnaire: FHIRQuestionnaire): FHIRExpression[] | undefined {
149+
const extensions = fhirQuestionnaire.extension?.filter(
150+
(ext) => ext.url === 'http://hl7.org/fhir/StructureDefinition/variable',
151+
);
152+
153+
if (!extensions?.length) {
154+
return undefined;
155+
}
156+
157+
return extensions.map((ext) => ext.valueExpression!);
158+
}

src/utils.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,13 +729,14 @@ export function getEnabledQuestions(
729729
export function calcInitialContext(
730730
qrfDataContext: QuestionnaireResponseFormData['context'],
731731
values: FormItems,
732+
evaluateFhirpath?: EvaluateFhirpath,
732733
): ItemContext {
733734
const questionnaireResponse = {
734735
...qrfDataContext.questionnaireResponse,
735736
...mapFormToResponse(values, qrfDataContext.questionnaire),
736737
};
737738

738-
return {
739+
const baseContext: ItemContext = {
739740
...qrfDataContext.launchContextParameters.reduce((acc, { name, resource, ...param }) => {
740741
const value = getChoiceTypeValue(param, 'value');
741742

@@ -754,6 +755,23 @@ export function calcInitialContext(
754755
Questionnaire: qrfDataContext.questionnaire,
755756
QuestionnaireResponse: questionnaireResponse,
756757
};
758+
759+
// Evaluate questionnaire-level FHIRPath variables in declaration order
760+
return (qrfDataContext.fceQuestionnaire.variable ?? []).reduce((ctx: ItemContext, variable) => {
761+
if (!variable?.name || !variable.expression || variable.language !== 'text/fhirpath') {
762+
console.warn(`Only fhirpath variables are supported. Variable ${variable.name} is not supported.`);
763+
return ctx;
764+
}
765+
return {
766+
...ctx,
767+
[variable.name]: evaluateFHIRPathExpression(
768+
variable,
769+
ctx,
770+
`questionnaire.variable.${variable.name}`,
771+
evaluateFhirpath,
772+
),
773+
};
774+
}, baseContext);
757775
}
758776

759777
export function resolveTemplateExpr(

tests/utils.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2549,6 +2549,75 @@ describe('calcInitialContext', () => {
25492549
StringValue: 'string',
25502550
});
25512551
});
2552+
2553+
test('evaluates questionnaire-level variables and adds them to the context', () => {
2554+
const result = calcInitialContext(
2555+
{
2556+
...qrfDataContext,
2557+
fceQuestionnaire: {
2558+
...questionnaire,
2559+
variable: [{ name: 'Greeting', language: 'text/fhirpath', expression: "'Hello'" }],
2560+
},
2561+
},
2562+
formValues,
2563+
);
2564+
2565+
expect(result.Greeting).toStrictEqual(['Hello']);
2566+
});
2567+
2568+
test('evaluates variables in declaration order so later ones can reference earlier ones', () => {
2569+
const result = calcInitialContext(
2570+
{
2571+
...qrfDataContext,
2572+
fceQuestionnaire: {
2573+
...questionnaire,
2574+
variable: [
2575+
{ name: 'First', language: 'text/fhirpath', expression: "'First'" },
2576+
{ name: 'Second', language: 'text/fhirpath', expression: "%First + 'Second'" },
2577+
],
2578+
},
2579+
},
2580+
formValues,
2581+
);
2582+
2583+
expect(result.First).toStrictEqual(['First']);
2584+
expect(result.Second).toStrictEqual(['FirstSecond']);
2585+
});
2586+
2587+
test('variables can reference launch context and questionnaire response values', () => {
2588+
const result = calcInitialContext(
2589+
{
2590+
...qrfDataContext,
2591+
fceQuestionnaire: {
2592+
...questionnaire,
2593+
variable: [{ name: 'PatientType', language: 'text/fhirpath', expression: '%Patient.resourceType' }],
2594+
},
2595+
},
2596+
formValues,
2597+
);
2598+
2599+
expect(result.PatientType).toStrictEqual(['Patient']);
2600+
});
2601+
2602+
test('skips variables that are not text/fhirpath or are missing name/expression', () => {
2603+
const result = calcInitialContext(
2604+
{
2605+
...qrfDataContext,
2606+
fceQuestionnaire: {
2607+
...questionnaire,
2608+
variable: [
2609+
{ name: 'Query', language: 'application/x-fhir-query', expression: '/Patient' } as any,
2610+
{ language: 'text/fhirpath', expression: "'no-name'" } as any,
2611+
{ name: 'NoExpression', language: 'text/fhirpath' } as any,
2612+
],
2613+
},
2614+
},
2615+
formValues,
2616+
);
2617+
2618+
expect(result).not.toHaveProperty('Query');
2619+
expect(result).not.toHaveProperty('NoExpression');
2620+
});
25522621
});
25532622

25542623
describe('parseFhirQueryExpression', () => {

0 commit comments

Comments
 (0)