diff --git a/package-lock.json b/package-lock.json index 3b5cb5634..0d5f1d99b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.4.5-beta.0", "license": "MIT", "dependencies": { + "json-logic-js": "^2.0.2", "lodash": "^4.17.21", "randexp": "^0.5.3", "yup": "^0.30.0" @@ -6461,6 +6462,11 @@ "node": ">=4" } }, + "node_modules/json-logic-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.2.tgz", + "integrity": "sha512-ZBtBdMJieqQcH7IX/LaBsr5pX+Y5JIW+EhejtM3Ffg2jdN9Iwf+Ht6TbHnvAZ/YtwyuhPaCBlnvzrwVeWdvGDQ==" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -13342,6 +13348,11 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-logic-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.2.tgz", + "integrity": "sha512-ZBtBdMJieqQcH7IX/LaBsr5pX+Y5JIW+EhejtM3Ffg2jdN9Iwf+Ht6TbHnvAZ/YtwyuhPaCBlnvzrwVeWdvGDQ==" + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", diff --git a/package.json b/package.json index 042444a7b..f95bff62c 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ ] }, "dependencies": { + "json-logic-js": "^2.0.2", "lodash": "^4.17.21", "randexp": "^0.5.3", "yup": "^0.30.0" diff --git a/src/createHeadlessForm.js b/src/createHeadlessForm.js index 3bd7e7332..4b089edc1 100644 --- a/src/createHeadlessForm.js +++ b/src/createHeadlessForm.js @@ -24,6 +24,7 @@ import { getInputType, } from './internals/fields'; import { pickXKey } from './internals/helpers'; +import { createValidationChecker } from './jsonLogic'; import { buildYupSchema } from './yupSchema'; // Some type definitions (to be migrated into .d.ts file or TS Interfaces) @@ -220,13 +221,15 @@ function getComposeFunctionForField(fieldParams, hasCustomizations) { * Create field object using a compose function * @param {FieldParameters} fieldParams - field parameters * @param {JsfConfig} config - parser config + * @param {Object} scopedJsonSchema - the matching JSON schema + * @param {Object} logic - logic used for validation json-logic * @returns {Object} field object */ -function buildField(fieldParams, config, scopedJsonSchema) { +function buildField(fieldParams, config, scopedJsonSchema, logic) { const customProperties = getCustomPropertiesForField(fieldParams, config); const composeFn = getComposeFunctionForField(fieldParams, !!customProperties); - const yupSchema = buildYupSchema(fieldParams, config); + const yupSchema = buildYupSchema(fieldParams, config, logic); const calculateConditionalFieldsClosure = fieldParams.isDynamic && calculateConditionalProperties(fieldParams, customProperties); @@ -267,7 +270,7 @@ function buildField(fieldParams, config, scopedJsonSchema) { * @param {JsfConfig} config - JSON-schema-form config * @returns {ParserFields} ParserFields */ -function getFieldsFromJSONSchema(scopedJsonSchema, config) { +function getFieldsFromJSONSchema(scopedJsonSchema, config, logic) { if (!scopedJsonSchema) { // NOTE: other type of verifications might be needed. return []; @@ -303,7 +306,7 @@ function getFieldsFromJSONSchema(scopedJsonSchema, config) { fields.push(groupField); }); } else { - fields.push(buildField(fieldParams, config, scopedJsonSchema)); + fields.push(buildField(fieldParams, config, scopedJsonSchema, logic)); } }); @@ -323,9 +326,10 @@ export function createHeadlessForm(jsonSchema, customConfig = {}) { }; try { - const fields = getFieldsFromJSONSchema(jsonSchema, config); + const logic = createValidationChecker(jsonSchema); + const fields = getFieldsFromJSONSchema(jsonSchema, config, logic); - const handleValidation = handleValuesChange(fields, jsonSchema, config); + const handleValidation = handleValuesChange(fields, jsonSchema, config, logic); updateFieldsProperties(fields, getPrefillValues(fields, config.initialValues), jsonSchema); diff --git a/src/helpers.js b/src/helpers.js index 5d7aca4d1..50495d015 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -40,8 +40,8 @@ export function getField(fieldName, fields) { * @param {any} value * @returns */ -export function validateFieldSchema(field, value) { - const validator = buildYupSchema(field); +export function validateFieldSchema(field, value, logic) { + const validator = buildYupSchema(field, {}, logic); return validator().isValidSync(value); } @@ -256,7 +256,14 @@ function updateField(field, requiredFields, node, formValues) { * @param {Set} accRequired - set of required field names gathered by traversing the tree * @returns {Object} */ -function processNode(node, formValues, formFields, accRequired = new Set()) { +export function processNode({ + node, + formValues, + formFields, + accRequired = new Set(), + parentID = 'root', + logic, +}) { // Set initial required fields const requiredFields = new Set(accRequired); @@ -277,21 +284,25 @@ function processNode(node, formValues, formFields, accRequired = new Set()) { // BUG HERE (unreleated) - what if it matches but doesn't has a then, // it should do nothing, but instead it jumps to node.else when it shouldn't. if (matchesCondition && node.then) { - const { required: branchRequired } = processNode( - node.then, + const { required: branchRequired } = processNode({ + node: node.then, formValues, formFields, - requiredFields - ); + accRequired: requiredFields, + parentID, + logic, + }); branchRequired.forEach((field) => requiredFields.add(field)); } else if (node.else) { - const { required: branchRequired } = processNode( - node.else, + const { required: branchRequired } = processNode({ + node: node.else, formValues, formFields, - requiredFields - ); + accRequired: requiredFields, + parentID, + logic, + }); branchRequired.forEach((field) => requiredFields.add(field)); } } @@ -312,7 +323,16 @@ function processNode(node, formValues, formFields, accRequired = new Set()) { if (node.allOf) { node.allOf - .map((allOfNode) => processNode(allOfNode, formValues, formFields, requiredFields)) + .map((allOfNode) => + processNode({ + node: allOfNode, + formValues, + formFields, + accRequired: requiredFields, + parentID, + logic, + }) + ) .forEach(({ required: allOfItemRequired }) => { allOfItemRequired.forEach(requiredFields.add, requiredFields); }); @@ -323,7 +343,13 @@ function processNode(node, formValues, formFields, accRequired = new Set()) { const inputType = getInputType(nestedNode); if (inputType === supportedTypes.FIELDSET) { // It's a fieldset, which might contain scoped conditions - processNode(nestedNode, formValues[name] || {}, getField(name, formFields).fields); + processNode({ + node: nestedNode, + formValues: formValues[name] || {}, + formFields: getField(name, formFields).fields, + parentID: name, + logic, + }); } }); } @@ -358,11 +384,11 @@ function clearValuesIfNotVisible(fields, formValues) { * @param {Object} formValues - current values of the form * @param {Object} jsonSchema - JSON schema object */ -export function updateFieldsProperties(fields, formValues, jsonSchema) { +export function updateFieldsProperties(fields, formValues, jsonSchema, logic) { if (!jsonSchema?.properties) { return; } - processNode(jsonSchema, formValues, fields); + processNode({ node: jsonSchema, formValues, formFields: fields, logic }); clearValuesIfNotVisible(fields, formValues); } @@ -425,6 +451,7 @@ export function extractParametersFromNode(schemaNode) { const presentation = pickXKey(schemaNode, 'presentation') ?? {}; const errorMessage = pickXKey(schemaNode, 'errorMessage') ?? {}; + const requiredValidations = schemaNode['x-jsf-logic-validations']; const node = omit(schemaNode, ['x-jsf-presentation', 'presentation']); @@ -471,6 +498,7 @@ export function extractParametersFromNode(schemaNode) { // Handle [name].presentation ...presentation, + requiredValidations, description: containsHTML(description) ? wrapWithSpan(description, { class: 'jsf-description', diff --git a/src/jsonLogic.js b/src/jsonLogic.js new file mode 100644 index 000000000..86f5ebab8 --- /dev/null +++ b/src/jsonLogic.js @@ -0,0 +1,124 @@ +import jsonLogic from 'json-logic-js'; + +/** + * Parses the JSON schema to extract the json-logic rules and returns an object + * containing the validation scopes, functions to retrieve the scopes, and evaluate the + * validation rules. + * + * @param {Object} schema - JSON schema node + * @returns {Object} An object containing: + * - scopes {Map} - A Map of the validation scopes (with IDs as keys) + * - getScope {Function} - Function to retrieve a scope by name/ID + * - validate {Function} - Function to evaluate a validation rule + * - applyValidationRuleInCondition {Function} - Evaluate a validation rule used in a condition + * - applyComputedValueInField {Function} - Evaluate a computed value rule for a field + * - applyComputedValueRuleInCondition {Function} - Evaluate a computed value rule used in a condition + */ +export function createValidationChecker(schema) { + const scopes = new Map(); + + function createScopes(jsonSchema, key = 'root') { + scopes.set(key, createValidationsScope(jsonSchema)); + Object.entries(jsonSchema?.properties ?? {}) + .filter(([, property]) => property.type === 'object' || property.type === 'array') + .forEach(([key, property]) => { + if (property.type === 'array') { + createScopes(property.items, `${key}[]`); + } else { + createScopes(property, key); + } + }); + } + + createScopes(schema); + + return { + scopes, + getScope(name = 'root') { + return scopes.get(name); + }, + }; +} + +function createValidationsScope(schema) { + const validationMap = new Map(); + const computedValuesMap = new Map(); + + const logic = schema?.['x-jsf-logic'] ?? { + validations: {}, + computedValues: {}, + }; + + const validations = Object.entries(logic.validations ?? {}); + const computedValues = Object.entries(logic.computedValues ?? {}); + + validations.forEach(([id, validation]) => { + validationMap.set(id, validation); + }); + + computedValues.forEach(([id, computedValue]) => { + computedValuesMap.set(id, computedValue); + }); + + function validate(rule, values) { + return jsonLogic.apply(rule, replaceUndefinedValuesWithNulls(values)); + } + + return { + validationMap, + computedValuesMap, + validate, + applyValidationRuleInCondition(id, values) { + const validation = validationMap.get(id); + return validate(validation.rule, values); + }, + applyComputedValueInField(id, values) { + const validation = computedValuesMap.get(id); + return validate(validation.rule, values); + }, + applyComputedValueRuleInCondition(id, values) { + const validation = computedValuesMap.get(id); + return validate(validation.rule, values); + }, + }; +} + +/** + * We removed undefined values in this function as `json-logic` ignores them. + * Means we will always check against a value for validations. + * + * @param {Object} values - a set of values from a form + * @returns {Object} values object without any undefined + */ +function replaceUndefinedValuesWithNulls(values = {}) { + return Object.entries(values).reduce((prev, [key, value]) => { + return { ...prev, [key]: value === undefined ? null : value }; + }, {}); +} + +/** + * Creates a Yup validation test function with custom JSON Logic for a specific field. + * + * @param {Object} options - The options for creating the validation function. + * @param {Object} options.field - The field configuration object. + * @param {string} options.field.name - The name of the field. + * @param {Object} options.logic - The logic object containing validation scopes and rules. + * @param {Object} options.config - Additional configuration options. + * @param {string} options.id - The ID of the validation rule. + * @param {string} [options.config.parentID='root'] - The ID of the validation rule scope. + * @returns {Function} A Yup validation test function. + */ +export function yupSchemaWithCustomJSONLogic({ field, logic, config, id }) { + const { parentID = 'root' } = config; + const validation = logic.getScope(parentID).validationMap.get(id); + + return (yupSchema) => + yupSchema.test( + `${field.name}-validation-${id}`, + validation?.errorMessage ?? 'This field is invalid.', + (value, { parent }) => { + if (value === undefined && !field.required) return true; + return jsonLogic.apply(validation.rule, parent); + } + ); +} diff --git a/src/tests/jsonLogic.fixtures.js b/src/tests/jsonLogic.fixtures.js new file mode 100644 index 000000000..e35a410a0 --- /dev/null +++ b/src/tests/jsonLogic.fixtures.js @@ -0,0 +1,137 @@ +export function createSchemaWithRulesOnFieldA(rules) { + return { + properties: { + field_a: { + type: 'number', + 'x-jsf-logic-validations': Object.keys(rules), + }, + field_b: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { validations: rules }, + }; +} + +export function createSchemaWithThreePropertiesWithRuleOnFieldA(rules) { + return { + properties: { + field_a: { + type: 'number', + 'x-jsf-logic-validations': Object.keys(rules), + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, + }, + 'x-jsf-logic': { validations: rules }, + required: ['field_a', 'field_b', 'field_c'], + }; +} + +export const schemaWithNonRequiredField = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + 'x-jsf-logic-validations': ['a_greater_than_field_b'], + }, + }, + 'x-jsf-logic': { + validations: { + a_greater_than_field_b: { + errorMessage: 'Must be greater than field_a', + rule: { + '>': [{ var: 'field_a' }, { var: 'field_b' }], + }, + }, + }, + }, + required: [], +}; + +export const schemaWithNativeAndJSONLogicChecks = { + properties: { + field_a: { + type: 'number', + minimum: 100, + 'x-jsf-logic-validations': ['a_multiple_of_ten'], + }, + }, + 'x-jsf-logic': { + validations: { + a_multiple_of_ten: { + errorMessage: 'Must be a multiple of 10', + rule: { + '===': [{ '%': [{ var: 'field_a' }, 10] }, 0], + }, + }, + }, + }, + required: ['field_a'], +}; + +export const multiRuleSchema = { + properties: { + field_a: { + type: 'number', + 'x-jsf-logic-validations': ['a_bigger_than_b', 'is_even_number'], + }, + field_b: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + validations: { + a_bigger_than_b: { + errorMessage: 'A must be bigger than B', + rule: { + '>': [{ var: 'field_a' }, { var: 'field_b' }], + }, + }, + is_even_number: { + errorMessage: 'A must be even', + rule: { + '===': [{ '%': [{ var: 'field_a' }, 2] }, 0], + }, + }, + }, + }, +}; + +export const schemaWithTwoRules = { + properties: { + field_a: { + type: 'number', + 'x-jsf-logic-validations': ['a_bigger_than_b'], + }, + field_b: { + type: 'number', + 'x-jsf-logic-validations': ['is_even_number'], + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + validations: { + a_bigger_than_b: { + errorMessage: 'A must be bigger than B', + rule: { + '>': [{ var: 'field_a' }, { var: 'field_b' }], + }, + }, + is_even_number: { + errorMessage: 'B must be even', + rule: { + '===': [{ '%': [{ var: 'field_b' }, 2] }, 0], + }, + }, + }, + }, +}; diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js new file mode 100644 index 000000000..e1cd0513f --- /dev/null +++ b/src/tests/jsonLogic.test.js @@ -0,0 +1,215 @@ +import { createHeadlessForm } from '../createHeadlessForm'; + +import { + createSchemaWithRulesOnFieldA, + createSchemaWithThreePropertiesWithRuleOnFieldA, + multiRuleSchema, + schemaWithNativeAndJSONLogicChecks, + schemaWithNonRequiredField, + schemaWithTwoRules, +} from './jsonLogic.fixtures'; + +describe('jsonLogic: cross-values validations', () => { + describe('Does not conflict with native JSON schema', () => { + it('Given an optional field and empty value, jsonLogic validations are ignored', () => { + const { handleValidation } = createHeadlessForm(schemaWithNonRequiredField, { + strictInputType: false, + }); + expect(handleValidation({}).formErrors).toBeUndefined(); + expect(handleValidation({ field_a: 0, field_b: 10 }).formErrors).toEqual({ + field_b: 'Must be greater than field_a', + }); + expect(handleValidation({ field_a: 'incorrect value' }).formErrors).toEqual({ + field_a: 'The value must be a number', + }); + expect(handleValidation({ field_a: 11 }).formErrors).toBeUndefined(); + }); + + it('Native validations have higher precedence than jsonLogic validations', () => { + const { handleValidation } = createHeadlessForm(schemaWithNativeAndJSONLogicChecks, { + strictInputType: false, + }); + expect(handleValidation({}).formErrors).toEqual({ field_a: 'Required field' }); + expect(handleValidation({ field_a: 0 }).formErrors).toEqual({ + field_a: 'Must be greater or equal to 100', + }); + expect(handleValidation({ field_a: 101 }).formErrors).toEqual({ + field_a: 'Must be a multiple of 10', + }); + expect(handleValidation({ field_a: 110 }).formErrors).toBeUndefined(); + }); + }); + + describe('Relative: <, >, =', () => { + it('bigger: field_a > field_b', () => { + const schema = createSchemaWithRulesOnFieldA({ + a_greater_than_b: { + errorMessage: 'Field A must be bigger than field B', + rule: { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + }, + }); + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { formErrors } = handleValidation({ field_a: 1, field_b: 2 }); + expect(formErrors.field_a).toEqual('Field A must be bigger than field B'); + expect(handleValidation({ field_a: 2, field_b: 0 }).formErrors).toEqual(undefined); + }); + + it('smaller: field_a < field_b', () => { + const schema = createSchemaWithRulesOnFieldA({ + a_less_than_b: { + errorMessage: 'Field A must be smaller than field B', + rule: { '<': [{ var: 'field_a' }, { var: 'field_b' }] }, + }, + }); + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { formErrors } = handleValidation({ field_a: 2, field_b: 2 }); + expect(formErrors.field_a).toEqual('Field A must be smaller than field B'); + expect(handleValidation({ field_a: 0, field_b: 2 }).formErrors).toEqual(undefined); + }); + + it('equal: field_a = field_b', () => { + const schema = createSchemaWithRulesOnFieldA({ + a_equals_b: { + errorMessage: 'Field A must equal field B', + rule: { '==': [{ var: 'field_a' }, { var: 'field_b' }] }, + }, + }); + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { formErrors } = handleValidation({ field_a: 3, field_b: 2 }); + expect(formErrors.field_a).toEqual('Field A must equal field B'); + expect(handleValidation({ field_a: 2, field_b: 2 }).formErrors).toEqual(undefined); + }); + }); + + describe('Arithmetic: +, -, *, /', () => { + it('multiple: field_a > field_b * 2', () => { + const schema = createSchemaWithRulesOnFieldA({ + a_greater_than_b_multiplied_by_2: { + errorMessage: 'Field A must be at least twice as big as field b', + rule: { '>': [{ var: 'field_a' }, { '*': [{ var: 'field_b' }, 2] }] }, + }, + }); + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + + const { formErrors } = handleValidation({ field_a: 1, field_b: 4 }); + expect(formErrors.field_a).toEqual('Field A must be at least twice as big as field b'); + expect(handleValidation({ field_a: 3, field_b: 1 }).formErrors).toEqual(undefined); + }); + + it('divide: field_a > field_b / 2', () => { + const { handleValidation } = createHeadlessForm( + createSchemaWithRulesOnFieldA({ + a_greater_than_b_divided_by_2: { + errorMessage: 'Field A must be greater than field_b / 2', + rule: { '>': [{ var: 'field_a' }, { '/': [{ var: 'field_b' }, 2] }] }, + }, + }), + { strictInputType: false } + ); + expect(handleValidation({ field_a: 2, field_b: 4 }).formErrors).toEqual({ + field_a: 'Field A must be greater than field_b / 2', + }); + expect(handleValidation({ field_a: 3, field_b: 5 }).formErrors).toEqual(undefined); + }); + + it('sum: field_a > field_b + field_c', () => { + const schema = createSchemaWithThreePropertiesWithRuleOnFieldA({ + a_is_greater_than_b_plus_c: { + errorMessage: 'Field A must be greater than field_b and field_b added together', + rule: { + '>': [{ var: 'field_a' }, { '+': [{ var: 'field_b' }, { var: 'field_c' }] }], + }, + }, + }); + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { formErrors } = handleValidation({ field_a: 0, field_b: 1, field_c: 2 }); + expect(formErrors.field_a).toEqual( + 'Field A must be greater than field_b and field_b added together' + ); + expect(handleValidation({ field_a: 4, field_b: 1, field_c: 2 }).formErrors).toEqual( + undefined + ); + }); + }); + + describe('Logical: ||, &&', () => { + it('AND: field_a > field_b && field_a > field_c (implicit with multiple rules in a single field)', () => { + const schema = createSchemaWithThreePropertiesWithRuleOnFieldA({ + a_is_greater_than_b: { + errorMessage: 'Field A must be greater than field_b', + rule: { + '>': [{ var: 'field_a' }, { var: 'field_b' }], + }, + }, + a_is_greater_than_c: { + errorMessage: 'Field A must be greater than field_c', + rule: { + '>': [{ var: 'field_a' }, { var: 'field_c' }], + }, + }, + }); + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + expect(handleValidation({ field_a: 1, field_b: 10, field_c: 0 }).formErrors.field_a).toEqual( + 'Field A must be greater than field_b' + ); + expect(handleValidation({ field_a: 1, field_b: 0, field_c: 10 }).formErrors.field_a).toEqual( + 'Field A must be greater than field_c' + ); + expect(handleValidation({ field_a: 10, field_b: 5, field_c: 5 }).formErrors).toEqual( + undefined + ); + }); + + it('OR: field_a > field_b or field_a > field_c', () => { + const schema = createSchemaWithThreePropertiesWithRuleOnFieldA({ + field_a_is_greater_than_b_or_c: { + errorMessage: 'Field A must be greater than field_b or field_c', + rule: { + or: [ + { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + { '>': [{ var: 'field_a' }, { var: 'field_c' }] }, + ], + }, + }, + }); + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + expect(handleValidation({ field_a: 0, field_b: 10, field_c: 10 }).formErrors.field_a).toEqual( + 'Field A must be greater than field_b or field_c' + ); + expect(handleValidation({ field_a: 1, field_b: 0, field_c: 10 }).formErrors).toEqual( + undefined + ); + expect(handleValidation({ field_a: 10, field_b: 5, field_c: 5 }).formErrors).toEqual( + undefined + ); + }); + }); + + describe('Multiple validations', () => { + it('two rules: A > B; A is even', () => { + const { handleValidation } = createHeadlessForm(multiRuleSchema, { strictInputType: false }); + expect(handleValidation({ field_a: 1 }).formErrors).toEqual({ + field_a: 'A must be even', + field_b: 'Required field', + }); + expect(handleValidation({ field_a: 1, field_b: 2 }).formErrors).toEqual({ + field_a: 'A must be bigger than B', + }); + expect(handleValidation({ field_a: 3, field_b: 2 }).formErrors).toEqual({ + field_a: 'A must be even', + }); + expect(handleValidation({ field_a: 4, field_b: 2 }).formErrors).toEqual(undefined); + }); + + it('2 seperate fields with rules failing', () => { + const { handleValidation } = createHeadlessForm(schemaWithTwoRules, { + strictInputType: false, + }); + expect(handleValidation({ field_a: 1, field_b: 3 }).formErrors).toEqual({ + field_a: 'A must be bigger than B', + field_b: 'B must be even', + }); + expect(handleValidation({ field_a: 4, field_b: 2 }).formErrors).toEqual(undefined); + }); + }); +}); diff --git a/src/yupSchema.js b/src/yupSchema.js index 45db9812e..c2659f36c 100644 --- a/src/yupSchema.js +++ b/src/yupSchema.js @@ -4,6 +4,7 @@ import { randexp } from 'randexp'; import { string, number, boolean, object, array } from 'yup'; import { supportedTypes } from './internals/fields'; +import { yupSchemaWithCustomJSONLogic } from './jsonLogic'; import { convertDiskSizeFromTo } from './utils'; /** @@ -196,7 +197,7 @@ const getYupSchema = ({ inputType, ...field }) => { * @param {FieldParameters} field Input fields * @returns {Function} Yup schema */ -export function buildYupSchema(field, config) { +export function buildYupSchema(field, config, logic) { const { inputType, jsonType: jsonTypeValue, errorMessage = {}, ...propertyFields } = field; const isCheckboxBoolean = typeof propertyFields.checkboxValue === 'boolean'; let baseSchema; @@ -406,6 +407,12 @@ export function buildYupSchema(field, config) { validators.push(withConst); } + if (propertyFields.requiredValidations) { + propertyFields.requiredValidations.forEach((id) => + validators.push(yupSchemaWithCustomJSONLogic({ field, id, logic, config })) + ); + } + return flow(validators); }