From 8d575f6bded289d72f4c24f5475ede17d3c90253 Mon Sep 17 00:00:00 2001 From: Valentin Serra Date: Thu, 11 Apr 2024 14:57:02 +0200 Subject: [PATCH] feat: implement instance validation in validator Related to #1147 --- packages/form-js-viewer/src/core/Validator.js | 51 ++++++++++++++++++- .../src/render/hooks/useCondition.js | 2 +- .../render/hooks/useExpressionEvaluation.js | 13 ++--- .../src/render/hooks/useReadonly.js | 2 +- .../src/render/hooks/useTemplateEvaluation.js | 2 +- .../form-js-viewer/src/util/expressions.js | 38 ++++++++++++++ packages/form-js-viewer/src/util/index.js | 1 + packages/form-js-viewer/src/util/simple.js | 23 +-------- 8 files changed, 98 insertions(+), 34 deletions(-) create mode 100644 packages/form-js-viewer/src/util/expressions.js diff --git a/packages/form-js-viewer/src/core/Validator.js b/packages/form-js-viewer/src/core/Validator.js index 8b7ec9262..4df86206d 100644 --- a/packages/form-js-viewer/src/core/Validator.js +++ b/packages/form-js-viewer/src/core/Validator.js @@ -1,5 +1,6 @@ import { isNil, get, set } from 'min-dash'; import { countDecimals } from '../render/components/util/numberFieldUtil'; +import { runExpressionEvaluation } from '../util/expressions'; import Big from 'big.js'; const EMAIL_PATTERN = @@ -18,6 +19,8 @@ export class Validator { } /** + * Validate against a field definition, does not support proper expression evaluation. + * * @deprecated use validateFieldInstance instead */ validateField(field, value) { @@ -33,7 +36,7 @@ export class Validator { return errors; } - const evaluatedValidation = evaluateFEELValues( + const evaluatedValidation = oldEvaluateFEELValues( validate, this._expressionLanguage, this._conditionChecker, @@ -44,6 +47,37 @@ export class Validator { return errors; } + + /** + * Validate a field instance. + * + * @param {Object} fieldInstance + * @param {string} value + * + * @returns {Array} + */ + validateFieldInstance(fieldInstance, value) { + const { id, expressionContextInfo } = fieldInstance; + + const field = this._formFieldRegistry.get(id); + const { type, validate } = field; + + let errors = []; + + if (type === 'number') { + errors = runNumberValidation(field, value, errors); + } + + if (!validate) { + return errors; + } + + const evaluatedValidation = evaluateFEELValues(validate, this._expressionLanguage, expressionContextInfo); + + errors = runPresetValidation(field, evaluatedValidation, value, errors); + + return errors; + } } Validator.$inject = ['expressionLanguage', 'conditionChecker', 'form', 'formFieldRegistry']; @@ -130,7 +164,20 @@ function runPresetValidation(field, validation, value, errors) { return errors; } -function evaluateFEELValues(validate, expressionLanguage, conditionChecker, form) { +function evaluateFEELValues(validate, expressionLanguage, expressionContextInfo) { + const evaluatedValidate = { ...validate }; + + VALIDATE_FEEL_PROPERTIES.forEach((property) => { + const path = property.split('.'); + const value = get(evaluatedValidate, path); + const evaluatedValue = runExpressionEvaluation(expressionLanguage, value, expressionContextInfo); + set(evaluatedValidate, path, evaluatedValue === null ? undefined : evaluatedValue); + }); + + return evaluatedValidate; +} + +function oldEvaluateFEELValues(validate, expressionLanguage, conditionChecker, form) { const evaluatedValidate = { ...validate }; VALIDATE_FEEL_PROPERTIES.forEach((property) => { diff --git a/packages/form-js-viewer/src/render/hooks/useCondition.js b/packages/form-js-viewer/src/render/hooks/useCondition.js index f0f680db7..b21f3cee9 100644 --- a/packages/form-js-viewer/src/render/hooks/useCondition.js +++ b/packages/form-js-viewer/src/render/hooks/useCondition.js @@ -1,7 +1,7 @@ import { useService } from './useService.js'; import { useContext, useMemo } from 'preact/hooks'; import { LocalExpressionContext } from '../context/LocalExpressionContext.js'; -import { buildExpressionContext } from '../../util/simple'; +import { buildExpressionContext } from '../../util/expressions.js'; /** * Evaluate if condition is met reactively based on the conditionChecker and form data. diff --git a/packages/form-js-viewer/src/render/hooks/useExpressionEvaluation.js b/packages/form-js-viewer/src/render/hooks/useExpressionEvaluation.js index 17b464099..9741ddea9 100644 --- a/packages/form-js-viewer/src/render/hooks/useExpressionEvaluation.js +++ b/packages/form-js-viewer/src/render/hooks/useExpressionEvaluation.js @@ -1,7 +1,7 @@ import { useService } from './useService'; import { LocalExpressionContext } from '../context/LocalExpressionContext'; import { useContext, useMemo } from 'preact/hooks'; -import { buildExpressionContext } from '../../util/simple'; +import { runExpressionEvaluation } from '../../util/expressions'; /** * Evaluate a string reactively based on the expressionLanguage and form data. @@ -14,11 +14,8 @@ import { buildExpressionContext } from '../../util/simple'; export function useExpressionEvaluation(value) { const expressionLanguage = useService('expressionLanguage'); const expressionContextInfo = useContext(LocalExpressionContext); - - return useMemo(() => { - if (expressionLanguage && expressionLanguage.isExpression(value)) { - return expressionLanguage.evaluate(value, buildExpressionContext(expressionContextInfo)); - } - return value; - }, [expressionLanguage, expressionContextInfo, value]); + return useMemo( + () => runExpressionEvaluation(expressionLanguage, value, expressionContextInfo), + [expressionLanguage, expressionContextInfo, value], + ); } diff --git a/packages/form-js-viewer/src/render/hooks/useReadonly.js b/packages/form-js-viewer/src/render/hooks/useReadonly.js index 8f8ad8bd9..d4ce7daf8 100644 --- a/packages/form-js-viewer/src/render/hooks/useReadonly.js +++ b/packages/form-js-viewer/src/render/hooks/useReadonly.js @@ -1,4 +1,4 @@ -import { buildExpressionContext } from '../../util/simple'; +import { buildExpressionContext } from '../../util/expressions.js'; import { LocalExpressionContext } from '../context/LocalExpressionContext.js'; import { useService } from './useService.js'; import { useContext } from 'preact/hooks'; diff --git a/packages/form-js-viewer/src/render/hooks/useTemplateEvaluation.js b/packages/form-js-viewer/src/render/hooks/useTemplateEvaluation.js index 7f6bd756c..7d3d71ce6 100644 --- a/packages/form-js-viewer/src/render/hooks/useTemplateEvaluation.js +++ b/packages/form-js-viewer/src/render/hooks/useTemplateEvaluation.js @@ -1,7 +1,7 @@ import { useService } from './useService'; import { useContext, useMemo } from 'preact/hooks'; import { LocalExpressionContext } from '../context/LocalExpressionContext'; -import { buildExpressionContext } from '../../util/simple'; +import { buildExpressionContext } from '../../util/expressions'; /** * Template a string reactively based on form data. If the string is not a template, it is returned as is. diff --git a/packages/form-js-viewer/src/util/expressions.js b/packages/form-js-viewer/src/util/expressions.js new file mode 100644 index 000000000..b15916760 --- /dev/null +++ b/packages/form-js-viewer/src/util/expressions.js @@ -0,0 +1,38 @@ +import { wrapObjectKeysWithUnderscores } from './simple'; + + +/** + * Transform a LocalExpressionContext object into a usable FEEL context. + * + * @param {Object} context - The LocalExpressionContext object. + * @returns {Object} The usable FEEL context. + */ + +export function buildExpressionContext(context) { + const { + data, + ...specialContextKeys + } = context; + + return { + ...specialContextKeys, + ...data, + ...wrapObjectKeysWithUnderscores(specialContextKeys) + }; +} + +/** + * Evaluate a string based on the expressionLanguage and context information. + * If the string is not an expression, it is returned as is. + * + * @param {any} expressionLanguage - The expression language to use. + * @param {string} value - The string to evaluate. + * @param {Object} expressionContextInfo - The context information to use. + * @returns {any} - Evaluated value or the original value if not an expression. + */ +export function runExpressionEvaluation(expressionLanguage, value, expressionContextInfo) { + if (expressionLanguage && expressionLanguage.isExpression(value)) { + return expressionLanguage.evaluate(value, buildExpressionContext(expressionContextInfo)); + } + return value; +} \ No newline at end of file diff --git a/packages/form-js-viewer/src/util/index.js b/packages/form-js-viewer/src/util/index.js index c59d5675d..95b99961e 100644 --- a/packages/form-js-viewer/src/util/index.js +++ b/packages/form-js-viewer/src/util/index.js @@ -4,3 +4,4 @@ export * from './form'; export * from './getSchemaVariables'; export * from './simple'; export * from './structure'; +export * from './expressions'; diff --git a/packages/form-js-viewer/src/util/simple.js b/packages/form-js-viewer/src/util/simple.js index 6fbfa653d..10ffa9309 100644 --- a/packages/form-js-viewer/src/util/simple.js +++ b/packages/form-js-viewer/src/util/simple.js @@ -42,36 +42,17 @@ export function clone(data, replacer) { return JSON.parse(JSON.stringify(data, replacer)); } -/** - * Transform a LocalExpressionContext object into a usable FEEL context. - * - * @param {Object} context - The LocalExpressionContext object. - * @returns {Object} The usable FEEL context. - */ - -export function buildExpressionContext(context) { - const { data, ...specialContextKeys } = context; - - return { - ...specialContextKeys, - ...data, - ..._wrapObjectKeysWithUnderscores(specialContextKeys), - }; -} - export function runRecursively(formField, fn) { const components = formField.components || []; - components.forEach((component, index) => { + components.forEach((component, _) => { runRecursively(component, fn); }); fn(formField); } -// helpers ////////////////////// - -function _wrapObjectKeysWithUnderscores(obj) { +export function wrapObjectKeysWithUnderscores(obj) { const newObj = {}; for (const [key, value] of Object.entries(obj)) { newObj[`_${key}_`] = value;