@openmrs/esm-form-engine-lib 2.1.0-pre.1510 → 2.1.0-pre.1513

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.
@@ -11,7 +11,6 @@ import { type FormNode } from './expression-runner';
11
11
  import { isEmpty as isValueEmpty } from '../validators/form-validator';
12
12
  import * as apiFunctions from '../api';
13
13
  import { getZRefByGenderAndAge } from './zscore-service';
14
- import { ConceptFalse, ConceptTrue } from '../constants';
15
14
  import { formatDate, parseDate } from '@openmrs/esm-framework';
16
15
 
17
16
  export class CommonExpressionHelpers {
@@ -19,20 +18,12 @@ export class CommonExpressionHelpers {
19
18
  patient: any = null;
20
19
  allFields: FormField[] = [];
21
20
  allFieldValues: Record<string, any> = {};
22
- allFieldsKeys: string[] = [];
23
21
  api = apiFunctions;
24
22
  isEmpty = isValueEmpty;
25
23
 
26
- constructor(
27
- node: FormNode,
28
- patient: any,
29
- allFields: FormField[],
30
- allFieldValues: Record<string, any>,
31
- allFieldsKeys: string[],
32
- ) {
24
+ constructor(node: FormNode, patient: any, allFields: FormField[], allFieldValues: Record<string, any>) {
33
25
  this.allFields = allFields;
34
26
  this.allFieldValues = allFieldValues;
35
- this.allFieldsKeys = allFieldsKeys;
36
27
  this.node = node;
37
28
  this.patient = patient;
38
29
  }
@@ -88,10 +79,12 @@ export class CommonExpressionHelpers {
88
79
  };
89
80
 
90
81
  useFieldValue = (questionId: string) => {
91
- if (this.allFieldsKeys.includes(questionId)) {
92
- return this.allFieldValues[questionId];
82
+ const targetField = this.allFields.find((field) => field.id === questionId);
83
+ if (targetField) {
84
+ // track field dependency
85
+ registerDependency(this.node, targetField);
93
86
  }
94
- return null;
87
+ return this.allFieldValues[questionId] ?? null;
95
88
  };
96
89
 
97
90
  doesNotMatchExpression = (regexString: string, val: string | null | undefined): boolean => {
@@ -473,6 +466,21 @@ export class CommonExpressionHelpers {
473
466
  };
474
467
  }
475
468
 
469
+ /**
470
+ * Simple hash function to generate a unique identifier for a string.
471
+ * @param str - The string to hash.
472
+ * @returns A unique identifier for the string.
473
+ */
474
+ export function simpleHash(str: string) {
475
+ let hash = 0;
476
+ for (let i = 0; i < str.length; i++) {
477
+ const char = str.charCodeAt(i);
478
+ hash = (hash << 5) - hash + char;
479
+ hash |= 0;
480
+ }
481
+ return hash;
482
+ }
483
+
476
484
  export function registerDependency(node: FormNode, determinant: FormField) {
477
485
  if (!node || !determinant) {
478
486
  return;
@@ -497,16 +505,3 @@ export function registerDependency(node: FormNode, determinant: FormField) {
497
505
  determinant.fieldDependents.add(node.value['id']);
498
506
  }
499
507
  }
500
-
501
- export const booleanConceptToBoolean = (booleanConceptRepresentation): boolean => {
502
- const { value } = booleanConceptRepresentation;
503
- if (!booleanConceptRepresentation) {
504
- throw new Error('booleanConceptRepresentation cannot be a null value');
505
- }
506
- if (value == ConceptTrue) {
507
- return true;
508
- }
509
- if (value == ConceptFalse) {
510
- return false;
511
- }
512
- };
@@ -1,7 +1,6 @@
1
1
  import { registerExpressionHelper } from '..';
2
2
  import { type FormField } from '../types';
3
- import { CommonExpressionHelpers } from './common-expression-helpers';
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
- describe('Common expression runner - evaluateExpression', () => {
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 evaluate basic boolean strings', () => {
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('false', { value: allFields[0], type: 'field' }, allFields, valuesMap, context),
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 two dimension expressions', () => {
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 multiple dimession expressions', () => {
144
- // replay and verify
145
- expect(
146
- evaluateExpression(
147
- "linkedToCare == 'cf82933b-3f3f-45e7-a5ab-5d31aaee3da3' && htsProviderRemarks !== '' && bodyTemperature >= 39",
148
- { value: allFields[1], type: 'field' },
149
- allFields,
150
- valuesMap,
151
- context,
152
- ),
153
- ).toBeFalsy();
154
- // provide some values
155
- valuesMap['linkedToCare'] = 'cf82933b-3f3f-45e7-a5ab-5d31aaee3da3';
156
- valuesMap['htsProviderRemarks'] = 'Some test remarks...';
157
- valuesMap['bodyTemperature'] = 40;
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 isEmpty(value) runtime helper function', () => {
274
+ it('should support async expressions', async () => {
171
275
  // setup
172
- valuesMap['linkedToCare'] = 'cf82933b-3f3f-45e7-a5ab-5d31aaee3da3';
173
- // replay and verify
174
- expect(
175
- evaluateExpression(
176
- "!isEmpty('linkedToCare') && isEmpty('htsProviderRemarks')",
177
- { value: allFields[1], type: 'field' },
178
- allFields,
179
- valuesMap,
180
- context,
181
- ),
182
- ).toBeTruthy();
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('referredToPreventionServices', '88cdde2b-753b-48ac-a51a-ae5e1ab24846') && !includes('referredToPreventionServices', '1691AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')",
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('htsProviderRemarks')",
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('referredToPreventionServices', '88cdde2b-753b-48ac-a51a-ae5e1ab24846') && isEmpty('htsProviderRemarks')",
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 { type Visit } from '@openmrs/esm-framework';
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 const HD = new HistoricalDataSourceService();
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
- const allFieldsKeys = fields.map((f) => f.id);
36
- const parts = parseExpression(expression.trim());
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 evaluate(expression, expressionContext);
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
- const allFieldsKeys = fields.map((f) => f.id);
100
- let parts = parseExpression(expression.trim());
101
-
102
- const visitType = context.visit?.visitType || { uuid: '' };
103
- const visitTypeUuid = visitType.uuid ?? '';
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 && 'sex' in patient && 'age' in patient ? patient : { sex: undefined, age: undefined };
111
- if (node.type === 'field' && myValue === undefined) {
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
- const expressionContext = {
129
- ...new CommonExpressionHelpers(node, patient, fields, fieldValues, allFieldsKeys),
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
- * Checks if the given token contains a reference to a resolved fragment
186
- * and returns the fragment and the remaining chained reference.
187
- * @param token - The token to check.
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
- export function checkReferenceToResolvedFragment(token: string) {
191
- // Match the substring that starts with the keyword "resolve" and continues until
192
- // the closing parenthesis of the inner function call.
193
- const match = token.match(/resolve\((.*)\)/) || [];
194
- const chainedRef = match.length ? token.substring(token.indexOf(match[0]) + match[0].length) : '';
195
- return [match[0] || '', chainedRef];
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
- * A slightly safer version of the built-in eval()
200
- *
201
- * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
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 evaluate(expression: string, expressionContext?: Record<string, any>) {
215
- return Function(...Object.keys(expressionContext), `"use strict"; return (${expression})`).call(
216
- undefined,
217
- ...Object.values(expressionContext),
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
  }