Skip to content

Commit bfc60d4

Browse files
Merge branch 'bugfix/LF-3212/read-variables-from-context-only-when-used-in-an-expression' into 'master'
Read variables from context only when they are used in expression See merge request lfor/fhirpath.js!23
2 parents 352cee5 + 52763a9 commit bfc60d4

7 files changed

+64
-27
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
This log documents significant changes for each release. This project follows
44
[Semantic Versioning](http://semver.org/).
55

6+
## [3.16.1] - 2025-01-09
7+
### Fixed
8+
- Read environment variables only when they are used in an expression, avoiding
9+
unnecessary getter calls when working with libraries like Jotai.
10+
611
## [3.16.0] - 2024-10-10
712
### Added
813
- Support for type factory API (%factory).

README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ These will define additional global variables like "fhirpath_dstu2_model",
5555
Evaluating FHIRPath:
5656

5757
```js
58-
evaluate(resourceObject, fhirPathExpression, environment, model, options);
58+
evaluate(resourceObject, fhirPathExpression, envVars, model, options);
5959
```
6060
where:
6161
* resourceObject - FHIR resource, part of a resource (in this case
@@ -65,7 +65,7 @@ where:
6565
or object, if fhirData represents the part of the FHIR resource:
6666
* fhirPathExpression.base - base path in resource from which fhirData was extracted
6767
* fhirPathExpression.expression - FHIRPath expression relative to path.base
68-
* environment - a hash of variable name/value pairs.
68+
* envVars - a hash of variable name/value pairs.
6969
* model - the "model" data object specific to a domain, e.g. R4.
7070
For example, you could pass in the result of require("fhirpath/fhir-context/r4");
7171
* options - additional options:
@@ -154,7 +154,7 @@ the option "resolveInternalTypes" = false:
154154
155155
```js
156156
const contextVariable = fhirpath.evaluate(
157-
resource, expression, context, model, {resolveInternalTypes: false}
157+
resource, expression, envVars, model, {resolveInternalTypes: false}
158158
);
159159
```
160160
@@ -181,7 +181,7 @@ In the next example, `res` will have a value like this:
181181
182182
```js
183183
const res = fhirpath.types(
184-
fhirpath.evaluate(resource, expression, context, model, {resolveInternalTypes: false})
184+
fhirpath.evaluate(resource, expression, envVars, model, {resolveInternalTypes: false})
185185
);
186186
```
187187
@@ -191,7 +191,7 @@ let tracefunction = function (x, label) {
191191
console.log("Trace output [" + label + "]: ", x);
192192
};
193193

194-
const res = fhirpath.evaluate(contextNode, path, environment, fhirpath_r4_model, { traceFn: tracefunction });
194+
const res = fhirpath.evaluate(contextNode, path, envVars, fhirpath_r4_model, { traceFn: tracefunction });
195195
```
196196
197197
### Asynchronous functions

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fhirpath",
3-
"version": "3.16.0",
3+
"version": "3.16.1",
44
"description": "A FHIRPath engine",
55
"main": "src/fhirpath.js",
66
"types": "src/fhirpath.d.ts",

src/fhirpath.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export function compile<T extends OptionVariants>(
77
export function evaluate<T extends OptionVariants>(
88
fhirData: any,
99
path: string | Path,
10-
context?: Context,
10+
envVars?: Context,
1111
model?: Model,
1212
options?: T
1313
): ReturnType<T>;
@@ -66,7 +66,7 @@ type ReturnType<T> =
6666
T extends NoAsyncOptions ? any[] :
6767
any[] | Promise<any[]>;
6868

69-
type Compile<T> = (resource: any, context?: Context) => ReturnType<T>;
69+
type Compile<T> = (resource: any, envVars?: Context) => ReturnType<T>;
7070

7171
type Context = void | Record<string, any>;
7272

src/fhirpath.js

+19-17
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,8 @@ engine.ExternalConstantTerm = function(ctx, parentData, node) {
262262
// Check the user-defined environment variables first as the user can override
263263
// the "context" variable like we do in unit tests. In this case, the user
264264
// environment variable can replace the system environment variable in "processedVars".
265-
if (varName in ctx.vars) {
265+
// If the user-defined environment variable has been processed, we don't need to process it again.
266+
if (varName in ctx.vars && !ctx.processedUserVarNames.has(varName)) {
266267
// Restore the ResourceNodes for the top-level objects of the environment
267268
// variables. The nested objects will be converted to ResourceNodes
268269
// in the MemberInvocation method.
@@ -284,7 +285,7 @@ engine.ExternalConstantTerm = function(ctx, parentData, node) {
284285
: value;
285286
}
286287
ctx.processedVars[varName] = value;
287-
delete ctx.vars[varName];
288+
ctx.processedUserVarNames.add(varName);
288289
} else if (varName in ctx.processedVars) {
289290
// "processedVars" are variables with ready-to-use values that have already
290291
// been converted to ResourceNodes if necessary.
@@ -705,7 +706,7 @@ function parse(path) {
705706
* @param {(object|object[])} resource - FHIR resource, bundle as js object or array of resources
706707
* This resource will be modified by this function to add type information.
707708
* @param {object} parsedPath - a special object created by the parser that describes the structure of a fhirpath expression.
708-
* @param {object} context - a hash of variable name/value pairs.
709+
* @param {object} envVars - a hash of variable name/value pairs.
709710
* @param {object} model - The "model" data object specific to a domain, e.g. R4.
710711
* For example, you could pass in the result of require("fhirpath/fhir-context/r4");
711712
* @param {object} options - additional options:
@@ -722,26 +723,27 @@ function parse(path) {
722723
* RESTful API that is used to create %terminologies that implements
723724
* the Terminology Service API.
724725
*/
725-
function applyParsedPath(resource, parsedPath, context, model, options) {
726+
function applyParsedPath(resource, parsedPath, envVars, model, options) {
726727
constants.reset();
727728
let dataRoot = util.arraify(resource).map(
728729
i => i?.__path__
729730
? makeResNode(i, i.__path__.parentResNode, i.__path__.path, null,
730731
i.__path__.fhirNodeDataType)
731-
: i );
732+
: i?.resourceType
733+
? makeResNode(i, null, null, null)
734+
: i);
732735
// doEval takes a "ctx" object, and we store things in that as we parse, so we
733736
// need to put user-provided variable data in a sub-object, ctx.vars.
734737
// Set up default standard variables, and allow override from the variables.
735738
// However, we'll keep our own copy of dataRoot for internal processing.
736739
let ctx = {
737740
dataRoot,
738741
processedVars: {
739-
ucum: 'http://unitsofmeasure.org'
740-
},
741-
vars: {
742-
context: dataRoot,
743-
...context
742+
ucum: 'http://unitsofmeasure.org',
743+
context: dataRoot
744744
},
745+
processedUserVarNames: new Set(),
746+
vars: envVars || {},
745747
model
746748
};
747749
if (options.traceFn) {
@@ -843,7 +845,7 @@ function resolveInternalTypes(val) {
843845
* or object, if fhirData represents the part of the FHIR resource:
844846
* @param {string} path.base - base path in resource from which fhirData was extracted
845847
* @param {string} path.expression - FHIRPath expression relative to path.base
846-
* @param {object} [context] - a hash of variable name/value pairs.
848+
* @param {object} [envVars] - a hash of variable name/value pairs.
847849
* @param {object} [model] - The "model" data object specific to a domain, e.g. R4.
848850
* For example, you could pass in the result of require("fhirpath/fhir-context/r4");
849851
* @param {object} [options] - additional options:
@@ -862,8 +864,8 @@ function resolveInternalTypes(val) {
862864
* RESTful API that is used to create %terminologies that implements
863865
* the Terminology Service API.
864866
*/
865-
function evaluate(fhirData, path, context, model, options) {
866-
return compile(path, model, options)(fhirData, context);
867+
function evaluate(fhirData, path, envVars, model, options) {
868+
return compile(path, model, options)(fhirData, envVars);
867869
}
868870

869871
/**
@@ -924,7 +926,7 @@ function compile(path, model, options) {
924926

925927
if (typeof path === 'object') {
926928
const node = parse(path.expression);
927-
return function (fhirData, context) {
929+
return function (fhirData, envVars) {
928930
if (path.base) {
929931
let basePath = model.pathsDefinedElsewhere[path.base] || path.base;
930932
const baseFhirNodeDataType = model && model.path2Type[basePath];
@@ -934,14 +936,14 @@ function compile(path, model, options) {
934936
}
935937
// Globally set model before applying parsed FHIRPath expression
936938
TypeInfo.model = model;
937-
return applyParsedPath(fhirData, node, context, model, options);
939+
return applyParsedPath(fhirData, node, envVars, model, options);
938940
};
939941
} else {
940942
const node = parse(path);
941-
return function (fhirData, context) {
943+
return function (fhirData, envVars) {
942944
// Globally set model before applying parsed FHIRPath expression
943945
TypeInfo.model = model;
944-
return applyParsedPath(fhirData, node, context, model, options);
946+
return applyParsedPath(fhirData, node, envVars, model, options);
945947
};
946948
}
947949
}

test/api.test.js

+30
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,33 @@ describe('evaluate type() on a FHIRPath evaluation result', () => {
273273
})
274274
});
275275

276+
describe('evaluate environment variables', () => {
277+
it('context can be immutable', () => {
278+
const vars = Object.freeze({a: 'abc', b: 'def'});
279+
expect(fhirpath.evaluate(
280+
{},
281+
'%a = \'abc\'',
282+
vars
283+
)).toStrictEqual([true]);
284+
})
285+
286+
it('context can be immutable when new variables are defined', () => {
287+
const vars = Object.freeze({a: 'abc'});
288+
expect(fhirpath.evaluate(
289+
{},
290+
"%a.defineVariable('b')",
291+
vars
292+
)).toStrictEqual(['abc']);
293+
});
294+
it('variables are only read when needed', () => {
295+
const vars = {
296+
get a() { return 'abc'; },
297+
get b() { throw new Error('b should not be read'); }
298+
};
299+
expect(fhirpath.evaluate(
300+
{},
301+
'%a = \'abc\'',
302+
vars
303+
)).toStrictEqual([true]);
304+
})
305+
});

0 commit comments

Comments
 (0)