diff --git a/packages/form-js-viewer/src/core/Validator.js b/packages/form-js-viewer/src/core/Validator.js index ab197f474..b614998a6 100644 --- a/packages/form-js-viewer/src/core/Validator.js +++ b/packages/form-js-viewer/src/core/Validator.js @@ -1,11 +1,14 @@ import { isNil, get, set } from 'min-dash'; import { countDecimals } from '../render/components/util/numberFieldUtil'; import Big from 'big.js'; +import { clone } from '../util'; +import { wrapObjectKeysWithUnderscores } from '../util/simple'; const EMAIL_PATTERN = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; const PHONE_PATTERN = /(\+|00)(297|93|244|1264|358|355|376|971|54|374|1684|1268|61|43|994|257|32|229|226|880|359|973|1242|387|590|375|501|1441|591|55|1246|673|975|267|236|1|61|41|56|86|225|237|243|242|682|57|269|238|506|53|5999|61|1345|357|420|49|253|1767|45|1809|1829|1849|213|593|20|291|212|34|372|251|358|679|500|33|298|691|241|44|995|44|233|350|224|590|220|245|240|30|1473|299|502|594|1671|592|852|504|385|509|36|62|44|91|246|353|98|964|354|972|39|1876|44|962|81|76|77|254|996|855|686|1869|82|383|965|856|961|231|218|1758|423|94|266|370|352|371|853|590|212|377|373|261|960|52|692|389|223|356|95|382|976|1670|258|222|1664|596|230|265|60|262|264|687|227|672|234|505|683|31|47|977|674|64|968|92|507|64|51|63|680|675|48|1787|1939|850|351|595|970|689|974|262|40|7|250|966|249|221|65|500|4779|677|232|503|378|252|508|381|211|239|597|421|386|46|268|1721|248|963|1649|235|228|66|992|690|993|670|676|1868|216|90|688|886|255|256|380|598|1|998|3906698|379|1784|58|1284|1340|84|678|681|685|967|27|260|263)(9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)\d{4,20}$/; -const VALIDATE_FEEL_PROPERTIES = [ +const PRECOMPUTE_FEEL_PROPERTIES = [ + 'custom', 'min', 'max', 'minLength', @@ -75,7 +78,7 @@ export class Validator { return errors; } - const evaluatedValidation = evaluateFEELValues( + const evaluatedValidation = precomputeFEELValues( validate, this._expressionLanguage, this._conditionChecker, @@ -144,6 +147,20 @@ export class Validator { ]; } + if ('custom' in evaluatedValidation && value && evaluatedValidation.custom.length) { + const { custom } = evaluatedValidation; + custom.forEach(customValidation => { + const { condition, message } = customValidation; + + if (condition && !evaluateExpression(condition, this._expressionLanguage, this._conditionChecker, this._form, { value })) { + errors = [ + ...errors, + message + ]; + } + }); + } + return errors; } } @@ -153,35 +170,29 @@ Validator.$inject = [ 'expressionLanguage', 'conditionChecker', 'form' ]; // helpers ////////// -/** - * Helper function to evaluate optional FEEL validation values. - */ -function evaluateFEELValues(validate, expressionLanguage, conditionChecker, form) { - - const evaluatedValidate = { ...validate }; - - VALIDATE_FEEL_PROPERTIES.forEach(property => { - const path = property.split('.'); +function precomputeFEELValues(_validate, expressionLanguage, conditionChecker, form) { - const value = get(evaluatedValidate, path); + const validate = clone(_validate); - // mirroring FEEL evaluation of our hooks - if (!expressionLanguage || !expressionLanguage.isExpression(value)) { - return value; + PRECOMPUTE_FEEL_PROPERTIES.forEach(propertyPath => { + const value = get(validate, propertyPath.split('.')); + const evaluatedValue = evaluateExpression(value, expressionLanguage, conditionChecker, form); + if (evaluatedValue) { + set(validate, propertyPath.split('.'), evaluatedValue); } + }); - const { initialData, data } = form._getState(); - - const newData = conditionChecker ? conditionChecker.applyConditions(data, data) : data; - const filteredData = { ...initialData, ...newData }; + return validate; +} - const evaluatedValue = expressionLanguage.evaluate(value, filteredData); +function evaluateExpression(expression, expressionLanguage, conditionChecker, form, additionalContext = {}) { + if (!expressionLanguage || !expressionLanguage.isExpression(expression)) { + return expression; + } - // replace validate property with evaluated value - if (evaluatedValue) { - set(evaluatedValidate, path, evaluatedValue); - } - }); + const { initialData, data } = form._getState(); + const filteredData = conditionChecker ? conditionChecker.applyConditions(data, data) : data; + const expressionContext = { ...additionalContext, ...initialData, ...filteredData, ...wrapObjectKeysWithUnderscores(additionalContext) }; - return evaluatedValidate; + return expressionLanguage.evaluate(expression, expressionContext); } \ No newline at end of file diff --git a/packages/form-js-viewer/src/util/simple.js b/packages/form-js-viewer/src/util/simple.js index 6cf8665cc..3e6f40872 100644 --- a/packages/form-js-viewer/src/util/simple.js +++ b/packages/form-js-viewer/src/util/simple.js @@ -63,7 +63,7 @@ export function buildExpressionContext(context) { return { ...specialContextKeys, ...data, - ..._wrapObjectKeysWithUnderscores(specialContextKeys) + ...wrapObjectKeysWithUnderscores(specialContextKeys) }; } @@ -77,9 +77,7 @@ export function runRecursively(formField, fn) { fn(formField); } -// helpers ////////////////////// - -function _wrapObjectKeysWithUnderscores(obj) { +export function wrapObjectKeysWithUnderscores(obj) { const newObj = {}; for (const [ key, value ] of Object.entries(obj)) { newObj[`_${key}_`] = value;