@openmrs/esm-form-engine-lib 2.1.0-pre.1362
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/.editorconfig +12 -0
- package/.eslintignore +2 -0
- package/.eslintrc +58 -0
- package/.husky/pre-commit +6 -0
- package/.husky/pre-push +6 -0
- package/.prettierignore +4 -0
- package/LICENSE.txt +401 -0
- package/README.md +136 -0
- package/__mocks__/concepts.mock.json +140 -0
- package/__mocks__/forms/afe-forms/component_art.json +38 -0
- package/__mocks__/forms/afe-forms/component_preclinic-review.json +38 -0
- package/__mocks__/forms/afe-forms/demo_hts-form.json +62 -0
- package/__mocks__/forms/afe-forms/form-component.json +38 -0
- package/__mocks__/forms/afe-forms/mini-form.json +31 -0
- package/__mocks__/forms/afe-forms/nested-form1.json +38 -0
- package/__mocks__/forms/afe-forms/nested-form2.json +38 -0
- package/__mocks__/forms/afe-forms/test-orders.json +72 -0
- package/__mocks__/forms/afe-forms/test-schema-transformer-form.json +88 -0
- package/__mocks__/forms/rfe-forms/age-validation-form.json +58 -0
- package/__mocks__/forms/rfe-forms/bmi-test-form.json +69 -0
- package/__mocks__/forms/rfe-forms/bsa-test-form.json +69 -0
- package/__mocks__/forms/rfe-forms/component_art.json +1705 -0
- package/__mocks__/forms/rfe-forms/component_preclinic-review.json +480 -0
- package/__mocks__/forms/rfe-forms/conditional-answered-form.json +97 -0
- package/__mocks__/forms/rfe-forms/conditional-required-form.json +281 -0
- package/__mocks__/forms/rfe-forms/demo_hts-form.json +346 -0
- package/__mocks__/forms/rfe-forms/edd-test-form.json +88 -0
- package/__mocks__/forms/rfe-forms/external_data_source_form.json +43 -0
- package/__mocks__/forms/rfe-forms/filter-answer-options-test-form.json +87 -0
- package/__mocks__/forms/rfe-forms/form-component.json +43 -0
- package/__mocks__/forms/rfe-forms/forms-loader.test.schema.ts +209 -0
- package/__mocks__/forms/rfe-forms/historical-expressions-form.json +170 -0
- package/__mocks__/forms/rfe-forms/labour_and_delivery_test_form.json +374 -0
- package/__mocks__/forms/rfe-forms/mini-form.json +29 -0
- package/__mocks__/forms/rfe-forms/mockHistoricalvisitsEncounter.json +89 -0
- package/__mocks__/forms/rfe-forms/months-on-art-form.json +90 -0
- package/__mocks__/forms/rfe-forms/multi-select-form.json +86 -0
- package/__mocks__/forms/rfe-forms/nested-form1.json +43 -0
- package/__mocks__/forms/rfe-forms/nested-form2.json +43 -0
- package/__mocks__/forms/rfe-forms/next-visit-test-form.json +78 -0
- package/__mocks__/forms/rfe-forms/obs-group-test_form.json +137 -0
- package/__mocks__/forms/rfe-forms/obs-list-data.ts +37 -0
- package/__mocks__/forms/rfe-forms/post-submission-test-form.json +116 -0
- package/__mocks__/forms/rfe-forms/reference-by-mapping-form.json +54 -0
- package/__mocks__/forms/rfe-forms/required-form.json +50 -0
- package/__mocks__/forms/rfe-forms/sample_fields.json +36 -0
- package/__mocks__/forms/rfe-forms/test-enrolment-form.json +241 -0
- package/__mocks__/forms/rfe-forms/treatment-end-date-test-form.json +121 -0
- package/__mocks__/forms/rfe-forms/viral-load-status-form.json +75 -0
- package/__mocks__/forms/rfe-forms/zscore-bmi-for-age-form.json +79 -0
- package/__mocks__/forms/rfe-forms/zscore-height-for-age-form.json +79 -0
- package/__mocks__/forms/rfe-forms/zscore-weight-height-form.json +77 -0
- package/__mocks__/packages/hiv/forms/hts_poc/1.0.json +8 -0
- package/__mocks__/packages/hiv/forms/hts_poc/1.1.json +91 -0
- package/__mocks__/packages/test-forms-registry.ts +12 -0
- package/__mocks__/patient.mock.ts +173 -0
- package/__mocks__/react-i18next.js +49 -0
- package/__mocks__/react-markdown.tsx +5 -0
- package/__mocks__/session.mock.ts +117 -0
- package/__mocks__/single-spa-react.js +11 -0
- package/__mocks__/use-initial-values/encounter.mock.json +963 -0
- package/__mocks__/use-initial-values/patient.mock.json +73 -0
- package/__mocks__/visit.mock.ts +19 -0
- package/dist/openmrs-esm-form-engine-lib.js +1 -0
- package/jest.config.js +30 -0
- package/package.json +104 -0
- package/prettier.config.js +8 -0
- package/readme/form-engine.jpeg +0 -0
- package/src/adapters/control-adapter.ts +29 -0
- package/src/adapters/encounter-datetime-adapter.ts +38 -0
- package/src/adapters/encounter-location-adapter.ts +39 -0
- package/src/adapters/encounter-provider-adapter.ts +48 -0
- package/src/adapters/encounter-role-adapter.ts +54 -0
- package/src/adapters/inline-date-adapter.ts +58 -0
- package/src/adapters/obs-adapter.ts +280 -0
- package/src/adapters/obs-comment-adapter.ts +60 -0
- package/src/adapters/orders-adapter.ts +75 -0
- package/src/adapters/patient-identifier-adapter.ts +40 -0
- package/src/adapters/program-state-adapter.ts +52 -0
- package/src/api/index.ts +178 -0
- package/src/components/error/error-modal.component.tsx +37 -0
- package/src/components/error/error.scss +4 -0
- package/src/components/extension/extension-parcel.component.tsx +32 -0
- package/src/components/field-label/field-label.component.tsx +32 -0
- package/src/components/field-label/field-label.scss +11 -0
- package/src/components/group/obs-group.component.tsx +29 -0
- package/src/components/group/obs-group.scss +12 -0
- package/src/components/inputs/content-switcher/content-switcher.component.tsx +71 -0
- package/src/components/inputs/content-switcher/content-switcher.scss +55 -0
- package/src/components/inputs/date/date.component.tsx +149 -0
- package/src/components/inputs/date/date.scss +36 -0
- package/src/components/inputs/file/camera/camera.component.tsx +34 -0
- package/src/components/inputs/file/camera/camera.scss +3 -0
- package/src/components/inputs/file/file.component.tsx +159 -0
- package/src/components/inputs/file/file.scss +101 -0
- package/src/components/inputs/fixed-value/fixed-value.component.tsx +19 -0
- package/src/components/inputs/markdown/markdown-wrapper.component.tsx +14 -0
- package/src/components/inputs/markdown/markdown.component.tsx +8 -0
- package/src/components/inputs/multi-select/multi-select.component.tsx +151 -0
- package/src/components/inputs/multi-select/multi-select.scss +25 -0
- package/src/components/inputs/multi-select/multi-select.test.tsx +90 -0
- package/src/components/inputs/number/number.component.tsx +69 -0
- package/src/components/inputs/number/number.scss +15 -0
- package/src/components/inputs/radio/radio.component.tsx +79 -0
- package/src/components/inputs/radio/radio.scss +36 -0
- package/src/components/inputs/select/dropdown.component.tsx +73 -0
- package/src/components/inputs/select/dropdown.scss +11 -0
- package/src/components/inputs/select/dropdown.test.tsx +120 -0
- package/src/components/inputs/text/text.component.tsx +65 -0
- package/src/components/inputs/text/text.scss +15 -0
- package/src/components/inputs/text/text.test.tsx +104 -0
- package/src/components/inputs/text-area/text-area.component.tsx +63 -0
- package/src/components/inputs/text-area/text-area.scss +11 -0
- package/src/components/inputs/toggle/toggle.component.tsx +66 -0
- package/src/components/inputs/toggle/toggle.scss +12 -0
- package/src/components/inputs/tooltip/tooltip.component.tsx +23 -0
- package/src/components/inputs/tooltip/tooltip.scss +8 -0
- package/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx +187 -0
- package/src/components/inputs/ui-select-extended/ui-select-extended.scss +15 -0
- package/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx +211 -0
- package/src/components/inputs/unspecified/unspecified.component.tsx +74 -0
- package/src/components/inputs/unspecified/unspecified.scss +7 -0
- package/src/components/inputs/unspecified/unspecified.test.tsx +95 -0
- package/src/components/inputs/workspace-launcher/workspace-launcher.component.tsx +35 -0
- package/src/components/inputs/workspace-launcher/workspace-launcher.scss +15 -0
- package/src/components/label/label.component.tsx +20 -0
- package/src/components/label/label.scss +11 -0
- package/src/components/loaders/loader.component.tsx +16 -0
- package/src/components/loaders/loader.scss +20 -0
- package/src/components/patient-banner/patient-banner.component.tsx +20 -0
- package/src/components/patient-banner/patient-banner.scss +12 -0
- package/src/components/previous-value-review/previous-value-review.component.tsx +49 -0
- package/src/components/previous-value-review/previous-value-review.scss +36 -0
- package/src/components/processor-factory/form-processor-factory.component.tsx +127 -0
- package/src/components/renderer/custom-hooks-renderer.component.tsx +30 -0
- package/src/components/renderer/field/fieldLogic.ts +214 -0
- package/src/components/renderer/field/form-field-renderer.component.tsx +281 -0
- package/src/components/renderer/field/form-field-renderer.scss +5 -0
- package/src/components/renderer/form/form-renderer.component.tsx +89 -0
- package/src/components/renderer/form/state.ts +54 -0
- package/src/components/renderer/page/page.renderer.component.tsx +50 -0
- package/src/components/renderer/page/page.renderer.scss +36 -0
- package/src/components/renderer/section/section-renderer.component.tsx +21 -0
- package/src/components/renderer/section/section-renderer.scss +19 -0
- package/src/components/repeat/helpers.test.ts +29 -0
- package/src/components/repeat/helpers.ts +68 -0
- package/src/components/repeat/repeat-controls.component.tsx +38 -0
- package/src/components/repeat/repeat-controls.scss +7 -0
- package/src/components/repeat/repeat.component.tsx +201 -0
- package/src/components/repeat/repeat.scss +30 -0
- package/src/components/repeat/repeat.test.ts +29 -0
- package/src/components/sidebar/sidebar.component.tsx +134 -0
- package/src/components/sidebar/sidebar.scss +121 -0
- package/src/components/value/value.component.tsx +27 -0
- package/src/components/value/value.scss +17 -0
- package/src/components/value/view/field-value-view.component.tsx +33 -0
- package/src/components/value/view/field-value-view.scss +31 -0
- package/src/constants.ts +12 -0
- package/src/datasources/concept-data-source.ts +42 -0
- package/src/datasources/data-source.ts +23 -0
- package/src/datasources/encounter-role-datasource.ts +15 -0
- package/src/datasources/historical-data-source.ts +11 -0
- package/src/datasources/location-data-source.ts +27 -0
- package/src/datasources/provider-datasource.ts +15 -0
- package/src/datasources/select-concept-answers-datasource.ts +15 -0
- package/src/declarations.d.ts +4 -0
- package/src/external-function-context.tsx +8 -0
- package/src/form-context.tsx +42 -0
- package/src/form-engine.component.tsx +178 -0
- package/src/form-engine.scss +140 -0
- package/src/form-engine.test.tsx +817 -0
- package/src/globals.ts +1 -0
- package/src/hooks/useClobData.tsx +21 -0
- package/src/hooks/useConcepts.tsx +55 -0
- package/src/hooks/useDatasourceDependentValue.ts +16 -0
- package/src/hooks/useEncounter.tsx +32 -0
- package/src/hooks/useEncounterRole.tsx +15 -0
- package/src/hooks/useEvaluateFormFieldExpressions.ts +138 -0
- package/src/hooks/useFieldValidationResults.ts +18 -0
- package/src/hooks/useFormCollapse.tsx +36 -0
- package/src/hooks/useFormFieldValidators.ts +22 -0
- package/src/hooks/useFormFieldValueAdapters.ts +24 -0
- package/src/hooks/useFormFields.ts +37 -0
- package/src/hooks/useFormFieldsMeta.ts +48 -0
- package/src/hooks/useFormJson.test.tsx +173 -0
- package/src/hooks/useFormJson.tsx +237 -0
- package/src/hooks/useFormStateHelpers.ts +50 -0
- package/src/hooks/useFormsConfig.tsx +27 -0
- package/src/hooks/useInitialValues.ts +38 -0
- package/src/hooks/usePatientData.tsx +32 -0
- package/src/hooks/usePatientPrograms.ts +32 -0
- package/src/hooks/usePostSubmissionActions.test.tsx +42 -0
- package/src/hooks/usePostSubmissionActions.ts +31 -0
- package/src/hooks/useProcessorDependencies.ts +30 -0
- package/src/hooks/useRestMaxResultsCount.ts +5 -0
- package/src/hooks/useSystemSetting.ts +36 -0
- package/src/hooks/useWorkspaceLayout.ts +29 -0
- package/src/index.ts +12 -0
- package/src/lifecycle.ts +33 -0
- package/src/post-submission-actions/program-enrollment-action.ts +138 -0
- package/src/processors/encounter/encounter-form-processor.ts +337 -0
- package/src/processors/encounter/encounter-processor-helper.ts +320 -0
- package/src/processors/form-processor.ts +41 -0
- package/src/provider/form-factory-helper.ts +100 -0
- package/src/provider/form-factory-provider.tsx +169 -0
- package/src/provider/form-provider.tsx +37 -0
- package/src/registry/inbuilt-components/InbuiltPostSubmissionActions.ts +9 -0
- package/src/registry/inbuilt-components/control-templates.ts +57 -0
- package/src/registry/inbuilt-components/inbuiltControls.ts +99 -0
- package/src/registry/inbuilt-components/inbuiltDataSources.ts +41 -0
- package/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts +64 -0
- package/src/registry/inbuilt-components/inbuiltTransformers.ts +10 -0
- package/src/registry/inbuilt-components/inbuiltValidators.ts +33 -0
- package/src/registry/inbuilt-components/template-component-map.ts +28 -0
- package/src/registry/registry.test.ts +20 -0
- package/src/registry/registry.ts +261 -0
- package/src/routes.json +1 -0
- package/src/setupI18n.ts +16 -0
- package/src/setupTests.ts +5 -0
- package/src/transformers/default-schema-transformer.test.ts +155 -0
- package/src/transformers/default-schema-transformer.ts +239 -0
- package/src/types/domain.ts +129 -0
- package/src/types/index.ts +130 -0
- package/src/types/schema.ts +238 -0
- package/src/typings.d.ts +9 -0
- package/src/utils/boolean-utils.ts +25 -0
- package/src/utils/common-expression-helpers.ts +503 -0
- package/src/utils/common-utils.test.ts +136 -0
- package/src/utils/common-utils.ts +55 -0
- package/src/utils/error-utils.ts +37 -0
- package/src/utils/expression-parser.test.ts +308 -0
- package/src/utils/expression-parser.ts +158 -0
- package/src/utils/expression-runner.test.ts +387 -0
- package/src/utils/expression-runner.ts +219 -0
- package/src/utils/form-helper.test.ts +482 -0
- package/src/utils/form-helper.ts +210 -0
- package/src/utils/form-page-utils.ts +13 -0
- package/src/utils/forms-loader.test.ts +323 -0
- package/src/utils/forms-loader.ts +306 -0
- package/src/utils/post-submission-action-helper.ts +71 -0
- package/src/utils/test-utils.ts +54 -0
- package/src/utils/zscore-service.ts +59 -0
- package/src/validators/conditional-answered-validator.test.ts +61 -0
- package/src/validators/conditional-answered-validator.ts +17 -0
- package/src/validators/date-validator.test.ts +46 -0
- package/src/validators/date-validator.ts +19 -0
- package/src/validators/default-value-validator.test.ts +90 -0
- package/src/validators/default-value-validator.ts +36 -0
- package/src/validators/form-validator.test.ts +188 -0
- package/src/validators/form-validator.ts +95 -0
- package/src/validators/js-expression-validator.test.ts +118 -0
- package/src/validators/js-expression-validator.ts +44 -0
- package/src/validators/schema.ts +34 -0
- package/src/zscore/bfa_boys_5_above.json +2522 -0
- package/src/zscore/bfa_girls_5_above.json +2522 -0
- package/src/zscore/hfa_boys_5_above.json +2186 -0
- package/src/zscore/hfa_boys_below5.json +22286 -0
- package/src/zscore/hfa_girls_5_above.json +2186 -0
- package/src/zscore/hfa_girls_below5.json +22286 -0
- package/src/zscore/wfl_boys_below5.json +7814 -0
- package/src/zscore/wfl_girls_below5.json +7814 -0
- package/src/zscore-tests/bmi-age.test.tsx +88 -0
- package/src/zscore-tests/height-age.test.tsx +96 -0
- package/src/zscore-tests/weight-height.test.tsx +87 -0
- package/tools/i18next-parser.config.js +93 -0
- package/translations/en.json +47 -0
- package/translations/es.json +38 -0
- package/translations/fr.json +38 -0
- package/translations/km.json +38 -0
- package/tsconfig.json +19 -0
- package/turbo.json +15 -0
- package/webpack.config.js +1 -0
@@ -0,0 +1,387 @@
|
|
1
|
+
import { registerExpressionHelper } from '..';
|
2
|
+
import { type FormField } from '../types';
|
3
|
+
import { CommonExpressionHelpers } from './common-expression-helpers';
|
4
|
+
import { checkReferenceToResolvedFragment, evaluateExpression, type ExpressionContext } from './expression-runner';
|
5
|
+
|
6
|
+
export const testFields: Array<FormField> = [
|
7
|
+
{
|
8
|
+
label: 'Was the client linked to care and treatment in this facility?',
|
9
|
+
type: 'obs',
|
10
|
+
questionOptions: {
|
11
|
+
rendering: 'radio',
|
12
|
+
concept: 'e8e8fe71-adbb-48e7-b531-589985094d30',
|
13
|
+
answers: [
|
14
|
+
{
|
15
|
+
concept: 'cf82933b-3f3f-45e7-a5ab-5d31aaee3da3',
|
16
|
+
label: 'Yes',
|
17
|
+
},
|
18
|
+
{
|
19
|
+
concept: '488b58ff-64f5-4f8a-8979-fa79940b1594',
|
20
|
+
label: 'No',
|
21
|
+
},
|
22
|
+
],
|
23
|
+
},
|
24
|
+
id: 'linkedToCare',
|
25
|
+
},
|
26
|
+
{
|
27
|
+
label: 'What Identification Number was issued to the client?',
|
28
|
+
type: 'obs',
|
29
|
+
questionOptions: {
|
30
|
+
rendering: 'text',
|
31
|
+
concept: '162576AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
32
|
+
},
|
33
|
+
id: 'patientIdentificationNumber',
|
34
|
+
},
|
35
|
+
{
|
36
|
+
label: 'Which of the following prevention services was the client referred to?',
|
37
|
+
type: 'obs',
|
38
|
+
questionOptions: {
|
39
|
+
rendering: 'checkbox',
|
40
|
+
concept: '5f394708-ca7d-4558-8d23-a73de181b02d',
|
41
|
+
answers: [
|
42
|
+
{
|
43
|
+
concept: '88cdde2b-753b-48ac-a51a-ae5e1ab24846',
|
44
|
+
label: 'Pre Exposure Prophylaxis (PEP)',
|
45
|
+
},
|
46
|
+
{
|
47
|
+
concept: '46da10c7-49e3-45e5-8e82-7c529d52a1a8',
|
48
|
+
label: 'STI Testing and Treatment',
|
49
|
+
},
|
50
|
+
{
|
51
|
+
concept: '1691AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
52
|
+
label: 'Post-exposure prophylaxis',
|
53
|
+
},
|
54
|
+
{
|
55
|
+
concept: '162223AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
56
|
+
label: 'Voluntary male circumcision clinic',
|
57
|
+
},
|
58
|
+
],
|
59
|
+
},
|
60
|
+
id: 'referredToPreventionServices',
|
61
|
+
},
|
62
|
+
{
|
63
|
+
label: 'What were the HTS provider’s remarks?',
|
64
|
+
type: 'obs',
|
65
|
+
questionOptions: {
|
66
|
+
rendering: 'textarea',
|
67
|
+
concept: '437d1e25-e7ab-481c-aabc-01f21c6cdef1',
|
68
|
+
},
|
69
|
+
id: 'htsProviderRemarks',
|
70
|
+
},
|
71
|
+
{
|
72
|
+
label: 'Body Temperature',
|
73
|
+
type: 'obs',
|
74
|
+
questionOptions: {
|
75
|
+
rendering: 'number',
|
76
|
+
concept: '537d1e25-e7av-481c-aabc-01f21c6cdefo',
|
77
|
+
},
|
78
|
+
id: 'bodyTemperature',
|
79
|
+
},
|
80
|
+
];
|
81
|
+
|
82
|
+
describe('Common expression runner - evaluateExpression', () => {
|
83
|
+
const context: ExpressionContext = { mode: 'enter', patient: {} };
|
84
|
+
const allFields = JSON.parse(JSON.stringify(testFields));
|
85
|
+
let valuesMap = {
|
86
|
+
linkedToCare: '',
|
87
|
+
patientIdentificationNumber: '',
|
88
|
+
htsProviderRemarks: '',
|
89
|
+
referredToPreventionServices: [],
|
90
|
+
bodyTemperature: 0,
|
91
|
+
};
|
92
|
+
|
93
|
+
afterEach(() => {
|
94
|
+
// teardown
|
95
|
+
valuesMap = {
|
96
|
+
linkedToCare: '',
|
97
|
+
patientIdentificationNumber: '',
|
98
|
+
htsProviderRemarks: '',
|
99
|
+
referredToPreventionServices: [],
|
100
|
+
bodyTemperature: 0,
|
101
|
+
};
|
102
|
+
allFields.forEach((field) => {
|
103
|
+
field.fieldDependents = undefined;
|
104
|
+
});
|
105
|
+
});
|
106
|
+
|
107
|
+
it('should evaluate basic boolean strings', () => {
|
108
|
+
// replay and verify
|
109
|
+
expect(
|
110
|
+
evaluateExpression('true', { value: allFields[0], type: 'field' }, allFields, valuesMap, context),
|
111
|
+
).toBeTruthy();
|
112
|
+
// replay and verify
|
113
|
+
expect(
|
114
|
+
evaluateExpression('false', { value: allFields[0], type: 'field' }, allFields, valuesMap, context),
|
115
|
+
).toBeFalsy();
|
116
|
+
});
|
117
|
+
|
118
|
+
it('should support two dimension expressions', () => {
|
119
|
+
// replay and verify
|
120
|
+
expect(
|
121
|
+
evaluateExpression(
|
122
|
+
"linkedToCare == '488b58ff-64f5-4f8a-8979-fa79940b1594'",
|
123
|
+
{ value: allFields[1], type: 'field' },
|
124
|
+
allFields,
|
125
|
+
valuesMap,
|
126
|
+
context,
|
127
|
+
),
|
128
|
+
).toBeFalsy();
|
129
|
+
// provide some values
|
130
|
+
valuesMap['linkedToCare'] = '488b58ff-64f5-4f8a-8979-fa79940b1594';
|
131
|
+
// replay and verify
|
132
|
+
expect(
|
133
|
+
evaluateExpression(
|
134
|
+
"linkedToCare == '488b58ff-64f5-4f8a-8979-fa79940b1594'",
|
135
|
+
{ value: allFields[1], type: 'field' },
|
136
|
+
allFields,
|
137
|
+
valuesMap,
|
138
|
+
context,
|
139
|
+
),
|
140
|
+
).toBeTruthy();
|
141
|
+
});
|
142
|
+
|
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();
|
168
|
+
});
|
169
|
+
|
170
|
+
it('should support isEmpty(value) runtime helper function', () => {
|
171
|
+
// 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();
|
183
|
+
});
|
184
|
+
|
185
|
+
it('should support includes(question, value) runtime helper function', () => {
|
186
|
+
// setup
|
187
|
+
valuesMap['referredToPreventionServices'] = [
|
188
|
+
'88cdde2b-753b-48ac-a51a-ae5e1ab24846',
|
189
|
+
'162223AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
190
|
+
];
|
191
|
+
// replay and verify
|
192
|
+
expect(
|
193
|
+
evaluateExpression(
|
194
|
+
"includes('referredToPreventionServices', '88cdde2b-753b-48ac-a51a-ae5e1ab24846') && !includes('referredToPreventionServices', '1691AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')",
|
195
|
+
{ value: allFields[1], type: 'field' },
|
196
|
+
allFields,
|
197
|
+
valuesMap,
|
198
|
+
context,
|
199
|
+
),
|
200
|
+
).toBeTruthy();
|
201
|
+
});
|
202
|
+
|
203
|
+
it('should support session mode as a runtime', () => {
|
204
|
+
expect(
|
205
|
+
evaluateExpression(
|
206
|
+
"mode == 'enter' && isEmpty('htsProviderRemarks')",
|
207
|
+
{ value: allFields[2], type: 'field' },
|
208
|
+
allFields,
|
209
|
+
valuesMap,
|
210
|
+
context,
|
211
|
+
),
|
212
|
+
).toBeTruthy();
|
213
|
+
});
|
214
|
+
|
215
|
+
it("should register dependency of the current node to it's determinant", () => {
|
216
|
+
// setup
|
217
|
+
const referredToPreventionServices = allFields[2];
|
218
|
+
const htsProviderRemarks = allFields[3];
|
219
|
+
// verify
|
220
|
+
expect(referredToPreventionServices.fieldDependents).toBeFalsy();
|
221
|
+
expect(htsProviderRemarks.fieldDependents).toBeFalsy();
|
222
|
+
// replay
|
223
|
+
expect(
|
224
|
+
evaluateExpression(
|
225
|
+
"!includes('referredToPreventionServices', '88cdde2b-753b-48ac-a51a-ae5e1ab24846') && isEmpty('htsProviderRemarks')",
|
226
|
+
{ value: allFields[4], type: 'field' },
|
227
|
+
allFields,
|
228
|
+
valuesMap,
|
229
|
+
context,
|
230
|
+
),
|
231
|
+
).toBeTruthy();
|
232
|
+
expect(Array.from(referredToPreventionServices.fieldDependents)).toStrictEqual(['bodyTemperature']);
|
233
|
+
expect(Array.from(htsProviderRemarks.fieldDependents)).toStrictEqual(['bodyTemperature']);
|
234
|
+
});
|
235
|
+
|
236
|
+
it('should support registered custom helper functions', () => {
|
237
|
+
// setup
|
238
|
+
function customHelper(a, b) {
|
239
|
+
return a + b;
|
240
|
+
}
|
241
|
+
registerExpressionHelper('customAdd', customHelper);
|
242
|
+
|
243
|
+
// verify
|
244
|
+
const result = evaluateExpression(
|
245
|
+
'customAdd(2, 3)',
|
246
|
+
{ value: allFields[1], type: 'field' },
|
247
|
+
allFields,
|
248
|
+
{},
|
249
|
+
context,
|
250
|
+
);
|
251
|
+
expect(result).toEqual(5);
|
252
|
+
});
|
253
|
+
});
|
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
|
+
});
|
277
|
+
|
278
|
+
describe('Common expression runner - validate helper functions', () => {
|
279
|
+
const allFields = JSON.parse(JSON.stringify(testFields));
|
280
|
+
const allFieldsKeys = allFields.map((f) => f.id);
|
281
|
+
let valuesMap = {
|
282
|
+
linkedToCare: '',
|
283
|
+
patientIdentificationNumber: '',
|
284
|
+
htsProviderRemarks: '',
|
285
|
+
referredToPreventionServices: [],
|
286
|
+
bodyTemperature: 0,
|
287
|
+
};
|
288
|
+
|
289
|
+
const users = [
|
290
|
+
{ id: 1, name: 'Alice', age: 25 },
|
291
|
+
{ id: 2, name: 'Bob', age: 30 },
|
292
|
+
{ id: 3, name: 'Charlie', age: 35 },
|
293
|
+
];
|
294
|
+
|
295
|
+
afterEach(() => {
|
296
|
+
// teardown
|
297
|
+
valuesMap = {
|
298
|
+
linkedToCare: '',
|
299
|
+
patientIdentificationNumber: '',
|
300
|
+
htsProviderRemarks: '',
|
301
|
+
referredToPreventionServices: [],
|
302
|
+
bodyTemperature: 0,
|
303
|
+
};
|
304
|
+
allFields.forEach((field) => {
|
305
|
+
field.fieldDependents = undefined;
|
306
|
+
});
|
307
|
+
});
|
308
|
+
const helper = new CommonExpressionHelpers(
|
309
|
+
{ value: allFields[1], type: 'field' },
|
310
|
+
{},
|
311
|
+
allFields,
|
312
|
+
valuesMap,
|
313
|
+
allFieldsKeys,
|
314
|
+
);
|
315
|
+
|
316
|
+
it('should return true if value is empty, null or undefined', () => {
|
317
|
+
let val = '';
|
318
|
+
|
319
|
+
expect(helper.isEmpty(val)).toBe(true);
|
320
|
+
|
321
|
+
val = 'test';
|
322
|
+
expect(helper.isEmpty(val)).toBe(false);
|
323
|
+
|
324
|
+
val = null;
|
325
|
+
expect(helper.isEmpty(val)).toBe(true);
|
326
|
+
|
327
|
+
val = undefined;
|
328
|
+
expect(helper.isEmpty(val)).toBe(true);
|
329
|
+
});
|
330
|
+
|
331
|
+
it('should return true if array contains items', () => {
|
332
|
+
const arr = [1, 2, 3, 4];
|
333
|
+
|
334
|
+
let members = [1, 4];
|
335
|
+
|
336
|
+
let result = helper.arrayContains(arr, members);
|
337
|
+
expect(result).toBe(true);
|
338
|
+
|
339
|
+
members = [4, 7, 8, 9, 0, 6];
|
340
|
+
result = helper.arrayContains(arr, members);
|
341
|
+
expect(result).toBe(false);
|
342
|
+
});
|
343
|
+
|
344
|
+
it('should return true if array contains atleast one item', () => {
|
345
|
+
const arr = [1, 2, 3, 4];
|
346
|
+
|
347
|
+
let members = [1, 4, 7, 8, 9, 0, 6];
|
348
|
+
|
349
|
+
let result = helper.arrayContainsAny(arr, members);
|
350
|
+
expect(result).toBe(true);
|
351
|
+
|
352
|
+
members = [7, 8, 9, 0, 6];
|
353
|
+
result = helper.arrayContainsAny(arr, members);
|
354
|
+
expect(result).toBe(false);
|
355
|
+
});
|
356
|
+
|
357
|
+
it('should evaluate values against regular expressions(Regex)', () => {
|
358
|
+
const regex = '[A-Za-z0-9]+-123456';
|
359
|
+
|
360
|
+
let result = helper.doesNotMatchExpression(regex, 'RandomID');
|
361
|
+
expect(result).toBe(true);
|
362
|
+
|
363
|
+
result = helper.doesNotMatchExpression(regex, 'REC12345-123456');
|
364
|
+
expect(result).toBe(false);
|
365
|
+
});
|
366
|
+
|
367
|
+
it('returns an array of values for a given key', () => {
|
368
|
+
const ages = helper.extractRepeatingGroupValues('age', users);
|
369
|
+
expect(ages).toEqual([25, 30, 35]);
|
370
|
+
});
|
371
|
+
|
372
|
+
it('returns an empty array if the input array is empty', () => {
|
373
|
+
const emptyArray = [];
|
374
|
+
const values = helper.extractRepeatingGroupValues('someKey', emptyArray);
|
375
|
+
expect(values).toEqual([]);
|
376
|
+
});
|
377
|
+
|
378
|
+
it('returns a Date object', () => {
|
379
|
+
const result = helper.formatDate('2023-04-13', 'yyyy-MM-dd', '+0300');
|
380
|
+
expect(result instanceof Date).toBe(true);
|
381
|
+
});
|
382
|
+
|
383
|
+
it('uses default format and offset values when passed as null arguments', () => {
|
384
|
+
const result = helper.formatDate('2023-04-13T01:23:45.678Z', null, null);
|
385
|
+
expect(result.toISOString()).toEqual('2023-04-13T01:23:45.678Z');
|
386
|
+
});
|
387
|
+
});
|
@@ -0,0 +1,219 @@
|
|
1
|
+
import { getRegisteredExpressionHelpers } from '../registry/registry';
|
2
|
+
import { isEmpty } from 'lodash-es';
|
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';
|
6
|
+
import { HistoricalDataSourceService } from '../datasources/historical-data-source';
|
7
|
+
import { type Visit } from '@openmrs/esm-framework';
|
8
|
+
|
9
|
+
export interface FormNode {
|
10
|
+
value: FormPage | FormSection | FormField;
|
11
|
+
type: 'field' | 'page' | 'section';
|
12
|
+
}
|
13
|
+
|
14
|
+
export interface ExpressionContext {
|
15
|
+
mode: 'enter' | 'edit' | 'view' | 'embedded-view';
|
16
|
+
myValue?: any;
|
17
|
+
patient: any;
|
18
|
+
previousEncounter?: OpenmrsEncounter;
|
19
|
+
visit?: Visit;
|
20
|
+
}
|
21
|
+
|
22
|
+
export const HD = new HistoricalDataSourceService();
|
23
|
+
|
24
|
+
export function evaluateExpression(
|
25
|
+
expression: string,
|
26
|
+
node: FormNode,
|
27
|
+
fields: Array<FormField>,
|
28
|
+
fieldValues: Record<string, any>,
|
29
|
+
context: ExpressionContext,
|
30
|
+
): any {
|
31
|
+
if (!expression?.trim()) {
|
32
|
+
return null;
|
33
|
+
}
|
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);
|
79
|
+
|
80
|
+
try {
|
81
|
+
return evaluate(expression, expressionContext);
|
82
|
+
} catch (error) {
|
83
|
+
console.error(`Error: ${error} \n\n failing expression: ${expression}`);
|
84
|
+
}
|
85
|
+
return null;
|
86
|
+
}
|
87
|
+
|
88
|
+
export async function evaluateAsyncExpression(
|
89
|
+
expression: string,
|
90
|
+
node: FormNode,
|
91
|
+
fields: Array<FormField>,
|
92
|
+
fieldValues: Record<string, any>,
|
93
|
+
context: ExpressionContext,
|
94
|
+
): Promise<any> {
|
95
|
+
if (!expression?.trim()) {
|
96
|
+
return null;
|
97
|
+
}
|
98
|
+
|
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
|
109
|
+
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) {
|
112
|
+
myValue = fieldValues[node.value['id']];
|
113
|
+
}
|
114
|
+
|
115
|
+
const HD = new HistoricalDataSourceService();
|
116
|
+
|
117
|
+
HD.putObject('prevEnc', {
|
118
|
+
value: context.previousEncounter || { obs: [] },
|
119
|
+
getValue(concept) {
|
120
|
+
return this.value.obs.find((obs) => obs.concept.uuid == concept);
|
121
|
+
},
|
122
|
+
});
|
123
|
+
|
124
|
+
const _ = {
|
125
|
+
isEmpty,
|
126
|
+
};
|
127
|
+
|
128
|
+
const expressionContext = {
|
129
|
+
...new CommonExpressionHelpers(node, patient, fields, fieldValues, allFieldsKeys),
|
130
|
+
...getRegisteredExpressionHelpers(),
|
131
|
+
...context,
|
132
|
+
fieldValues,
|
133
|
+
patient,
|
134
|
+
myValue,
|
135
|
+
sex,
|
136
|
+
age,
|
137
|
+
temporaryObjectsMap: {},
|
138
|
+
HD,
|
139
|
+
visitType,
|
140
|
+
visitTypeUuid,
|
141
|
+
_,
|
142
|
+
};
|
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
|
+
}
|
183
|
+
|
184
|
+
/**
|
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.
|
189
|
+
*/
|
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];
|
196
|
+
}
|
197
|
+
|
198
|
+
/**
|
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.
|
213
|
+
*/
|
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
|
+
);
|
219
|
+
}
|