Repository navigation
sdc-populate: item FHIRPath variables are never evaluated when the Questionnaire has no root extensions #2162
Description
Summary
In @aehrc/sdc-populate 4.7.1, item-level variable extensions with language: text/fhirpath are evaluated only when Questionnaire.extension is present and non-empty. With no root extensions (or extension: []), population skips every item FHIRPath variable, so an initialExpression that reads one prefills nothing. Population still reports success, and the only issue is Attempting to access an undefined environment variable: <name>, which points at the initialExpression rather than at the skipped variable.
Item application/x-fhir-query variables are not affected: they are collected separately and their results are bound as usual. Only the text/fhirpath variables on items are skipped.
The impact is narrow. Any root extension, even an unrelated one, avoids it, and most populated forms have a root launchContext. populateQuestionnaire also runs population only when the form has a root launchContext, a root sourceQueries or an x-fhir-query variable somewhere (inputParameters.ts:65-76). So in practice the trigger is a form with no root extensions whose data comes from an x-fhir-query variable on an item, read through an item FHIRPath variable. Without a launchContext such a query cannot embed {{%patient.id}}: it is sent literally.
Steps to reproduce
- A Questionnaire with no root
extension, and a group item with two variables and a child item:weightObs(application/x-fhir-query):Observation?code=http://loinc.org|29463-7weight(text/fhirpath):%weightObs.entry.resource.value.value- child item
weight(decimal) withinitialExpression: %weight
- Call
populateQuestionnairewith afetchResourceCallbackthat returns a searchset Bundle holding one Observation withvalueQuantity.value72.
Expected: the weight item is prefilled with 72, and populatedContext.weight is [72].
Actual: no answer. The query runs once and populatedContext.weightObs holds the Bundle, but populatedContext has no weight. populateSuccess is true, with one issue: Error: Attempting to access an undefined environment variable: weight.
The same form with different root extensions:
Root extension |
Answer | populatedContext.weight |
Issues |
|---|---|---|---|
| absent | none | missing | undefined environment variable: weight |
[] |
none | missing | undefined environment variable: weight |
one unrelated extension (http://example.org/dummy) |
valueDecimal: 72 |
[72] |
none |
Reproduced with @aehrc/sdc-populate@4.7.1, fhirpath 4.10.1 and Node 22, calling populateQuestionnaire directly with a fetchResourceCallback that resolves to the Bundle itself. Calling populate directly gives the same result: a form with no root extensions, one item variable %patient.birthDate and a patient context parameter prefills nothing, and prefills the birth date once an unrelated root extension is added. (SDC requires a context name to match a root declaration, so that call is not conformant; it only isolates the gate.)
Cause
extractAndEvaluateFhirPathVariables in packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/createFhirPathContext.ts (:119-154, at f846e2d8, tag sdc-populate@4.7.1; unchanged on main at 9ec3e213, 4.7.2) evaluates root variables, then item variables. It starts with a guard meant for the root part (:126-128):
if (!questionnaire.extension || questionnaire.extension.length === 0) {
return;
}It returns before the item variables are read (:140-153). It is called once, from createFhirPathContext (:101-108), which both populate and syncXFhirQueries use.
History: the function, guard included, was added in 66169ce (December 2024, sdc-populate 3.0.0), when population started evaluating FHIRPath variables. Every release since then is affected.
The unit test skips evaluation if no extensions exist (createFhirPathContext.test.ts:228-243) uses item: [], so no test has item variables without root extensions.
The renderer has no such gate. extractQuestionnaireLevelVariables (questionnaireStoreUtils/extractVariables.ts:26-28) returns early on no root extensions, but only root variables are read there. Item variables are collected per item in extractOtherExtensions.ts:318-329 (from the source; not reproduced).
Suggested fix
Guard only the root part. evaluateFhirPathVariables already returns on an empty list, so the early return can go:
// Extract and evaluate FHIRPath variables from questionnaire-level
const questionnaireLevelVariables = getFhirPathVariables(questionnaire.extension ?? []);
await evaluateFhirPathVariables(
questionnaireLevelVariables,
fhirPathContext,
issues,
fetchTerminologyRequestConfig,
fetchResourceRequestConfig
);
// Extract and evaluate FHIRPath variables from item-level (unchanged)Add a test next to skips evaluation if no extensions exist: a Questionnaire with no extension and an item variable q1Var = 1 + 1, expecting fhirPathContext.q1Var to be [2] and no issues.
Activity