Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/converter/__tests__/fce.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import fce_practitioner_create_structure_map from './resources/questionnaire_fce
import fce_practitioner_edit from './resources/questionnaire_fce/practitioner_edit.json';
import fce_practitioner_role_create from './resources/questionnaire_fce/practitioner_role_create.json';
import fce_public_appointment from './resources/questionnaire_fce/public_appointment.json';
import fce_questionnaire_variable from './resources/questionnaire_fce/questionnaire_variable.json';
import fce_review_of_systems from './resources/questionnaire_fce/review_of_systems.json';
import fce_source_queries from './resources/questionnaire_fce/source_queries.json';
import fce_sub_questionnaire from './resources/questionnaire_fce/sub-questionnaire.json';
Expand Down Expand Up @@ -81,6 +82,7 @@ import fhir_practitioner_create_structure_map from './resources/questionnaire_fh
import fhir_practitioner_edit from './resources/questionnaire_fhir/practitioner_edit.json';
import fhir_practitioner_role_create from './resources/questionnaire_fhir/practitioner_role_create.json';
import fhir_public_appointment from './resources/questionnaire_fhir/public_appointment.json';
import fhir_questionnaire_variable from './resources/questionnaire_fhir/questionnaire_variable.json';
import fhir_review_of_systems from './resources/questionnaire_fhir/review_of_systems.json';
import fhir_source_queries from './resources/questionnaire_fhir/source_queries.json';
import fhir_sub_questionnaire from './resources/questionnaire_fhir/sub-questionnaire.json';
Expand Down Expand Up @@ -128,6 +130,7 @@ describe('Questionanire and QuestionnaireResponses transformation', () => {
['answerOptionsToggleExpression', fhir_answer_options_toggle_expression, fce_answer_options_toggle_expression],
['unit-option', fhir_unit_option, fce_unit_option],
['variable', fhir_variable, fce_variable],
['questionnaire-variable', fhir_questionnaire_variable, fce_questionnaire_variable],
['sub-questionnaire', fhir_sub_questionnaire, fce_sub_questionnaire],
['with-attachment-questionnaire', fhir_with_attachment_questionnaire, fce_with_attachment_questionnaire],
// NOTE: this following is not included here because it's example of mixed FCE + extensions, it is only for FCE -> FHIR
Expand Down Expand Up @@ -180,6 +183,7 @@ describe('Questionanire and QuestionnaireResponses transformation', () => {
['answerOptionsToggleExpression', fce_answer_options_toggle_expression, fhir_answer_options_toggle_expression],
['unit-option', fce_unit_option, fhir_unit_option],
['variable', fce_variable, fhir_variable],
['questionnaire-variable', fce_questionnaire_variable, fhir_questionnaire_variable],
['sub-questionnaire', fce_sub_questionnaire, fhir_sub_questionnaire],
['with-attachment-questionnaire', fce_with_attachment_questionnaire, fhir_with_attachment_questionnaire],
['mixed-fce-with-extensions', fce_mixed_fce_with_extensions, fhir_mixed_fce_with_extensions],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"resourceType": "Questionnaire",
"id": "questionnaire-variable",
"meta": {
"profile": [
"https://emr-core.beda.software/StructureDefinition/fhir-emr-questionnaire"
]
},
"status": "active",
"variable": [
{
"name": "First",
"language": "text/fhirpath",
"expression": "'First'"
},
{
"name": "Second",
"language": "text/fhirpath",
"expression": "%First + 'Second'"
}
],
"item": [
{
"type": "string",
"linkId": "test"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"item": [
{
"type": "string",
"linkId": "test"
}
],
"meta": {
"profile": ["https://emr-core.beda.software/StructureDefinition/fhir-emr-questionnaire"]
},
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/variable",
"valueExpression": {
"name": "First",
"language": "text/fhirpath",
"expression": "'First'"
}
},
{
"url": "http://hl7.org/fhir/StructureDefinition/variable",
"valueExpression": {
"name": "Second",
"language": "text/fhirpath",
"expression": "%First + 'Second'"
}
}
],
"status": "active",
"id": "questionnaire-variable",
"resourceType": "Questionnaire"
}
10 changes: 10 additions & 0 deletions src/converter/fceToFhir/questionnaire/processExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export function processExtensions(questionnaire: FCEQuestionnaire): FHIRQuestion
assembledFrom,
assembleContext,
itemPopulationContext,
variable,
...fhirQuestionnaire
} = questionnaire;

Expand Down Expand Up @@ -107,6 +108,15 @@ export function processExtensions(questionnaire: FCEQuestionnaire): FHIRQuestion
});
}

if (variable) {
extensions = extensions.concat(
variable.map((expr) => ({
url: 'http://hl7.org/fhir/StructureDefinition/variable',
valueExpression: expr,
})),
);
}

if (extensions.length) {
fhirQuestionnaire.extension = (fhirQuestionnaire.extension ?? []).concat(extensions);
}
Expand Down
15 changes: 15 additions & 0 deletions src/converter/fhirToFce/questionnaire/processExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export function processExtensions(fhirQuestionnaire: FHIRQuestionnaire): {
assembledFrom?: any;
assembleContext?: string[];
itemPopulationContext?: FHIRExpression;
variable?: FHIRExpression[];
} {
const launchContext = processLaunchContext(fhirQuestionnaire);
const mapping = processMapping(fhirQuestionnaire);
Expand All @@ -16,6 +17,7 @@ export function processExtensions(fhirQuestionnaire: FHIRQuestionnaire): {
const assembledFrom = processAssembledFrom(fhirQuestionnaire);
const assembleContext = processAssembleContext(fhirQuestionnaire);
const itemPopulationContext = processItemPopulationContext(fhirQuestionnaire);
const variable = processVariable(fhirQuestionnaire);

return {
launchContext: launchContext?.length ? launchContext : undefined,
Expand All @@ -25,6 +27,7 @@ export function processExtensions(fhirQuestionnaire: FHIRQuestionnaire): {
assembledFrom: assembledFrom ? assembledFrom : undefined,
assembleContext: assembleContext.length ? assembleContext : undefined,
itemPopulationContext: itemPopulationContext ? itemPopulationContext : undefined,
variable: variable?.length ? variable : undefined,
};
}

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

return extension.valueExpression;
}

function processVariable(fhirQuestionnaire: FHIRQuestionnaire): FHIRExpression[] | undefined {
const extensions = fhirQuestionnaire.extension?.filter(
(ext) => ext.url === 'http://hl7.org/fhir/StructureDefinition/variable',
);

if (!extensions?.length) {
return undefined;
}

return extensions.map((ext) => ext.valueExpression!);
}
20 changes: 19 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,13 +729,14 @@ export function getEnabledQuestions(
export function calcInitialContext(
qrfDataContext: QuestionnaireResponseFormData['context'],
values: FormItems,
evaluateFhirpath?: EvaluateFhirpath,
): ItemContext {
const questionnaireResponse = {
...qrfDataContext.questionnaireResponse,
...mapFormToResponse(values, qrfDataContext.questionnaire),
};

return {
const baseContext: ItemContext = {
...qrfDataContext.launchContextParameters.reduce((acc, { name, resource, ...param }) => {
const value = getChoiceTypeValue(param, 'value');

Expand All @@ -754,6 +755,23 @@ export function calcInitialContext(
Questionnaire: qrfDataContext.questionnaire,
QuestionnaireResponse: questionnaireResponse,
};

// Evaluate questionnaire-level FHIRPath variables in declaration order
return (qrfDataContext.fceQuestionnaire.variable ?? []).reduce((ctx: ItemContext, variable) => {
if (!variable?.name || !variable.expression || variable.language !== 'text/fhirpath') {
console.warn(`Only fhirpath variables are supported. Variable ${variable.name} is not supported.`);
return ctx;
}
return {
...ctx,
[variable.name]: evaluateFHIRPathExpression(
variable,
ctx,
`questionnaire.variable.${variable.name}`,
evaluateFhirpath,
),
};
}, baseContext);
}

export function resolveTemplateExpr(
Expand Down
69 changes: 69 additions & 0 deletions tests/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2549,6 +2549,75 @@ describe('calcInitialContext', () => {
StringValue: 'string',
});
});

test('evaluates questionnaire-level variables and adds them to the context', () => {
const result = calcInitialContext(
{
...qrfDataContext,
fceQuestionnaire: {
...questionnaire,
variable: [{ name: 'Greeting', language: 'text/fhirpath', expression: "'Hello'" }],
},
},
formValues,
);

expect(result.Greeting).toStrictEqual(['Hello']);
});

test('evaluates variables in declaration order so later ones can reference earlier ones', () => {
const result = calcInitialContext(
{
...qrfDataContext,
fceQuestionnaire: {
...questionnaire,
variable: [
{ name: 'First', language: 'text/fhirpath', expression: "'First'" },
{ name: 'Second', language: 'text/fhirpath', expression: "%First + 'Second'" },
],
},
},
formValues,
);

expect(result.First).toStrictEqual(['First']);
expect(result.Second).toStrictEqual(['FirstSecond']);
});

test('variables can reference launch context and questionnaire response values', () => {
const result = calcInitialContext(
{
...qrfDataContext,
fceQuestionnaire: {
...questionnaire,
variable: [{ name: 'PatientType', language: 'text/fhirpath', expression: '%Patient.resourceType' }],
},
},
formValues,
);

expect(result.PatientType).toStrictEqual(['Patient']);
});

test('skips variables that are not text/fhirpath or are missing name/expression', () => {
const result = calcInitialContext(
{
...qrfDataContext,
fceQuestionnaire: {
...questionnaire,
variable: [
{ name: 'Query', language: 'application/x-fhir-query', expression: '/Patient' } as any,
{ language: 'text/fhirpath', expression: "'no-name'" } as any,
{ name: 'NoExpression', language: 'text/fhirpath' } as any,
],
},
},
formValues,
);

expect(result).not.toHaveProperty('Query');
expect(result).not.toHaveProperty('NoExpression');
});
});

describe('parseFhirQueryExpression', () => {
Expand Down
Loading