@openmrs/esm-form-engine-lib 2.1.0-pre.1510 → 2.1.0-pre.1511
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/22ec231da647cd2c/22ec231da647cd2c.gz +0 -0
- package/3a3e1d216bd6470d/3a3e1d216bd6470d.gz +0 -0
- package/485e0040f135cee8/485e0040f135cee8.gz +0 -0
- package/__mocks__/forms/rfe-forms/bmi-test-form.json +66 -66
- package/__mocks__/forms/rfe-forms/bsa-test-form.json +66 -66
- package/__mocks__/forms/rfe-forms/edd-test-form.json +87 -87
- package/__mocks__/forms/rfe-forms/external_data_source_form.json +35 -36
- package/__mocks__/forms/rfe-forms/historical-expressions-form.json +1 -1
- package/__mocks__/forms/rfe-forms/labour_and_delivery_test_form.json +1 -1
- package/__mocks__/forms/rfe-forms/months-on-art-form.json +89 -89
- package/__mocks__/forms/rfe-forms/next-visit-test-form.json +77 -77
- package/b3059e748360776a/b3059e748360776a.gz +0 -0
- package/dist/openmrs-esm-form-engine-lib.js +1 -1
- package/package.json +1 -1
- package/src/components/inputs/number/number.component.tsx +2 -1
- package/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx +1 -1
- package/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx +5 -0
- package/src/processors/encounter/encounter-form-processor.ts +2 -7
- package/src/utils/common-expression-helpers.test.ts +74 -5
- package/src/utils/common-expression-helpers.ts +21 -26
- package/src/utils/expression-runner.test.ts +156 -69
- package/src/utils/expression-runner.ts +85 -135
- package/src/utils/expression-parser.test.ts +0 -308
- package/src/utils/expression-parser.ts +0 -158
@@ -1,7 +1,6 @@
|
|
1
1
|
import { registerExpressionHelper } from '..';
|
2
2
|
import { type FormField } from '../types';
|
3
|
-
import {
|
4
|
-
import { checkReferenceToResolvedFragment, evaluateExpression, type ExpressionContext } from './expression-runner';
|
3
|
+
import { evaluateAsyncExpression, evaluateExpression, type ExpressionContext } from './expression-runner';
|
5
4
|
|
6
5
|
export const testFields: Array<FormField> = [
|
7
6
|
{
|
@@ -79,7 +78,100 @@ export const testFields: Array<FormField> = [
|
|
79
78
|
},
|
80
79
|
];
|
81
80
|
|
82
|
-
|
81
|
+
export const fields: Array<FormField> = [
|
82
|
+
{
|
83
|
+
label: 'No Interest',
|
84
|
+
type: 'obs',
|
85
|
+
questionOptions: {
|
86
|
+
rendering: 'radio',
|
87
|
+
concept: 'no_interest_concept',
|
88
|
+
answers: [],
|
89
|
+
},
|
90
|
+
id: 'no_interest',
|
91
|
+
},
|
92
|
+
{
|
93
|
+
label: 'Depressed',
|
94
|
+
type: 'obs',
|
95
|
+
questionOptions: {
|
96
|
+
rendering: 'radio',
|
97
|
+
concept: 'depressed_concept',
|
98
|
+
answers: [],
|
99
|
+
},
|
100
|
+
id: 'depressed',
|
101
|
+
},
|
102
|
+
{
|
103
|
+
label: 'Bad Sleep',
|
104
|
+
type: 'obs',
|
105
|
+
questionOptions: {
|
106
|
+
rendering: 'radio',
|
107
|
+
concept: 'bad_sleep_concept',
|
108
|
+
answers: [],
|
109
|
+
},
|
110
|
+
id: 'bad_sleep',
|
111
|
+
},
|
112
|
+
{
|
113
|
+
label: 'Feeling Tired',
|
114
|
+
type: 'obs',
|
115
|
+
questionOptions: {
|
116
|
+
rendering: 'radio',
|
117
|
+
concept: 'feeling_tired_concept',
|
118
|
+
answers: [],
|
119
|
+
},
|
120
|
+
id: 'feeling_tired',
|
121
|
+
},
|
122
|
+
{
|
123
|
+
label: 'Poor Appetite',
|
124
|
+
type: 'obs',
|
125
|
+
questionOptions: {
|
126
|
+
rendering: 'radio',
|
127
|
+
concept: 'poor_appetite_concept',
|
128
|
+
answers: [],
|
129
|
+
},
|
130
|
+
id: 'poor_appetite',
|
131
|
+
},
|
132
|
+
{
|
133
|
+
label: 'Troubled',
|
134
|
+
type: 'obs',
|
135
|
+
questionOptions: {
|
136
|
+
rendering: 'radio',
|
137
|
+
concept: 'troubled_concept',
|
138
|
+
answers: [],
|
139
|
+
},
|
140
|
+
id: 'troubled',
|
141
|
+
},
|
142
|
+
{
|
143
|
+
label: 'Feeling Bad',
|
144
|
+
type: 'obs',
|
145
|
+
questionOptions: {
|
146
|
+
rendering: 'radio',
|
147
|
+
concept: 'feeling_bad_concept',
|
148
|
+
answers: [],
|
149
|
+
},
|
150
|
+
id: 'feeling_bad',
|
151
|
+
},
|
152
|
+
{
|
153
|
+
label: 'Speaking Slowly',
|
154
|
+
type: 'obs',
|
155
|
+
questionOptions: {
|
156
|
+
rendering: 'radio',
|
157
|
+
concept: 'speaking_slowly_concept',
|
158
|
+
answers: [],
|
159
|
+
},
|
160
|
+
id: 'speaking_slowly',
|
161
|
+
},
|
162
|
+
{
|
163
|
+
label: 'Better Off Dead',
|
164
|
+
type: 'obs',
|
165
|
+
questionOptions: {
|
166
|
+
rendering: 'radio',
|
167
|
+
concept: 'better_dead_concept',
|
168
|
+
answers: [],
|
169
|
+
},
|
170
|
+
id: 'better_dead',
|
171
|
+
},
|
172
|
+
];
|
173
|
+
|
174
|
+
describe('Expression runner', () => {
|
83
175
|
const context: ExpressionContext = { mode: 'enter', patient: {} };
|
84
176
|
const allFields = JSON.parse(JSON.stringify(testFields));
|
85
177
|
let valuesMap = {
|
@@ -88,6 +180,15 @@ describe('Common expression runner - evaluateExpression', () => {
|
|
88
180
|
htsProviderRemarks: '',
|
89
181
|
referredToPreventionServices: [],
|
90
182
|
bodyTemperature: 0,
|
183
|
+
no_interest: '',
|
184
|
+
depressed: '',
|
185
|
+
bad_sleep: '',
|
186
|
+
feeling_tired: '',
|
187
|
+
poor_appetite: '',
|
188
|
+
troubled: '',
|
189
|
+
feeling_bad: '',
|
190
|
+
speaking_slowly: '',
|
191
|
+
better_dead: '',
|
91
192
|
};
|
92
193
|
|
93
194
|
afterEach(() => {
|
@@ -98,24 +199,37 @@ describe('Common expression runner - evaluateExpression', () => {
|
|
98
199
|
htsProviderRemarks: '',
|
99
200
|
referredToPreventionServices: [],
|
100
201
|
bodyTemperature: 0,
|
202
|
+
no_interest: '',
|
203
|
+
depressed: '',
|
204
|
+
bad_sleep: '',
|
205
|
+
feeling_tired: '',
|
206
|
+
poor_appetite: '',
|
207
|
+
troubled: '',
|
208
|
+
feeling_bad: '',
|
209
|
+
speaking_slowly: '',
|
210
|
+
better_dead: '',
|
101
211
|
};
|
102
212
|
allFields.forEach((field) => {
|
103
213
|
field.fieldDependents = undefined;
|
104
214
|
});
|
105
215
|
});
|
106
216
|
|
107
|
-
it('should
|
217
|
+
it('should support unary expressions', () => {
|
108
218
|
// replay and verify
|
109
219
|
expect(
|
110
220
|
evaluateExpression('true', { value: allFields[0], type: 'field' }, allFields, valuesMap, context),
|
111
221
|
).toBeTruthy();
|
112
222
|
// replay and verify
|
113
223
|
expect(
|
114
|
-
evaluateExpression('
|
224
|
+
evaluateExpression('!true', { value: allFields[0], type: 'field' }, allFields, valuesMap, context),
|
115
225
|
).toBeFalsy();
|
226
|
+
// replay and verify
|
227
|
+
expect(
|
228
|
+
evaluateExpression('!false', { value: allFields[0], type: 'field' }, allFields, valuesMap, context),
|
229
|
+
).toBeTruthy();
|
116
230
|
});
|
117
231
|
|
118
|
-
it('should support
|
232
|
+
it('should support binary expressions', () => {
|
119
233
|
// replay and verify
|
120
234
|
expect(
|
121
235
|
evaluateExpression(
|
@@ -140,46 +254,42 @@ describe('Common expression runner - evaluateExpression', () => {
|
|
140
254
|
).toBeTruthy();
|
141
255
|
});
|
142
256
|
|
143
|
-
it('should support
|
144
|
-
//
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
// replay and verify
|
159
|
-
expect(
|
160
|
-
evaluateExpression(
|
161
|
-
"linkedToCare == 'cf82933b-3f3f-45e7-a5ab-5d31aaee3da3' && htsProviderRemarks !== '' && bodyTemperature >= 39",
|
162
|
-
{ value: allFields[1], type: 'field' },
|
163
|
-
allFields,
|
164
|
-
valuesMap,
|
165
|
-
context,
|
166
|
-
),
|
167
|
-
).toBeTruthy();
|
257
|
+
it('should support complex expressions', () => {
|
258
|
+
// setup
|
259
|
+
valuesMap.bad_sleep = 'a53f32bc-6904-4692-8a4c-fb7403cf0306';
|
260
|
+
valuesMap.better_dead = '296b39ec-06c5-4310-8f30-d2c9f083fb71';
|
261
|
+
valuesMap.depressed = '5eb5852d-3d29-41f9-b2ff-d194e062003d';
|
262
|
+
valuesMap.feeling_bad = '349260db-8e0f-4c06-be92-5120b3708d1e';
|
263
|
+
valuesMap.feeling_tired = '0ea1378d-04eb-4e7e-908b-26d8c27d37e1';
|
264
|
+
valuesMap.troubled = '57766c65-6548-486b-9dad-0fedf531ed7d';
|
265
|
+
|
266
|
+
const expression =
|
267
|
+
"(no_interest === 'b631d160-8d40-4cf7-92cd-67f628c889e8' ? 1 : isEmpty(no_interest) ? 2 : no_interest === '8ff1f85c-4f04-4f5b-936a-5aa9320cb66e' ? 3 : 0) + (depressed === 'b631d160-8d40-4cf7-92cd-67f628c889e8' ? 1 : depressed === '5eb5852d-3d29-41f9-b2ff-d194e062003d' ? 2 : depressed==='8ff1f85c-4f04-4f5b-936a-5aa9320cb66e' ? 3 : 0) + (bad_sleep === 'a53f32bc-6904-4692-8a4c-fb7403cf0306' ? 1 : bad_sleep === '234259ec-5368-4488-8482-4f261cc76714' ? 2 : bad_sleep === '8ff1f85c-4f04-4f5b-936a-5aa9320cb66e' ? 3 : 0) + (feeling_tired === 'b631d160-8d40-4cf7-92cd-67f628c889e8' ? 1 : feeling_tired === '234259ec-5368-4488-8482-4f261cc76714' ? 2 : feeling_tired === '0ea1378d-04eb-4e7e-908b-26d8c27d37e1' ? 3 : 0) +(poor_appetite === 'b631d160-8d40-4cf7-92cd-67f628c889e8' ? 1 : poor_appetite === '234259ec-5368-4488-8482-4f261cc76714' ? 2 : poor_appetite === '8ff1f85c-4f04-4f5b-936a-5aa9320cb66e' ? 3 : 0) + (troubled === '57766c65-6548-486b-9dad-0fedf531ed7d' ? 1 : troubled === '234259ec-5368-4488-8482-4f261cc76714' ? 2 : troubled === '8ff1f85c-4f04-4f5b-936a-5aa9320cb66e' ? 3 : 0) + (feeling_bad === 'b631d160-8d40-4cf7-92cd-67f628c889e8' ? 1 : feeling_bad === '234259ec-5368-4488-8482-4f261cc76714' ? 2 : feeling_bad === '349260db-8e0f-4c06-be92-5120b3708d1e' ? 3 : 0) + (speaking_slowly === 'b631d160-8d40-4cf7-92cd-67f628c889e8' ? 1 : speaking_slowly === '234259ec-5368-4488-8482-4f261cc76714' ? 2 : speaking_slowly === '8ff1f85c-4f04-4f5b-936a-5aa9320cb66e' ? 3 : 0) + (better_dead === 'b631d160-8d40-4cf7-92cd-67f628c889e8' ? 1 : better_dead === '296b39ec-06c5-4310-8f30-d2c9f083fb71' ? 2 : better_dead === '8ff1f85c-4f04-4f5b-936a-5aa9320cb66e' ? 3 : 0)";
|
268
|
+
|
269
|
+
expect(evaluateExpression(expression, { value: allFields[9], type: 'field' }, allFields, valuesMap, context)).toBe(
|
270
|
+
14,
|
271
|
+
);
|
168
272
|
});
|
169
273
|
|
170
|
-
it('should support
|
274
|
+
it('should support async expressions', async () => {
|
171
275
|
// setup
|
172
|
-
valuesMap
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
276
|
+
valuesMap.bad_sleep = 'a53f32bc-6904-4692-8a4c-fb7403cf0306';
|
277
|
+
registerExpressionHelper('getAsyncValue', () => {
|
278
|
+
return new Promise((resolve) => {
|
279
|
+
setTimeout(() => {
|
280
|
+
resolve(18);
|
281
|
+
}, 10);
|
282
|
+
});
|
283
|
+
});
|
284
|
+
|
285
|
+
const result = await evaluateAsyncExpression(
|
286
|
+
'getAsyncValue().then(value => !isEmpty(bad_sleep) ? value + 3 : value)',
|
287
|
+
{ value: allFields[9], type: 'field' },
|
288
|
+
allFields,
|
289
|
+
valuesMap,
|
290
|
+
context,
|
291
|
+
);
|
292
|
+
expect(result).toBe(21);
|
183
293
|
});
|
184
294
|
|
185
295
|
it('should support includes(question, value) runtime helper function', () => {
|
@@ -191,7 +301,7 @@ describe('Common expression runner - evaluateExpression', () => {
|
|
191
301
|
// replay and verify
|
192
302
|
expect(
|
193
303
|
evaluateExpression(
|
194
|
-
"includes(
|
304
|
+
"includes(referredToPreventionServices, '88cdde2b-753b-48ac-a51a-ae5e1ab24846') && !includes(referredToPreventionServices, '1691AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')",
|
195
305
|
{ value: allFields[1], type: 'field' },
|
196
306
|
allFields,
|
197
307
|
valuesMap,
|
@@ -203,7 +313,7 @@ describe('Common expression runner - evaluateExpression', () => {
|
|
203
313
|
it('should support session mode as a runtime', () => {
|
204
314
|
expect(
|
205
315
|
evaluateExpression(
|
206
|
-
"mode == 'enter' && isEmpty(
|
316
|
+
"mode == 'enter' && isEmpty(htsProviderRemarks)",
|
207
317
|
{ value: allFields[2], type: 'field' },
|
208
318
|
allFields,
|
209
319
|
valuesMap,
|
@@ -222,7 +332,7 @@ describe('Common expression runner - evaluateExpression', () => {
|
|
222
332
|
// replay
|
223
333
|
expect(
|
224
334
|
evaluateExpression(
|
225
|
-
"!includes(
|
335
|
+
"!includes(referredToPreventionServices, '88cdde2b-753b-48ac-a51a-ae5e1ab24846') && isEmpty(htsProviderRemarks)",
|
226
336
|
{ value: allFields[4], type: 'field' },
|
227
337
|
allFields,
|
228
338
|
valuesMap,
|
@@ -251,26 +361,3 @@ describe('Common expression runner - evaluateExpression', () => {
|
|
251
361
|
expect(result).toEqual(5);
|
252
362
|
});
|
253
363
|
});
|
254
|
-
|
255
|
-
describe('Common expression runner - checkReferenceToResolvedFragment', () => {
|
256
|
-
it('should extract resolved fragment and chained reference when given a valid input', () => {
|
257
|
-
const token = 'resolve(api.fetchSomeValue("arg1", "arg2")).someOtherRef';
|
258
|
-
const expected = ['resolve(api.fetchSomeValue("arg1", "arg2"))', '.someOtherRef'];
|
259
|
-
const result = checkReferenceToResolvedFragment(token);
|
260
|
-
expect(result).toEqual(expected);
|
261
|
-
});
|
262
|
-
|
263
|
-
it('should extract only resolved fragment when there is no chained reference', () => {
|
264
|
-
const token = 'resolve(AnotherFragment)';
|
265
|
-
const expected = ['resolve(AnotherFragment)', ''];
|
266
|
-
const result = checkReferenceToResolvedFragment(token);
|
267
|
-
expect(result).toEqual(expected);
|
268
|
-
});
|
269
|
-
|
270
|
-
it('should return an empty string for the resolved fragment and chained reference when given an invalid input', () => {
|
271
|
-
const token = 'invalidToken';
|
272
|
-
const expected = ['', ''];
|
273
|
-
const result = checkReferenceToResolvedFragment(token);
|
274
|
-
expect(result).toEqual(expected);
|
275
|
-
});
|
276
|
-
});
|
@@ -1,10 +1,17 @@
|
|
1
1
|
import { getRegisteredExpressionHelpers } from '../registry/registry';
|
2
2
|
import { isEmpty } from 'lodash-es';
|
3
3
|
import { type OpenmrsEncounter, type FormField, type FormPage, type FormSection } from '../types';
|
4
|
-
import { CommonExpressionHelpers } from './common-expression-helpers';
|
5
|
-
import { findAndRegisterReferencedFields, linkReferencedFieldValues, parseExpression } from './expression-parser';
|
4
|
+
import { CommonExpressionHelpers, registerDependency, simpleHash } from './common-expression-helpers';
|
6
5
|
import { HistoricalDataSourceService } from '../datasources/historical-data-source';
|
7
|
-
import {
|
6
|
+
import {
|
7
|
+
compile,
|
8
|
+
type DefaultEvaluateReturnType,
|
9
|
+
evaluateAsType,
|
10
|
+
evaluateAsTypeAsync,
|
11
|
+
extractVariableNames,
|
12
|
+
type VariablesMap,
|
13
|
+
type Visit,
|
14
|
+
} from '@openmrs/esm-framework';
|
8
15
|
|
9
16
|
export interface FormNode {
|
10
17
|
value: FormPage | FormSection | FormField;
|
@@ -19,7 +26,21 @@ export interface ExpressionContext {
|
|
19
26
|
visit?: Visit;
|
20
27
|
}
|
21
28
|
|
22
|
-
export
|
29
|
+
export type EvaluateReturnType = DefaultEvaluateReturnType | Record<string, any>;
|
30
|
+
|
31
|
+
export const astCache = new Map();
|
32
|
+
|
33
|
+
function typePredicate(result: unknown): result is EvaluateReturnType {
|
34
|
+
return (
|
35
|
+
typeof result === 'string' ||
|
36
|
+
typeof result === 'number' ||
|
37
|
+
typeof result === 'boolean' ||
|
38
|
+
typeof result === 'undefined' ||
|
39
|
+
typeof result === 'object' || // Support for arbitrary objects
|
40
|
+
result === null ||
|
41
|
+
result === undefined
|
42
|
+
);
|
43
|
+
}
|
23
44
|
|
24
45
|
export function evaluateExpression(
|
25
46
|
expression: string,
|
@@ -31,54 +52,12 @@ export function evaluateExpression(
|
|
31
52
|
if (!expression?.trim()) {
|
32
53
|
return null;
|
33
54
|
}
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
// register dependencies
|
38
|
-
findAndRegisterReferencedFields(node, parts, fields);
|
39
|
-
// setup function scope
|
40
|
-
let { myValue, patient } = context;
|
41
|
-
const { sex, age } = patient && 'sex' in patient && 'age' in patient ? patient : { sex: undefined, age: undefined };
|
42
|
-
|
43
|
-
if (node.type === 'field' && myValue === undefined && node.value) {
|
44
|
-
myValue = fieldValues[node.value['id']];
|
45
|
-
}
|
46
|
-
|
47
|
-
const HD = new HistoricalDataSourceService();
|
48
|
-
|
49
|
-
HD.putObject('prevEnc', {
|
50
|
-
value: context.previousEncounter || { obs: [] },
|
51
|
-
getValue(concept) {
|
52
|
-
return this.value.obs.find((obs) => obs.concept.uuid == concept);
|
53
|
-
},
|
54
|
-
});
|
55
|
-
|
56
|
-
const visitType = context.visit?.visitType || { uuid: '' };
|
57
|
-
const visitTypeUuid = visitType.uuid ?? '';
|
58
|
-
|
59
|
-
const _ = {
|
60
|
-
isEmpty,
|
61
|
-
};
|
62
|
-
|
63
|
-
const expressionContext = {
|
64
|
-
...new CommonExpressionHelpers(node, patient, fields, fieldValues, allFieldsKeys),
|
65
|
-
...getRegisteredExpressionHelpers(),
|
66
|
-
...context,
|
67
|
-
fieldValues,
|
68
|
-
patient,
|
69
|
-
myValue,
|
70
|
-
sex,
|
71
|
-
age,
|
72
|
-
HD,
|
73
|
-
visitType,
|
74
|
-
visitTypeUuid,
|
75
|
-
_,
|
76
|
-
};
|
77
|
-
|
78
|
-
expression = linkReferencedFieldValues(fields, fieldValues, parts);
|
55
|
+
const compiledExpression = getExpressionAst(expression);
|
56
|
+
// track dependencies
|
57
|
+
trackFieldDependencies(compiledExpression, node, fields);
|
79
58
|
|
80
59
|
try {
|
81
|
-
return
|
60
|
+
return evaluateAsType(compiledExpression, getEvaluationContext(node, fields, fieldValues, context), typePredicate);
|
82
61
|
} catch (error) {
|
83
62
|
console.error(`Error: ${error} \n\n failing expression: ${expression}`);
|
84
63
|
}
|
@@ -95,25 +74,35 @@ export async function evaluateAsyncExpression(
|
|
95
74
|
if (!expression?.trim()) {
|
96
75
|
return null;
|
97
76
|
}
|
77
|
+
const compiledExpression = getExpressionAst(expression);
|
78
|
+
// track dependencies
|
79
|
+
trackFieldDependencies(compiledExpression, node, fields);
|
80
|
+
try {
|
81
|
+
return evaluateAsTypeAsync(
|
82
|
+
compiledExpression,
|
83
|
+
getEvaluationContext(node, fields, fieldValues, context),
|
84
|
+
typePredicate,
|
85
|
+
);
|
86
|
+
} catch (error) {
|
87
|
+
console.error(`Error: ${error} \n\n failing expression: ${expression}`);
|
88
|
+
}
|
89
|
+
return null;
|
90
|
+
}
|
98
91
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
// register dependencies
|
106
|
-
findAndRegisterReferencedFields(node, parts, fields);
|
107
|
-
|
108
|
-
// setup function scope
|
92
|
+
function getEvaluationContext(
|
93
|
+
node: FormNode,
|
94
|
+
formFields: FormField[],
|
95
|
+
fieldValues: Record<string, any>,
|
96
|
+
context: ExpressionContext,
|
97
|
+
): VariablesMap {
|
109
98
|
let { myValue, patient } = context;
|
110
|
-
const { sex, age } = patient
|
111
|
-
|
99
|
+
const { sex, age } = patient ?? {};
|
100
|
+
|
101
|
+
if (node.type === 'field' && myValue === undefined && node.value) {
|
112
102
|
myValue = fieldValues[node.value['id']];
|
113
103
|
}
|
114
104
|
|
115
105
|
const HD = new HistoricalDataSourceService();
|
116
|
-
|
117
106
|
HD.putObject('prevEnc', {
|
118
107
|
value: context.previousEncounter || { obs: [] },
|
119
108
|
getValue(concept) {
|
@@ -121,99 +110,60 @@ export async function evaluateAsyncExpression(
|
|
121
110
|
},
|
122
111
|
});
|
123
112
|
|
113
|
+
const visitType = context.visit?.visitType || { uuid: '' };
|
114
|
+
const visitTypeUuid = visitType.uuid ?? '';
|
115
|
+
|
124
116
|
const _ = {
|
125
117
|
isEmpty,
|
126
118
|
};
|
127
119
|
|
128
|
-
|
129
|
-
...new CommonExpressionHelpers(node, patient,
|
120
|
+
return {
|
121
|
+
...new CommonExpressionHelpers(node, patient, formFields, fieldValues),
|
130
122
|
...getRegisteredExpressionHelpers(),
|
131
123
|
...context,
|
132
|
-
fieldValues,
|
124
|
+
...fieldValues,
|
133
125
|
patient,
|
134
126
|
myValue,
|
135
127
|
sex,
|
136
128
|
age,
|
137
|
-
temporaryObjectsMap: {},
|
138
129
|
HD,
|
139
130
|
visitType,
|
140
131
|
visitTypeUuid,
|
141
132
|
_,
|
142
133
|
};
|
143
|
-
|
144
|
-
expression = linkReferencedFieldValues(fields, fieldValues, parts);
|
145
|
-
|
146
|
-
// parts with resolve-able field references
|
147
|
-
parts = parseExpression(expression);
|
148
|
-
const lazyFragments = [];
|
149
|
-
parts.forEach((part, index) => {
|
150
|
-
if (index % 2 == 0) {
|
151
|
-
if (part.startsWith('resolve(')) {
|
152
|
-
const [refinedSubExpression] = checkReferenceToResolvedFragment(part);
|
153
|
-
lazyFragments.push({ expression: refinedSubExpression, index });
|
154
|
-
}
|
155
|
-
}
|
156
|
-
});
|
157
|
-
|
158
|
-
const temporaryObjectsMap = {};
|
159
|
-
// resolve lazy fragments
|
160
|
-
const fragments = await Promise.all(lazyFragments.map(({ expression }) => evaluate(expression, expressionContext)));
|
161
|
-
lazyFragments.forEach((fragment, index) => {
|
162
|
-
if (typeof fragments[index] == 'object') {
|
163
|
-
const objectKey = `obj_${index}`;
|
164
|
-
temporaryObjectsMap[objectKey] = fragments[index];
|
165
|
-
expression = expression.replace(fragment.expression, `temporaryObjectsMap.${objectKey}`);
|
166
|
-
} else {
|
167
|
-
expression = expression.replace(
|
168
|
-
fragment.expression,
|
169
|
-
typeof fragments[index] == 'string' ? `'${fragments[index]}'` : fragments[index],
|
170
|
-
);
|
171
|
-
}
|
172
|
-
});
|
173
|
-
|
174
|
-
expressionContext.temporaryObjectsMap = temporaryObjectsMap;
|
175
|
-
|
176
|
-
try {
|
177
|
-
return evaluate(expression, expressionContext);
|
178
|
-
} catch (error) {
|
179
|
-
console.error(`Error: ${error} \n\n failing expression: ${expression}`);
|
180
|
-
}
|
181
|
-
return null;
|
182
134
|
}
|
183
135
|
|
184
136
|
/**
|
185
|
-
*
|
186
|
-
*
|
187
|
-
* @
|
188
|
-
* @returns An array containing the resolved fragment and the remaining chained reference.
|
137
|
+
* Compiles an expression into an abstract syntax tree (AST) and caches the result.
|
138
|
+
* @param expression - The expression to compile.
|
139
|
+
* @returns The abstract syntax tree (AST) of the compiled expression.
|
189
140
|
*/
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
141
|
+
function getExpressionAst(expression: string): ReturnType<typeof compile> {
|
142
|
+
const hash = simpleHash(expression);
|
143
|
+
if (astCache.has(hash)) {
|
144
|
+
return astCache.get(hash);
|
145
|
+
}
|
146
|
+
const ast = compile(expression);
|
147
|
+
astCache.set(hash, ast);
|
148
|
+
return ast;
|
196
149
|
}
|
197
150
|
|
198
151
|
/**
|
199
|
-
*
|
200
|
-
*
|
201
|
-
*
|
202
|
-
*
|
203
|
-
* ```js
|
204
|
-
* evaluate("myNum + 2", { myNum: 5 }); // 7
|
205
|
-
* ```
|
206
|
-
*
|
207
|
-
* Note that references to variables not included in the `expressionContext` will result at
|
208
|
-
* `undefined` during evaluation.
|
209
|
-
*
|
210
|
-
* @param expression A JS expression to execute
|
211
|
-
* @param expressionContext A JS object consisting of the names to make available in the scope
|
212
|
-
* the expression is executed in.
|
152
|
+
* Extracts all referenced fields in the expression and registers them as dependencies.
|
153
|
+
* @param expression - The expression to track dependencies for.
|
154
|
+
* @param fieldNode - The node representing the field.
|
155
|
+
* @param allFields - The list of all fields in the form.
|
213
156
|
*/
|
214
|
-
function
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
157
|
+
export function trackFieldDependencies(
|
158
|
+
expression: ReturnType<typeof compile>,
|
159
|
+
fieldNode: FormNode,
|
160
|
+
allFields: FormField[],
|
161
|
+
) {
|
162
|
+
const variables = extractVariableNames(expression);
|
163
|
+
for (const variable of variables) {
|
164
|
+
const field = allFields.find((field) => field.id === variable);
|
165
|
+
if (field) {
|
166
|
+
registerDependency(fieldNode, field);
|
167
|
+
}
|
168
|
+
}
|
219
169
|
}
|